Commit ceb3fd15 authored by Mehmedovski's avatar Mehmedovski
Browse files

Initial commit: KNIGHT adaptivequiz for Moodle 5.0-5.2 (based on upstream...

Initial commit: KNIGHT adaptivequiz for Moodle 5.0-5.2 (based on upstream MOODLE_500, tested with 5.2, KNIGHT customizations)
parents
// 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/>.
/**
* JavaScript library for the adaptivequiz module.
*
* @package mod_adaptivequiz
* @copyright 2013 Remote-Learner {@link http://www.remote-learner.ca/}
* @copyright 2024 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
M.mod_adaptivequiz = M.mod_adaptivequiz || {};
M.mod_adaptivequiz.init_attempt_form = function(Y, url, secure) {
// Check if the page is on the password required page
if (null == document.getElementById('id_quizpassword')) {
require(['core_question/question_engine'], function(QuestionEngine) {
QuestionEngine.initForm('#responseform');
});
require(['core_form/changechecker'], function(FormChangeChecker) {
FormChangeChecker.watchFormById('responseform');
});
} else {
if ('1' == secure) {
Y.on('click', function(e) {
M.mod_adaptivequiz.secure_window.close(url, 0)
}, '#id_cancel');
}
}
};
M.mod_adaptivequiz.secure_window = {
init: function(Y) {
if (window.location.href.substring(0, 4) == 'file') {
window.location = 'about:blank';
}
Y.delegate('contextmenu', M.mod_adaptivequiz.secure_window.prevent, document, '*');
Y.delegate('mousedown', M.mod_adaptivequiz.secure_window.prevent_mouse, document, '*');
Y.delegate('mouseup', M.mod_adaptivequiz.secure_window.prevent_mouse, document, '*');
Y.delegate('dragstart', M.mod_adaptivequiz.secure_window.prevent, document, '*');
Y.delegate('selectstart', M.mod_adaptivequiz.secure_window.prevent, document, '*');
Y.delegate('cut', M.mod_adaptivequiz.secure_window.prevent, document, '*');
Y.delegate('copy', M.mod_adaptivequiz.secure_window.prevent, document, '*');
Y.delegate('paste', M.mod_adaptivequiz.secure_window.prevent, document, '*');
M.mod_adaptivequiz.secure_window.clear_status;
Y.on('beforeprint', function() {
Y.one(document.body).setStyle('display', 'none');
}, window);
Y.on('afterprint', function() {
Y.one(document.body).setStyle('display', 'block');
}, window);
Y.on('key', M.mod_adaptivequiz.secure_window.prevent, '*', 'press:67,86,88+ctrl');
Y.on('key', M.mod_adaptivequiz.secure_window.prevent, '*', 'up:67,86,88+ctrl');
Y.on('key', M.mod_adaptivequiz.secure_window.prevent, '*', 'down:67,86,88+ctrl');
Y.on('key', M.mod_adaptivequiz.secure_window.prevent, '*', 'press:67,86,88+meta');
Y.on('key', M.mod_adaptivequiz.secure_window.prevent, '*', 'up:67,86,88+meta');
Y.on('key', M.mod_adaptivequiz.secure_window.prevent, '*', 'down:67,86,88+meta');
},
clear_status: function() {
window.status = '';
setTimeout(M.mod_adaptivequiz.secure_window.clear_status, 10);
},
prevent: function(e) {
alert(M.str.adaptivequiz.functiondisabledbysecuremode);
e.halt();
},
prevent_mouse: function(e) {
if (e.button == 1 && /^(INPUT|TEXTAREA|BUTTON|SELECT|LABEL|A)$/i.test(e.target.get('tagName'))) {
// Left click on a button or similar. No worries.
return;
}
e.halt();
},
/**
* Event handler for the adaptivequiz start attempt button.
*/
start_attempt_action: function(e, args) {
if (args.startattemptwarning == '') {
openpopup(e, args);
} else {
M.util.show_confirm_dialog(e, {
message: args.startattemptwarning,
callback: function() {
openpopup(e, args);
},
continuelabel: M.util.get_string('startattempt', 'quiz')
});
}
},
init_close_button: function(Y, url) {
Y.on('click', function(e) {
M.mod_adaptivequiz.secure_window.close(url, 0)
}, '#secureclosebutton');
},
close: function(Y, url, delay) {
setTimeout(function() {
if (window.opener) {
window.opener.document.location.reload();
window.close();
} else {
window.location.href = url;
}
}, delay * 1000);
}
};
M.mod_adaptivequiz.init_comment_popup = function(Y) {
// Add a close button to the window.
var closebutton = Y.Node.create('<input type="button" />');
closebutton.set('value', M.util.get_string('cancel', 'moodle'));
Y.one('#id_submitbutton').ancestor().append(closebutton);
Y.on('click', function() { window.close() }, closebutton);
}
M.mod_adaptivequiz.init_reviewattempt = function(Y) {
Y.one('#adpq_scoring_table').hide();
Y.one('#adpq_scoring_table_link_icon').setContent('&#9654;');
Y.use('node', function(Y) {
Y.delegate('click', function(e) {
if (e.currentTarget.get('id') === 'adpq_scoring_table_link') {
var table = Y.one('#adpq_scoring_table');
table.toggleView();
if (table.getComputedStyle('display') === 'none') {
Y.one('#adpq_scoring_table_link_icon').setContent('&#9654;');
} else {
Y.one('#adpq_scoring_table_link_icon').setContent('&#9660;');
}
}
}, document, 'a');
});
};
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" preserveAspectRatio="xMinYMid meet">
<path fill-rule="evenodd" clip-rule="evenodd" stroke="#212529" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" d="m 3.5255474,15.525547 v 1.7998 c 0,1.1201 0,1.6798 0.21799,2.1076 0.19174,0.3763 0.49748,0.6829 0.87381,0.8746 0.4274,0.2178 0.9871903,0.218556 2.10511,0.2178 l 13.7519956,-0.0093 m -16.9489056,-4.9905 0,-11.0220973 m -1e-7,11.0220973 3.8534,-3.2111 0.00318,-0.0026 c 0.69708,-0.5809 1.0463,-0.8719 1.42496,-0.9901 0.44733,-0.1397 0.92917,-0.1176 1.3620107,0.062 0.36695,0.1522 0.68875,0.474 1.33225,1.1175 l 0.0064,0.0064 c 0.6535,0.6535 0.9811,0.9812 1.354,1.133 0.4412,0.1798 0.9323,0.1953 1.3845,0.0453 0.3834,-0.1272 0.7335,-0.4331 1.4336,-1.0456 l 4.804566,-4.2770198"/>
</svg>
<?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/>.
/**
* @copyright 2013 Middlebury College {@link http://www.middlebury.edu/}
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__).'/../../../config.php');
require_once($CFG->dirroot.'/lib/grouplib.php');
require_once(dirname(__FILE__).'/../locallib.php');
use mod_adaptivequiz\local\questionanalysis\quiz_analyser;
use mod_adaptivequiz\local\questionanalysis\statistics\discrimination_statistic;
use mod_adaptivequiz\local\questionanalysis\statistics\percent_correct_statistic;
use mod_adaptivequiz\local\questionanalysis\statistics\times_used_statistic;
$id = required_param('cmid', PARAM_INT);
$sortdir = optional_param('sortdir', 'DESC', PARAM_ALPHA);
$sort = optional_param('sort', 'times_used', PARAM_ALPHANUMEXT);
$page = optional_param('page', 0, PARAM_INT);
if (!$cm = get_coursemodule_from_id('adaptivequiz', $id)) {
throw new moodle_exception('invalidcoursemodule');
}
if (!$course = $DB->get_record('course', array('id' => $cm->course))) {
throw new moodle_exception("coursemisconf");
}
require_login($course, true, $cm);
$context = context_module::instance($cm->id);
require_capability('mod/adaptivequiz:viewreport', $context);
$adaptivequiz = $DB->get_record('adaptivequiz', array('id' => $cm->instance), '*');
// KNIGHT (Feature 3): visiting the question analysis acknowledges a pending reminder by recording the
// completed-attempts count at this review; the reminder fires again only after a further trigger interval.
$completedattempts = adaptivequiz_count_completed_attempts($adaptivequiz->id);
if (adaptivequiz_question_check_reminder_due($adaptivequiz, $completedattempts)) {
$adaptivequiz->questionschecked = $completedattempts;
$DB->update_record('adaptivequiz', $adaptivequiz);
}
$PAGE->set_url('/mod/adaptivequiz/questionanalysis/overview.php', array('cmid' => $cm->id));
$title = get_string('reportquestionanalysispageheading', 'adaptivequiz', format_string($adaptivequiz->name));
$PAGE->set_title($title);
$PAGE->set_heading(format_string($course->fullname));
$PAGE->set_context($context);
$output = $PAGE->get_renderer('mod_adaptivequiz', 'questionanalysis');
$quizanalyzer = new quiz_analyser();
$quizanalyzer->load_attempts($cm->instance);
$quizanalyzer->add_statistic('times_used', new times_used_statistic());
$quizanalyzer->add_statistic('percent_correct', new percent_correct_statistic());
$quizanalyzer->add_statistic('discrimination', new discrimination_statistic());
$headers = $quizanalyzer->get_header();
$records = $quizanalyzer->get_records($sort, $sortdir);
$recordscount = count($records);
$records = array_slice($records, $page * ADAPTIVEQUIZ_REC_PER_PAGE, ADAPTIVEQUIZ_REC_PER_PAGE);
// Merge the question id and names into links.
unset($headers['id']);
foreach ($records as &$record) {
$id = array_shift($record);
$url = new moodle_url('/mod/adaptivequiz/questionanalysis/single.php',
array('cmid' => $cm->id, 'qid' => $id, 'sort' => $sort, 'sortdir' => $sortdir, 'page' => $page));
$record[0] = html_writer::link($url, $record[0]);
}
/* print header information */
$header = $output->print_header();
$title = $output->heading($title);
/* Output attempts table */
$reporttable = $output->get_report_table($headers, $records, $cm, '/mod/adaptivequiz/questionanalysis/overview.php', $sort,
$sortdir);
/* Output paging bar */
$pagingbar = $output->print_paging_bar($recordscount, $page, ADAPTIVEQUIZ_REC_PER_PAGE, $cm,
'/mod/adaptivequiz/questionanalysis/overview.php', $sort, $sortdir);
/* Output footer information */
$footer = $output->print_footer();
echo $header;
echo $pagingbar;
echo $title;
echo $reporttable;
echo $pagingbar;
echo $footer;
<?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/>.
/**
* @copyright 2013 Middlebury College {@link http://www.middlebury.edu/}
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__).'/../../../config.php');
require_once($CFG->dirroot.'/lib/grouplib.php');
require_once(dirname(__FILE__).'/../locallib.php');
use mod_adaptivequiz\local\questionanalysis\quiz_analyser;
use mod_adaptivequiz\local\questionanalysis\statistics\answers_statistic;
use mod_adaptivequiz\local\questionanalysis\statistics\discrimination_statistic;
use mod_adaptivequiz\local\questionanalysis\statistics\percent_correct_statistic;
use mod_adaptivequiz\local\questionanalysis\statistics\times_used_statistic;
$id = required_param('cmid', PARAM_INT);
$qid = required_param('qid', PARAM_INT);
$sortdir = optional_param('sortdir', 'DESC', PARAM_ALPHA);
$sort = optional_param('sort', 'times_used', PARAM_ALPHANUMEXT);
$page = optional_param('page', 0, PARAM_INT);
if (!$cm = get_coursemodule_from_id('adaptivequiz', $id)) {
throw new moodle_exception('invalidcoursemodule');
}
if (!$course = $DB->get_record('course', array('id' => $cm->course))) {
throw new moodle_exception("coursemisconf");
}
require_login($course, true, $cm);
$context = context_module::instance($cm->id);
require_capability('mod/adaptivequiz:viewreport', $context);
$adaptivequiz = $DB->get_record('adaptivequiz', array('id' => $cm->instance), '*');
$quizanalyzer = new quiz_analyser();
$quizanalyzer->load_attempts($cm->instance);
$questionanalyzer = $quizanalyzer->get_question_analyzer($qid);
$definition = $questionanalyzer->get_question_definition();
$PAGE->set_url('/mod/adaptivequiz/questionanalysis/single.php', array('cmid' => $cm->id, 'qid' => $qid));
$PAGE->set_title(format_string($definition->name));
$PAGE->set_heading(format_string($course->fullname));
$PAGE->set_context($context);
$output = $PAGE->get_renderer('mod_adaptivequiz', 'questionanalysis');
$quizanalyzer->add_statistic('times_used', new times_used_statistic());
$quizanalyzer->add_statistic('percent_correct', new percent_correct_statistic());
$quizanalyzer->add_statistic('discrimination', new discrimination_statistic());
$quizanalyzer->add_statistic('answers', new answers_statistic());
$headers = $quizanalyzer->get_header();
$record = $quizanalyzer->get_record($qid);
// Get rid of the question id and name columns.
unset($headers['id'], $headers['name']);
array_shift($record);
array_shift($record);
/* print header information */
$header = $output->print_header();
$title = $output->heading(get_string('question_report', 'adaptivequiz'));
/* return link */
$url = new moodle_url('/mod/adaptivequiz/questionanalysis/overview.php',
array('cmid' => $cm->id, 'sort' => $sort, 'sortdir' => $sortdir, 'page' => $page));
$returnlink = html_writer::link($url, get_string('back_to_all_questions', 'adaptivequiz'));
/* Output attempts table */
$details = $output->get_question_details($questionanalyzer, $context);
$reporttable = $output->get_single_question_report($headers, $record, $cm, '/mod/adaptivequiz/questionanalysis/overview.php',
$sort, $sortdir);
/* Output footer information */
$footer = $output->print_footer();
echo $header;
echo $returnlink;
echo $title;
echo $details;
echo $reporttable;
echo $footer;
<?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/>.
/**
* Contains definition of the renderer class for the plugin.
*
* @package mod_adaptivequiz
* @copyright 2013 Remote-Learner {@link http://www.remote-learner.ca/}
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
use core\output\notification;
use core\output\single_button;
use mod_adaptivequiz\form\requiredpassword;
use mod_adaptivequiz\local\attempt\attempt_state;
use mod_adaptivequiz\local\catalgo;
use mod_adaptivequiz\output\ability_measure;
use mod_adaptivequiz\output\attempt_debug_info;
use mod_adaptivequiz\output\attempt_finished_page;
use mod_adaptivequiz\output\attempt_progress;
use mod_adaptivequiz\output\item_administration_params;
use mod_adaptivequiz\output\item_bank_notification;
use mod_adaptivequiz\output\item_bank_page;
use mod_adaptivequiz\output\item_bank_qbanks;
use mod_adaptivequiz\output\item_bank_qcategories;
use mod_adaptivequiz\output\report\attempt_administration_report;
use mod_adaptivequiz\output\report\attempt_answers_distribution_report;
use mod_adaptivequiz\output\report\individual_user_attempts\individual_user_attempt_action;
use mod_adaptivequiz\output\report\individual_user_attempts\individual_user_attempt_actions;
use mod_adaptivequiz\output\start_attempt;
use mod_adaptivequiz\output\user_attempt_summary;
/**
* The renderer class for the plugin.
*
* @package mod_adaptivequiz
*/
class mod_adaptivequiz_renderer extends plugin_renderer_base {
/** @var string $sortdir the sorting direction being used */
protected $sortdir = '';
/** @var moodle_url $sorturl the current base url used for keeping the table sorted */
protected $sorturl = '';
/** @var int $groupid variable used to reference the groupid that is currently being used to filter by */
public $groupid = 0;
/** @var array options that should be used for opening the secure popup. */
protected static $popupoptions = array(
'left' => 0,
'top' => 0,
'fullscreen' => true,
'scrollbars' => false,
'resizeable' => false,
'directories' => false,
'toolbar' => false,
'titlebar' => false,
'location' => false,
'status' => false,
'menubar' => false
);
/**
* Returns content for the attempting widget.
*
* Renders either a button to start/continue as attempt or a notification with reasoning why attempting is not
* available for the current adaptive quiz instance.
*
* @param moodle_url|null $attempturl URL of the start attempt script, null when attempting is not available.
* @param string $notificationtext Attempt unavailability notification string, empty when no restrictions.
* @param bool $browsersecurityenabled
*/
public function start_attempt(
?moodle_url $attempturl = null,
string $notificationtext = '',
bool $browsersecurityenabled = false
): string {
if ($browsersecurityenabled && $attempturl) {
return $this->display_start_attempt_form_secured($attempturl);
}
$button = null;
if ($attempturl) {
$button = new single_button(
url: $attempturl,
label: get_string('startattemptbtn', 'adaptivequiz'),
type: single_button::BUTTON_PRIMARY
);
}
$notification = null;
if ($notificationtext) {
$notification = new notification(
message: $notificationtext,
messagetype: notification::NOTIFY_INFO,
closebutton: false
);
}
return $this->render(new start_attempt($button, $notification));
}
/**
* This function sets up the javascript required by the page
* @return array a standard jsmodule structure.
*/
public function adaptivequiz_get_js_module() {
return array(
'name' => 'mod_adaptivequiz',
'fullpath' => '/mod/adaptivequiz/module.js',
'requires' => array('base', 'dom', 'event-delegate', 'event-key', 'core_question_engine',
'moodle-core-formchangechecker'),
'strings' => array(array('cancel', 'moodle'), array('changesmadereallygoaway', 'moodle'),
array('functiondisabledbysecuremode', 'adaptivequiz'))
);
}
/**
* This function generates the HTML markup to render the submission form.
*
* @param int $cmid
* @param question_usage_by_activity $quba
* @param int $slot Slot number of the question to be displayed.
* @param int $level Difficulty level of question.
* @param int $questionnumber The order number of question in the quiz.
*/
public function question_submit_form(
$cmid,
$quba,
$slot,
$level,
int $questionnumber,
$showquestiondifficultylevel = false
): string {
$output = '';
$processurl = new moodle_url('/mod/adaptivequiz/attempt.php');
// Start the form.
$attr = array('action' => $processurl, 'method' => 'post', 'enctype' => 'multipart/form-data', 'accept-charset' => 'utf-8',
'id' => 'responseform');
$output .= html_writer::start_tag('form', $attr);
$output .= html_writer::start_tag('div');
// Print the question.
$options = new question_display_options();
$options->hide_all_feedback();
$options->flags = question_display_options::HIDDEN;
$options->marks = question_display_options::HIDDEN;
// KNIGHT (Feature 5): optionally show the current question's difficulty level at the bottom of the
// question's info box (the small column holding the question number and status). The question engine
// renders that box as <div class="info">...</div><div class="content">, so the difficulty is inserted
// just before that boundary; if the structure is ever unexpected it is appended after the question.
$questionhtml = $quba->render_question($slot, $options, $questionnumber);
if ($showquestiondifficultylevel) {
$difficulty = html_writer::div(
get_string('attemptquestion_difficulty_level', 'adaptivequiz') . ': ' . format_string($level),
'difficulty-display'
);
$infoboundary = '</div><div class="content">';
if (strpos($questionhtml, $infoboundary) !== false) {
$questionhtml = str_replace($infoboundary, $difficulty . $infoboundary, $questionhtml);
} else {
$questionhtml .= $difficulty;
}
}
$output .= $questionhtml;
$output .= html_writer::start_tag('div', ['class' => 'submitbtns adaptivequizbtn mdl-align']);
$output .= html_writer::empty_tag('input', ['type' => 'submit', 'name' => 'submitanswer',
'value' => get_string('submitanswer', 'mod_adaptivequiz'), 'class' => 'btn btn-primary']);
$output .= html_writer::end_tag('div');
// Some hidden fields to track what is going on.
$output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'cmid', 'value' => $cmid));
$output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'uniqueid', 'value' => $quba->get_id()));
$output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()));
$output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'slots', 'value' => $slot));
$output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'dl', 'value' => $level));
// Finish the form.
$output .= html_writer::end_tag('div');
$output .= html_writer::end_tag('form');
return $output;
}
/**
* KNIGHT (Feature 6): renders the immediate feedback for a just-answered (finished) question.
*
* Shows whether the graded question was answered correctly, together with any question feedback.
* The correct answer is only revealed when $showsolution is true. Rendered read-only, so it never
* offers a new input.
*
* @param question_usage_by_activity $quba The usage holding the finished question.
* @param int $slot The slot of the question to render feedback for.
* @param bool $showsolution Whether to also reveal the correct answer/solution.
* @return string
*/
public function question_feedback($quba, $slot, bool $showsolution = false, ?float $acceptancethreshold = null): string {
$options = new question_display_options();
$options->hide_all_feedback();
$options->readonly = true;
$options->marks = question_display_options::HIDDEN;
$options->correctness = question_display_options::VISIBLE;
$options->feedback = question_display_options::VISIBLE;
$options->generalfeedback = question_display_options::VISIBLE;
$options->numpartscorrect = question_display_options::VISIBLE;
if ($showsolution) {
$options->rightanswer = question_display_options::VISIBLE;
}
// Always show the correctness status first, so it is guaranteed for every question type and even
// when no feedback is authored. Then append the question's feedback block (outcome) if it has one -
// the question itself is never restated, as the student just answered it.
$state = $quba->get_question_state($slot);
$feedbackclass = $state->get_feedback_class();
if ($feedbackclass === 'partiallycorrect' && $acceptancethreshold !== null) {
// Colour a partial answer by whether it reached the activity's acceptance threshold - i.e.
// whether it counted as correct for the adaptive quiz - using the algorithm's own authority.
$counted = \mod_adaptivequiz\local\catalgo::question_is_correct($quba, $slot, $acceptancethreshold);
$alertclass = is_null($counted) ? 'alert-warning' : ($counted ? 'alert-success' : 'alert-danger');
} else {
$alertclasses = [
'correct' => 'alert-success',
'partiallycorrect' => 'alert-warning',
'incorrect' => 'alert-danger',
];
$alertclass = $alertclasses[$feedbackclass] ?? 'alert-secondary';
}
$body = html_writer::div(
$state->default_string(true),
'adaptivequiz-immediate-feedback-status alert ' . $alertclass . ' py-2 mb-2'
);
$body .= $this->extract_question_outcome($quba->render_question($slot, $options));
$output = html_writer::start_div('adaptivequiz-immediate-feedback card mb-3');
$output .= html_writer::div(get_string('immediatefeedbackheading', 'adaptivequiz'), 'card-header h5 mb-0');
$output .= html_writer::div($body, 'card-body');
$output .= html_writer::end_div();
return $output;
}
/**
* KNIGHT (Feature 6): extracts the feedback (outcome) block from a fully rendered question, discarding
* the question text and info column, so the immediate feedback shows only the feedback (and, when
* enabled, the correct answer).
*
* @param string $questionhtml The HTML produced by render_question().
* @return string The outcome block's HTML, or an empty string if the question has no feedback block.
*/
protected function extract_question_outcome(string $questionhtml): string {
$dom = new DOMDocument();
$previous = libxml_use_internal_errors(true);
$dom->loadHTML('<?xml encoding="utf-8"?>' . $questionhtml, LIBXML_NOERROR | LIBXML_NOWARNING);
libxml_use_internal_errors($previous);
$xpath = new DOMXPath($dom);
$nodes = $xpath->query("//div[contains(concat(' ', normalize-space(@class), ' '), ' outcome ')]");
if ($nodes === false || $nodes->length === 0) {
return '';
}
$outcome = '';
foreach ($nodes as $node) {
$outcome .= $dom->saveHTML($node);
}
return $outcome;
}
/**
* This function initializing the metadata that needs to be included in the page header
* before the page is rendered.
* @param question_usage_by_activity $quba a question usage by activity object
* @param int|array $slots slot number of the question to be displayed or an array of slot numbers
* @return string HTML header information for displaying the question
*/
public function init_metadata($quba, $slots) {
$meta = '';
if (is_array($slots)) {
foreach ($slots as $slot) {
$meta .= $quba->render_question_head_html($slot);
}
} else {
$meta .= $quba->render_question_head_html($slots);
}
$meta .= question_engine::initialise_js();
return $meta;
}
/**
* A wrapper method to render contents of the page displayed to the user when attempt is finished.
*
* The method accepts basic instances of data related to the activity and calls the rendering API.
*
* @param stdClass $adaptivequiz
* @param stdClass $cm
* @param stdClass $attempt
*/
public function attempt_finished_page(stdClass $adaptivequiz, stdClass $cm, stdClass $attempt): string {
return $this->render(attempt_finished_page::create($adaptivequiz, $cm, $attempt));
}
/**
* A wrapper method to render contents of the item bank management page.
*
* @param stdClass $adaptivequiz An instance of the adaptive quiz activity.
* @param cm_info $cminfo The course module.
*/
public function item_bank_page(stdClass $adaptivequiz, cm_info $cminfo): string {
return $this->render(
new item_bank_page(
new item_bank_notification($adaptivequiz),
new item_bank_qbanks($adaptivequiz, $cminfo),
new item_bank_qcategories($adaptivequiz, $cminfo),
new item_administration_params($adaptivequiz)
)
);
}
/**
* Output a page with an optional message, and JavaScript code to close the
* current window and redirect the parent window to a new URL.
* @param moodle_url $url the URL to redirect the parent window to.
* @param string $message message to display before closing the window. (optional)
* @return string HTML to output.
*/
public function close_attempt_popup($url, $message = '') {
$output = '';
$output .= $this->header();
$output .= $this->box_start();
if ($message) {
$output .= html_writer::tag('p', $message);
$output .= html_writer::tag('p', get_string('windowclosing', 'quiz'));
$delay = 5;
} else {
$output .= html_writer::tag('p', get_string('pleaseclose', 'quiz'));
$delay = 0;
}
$this->page->requires->js_init_call('M.mod_quiz.secure_window.close',
array($url, $delay), false, adaptivequiz_get_js_module());
$output .= $this->box_end();
$output .= $this->footer();
return $output;
}
/**
* This function returns page header information to be printed to the page
* @return string HTML markup for header inforation
*/
public function print_header() {
return $this->header();
}
/**
* This function returns page footer information to be printed to the page
* @return string HTML markup for footer inforation
*/
public function print_footer() {
return $this->footer();
}
/**
* This function creates the table header links that will be used to allow instructor to sort the data
* @param stdClass $cm a course module object set to the instance of the activity
* @param string $sort the column the the table is to be sorted by
* @param string $sortdir the direction of the sort
* @return array an array of column headers (firstname / lastname, number of attempts, standard error)
*/
public function format_report_table_headers($cm, $sort, $sortdir) {
$firstname = '';
$lastname = '';
$email = '';
$numofattempts = '';
$measure = '';
$standarderror = '';
$timemodified = '';
/* Determine the next sorting direction and icon to display */
switch ($sortdir) {
case 'ASC':
$imageparam = array('src' => $this->image_url('t/down'), 'alt' => '');
$columnicon = html_writer::empty_tag('img', $imageparam);
$newsortdir = 'DESC';
break;
default:
$imageparam = array('src' => $this->image_url('t/up'), 'alt' => '');
$columnicon = html_writer::empty_tag('img', $imageparam);
$newsortdir = 'ASC';
break;
}
/* Set the sort direction class variable */
$this->sortdir = $sortdir;
/* Create header links */
$param = array('cmid' => $cm->id, 'sort' => 'firstname', 'sortdir' => 'ASC', 'group' => $this->groupid);
$firstnameurl = new moodle_url('/mod/adaptivequiz/viewreport.php', $param);
$param['sort'] = 'lastname';
$lastnameurl = new moodle_url('/mod/adaptivequiz/viewreport.php', $param);
$param['sort'] = 'email';
$emailurl = new moodle_url('/mod/adaptivequiz/viewreport.php', $param);
$param['sort'] = 'attempts';
$numofattemptsurl = new moodle_url('/mod/adaptivequiz/viewreport.php', $param);
$param['sort'] = 'measure';
$measureurl = new moodle_url('/mod/adaptivequiz/viewreport.php', $param);
$param['sort'] = 'stderror';
$standarderrorurl = new moodle_url('/mod/adaptivequiz/viewreport.php', $param);
$param['sort'] = 'timemodified';
$timemodifiedurl = new moodle_url('/mod/adaptivequiz/viewreport.php', $param);
/* Update column header links with a sorting directional icon */
switch ($sort) {
case 'firstname':
$firstnameurl->params(array('sortdir' => $newsortdir));
$this->sorturl = $firstnameurl;
$firstname .= '&nbsp;'.$columnicon;
break;
case 'lastname':
$lastnameurl->params(array('sortdir' => $newsortdir));
$this->sorturl = $lastnameurl;
$lastname .= '&nbsp;'.$columnicon;
break;
case 'email':
$emailurl->params(array('sortdir' => $newsortdir));
$this->sorturl = $emailurl;
$email .= '&nbsp;'.$columnicon;
break;
case 'attempts':
$numofattemptsurl->params(array('sortdir' => $newsortdir));
$this->sorturl = $numofattemptsurl;
$numofattempts .= '&nbsp;'.$columnicon;
break;
case 'measure':
$measureurl->params(array('sortdir' => $newsortdir));
$this->sorturl = $measureurl;
$measure .= '&nbsp;'.$columnicon;
break;
case 'stderror':
$standarderrorurl->params(array('sortdir' => $newsortdir));
$this->sorturl = $standarderrorurl;
$standarderror .= '&nbsp;'.$columnicon;
break;
case 'timemodified':
$timemodifiedurl->params(array('sortdir' => $newsortdir));
$this->sorturl = $timemodifiedurl;
$timemodified .= '&nbsp;'.$columnicon;
break;
}
// Create header HTML markup.
$firstname = html_writer::link($firstnameurl, get_string('firstname')).$firstname;
$lastname = html_writer::link($lastnameurl, get_string('lastname')).$lastname;
$email = html_writer::link($emailurl, get_string('email')).$email;
$numofattempts = html_writer::link($numofattemptsurl, get_string('numofattemptshdr', 'adaptivequiz')).$numofattempts;
$measure = html_writer::link($measureurl, get_string('bestscore', 'adaptivequiz')).$measure;
$standarderror = html_writer::link($standarderrorurl, get_string('bestscorestderror', 'adaptivequiz')).$standarderror;
$timemodified = html_writer::link($timemodifiedurl, get_string('attemptfinishedtimestamp', 'adaptivequiz')).$timemodified;
return array($firstname.' / '.$lastname, $email, $numofattempts, $measure, $standarderror, $timemodified);
}
/**
* This function adds rows to the html_table object
* @param stdClass $records adaptivequiz_attempt records
* @param stdClass $cm course module object set to the instance of the activity
* @param html_table $table an instance of the html_table class
*/
protected function get_report_table_rows($records, $cm, $table) {
foreach ($records as $record) {
$attemptlink = new moodle_url('/mod/adaptivequiz/viewattemptreport.php',
array('userid' => $record->id, 'cmid' => $cm->id));
$link = html_writer::link($attemptlink, $record->attempts);
$measure = $this->format_measure($record);
if ($record->uniqueid) {
$attemptlink = new moodle_url('/mod/adaptivequiz/reviewattempt.php',
array('userid' => $record->id, 'uniqueid' => $record->uniqueid, 'cmid' => $cm->id));
$measure = html_writer::link($attemptlink, $measure);
}
$stderror = $this->format_standard_error($record);
if (intval($record->timemodified)) {
$timemodified = userdate(intval($record->timemodified));
} else {
$timemodified = get_string('na', 'adaptivequiz');
}
$profileurl = new moodle_url('/user/profile.php', array('id' => $record->id));
$name = $record->firstname.' '.$record->lastname;
$namelink = html_writer::link($profileurl, $name);
$emaillink = html_writer::link('mailto:'.$record->email, $record->email);
$row = array($namelink, $emaillink, $link, $measure, $stderror, $timemodified);
$table->data[] = $row;
$table->rowclasses[] = 'studentattempt';
}
}
/**
* This function prints paging information
* @param int $totalrecords the total number of records returned
* @param int $page the current page the user is on
* @param int $perpage the number of records displayed on one page
* @return string HTML markup
*/
public function print_paging_bar($totalrecords, $page, $perpage) {
$baseurl = $this->sorturl;
/* Set the currently set group filter and sort dir */
$baseurl->params(array('group' => $this->groupid, 'sortdir' => $this->sortdir));
$output = '';
$output .= $this->paging_bar($totalrecords, $page, $perpage, $baseurl);
return $output;
}
/**
* This function prints a grouping selector
* @param stdClass $cm course module object set to the instance of the activity
* @param stdClass $course a data record for the current course
* @param stdClass $context the context instance for the activity
* @param int $userid the current user id
* @return string HTML markup
*/
public function print_groups_selector($cm, $course, $context, $userid) {
$output = '';
$groupmode = groups_get_activity_groupmode($cm, $course);
if (0 != $groupmode) {
$baseurl = new moodle_url('/mod/adaptivequiz/viewreport.php', array('cmid' => $cm->id));
$output = groups_print_activity_menu($cm, $baseurl, true);
}
return $output;
}
/**
* Initialize secure browsing mode.
*/
public function init_browser_security($disablejsfeatures = true) {
$this->page->set_popup_notification_allowed(false); // Prevent message notifications.
$this->page->set_cacheable(false);
$this->page->set_pagelayout('popup');
if ($disablejsfeatures) {
$this->page->add_body_class('quiz-secure-window');
$this->page->requires->js_init_call('M.mod_adaptivequiz.secure_window.init',
null, false, $this->adaptivequiz_get_js_module());
}
}
/**
* This function displays a form for users to enter a password before entering the attempt
* @param int $cmid course module id
* @return requiredpassword instance of a formslib object
*/
public function display_password_form($cmid): requiredpassword {
$url = new moodle_url('/mod/adaptivequiz/attempt.php');
return new requiredpassword($url->out_omit_querystring(),
array('hidden' => array('cmid' => $cmid, 'uniqueid' => 0)));
}
/**
* This function prints a form and a button that is centered on the page, then the user clicks on the button the user is taken
* to the url
* @param moodle_url $url a url
* @param string $buttontext button caption
* @return string - HTML markup displaying the description and form with a submit button
*/
public function print_form_and_button($url, $buttontext) {
$html = '';
$attributes = array('method' => 'POST', 'action' => $url);
$html .= html_writer::start_tag('form', $attributes);
$html .= html_writer::empty_tag('br');
$html .= html_writer::empty_tag('br');
$html .= html_writer::start_tag('center');
$params = array('type' => 'submit', 'value' => $buttontext, 'class' => 'submitbtns adaptivequizbtn');
$html .= html_writer::empty_tag('input', $params);
$html .= html_writer::end_tag('center');
$html .= html_writer::end_tag('form');
return $html;
}
/**
* This function formats the ability measure into a user friendly format
* @param stdClass an object with the following properties: measure, highestlevel, lowestlevel and stderror. The values must
* come from the activty instance and the user's
* attempt record
* @return string a user friendly format of the ability measure. Ability measure is rounded to the nearest decimal.
*/
public function format_measure($record) {
if (is_null($record->measure)) {
return 'n/a';
}
return round(catalgo::map_logit_to_scale($record->measure, $record->highestlevel, $record->lowestlevel), 1);
}
/**
* This function formats the standard error into a user friendly format
* @param stdClass an object with the following properties: measure, highestlevel, lowestlevel and stderror. The values must
* come from the activty instance and the user's
* attempt record
* @return string a user friendly format of the standard error. Standard error is
* rounded to the nearest one hundredth then multiplied by 100
*/
public function format_standard_error($record) {
if (is_null($record->stderror) || $record->stderror == 0.0) {
return 'n/a';
}
$percent = round(catalgo::convert_logit_to_percent($record->stderror), 2) * 100;
return '&plusmn; '.$percent.'%';
}
/**
* This function formats the standard error and ability measure into a user friendly format
* @param stdClass an object with the following properties: measure, highestlevel, lowestlevel and stderror. The values must
* come from the activty instance and the user's
* attempt record
* @return string a user friendly format of the ability measure and standard error. Ability measure is rounded to the nearest
* decimal. Standard error is rounded to the
* nearest one hundredth then multiplied by 100
*/
protected function format_measure_and_standard_error($record) {
if (is_null($record->measure) || is_null($record->stderror) || $record->stderror == 0.0) {
return 'n/a';
}
$measure = round(catalgo::map_logit_to_scale($record->measure, $record->highestlevel, $record->lowestlevel), 1);
$percent = round(catalgo::convert_logit_to_percent($record->stderror), 2) * 100;
$format = $measure.' &plusmn; '.$percent.'%';
return $format;
}
/**
* Answer the summery information about an attempt
*
* @param stdClass $adaptivequiz See {@link mod_adaptivequiz_renderer::attempt_report_page_by_tab()}.
* @param stdClass $attempt See {@link mod_adaptivequiz_renderer::attempt_report_page_by_tab()}.
* @param stdClass $user The user who took the quiz that created the attempt.
* @return string
* @throws coding_exception
*/
public function attempt_summary_listing(stdClass $adaptivequiz, stdClass $attempt, stdClass $user): string {
$table = new html_table();
$table->attributes['class'] = 'generaltable attemptsummarytable';
$row = new html_table_row();
$headercell = new html_table_cell(get_string('attempt_user', 'adaptivequiz'));
$headercell->header = true;
$datacell = new html_table_cell(fullname($user) . ' (' . $user->email . ')');
$row->cells = [$headercell, $datacell];
$table->data[] = $row;
$row = new html_table_row();
$headercell = new html_table_cell(get_string('attempt_state', 'adaptivequiz'));
$headercell->header = true;
$datacell = new html_table_cell($attempt->attemptstate);
$row->cells = [$headercell, $datacell];
$table->data[] = $row;
$row = new html_table_row();
$headercell = new html_table_cell(get_string('score', 'adaptivequiz'));
$headercell->header = true;
$abilityfraction = 1 / ( 1 + exp( (-1 * $attempt->measure) ) );
$ability = (($adaptivequiz->highestlevel - $adaptivequiz->lowestlevel) * $abilityfraction) + $adaptivequiz->lowestlevel;
$stderror = catalgo::convert_logit_to_percent($attempt->standarderror);
$score = ($stderror > 0)
? round($ability, 2)." &nbsp; &plusmn; ".round($stderror * 100, 1)."%"
: 'n/a';
$datacell = new html_table_cell($score);
$row->cells = [$headercell, $datacell];
$table->data[] = $row;
$row = new html_table_row();
$headercell = new html_table_cell(get_string('attemptstarttime', 'adaptivequiz'));
$headercell->header = true;
$datacell = new html_table_cell(userdate($attempt->timecreated));
$row->cells = [$headercell, $datacell];
$table->data[] = $row;
$row = new html_table_row();
$headercell = new html_table_cell(get_string('attemptfinishedtimestamp', 'adaptivequiz'));
$headercell->header = true;
$datacell = new html_table_cell(userdate($attempt->timemodified));
$row->cells = [$headercell, $datacell];
$table->data[] = $row;
$row = new html_table_row();
$headercell = new html_table_cell(get_string('attempttotaltime', 'adaptivequiz'));
$headercell->header = true;
$totaltime = $attempt->timemodified - $attempt->timecreated;
$hours = floor($totaltime / 3600);
$remainder = $totaltime - ($hours * 3600);
$minutes = floor($remainder / 60);
$seconds = $remainder - ($minutes * 60);
$cellcontent = sprintf('%02d', $hours).":".sprintf('%02d', $minutes).":".sprintf('%02d', $seconds);
$datacell = new html_table_cell($cellcontent);
$row->cells = [$headercell, $datacell];
$table->data[] = $row;
$row = new html_table_row();
$headercell = new html_table_cell(get_string('attemptstopcriteria', 'adaptivequiz'));
$headercell->header = true;
$datacell = new html_table_cell($attempt->attemptstopcriteria);
$row->cells = [$headercell, $datacell];
$table->data[] = $row;
return html_writer::table($table);
}
/**
* KNIGHT (Feature 2): renders the tab bar shown when reviewing an attempt.
*
* @param moodle_url $pageurl Base URL of the attempt review page.
* @param string $selected The id of the currently selected tab.
* @param bool $showdetailed Whether to include the teacher-only detailed report tabs (summary, graph,
* answer distribution) in addition to the question details tab.
* @return string
*/
public function attempt_review_tabs(moodle_url $pageurl, string $selected, bool $showdetailed = true): string {
$tabs = [];
// KNIGHT (Feature 2): the detailed report tabs are for teachers only; a student reviewing their
// own attempt sees just the question details tab.
if ($showdetailed) {
$attemptsummarytaburl = clone($pageurl);
$attemptsummarytaburl->param('tab', 'attemptsummary');
$tabs[] = new tabobject(
'attemptsummary',
$attemptsummarytaburl,
get_string('reportattemptsummarytab', 'adaptivequiz')
);
$attemptgraphtaburl = clone($pageurl);
$attemptgraphtaburl->param('tab', 'attemptgraph');
$tabs[] = new tabobject(
'attemptgraph',
$attemptgraphtaburl,
get_string('reportattemptgraphtab', 'adaptivequiz')
);
$answerdistributiontaburl = clone($pageurl);
$answerdistributiontaburl->param('tab', 'answerdistribution');
$tabs[] = new tabobject(
'answerdistribution',
$answerdistributiontaburl,
get_string('reportattemptanswerdistributiontab', 'adaptivequiz')
);
}
$questionsdetailstaburl = clone($pageurl);
$questionsdetailstaburl->param('tab', 'questionsdetails');
$tabs[] = new tabobject('questionsdetails', $questionsdetailstaburl,
get_string('reportattemptquestionsdetailstab', 'adaptivequiz'));
return $this->tabtree($tabs, $selected);
}
/**
* Renders attempt report content for the given tab option.
*
* @param string $tabid A tab defined in {@see self::attempt_review_tabs()}.
* @param stdClass $adaptivequiz A record from {adaptivequiz}.
* @param stdClass $attempt A record from {adaptivequiz_attempt}.
* @param stdClass $user A record from {user}.
* @param question_usage_by_activity $quba
* @param moodle_url $pageurl
* @param int $page
* @return string
*/
public function attempt_report_page_by_tab(
string $tabid,
stdClass $adaptivequiz,
stdClass $attempt,
stdClass $user,
question_usage_by_activity $quba,
moodle_url $pageurl,
int $page
): string {
if ($tabid == 'attemptgraph') {
return $this->attempt_administration_report($attempt->id);
}
if ($tabid == 'answerdistribution') {
return $this->attempt_answers_distribution_report($attempt->id);
}
if ($tabid == 'questionsdetails') {
return $this->attempt_questions_review($quba, $pageurl, $page);
}
return $this->attempt_summary_listing($adaptivequiz, $attempt, $user);
}
public function reset_users_attempts_filter_action(moodle_url $url): string {
return html_writer::link($url, get_string('reportattemptsresetfilter', 'adaptivequiz'));
}
/**
* A wrapper method to call rendering of attempt progress, accepts minimum parameters to base rendering on.
*/
public function attempt_progress(string $questionsanswered, string $maximumquestions): string {
return $this->render_attempt_progress(attempt_progress::with_defaults($questionsanswered, $maximumquestions));
}
/**
* To be overridden by a theme to render start attempt controls or an unavailability notification.
*
* @param start_attempt $attemptwidget
*/
protected function render_start_attempt(start_attempt $attemptwidget): string {
return $this->render_from_template(
'mod_adaptivequiz/start_attempt',
$attemptwidget->export_for_template($this)
);
}
/**
* Renders an attempt progress object, to be overridden by a theme if required.
*/
protected function render_attempt_progress(attempt_progress $progress): string {
$progress = $progress->with_help_icon_content($this->help_icon('attemptquestionsprogress', 'adaptivequiz'));
return $this->render_from_template('mod_adaptivequiz/attempt_progress', $progress->export_for_template($this));
}
/**
* Outputs available action links for an attempt in the user's attempts report.
*
* @param stdClass $attempt A record from {adaptivequiz_attempt}.
*/
public function individual_user_attempt_actions(stdClass $attempt): string {
$actions = new individual_user_attempt_actions();
$actions->add(
new individual_user_attempt_action(
new moodle_url('/mod/adaptivequiz/reviewattempt.php', ['attempt' => $attempt->id]),
new pix_icon('i/search', ''),
get_string('reviewattempt', 'adaptivequiz')
)
);
// KNIGHT: closing and deleting are managing actions - only offer them to users who may manage
// attempts (so a view-only role such as a non-editing teacher does not see icons it cannot use).
$canmanage = has_capability('mod/adaptivequiz:manage', $this->page->context);
if ($canmanage && $attempt->attemptstate !== attempt_state::COMPLETED) {
$actions->add(
new individual_user_attempt_action(
new moodle_url('/mod/adaptivequiz/closeattempt.php', ['attempt' => $attempt->id]),
new pix_icon('t/stop', ''),
get_string('closeattempt', 'adaptivequiz')
)
);
}
if ($canmanage) {
$actions->add(
new individual_user_attempt_action(
new moodle_url('/mod/adaptivequiz/delattempt.php', ['attempt' => $attempt->id]),
new pix_icon('t/delete', ''),
get_string('deleteattemp', 'adaptivequiz')
)
);
}
return $this->render($actions);
}
/**
* Renders the renderable actions object, intended to be overridden by the theme if needed.
*
* @param individual_user_attempt_actions $actions
*/
protected function render_individual_user_attempt_actions(individual_user_attempt_actions $actions): string {
return $this->render_from_template(
'mod_adaptivequiz/report/individual_user_attempt_actions',
$actions->export_for_template($this)
);
}
/**
* A wrapper method to render answers distribution report for the given attempt.
*
* @param int $attemptid
* @return string
*/
public function attempt_answers_distribution_report(int $attemptid): string {
return $this->render(new attempt_answers_distribution_report($attemptid));
}
/**
* A wrapper method to render items administration report for the given attempt.
*
* @param int $attemptid
* @return string
*/
public function attempt_administration_report(int $attemptid): string {
return $this->render(new attempt_administration_report($attemptid));
}
/**
* Renders debugging info for an attempt.
*
* @return string
*/
protected function render_attempt_debug_info(attempt_debug_info $info): string {
$output = print_collapsible_region_start(classes: '', id: 'attemptdebuginfo',
caption: get_string('attemptdebuginfocaption', 'adaptivequiz'), default: true, return: true);
$output .= $this->render_from_template('mod_adaptivequiz/attempt_debug_info', $info->export_for_template($this));
$output .= print_collapsible_region_end(return: true);
return $output;
}
/**
* Renders answers distribution report.
*
* @param attempt_answers_distribution_report $report
* @return string
*/
protected function render_attempt_answers_distribution_report(attempt_answers_distribution_report $report): string {
return $this->render_from_template('mod_adaptivequiz/attempt_answers_distribution_report',
$report->export_for_template($this));
}
/**
* Renders items administration report.
*
* @param attempt_administration_report $report
* @return string
*/
protected function render_attempt_administration_report(attempt_administration_report $report): string {
return $this->render_from_template('mod_adaptivequiz/attempt_administration_report',
$report->export_for_template($this));
}
/**
* Returns contents of the page displayed to the user when attempt is finished.
*
* To be potentially overridden by themes.
*
* @param attempt_finished_page $page
*/
protected function render_attempt_finished_page(attempt_finished_page $page): string {
return $this->render_from_template('mod_adaptivequiz/attempt_finished_page', $page->export_for_template($this));
}
/**
* Returns contents of the item bank management page.
*
* Suitable for theme overrides.
*
* @param item_bank_page $page
*/
protected function render_item_bank_page(item_bank_page $page): string {
return $this->render_from_template('mod_adaptivequiz/item_bank_page', $page->export_for_template($this));
}
/**
* This function returns HTML markup of questions and student's responses.
* See {@link mod_adaptivequiz_renderer::attempt_report_page_by_tab} for partial parameters description.
*
* @param moodle_url $pageurl
* @param question_usage_by_activity $quba
* @param int $offset An offset used to determine which question to start processing from.
* @return string
* @throws coding_exception
* @throws moodle_exception
*/
protected function attempt_questions_review(
question_usage_by_activity $quba,
moodle_url $pageurl,
int $offset
): string {
$pager = $this->attempt_questions_review_pager($quba, $pageurl, $offset);
$questslots = $quba->get_slots();
$attr = ['class' => 'questiontags'];
$offset *= ADAPTIVEQUIZ_REV_QUEST_PER_PAGE;
$output = $pager;
// Take a portion of the array of question slots for display.
$pageqslots = array_slice($questslots, $offset, ADAPTIVEQUIZ_REV_QUEST_PER_PAGE);
// Setup display options.
$options = new question_display_options();
$options->readonly = true;
$options->flags = question_display_options::HIDDEN;
$options->marks = question_display_options::MAX_ONLY;
$options->rightanswer = question_display_options::VISIBLE;
$options->correctness = question_display_options::VISIBLE;
$options->numpartscorrect = question_display_options::VISIBLE;
// Setup quesiton header metadata.
$output .= $this->init_metadata($quba, $pageqslots);
foreach ($pageqslots as $slot) {
$label = html_writer::tag('label', get_string('questionnumber', 'adaptivequiz'));
$output .= html_writer::tag('div', $label.': '.format_string($slot));
// Retrieve question attempt object.
$questattempt = $quba->get_question_attempt($slot);
// Get question definition object.
$questdef = $questattempt->get_question();
// Retrieve the tags associated with this question.
$qtags = core_tag_tag::get_item_tags_array('core_question', 'question', $questdef->id);
$label = html_writer::tag('label', get_string('attemptquestion_level', 'adaptivequiz'));
$output .= html_writer::tag('div', $label.': '.format_string(adaptivequiz_get_difficulty_from_tags($qtags)));
$label = html_writer::tag('label', get_string('tags'));
$output .= html_writer::tag('div', $label.': '.format_string(implode(' ', $qtags)), $attr);
$output .= $quba->render_question($slot, $options);
$output .= html_writer::empty_tag('hr');
}
$output .= html_writer::empty_tag('br');
$output .= $pager;
return $output;
}
/**
* This function prints a paging link for the attempt review page.
* See {@link mod_adaptivequiz_renderer::attempt_questions_review()} for parameters description.
*
* @throws moodle_exception
*/
protected function attempt_questions_review_pager(
question_usage_by_activity $quba,
moodle_url $pageurl,
int $page
): string {
$questslots = $quba->get_slots();
$output = '';
$attr = ['class' => 'viewattemptreportpages'];
$pages = ceil(count($questslots) / ADAPTIVEQUIZ_REV_QUEST_PER_PAGE);
// Don't print anything if there is only one page.
if (1 == $pages) {
return '';
}
// Print all of the page links.
$output .= html_writer::start_tag('center');
for ($i = 0; $i < $pages; $i++) {
// If we are currently on this page, then don't make it an anchor tag.
if ($i == $page) {
$output .= '&nbsp'.html_writer::tag('span', $i + 1, $attr).'&nbsp';
continue;
}
$pageurl->params(['page' => $i]);
$output .= '&nbsp'.html_writer::link($pageurl, $i + 1, $attr).'&nbsp';
}
$output .= html_writer::end_tag('center');
return $output;
}
protected function render_ability_measure(ability_measure $measure): string {
$output = html_writer::start_div('box py-3');
$abilitymeasurecontents = get_string('abilityestimated', 'adaptivequiz') . ': ' .
html_writer::tag('strong', $this->format_measure($measure->as_object_to_format())) . ' / ' .
$measure->lowestquestiondifficulty . ' - ' . $measure->highestquestiondifficulty .
$this->help_icon('abilityestimated', 'adaptivequiz');
$output .= $this->heading($abilitymeasurecontents, 3);
$output .= html_writer::end_div();
return $output;
}
/**
* Renders the user attempt summary.
*
* @param user_attempt_summary $summary
*/
protected function render_user_attempt_summary(user_attempt_summary $summary): string {
// For properties, see the definition of export_for_template()'s return structure.
$summaryexported = $summary->export_for_template($this);
$table = new html_table();
$table->attributes['class'] = 'generaltable attemptsummarytable';
$row = new html_table_row();
$headercell = new html_table_cell(get_string('attempt_state', 'adaptivequiz'));
$headercell->header = true;
$datacell = new html_table_cell($summaryexported->attemptstate);
$datacell->id = 'attemptstatecell';
$row->cells = [$headercell, $datacell];
$table->data[] = $row;
$row = new html_table_row();
$headercell = new html_table_cell(get_string('attemptfinishedtimestamp', 'adaptivequiz'));
$headercell->header = true;
$datacell = ($summaryexported->attemptstateraw == attempt_state::COMPLETED)
? userdate($summaryexported->attempttimefinished)
: '-';
$row->cells = [$headercell, $datacell];
$table->data[] = $row;
if ($summaryexported->abilitymeasure) {
$row = new html_table_row();
$headercell = new html_table_cell(get_string('attemptquestion_ability', 'adaptivequiz') .
$this->help_icon('abilityestimated', 'adaptivequiz'));
$headercell->header = true;
$datacell = new html_table_cell(html_writer::tag('strong', $summaryexported->abilitymeasure)
. ' / ' . $summaryexported->adaptivequizlowestlevel . ' - ' . $summaryexported->adaptivequizhighestlevel);
$datacell->id = 'abilitymeasurecell';
$row->cells = [$headercell, $datacell];
$table->data[] = $row;
}
return html_writer::table($table);
}
/**
* This functions returns content for the start attempt button to start a secured browser attempt.
*
* @param moodle_url $attempturl
*/
private function display_start_attempt_form_secured(moodle_url $attempturl): string {
$startlink = new action_link($attempturl, get_string('startattemptbtn', 'adaptivequiz'), null, ['class' => 'btn btn-primary']);
$this->page->requires->js_module($this->adaptivequiz_get_js_module());
$this->page->requires->js('/mod/adaptivequiz/module.js');
$popupaction = new popup_action('click', $attempturl, 'adaptivequizpopup', self::$popupoptions);
$startlink->add_action(new component_action('click',
'M.mod_adaptivequiz.secure_window.start_attempt_action', [
'url' => $attempturl->out(false),
'windowname' => 'adaptivequizpopup',
'options' => $popupaction->get_js_options(),
'fullscreen' => true,
'startattemptwarning' => '',
]));
$warning = html_writer::tag('noscript', $this->heading(get_string('noscript', 'quiz')));
return $this->render($startlink) . $warning;
}
}
/**
* A substitute renderer class that outputs CSV results instead of HTML.
*/
class mod_adaptivequiz_csv_renderer extends mod_adaptivequiz_renderer {
/**
* This function returns page header information to be printed to the page
* @return string HTML markup for header inforation
*/
public function print_header() {
header('Content-type: text/csv');
$filename = $this->page->title;
$filename = preg_replace('/[^a-z0-9_-]/i', '_', $filename);
$filename = preg_replace('/_{2,}/', '_', $filename);
$filename = $filename.'.csv';
header("Content-Disposition: attachment; filename=$filename");
}
/**
* This function returns page footer information to be printed to the page
* @return string HTML markup for footer inforation
*/
public function print_footer() {
// Do nothing.
}
/**
* This function prints paging information
* @param int $totalrecords the total number of records returned
* @param int $page the current page the user is on
* @param int $perpage the number of records displayed on one page
* @return string HTML markup
*/
public function print_paging_bar($totalrecords, $page, $perpage) {
// Do nothing.
}
/**
* This function returns the HTML markup to display a table of the attempts taken at the activity
* @param stdClass $records attempt records from adaptivequiz_attempt table
* @param stdClass $cm course module object set to the instance of the activity
* @param string $sort the column the the table is to be sorted by
* @param string $sortdir the direction of the sort
* @return string HTML markup
*/
public function print_report_table($records, $cm, $sort, $sortdir) {
ob_start();
$output = fopen('php://output', 'w');
$headers = array(
get_string('firstname'),
get_string('lastname'),
get_string('email'),
get_string('numofattemptshdr', 'adaptivequiz'),
get_string('bestscore', 'adaptivequiz'),
get_string('bestscorestderror', 'adaptivequiz'),
get_string('attemptfinishedtimestamp', 'adaptivequiz'),
);
fputcsv($output, $headers);
foreach ($records as $record) {
if (intval($record->timemodified)) {
$timemodified = date('c', intval($record->timemodified));
} else {
$timemodified = get_string('na', 'adaptivequiz');
}
$row = array(
$record->firstname,
$record->lastname,
$record->email,
$record->attempts,
$this->format_measure($record),
$this->format_standard_error($record),
$timemodified,
);
fputcsv($output, $row);
}
return ob_get_clean();
}
/**
* This function formats the ability measure into a user friendly format
* @param stdClass an object with the following properties: measure, highestlevel, lowestlevel and stderror. The values must
* come from the activty instance and the user's
* attempt record
* @return string a user friendly format of the ability measure. Ability measure is rounded to the nearest decimal.
*/
public function format_measure($record) {
if (is_null($record->measure)) {
return 'n/a';
}
return round(catalgo::map_logit_to_scale($record->measure, $record->highestlevel, $record->lowestlevel), 2);
}
/**
* This function formats the standard error into a user friendly format
* @param stdClass an object with the following properties: measure, highestlevel, lowestlevel and stderror. The values must
* come from the activty instance and the user's
* attempt record
* @return string a user friendly format of the standard error. Standard error is
* rounded to the nearest one hundredth then multiplied by 100
*/
public function format_standard_error($record) {
if (is_null($record->stderror) || $record->stderror == 0.0) {
return 'n/a';
}
$percent = round(catalgo::convert_logit_to_percent($record->stderror), 2) * 100;
return $percent.'%';
}
}
<?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/>.
/**
* Page to view info about a certain attempt.
*
* @package mod_adaptivequiz
* @copyright 2013 Remote-Learner {@link http://www.remote-learner.ca/}
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../config.php');
require_once($CFG->dirroot . '/tag/lib.php');
require_once($CFG->dirroot . '/mod/adaptivequiz/locallib.php');
$attemptid = required_param('attempt', PARAM_INT);
$page = optional_param('page', 0, PARAM_INT);
$tab = optional_param('tab', 'attemptsummary', PARAM_ALPHA);
$attempt = $DB->get_record('adaptivequiz_attempt', ['id' => $attemptid], '*', MUST_EXIST);
$adaptivequiz = $DB->get_record('adaptivequiz', ['id' => $attempt->instance], '*', MUST_EXIST);
$cm = get_coursemodule_from_instance('adaptivequiz', $adaptivequiz->id, $adaptivequiz->course, false, MUST_EXIST);
$course = $DB->get_record('course', ['id' => $adaptivequiz->course], '*', MUST_EXIST);
require_login($course, true, $cm);
$context = context_module::instance($cm->id);
// KNIGHT (Feature 2): access is decided per attempt by adaptivequiz_user_can_review_attempt() - teachers
// (viewreport) may review any attempt and see all tabs; a student may review only their own completed
// attempt, only when the activity enables it (showownattemptresult), and only the question details tab.
$showdetailed = has_capability('mod/adaptivequiz:viewreport', $context);
if (!adaptivequiz_user_can_review_attempt($adaptivequiz, $attempt, $context, $USER->id)) {
throw new moodle_exception('nopermission', 'adaptivequiz');
}
if (!$showdetailed) {
$tab = 'questionsdetails';
}
$user = $DB->get_record('user', ['id' => $attempt->userid], '*', MUST_EXIST);
$quba = question_engine::load_questions_usage_by_activity($attempt->uniqueid);
$a = new stdClass();
$a->quizname = format_string($adaptivequiz->name);
$a->fullname = fullname($user);
$a->finished = userdate($attempt->timemodified);
$title = get_string('reportattemptreviewpageheading', 'adaptivequiz', $a);
$PAGE->set_url('/mod/adaptivequiz/reviewattempt.php', ['attempt' => $attempt->id, 'tab' => $tab]);
$PAGE->set_title($title);
$PAGE->set_heading(format_string($course->fullname));
$PAGE->set_context($context);
$PAGE->navbar->add(get_string('reports'));
// KNIGHT (Feature 2): the per-user attempts report is teacher-only, so do not offer the link to students.
if ($showdetailed) {
$PAGE->navbar->add(
get_string('reportuserattemptstitleshort', 'adaptivequiz', fullname($user)),
new moodle_url('/mod/adaptivequiz/viewattemptreport.php', ['userid' => $user->id, 'cmid' => $cm->id])
);
}
$PAGE->navbar->add(get_string('reviewattempt', 'adaptivequiz'));
$renderer = $PAGE->get_renderer('mod_adaptivequiz');
echo $renderer->print_header();
echo $renderer->heading($title);
echo $renderer->attempt_review_tabs($PAGE->url, $tab, $showdetailed);
echo $renderer->attempt_report_page_by_tab($tab, $adaptivequiz, $attempt, $user, $quba, $PAGE->url, $page);
echo $renderer->print_footer();
.adaptivequiz-summarylist {
float: left;
}
.adaptivequiz-summarylist dt {
font-weight: bold;
}
.adaptivequiz-attemptgraph {
clear: both;
}
#adpq_scoring_table table {
float: left;
margin-right: 20px;
}
#adpq_scoring_table {
clear: both;
}
.adpq_download {
text-align: center;
margin: 10px;
}
/* Styles related to the question analysis reporting */
table.adpq_answers_table thead th.section {
text-align: left;
padding-top: 30px;
}
table.adpq_answers_table thead:first-child th.section {
padding-top: inherit;
}
.adpq_correct {
color: green;
}
.adpq_incorrect {
color: red;
}
.adpq_highlevel .adpq_incorrect {
font-weight: bold;
}
.adpq_lowlevel .adpq_correct {
font-weight: bold;
}
/**/
.usersattemptstable-wrapper {
margin-top: 1rem;
}
.usersattemptstable {
margin-bottom: 0.75rem !important;
}
#page-mod-adaptivequiz-view .attempt-progress-container {
margin-bottom: 25px;
padding-top: 10px;
}
#page-mod-adaptivequiz-view .attempt-progress .progress-bar-wrapper {
width: 60%;
border-radius: 0.5rem;
background-color: #dee2e6;
}
#page-mod-adaptivequiz-view .attempt-progress .progress-bar-inner {
height: 0.8rem;
border-radius: 0.5rem 0 0 0.5rem;
}
#page-mod-adaptivequiz-view .attempt-progress .no-out-of {
font-weight: 700;
font-size: 1.25rem;
}
.col-form-label-details-align {
padding-top: calc(0.375rem + 1px);
padding-bottom: calc(0.375rem + 1px);
margin-bottom: 0;
line-height: 1.5;
}
{{!
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/>.
}}
{{!
@template mod_adaptivequiz/ability_measure
Formatted ability measure content to display to a user.
Variables required for this template:
* abilitymeasurevalue - the value of ability measure
* lowestlevel - a parameter from the activity settings
* highestlevel - a parameter from the activity settings
* helpicon - a pre-rendered help icon
Example context (json):
{
"abilitymeasurevalue": 12,
"lowestlevel": 5,
"highestlevel": 15,
"helpicon": "<a class='btn ..'><i class='icon fa fa-question-circle ..'></i></a>",
}
}}
<span>
{{#str}} abilityestimated, adaptivequiz {{/str}}: <strong>{{abilitymeasurevalue}}</strong> / {{lowestlevel}} - {{highestlevel}}{{{helpicon}}}
</span>
{{!
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/>.
}}
{{!
@template mod_adaptivequiz/attempt_administration_report
Renders a line chart for the questions administration data.
}}
{{< mod_adaptivequiz/attempt_report_chart }}
{{$initjs}}
{{#js}}
require([
'jquery',
'core/chart_builder',
'mod_adaptivequiz/attempt_administration_chart_output',
'mod_adaptivequiz/attempt_administration_chart_output_htmltable'
], function(
$,
Builder,
Output,
OutputTable
) {
var data = {{{chartdata}}},
uniqid = "{{uniqid}}",
chartArea = $('#chart-area-' + uniqid),
chartImage = chartArea.find('.chart-image'),
chartTable = chartArea.find('.chart-table-data'),
chartLink = chartArea.find('.chart-table-expand a');
Builder.make(data).then(function(ChartInst) {
new Output(chartImage, ChartInst);
new OutputTable(chartTable, ChartInst);
});
chartLink.on('click', function(e) {
e.preventDefault();
if (chartTable.is(':visible')) {
chartTable.hide();
chartLink.text({{#quote}}{{#str}}showchartdata, moodle{{/str}}{{/quote}});
chartTable.attr('aria-expanded', false);
} else {
chartTable.show();
chartLink.text({{#quote}}{{#str}}hidechartdata, moodle{{/str}}{{/quote}});
chartTable.attr('aria-expanded', true);
}
});
});
{{/js}}
{{/initjs}}
{{/ mod_adaptivequiz/attempt_report_chart }}
{{!
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/>.
}}
{{!
@template mod_adaptivequiz/attempt_answers_distribution_report
Renders a bar chart for the answers distribution data.
Context variables required for this template:
* showchartstacked - whether the chart should be displayed in stacked form
* userid - current user id
* adaptivequizid - id of the current adaptive quiz instance
Data attributes required for JS:
* data-action
Example context (json):
{
showchartstacked: true,
userid: 5,
adaptivequizid: 3
}
}}
<div class="mdl-align mb-3">
<span class="mr-1">
<input id="answers-distribution-chart-stacked" type="checkbox" data-action="set-answers-distribution-chart-stacked" {{#showchartstacked}}checked{{/showchartstacked}} />
<label class="mr-1" for="answers-distribution-chart-stacked">{{#str}}reportanswersdistributionchartdisplaystacked, adaptivequiz{{/str}}</label>
</span>
</div>
{{< mod_adaptivequiz/attempt_report_chart }}
{{$initjs}}
{{#js}}
require([
'jquery',
'core/chart_builder',
'core/chart_output_chartjs',
'core/chart_output_htmltable',
'mod_adaptivequiz/attempt_answers_distribution_chart_manager'
], function(
$,
Builder,
Output,
OutputTable,
ChartManager
) {
var data = {{{chartdata}}},
uniqid = "{{uniqid}}",
chartArea = $('#chart-area-' + uniqid),
chartImage = chartArea.find('.chart-image'),
chartTable = chartArea.find('.chart-table-data'),
chartLink = chartArea.find('.chart-table-expand a');
Builder.make(data).then(function(ChartInst) {
const output = new Output(chartImage, ChartInst);
ChartManager.init(output, ChartInst, {{userid}}, {{adaptivequizid}});
new OutputTable(chartTable, ChartInst);
});
chartLink.on('click', function(e) {
e.preventDefault();
if (chartTable.is(':visible')) {
chartTable.hide();
chartLink.text({{#quote}}{{#str}}showchartdata, moodle{{/str}}{{/quote}});
chartTable.attr('aria-expanded', false);
} else {
chartTable.show();
chartLink.text({{#quote}}{{#str}}hidechartdata, moodle{{/str}}{{/quote}});
chartTable.attr('aria-expanded', true);
}
});
});
{{/js}}
{{/initjs}}
{{/ mod_adaptivequiz/attempt_report_chart }}
{{!
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/>.
}}
{{!
@template mod_adaptivequiz/attempt_debug_info
Renders debugging information for an attempt.
Context variables required for this template:
* params - an array of objects with 'name' and 'value' properties
Example context (json):
{
"params": [
{
"name": "Difficulty Sum",
"value": "0.7114963"
},
{
"name": "Standard error",
"value": "0.00000"
}
]
}
}}
{{#params}}
{{#value}}
<div>{{name}}: {{value}}</div>
{{/value}}
{{/params}}
{{!
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/>.
}}
{{!
@template mod_adaptivequiz/attempt_finished_page
Renders contents of the page displayed to users when attempt is finished.
Variables required for this template:
* feedbacktext - text's content/HTML
* continuebutton - button's HTML
* browsersecurityenabled - whether the 'secure' mode is requested
* debuginfo - pre-rendered debugging info (when avaialble)
Example context (json):
{
"feedbacktext": "<p>Thank you for an attempt!</p>",
"continuebutton": "<a class="..">..</a>",
"browsersecurityenabled": true,
"debuginfo": "<div>..</div>"
}
}}
<div class="text-center">
<p class="adaptivequizfeedback">{{{feedbacktext}}}</p>
{{#showabilitymeasure}}
<div class="box py-3">
{{> mod_adaptivequiz/ability_measure }}
</div>
{{/showabilitymeasure}}
<div>
{{^browsersecurityenabled}}
{{{continuebutton}}}
{{/browsersecurityenabled}}
{{#browsersecurityenabled}}
<input type="button" id="secureclosebutton" class="btn btn-primary" value="{{#str}} continue {{/str}}" />
{{/browsersecurityenabled}}
</div>
{{#debuginfo}}
<div class="mt-2">
{{{debuginfo}}}
</div>
{{/debuginfo}}
</div>
{{!
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/>.
}}
{{!
@template mod_adaptivequiz/attempt_progress
Indicator of how many questions are answered out of the maximum number set for the attempt.
Variables required for this template:
* questionsanswerednumber - number of question already answered during the attempt
* maximumquestionsnumber - number of maximum questions set for the quiz in activity settings
* showprogressbar - whether a bar depicting the progress should be shown
* percentprogressbarfilled - percent of questions already answered out of maximum number during the attempt. Optional, but
should be present when 'showprogressbar' is present also
* helpiconcontent - already rendered help icon, optional.
Example context (json):
{
"questionsanswerednumber": "23",
"maximumquestionsnumber": "50",
"showprogressbar": true,
"percentprogressbarfilled": "46",
"helpiconcontent": "<a class='btn ..'><i class='icon fa fa-question-circle ..'></i></a>"
}
}}
<div class="attempt-progress">
<h3 class="no-out-of">
{{#str}} attemptquestionsprogress, adaptivequiz, {{questionsanswerednumber}} / {{maximumquestionsnumber}} {{/str}}
{{{helpiconcontent}}}
</h3>
{{#showprogressbar}}
<div class="progress-bar-wrapper">
<div class="bg-primary progress-bar-inner" style="width: {{percentprogressbarfilled}}%">&nbsp;</div>
</div>
{{/showprogressbar}}
</div>
{{!
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/>.
}}
{{!
@template mod_adaptivequiz/attempt_report_chart
This is a copy of core/chart.
The purpose of copying is to enable custom initialization of the chart, as not all the config options can be passed through
the chart renderable objects. Thus, the js section is implemented as a block to allow overriding in specific charts templates.
See core/chart for the required context.
}}
<div class="chart-area" id="chart-area-{{uniqid}}">
<div class="chart-image" role="presentation" aria-describedby="chart-table-data-{{uniqid}}"></div>
<div class="chart-table {{^withtable}}accesshide{{/withtable}}">
<p class="chart-table-expand">
<a href="#" aria-controls="chart-table-data-{{uniqid}}" role="button">
{{#str}}showchartdata, moodle{{/str}}
</a>
</p>
<div class="chart-table-data" id="chart-table-data-{{uniqid}}" {{#withtable}}role="complementary" aria-expanded="false"{{/withtable}}></div>
</div>
</div>
{{$initjs}}
{{#js}}
require([
'jquery',
'core/chart_builder',
'core/chart_output_chartjs',
'core/chart_output_htmltable',
], function(
$,
Builder,
Output,
OutputTable
) {
var data = {{{chartdata}}},
uniqid = "{{uniqid}}",
chartArea = $('#chart-area-' + uniqid),
chartImage = chartArea.find('.chart-image'),
chartTable = chartArea.find('.chart-table-data'),
chartLink = chartArea.find('.chart-table-expand a');
Builder.make(data).then(function(ChartInst) {
new Output(chartImage, ChartInst);
new OutputTable(chartTable, ChartInst);
});
chartLink.on('click', function(e) {
e.preventDefault();
if (chartTable.is(':visible')) {
chartTable.hide();
chartLink.text({{#quote}}{{#str}}showchartdata, moodle{{/str}}{{/quote}});
chartTable.attr('aria-expanded', false);
} else {
chartTable.show();
chartLink.text({{#quote}}{{#str}}hidechartdata, moodle{{/str}}{{/quote}});
chartTable.attr('aria-expanded', true);
}
});
});
{{/js}}
{{/initjs}}
{{!
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/>.
}}
{{!
@template mod_adaptivequiz/competency_feedback
KNIGHT (Feature 4): result-dependent competency feedback for a completed attempt.
Context variables required for this template:
* narrative_summary string - short summary of the achieved ability
* algorithm_explanation string - how the ability maps onto the levels
* show_feedback bool, feedback_intro string, feedback_level int, feedback_block string
* show_feedforward bool, feedforward_intro string, feedforward_level int, feedforward_block string
* show_feedup bool, feedup_intro string, feedup_level int, feedup_block string
* passing_requirement string (optional), passing_result string (optional)
* encouragement string (optional)
Example context (json):
{
"narrative_summary": "You reached an ability of 3.42 on a scale from 1 to 9.",
"algorithm_explanation": "Your ability 3.42 lies between levels 3 and 4.",
"show_feedback": true,
"feedback_intro": "What you can already do",
"feedback_level": 3,
"feedback_block": "You can solve routine problems.",
"show_feedforward": false,
"show_feedup": true,
"feedup_intro": "Your next goal",
"feedup_level": 4,
"feedup_block": "Tackle multi-step problems.",
"encouragement": "Keep going!"
}
}}
<div class="feedback-wrapper">
<h3>{{#str}}adaptivequizfeedbackheading, adaptivequiz{{/str}}</h3>
<p><strong>{{narrative_summary}}</strong></p>
<p>{{algorithm_explanation}}</p>
{{#show_feedback}}
<h4>{{#str}}feedbackheading, adaptivequiz{{/str}}</h4>
<p>{{feedback_intro}} (Level {{feedback_level}}):</p>
<p><strong>{{feedback_block}}</strong></p>
{{/show_feedback}}
{{#show_feedforward}}
<h4>{{#str}}feedforwardheading, adaptivequiz{{/str}}</h4>
<p>{{feedforward_intro}} (Level {{feedforward_level}}):</p>
<p><strong>{{feedforward_block}}</strong></p>
{{/show_feedforward}}
{{#show_feedup}}
<h4>{{#str}}feedupheading, adaptivequiz{{/str}}</h4>
<p>{{feedup_intro}} (Level {{feedup_level}}):</p>
<p><strong>{{feedup_block}}</strong></p>
{{/show_feedup}}
{{#passing_requirement}}
<h4>{{#str}}passinggradeheading, adaptivequiz{{/str}}</h4>
<div class="passing-grade-warning">
<p>{{passing_requirement}}</p>
{{#passing_result}}
<p><strong>{{passing_result}}</strong></p>
{{/passing_result}}
</div>
{{/passing_requirement}}
{{#encouragement}}
<div class="encouragement-message mt-3">
<p><em>{{encouragement}}</em></p>
</div>
{{/encouragement}}
</div>
{{!
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/>.
}}
{{!
@template mod_adaptivequiz/editor_placeholder
Renders a single placeholder's description for a text editor.
The template can be used for any text editor within the plugin.
Variables required for this template:
* placeholderid - string id of the placeholder
* placeholderkey - what will be replaced in the text editor's contents
* placeholderdesc - text description of the placeholder
Example context (json):
{
"placeholderid": "abilitimeasure",
"placeholderkey": "\{\{abilitimeasure\}\}",
"placeholderdesc": "Ability measure",
}
}}
<code><span id="copy-to-clipboard-{{placeholderid}}">{{{placeholderkey}}}</span></code> - {{{placeholderdesc}}}
<a href="#"
data-action="copytoclipboard"
data-clipboard-target="#copy-to-clipboard-{{placeholderid}}"
data-clipboard-success-message="{{#str}} placeholdercopied, adaptivequiz {{/str}}"
title="{{#str}} copytoclipboard, core {{/str}}"
>
{{#pix}} t/clipboard, core {{/pix}}
</a>
{{#js}}
require(['core/copy_to_clipboard']);
{{/js}}
{{!
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/>.
}}
{{!
@template mod_adaptivequiz/editor_placeholders_desc
Renders pre-defined descriptions for text editor's placeholders.
The template can be used for any text editor within the plugin.
Variables required for this template:
* placeholders - an array of placeholders to be passed to the include
Example context (json):
{
"placeholders": [
{...}
].
}
}}
<div class="col-form-label-details-align">
<details>
<summary>{{#str}} attemptfeedbackplaceholdersdesc, adaptivequiz {{/str}}</summary>
<ul class="list-unstyled">
{{#placeholders}}
<li>
{{> mod_adaptivequiz/editor_placeholder }}
</li>
{{/placeholders}}
</ul>
</details>
</div>
<div class="row">
<div class="col-4 text-right font-weight-bold">{{name}}:</div>
<div class="col-2">{{value}}</div>
<div class="col-2">
<span
class="badge {{^error}}bg-success{{/error}}{{#error}}bg-danger{{/error}} text-white"
>
{{^error}}{{#str}} ok {{/str}}{{/error}}{{#error}}{{error}}{{/error}}
</span>
</div>
</div>
{{< mod_adaptivequiz/item_bank_settings_block}}
{{$title}}{{#str}} itembankitemadmparams, adaptivequiz {{/str}}{{/title}}
{{$body}}
{{^hasqbanks}}
{{#noqbanksnotification}}
{{> core/notification_info}}
{{/noqbanksnotification}}
{{/hasqbanks}}
{{#hasqbanks}}
{{#noparamsnotification}}
{{> core/notification_warning}}
{{/noparamsnotification}}
{{#invalidparamsnotification}}
{{> core/notification_error}}
{{/invalidparamsnotification}}
{{#paramset}}
<div class="mb-4">
{{#itemadministrationparams}}
{{> mod_adaptivequiz/item_administration_parameter}}
{{/itemadministrationparams}}
</div>
{{/paramset}}
{{/hasqbanks}}
{{/body}}
{{$footer}}
{{#hasqbanks}}
<button
class="btn btn-secondary"
data-action="show-edit-params-dialog"
data-id="{{id}}"
>
{{#str}} itembankitemadmparamsedit, adaptivequiz {{/str}}
</button>
{{/hasqbanks}}
{{/footer}}
{{$js}}
require(['mod_adaptivequiz/item_administration_params'], function(ItemBank) {
ItemBank.init();
});
{{/js}}
{{/mod_adaptivequiz/item_bank_settings_block}}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment