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
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_adaptivequiz\form;
use context;
use context_module;
use core\notification;
use core\output\html_writer;
use core_form\dynamic_form;
use mod_adaptivequiz\item_bank;
use moodle_url;
/**
* Assigns Moodle question banks to an adaptive quiz's item bank.
*
* @package mod_adaptivequiz
* @copyright 2026 Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class assign_question_bank_form extends dynamic_form {
/**
* Implements the abstract method.
*/
protected function definition() {
$form = $this->_form;
$form->addElement('html', html_writer::tag('h5', get_string('itembankthiscourseqbanks', 'adaptivequiz')));
$name = get_string('itembankselectqbank', 'adaptivequiz');
$options = [
'ajax' => 'mod_adaptivequiz/question_banks_datasource',
'multiple' => true,
'data-contextid' => $this->get_context_for_dynamic_submission()->id,
'data-incourseid' => $this->optional_param('course', null, PARAM_INT),
'id' => 'addqbanksthis',
];
$form->addElement('autocomplete', 'addqbanksthis', $name, [], $options);
$form->addElement('html', html_writer::tag('h5', get_string('itembankothercoursesqbanks', 'adaptivequiz')));
$name = get_string('itembankselectqbank', 'adaptivequiz');
$options = [
'ajax' => 'mod_adaptivequiz/question_banks_datasource',
'multiple' => true,
'data-contextid' => $this->get_context_for_dynamic_submission()->id,
'data-notincourseid' => $this->optional_param('course', null, PARAM_INT),
'id' => 'addqbanksother',
];
$form->addElement('autocomplete', 'addqbanksother', $name, [], $options);
$form->addElement('hidden', 'id');
$form->setType('id', PARAM_INT);
}
/**
* Implements the abstract method.
*/
public function process_dynamic_submission(): void {
global $DB;
$id = $this->optional_param('id', null, PARAM_INT);
$cm = get_coursemodule_from_id('adaptivequiz', $id, 0, false, MUST_EXIST);
$adaptivequiz = $DB->get_record('adaptivequiz', ['id' => $cm->instance], '*', MUST_EXIST);
$data = $this->get_data();
$addqbankidlist = array_merge($data->addqbanksthis, $data->addqbanksother);
item_bank::assign_qbanks_to_adaptivequiz($adaptivequiz->id, $addqbankidlist);
notification::success(get_string('itembanknewassignflash', 'adaptivequiz'));
}
/**
* Implements the abstract method.
*/
public function set_data_for_dynamic_submission(): void {
$this->set_data((object) ['id' => $this->optional_param('id', null, PARAM_INT)]);
}
/**
* Implements the abstract method.
*/
protected function get_context_for_dynamic_submission(): context {
$id = $this->optional_param('id', null, PARAM_INT);
$cm = get_coursemodule_from_id('adaptivequiz', $id, 0, false, MUST_EXIST);
return context_module::instance($cm->id);
}
/**
* Implements the abstract method.
*/
protected function check_access_for_dynamic_submission(): void {
// KNIGHT: assigning/removing a question bank is a managing action, so guard the AJAX endpoint
// with the manage capability (the upstream stub left it open to any logged-in user).
require_capability('mod/adaptivequiz:manage', $this->get_context_for_dynamic_submission());
}
/**
* Implements the abstract method.
*/
protected function get_page_url_for_dynamic_submission(): moodle_url {
$id = $this->optional_param('id', null, PARAM_INT);
return new moodle_url('/mod/adaptivequiz/itembank.php', ['id' => $id]);
}
}
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_adaptivequiz\form;
use context;
use context_module;
use core_form\dynamic_form;
use mod_adaptivequiz\item_administration_params_helper;
use mod_adaptivequiz\item_bank_helper;
use moodle_url;
/**
* Edits item administration settings for an 'adaptivequiz' instance.
*
* @package mod_adaptivequiz
* @copyright 2026 Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class item_administration_params_form extends dynamic_form {
#[\Override]
protected function definition() {
$form = $this->_form;
$levelattrs = ['size' => '3', 'maxlength' => '3'];
$form->addElement('text', 'highestlevel', get_string('highestlevel', 'adaptivequiz'), $levelattrs);
$form->addHelpButton('highestlevel', 'highestlevel', 'adaptivequiz');
$form->addRule('highestlevel', get_string('formelementempty', 'adaptivequiz'), 'required', null, 'client');
$form->addRule('highestlevel', get_string('formelementnumeric', 'adaptivequiz'), 'numeric', null, 'client');
$form->setType('highestlevel', PARAM_INT);
$form->addElement('text', 'lowestlevel', get_string('lowestlevel', 'adaptivequiz'), $levelattrs);
$form->addHelpButton('lowestlevel', 'lowestlevel', 'adaptivequiz');
$form->addRule('lowestlevel', get_string('formelementempty', 'adaptivequiz'), 'required', null, 'client');
$form->addRule('lowestlevel', get_string('formelementnumeric', 'adaptivequiz'), 'numeric', null, 'client');
$form->setType('lowestlevel', PARAM_INT);
$form->addElement('text', 'startinglevel', get_string('startinglevel', 'adaptivequiz'), $levelattrs);
$form->addHelpButton('startinglevel', 'startinglevel', 'adaptivequiz');
$form->addRule('startinglevel', get_string('formelementempty', 'adaptivequiz'), 'required', null, 'client');
$form->addRule('startinglevel', get_string('formelementnumeric', 'adaptivequiz'), 'numeric', null, 'client');
$form->setType('startinglevel', PARAM_INT);
$qnumattrs = ['size' => '3', 'maxlength' => '3'];
$form->addElement('text', 'minimumquestions', get_string('minimumquestions', 'adaptivequiz'), $qnumattrs);
$form->addHelpButton('minimumquestions', 'minimumquestions', 'adaptivequiz');
$form->addRule('minimumquestions', get_string('formelementempty', 'adaptivequiz'), 'required', null, 'client');
$form->addRule('minimumquestions', get_string('formelementnumeric', 'adaptivequiz'), 'numeric', null, 'client');
$form->setType('minimumquestions', PARAM_INT);
$form->addElement('text', 'maximumquestions', get_string('maximumquestions', 'adaptivequiz'), $qnumattrs);
$form->addHelpButton('maximumquestions', 'maximumquestions', 'adaptivequiz');
$form->addRule('maximumquestions', get_string('formelementempty', 'adaptivequiz'), 'required', null, 'client');
$form->addRule('maximumquestions', get_string('formelementnumeric', 'adaptivequiz'), 'numeric', null, 'client');
$form->setType('maximumquestions', PARAM_INT);
$standarderrorattrs = ['size' => '10', 'maxlength' => '10'];
$form->addElement('text', 'standarderror', get_string('standarderror', 'adaptivequiz'), $standarderrorattrs);
$form->addHelpButton('standarderror', 'standarderror', 'adaptivequiz');
$form->addRule('standarderror', get_string('formelementempty', 'adaptivequiz'), 'required', null, 'client');
$form->addRule('standarderror', get_string('formelementdecimal', 'adaptivequiz'), 'numeric', null, 'client');
$form->setDefault('standarderror', 5.0);
$form->setType('standarderror', PARAM_FLOAT);
// KNIGHT: acceptance threshold for partial-credit questions (0..1).
$thresholdattrs = ['size' => '4', 'maxlength' => '4'];
$form->addElement('text', 'acceptancethreshold', get_string('acceptancethreshold', 'adaptivequiz'), $thresholdattrs);
$form->addHelpButton('acceptancethreshold', 'acceptancethreshold', 'adaptivequiz');
$form->addRule('acceptancethreshold', get_string('formelementdecimal', 'adaptivequiz'), 'numeric', null, 'client');
$form->setDefault('acceptancethreshold', 0);
$form->setType('acceptancethreshold', PARAM_FLOAT);
// KNIGHT (Feature 3): completed-attempts threshold after which a question-analysis review is advised.
// It belongs with the question pool configuration, so it lives here rather than on the settings form.
$form->addElement(
'text',
'questionchecktrigger',
get_string('questionchecktrigger', 'adaptivequiz'),
['size' => '3', 'maxlength' => '3']
);
$form->addHelpButton('questionchecktrigger', 'questionchecktrigger', 'adaptivequiz');
$form->addRule('questionchecktrigger', get_string('formelementnumeric', 'adaptivequiz'), 'numeric', null, 'client');
$form->setDefault('questionchecktrigger', 0);
$form->setType('questionchecktrigger', PARAM_INT);
$form->addElement('hidden', 'id');
$form->setType('id', PARAM_INT);
}
/**
* Implements the abstract method.
*/
public function process_dynamic_submission(): void {
$id = $this->optional_param('id', null, PARAM_INT);
$cm = get_coursemodule_from_id('adaptivequiz', $id, 0, false, MUST_EXIST);
$data = $this->get_data();
$data->id = $cm->instance;
adaptivequiz_update_item_administration_params($data);
}
/**
* Implements the abstract method.
*/
public function set_data_for_dynamic_submission(): void {
global $DB;
$id = $this->optional_param('id', null, PARAM_INT);
$cm = get_coursemodule_from_id('adaptivequiz', $id, 0, false, MUST_EXIST);
$adaptivequiz = $DB->get_record('adaptivequiz', ['id' => $cm->instance], '*', MUST_EXIST);
$fields = item_administration_params_helper::fields();
$formdata = ['id' => $id];
foreach ($fields as $field) {
$formdata[$field] = $adaptivequiz->{$field};
}
// KNIGHT: acceptancethreshold and questionchecktrigger (Feature 3) are loaded separately - they
// are not among the "required" item administration params (a value of 0 is valid for both), so
// they must not join that completeness check.
$formdata['acceptancethreshold'] = $adaptivequiz->acceptancethreshold;
$formdata['questionchecktrigger'] = $adaptivequiz->questionchecktrigger;
$this->set_data($formdata);
}
/**
* Implements the abstract method.
*/
protected function get_context_for_dynamic_submission(): context {
$id = $this->optional_param('id', null, PARAM_INT);
$cm = get_coursemodule_from_id('adaptivequiz', $id, 0, false, MUST_EXIST);
return context_module::instance($cm->id);
}
/**
* Implements the abstract method.
*/
protected function check_access_for_dynamic_submission(): void {
// KNIGHT: this form writes the item administration / scoring settings (levels, standard error
// and the acceptance threshold), so guard it with the same manage capability as the item bank.
require_capability('mod/adaptivequiz:manage', $this->get_context_for_dynamic_submission());
}
/**
* Implements the abstract method.
*/
protected function get_page_url_for_dynamic_submission(): moodle_url {
$id = $this->optional_param('id', null, PARAM_INT);
return new moodle_url('/mod/adaptivequiz/itembank.php', ['id' => $id]);
}
#[\Override]
public function validation($data, $files) {
$errors = parent::validation($data, $files);
if (0 >= $data['minimumquestions']) {
$errors['minimumquestions'] = get_string('formelementnegative', 'adaptivequiz');
}
if (0 >= $data['maximumquestions']) {
$errors['maximumquestions'] = get_string('formelementnegative', 'adaptivequiz');
}
if (0 >= $data['startinglevel']) {
$errors['startinglevel'] = get_string('formelementnegative', 'adaptivequiz');
}
if (0 >= $data['lowestlevel']) {
$errors['lowestlevel'] = get_string('formelementnegative', 'adaptivequiz');
}
if (0 >= $data['highestlevel']) {
$errors['highestlevel'] = get_string('formelementnegative', 'adaptivequiz');
}
if (0.0 > (float) $data['standarderror'] || 50.0 <= (float) $data['standarderror']) {
$errors['standarderror'] = get_string('formstderror', 'adaptivequiz');
}
// KNIGHT: the acceptance threshold is a point fraction within 0..1 and is stored to two
// decimal places, so reject any finer input rather than silently rounding it (e.g. 0.001).
$threshold = (float) $data['acceptancethreshold'];
if (0.0 > $threshold || 1.0 < $threshold) {
$errors['acceptancethreshold'] = get_string('formacceptanceleveloutofbounds', 'adaptivequiz');
} else if (abs(round($threshold, 2) - $threshold) > 1e-9) {
$errors['acceptancethreshold'] = get_string('formacceptancethresholdprecision', 'adaptivequiz');
}
// Validate higher and lower values.
if ($data['minimumquestions'] >= $data['maximumquestions']) {
$errors['minimumquestions'] = get_string('formminquestgreaterthan', 'adaptivequiz');
}
if ($data['lowestlevel'] >= $data['highestlevel']) {
$errors['lowestlevel'] = get_string('formlowlevelgreaterthan', 'adaptivequiz');
}
if (!($data['startinglevel'] >= $data['lowestlevel'] && $data['startinglevel'] <= $data['highestlevel'])) {
$errors['startinglevel'] = get_string('formstartleveloutofbounds', 'adaptivequiz');
}
// No need to trigger the database when there's some basic error already.
if ($errors) {
return $errors;
}
$cm = get_coursemodule_from_id('adaptivequiz', $data['id'], 0, false, MUST_EXIST);
$lowestlevelnum = item_bank_helper::count_adaptivequiz_questions_with_difficulty_level(
$cm->instance,
$data['lowestlevel']
);
if (!$lowestlevelnum) {
$errors['lowestlevel'] = get_string('itembankinvalidlevel', 'adaptivequiz');
}
$highestlevelnum = item_bank_helper::count_adaptivequiz_questions_with_difficulty_level(
$cm->instance,
$data['highestlevel']
);
if (!$highestlevelnum) {
$errors['highestlevel'] = get_string('itembankinvalidlevel', 'adaptivequiz');
}
// TODO: further validation.
return $errors;
}
}
<?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/>.
/**
* Adaptivequiz required password form
*
* @copyright 2013 onwards 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
*/
namespace mod_adaptivequiz\form;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir.'/formslib.php');
use moodleform;
use html_writer;
class requiredpassword extends moodleform {
/** @var string $passwordmessage a string containing text for a failed password attempt */
public $passwordmessage = '';
/**
* This method is refactored code from the quiz's password rule add_preflight_check_form_fields() method.
* It prints a form for the user to enter a password
*/
protected function definition() {
$mform = $this->_form;
foreach ($this->_customdata['hidden'] as $name => $value) {
if ($name === 'sesskey') {
continue;
}
if ($name === 'cmid' || $name === 'uniqueid') {
$mform->setType($name, PARAM_INT);
}
$mform->addElement('hidden', $name, $value);
}
$mform->addElement('header', 'passwordheader', get_string('password'));
$mform->addElement('static', 'passwordmessage', '', get_string('requirepasswordmessage', 'adaptivequiz'));
$attr = array('style' => 'color:red;', 'class' => 'wrongpassword');
$html = html_writer::start_tag('div', $attr);
$mform->addElement('html', $html);
$mform->addElement('static', 'message');
$html = html_writer::end_tag('div');
$mform->addElement('html', $html);
// Don't use the 'proper' field name of 'password' since that get's
// Firefox's password auto-complete over-excited.
$mform->addElement('password', 'quizpassword', get_string('enterrequiredpassword', 'adaptivequiz'));
$this->add_action_buttons(true, get_string('continue'));
}
}
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_adaptivequiz;
use stdClass;
/**
* Provides methods to read information about item administration parameters.
*
* @package mod_adaptivequiz
* @copyright 2026 Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class item_administration_params_helper {
/**
* @var string[] Proeprties of the 'adaptivequiz' module related to item administration.
*/
private const PARAMS = ['highestlevel', 'lowestlevel', 'startinglevel', 'minimumquestions', 'maximumquestions',
'standarderror'];
/**
* Returns names of the fields related to item administration settings.
*
* @return string[] An array of 'adaptivequiz' module's field names.
*/
public static function fields(): array {
return self::PARAMS;
}
/**
* Reports whether all required parameters for item administration are set.
*
* @param stdClass $adaptivequiz An 'adaptivequiz' instance.
*/
public static function is_all_set_for_adaptivequiz(stdClass $adaptivequiz): bool {
// If at least one fails the whole thing does as well.
foreach (self::PARAMS as $field) {
if (!$adaptivequiz->{$field}) {
return false;
}
}
return true;
}
/**
* Provides detailed information on the validity of item administration parameters.
*
* @param stdClass $adaptivequiz An 'adaptivequiz' instance.
* @return array The key is a param shortname, the value is an error message (if the param is invalid).
*/
public static function get_validation_results_for_adaptivequiz(stdClass $adaptivequiz): array {
$return = [];
foreach (self::PARAMS as $param) {
$return[$param] = '';
}
$lowestlevelnum = item_bank_helper::count_adaptivequiz_questions_with_difficulty_level(
$adaptivequiz->id,
$adaptivequiz->lowestlevel
);
if (!$lowestlevelnum) {
$return['lowestlevel'] = get_string('itembankinvalidlevel', 'adaptivequiz');
}
$highestlevelnum = item_bank_helper::count_adaptivequiz_questions_with_difficulty_level(
$adaptivequiz->id,
$adaptivequiz->highestlevel
);
if (!$highestlevelnum) {
$return['highestlevel'] = get_string('itembankinvalidlevel', 'adaptivequiz');
}
return $return;
}
/**
* A shortcut method to wrap all checks for item administration parameters.
*
* @param stdClass $adaptivequiz An 'adaptivequiz' instance.
*/
public static function is_all_valid_for_adaptivequiz(stdClass $adaptivequiz): bool {
if (!self::is_all_set_for_adaptivequiz($adaptivequiz)) {
return false;
}
$paramsvalidation = self::get_validation_results_for_adaptivequiz($adaptivequiz);
foreach ($paramsvalidation as $validationresult) {
$validationfailed = $validationresult != '';
if ($validationfailed) {
return false;
}
}
return true;
}
}
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_adaptivequiz;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/mod/adaptivequiz/locallib.php');
use context_module;
use core_question\local\bank\question_bank_helper;
use stdClass;
/**
* A high level class to manage item banks for adaptive quizzes.
*
* It contains only static methods wrapping operations as a whole with no explicit dependencies. The class is intended
* to be used for item bank management only.
*
* @package mod_adaptivequiz
* @copyright 2026 Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class item_bank {
/**
* Creates new links between the given adaptive quiz instance and questions banks.
*
* @param int $adaptivequizid ID of the adaptive quiz instance to link the question banks to.
* @param int[] $qbankidlist List of ID of question bank instances to link.
*/
public static function assign_qbanks_to_adaptivequiz(int $adaptivequizid, array $qbankidlist): void {
global $DB;
$cm = get_coursemodule_from_instance('adaptivequiz', $adaptivequizid, 0, false, MUST_EXIST);
$context = context_module::instance($cm->id);
// A way to validate the questions banks being assigned. We fetch what's available to match the passed
// question banks against.
$availableqbanks = question_bank_helper::get_activity_instances_with_shareable_questions(
filtercontext: $context
);
$qbankidlist = array_intersect(
$qbankidlist,
array_map(fn($qbank): int => $qbank->cminfo->instance, $availableqbanks)
);
// Filter out what's added already.
$linkedqbankidlist = $DB->get_fieldset('adaptivequiz_qbank', 'qbankid', ['adaptivequizid' => $adaptivequizid]);
$qbankidlist = array_diff($qbankidlist, $linkedqbankidlist);
$insert = array_map(function(int $qbankid) use ($adaptivequizid): stdClass {
$qbankcm = get_coursemodule_from_instance('qbank', $qbankid, 0, false, MUST_EXIST);
$qbankcontext = context_module::instance($qbankcm->id);
return (object) [
'adaptivequizid' => $adaptivequizid,
'qbankid' => $qbankid,
'qbankcontextid' => $qbankcontext->id,
];
}, $qbankidlist);
$DB->insert_records('adaptivequiz_qbank', $insert);
}
/**
* Unlinks the given question bank from the adaptive quiz instance.
*
* Can be used as a high-level API method, contains all necessary permissions checks.
*
* @param int $adaptivequizid ID of the adaptive quiz instance to unlink the question bank from.
* @param int $qbankid ID of th question bank to unlink.
*/
public static function unassign_qbank_from_adaptivequiz(int $adaptivequizid, int $qbankid): void {
global $DB;
$cm = get_coursemodule_from_instance(
modulename: 'adaptivequiz',
instance: $adaptivequizid,
strictness: MUST_EXIST
);
$context = context_module::instance($cm->id);
require_capability('mod/adaptivequiz:manage', $context);
$DB->delete_records('adaptivequiz_qbank', ['adaptivequizid' => $adaptivequizid, 'qbankid' => $qbankid]);
}
/**
* Unlinks the given question category from the adaptive quiz instance.
*
* Can be used as a high-level API method, contains all necessary permissions checks.
*
* @param int $adaptivequizid ID of the adaptive quiz instance to unlink the question category from.
* @param int $qcatid ID of th question category to unlink.
*/
public static function unassign_question_category_from_adaptivequiz(int $adaptivequizid, int $qcatid): void {
global $DB;
$cm = get_coursemodule_from_instance(
modulename: 'adaptivequiz',
instance: $adaptivequizid,
strictness: MUST_EXIST
);
$context = context_module::instance($cm->id);
require_capability('mod/adaptivequiz:manage', $context);
$DB->delete_records('adaptivequiz_question', ['instance' => $adaptivequizid, 'questioncategory' => $qcatid]);
}
/**
* Returns the list of question banks assigned to the given adaptive quiz instance.
*
* It may distinguish question banks by courses: either fetch from a particular course or skip question banks in
* a particular course. If no course parameters are specified it fetches all the question banks assigned for
* the adaptive quiz instance.
*
* @param int $adaptivequizid
* @param string $fields Comma separated list of fields to return for each item.
* @param int|null $incourseid A specific course question banks must be from.
* @param int|null $notincourseid A specific course question banks must not be from.
* @return stdClass[] An array of question bank instances.
*/
public static function get_question_banks_assigned_to_adaptivequiz(
int $adaptivequizid,
string $fields = '*',
?int $incourseid = null,
?int $notincourseid = null
): array {
global $DB;
$whereextra = '';
$paramsextra = [];
if ($incourseid) {
$whereextra .= "AND qb.course = ?";
$paramsextra[] = $incourseid;
}
if ($notincourseid) {
$whereextra .= "AND qb.course != ?";
$paramsextra[] = $notincourseid;
}
$sql = "SELECT qb.{$fields}
FROM {adaptivequiz_qbank} aqb
JOIN {qbank} qb ON qb.id = aqb.qbankid
WHERE aqb.adaptivequizid = ?
{$whereextra}";
$params = array_merge([$adaptivequizid], $paramsextra);
return $DB->get_records_sql($sql, $params);
}
/**
* Provides information about single question categories linked to the given adaptive quiz activity.
*
* @return stdClass[] Each item is a record from {question_categories} + 'cmid' and 'qbankname' fields.
*/
public static function get_question_categories_assigned_to_adaptivequiz(
int $adaptivequizid,
string $fields = '*',
?int $incourseid = null,
?int $notincourseid = null
): array {
global $DB;
$whereextra = '';
$paramsextra = [];
if ($incourseid) {
$whereextra .= "AND cm.course = ?";
$paramsextra[] = $incourseid;
}
if ($notincourseid) {
$whereextra .= "AND cm.course != ?";
$paramsextra[] = $notincourseid;
}
// Add a prefix to the fields to be returned.
$fieldlist = array_map(fn (string $field) => 'qc.' . $field, explode(',', $fields));
$fields = implode(',', $fieldlist);
// TODO: consider post-loading of cm_info instances for categories' qbanks.
$sql = "SELECT {$fields}, cm.id AS cmid, qb.name AS qbankname
FROM {adaptivequiz_question} aq
JOIN {question_categories} qc ON qc.id = aq.questioncategory
JOIN {context} c ON c.id = qc.contextid
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = ?
JOIN {qbank} qb ON qb.id = cm.instance
WHERE aq.instance = ?
{$whereextra}";
$params = array_merge([CONTEXT_MODULE, $adaptivequizid], $paramsextra);
return $DB->get_records_sql($sql, $params);
}
/**
* A wrapper method to know whether the adaptive quiz has any question banks or single question categories assigned.
*
* @param int $adaptivequizid ID of the adaptive quiz instance.
*/
public static function adaptive_quiz_instance_has_question_banks_or_categories_linked(int $adaptivequizid): bool {
global $DB;
$hasqbanks = $DB->record_exists('adaptivequiz_qbank', ['adaptivequizid' => $adaptivequizid]);
$hasqcats = $DB->record_exists('adaptivequiz_question', ['instance' => $adaptivequizid]);
return $hasqbanks || $hasqcats;
}
}
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_adaptivequiz;
use mod_adaptivequiz\local\repository\questions_repository;
/**
* Provides methods to read information from item banks.
*
* The purpose of this class is to provide methods for the item administration context only.
*
* @package mod_adaptivequiz
* @copyright 2026 Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class item_bank_helper {
/**
* Counts questions with the given difficulty level.
*
* @param int $adaptivequizid ID of the 'adaptivequiz' instance.
* @param int $level The difficulty level to search questions by.
* @return int The number of questions found.
*/
public static function count_adaptivequiz_questions_with_difficulty_level(int $adaptivequizid, int $level): int {
$qcategoryidlist = self::get_question_categories($adaptivequizid);
if (!$qcategoryidlist) {
return 0;
}
return questions_repository::count_adaptive_questions_in_pool_with_level($qcategoryidlist, $level);
}
/**
* Gets the list of question categories in the instance's item bank.
*
* @param int $adaptivequizid ID of the 'adaptivequiz' instance.
* @return int[] A list of question category ID.
*/
public static function get_question_categories(int $adaptivequizid): array {
global $DB;
// Single categories.
$return = $DB->get_fieldset('adaptivequiz_question', 'questioncategory', ['instance' => $adaptivequizid]);
// Entire question banks.
$sql = "SELECT qc.id
FROM {adaptivequiz_qbank} aqb
JOIN {question_categories} qc ON aqb.qbankcontextid = qc.contextid
WHERE aqb.adaptivequizid = ?";
$params = [$adaptivequizid];
$return = array_merge($return, $DB->get_fieldset_sql($sql, $params));
$return = array_unique($return);
return $return;
}
}
This diff is collapsed.
<?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/>.
/**
* A class to emulate enum type for attempt state.
*
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types=1);
namespace mod_adaptivequiz\local\attempt;
final class attempt_state {
public const IN_PROGRESS = 'inprogress';
public const COMPLETED = 'complete';
/**
* @var string $stateasstring
*/
private $stateasstring;
private function __construct(string $state) {
$this->stateasstring = $state;
}
public function is_in_progress(): bool {
return self::IN_PROGRESS === $this->stateasstring;
}
public function is_completed(): bool {
return self::COMPLETED === $this->stateasstring;
}
public static function in_progress(): self {
return new self(self::IN_PROGRESS);
}
public static function completed(): self {
return new self(self::COMPLETED);
}
}
This diff is collapsed.
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_adaptivequiz\local;
use coding_exception;
use dml_exception;
use dml_read_exception;
use invalid_parameter_exception;
use mod_adaptivequiz\item_bank_helper;
use mod_adaptivequiz\local\repository\questions_number_per_difficulty;
use mod_adaptivequiz\local\repository\questions_repository;
use mod_adaptivequiz\local\repository\tags_repository;
use moodle_exception;
use stdClass;
/**
* This class does the work of fetching questions associated with a level of difficulty in the item bank.
*
* @package mod_adaptivequiz
* @copyright 2013 onwards 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
*/
class fetchquestion {
/**
* The maximum number of attempts at finding a tag containing questions
*/
const MAXTAGRETRY = 5;
/**
* The maximum number of tries at finding avaiable questions
*/
const MAXNUMTRY = 100000;
/** @var stdClass $adaptivequiz object, properties come from the adaptivequiz table */
protected $adaptivequiz;
/**
* @var bool $debugenabled flag to denote developer debugging is enabled and this class should write message to the debug array
*/
protected $debugenabled = false;
/** @var array $debug array containing debugging information */
protected $debug = array();
/** @var array $tags an array of tags that used to identify eligible questions for the attempt */
protected $tags = array();
/** @var int $level the level of difficutly that will be used to fetch questions */
protected $level = 1;
/** @var int[] $questcatids An array of question category ID. */
protected $questcatids = [];
/** @var int $minimumlevel the minimum level achievable in the attempt */
protected $minimumlevel;
/** @var int $maximumlevel the maximum level achievable in the attempt */
protected $maximumlevel;
/**
* @var array $tagquestsum an array whose keys are difficulty numbers and values are the sum of questions associated with the
* difficulty level
*/
protected $tagquestsum = array();
/** @var bool $rebuild a flag used to force the rebuilding of the $tagquestsum property */
public $rebuild = false;
/**
* The constructor.
*
* @param stdClass $adaptivequiz A record object from {adaptivequiz}.
* @param int $level Level of difficulty to look for when fetching a question.
* @param int $minimumlevel The minimum level the student can achieve.
* @param int $maximumlevel The maximum level the student can achieve.
* @param array $tags An array of accepted tags.
* @throws coding_exception
*/
public function __construct($adaptivequiz, $level, $minimumlevel, $maximumlevel, $tags = []) {
global $SESSION;
$this->adaptivequiz = $adaptivequiz;
$this->tags = $tags;
$this->tags[] = ADAPTIVEQUIZ_QUESTION_TAG;
$this->minimumlevel = $minimumlevel;
$this->maximumlevel = $maximumlevel;
if (!is_int($level) || 0 >= $level) {
throw new coding_exception('Argument 2 is not an positive integer', 'Second parameter must be a positive integer');
}
if ($minimumlevel >= $maximumlevel) {
throw new coding_exception('Minimum level is greater than maximum level',
'Invalid minimum and maximum parameters passed');
}
$this->level = $level;
// Initialize $tagquestsum property.
if (!isset($SESSION->adpqtagquestsum)) {
$SESSION->adpqtagquestsum = array();
$this->tagquestsum = $SESSION->adpqtagquestsum;
} else {
$this->tagquestsum = $SESSION->adpqtagquestsum;
}
if (debugging('', DEBUG_DEVELOPER)) {
$this->debugenabled = true;
}
}
/**
* This function sets the level of difficulty property
* @param int $level level of difficulty
* @return void
*/
public function set_level($level = 1) {
if (!is_int($level) || 0 >= $level) {
throw new coding_exception('Argument 1 is not an positive integer', 'First parameter must be a positive integer');
}
$this->level = $level;
}
/**
* This function returns the level of difficulty property
* @return int - level of difficulty
*/
public function get_level() {
return $this->level;
}
/**
* Reset the maximum question level to search for to a new value
*
* @param int $maximumlevel
* @return void
* @throws coding_exception if the maximum level is less than minimum level
*/
public function set_maximum_level($maximumlevel) {
if ($maximumlevel < $this->minimumlevel) {
throw new coding_exception('Maximum level is less than minimum level', 'Invalid maximum level set.');
}
$this->maximumlevel = $maximumlevel;
}
/**
* Reset the maximum question level to search for to a new value
*
* @param int $maximumlevel
* @return void
* @throws coding_exception if the minimum level is less than maximum level
*/
public function set_minimum_level($minimumlevel) {
if ($minimumlevel > $this->maximumlevel) {
throw new coding_exception('Minimum level is less than maximum level', 'Invalid minimum level set.');
}
$this->minimumlevel = $minimumlevel;
}
/**
* This functions adds a message to the debugging array
* @param string $message: details of the debugging message
* @return void
*/
protected function print_debug($message = '') {
if ($this->debugenabled) {
$this->debug[] = $message;
}
}
/**
* Answer a string view of a variable for debugging purposes
* @param mixed $variable
*/
protected function vardump($variable) {
ob_start();
var_dump($variable);
return ob_get_clean();
}
/**
* This function returns the debug array
* @return array - array of debugging messages
*/
public function get_debug() {
return $this->debug;
}
/**
* This functions returns the $tagquestsum class property
* @return array an array whose keys are difficulty levels and values are the sum of questions associated with the difficulty
*/
public function get_tagquestsum() {
return $this->tagquestsum;
}
/**
* This functions sets the $tagquestsum class property
* @param array an array whose keys are difficulty levels and values are the sum of questions associated with the difficulty
*/
public function set_tagquestsum($tagquestsum) {
$this->tagquestsum = $tagquestsum;
}
/**
* This function decrements 1 from the sum of questions in a difficulty level
* @param array $tagquestsum an array equal to the $tagquestsum property, where the key is the difficulty level and the value
* is the total number of
* questions associated with it. This parameter will be modified.
* @param int $level the difficulty level
* @return array an array whose keys are difficulty levels and values are the sum of questions associated with the difficulty
*/
public function decrement_question_sum_from_difficulty($tagquestsum, $level) {
if (array_key_exists($level, $tagquestsum)) {
$tagquestsum[$level] -= 1;
}
return $tagquestsum;
}
/**
* This function first checks if the session variable already contains a mapping of difficulty levels and the number of
* questions associated with each level. Otherwise it constructos a mapping of difficulty levels and the number of questions
* in each difficulty level.
* @param array $tagquestsum an array equal to the $tagquestsum property, where the key is the difficulty level and the value
* is the total number of
* questions associated with it. This parameter will be modified.
* @param array $tags an array of tags used by the activity
* @param int $min the minimum difficulty allowed for the attempt
* @param int $max the maximum difficulty allowed for the attempt
* @param bool $rebuild true to force the rebuilding the difficulty question count array, otherwise false. Set to "true" only
* for brand new attempts
* @return array an array whose keys are difficulty levels and values are the sum of questions associated with the difficulty
*/
public function initalize_tags_with_quest_count($tagquestsum, $tags, $min, $max, $rebuild = false) {
global $SESSION;
// Check to see if the tagquestsum argument is initialized.
$count = count($tagquestsum);
if (empty($count) || !empty($rebuild)) {
$tagquestsum = array();
// Retrieve the question categories set for this activity.
$questcat = $this->retrieve_question_categories();
// Traverse through the array of configured tags used by the activity.
foreach ($tags as $tag) {
// Retrieve all of id for the configured tag.
$tagids = $this->retrieve_all_tag_ids($min, $max, $tag);
// Retrieve a count of all of the questions associated with each tag.
$difficultiesquestionsnumber = $this->retrieve_tags_with_question_count($tagids, $questcat);
// Traverse the $difficultiesquestionsnumber array and add the values with the values current in the
// $tagquestsum argument.
foreach ($difficultiesquestionsnumber as $questionsnumberperdifficulty) {
$difflevel = $questionsnumberperdifficulty->difficulty();
$totalquestindiff = $questionsnumberperdifficulty->questions_number();
// If the array key exists, then add the sum to what is already in the array.
if (array_key_exists($difflevel, $tagquestsum)) {
$tagquestsum[$difflevel] += $totalquestindiff;
} else {
$tagquestsum[$difflevel] = $totalquestindiff;
}
}
}
} else {
$tagquestsum = $SESSION->adpqtagquestsum;
}
return $tagquestsum;
}
/**
* This function retrieves a question associated with a Moodle tag level of difficulty. If the search for the tag turns up
* empty the function tries to find another tag whose difficulty level is either higher or lower
* @param array $excquestids an array of question ids to exclude from the search
* @return array an array of question ids
*/
public function fetch_questions($excquestids = array()) {
$questids = array();
// Initialize the difficulty tag question sum property for searching.
$this->tagquestsum = $this->initalize_tags_with_quest_count($this->tagquestsum, $this->tags, $this->minimumlevel,
$this->maximumlevel, $this->rebuild);
// If tagquestsum property ie empty then return with nothing.
if (empty($this->tagquestsum)) {
$this->print_debug('fetch_questions() - tagquestsum is empty');
return array();
}
// Check if the requested level has available questions.
if (array_key_exists($this->level, $this->tagquestsum) && 0 < $this->tagquestsum[$this->level]) {
$tagids = $this->retrieve_tag($this->level);
$questids = $this->find_questions_with_tags($tagids, $excquestids);
$this->print_debug('fetch_questions() - Requested level '.$this->level.' has available questions. '.
$this->tagquestsum[$this->level].' question remaining.');
return $questids;
}
// Look for a level that has avaialbe qustions.
$level = $this->level;
for ($i = 1; $i <= self::MAXNUMTRY; $i++) {
// Check if the offset level is now out of bounds and stop the loop.
if ($this->minimumlevel > $level - $i && $this->maximumlevel < $level + $i) {
$i += self::MAXNUMTRY + 1;
$this->print_debug('fetch_questions() - searching levels has gone out of bounds of the min and max levels. '.
'No questions returned');
continue;
}
// First check a level higher than the originally requested level.
$newlevel = $level + $i;
/*
* If the level is within the boundries set for the attempt and the level exists and the count of question is greater
* than zero, retrieve the tag id and the questions available
*/
$condition = $newlevel <= $this->maximumlevel && array_key_exists($newlevel, $this->tagquestsum)
&& 0 < $this->tagquestsum[$newlevel];
if ($condition) {
$tagids = $this->retrieve_tag($newlevel);
$questids = $this->find_questions_with_tags($tagids, $excquestids);
$this->level = $newlevel;
$i += self::MAXNUMTRY + 1;
$this->print_debug('fetch_questions() - original level could not be found. Returned a question from level '.
$newlevel.' instead');
continue;
}
// Check a level lower than the originally requested level.
$newlevel = $level - $i;
/*
* If the level is within the boundries set for the attempt and the level exists and the count of question is greater
* than zero, retrieve the tag id and thequestions available
*/
$condition = $newlevel >= $this->minimumlevel && array_key_exists($newlevel, $this->tagquestsum)
&& 0 < $this->tagquestsum[$newlevel];
if ($condition) {
$tagids = $this->retrieve_tag($newlevel);
$questids = $this->find_questions_with_tags($tagids, $excquestids);
$this->level = $newlevel;
$i += self::MAXNUMTRY + 1;
$this->print_debug('fetch_questions() - original level could not be found. Returned a question from level '
.$newlevel.' instead');
continue;
}
}
return $questids;
}
/**
* This function retrieves all the tag ids that can be used in this attempt.
*
* @param int $minimumlevel The minimum level the student can achieve.
* @param int $maximumlevel The maximum level the student can achieve.
* @param string $tagprefix The tag prefix used.
* @return array An array whose keys represent the difficulty level and values are tag ids.
* @throws coding_exception
* @throws dml_exception
* @throws moodle_exception
*/
public function retrieve_all_tag_ids(int $minimumlevel, int $maximumlevel, string $tagprefix): array {
if (empty(trim($tagprefix))) {
throw new invalid_parameter_exception('Tag prefix cannot be empty.');
}
$tags = array_map(function(int $level): string {
return ADAPTIVEQUIZ_QUESTION_TAG . $level;
}, range($minimumlevel, $maximumlevel));
if (!$leveltagidmap = tags_repository::get_question_level_to_tag_id_mapping_by_tag_names($tags)) {
return [];
}
return $leveltagidmap;
}
/**
* This function determines how many questions are associated with a tag, for questions contained in the category
* used by the activity.
*
* @param array $tagids an array whose key is the difficulty level and value is the tag id representing the difficulty level
* @param array $categories an array whose key and value is the question category id
* @return questions_number_per_difficulty[]
* @throws coding_exception
* @throws dml_read_exception
* @throws dml_exception
*/
public function retrieve_tags_with_question_count($tagids, $categories): array {
return questions_repository::count_questions_number_per_difficulty($tagids, $categories);
}
/**
* This function retrieves all tag ids, used by this activity and associated with a particular level of difficulty.
*
* @param int $level The level of difficulty (optional). If 0 is passed then the function will use the level class
* property, otherwise the argument value will be used.
* @return array An array whose keys represent the difficulty level and values are tag ids.
* @throws dml_exception
* @throws coding_exception
*/
public function retrieve_tag(int $level = 0): array {
$tags = array_map(function(string $tag) use($level): string {
return $tag . $level;
}, $this->tags);
if (!$tagidlist = tags_repository::get_tag_id_list_by_tag_names($tags)) {
return [];
}
return $tagidlist;
}
/**
* This function retrieves questions within the assigned question categories and
* questions associated with tagids
* @param array $tagids an array of tag is
* @param array $exclude an array of question ids to exclude from the search
* @return array an array whose keys are qustion ids and values are the question names
*/
public function find_questions_with_tags($tagids = [], $exclude = []) {
$questcat = $this->retrieve_question_categories();
return questions_repository::find_questions_with_tags($tagids, $questcat, $exclude);
}
/**
* This function retrieves all of the question categories used the activity.
*
* @return int[] An array of quesiton category ids.
*/
protected function retrieve_question_categories(): array {
// Check cached result.
if (!empty($this->questcatids)) {
return $this->questcatids;
}
$qcategoryidlist = item_bank_helper::get_question_categories($this->adaptivequiz->id);
// Cache the results.
$this->questcatids = $qcategoryidlist;
return $qcategoryidlist;
}
/**
* The destruct method saves the difficult level and qustion number mapping to the session variable
*/
public function __destruct() {
global $SESSION;
$SESSION->adpqtagquestsum = $this->tagquestsum;
}
}
<?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/>.
/**
* This class provides access to various numeric representations of a score.
*
* @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
*/
namespace mod_adaptivequiz\local\questionanalysis;
use mod_adaptivequiz\local\catalgo;
class attempt_score {
/** @var float $measuredabilitylogits The measured ability of the attempt in logits. */
protected $measuredabilitylogits = null;
/** @var float $standarderrorlogits The standard error in the score in logits. */
protected $standarderrorlogits = null;
/** @var float $lowestlevel The lowest level of question in the adaptive quiz. */
protected $lowestlevel = null;
/** @var float $highestlevel The highest level of question in the adaptive quiz. */
protected $highestlevel = null;
/**
* Constructor
*
* @return void
*/
public function __construct ($measuredabilitylogits, $standarderrorlogits, $lowestlevel, $highestlevel) {
$this->measuredabilitylogits = $measuredabilitylogits;
$this->standarderrorlogits = $standarderrorlogits;
$this->lowestlevel = $lowestlevel;
$this->highestlevel = $highestlevel;
}
/**
* Answer the measured ability in logits.
*
* @return float
*/
public function measured_ability_in_logits () {
return $this->measuredabilitylogits;
}
/**
* Answer the standard error in logits.
*
* @return float
*/
public function standard_error_in_logits () {
return $this->standarderrorlogits;
}
/**
* Answer the measured ability as a fraction 0-1.
*
* @return float
*/
public function measured_ability_in_fraction () {
return catalgo::convert_logit_to_fraction($this->measuredabilitylogits);
}
/**
* Answer the standard error a fraction 0-0.5.
*
* @return float
*/
public function standard_error_in_fraction () {
return catalgo::convert_logit_to_percent($this->standarderrorlogits);
}
/**
* Answer the measured ability on the adaptive quiz's scale
*
* @return float
*/
public function measured_ability_in_scale () {
return catalgo::map_logit_to_scale($this->measuredabilitylogits, $this->highestlevel, $this->lowestlevel);
}
/**
* Answer the standard error on the adaptive quiz's scale
*
* @return float
*/
public function standard_error_in_scale () {
return catalgo::convert_logit_to_percent($this->standarderrorlogits) * ($this->highestlevel - $this->lowestlevel);
}
}
This diff is collapsed.
<?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/>.
/**
* This class stores information about a particular attempt's result on a question
*
* @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
*/
namespace mod_adaptivequiz\local\questionanalysis;
use Exception;
use InvalidArgumentException;
class question_result {
/** @var float $_measuredability The measured ability of the user who attempted this question */
protected $_measuredability = null;
/** @var boolean $_correct True if the user was correct in their answer */
protected $_correct = null;
/**
* Constructor - Create a new result.
*
* @param float $measuredability The measured ability (0-1) of the user in this attempt.
* @param boolean $correct
* @return void
*/
public function __construct ($measuredability, $correct) {
if (!is_numeric($measuredability) || $measuredability < 0 || $measuredability > 1) {
throw new InvalidArgumentException('$measuredability must be a float between 0 and 1.');
}
$this->_measuredability = $measuredability;
$this->_correct = (bool)$correct;
}
/**
* Magic method to provide read-only access to our parameters
*
* @param $key
* @return mixed
*/
public function __get ($key) {
$param = '$_'.$key;
if (isset($this->$param)) {
return $this->$param;
} else {
throw new Exception('Unknown property, '.get_class($this).'->'.$key.'.');
}
}
}
This diff is collapsed.
<?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/>.
/**
* This interface defines the methods required for pluggable statistic-results that may be added to the question analysis.
*
* @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
*/
namespace mod_adaptivequiz\local\questionanalysis\statistics;
class answers_statistic_result implements question_statistic_result {
/** @var int $count */
protected $count = null;
/** @var string $printable */
protected $printable = null;
/**
* Constructor
*
* @param int $count
* @return void
*/
public function __construct ($count, $printable) {
$this->count = $count;
$this->printable = $printable;
}
/**
* A sortable version of the result.
*
* @return mixed string or numeric
*/
public function sortable () {
return $this->count;
}
/**
* A printable version of the result.
*
* @param numeric $result
* @return mixed string or numeric
*/
public function printable () {
return $this->printable;
}
}
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