diff --git a/lib/classes/plugin_manager.php b/lib/classes/plugin_manager.php index c7a053e0242..54bf27cb7e3 100644 --- a/lib/classes/plugin_manager.php +++ b/lib/classes/plugin_manager.php @@ -1958,6 +1958,7 @@ class core_plugin_manager { 'importquestions', 'managecategories', 'previewquestion', + 'statistics', 'tagquestion', 'viewcreator', 'viewquestionname', diff --git a/question/bank/statistics/classes/columns/discrimination_index.php b/question/bank/statistics/classes/columns/discrimination_index.php new file mode 100644 index 00000000000..9110947df59 --- /dev/null +++ b/question/bank/statistics/classes/columns/discrimination_index.php @@ -0,0 +1,56 @@ +. + +namespace qbank_statistics\columns; + +use core_question\local\bank\column_base; +use qbank_statistics\helper; +/** + * Discrimination index column + * + * @package qbank_statistics + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Nathan Nguyen + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class discrimination_index extends column_base { + + /** + * {@inheritdoc} + */ + protected function get_title(): string { + return get_string('discrimination_index', 'qbank_statistics'); + } + + /** + * {@inheritdoc} + */ + public function get_name(): string { + return 'discrimination_index'; + } + + /** + * Output the contents of this column. + * @param object $question the row from the $question table, augmented with extra information. + * @param string $rowclasses CSS class names that should be applied to this row of output. + */ + protected function display_content($question, $rowclasses) { + global $PAGE; + // Average discrimination index per quiz. + $discriminationindex = helper::calculate_average_question_discrimination_index($question->id); + echo $PAGE->get_renderer('qbank_statistics')->render_discrimination_index($discriminationindex); + } +} diff --git a/question/bank/statistics/classes/columns/discriminative_efficiency.php b/question/bank/statistics/classes/columns/discriminative_efficiency.php new file mode 100644 index 00000000000..381d02a1851 --- /dev/null +++ b/question/bank/statistics/classes/columns/discriminative_efficiency.php @@ -0,0 +1,56 @@ +. + +namespace qbank_statistics\columns; + +use core_question\local\bank\column_base; +use qbank_statistics\helper; +/** + * Discriminative efficiency column + * + * @package qbank_statistics + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Nathan Nguyen + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class discriminative_efficiency extends column_base { + + /** + * {@inheritdoc} + */ + protected function get_title(): string { + return get_string('discriminative_efficiency', 'qbank_statistics'); + } + + /** + * {@inheritdoc} + */ + public function get_name(): string { + return 'discriminative_efficiency'; + } + + /** + * Output the contents of this column. + * @param object $question the row from the $question table, augmented with extra information. + * @param string $rowclasses CSS class names that should be applied to this row of output. + */ + protected function display_content($question, $rowclasses) { + global $PAGE; + // Average discriminative efficiency per quiz. + $discriminativeefficiency = helper::calculate_average_question_discriminative_efficiency($question->id); + echo $PAGE->get_renderer('qbank_statistics')->render_discriminative_efficiency($discriminativeefficiency); + } +} diff --git a/question/bank/statistics/classes/columns/facility_index.php b/question/bank/statistics/classes/columns/facility_index.php new file mode 100644 index 00000000000..8c11b95984b --- /dev/null +++ b/question/bank/statistics/classes/columns/facility_index.php @@ -0,0 +1,56 @@ +. + +namespace qbank_statistics\columns; + +use core_question\local\bank\column_base; +use qbank_statistics\helper; +/** + * Facility index column + * + * @package qbank_statistics + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Nathan Nguyen + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class facility_index extends column_base { + + /** + * {@inheritdoc} + */ + protected function get_title(): string { + return get_string('facility_index', 'qbank_statistics'); + } + + /** + * {@inheritdoc} + */ + public function get_name(): string { + return 'facility_index'; + } + + /** + * Output the contents of this column. + * @param object $question the row from the $question table, augmented with extra information. + * @param string $rowclasses CSS class names that should be applied to this row of output. + */ + protected function display_content($question, $rowclasses) { + global $PAGE; + // Average facility index per quiz. + $facility = helper::calculate_average_question_facility($question->id); + echo $PAGE->get_renderer('qbank_statistics')->render_facility_index($facility); + } +} diff --git a/question/bank/statistics/classes/helper.php b/question/bank/statistics/classes/helper.php new file mode 100644 index 00000000000..484eb510796 --- /dev/null +++ b/question/bank/statistics/classes/helper.php @@ -0,0 +1,214 @@ +. + +namespace qbank_statistics; + +defined('MOODLE_INTERNAL') || die(); +require_once($CFG->dirroot . '/mod/quiz/report/statistics/statisticslib.php'); +require_once($CFG->dirroot . '/mod/quiz/report/default.php'); +require_once($CFG->dirroot . '/mod/quiz/report/statistics/report.php'); +require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php'); +require_once($CFG->dirroot . '/mod/quiz/attemptlib.php'); +use core_question\statistics\questions\all_calculated_for_qubaid_condition; +use quiz_statistics_report; +/** + * Helper for statistics + * + * @package qbank_statistics + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Nathan Nguyen + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class helper { + + /** + * @var float Threshold to determine 'need for revision' + */ + private const NEED_FOR_REVISION_LOWER_THRESHOLD = 30; + + /** + * @var float Threshold to determine 'need for revision' + */ + private const NEED_FOR_REVISION_UPPER_THRESHOLD = 50; + + /** + * Return ids of all quizzes that use the question + * + * @param int $questionid id of the question + * @return array list of quizids + * @throws \dml_exception + */ + public static function get_quizzes(int $questionid): array { + global $DB; + + $quizzes = $DB->get_fieldset_sql(" + SELECT DISTINCT qa.quiz as id + FROM {quiz_attempts} qa + JOIN {question_usages} qu ON qu.id = qa.uniqueid + JOIN {question_attempts} qatt ON qatt.questionusageid = qu.id + WHERE qatt.questionid = :questionid", ['questionid' => $questionid]); + return $quizzes; + } + + /** + * Load question stats from a quiz + * + * @param int $quizid quiz object or its id + * @return all_calculated_for_qubaid_condition + */ + private static function load_question_stats(int $quizid): all_calculated_for_qubaid_condition { + // Turn to quiz object. + $quiz = new \stdClass(); + $quiz->id = $quizid; + // All questions, no groups. + $report = new quiz_statistics_report(); + $questions = $report->load_and_initialise_questions_for_calculations($quiz); + $qubaids = quiz_statistics_qubaids_condition($quiz->id, new \core\dml\sql_join()); + $progress = new \core\progress\none(); + $qcalc = new \core_question\statistics\questions\calculator($questions, $progress); + $quizcalc = new \quiz_statistics\calculator($progress); + if ($quizcalc->get_last_calculated_time($qubaids) === false) { + $questionstats = $qcalc->calculate($qubaids); + } else { + $questionstats = $qcalc->get_cached($qubaids); + } + return $questionstats; + } + + /** + * Load a specified stats item for a question + * + * @param int $quizid quiz id + * @param int $questionid question id + * @param string $item a stats item + * @return float|int + */ + public static function load_question_stats_item(int $quizid, int $questionid, string $item): ?float { + $questionstats = self::load_question_stats($quizid); + // Find in main question. + foreach ($questionstats->questionstats as $stats) { + if ($stats->questionid == $questionid && isset($stats->$item)) { + return $stats->$item; + } + } + // If not found, find in sub questions. + foreach ($questionstats->subquestionstats as $stats) { + if ($stats->questionid == $questionid && isset($stats->$item)) { + return $stats->$item; + } + } + return null; + } + + /** + * Calculate average for a stats item on a question. + * + * @param int $questionid id of the question + * @param string $item stats item + * @return float|null + */ + private static function calculate_average_question_stats_item(int $questionid, string $item): ?float { + $quizzes = self::get_quizzes($questionid); + + $sum = 0; + $quizcount = count($quizzes); + foreach ($quizzes as $quizid) { + $value = self::load_question_stats_item($quizid, $questionid, $item); + if (!is_null($value)) { + $sum += $value; + } else { + // Exclude this value when it is null. + $quizcount--; + } + } + + // Return null if there is no quizzes. + if (empty($quizcount)) { + return null; + } + + // Average value per quiz. + $average = $sum / $quizcount; + return $average; + } + + /** + * Calculate average facility index + * + * @param int $questionid + * @return float|null + */ + public static function calculate_average_question_facility(int $questionid): ?float { + return self::calculate_average_question_stats_item($questionid, 'facility'); + } + + /** + * Calculate average discriminative efficiency + * + * @param int $questionid question id + * @return float|null + */ + public static function calculate_average_question_discriminative_efficiency(int $questionid): ?float { + return self::calculate_average_question_stats_item($questionid, 'discriminativeefficiency'); + } + + /** + * Calculate average discriminative efficiency + * + * @param int $questionid question id + * @return float|null + */ + public static function calculate_average_question_discrimination_index(int $questionid): ?float { + return self::calculate_average_question_stats_item($questionid, 'discriminationindex'); + } + + /** + * Format a number to a localised percentage with specified decimal points. + * + * @param float|null $number The number being formatted + * @param bool $fraction An indicator for whether the number is a fraction or is already multiplied by 100 + * @param int $decimals Sets the number of decimal points + * @return string + * @throws \coding_exception + */ + public static function format_percentage(?float $number, bool $fraction = true, int $decimals = 2): string { + if (is_null($number)) { + return get_string('na', 'qbank_statistics'); + } + $coefficient = $fraction ? 100 : 1; + return get_string('percents', 'moodle', format_float($number * $coefficient, $decimals)); + } + + /** + * Format discrimination index (need for revision). + * + * @param float|null $value stats value + * @return array + */ + public static function format_discrimination_index(?float $value): array { + if (is_null($value) || $value < self::NEED_FOR_REVISION_LOWER_THRESHOLD) { + $content = get_string('verylikely', 'qbank_statistics'); + $classes = 'alert-danger'; + } else if ($value < self::NEED_FOR_REVISION_UPPER_THRESHOLD) { + $content = get_string('likely', 'qbank_statistics'); + $classes = 'alert-warning'; + } else { + $content = get_string('unlikely', 'qbank_statistics'); + $classes = 'alert-success'; + } + return [$content, $classes]; + } +} diff --git a/question/bank/statistics/classes/output/renderer.php b/question/bank/statistics/classes/output/renderer.php new file mode 100644 index 00000000000..9e14ff7659f --- /dev/null +++ b/question/bank/statistics/classes/output/renderer.php @@ -0,0 +1,64 @@ +. + +namespace qbank_statistics\output; + +use qbank_statistics\helper; +/** + * Description + * + * @package qbank_statistics + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Nathan Nguyen + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer extends \plugin_renderer_base { + + /** + * Render facility index column. + * + * @param float|null $facility facility index + * @return string + */ + public function render_facility_index(?float $facility): string { + $displaydata['facility_index'] = helper::format_percentage($facility); + return $this->render_from_template('qbank_statistics/facility_index', $displaydata); + } + + /** + * Render discriminative_efficiency column. + * + * @param float|null $discriminativeefficiency discriminative efficiency + * @return string + */ + public function render_discriminative_efficiency(?float $discriminativeefficiency): string { + $displaydata['discriminative_efficiency'] = helper::format_percentage($discriminativeefficiency, false); + return $this->render_from_template('qbank_statistics/discriminative_efficiency', $displaydata); + } + + /** + * Render discrimination index column. + * + * @param float|null $discriminationindex discrimination index + * @return string + */ + public function render_discrimination_index(?float $discriminationindex): string { + list($content, $classes) = helper::format_discrimination_index($discriminationindex); + $displaydata['discrimination_index'] = $content; + $displaydata['classes'] = $classes; + return $this->render_from_template('qbank_statistics/discrimination_index', $displaydata); + } +} diff --git a/question/bank/statistics/classes/plugin_feature.php b/question/bank/statistics/classes/plugin_feature.php new file mode 100644 index 00000000000..8f97436bb72 --- /dev/null +++ b/question/bank/statistics/classes/plugin_feature.php @@ -0,0 +1,45 @@ +. + +namespace qbank_statistics; + +use qbank_statistics\columns\facility_index; +use qbank_statistics\columns\discrimination_index; +use qbank_statistics\columns\discriminative_efficiency; +/** + * Class plugin_features is the entrypoint for the columns. + * + * @package qbank_statistics + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Nathan Nguyen + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class plugin_feature extends \core_question\local\bank\plugin_features_base { + + /** + * This method will return the array of objects to be rendered as a prt of question bank columns/actions. + * + * @param view $qbank + * @return array + */ + public function get_question_columns($qbank): array { + return [ + new discrimination_index($qbank), + new facility_index($qbank), + new discriminative_efficiency($qbank) + ]; + } +} diff --git a/question/bank/statistics/classes/privacy/provider.php b/question/bank/statistics/classes/privacy/provider.php new file mode 100644 index 00000000000..9f11fc2db3c --- /dev/null +++ b/question/bank/statistics/classes/privacy/provider.php @@ -0,0 +1,34 @@ +. + +namespace qbank_statistics\privacy; + +/** + * Privacy Subsystem implementation. + * + * @package qbank_statistics + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Nathan Nguyen + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + /** + * {@inheritdoc} + */ + public static function get_reason(): string { + return 'privacy:metadata'; + } +} diff --git a/question/bank/statistics/lang/en/qbank_statistics.php b/question/bank/statistics/lang/en/qbank_statistics.php new file mode 100644 index 00000000000..ef0f69e20bb --- /dev/null +++ b/question/bank/statistics/lang/en/qbank_statistics.php @@ -0,0 +1,40 @@ +. + +/** + * Strings for component qbank_statistics, language 'en' + * + * @package qbank_statistics + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Nathan Nguyen + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$string['pluginname'] = 'Question statistics'; +$string['privacy:metadata'] = 'Question statistics plugin does not store any user data.'; + +// Columns. +$string['facility_index'] = 'Facility index'; +$string['discriminative_efficiency'] = 'Discriminative efficiency'; +$string['discrimination_index'] = 'Need for revision'; + +// Text format. +$string['verylikely'] = 'very likely'; +$string['likely'] = 'likely'; +$string['unlikely'] = 'unlikely'; +$string['na'] = 'N/A'; diff --git a/question/bank/statistics/templates/discrimination_index.mustache b/question/bank/statistics/templates/discrimination_index.mustache new file mode 100644 index 00000000000..15133914ab6 --- /dev/null +++ b/question/bank/statistics/templates/discrimination_index.mustache @@ -0,0 +1,31 @@ +{{! + 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 . +}} +{{! + @template qbank_statistics/discrimination_index + Example context (json): + { + "statistics": [ + { + "discriminative_efficiency": "html content" + } + ] + } +}} + +
+ {{{discrimination_index}}} +
diff --git a/question/bank/statistics/templates/discriminative_efficiency.mustache b/question/bank/statistics/templates/discriminative_efficiency.mustache new file mode 100644 index 00000000000..bcff7d033d9 --- /dev/null +++ b/question/bank/statistics/templates/discriminative_efficiency.mustache @@ -0,0 +1,31 @@ +{{! + 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 . +}} +{{! + @template qbank_statistics/discriminative_efficiency + Example context (json): + { + "statistics": [ + { + "discriminative_efficiency": "html content" + } + ] + } +}} + +
+ {{{discriminative_efficiency}}} +
diff --git a/question/bank/statistics/templates/facility_index.mustache b/question/bank/statistics/templates/facility_index.mustache new file mode 100644 index 00000000000..8aed4b4f940 --- /dev/null +++ b/question/bank/statistics/templates/facility_index.mustache @@ -0,0 +1,31 @@ +{{! + 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 . +}} +{{! + @template qbank_statistics/facility_index + Example context (json): + { + "statistics": [ + { + "facility_index": "html content" + } + ] + } +}} + +
+ {{{facility_index}}} +
diff --git a/question/bank/statistics/tests/behat/statistics_action.feature b/question/bank/statistics/tests/behat/statistics_action.feature new file mode 100644 index 00000000000..a9cc033d460 --- /dev/null +++ b/question/bank/statistics/tests/behat/statistics_action.feature @@ -0,0 +1,35 @@ +@qbank @qbank_statistics +Feature: Use the qbank plugin manager page for statistics + In order to check the plugin behaviour with enable and disable + + Background: + Given the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "activities" exist: + | activity | name | course | idnumber | + | quiz | Test quiz | C1 | quiz1 | + And the following "question categories" exist: + | contextlevel | reference | name | + | Course | C1 | Test questions | + And the following "questions" exist: + | questioncategory | qtype | name | questiontext | + | Test questions | truefalse | First question | Answer the first question | + + Scenario: Enable/disable statistics columns from the base view + Given I log in as "admin" + When I navigate to "Plugins > Question bank plugins > Manage question bank plugins" in site administration + And I should see "Question statistics" + And I click on "Disable" "link" in the "Question statistics" "table_row" + And I am on the "Test quiz" "quiz activity" page + And I navigate to "Question bank > Questions" in current page administration + Then I should not see "Facility index" + And I should not see "Discriminative efficiency" + And I should not see "Need for revision" + And I navigate to "Plugins > Question bank plugins > Manage question bank plugins" in site administration + And I click on "Enable" "link" in the "Question statistics" "table_row" + And I am on the "Test quiz" "quiz activity" page + And I navigate to "Question bank > Questions" in current page administration + And I should see "Facility index" + And I should see "Discriminative efficiency" + And I should see "Need for revision" diff --git a/question/bank/statistics/tests/behat/statistics_values.feature b/question/bank/statistics/tests/behat/statistics_values.feature new file mode 100644 index 00000000000..26015d44e48 --- /dev/null +++ b/question/bank/statistics/tests/behat/statistics_values.feature @@ -0,0 +1,136 @@ +@qbank @qbank_statistics +Feature: Show statistics in question bank + + Background: + Given the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "users" exist: + | username | firstname | lastname | email | + | student1 | user1 | Student1 | student1@example.com | + | student2 | user2 | Student2 | student2@example.com | + | student3 | user3 | Student3 | student3@example.com | + | student4 | user4 | Student4 | student4@example.com | + And the following "course enrolments" exist: + | user | course | role | + | student1 | C1 | student | + | student2 | C1 | student | + And the following "question categories" exist: + | contextlevel | reference | name | + | Course | C1 | Test questions | + And the following "questions" exist: + | questioncategory | qtype | name | questiontext | + | Test questions | truefalse | TF1 | First question | + | Test questions | truefalse | TF2 | Second question | + | Test questions | truefalse | TF3 | Third question | + | Test questions | truefalse | TF4 | Fourth question | + And the following "activities" exist: + | activity | name | intro | course | idnumber | + | quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | + | quiz | Quiz 2 | Quiz 2 description | C1 | quiz2 | + And quiz "Quiz 1" contains the following questions: + | question | page | maxmark | + | TF1 | 1 | 1.0 | + | TF2 | 1 | 1.0 | + | TF3 | 1 | 1.0 | + | TF4 | 1 | 1.0 | + And quiz "Quiz 2" contains the following questions: + | question | page | maxmark | + | TF2 | 1 | 1.0 | + | TF3 | 1 | 1.0 | + And user "student1" has attempted "Quiz 1" with responses: + | slot | response | + | 1 | False | + | 2 | False | + | 3 | False | + | 4 | False | + And user "student2" has attempted "Quiz 1" with responses: + | slot | response | + | 1 | True | + | 2 | True | + | 3 | True | + | 4 | True | + And user "student3" has attempted "Quiz 1" with responses: + | slot | response | + | 1 | True | + | 2 | False | + | 3 | False | + | 4 | True | + And user "student4" has attempted "Quiz 1" with responses: + | slot | response | + | 1 | False | + | 2 | True | + | 3 | True | + | 4 | False | + + Scenario: View facility index in question bank + Given user "student1" has attempted "Quiz 2" with responses: + | slot | response | + | 1 | True | + | 2 | True | + And user "student2" has attempted "Quiz 2" with responses: + | slot | response | + | 1 | True | + | 2 | True | + When I am on the "Course 1" course page logged in as admin + And I navigate to "Question bank > Questions" in current page administration + And I should see "50.00%" in the "TF1" "table_row" + And I should see "75.00%" in the "TF2" "table_row" + And I should see "75.00%" in the "TF3" "table_row" + And I should see "50.00%" in the "TF4" "table_row" + + Scenario: View discriminative efficiency in question bank + Given user "student1" has attempted "Quiz 2" with responses: + | slot | response | + | 1 | False | + | 2 | False | + And user "student2" has attempted "Quiz 2" with responses: + | slot | response | + | 1 | True | + | 2 | True | + When I am on the "Course 1" course page logged in as admin + And I navigate to "Question bank > Questions" in current page administration + And I should see "50.00%" in the "TF1" "table_row" + And I should see "75.00%" in the "TF2" "table_row" + And I should see "75.00%" in the "TF3" "table_row" + And I should see "50.00%" in the "TF4" "table_row" + + Scenario: View discrimination index in question bank, good questions + Given user "student1" has attempted "Quiz 2" with responses: + | slot | response | + | 1 | False | + | 2 | False | + And user "student2" has attempted "Quiz 2" with responses: + | slot | response | + | 1 | True | + | 2 | True | + When I am on the "Course 1" course page logged in as admin + And I navigate to "Question bank > Questions" in current page administration + And I should see "likely" in the "TF1" "table_row" + And I should see "unlikely" in the "TF2" "table_row" + And I should see "unlikely" in the "TF3" "table_row" + And I should see "likely" in the "TF4" "table_row" + And I should see "likely" in the ".alert-warning" "css_element" + And I should see "unlikely" in the ".alert-success" "css_element" + + Scenario: View discrimination index in question bank, bad questions + Given user "student1" has attempted "Quiz 2" with responses: + | slot | response | + | 1 | True | + | 2 | True | + And user "student2" has attempted "Quiz 2" with responses: + | slot | response | + | 1 | False | + | 2 | True | + And user "student3" has attempted "Quiz 2" with responses: + | slot | response | + | 1 | True | + | 2 | False | + When I am on the "Course 1" course page logged in as admin + And I navigate to "Question bank > Questions" in current page administration + And I should see "likely" in the "TF1" "table_row" + And I should see "very likely" in the "TF2" "table_row" + And I should see "very likely" in the "TF3" "table_row" + And I should see "likely" in the "TF4" "table_row" + And I should see "very likely" in the ".alert-danger" "css_element" + And I should see "likely" in the ".alert-warning" "css_element" diff --git a/question/bank/statistics/tests/helper_test.php b/question/bank/statistics/tests/helper_test.php new file mode 100644 index 00000000000..7dbe71a897f --- /dev/null +++ b/question/bank/statistics/tests/helper_test.php @@ -0,0 +1,538 @@ +. + +namespace qbank_statistics; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/mod/quiz/locallib.php'); +use quiz; +use question_engine; +use quiz_attempt; +/** + * Tests for question statistics. + * + * @package qbank_statistics + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Nathan Nguyen + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class helper_test extends \advanced_testcase { + + /** + * Test quizzes that contain a specified question. + * + * @throws \coding_exception + * @throws \dml_exception + */ + public function test_get_quizziess(): void { + global $DB; + $this->resetAfterTest(); + $this->setAdminUser(); + // Create a course. + $course = $this->getDataGenerator()->create_course(); + + // Create quizzes. + $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); + $quiz1 = $quizgenerator->create_instance([ + 'course' => $course->id, + 'grade' => 100.0, 'sumgrades' => 2, + 'layout' => '1,2,0' + ]); + $quiz2 = $quizgenerator->create_instance([ + 'course' => $course->id, + 'grade' => 100.0, 'sumgrades' => 2, + 'layout' => '1,2,0' + ]); + $quiz3 = $quizgenerator->create_instance([ + 'course' => $course->id, + 'grade' => 100.0, 'sumgrades' => 2, + 'layout' => '1,2,0' + ]); + $this->assertEquals(3, $DB->count_records('quiz')); + + // Create questions. + $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); + $cat = $questiongenerator->create_question_category(); + $question1 = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id)); + $question2 = $questiongenerator->create_question('numerical', null, array('category' => $cat->id)); + + // Add question 1 to quiz 1, 2. + quiz_add_quiz_question($question1->id, $quiz1); + quiz_add_quiz_question($question1->id, $quiz2); + // Quiz 1 attempt. + $attempt = ['answer' => 'frog', 'answer' => 10]; + $this->submit_quiz($quiz1, $attempt); + + // Add question 2 to quiz 2. + quiz_add_quiz_question($question2->id, $quiz2); + $this->submit_quiz($quiz2, $attempt); + + // Checking quizzes that use question 1. + $question1quizzes = helper::get_quizzes($question1->id); + $this->assertCount(2, $question1quizzes); + $this->assertContains($quiz1->id, $question1quizzes); + $this->assertContains($quiz2->id, $question1quizzes); + + // Checking quizzes that contain question 2. + $question2quizzes = helper::get_quizzes($question2->id); + $this->assertCount(1, $question2quizzes); + $this->assertContains($quiz2->id, $question2quizzes); + + // Add random question to quiz3. + quiz_add_random_questions($quiz3, 0, $cat->id, 1, false); + $this->submit_quiz($quiz3, $attempt); + // Quiz 3 will be in one of these arrays. + $question1quizzes = helper::get_quizzes($question1->id); + $question2quizzes = helper::get_quizzes($question2->id); + $this->assertContains($quiz3->id, array_merge($question1quizzes, $question2quizzes)); + } + + /** + * Load facility for a question + * + * @param object $quiz quiz object + * @param int $questionid question id + * @return float|int + */ + private function load_question_facility(object $quiz, int $questionid): ?float { + return helper::load_question_stats_item($quiz->id, $questionid, 'facility'); + } + + /** + * Load discriminative efficiency for a question + * + * @param object $quiz quiz object + * @param int $questionid question id + * @return float|int + */ + private function load_question_discriminative_efficiency(object $quiz, int $questionid): ?float { + return helper::load_question_stats_item($quiz->id, $questionid, 'discriminativeefficiency'); + } + + /** + * Load discrimination index for a question + * + * @param object $quiz quiz object + * @param int $questionid question id + * @return float|int + */ + private function load_question_discrimination_index(object $quiz, int $questionid): ?float { + return helper::load_question_stats_item($quiz->id, $questionid, 'discriminationindex'); + } + + /** + * Create 2 quizzes. + * + * @return array return 2 quizzes + * @throws \coding_exception + */ + private function prepare_quizzes(): array { + // Create a course. + $course = $this->getDataGenerator()->create_course(); + + // Make 2 quizzes. + $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); + $layout = '1,2,0,3,4,0'; + $quiz1 = $quizgenerator->create_instance([ + 'course' => $course->id, + 'grade' => 100.0, 'sumgrades' => 2, + 'layout' => $layout + ]); + + $quiz2 = $quizgenerator->create_instance([ + 'course' => $course->id, + 'grade' => 100.0, 'sumgrades' => 2, + 'layout' => $layout + ]); + + $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); + $cat = $questiongenerator->create_question_category(); + + $page = 1; + $questions = []; + foreach (explode(',', $layout) as $slot) { + if ($slot == 0) { + $page += 1; + continue; + } + + $question = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]); + $questions[$slot] = $question; + quiz_add_quiz_question($question->id, $quiz1, $page); + quiz_add_quiz_question($question->id, $quiz2, $page); + } + + return [$quiz1, $quiz2, $questions]; + } + + /** + * Submit quiz answers + * + * @param object $quiz + * @param array $answers + * @throws \moodle_exception + */ + private function submit_quiz(object $quiz, array $answers): void { + // Create user. + $user = $this->getDataGenerator()->create_user(); + // Create attempt. + $quizobj = quiz::create($quiz->id, $user->id); + $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); + $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); + $timenow = time(); + $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $user->id); + quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow); + quiz_attempt_save_started($quizobj, $quba, $attempt); + // Submit attempt. + $attemptobj = quiz_attempt::create($attempt->id); + $attemptobj->process_submitted_actions($timenow, false, $answers); + $attemptobj->process_finish($timenow, false); + } + + /** + * Generate attempt answers. + * + * @param array $correctanswerflags array of 1 or 0 + * 1 : generate correct answer + * 0 : generate wrong answer + * + * @return array + */ + private function generate_attempt_answers(array $correctanswerflags): array { + $attempt = []; + for ($i = 1; $i <= 4; $i++) { + if (isset($correctanswerflags) && $correctanswerflags[$i - 1] == 1) { + // Correct answer. + $attempt[$i] = ['answer' => 'frog']; + } else { + $attempt[$i] = ['answer' => 'false']; + } + } + return $attempt; + } + + /** + * + * Generate quizzes and submit answers. + * + * @param array $quiz1attempts quiz 1 attempts + * @param array $quiz2attempts quiz 2 attempts + * + * @return array + */ + private function prepare_and_submit_quizzes(array $quiz1attempts, array $quiz2attempts): array { + list($quiz1, $quiz2, $questions) = $this->prepare_quizzes(); + // Submit attempts of quiz1. + foreach ($quiz1attempts as $attempt) { + $this->submit_quiz($quiz1, $attempt); + } + // Submit attempts of quiz2. + foreach ($quiz2attempts as $attempt) { + $this->submit_quiz($quiz2, $attempt); + } + return [$quiz1, $quiz2, $questions]; + } + + /** + * Data provider for {@see test_load_question_facility()}. + * + * @return \Generator + */ + public function load_question_facility_provider(): \Generator { + yield 'Facility case 1' => [ + 'Quiz 1 attempts' => [ + $this->generate_attempt_answers([1, 0, 0, 0]), + ], + 'Expected quiz 1 facilities' => ['100.00%', '0.00%', '0.00%', '0.00%'], + 'Quiz 2 attempts' => [ + $this->generate_attempt_answers([1, 0, 0, 0]), + $this->generate_attempt_answers([1, 1, 0, 0]), + ], + 'Expected quiz 2 facilities' => ['100.00%', '50.00%', '0.00%', '0.00%'], + 'Expected average facilities' => ['100.00%', '25.00%', '0.00%', '0.00%'], + ]; + yield 'Facility case 2' => [ + 'Quiz 1 attempts' => [ + $this->generate_attempt_answers([1, 0, 0, 0]), + $this->generate_attempt_answers([1, 1, 0, 0]), + $this->generate_attempt_answers([1, 1, 1, 0]), + ], + 'Expected quiz 1 facilities' => ['100.00%', '66.67%', '33.33%', '0.00%'], + 'Quiz 2 attempts' => [ + $this->generate_attempt_answers([1, 0, 0, 0]), + $this->generate_attempt_answers([1, 1, 0, 0]), + $this->generate_attempt_answers([1, 1, 1, 0]), + $this->generate_attempt_answers([1, 1, 1, 1]), + ], + 'Expected quiz 2 facilities' => ['100.00%', '75.00%', '50.00%', '25.00%'], + 'Expected average facilities' => ['100.00%', '70.83%', '41.67%', '12.50%'], + ]; + } + + /** + * Test question facility + * + * @dataProvider load_question_facility_provider + * + * @param array $quiz1attempts quiz 1 attempts + * @param array $expectedquiz1facilities expected quiz 1 facilities + * @param array $quiz2attempts quiz 2 attempts + * @param array $expectedquiz2facilities expected quiz 2 facilities + * @param array $expectedaveragefacilities expected average facilities + */ + public function test_load_question_facility(array $quiz1attempts, array $expectedquiz1facilities, + array $quiz2attempts, array $expectedquiz2facilities, + array $expectedaveragefacilities): void { + $this->resetAfterTest(); + + list($quiz1, $quiz2, $questions) = $this->prepare_and_submit_quizzes($quiz1attempts, $quiz2attempts); + + // Quiz 1 facilities. + $quiz1facility1 = $this->load_question_facility($quiz1, $questions[1]->id); + $quiz1facility2 = $this->load_question_facility($quiz1, $questions[2]->id); + $quiz1facility3 = $this->load_question_facility($quiz1, $questions[3]->id); + $quiz1facility4 = $this->load_question_facility($quiz1, $questions[4]->id); + + $this->assertEquals($expectedquiz1facilities[0], helper::format_percentage($quiz1facility1)); + $this->assertEquals($expectedquiz1facilities[1], helper::format_percentage($quiz1facility2)); + $this->assertEquals($expectedquiz1facilities[2], helper::format_percentage($quiz1facility3)); + $this->assertEquals($expectedquiz1facilities[3], helper::format_percentage($quiz1facility4)); + + // Quiz 2 facilities. + $quiz2facility1 = $this->load_question_facility($quiz2, $questions[1]->id); + $quiz2facility2 = $this->load_question_facility($quiz2, $questions[2]->id); + $quiz2facility3 = $this->load_question_facility($quiz2, $questions[3]->id); + $quiz2facility4 = $this->load_question_facility($quiz2, $questions[4]->id); + + $this->assertEquals($expectedquiz2facilities[0], helper::format_percentage($quiz2facility1)); + $this->assertEquals($expectedquiz2facilities[1], helper::format_percentage($quiz2facility2)); + $this->assertEquals($expectedquiz2facilities[2], helper::format_percentage($quiz2facility3)); + $this->assertEquals($expectedquiz2facilities[3], helper::format_percentage($quiz2facility4)); + + // Average question facilities. + $averagefacility1 = helper::calculate_average_question_facility($questions[1]->id); + $averagefacility2 = helper::calculate_average_question_facility($questions[2]->id); + $averagefacility3 = helper::calculate_average_question_facility($questions[3]->id); + $averagefacility4 = helper::calculate_average_question_facility($questions[4]->id); + + $this->assertEquals($expectedaveragefacilities[0], helper::format_percentage($averagefacility1)); + $this->assertEquals($expectedaveragefacilities[1], helper::format_percentage($averagefacility2)); + $this->assertEquals($expectedaveragefacilities[2], helper::format_percentage($averagefacility3)); + $this->assertEquals($expectedaveragefacilities[3], helper::format_percentage($averagefacility4)); + } + + /** + * Data provider for {@see test_load_question_discriminative_efficiency()}. + * @return \Generator + */ + public function load_question_discriminative_efficiency_provider() { + yield 'Discriminative efficiency' => [ + 'Quiz 1 attempts' => [ + $this->generate_attempt_answers([1, 0, 0, 0]), + $this->generate_attempt_answers([1, 1, 0, 0]), + $this->generate_attempt_answers([1, 0, 1, 0]), + $this->generate_attempt_answers([1, 1, 1, 1]), + ], + 'Expected quiz 1 discriminative efficiency' => ['N/A', '33.33%', '33.33%', '100.00%'], + 'Quiz 2 attempts' => [ + $this->generate_attempt_answers([1, 1, 1, 1]), + $this->generate_attempt_answers([0, 0, 0, 0]), + $this->generate_attempt_answers([1, 0, 0, 1]), + $this->generate_attempt_answers([0, 1, 1, 0]), + ], + 'Expected quiz 2 discriminative efficiency' => ['50.00%', '50.00%', '50.00%', '50.00%'], + 'Expected average discriminative efficiency' => ['50.00%', '41.67%', '41.67%', '75.00%'], + ]; + } + + /** + * Test discriminative efficiency + * + * @dataProvider load_question_discriminative_efficiency_provider + * + * @param array $quiz1attempts quiz 1 attempts + * @param array $expectedquiz1discriminativeefficiency expected quiz 1 discriminative efficiency + * @param array $quiz2attempts quiz 2 attempts + * @param array $expectedquiz2discriminativeefficiency expected quiz 2 discriminative efficiency + * @param array $expectedaveragediscriminativeefficiency expected average discriminative efficiency + */ + public function test_load_question_discriminative_efficiency(array $quiz1attempts, array $expectedquiz1discriminativeefficiency, + array $quiz2attempts, array $expectedquiz2discriminativeefficiency, + array $expectedaveragediscriminativeefficiency): void { + $this->resetAfterTest(); + + list($quiz1, $quiz2, $questions) = $this->prepare_and_submit_quizzes($quiz1attempts, $quiz2attempts); + + // Quiz 1 discriminative efficiency. + $discriminativeefficiency1 = $this->load_question_discriminative_efficiency($quiz1, $questions[1]->id); + $discriminativeefficiency2 = $this->load_question_discriminative_efficiency($quiz1, $questions[2]->id); + $discriminativeefficiency3 = $this->load_question_discriminative_efficiency($quiz1, $questions[3]->id); + $discriminativeefficiency4 = $this->load_question_discriminative_efficiency($quiz1, $questions[4]->id); + + $this->assertEquals($expectedquiz1discriminativeefficiency[0], + helper::format_percentage($discriminativeefficiency1, false), + "Failure in quiz 1 - question 1 discriminative efficiency"); + $this->assertEquals($expectedquiz1discriminativeefficiency[1], + helper::format_percentage($discriminativeefficiency2, false), + "Failure in quiz 1 - question 2 discriminative efficiency"); + $this->assertEquals($expectedquiz1discriminativeefficiency[2], + helper::format_percentage($discriminativeefficiency3, false), + "Failure in quiz 1 - question 3 discriminative efficiency"); + $this->assertEquals($expectedquiz1discriminativeefficiency[3], + helper::format_percentage($discriminativeefficiency4, false), + "Failure in quiz 1 - question 4 discriminative efficiency"); + + // Quiz 2 discriminative efficiency. + $discriminativeefficiency1 = $this->load_question_discriminative_efficiency($quiz2, $questions[1]->id); + $discriminativeefficiency2 = $this->load_question_discriminative_efficiency($quiz2, $questions[2]->id); + $discriminativeefficiency3 = $this->load_question_discriminative_efficiency($quiz2, $questions[3]->id); + $discriminativeefficiency4 = $this->load_question_discriminative_efficiency($quiz2, $questions[4]->id); + + $this->assertEquals($expectedquiz2discriminativeefficiency[0], + helper::format_percentage($discriminativeefficiency1, false), + "Failure in quiz 2 - question 1 discriminative efficiency"); + $this->assertEquals($expectedquiz2discriminativeefficiency[1], + helper::format_percentage($discriminativeefficiency2, false), + "Failure in quiz 2 - question 2 discriminative efficiency"); + $this->assertEquals($expectedquiz2discriminativeefficiency[2], + helper::format_percentage($discriminativeefficiency3, false), + "Failure in quiz 2 - question 3 discriminative efficiency"); + $this->assertEquals($expectedquiz2discriminativeefficiency[3], + helper::format_percentage($discriminativeefficiency4, false), + "Failure in quiz 2 - question 4 discriminative efficiency"); + + // Average question discriminative efficiency. + $avgdiscriminativeefficiency1 = helper::calculate_average_question_discriminative_efficiency($questions[1]->id); + $avgdiscriminativeefficiency2 = helper::calculate_average_question_discriminative_efficiency($questions[2]->id); + $avgdiscriminativeefficiency3 = helper::calculate_average_question_discriminative_efficiency($questions[3]->id); + $avgdiscriminativeefficiency4 = helper::calculate_average_question_discriminative_efficiency($questions[4]->id); + + $this->assertEquals($expectedaveragediscriminativeefficiency[0], + helper::format_percentage($avgdiscriminativeefficiency1, false), + "Failure in question 1 average discriminative efficiency"); + $this->assertEquals($expectedaveragediscriminativeefficiency[1], + helper::format_percentage($avgdiscriminativeefficiency2, false), + "Failure in question 2 average discriminative efficiency"); + $this->assertEquals($expectedaveragediscriminativeefficiency[2], + helper::format_percentage($avgdiscriminativeefficiency3, false), + "Failure in question 3 average discriminative efficiency"); + $this->assertEquals($expectedaveragediscriminativeefficiency[3], + helper::format_percentage($avgdiscriminativeefficiency4, false), + "Failure in question 4 average discriminative efficiency"); + } + + /** + * Data provider for {@see test_load_question_discrimination_index()}. + * @return \Generator + */ + public function load_question_discrimination_index_provider() { + yield 'Discrimination Index' => [ + 'Quiz 1 attempts' => [ + $this->generate_attempt_answers([1, 0, 0, 0]), + $this->generate_attempt_answers([1, 1, 0, 0]), + $this->generate_attempt_answers([1, 0, 1, 0]), + $this->generate_attempt_answers([1, 1, 1, 1]), + ], + 'Expected quiz 1 Discrimination Index' => ['N/A', '30.15%', '30.15%', '81.65%'], + 'Quiz 2 attempts' => [ + $this->generate_attempt_answers([1, 1, 1, 1]), + $this->generate_attempt_answers([0, 0, 0, 0]), + $this->generate_attempt_answers([1, 0, 0, 1]), + $this->generate_attempt_answers([0, 1, 1, 0]), + ], + 'Expected quiz 2 discrimination Index' => ['44.72%', '44.72%', '44.72%', '44.72%'], + 'Expected average discrimination Index' => ['44.72%', '37.44%', '37.44%', '63.19%'], + ]; + } + + /** + * Test discrimination index + * + * @dataProvider load_question_discrimination_index_provider + * + * @param array $quiz1attempts quiz 1 attempts + * @param array $expectedquiz1discriminationindex expected quiz 1 discrimination index + * @param array $quiz2attempts quiz 2 attempts + * @param array $expectedquiz2discriminationindex expected quiz 2 discrimination index + * @param array $expectedaveragediscriminationindex expected average discrimination index + */ + public function test_load_question_discrimination_index(array $quiz1attempts, array $expectedquiz1discriminationindex, + array $quiz2attempts, array $expectedquiz2discriminationindex, + array $expectedaveragediscriminationindex): void { + $this->resetAfterTest(); + + list($quiz1, $quiz2, $questions) = $this->prepare_and_submit_quizzes($quiz1attempts, $quiz2attempts); + + // Quiz 1 discrimination index. + $discriminationindex1 = $this->load_question_discrimination_index($quiz1, $questions[1]->id); + $discriminationindex2 = $this->load_question_discrimination_index($quiz1, $questions[2]->id); + $discriminationindex3 = $this->load_question_discrimination_index($quiz1, $questions[3]->id); + $discriminationindex4 = $this->load_question_discrimination_index($quiz1, $questions[4]->id); + + $this->assertEquals($expectedquiz1discriminationindex[0], + helper::format_percentage($discriminationindex1, false), + "Failure in quiz 1 - question 1 discrimination index"); + $this->assertEquals($expectedquiz1discriminationindex[1], + helper::format_percentage($discriminationindex2, false), + "Failure in quiz 1 - question 2 discrimination index"); + $this->assertEquals($expectedquiz1discriminationindex[2], + helper::format_percentage($discriminationindex3, false), + "Failure in quiz 1 - question 3 discrimination index"); + $this->assertEquals($expectedquiz1discriminationindex[3], + helper::format_percentage($discriminationindex4, false), + "Failure in quiz 1 - question 4 discrimination index"); + + // Quiz 2 discrimination index. + $discriminationindex1 = $this->load_question_discrimination_index($quiz2, $questions[1]->id); + $discriminationindex2 = $this->load_question_discrimination_index($quiz2, $questions[2]->id); + $discriminationindex3 = $this->load_question_discrimination_index($quiz2, $questions[3]->id); + $discriminationindex4 = $this->load_question_discrimination_index($quiz2, $questions[4]->id); + + $this->assertEquals($expectedquiz2discriminationindex[0], + helper::format_percentage($discriminationindex1, false), + "Failure in quiz 2 - question 1 discrimination index"); + $this->assertEquals($expectedquiz2discriminationindex[1], + helper::format_percentage($discriminationindex2, false), + "Failure in quiz 2 - question 2 discrimination index"); + $this->assertEquals($expectedquiz2discriminationindex[2], + helper::format_percentage($discriminationindex3, false), + "Failure in quiz 2 - question 3 discrimination index"); + $this->assertEquals($expectedquiz2discriminationindex[3], + helper::format_percentage($discriminationindex4, false), + "Failure in quiz 2 - question 4 discrimination index"); + + // Average question discrimination index. + $avgdiscriminationindex1 = helper::calculate_average_question_discrimination_index($questions[1]->id); + $avgdiscriminationindex2 = helper::calculate_average_question_discrimination_index($questions[2]->id); + $avgdiscriminationindex3 = helper::calculate_average_question_discrimination_index($questions[3]->id); + $avgdiscriminationindex4 = helper::calculate_average_question_discrimination_index($questions[4]->id); + + $this->assertEquals($expectedaveragediscriminationindex[0], + helper::format_percentage($avgdiscriminationindex1, false), + "Failure in question 1 average discrimination index"); + $this->assertEquals($expectedaveragediscriminationindex[1], + helper::format_percentage($avgdiscriminationindex2, false), + "Failure in question 2 average discrimination index"); + $this->assertEquals($expectedaveragediscriminationindex[2], + helper::format_percentage($avgdiscriminationindex3, false), + "Failure in question 3 average discrimination index"); + $this->assertEquals($expectedaveragediscriminationindex[3], + helper::format_percentage($avgdiscriminationindex4, false), + "Failure in question 4 average discrimination index"); + } +} diff --git a/question/bank/statistics/version.php b/question/bank/statistics/version.php new file mode 100644 index 00000000000..eee106f59e0 --- /dev/null +++ b/question/bank/statistics/version.php @@ -0,0 +1,31 @@ +. + +/** + * Question statistics + * + * @package qbank_statistics + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Nathan Nguyen + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->component = 'qbank_statistics'; +$plugin->version = 2021092000; +$plugin->requires = 2021052500; +$plugin->maturity = MATURITY_STABLE;