MDL-75576 question bank: bulk-load statistics for all questions at once
There is a new method on the column class, for the column to declare any statistics it requires. The question then efficiently loads all those statistics in one go, and makes them available as the columns are rendererd, or do any subsequent processing.
This commit is contained in:
@@ -17,7 +17,6 @@
|
||||
namespace qbank_statistics\columns;
|
||||
|
||||
use core_question\local\bank\column_base;
|
||||
use qbank_statistics\helper;
|
||||
|
||||
/**
|
||||
* This columns shows a message about whether this question is OK or needs revision.
|
||||
@@ -43,10 +42,14 @@ class discrimination_index extends column_base {
|
||||
return 'discrimination_index';
|
||||
}
|
||||
|
||||
public function get_required_statistics_fields(): array {
|
||||
return ['discriminationindex'];
|
||||
}
|
||||
|
||||
protected function display_content($question, $rowclasses) {
|
||||
global $PAGE;
|
||||
// Average discrimination index per quiz.
|
||||
$discriminationindex = helper::calculate_average_question_discrimination_index($question->id);
|
||||
|
||||
$discriminationindex = $this->qbank->get_aggregate_statistic($question->id, 'discriminationindex');
|
||||
echo $PAGE->get_renderer('qbank_statistics')->render_discrimination_index($discriminationindex);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
namespace qbank_statistics\columns;
|
||||
|
||||
use core_question\local\bank\column_base;
|
||||
use qbank_statistics\helper;
|
||||
|
||||
/**
|
||||
* This column show the average discriminative efficiency for this question.
|
||||
@@ -41,10 +40,14 @@ class discriminative_efficiency extends column_base {
|
||||
return 'discriminative_efficiency';
|
||||
}
|
||||
|
||||
public function get_required_statistics_fields(): array {
|
||||
return ['discriminativeefficiency'];
|
||||
}
|
||||
|
||||
protected function display_content($question, $rowclasses) {
|
||||
global $PAGE;
|
||||
// Average discriminative efficiency per quiz.
|
||||
$discriminativeefficiency = helper::calculate_average_question_discriminative_efficiency($question->id);
|
||||
|
||||
$discriminativeefficiency = $this->qbank->get_aggregate_statistic($question->id, 'discriminativeefficiency');
|
||||
echo $PAGE->get_renderer('qbank_statistics')->render_discriminative_efficiency($discriminativeefficiency);
|
||||
}
|
||||
|
||||
|
||||
@@ -41,15 +41,18 @@ class facility_index extends column_base {
|
||||
return 'facility_index';
|
||||
}
|
||||
|
||||
public function get_required_statistics_fields(): array {
|
||||
return ['facility'];
|
||||
}
|
||||
|
||||
protected function display_content($question, $rowclasses) {
|
||||
global $PAGE;
|
||||
// Average facility index per quiz.
|
||||
$facility = helper::calculate_average_question_facility($question->id);
|
||||
|
||||
$facility = $this->qbank->get_aggregate_statistic($question->id, 'facility');
|
||||
echo $PAGE->get_renderer('qbank_statistics')->render_facility_index($facility);
|
||||
}
|
||||
|
||||
public function get_extra_classes(): array {
|
||||
return ['pr-3'];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ class helper {
|
||||
private const NEED_FOR_REVISION_UPPER_THRESHOLD = 50;
|
||||
|
||||
/**
|
||||
* For a list of questions find all the places (defined by (component, contextid) where there are attempts.
|
||||
* For a list of questions find all the places, defined by (component, contextid) where there are attempts.
|
||||
*
|
||||
* @param int[] $questionids array of question ids that we are interested in.
|
||||
* @return \stdClass[] list of objects with fields ->component and ->contextid.
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
This file describes core qbank plugin changes in /question/bank/*,
|
||||
information provided here is intended especially for developers.
|
||||
|
||||
=== 4.2.1 ===
|
||||
|
||||
* There is a new more effecient way to display statistics in the question bank, you should now override
|
||||
the get_required_statistics_fields() method in your column class, and then
|
||||
the values you need will be available from $this->qbank->get_aggregate_statistic(...).
|
||||
If you are not in a question_bank_column class, you can directly access efficient
|
||||
statistics-loading from the core_question\local\statistics\statistics_bulk_loader class.
|
||||
The old method will be deprecated in 4.3.
|
||||
|
||||
=== 4.1 ===
|
||||
|
||||
* New functions qbank_usage\helper::get_question_bank_usage_sql and
|
||||
qbank_usage\helper::get_question_attempt_usage_sql have been implemented.
|
||||
When calling a query with the SQL those methods returned, you have to be sure
|
||||
|
||||
@@ -325,6 +325,20 @@ abstract class column_base {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* If this column requires any aggregated statistics, it should declare that here.
|
||||
*
|
||||
* This is those statistics can be efficiently loaded in bulk.
|
||||
*
|
||||
* The statistics are all loaded just before load_additional_data is called on each column.
|
||||
* The values are then available from $this->qbank->get_aggregate_statistic(...);
|
||||
*
|
||||
* @return string[] the names of the required statistics fields. E.g. ['facility'].
|
||||
*/
|
||||
public function get_required_statistics_fields(): array {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* If this column needs extra data (e.g. tags) then load that here.
|
||||
*
|
||||
@@ -332,7 +346,7 @@ abstract class column_base {
|
||||
* Probably a good idea to check that another column has not already
|
||||
* loaded the data you want.
|
||||
*
|
||||
* @param \stdClass[] $questions the questions that will be displayed.
|
||||
* @param \stdClass[] $questions the questions that will be displayed, indexed by question id.
|
||||
*/
|
||||
public function load_additional_data(array $questions) {
|
||||
}
|
||||
|
||||
@@ -24,14 +24,15 @@
|
||||
|
||||
namespace core_question\local\bank;
|
||||
|
||||
use core_plugin_manager;
|
||||
use core_question\bank\search\condition;
|
||||
use core_question\local\statistics\statistics_bulk_loader;
|
||||
use qbank_columnsortorder\column_manager;
|
||||
use qbank_editquestion\editquestion_helper;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->dirroot . '/question/editlib.php');
|
||||
use core_plugin_manager;
|
||||
use core_question\bank\search\condition;
|
||||
use qbank_columnsortorder\column_manager;
|
||||
use qbank_editquestion\editquestion_helper;
|
||||
use qbank_managecategories\helper;
|
||||
|
||||
/**
|
||||
* This class prints a view of the question bank.
|
||||
@@ -90,19 +91,19 @@ class view {
|
||||
public $course;
|
||||
|
||||
/**
|
||||
* @var \question_bank_column_base[] these are all the 'columns' that are
|
||||
* @var column_base[] these are all the 'columns' that are
|
||||
* part of the display. Array keys are the class name.
|
||||
*/
|
||||
protected $requiredcolumns;
|
||||
|
||||
/**
|
||||
* @var \question_bank_column_base[] these are the 'columns' that are
|
||||
* @var column_base[] these are the 'columns' that are
|
||||
* actually displayed as a column, in order. Array keys are the class name.
|
||||
*/
|
||||
protected $visiblecolumns;
|
||||
|
||||
/**
|
||||
* @var \question_bank_column_base[] these are the 'columns' that are
|
||||
* @var column_base[] these are the 'columns' that are
|
||||
* actually displayed as an additional row (e.g. question text), in order.
|
||||
* Array keys are the class name.
|
||||
*/
|
||||
@@ -139,6 +140,15 @@ class view {
|
||||
*/
|
||||
protected $sqlparams;
|
||||
|
||||
/**
|
||||
* @var ?array Stores all the average statistics that this question bank view needs.
|
||||
*
|
||||
* This field gets initialised in {@see display_question_list()}. It is a two dimensional
|
||||
* $this->loadedstatistics[$questionid][$fieldname] = $average value of that statistics for that question.
|
||||
* Column classes in qbank plugins can access these values using {@see get_aggregate_statistic()}.
|
||||
*/
|
||||
protected $loadedstatistics = null;
|
||||
|
||||
/**
|
||||
* @var condition[] search conditions.
|
||||
*/
|
||||
@@ -979,6 +989,11 @@ class view {
|
||||
}
|
||||
}
|
||||
$questionsrs->close();
|
||||
|
||||
// Bulk load any required statistics.
|
||||
$this->load_required_statistics($questions);
|
||||
|
||||
// Bulk load any extra data that any column requires.
|
||||
foreach ($this->requiredcolumns as $name => $column) {
|
||||
$column->load_additional_data($questions);
|
||||
}
|
||||
@@ -1005,6 +1020,60 @@ class view {
|
||||
echo \html_writer::end_tag('form');
|
||||
}
|
||||
|
||||
/**
|
||||
* Work out the list of all the required statistics fields for this question bank view.
|
||||
*
|
||||
* This gathers all the required fields from all columns, so they can all be loaded at once.
|
||||
*
|
||||
* @return string[] the names of all the required fields for this question bank view.
|
||||
*/
|
||||
protected function determine_required_statistics(): array {
|
||||
$requiredfields = [];
|
||||
foreach ($this->requiredcolumns as $column) {
|
||||
$requiredfields = array_merge($requiredfields, $column->get_required_statistics_fields());
|
||||
}
|
||||
|
||||
return array_unique($requiredfields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the aggregate statistics that all the columns require.
|
||||
*
|
||||
* @param \stdClass[] $questions the questions that will be displayed indexed by question id.
|
||||
*/
|
||||
protected function load_required_statistics(array $questions): void {
|
||||
$requiredstatistics = $this->determine_required_statistics();
|
||||
$this->loadedstatistics = statistics_bulk_loader::load_aggregate_statistics(
|
||||
array_keys($questions), $requiredstatistics);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the aggregated value of a particular statistic for a particular question.
|
||||
*
|
||||
* You can only get values for the questions on the current page of the question bank view,
|
||||
* and only if you declared the need for this statistic in the get_required_statistics_fields()
|
||||
* method of your question bank column.
|
||||
*
|
||||
* @param int $questionid the id of a question
|
||||
* @param string $fieldname the name of a statistics field, e.g. 'facility'.
|
||||
* @return float|null the average (across all users) of this statistic for this question.
|
||||
* Null if the value is not available right now.
|
||||
*/
|
||||
public function get_aggregate_statistic(int $questionid, string $fieldname): ?float {
|
||||
if (!array_key_exists($questionid, $this->loadedstatistics)) {
|
||||
throw new \coding_exception('Question ' . $questionid . ' is not on the current page of ' .
|
||||
'this question bank view, so its statistics are not available.');
|
||||
}
|
||||
|
||||
// Must be array_key_exists, not isset, because we care about null values.
|
||||
if (!array_key_exists($fieldname, $this->loadedstatistics[$questionid])) {
|
||||
throw new \coding_exception('Statistics field ' . $fieldname . ' was not requested by any ' .
|
||||
'question bank column in this view, so it is not available.');
|
||||
}
|
||||
|
||||
return $this->loadedstatistics[$questionid][$fieldname];
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the top pagination bar.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
<?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 core_question\local\statistics;
|
||||
|
||||
use core_question\local\bank\column_base;
|
||||
use core_question\statistics\questions\all_calculated_for_qubaid_condition;
|
||||
use core_component;
|
||||
|
||||
/**
|
||||
* Helper to efficiently load all the statistics for a set of questions.
|
||||
*
|
||||
* If you are implementing a question bank column, do not use this method directly.
|
||||
* Instead, override the {@see column_base::get_required_statistics_fields()} method
|
||||
* in your column class, and the question bank view will take care of it for you.
|
||||
*
|
||||
* @package core_question
|
||||
* @copyright 2023 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class statistics_bulk_loader {
|
||||
|
||||
/**
|
||||
* Load and aggregate the requested statistics for all the places where the given questions are used.
|
||||
*
|
||||
* The returned array will contain a values for each questionid and field, which will be null if the value is not available.
|
||||
*
|
||||
* @param int[] $questionids array of question ids.
|
||||
* @param string[] $requiredstatistics array of the fields required, e.g. ['facility', 'discriminationindex'].
|
||||
* @return float[][] if a value is not available, it will be set to null.
|
||||
*/
|
||||
public static function load_aggregate_statistics(array $questionids, array $requiredstatistics): array {
|
||||
$places = self::get_all_places_where_questions_were_attempted($questionids);
|
||||
|
||||
// Set up blank two-dimensional arrays to store the running totals. Indexed by questionid and field name.
|
||||
$zerovaluesforonequestion = array_combine($requiredstatistics, array_fill(0, count($requiredstatistics), 0));
|
||||
$counts = array_combine($questionids, array_fill(0, count($questionids), $zerovaluesforonequestion));
|
||||
$sums = array_combine($questionids, array_fill(0, count($questionids), $zerovaluesforonequestion));
|
||||
|
||||
// Load the data for each place, and add to the running totals.
|
||||
foreach ($places as $place) {
|
||||
$statistics = self::load_statistics_for_place($place->component,
|
||||
\context::instance_by_id($place->contextid));
|
||||
if ($statistics === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($questionids as $questionid) {
|
||||
foreach ($requiredstatistics as $item) {
|
||||
$value = self::extract_item_value($statistics, $questionid, $item);
|
||||
if ($value === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$counts[$questionid][$item] += 1;
|
||||
$sums[$questionid][$item] += $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compute the averages from the final totals.
|
||||
$aggregates = [];
|
||||
foreach ($questionids as $questionid) {
|
||||
$aggregates[$questionid] = [];
|
||||
foreach ($requiredstatistics as $item) {
|
||||
if ($counts[$questionid][$item] > 0) {
|
||||
$aggregates[$questionid][$item] = $sums[$questionid][$item] / $counts[$questionid][$item];
|
||||
} else {
|
||||
$aggregates[$questionid][$item] = null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return $aggregates;
|
||||
}
|
||||
|
||||
/**
|
||||
* For a list of questions find all the places, defined by (component, contextid), where there are attempts.
|
||||
*
|
||||
* @param int[] $questionids array of question ids that we are interested in.
|
||||
* @return \stdClass[] list of objects with fields ->component and ->contextid.
|
||||
*/
|
||||
protected static function get_all_places_where_questions_were_attempted(array $questionids): array {
|
||||
global $DB;
|
||||
|
||||
[$questionidcondition, $params] = $DB->get_in_or_equal($questionids);
|
||||
// The MIN(qu.id) is just to ensure that the rows have a unique key.
|
||||
$places = $DB->get_records_sql("
|
||||
SELECT MIN(qu.id) AS somethingunique, qu.component, qu.contextid
|
||||
FROM {question_usages} qu
|
||||
JOIN {question_attempts} qatt ON qatt.questionusageid = qu.id
|
||||
WHERE qatt.questionid $questionidcondition
|
||||
GROUP BY qu.component, qu.contextid
|
||||
ORDER BY qu.contextid ASC
|
||||
", $params);
|
||||
|
||||
// Strip out the unwanted ids.
|
||||
$places = array_values($places);
|
||||
foreach ($places as $place) {
|
||||
unset($place->somethingunique);
|
||||
}
|
||||
|
||||
return $places;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the question statistics for all the attempts belonging to a particular component in a particular context.
|
||||
*
|
||||
* @param string $component frankenstyle component name, e.g. 'mod_quiz'.
|
||||
* @param \context $context the context to load the statistics for.
|
||||
* @return all_calculated_for_qubaid_condition|null question statistics.
|
||||
*/
|
||||
protected static function load_statistics_for_place(
|
||||
string $component,
|
||||
\context $context
|
||||
): ?all_calculated_for_qubaid_condition {
|
||||
// This check is basically if (component_exists).
|
||||
if (empty(core_component::get_component_directory($component))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!component_callback_exists($component, 'calculate_question_stats')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return component_callback($component, 'calculate_question_stats', [$context]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the value for one question and one type of statistic from a set of statistics.
|
||||
*
|
||||
* @param all_calculated_for_qubaid_condition $statistics the batch of statistics.
|
||||
* @param int $questionid a question id.
|
||||
* @param string $item one of the field names in all_calculated_for_qubaid_condition, e.g. 'facility'.
|
||||
* @return float|null the required value.
|
||||
*/
|
||||
protected static function extract_item_value(all_calculated_for_qubaid_condition $statistics,
|
||||
int $questionid, string $item): ?float {
|
||||
|
||||
// Look in main questions.
|
||||
foreach ($statistics->questionstats as $stats) {
|
||||
if ($stats->questionid == $questionid && isset($stats->$item)) {
|
||||
return $stats->$item;
|
||||
}
|
||||
}
|
||||
|
||||
// If not found, look in sub questions.
|
||||
foreach ($statistics->subquestionstats as $stats) {
|
||||
if ($stats->questionid == $questionid && isset($stats->$item)) {
|
||||
return $stats->$item;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,550 @@
|
||||
<?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 core_question\local\statistics;
|
||||
|
||||
use advanced_testcase;
|
||||
use context;
|
||||
use context_module;
|
||||
use core_question\statistics\questions\all_calculated_for_qubaid_condition;
|
||||
use core_question_generator;
|
||||
use Generator;
|
||||
use mod_quiz\quiz_settings;
|
||||
use mod_quiz\quiz_attempt;
|
||||
use question_engine;
|
||||
use ReflectionMethod;
|
||||
|
||||
/**
|
||||
* Tests for question statistics.
|
||||
*
|
||||
* @package core_question
|
||||
* @copyright 2021 Catalyst IT Australia Pty Ltd
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \core_question\local\statistics\statistics_bulk_loader
|
||||
*/
|
||||
class statistics_bulk_loader_test extends advanced_testcase {
|
||||
|
||||
/** @var float Delta used when comparing statistics values out-of 1. */
|
||||
protected const DELTA = 0.00005;
|
||||
|
||||
/** @var float Delta used when comparing statistics values out-of 100. */
|
||||
protected const PERCENT_DELTA = 0.005;
|
||||
|
||||
/**
|
||||
* Test quizzes that contain a specified question.
|
||||
*
|
||||
* @covers ::get_all_places_where_questions_were_attempted
|
||||
*/
|
||||
public function test_get_all_places_where_questions_were_attempted(): void {
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
$rcm = new ReflectionMethod(statistics_bulk_loader::class, 'get_all_places_where_questions_were_attempted');
|
||||
$rcm->setAccessible(true);
|
||||
|
||||
// Create a course.
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
|
||||
// Create three 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'
|
||||
]);
|
||||
$quiz1context = context_module::instance($quiz1->cmid);
|
||||
|
||||
$quiz2 = $quizgenerator->create_instance([
|
||||
'course' => $course->id,
|
||||
'grade' => 100.0, 'sumgrades' => 2,
|
||||
'layout' => '1,2,0'
|
||||
]);
|
||||
$quiz2context = context_module::instance($quiz2->cmid);
|
||||
|
||||
$quiz3 = $quizgenerator->create_instance([
|
||||
'course' => $course->id,
|
||||
'grade' => 100.0, 'sumgrades' => 2,
|
||||
'layout' => '1,2,0'
|
||||
]);
|
||||
$quiz3context = context_module::instance($quiz3->cmid);
|
||||
|
||||
// Create questions.
|
||||
/** @var core_question_generator $questiongenerator */
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$cat = $questiongenerator->create_question_category();
|
||||
$question1 = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
|
||||
$question2 = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
|
||||
|
||||
// Add question 1 to quiz 1 and make an attempt.
|
||||
quiz_add_quiz_question($question1->id, $quiz1);
|
||||
// Quiz 1 attempt.
|
||||
$this->submit_quiz($quiz1, [1 => ['answer' => 'frog']]);
|
||||
|
||||
// Add questions 1 and 2 to quiz 2.
|
||||
quiz_add_quiz_question($question1->id, $quiz2);
|
||||
quiz_add_quiz_question($question2->id, $quiz2);
|
||||
$this->submit_quiz($quiz2, [1 => ['answer' => 'frog'], 2 => ['answer' => 10]]);
|
||||
|
||||
// Checking quizzes that use question 1.
|
||||
$q1places = $rcm->invoke(null, [$question1->id]);
|
||||
$this->assertCount(2, $q1places);
|
||||
$this->assertEquals((object) ['component' => 'mod_quiz', 'contextid' => $quiz1context->id], $q1places[0]);
|
||||
$this->assertEquals((object) ['component' => 'mod_quiz', 'contextid' => $quiz2context->id], $q1places[1]);
|
||||
|
||||
// Checking quizzes that contain question 2.
|
||||
$q2places = $rcm->invoke(null, [$question2->id]);
|
||||
$this->assertCount(1, $q2places);
|
||||
$this->assertEquals((object) ['component' => 'mod_quiz', 'contextid' => $quiz2context->id], $q2places[0]);
|
||||
|
||||
// Add a random question to quiz3.
|
||||
quiz_add_random_questions($quiz3, 0, $cat->id, 1, false);
|
||||
$this->submit_quiz($quiz3, [1 => ['answer' => 'willbewrong']]);
|
||||
|
||||
// Quiz 3 will now be in one of these arrays.
|
||||
$q1places = $rcm->invoke(null, [$question1->id]);
|
||||
$q2places = $rcm->invoke(null, [$question2->id]);
|
||||
if (count($q1places) == 3) {
|
||||
$newplace = end($q1places);
|
||||
} else {
|
||||
$newplace = end($q2places);
|
||||
}
|
||||
$this->assertEquals((object) ['component' => 'mod_quiz', 'contextid' => $quiz3context->id], $newplace);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create 2 quizzes.
|
||||
*
|
||||
* @return array return 2 quizzes
|
||||
*/
|
||||
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
|
||||
]);
|
||||
|
||||
/** @var core_question_generator $questiongenerator */
|
||||
$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
|
||||
*/
|
||||
private function submit_quiz(object $quiz, array $answers): void {
|
||||
// Create user.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
// Create attempt.
|
||||
$quizobj = quiz_settings::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, null, $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];
|
||||
}
|
||||
|
||||
/**
|
||||
* To use private helper::extract_item_value function.
|
||||
*
|
||||
* @param all_calculated_for_qubaid_condition $statistics the batch of statistics.
|
||||
* @param int $questionid a question id.
|
||||
* @param string $item one of the field names in all_calculated_for_qubaid_condition, e.g. 'facility'.
|
||||
* @return float|null the required value.
|
||||
*/
|
||||
private function extract_item_value(all_calculated_for_qubaid_condition $statistics,
|
||||
int $questionid, string $item): ?float {
|
||||
$rcm = new ReflectionMethod(statistics_bulk_loader::class, 'extract_item_value');
|
||||
$rcm->setAccessible(true);
|
||||
return $rcm->invoke(null, $statistics, $questionid, $item);
|
||||
}
|
||||
|
||||
/**
|
||||
* To use private helper::load_statistics_for_place function (with mod_quiz component).
|
||||
*
|
||||
* @param context $context the context to load the statistics for.
|
||||
* @return all_calculated_for_qubaid_condition|null question statistics.
|
||||
*/
|
||||
private function load_quiz_statistics_for_place(context $context): ?all_calculated_for_qubaid_condition {
|
||||
$rcm = new ReflectionMethod(statistics_bulk_loader::class, 'load_statistics_for_place');
|
||||
$rcm->setAccessible(true);
|
||||
return $rcm->invoke(null, 'mod_quiz', $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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' => [1.0, 0.0, 0.0, 0.0],
|
||||
'Quiz 2 attempts' => [
|
||||
$this->generate_attempt_answers([1, 0, 0, 0]),
|
||||
$this->generate_attempt_answers([1, 1, 0, 0]),
|
||||
],
|
||||
'Expected quiz 2 facilities' => [1.0, 0.5, 0.0, 0.0],
|
||||
'Expected average facilities' => [1.0, 0.25, 0.0, 0.0],
|
||||
];
|
||||
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' => [1.0, 0.6667, 0.3333, 0.0],
|
||||
'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' => [1.0, 0.75, 0.5, 0.25],
|
||||
'Expected average facilities' => [1.0, 0.7083, 0.4167, 0.1250],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
$stats = $this->load_quiz_statistics_for_place(context_module::instance($quiz1->cmid));
|
||||
$quiz1facility1 = $this->extract_item_value($stats, $questions[1]->id, 'facility');
|
||||
$quiz1facility2 = $this->extract_item_value($stats, $questions[2]->id, 'facility');
|
||||
$quiz1facility3 = $this->extract_item_value($stats, $questions[3]->id, 'facility');
|
||||
$quiz1facility4 = $this->extract_item_value($stats, $questions[4]->id, 'facility');
|
||||
|
||||
$this->assertEqualsWithDelta($expectedquiz1facilities[0], $quiz1facility1, self::DELTA);
|
||||
$this->assertEqualsWithDelta($expectedquiz1facilities[1], $quiz1facility2, self::DELTA);
|
||||
$this->assertEqualsWithDelta($expectedquiz1facilities[2], $quiz1facility3, self::DELTA);
|
||||
$this->assertEqualsWithDelta($expectedquiz1facilities[3], $quiz1facility4, self::DELTA);
|
||||
|
||||
// Quiz 2 facilities.
|
||||
$stats = $this->load_quiz_statistics_for_place(context_module::instance($quiz2->cmid));
|
||||
$quiz2facility1 = $this->extract_item_value($stats, $questions[1]->id, 'facility');
|
||||
$quiz2facility2 = $this->extract_item_value($stats, $questions[2]->id, 'facility');
|
||||
$quiz2facility3 = $this->extract_item_value($stats, $questions[3]->id, 'facility');
|
||||
$quiz2facility4 = $this->extract_item_value($stats, $questions[4]->id, 'facility');
|
||||
|
||||
$this->assertEqualsWithDelta($expectedquiz2facilities[0], $quiz2facility1, self::DELTA);
|
||||
$this->assertEqualsWithDelta($expectedquiz2facilities[1], $quiz2facility2, self::DELTA);
|
||||
$this->assertEqualsWithDelta($expectedquiz2facilities[2], $quiz2facility3, self::DELTA);
|
||||
$this->assertEqualsWithDelta($expectedquiz2facilities[3], $quiz2facility4, self::DELTA);
|
||||
|
||||
// Average question facilities.
|
||||
$stats = statistics_bulk_loader::load_aggregate_statistics(
|
||||
[$questions[1]->id, $questions[2]->id, $questions[3]->id, $questions[4]->id],
|
||||
['facility']
|
||||
);
|
||||
|
||||
$this->assertEqualsWithDelta($expectedaveragefacilities[0],
|
||||
$stats[$questions[1]->id]['facility'], self::DELTA);
|
||||
$this->assertEqualsWithDelta($expectedaveragefacilities[1],
|
||||
$stats[$questions[2]->id]['facility'], self::DELTA);
|
||||
$this->assertEqualsWithDelta($expectedaveragefacilities[2],
|
||||
$stats[$questions[3]->id]['facility'], self::DELTA);
|
||||
$this->assertEqualsWithDelta($expectedaveragefacilities[3],
|
||||
$stats[$questions[4]->id]['facility'], self::DELTA);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for {@see test_load_question_discriminative_efficiency()}.
|
||||
* @return Generator
|
||||
*/
|
||||
public function load_question_discriminative_efficiency_provider(): Generator {
|
||||
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' => [null, 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.
|
||||
$stats = $this->load_quiz_statistics_for_place(context_module::instance($quiz1->cmid));
|
||||
$discriminativeefficiency1 = $this->extract_item_value($stats, $questions[1]->id, 'discriminativeefficiency');
|
||||
$discriminativeefficiency2 = $this->extract_item_value($stats, $questions[2]->id, 'discriminativeefficiency');
|
||||
$discriminativeefficiency3 = $this->extract_item_value($stats, $questions[3]->id, 'discriminativeefficiency');
|
||||
$discriminativeefficiency4 = $this->extract_item_value($stats, $questions[4]->id, 'discriminativeefficiency');
|
||||
|
||||
$this->assertEqualsWithDelta($expectedquiz1discriminativeefficiency[0],
|
||||
$discriminativeefficiency1, self::PERCENT_DELTA);
|
||||
$this->assertEqualsWithDelta($expectedquiz1discriminativeefficiency[1],
|
||||
$discriminativeefficiency2, self::PERCENT_DELTA);
|
||||
$this->assertEqualsWithDelta($expectedquiz1discriminativeefficiency[2],
|
||||
$discriminativeefficiency3, self::PERCENT_DELTA);
|
||||
$this->assertEqualsWithDelta($expectedquiz1discriminativeefficiency[3],
|
||||
$discriminativeefficiency4, self::PERCENT_DELTA);
|
||||
|
||||
// Quiz 2 discriminative efficiency.
|
||||
$stats = $this->load_quiz_statistics_for_place(context_module::instance($quiz2->cmid));
|
||||
$discriminativeefficiency1 = $this->extract_item_value($stats, $questions[1]->id, 'discriminativeefficiency');
|
||||
$discriminativeefficiency2 = $this->extract_item_value($stats, $questions[2]->id, 'discriminativeefficiency');
|
||||
$discriminativeefficiency3 = $this->extract_item_value($stats, $questions[3]->id, 'discriminativeefficiency');
|
||||
$discriminativeefficiency4 = $this->extract_item_value($stats, $questions[4]->id, 'discriminativeefficiency');
|
||||
|
||||
$this->assertEqualsWithDelta($expectedquiz2discriminativeefficiency[0],
|
||||
$discriminativeefficiency1, self::PERCENT_DELTA);
|
||||
$this->assertEqualsWithDelta($expectedquiz2discriminativeefficiency[1],
|
||||
$discriminativeefficiency2, self::PERCENT_DELTA);
|
||||
$this->assertEqualsWithDelta($expectedquiz2discriminativeefficiency[2],
|
||||
$discriminativeefficiency3, self::PERCENT_DELTA);
|
||||
$this->assertEqualsWithDelta($expectedquiz2discriminativeefficiency[3],
|
||||
$discriminativeefficiency4, self::PERCENT_DELTA);
|
||||
|
||||
// Average question discriminative efficiency.
|
||||
$stats = statistics_bulk_loader::load_aggregate_statistics(
|
||||
[$questions[1]->id, $questions[2]->id, $questions[3]->id, $questions[4]->id],
|
||||
['discriminativeefficiency']
|
||||
);
|
||||
|
||||
$this->assertEqualsWithDelta($expectedaveragediscriminativeefficiency[0],
|
||||
$stats[$questions[1]->id]['discriminativeefficiency'], self::PERCENT_DELTA);
|
||||
$this->assertEqualsWithDelta($expectedaveragediscriminativeefficiency[1],
|
||||
$stats[$questions[2]->id]['discriminativeefficiency'], self::PERCENT_DELTA);
|
||||
$this->assertEqualsWithDelta($expectedaveragediscriminativeefficiency[2],
|
||||
$stats[$questions[3]->id]['discriminativeefficiency'], self::PERCENT_DELTA);
|
||||
$this->assertEqualsWithDelta($expectedaveragediscriminativeefficiency[3],
|
||||
$stats[$questions[4]->id]['discriminativeefficiency'], self::PERCENT_DELTA);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for {@see test_load_question_discrimination_index()}.
|
||||
* @return Generator
|
||||
*/
|
||||
public function load_question_discrimination_index_provider(): Generator {
|
||||
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' => [null, 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.
|
||||
$stats = $this->load_quiz_statistics_for_place(context_module::instance($quiz1->cmid));
|
||||
$discriminationindex1 = $this->extract_item_value($stats, $questions[1]->id, 'discriminationindex');
|
||||
$discriminationindex2 = $this->extract_item_value($stats, $questions[2]->id, 'discriminationindex');
|
||||
$discriminationindex3 = $this->extract_item_value($stats, $questions[3]->id, 'discriminationindex');
|
||||
$discriminationindex4 = $this->extract_item_value($stats, $questions[4]->id, 'discriminationindex');
|
||||
|
||||
$this->assertEqualsWithDelta($expectedquiz1discriminationindex[0],
|
||||
$discriminationindex1, self::PERCENT_DELTA);
|
||||
$this->assertEqualsWithDelta($expectedquiz1discriminationindex[1],
|
||||
$discriminationindex2, self::PERCENT_DELTA);
|
||||
$this->assertEqualsWithDelta($expectedquiz1discriminationindex[2],
|
||||
$discriminationindex3, self::PERCENT_DELTA);
|
||||
$this->assertEqualsWithDelta($expectedquiz1discriminationindex[3],
|
||||
$discriminationindex4, self::PERCENT_DELTA);
|
||||
|
||||
// Quiz 2 discrimination index.
|
||||
$stats = $this->load_quiz_statistics_for_place(context_module::instance($quiz2->cmid));
|
||||
$discriminationindex1 = $this->extract_item_value($stats, $questions[1]->id, 'discriminationindex');
|
||||
$discriminationindex2 = $this->extract_item_value($stats, $questions[2]->id, 'discriminationindex');
|
||||
$discriminationindex3 = $this->extract_item_value($stats, $questions[3]->id, 'discriminationindex');
|
||||
$discriminationindex4 = $this->extract_item_value($stats, $questions[4]->id, 'discriminationindex');
|
||||
|
||||
$this->assertEqualsWithDelta($expectedquiz2discriminationindex[0],
|
||||
$discriminationindex1, self::PERCENT_DELTA);
|
||||
$this->assertEqualsWithDelta($expectedquiz2discriminationindex[1],
|
||||
$discriminationindex2, self::PERCENT_DELTA);
|
||||
$this->assertEqualsWithDelta($expectedquiz2discriminationindex[2],
|
||||
$discriminationindex3, self::PERCENT_DELTA);
|
||||
$this->assertEqualsWithDelta($expectedquiz2discriminationindex[3],
|
||||
$discriminationindex4, self::PERCENT_DELTA);
|
||||
|
||||
// Average question discrimination index.
|
||||
$stats = statistics_bulk_loader::load_aggregate_statistics(
|
||||
[$questions[1]->id, $questions[2]->id, $questions[3]->id, $questions[4]->id],
|
||||
['discriminationindex']
|
||||
);
|
||||
|
||||
$this->assertEqualsWithDelta($expectedaveragediscriminationindex[0],
|
||||
$stats[$questions[1]->id]['discriminationindex'], self::PERCENT_DELTA);
|
||||
$this->assertEqualsWithDelta($expectedaveragediscriminationindex[1],
|
||||
$stats[$questions[2]->id]['discriminationindex'], self::PERCENT_DELTA);
|
||||
$this->assertEqualsWithDelta($expectedaveragediscriminationindex[2],
|
||||
$stats[$questions[3]->id]['discriminationindex'], self::PERCENT_DELTA);
|
||||
$this->assertEqualsWithDelta($expectedaveragediscriminationindex[3],
|
||||
$stats[$questions[4]->id]['discriminationindex'], self::PERCENT_DELTA);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user