$string['paymentsorry'] = 'Thank you for your payment! Unfortunately your payment has not yet been fully processed, and you are not yet registered to enter the course "{$a->fullname}". Please try continuing to the course in a few seconds, but if you continue to have trouble then please alert the {$a->teacher} or the site administrator';
$string['paymentthanks'] = 'Thank you for your payment! You are now enrolled in your course:<br />"{$a}"';
$string['pendingrequests'] = 'Pending requests';
+$string['percents'] = '{$a} %';
$string['periodending'] = 'Period ending ({$a})';
$string['perpage'] = 'Per page';
$string['perpagea'] = 'Per page: {$a}';
return $DB->get_record('user', ['id' => $userid]);
}
+ /**
+ * Set current $USER, reset access cache.
+ *
+ * In some cases, behat will execute the code as admin but in many cases we need to set an specific user as some
+ * API's might rely on the logged user to take some action.
+ *
+ * @param null|int|stdClass $user user record, null or 0 means non-logged-in, positive integer means userid
+ */
+ public static function set_user($user = null) {
+ global $DB;
+
+ if (is_object($user)) {
+ $user = clone($user);
+ } else if (!$user) {
+ // Assign valid data to admin user (some generator-related code needs a valid user).
+ $user = $DB->get_record('user', array('username' => 'admin'));
+ } else {
+ $user = $DB->get_record('user', array('id' => $user));
+ }
+ unset($user->description);
+ unset($user->access);
+ unset($user->preference);
+
+ // Ensure session is empty, as it may contain caches and user specific info.
+ \core\session\manager::init_empty_session();
+
+ \core\session\manager::set_user($user);
+ }
/**
* Trigger click on node via javascript instead of actually clicking on it via pointer.
*
* @return question_usage_by_activity the usage.
*/
public function get_question_usage() {
- if (!PHPUNIT_TEST) {
+ if (!(PHPUNIT_TEST || defined('BEHAT_TEST'))) {
throw new coding_exception('get_question_usage is only for use in unit tests. ' .
'For other operations, use the quiz_attempt api, or extend it properly.');
}
* @param int $timestamp the timestamp that should be stored as the modifed
* time in the database for these actions. If null, will use the current time.
* @param bool $becomingoverdue
- * @param array|null $simulatedresponses If not null, then we are testing, and this is an array of simulated data, keys are slot
- * nos and values are arrays representing student responses which will be passed to
- * question_definition::prepare_simulated_post_data method and then have the
- * appropriate prefix added.
+ * @param array|null $simulatedresponses If not null, then we are testing, and this is an array of simulated data.
+ * There are two formats supported here, for historical reasons. The newer approach is to pass an array created by
+ * {@link core_question_generator::get_simulated_post_data_for_questions_in_usage()}.
+ * the second is to pass an array slot no => contains arrays representing student
+ * responses which will be passed to {@link question_definition::prepare_simulated_post_data()}.
+ * This second method will probably get deprecated one day.
*/
public function process_submitted_actions($timestamp, $becomingoverdue = false, $simulatedresponses = null) {
global $DB;
$transaction = $DB->start_delegated_transaction();
if ($simulatedresponses !== null) {
- $simulatedpostdata = $this->quba->prepare_simulated_post_data($simulatedresponses);
+ if (is_int(key($simulatedresponses))) {
+ // Legacy approach. Should be removed one day.
+ $simulatedpostdata = $this->quba->prepare_simulated_post_data($simulatedresponses);
+ } else {
+ $simulatedpostdata = $simulatedresponses;
+ }
} else {
$simulatedpostdata = null;
}
/**
* Prepare and start a new attempt deleting the previous preview attempts.
*
- * @param quiz $quizobj quiz object
- * @param int $attemptnumber the attempt number
- * @param object $lastattempt last attempt object
+ * @param quiz $quizobj quiz object
+ * @param int $attemptnumber the attempt number
+ * @param object $lastattempt last attempt object
* @param bool $offlineattempt whether is an offline attempt or not
+ * @param array $forcedrandomquestions slot number => question id. Used for random questions,
+ * to force the choice of a particular actual question. Intended for testing purposes only.
+ * @param array $forcedvariants slot number => variant. Used for questions with variants,
+ * to force the choice of a particular variant. Intended for testing purposes only.
* @return object the new attempt
* @since Moodle 3.1
*/
-function quiz_prepare_and_start_new_attempt(quiz $quizobj, $attemptnumber, $lastattempt, $offlineattempt = false) {
+function quiz_prepare_and_start_new_attempt(quiz $quizobj, $attemptnumber, $lastattempt,
+ $offlineattempt = false, $forcedrandomquestions = [], $forcedvariants = []) {
global $DB, $USER;
// Delete any previous preview attempts belonging to this user.
$attempt = quiz_create_attempt($quizobj, $attemptnumber, $lastattempt, $timenow, $quizobj->is_preview_user());
if (!($quizobj->get_quiz()->attemptonlast && $lastattempt)) {
- $attempt = quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow);
+ $attempt = quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow,
+ $forcedrandomquestions, $forcedvariants);
} else {
$attempt = quiz_start_attempt_built_on_last($quba, $attempt, $lastattempt);
}
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
- And I log in as "teacher1"
- And I am on "Course 1" course homepage with editing mode on
- And I add a "Quiz" to section "1" and I fill the form with:
- | Name | Quiz 1 |
- | Description | Quiz 1 description |
- And I add a "Short answer" question to the "Quiz 1" quiz with:
- | Question name | Short answer 001 |
- | Question text | Where is the capital city of France? |
- | Answer 1 | Paris |
- | Grade | 100% |
+ And the following "question categories" exist:
+ | contextlevel | reference | name |
+ | Course | C1 | Test questions |
+ And the following "questions" exist:
+ | questioncategory | qtype | name | questiontext | answer 1 | grade |
+ | Test questions | shortanswer | Short answer 001 | Where is the capital city of France? | Paris | 100% |
+ And the following "activities" exist:
+ | activity | name | course | idnumber |
+ | quiz | Quiz 1 | C1 | quiz1 |
+ And quiz "Quiz 1" contains the following questions:
+ | question | page |
+ | Short answer 001 | 1 |
# Check report shows nothing when there are no attempts.
+ When I log in as "teacher1"
And I am on "Course 1" course homepage
And I follow "Quiz 1"
- When I navigate to "Results > Manual grading" in current page administration
+ And I navigate to "Results > Manual grading" in current page administration
Then I should see "Manual grading"
And I should see "Quiz 1"
And I should see "Nothing to display"
And I follow "Also show questions that have been graded automatically"
And I should see "Nothing to display"
- And I log out
-
- # Create an attempt.
- And I log in as "student1"
- And I am on "Course 1" course homepage
- And I follow "Quiz 1"
- And I press "Attempt quiz now"
- And I should see "Question 1"
- And I should see "Not yet answered"
- And I should see "Where is the capital city of France?"
- And I set the field "Answer:" to "Paris"
- And I press "Finish attempt ..."
- And I should see "Answer saved"
- And I press "Submit all and finish"
- And I click on "Submit all and finish" "button" in the "Confirmation" "dialogue"
- And I log out
# Use the manual grading report.
- And I log in as "teacher1"
- And I am on "Course 1" course homepage
- And I follow "Quiz 1"
- And I navigate to "Results > Manual grading" in current page administration
- And I should see "Manual grading"
- And I follow "Also show questions that have been graded automatically"
+ And user "student1" has attempted "Quiz 1" with responses:
+ | slot | response |
+ | 1 | Paris |
+ And I reload the page
And I should see "Short answer 001"
And "Short answer 001" row "To grade" column of "questionstograde" table should contain "0"
And "Short answer 001" row "Already graded" column of "questionstograde" table should contain "0"
| question | page | maxmark |
| TF1 | 1 | |
| TF2 | 1 | 3.0 |
-
- # Add some attempts
- And I log in as "student1"
- And I am on "Course 1" course homepage
- And I follow "Quiz 1"
- And I press "Attempt quiz now"
- And I click on "True" "radio" in the "First question" "question"
- And I click on "False" "radio" in the "Second question" "question"
- And I press "Finish attempt ..."
- And I press "Submit all and finish"
- And I click on "Submit all and finish" "button" in the "Confirmation" "dialogue"
- And I log out
-
- And I log in as "student2"
- And I am on "Course 1" course homepage
- And I follow "Quiz 1"
- And I press "Attempt quiz now"
- And I click on "True" "radio" in the "First question" "question"
- And I click on "True" "radio" in the "Second question" "question"
- And I press "Finish attempt ..."
- And I press "Submit all and finish"
- And I click on "Submit all and finish" "button" in the "Confirmation" "dialogue"
- And I log out
+ And user "student1" has attempted "Quiz 1" with responses:
+ | slot | response |
+ | 1 | True |
+ | 2 | False |
+ And user "student2" has attempted "Quiz 1" with responses:
+ | slot | response |
+ | 1 | True |
+ | 2 | True |
# Basic check of the Grades report
When I log in as "teacher1"
if ($round) {
$mark = quiz_format_grade($quiz, $mark);
}
- return $mark . '%';
+
+ return get_string('percents', 'moodle', $mark);
}
/**
Then I should see "Attempts: 0"
And I should see "Nothing to display"
And I set the field "Attempts from" to "enrolled users who have not attempted the quiz"
- And I press "Show report"
- And "Student One" row "State" column of "responses" table should contain "-"
+ And I log out
@javascript
Scenario: Report works when there are attempts
- # Add an attempt
- Given I log in as "student1"
- And I am on "Course 1" course homepage
- And I follow "Quiz 1"
- And I press "Attempt quiz now"
- And I set the field "Answer" to "1.0"
- And I press "Check"
- And I press "Try again"
- And I set the field "Answer" to "3.0"
- And I press "Check"
- And I press "Try again"
- And I set the field "Answer" to "3.14"
- And I press "Check"
- And I press "Finish attempt ..."
- And I press "Submit all and finish"
- And I click on "Submit all and finish" "button" in the "Confirmation" "dialogue"
- And I log out
-
+ Given user "student1" has started an attempt at quiz "Quiz 1"
+ And user "student1" has checked answers in their attempt at quiz "Quiz 1":
+ | slot | response |
+ | 1 | 1.0 |
+ And user "student1" has checked answers in their attempt at quiz "Quiz 1":
+ | slot | response |
+ | 1 | 3.0 |
+ And user "student1" has checked answers in their attempt at quiz "Quiz 1":
+ | slot | response |
+ | 1 | 3.14 |
+ And user "student1" has finished an attempt at quiz "Quiz 1"
When I log in as "teacher"
And I am on "Course 1" course homepage
And I follow "Quiz 1"
$string['quizoverallstatistics'] = 'Quiz overall statistics';
$string['quizstructureanalysis'] = 'Quiz structure analysis';
$string['random_guess_score'] = 'Random guess score';
+$string['rangeofvalues'] = 'Range of statistics for these questions';
+$string['rangebetween'] = '{$a->min} − {$a->max}';
$string['recalculatenow'] = 'Recalculate now';
$string['reportsettings'] = 'Statistics calculation settings';
$string['response'] = 'Response';
$string['statistics:view'] = 'View statistics report';
$string['statsfor'] = 'Quiz statistics (for {$a})';
$string['variant'] = 'Variant';
+$string['viewanalysis'] = 'View details';
$string['whichtries'] = 'Analyze responses for';
require_once($CFG->libdir.'/tablelib.php');
+use \core_question\statistics\questions\calculated_question_summary;
+
/**
* This table has one row for each question in the quiz, with sub-rows when
* random questions and variants appear.
parent::setup();
}
+ /**
+ * Open a div tag to wrap statistics table.
+ */
+ public function wrap_html_start() {
+ // Horrible Moodle 2.0 wide-content work-around.
+ if (!$this->is_downloading()) {
+ echo html_writer::start_tag('div', array('id' => 'tablecontainer',
+ 'class' => 'statistics-tablecontainer'));
+ }
+ }
+
+ /**
+ * Close a statistics table div.
+ */
+ public function wrap_html_finish() {
+ if (!$this->is_downloading()) {
+ echo html_writer::end_tag('div');
+ }
+ }
+
/**
* The question number.
* @param \core_question\statistics\questions\calculated $questionstat stats for the question.
* @return string contents of this table cell.
*/
protected function col_number($questionstat) {
+ if ($this->is_calculated_question_summary($questionstat)) {
+ return '';
+ }
if (!isset($questionstat->question->number)) {
return '';
}
* @return string contents of this table cell.
*/
protected function col_icon($questionstat) {
- return print_question_icon($questionstat->question, true);
+ if ($this->is_calculated_question_summary($questionstat)) {
+ return '';
+ } else {
+ return print_question_icon($questionstat->question, true);
+ }
}
/**
* @return string contents of this table cell.
*/
protected function col_actions($questionstat) {
- return quiz_question_action_icons($this->quiz, $this->cmid,
- $questionstat->question, $this->baseurl, $questionstat->variant);
+ if ($this->is_calculated_question_summary($questionstat)) {
+ return '';
+ } else {
+ return quiz_question_action_icons($this->quiz, $this->cmid,
+ $questionstat->question, $this->baseurl, $questionstat->variant);
+ }
}
/**
// Question in a slot, we are not on a page showing structural analysis of one slot,
// we don't want linking on those pages.
$number = $questionstat->question->number;
+ $israndomquestion = $questionstat->question->qtype == 'random';
$url = new moodle_url($baseurl, array('slot' => $questionstat->slot));
- if ($questionstat->get_variants() || $questionstat->get_sub_question_ids()) {
- // Question can be broken down into sub-questions or variants. Link will show structural analysis page.
- $name = html_writer::link($url,
- $name,
- array('title' => get_string('slotstructureanalysis', 'quiz_statistics', $number)));
- } else {
+
+ if ($this->is_calculated_question_summary($questionstat)) {
+ // Only make the random question summary row name link to the slot structure
+ // analysis page with specific text to clearly indicate the link to the user.
+ // Random and variant question rows will render the name without a link to improve clarity
+ // in the UI.
+ $name = html_writer::div(get_string('rangeofvalues', 'quiz_statistics'));
+ } else if (!$israndomquestion && !$questionstat->get_variants() && !$questionstat->get_sub_question_ids()) {
// Question cannot be broken down into sub-questions or variants. Link will show response analysis page.
$name = html_writer::link($url,
$name,
$name = html_writer::tag('div', $name, array('class' => 'dubious'));
}
- if (!empty($questionstat->minmedianmaxnotice)) {
+ if ($this->is_calculated_question_summary($questionstat)) {
+ $name .= html_writer::link($url, get_string('viewanalysis', 'quiz_statistics'));
+ } else if (!empty($questionstat->minmedianmaxnotice)) {
$name = get_string($questionstat->minmedianmaxnotice, 'quiz_statistics') . '<br />' . $name;
}
* @return string contents of this table cell.
*/
protected function col_s($questionstat) {
- if (!isset($questionstat->s)) {
+ if ($this->is_calculated_question_summary($questionstat)) {
+ list($min, $max) = $questionstat->get_min_max_of('s');
+ $min = $min ?: 0;
+ $max = $max ?: 0;
+ return $this->format_range($min, $max);
+ } else if (!isset($questionstat->s)) {
return 0;
+ } else {
+ return $questionstat->s;
}
-
- return $questionstat->s;
}
/**
* @return string contents of this table cell.
*/
protected function col_facility($questionstat) {
- if (is_null($questionstat->facility)) {
+ if ($this->is_calculated_question_summary($questionstat)) {
+ list($min, $max) = $questionstat->get_min_max_of('facility');
+ return $this->format_percentage_range($min, $max);
+ } else if (is_null($questionstat->facility)) {
return '';
+ } else {
+ return $this->format_percentage($questionstat->facility);
}
-
- return number_format($questionstat->facility*100, 2) . '%';
}
/**
* @return string contents of this table cell.
*/
protected function col_sd($questionstat) {
- if (is_null($questionstat->sd) || $questionstat->maxmark == 0) {
+ if ($this->is_calculated_question_summary($questionstat)) {
+ list($min, $max) = $questionstat->get_min_max_of('sd');
+ return $this->format_percentage_range($min, $max);
+ } else if (is_null($questionstat->sd) || $questionstat->maxmark == 0) {
return '';
+ } else {
+ return $this->format_percentage($questionstat->sd / $questionstat->maxmark);
}
-
- return number_format($questionstat->sd*100 / $questionstat->maxmark, 2) . '%';
}
/**
* @return string contents of this table cell.
*/
protected function col_random_guess_score($questionstat) {
- if (is_null($questionstat->randomguessscore)) {
+ if ($this->is_calculated_question_summary($questionstat)) {
+ list($min, $max) = $questionstat->get_min_max_of('randomguessscore');
+ return $this->format_percentage_range($min, $max);
+ } else if (is_null($questionstat->randomguessscore)) {
return '';
+ } else {
+ return $this->format_percentage($questionstat->randomguessscore);
}
-
- return number_format($questionstat->randomguessscore * 100, 2).'%';
}
/**
* @return string contents of this table cell.
*/
protected function col_intended_weight($questionstat) {
- return quiz_report_scale_summarks_as_percentage($questionstat->maxmark, $this->quiz);
+ if ($this->is_calculated_question_summary($questionstat)) {
+ list($min, $max) = $questionstat->get_min_max_of('maxmark');
+
+ if (is_null($min) && is_null($max)) {
+ return '';
+ } else {
+ $min = quiz_report_scale_summarks_as_percentage($min, $this->quiz);
+ $max = quiz_report_scale_summarks_as_percentage($max, $this->quiz);
+ return $this->format_range($min, $max);
+ }
+ } else {
+ return quiz_report_scale_summarks_as_percentage($questionstat->maxmark, $this->quiz);
+ }
}
/**
protected function col_effective_weight($questionstat) {
global $OUTPUT;
- if (is_null($questionstat->effectiveweight)) {
- return '';
- }
+ if ($this->is_calculated_question_summary($questionstat)) {
+ list($min, $max) = $questionstat->get_min_max_of('effectiveweight');
+
+ if (is_null($min) && is_null($max)) {
+ return '';
+ } else {
+ list( , $negcovar) = $questionstat->get_min_max_of('negcovar');
+ if ($negcovar) {
+ $min = get_string('negcovar', 'quiz_statistics');
+ }
- if ($questionstat->negcovar) {
+ return $this->format_range($min, $max);
+ }
+ } else if (is_null($questionstat->effectiveweight)) {
+ return '';
+ } else if ($questionstat->negcovar) {
$negcovar = get_string('negcovar', 'quiz_statistics');
if (!$this->is_downloading()) {
}
return $negcovar;
+ } else {
+ return $this->format_percentage($questionstat->effectiveweight, false);
}
-
- return number_format($questionstat->effectiveweight, 2) . '%';
}
/**
* @return string contents of this table cell.
*/
protected function col_discrimination_index($questionstat) {
- if (!is_numeric($questionstat->discriminationindex)) {
+ if ($this->is_calculated_question_summary($questionstat)) {
+ list($min, $max) = $questionstat->get_min_max_of('discriminationindex');
+
+ if (isset($max)) {
+ $min = $min ?: 0;
+ }
+
+ if (is_numeric($min)) {
+ $min = $this->format_percentage($min, false);
+ }
+ if (is_numeric($max)) {
+ $max = $this->format_percentage($max, false);
+ }
+
+ return $this->format_range($min, $max);
+ } else if (!is_numeric($questionstat->discriminationindex)) {
return $questionstat->discriminationindex;
+ } else {
+ return $this->format_percentage($questionstat->discriminationindex, false);
}
-
- return number_format($questionstat->discriminationindex, 2) . '%';
}
/**
* @return string contents of this table cell.
*/
protected function col_discriminative_efficiency($questionstat) {
- if (!is_numeric($questionstat->discriminativeefficiency)) {
+ if ($this->is_calculated_question_summary($questionstat)) {
+ list($min, $max) = $questionstat->get_min_max_of('discriminativeefficiency');
+
+ if (!is_numeric($min) && !is_numeric($max)) {
+ return '';
+ } else {
+ return $this->format_percentage_range($min, $max, false);
+ }
+ } else if (!is_numeric($questionstat->discriminativeefficiency)) {
return '';
+ } else {
+ return $this->format_percentage($questionstat->discriminativeefficiency);
}
-
- return number_format($questionstat->discriminativeefficiency, 2) . '%';
}
/**
* @return bool is this question possibly not pulling it's weight?
*/
protected function is_dubious_question($questionstat) {
- if (!is_numeric($questionstat->discriminativeefficiency)) {
+ if ($this->is_calculated_question_summary($questionstat)) {
+ // We only care about the minimum value here.
+ // If the minimum value is less than the threshold, then we know that there is at least one value below the threshold.
+ list($discriminativeefficiency) = $questionstat->get_min_max_of('discriminativeefficiency');
+ } else {
+ $discriminativeefficiency = $questionstat->discriminativeefficiency;
+ }
+
+ if (!is_numeric($discriminativeefficiency)) {
return false;
}
- return $questionstat->discriminativeefficiency < 15;
+ return $discriminativeefficiency < 15;
}
- public function wrap_html_start() {
- // Horrible Moodle 2.0 wide-content work-around.
- if (!$this->is_downloading()) {
- echo html_writer::start_tag('div', array('id' => 'tablecontainer',
- 'class' => 'statistics-tablecontainer'));
+ /**
+ * Check if the given stats object is an instance of calculated_question_summary.
+ *
+ * @param \core_question\statistics\questions\calculated $questionstat Stats object
+ * @return bool
+ */
+ protected function is_calculated_question_summary($questionstat) {
+ return $questionstat instanceof calculated_question_summary;
+ }
+
+ /**
+ * Format inputs to represent a range between $min and $max.
+ * This function does not check if $min is less than $max or not.
+ * If both $min and $max are equal to null, this function returns an empty string.
+ *
+ * @param string|null $min The minimum value in the range
+ * @param string|null $max The maximum value in the range
+ * @return string
+ */
+ protected function format_range(string $min = null, string $max = null) {
+ if (is_null($min) && is_null($max)) {
+ return '';
+ } else {
+ $a = new stdClass();
+ $a->min = $min;
+ $a->max = $max;
+
+ return get_string('rangebetween', 'quiz_statistics', $a);
}
}
- public function wrap_html_finish() {
- if (!$this->is_downloading()) {
- echo html_writer::end_tag('div');
+ /**
+ * Format a number to a localised percentage with specified decimal points.
+ *
+ * @param float $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
+ */
+ protected function format_percentage(float $number, bool $fraction = true, int $decimals = 2) {
+ $coefficient = $fraction ? 100 : 1;
+ return get_string('percents', 'moodle', format_float($number * $coefficient, $decimals));
+ }
+
+ /**
+ * Format $min and $max to localised percentages and form a string that represents a range between them.
+ * This function does not check if $min is less than $max or not.
+ * If both $min and $max are equal to null, this function returns an empty string.
+ *
+ * @param float|null $min The minimum value of the range
+ * @param float|null $max The maximum value of the range
+ * @param bool $fraction An indicator for whether min and max are a fractions or are already multiplied by 100
+ * @param int $decimals Sets the number of decimal points
+ * @return string A formatted string that represents a range between $min to $max.
+ */
+ protected function format_percentage_range(float $min = null, float $max = null, bool $fraction = true, int $decimals = 2) {
+ if (is_null($min) && is_null($max)) {
+ return '';
+ } else {
+ $min = $min ?: 0;
+ $max = $max ?: 0;
+ return $this->format_range(
+ $this->format_percentage($min, $fraction, $decimals),
+ $this->format_percentage($max, $fraction, $decimals)
+ );
}
}
}
--- /dev/null
+@mod @mod_quiz @quiz @quiz_statistics
+Feature: Basic use of the Statistics report
+ In order to see how my students are progressing
+ As a teacher
+ I need to see all their quiz responses
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | 1 | teacher1@example.com |
+ | student1 | Student | 1 | student1@example.com |
+ | student2 | Student | 2 | student2@example.com |
+ | student3 | Student | 3 | student3@example.com |
+ And the following "courses" exist:
+ | fullname | shortname | category |
+ | Course 1 | C1 | 0 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ | student1 | C1 | student |
+ | student2 | C1 | student |
+ | student3 | 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 | Question A | This is question 01 |
+ | Test questions | truefalse | Question B | This is question 02 |
+ | Test questions | truefalse | Question C | This is question 03 |
+ And the following "activities" exist:
+ | activity | name | course | idnumber |
+ | quiz | Quiz 1 | C1 | quiz1 |
+ And quiz "Quiz 1" contains the following questions:
+ | question | page |
+ | Question A | 1 |
+ | Question B | 1 |
+ | Question C | 2 |
+
+ @javascript
+ Scenario: Report works when there are no attempts
+ When I log in as "teacher1"
+ And I am on "Course 1" course homepage
+ And I follow "Quiz 1"
+ And I navigate to "Results > Statistics" in current page administration
+ Then I should see "No attempts have been made at this quiz, or all attempts have questions that need manual grading."
+ And I should not see "Statistics for question positions"
+ And "Show chart data" "link" should not exist
+ When user "student1" has attempted "Quiz 1" with responses:
+ | slot | response |
+ | 1 | True |
+ | 2 | False |
+ | 3 | False |
+ And user "student2" has attempted "Quiz 1" with responses:
+ | slot | response |
+ | 1 | True |
+ | 2 | True |
+ | 3 | True |
+ And user "student3" has attempted "Quiz 1" with responses:
+ | slot | response |
+ | 1 | False |
+ | 2 | False |
+ | 3 | False |
+ And I press "Show report"
+ Then I should not see "No attempts have been made at this quiz, or all attempts have questions that need manual grading."
+ And "Show chart data" "link" should exist
+
+ # Question A statistics breakdown.
+ And "1" row "Question name" column of "questionstatistics" table should contain "Question A"
+ And "1" row "Attempts" column of "questionstatistics" table should contain "3"
+ And "1" row "Facility index" column of "questionstatistics" table should contain "66.67 %"
+ And "1" row "Standard deviation" column of "questionstatistics" table should contain "57.74 %"
+ And "1" row "Random guess score" column of "questionstatistics" table should contain "50.00 %"
+ And "1" row "Intended weight" column of "questionstatistics" table should contain "33.33 %"
+ And "1" row "Effective weight" column of "questionstatistics" table should contain "30.90 %"
+ And "1" row "Discrimination index" column of "questionstatistics" table should contain "50.00 %"
+
+ # Question B statistics breakdown.
+ And "2" row "Question name" column of "questionstatistics" table should contain "Question B"
+ And "2" row "Attempts" column of "questionstatistics" table should contain "3"
+ And "2" row "Facility index" column of "questionstatistics" table should contain "33.33 %"
+ And "2" row "Standard deviation" column of "questionstatistics" table should contain "57.74 %"
+ And "2" row "Random guess score" column of "questionstatistics" table should contain "50.00 %"
+ And "2" row "Intended weight" column of "questionstatistics" table should contain "33.33 %"
+ And "2" row "Effective weight" column of "questionstatistics" table should contain "34.55 %"
+ And "2" row "Discrimination index" column of "questionstatistics" table should contain "86.60 %"
+
+ # Question C statistics breakdown.
+ And "3" row "Question name" column of "questionstatistics" table should contain "Question C"
+ And "3" row "Attempts" column of "questionstatistics" table should contain "3"
+ And "3" row "Facility index" column of "questionstatistics" table should contain "33.33 %"
+ And "3" row "Standard deviation" column of "questionstatistics" table should contain "57.74 %"
+ And "3" row "Random guess score" column of "questionstatistics" table should contain "50.00 %"
+ And "3" row "Intended weight" column of "questionstatistics" table should contain "33.33 %"
+ And "3" row "Effective weight" column of "questionstatistics" table should contain "34.55 %"
+ And "3" row "Discrimination index" column of "questionstatistics" table should contain "86.60 %"
+
--- /dev/null
+<?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/>.
+
+/**
+ * Unit tests for the quiz_statistics_table class.
+ *
+ * @package quiz_statistics
+ * @category test
+ * @copyright 2018 Shamim Rezaie <shamim@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_table.php');
+
+/**
+ * Class quiz_statistics_statistics_table_testcase
+ *
+ * @copyright 2018 Shamim Rezaie <shamim@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class quiz_statistics_statistics_table_testcase extends advanced_testcase {
+
+ public function test_format_percentage() {
+ $table = new quiz_statistics_table();
+
+ // The format_percentage method is protected. Use Reflection to call the method.
+ $reflector = new ReflectionClass('quiz_statistics_table');
+ $method = $reflector->getMethod('format_percentage');
+ $method->setAccessible(true);
+
+ $this->assertEquals(
+ '84.758 %',
+ $method->invokeArgs($table, [0.847576, true, 3])
+ );
+
+ $this->assertEquals(
+ '84.758 %',
+ $method->invokeArgs($table, [84.7576, false, 3])
+ );
+ }
+
+ public function test_format_percentage_range() {
+ $table = new quiz_statistics_table();
+
+ // The format_percentage_range method is protected. Use Reflection to call the method.
+ $reflector = new ReflectionClass('quiz_statistics_table');
+ $method = $reflector->getMethod('format_percentage_range');
+ $method->setAccessible(true);
+
+ $this->assertEquals(
+ '54.400 % − 84.758 %',
+ $method->invokeArgs($table, [0.544, 0.847576, true, 3])
+ );
+
+ $this->assertEquals(
+ '54.400 % − 84.758 %',
+ $method->invokeArgs($table, [54.4, 84.7576, false, 3])
+ );
+ }
+
+ public function test_format_range() {
+ $table = new quiz_statistics_table();
+
+ // The format_range method is protected. Use Reflection to call the method.
+ $reflector = new ReflectionClass('quiz_statistics_table');
+ $method = $reflector->getMethod('format_range');
+ $method->setAccessible(true);
+
+ $this->assertEquals(
+ '5 − 10',
+ $method->invokeArgs($table, [5, 10])
+ );
+
+ $this->assertEquals(
+ 'Some Text − 10',
+ $method->invokeArgs($table, ['Some Text', 10])
+ );
+ }
+}
| question | page | maxmark |
| TF1 | 1 | |
| TF2 | 1 | 3.0 |
+ And user "student" has attempted "Quiz 1" with responses:
+ | slot | response |
+ | 1 | True |
+ | 2 | False |
When I log in as "student"
And I am on "Course 1" course homepage
And I follow "Quiz 1"
- And I press "Attempt quiz now"
- And I click on "True" "radio" in the "First question" "question"
- And I click on "False" "radio" in the "Second question" "question"
- And I press "Finish attempt ..."
- And I press "Submit all and finish"
- And I click on "Submit all and finish" "button" in the "Confirmation" "dialogue"
+ And I follow "Review"
Then I should see "25.00 out of 100.00"
@javascript
"contains(., {$questionnumberliteral}) and contains(preceding-sibling::h3[1], {$headingliteral})]";
$this->find('xpath', $xpath);
}
+
+ /**
+ * Helper used by user_has_attempted_with_responses,
+ * user_has_started_an_attempt_at_quiz_with_details, etc.
+ *
+ * @param TableNode $attemptinfo data table from the Behat step
+ * @return array with two elements, $forcedrandomquestions, $forcedvariants,
+ * that can be passed to $quizgenerator->create_attempt.
+ */
+ protected function extract_forced_randomisation_from_attempt_info(TableNode $attemptinfo) {
+ global $DB;
+
+ $forcedrandomquestions = [];
+ $forcedvariants = [];
+ foreach ($attemptinfo->getHash() as $slotinfo) {
+ if (empty($slotinfo['slot'])) {
+ throw new ExpectationException('When simulating a quiz attempt, ' .
+ 'the slot column is required.', $this->getSession());
+ }
+
+ if (!empty($slotinfo['actualquestion'])) {
+ $forcedrandomquestions[$slotinfo['slot']] = $DB->get_field('question', 'id',
+ ['name' => $slotinfo['actualquestion']], MUST_EXIST);
+ }
+
+ if (!empty($slotinfo['variant'])) {
+ $forcedvariants[$slotinfo['slot']] = (int) $slotinfo['variant'];
+ }
+ }
+ return [$forcedrandomquestions, $forcedvariants];
+ }
+
+ /**
+ * Helper used by user_has_attempted_with_responses, user_has_checked_answers_in_their_attempt_at_quiz,
+ * user_has_input_answers_in_their_attempt_at_quiz, etc.
+ *
+ * @param TableNode $attemptinfo data table from the Behat step
+ * @return array of responses that can be passed to $quizgenerator->submit_responses.
+ */
+ protected function extract_responses_from_attempt_info(TableNode $attemptinfo) {
+ $responses = [];
+ foreach ($attemptinfo->getHash() as $slotinfo) {
+ if (empty($slotinfo['slot'])) {
+ throw new ExpectationException('When simulating a quiz attempt, ' .
+ 'the slot column is required.', $this->getSession());
+ }
+ if (!array_key_exists('response', $slotinfo)) {
+ throw new ExpectationException('When simulating a quiz attempt, ' .
+ 'the response column is required.', $this->getSession());
+ }
+ $responses[$slotinfo['slot']] = $slotinfo['response'];
+ }
+ return $responses;
+ }
+
+ /**
+ * Attempt a quiz.
+ *
+ * The first row should be column names:
+ * | slot | actualquestion | variant | response |
+ * The first two of those are required. The others are optional.
+ *
+ * slot The slot
+ * actualquestion This column is optional, and is only needed if the quiz contains
+ * random questions. If so, this will let you control which actual
+ * question gets picked when this slot is 'randomised' at the
+ * start of the attempt. If you don't specify, then one will be picked
+ * at random (which might make the reponse meaningless).
+ * variant This column is similar, and also options. It is only needed if
+ * the question that ends up in this slot returns something greater
+ * than 1 for $question->get_num_variants(). Like with actualquestion,
+ * if you specify a value here it is used the fix the 'random' choice
+ * made when the quiz is started.
+ * response The response that was submitted. How this is interpreted depends on
+ * the question type. It gets passed to
+ * {@link core_question_generator::get_simulated_post_data_for_question_attempt()}
+ * and therefore to the un_summarise_response method of the question to decode.
+ *
+ * Then there should be a number of rows of data, one for each question you want to add.
+ * There is no need to supply answers to all questions. If so, other qusetions will be
+ * left unanswered.
+ *
+ * @param string $username the username of the user that will attempt.
+ * @param string $quizname the name of the quiz the user will attempt.
+ * @param TableNode $attemptinfo information about the questions to add, as above.
+ * @Given /^user "([^"]*)" has attempted "([^"]*)" with responses:$/
+ */
+ public function user_has_attempted_with_responses($username, $quizname, TableNode $attemptinfo) {
+ global $DB;
+
+ /** @var mod_quiz_generator $quizgenerator */
+ $quizgenerator = behat_util::get_data_generator()->get_plugin_generator('mod_quiz');
+
+ $quizid = $DB->get_field('quiz', 'id', ['name' => $quizname], MUST_EXIST);
+ $user = $DB->get_record('user', ['username' => $username], '*', MUST_EXIST);
+
+ list($forcedrandomquestions, $forcedvariants) =
+ $this->extract_forced_randomisation_from_attempt_info($attemptinfo);
+ $responses = $this->extract_responses_from_attempt_info($attemptinfo);
+
+ $this->set_user($user);
+
+ $attempt = $quizgenerator->create_attempt($quizid, $user->id,
+ $forcedrandomquestions, $forcedvariants);
+
+ $quizgenerator->submit_responses($attempt->id, $responses, false, true);
+
+ $this->set_user();
+ }
+
+ /**
+ * Start a quiz attempt without answers.
+ *
+ * Then there should be a number of rows of data, one for each question you want to add.
+ * There is no need to supply answers to all questions. If so, other qusetions will be
+ * left unanswered.
+ *
+ * @param string $username the username of the user that will attempt.
+ * @param string $quizname the name of the quiz the user will attempt.
+ * @Given /^user "([^"]*)" has started an attempt at quiz "([^"]*)"$/
+ */
+ public function user_has_started_an_attempt_at_quiz($username, $quizname) {
+ global $DB;
+
+ /** @var mod_quiz_generator $quizgenerator */
+ $quizgenerator = behat_util::get_data_generator()->get_plugin_generator('mod_quiz');
+
+ $quizid = $DB->get_field('quiz', 'id', ['name' => $quizname], MUST_EXIST);
+ $user = $DB->get_record('user', ['username' => $username], '*', MUST_EXIST);
+ $this->set_user($user);
+ $quizgenerator->create_attempt($quizid, $user->id);
+ $this->set_user();
+ }
+
+ /**
+ * Start a quiz attempt without answers.
+ *
+ * The supplied data table for have a row for each slot where you want
+ * to force either which random question was chose, or which random variant
+ * was used, as for {@link user_has_attempted_with_responses()} above.
+ *
+ * @param string $username the username of the user that will attempt.
+ * @param string $quizname the name of the quiz the user will attempt.
+ * @param TableNode $attemptinfo information about the questions to add, as above.
+ * @Given /^user "([^"]*)" has started an attempt at quiz "([^"]*) randomised as follows:"$/
+ */
+ public function user_has_started_an_attempt_at_quiz_with_details($username, $quizname, TableNode $attemptinfo) {
+ global $DB;
+
+ /** @var mod_quiz_generator $quizgenerator */
+ $quizgenerator = behat_util::get_data_generator()->get_plugin_generator('mod_quiz');
+
+ $quizid = $DB->get_field('quiz', 'id', ['name' => $quizname], MUST_EXIST);
+ $user = $DB->get_record('user', ['username' => $username], '*', MUST_EXIST);
+
+ list($forcedrandomquestions, $forcedvariants) =
+ $this->extract_forced_randomisation_from_attempt_info($attemptinfo);
+
+ $this->set_user($user);
+
+ $quizgenerator->create_attempt($quizid, $user->id,
+ $forcedrandomquestions, $forcedvariants);
+
+ $this->set_user();
+ }
+
+ /**
+ * Input answers to particular questions an existing quiz attempt, without
+ * simulating a click of the 'Check' button, if any.
+ *
+ * Then there should be a number of rows of data, with two columns slot and response,
+ * as for {@link user_has_attempted_with_responses()} above.
+ * There is no need to supply answers to all questions. If so, other questions will be
+ * left unanswered.
+ *
+ * @param string $username the username of the user that will attempt.
+ * @param string $quizname the name of the quiz the user will attempt.
+ * @param TableNode $attemptinfo information about the questions to add, as above.
+ * @throws \Behat\Mink\Exception\ExpectationException
+ * @Given /^user "([^"]*)" has input answers in their attempt at quiz "([^"]*)":$/
+ */
+ public function user_has_input_answers_in_their_attempt_at_quiz($username, $quizname, TableNode $attemptinfo) {
+ global $DB;
+
+ /** @var mod_quiz_generator $quizgenerator */
+ $quizgenerator = behat_util::get_data_generator()->get_plugin_generator('mod_quiz');
+
+ $quizid = $DB->get_field('quiz', 'id', ['name' => $quizname], MUST_EXIST);
+ $user = $DB->get_record('user', ['username' => $username], '*', MUST_EXIST);
+
+ $responses = $this->extract_responses_from_attempt_info($attemptinfo);
+
+ $this->set_user($user);
+
+ $attempts = quiz_get_user_attempts($quizid, $user->id, 'unfinished', true);
+ $quizgenerator->submit_responses(key($attempts), $responses, false, false);
+
+ $this->set_user();
+ }
+
+ /**
+ * Submit answers to questions an existing quiz attempt, with a simulated click on the 'Check' button.
+ *
+ * This step should only be used with question behaviours that have have
+ * a 'Check' button. Those include Interactive with multiple tires, Immediate feedback
+ * and Immediate feedback with CBM.
+ *
+ * Then there should be a number of rows of data, with two columns slot and response,
+ * as for {@link user_has_attempted_with_responses()} above.
+ * There is no need to supply answers to all questions. If so, other questions will be
+ * left unanswered.
+ *
+ * @param string $username the username of the user that will attempt.
+ * @param string $quizname the name of the quiz the user will attempt.
+ * @param TableNode $attemptinfo information about the questions to add, as above.
+ * @throws \Behat\Mink\Exception\ExpectationException
+ * @Given /^user "([^"]*)" has checked answers in their attempt at quiz "([^"]*)":$/
+ */
+ public function user_has_checked_answers_in_their_attempt_at_quiz($username, $quizname, TableNode $attemptinfo) {
+ global $DB;
+
+ /** @var mod_quiz_generator $quizgenerator */
+ $quizgenerator = behat_util::get_data_generator()->get_plugin_generator('mod_quiz');
+
+ $quizid = $DB->get_field('quiz', 'id', ['name' => $quizname], MUST_EXIST);
+ $user = $DB->get_record('user', ['username' => $username], '*', MUST_EXIST);
+
+ $responses = $this->extract_responses_from_attempt_info($attemptinfo);
+
+ $this->set_user($user);
+
+ $attempts = quiz_get_user_attempts($quizid, $user->id, 'unfinished', true);
+ $quizgenerator->submit_responses(key($attempts), $responses, true, false);
+
+ $this->set_user();
+ }
+
+ /**
+ * Finish an existing quiz attempt.
+ *
+ * @param string $username the username of the user that will attempt.
+ * @param string $quizname the name of the quiz the user will attempt.
+ * @Given /^user "([^"]*)" has finished an attempt at quiz "([^"]*)"$/
+ */
+ public function user_has_finished_an_attempt_at_quiz($username, $quizname) {
+ global $DB;
+
+ $quizid = $DB->get_field('quiz', 'id', ['name' => $quizname], MUST_EXIST);
+ $user = $DB->get_record('user', ['username' => $username], '*', MUST_EXIST);
+
+ $this->set_user($user);
+
+ $attempts = quiz_get_user_attempts($quizid, $user->id, 'unfinished', true);
+ $attemptobj = quiz_attempt::create(key($attempts));
+ $attemptobj->process_finish(time(), true);
+
+ $this->set_user();
+ }
}
And quiz "Test quiz name" contains the following questions:
| question | page |
| First question | 1 |
+ And user "student1" has attempted "Test quiz name" with responses:
+ | slot | response |
+ | 1 | False |
Scenario: student1 uses up both attempts without passing
When I log in as "student1"
And I am on "Course 1" course homepage
And the "Test quiz name" "quiz" activity with "auto" completion should be marked as not complete
And I follow "Test quiz name"
- And I press "Attempt quiz now"
- And I set the field "False" to "1"
- And I press "Finish attempt ..."
- And I press "Submit all and finish"
- And I am on "Course 1" course homepage
- And the "Test quiz name" "quiz" activity with "auto" completion should be marked as not complete
- And I follow "Test quiz name"
And I press "Re-attempt quiz"
And I set the field "False" to "1"
And I press "Finish attempt ..."
When I log in as "student1"
And I am on "Course 1" course homepage
And the "Test quiz name" "quiz" activity with "auto" completion should be marked as not complete
- And I follow "Test quiz name"
- And I press "Attempt quiz now"
- And I set the field "True" to "1"
- And I press "Finish attempt ..."
- And I press "Submit all and finish"
+ And user "student1" has attempted "Test quiz name" with responses:
+ | slot | response |
+ | 1 | True |
And I am on "Course 1" course homepage
Then "Completed: Test quiz name" "icon" should exist in the "li.modtype_quiz" "css_element"
And I log out
| question | page | maxmark |
| TF1 | 1 | |
| TF2 | 1 | 3.0 |
- And I log in as "teacher"
- And I am on "Course 1" course homepage
+ And user "teacher" has attempted "Quiz 1" with responses:
+ | slot | response |
+ | 1 | True |
+ | 2 | False |
@javascript
Scenario: Preview a quiz
+ When I log in as "teacher"
+ And I am on "Course 1" course homepage
When I follow "Quiz 1"
- And I press "Preview quiz now"
- And I click on "True" "radio" in the "First question" "question"
- And I click on "False" "radio" in the "Second question" "question"
- And I press "Finish attempt ..."
- And I press "Submit all and finish"
- And I click on "Submit all and finish" "button" in the "Confirmation" "dialogue"
+ And I follow "Review"
Then I should see "25.00 out of 100.00"
And I follow "Finish review"
And "Review" "link" in the "Preview" "table_row" should be visible
And quiz "Test quiz name" contains the following questions:
| question | page |
| TF1 | 1 |
+ And user "student1" has attempted "Test quiz name" with responses:
+ | slot | response |
+ | 1 | True |
Scenario: Use course reset to clear all attempt data
- When I log in as "student1"
- And I am on "Course 1" course homepage
- And I follow "Test quiz name"
- And I press "Attempt quiz now"
- And I set the field "True" to "1"
- And I press "Finish attempt ..."
- And I press "Submit all and finish"
- And I log out
- And I log in as "teacher1"
+ When I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to "Reset" in current page administration
And I set the following fields to these values:
return parent::create_instance($record, (array)$options);
}
+
+ /**
+ * Create a quiz attempt for a particular user at a particular course.
+ *
+ * Currently this method can only create a first attempt for each
+ * user at each quiz. TODO remove this limitation.
+ *
+ * @param int $quizid the quiz id (from the mdl_quit table, not cmid).
+ * @param int $userid the user id.
+ * @param array $forcedrandomquestions slot => questionid. Optional,
+ * used with random questions, to control which one is 'randomly' selected in that slot.
+ * @param array $forcedvariants slot => variantno. Optional. Optional,
+ * used with question where get_num_variants is > 1, to control which
+ * variants is 'randomly' selected.
+ * @return stdClass the new attempt.
+ */
+ public function create_attempt($quizid, $userid, array $forcedrandomquestions = [],
+ array $forcedvariants = []) {
+ // Build quiz object and load questions.
+ $quizobj = quiz::create($quizid, $userid);
+
+ if (quiz_get_user_attempts($quizid, $userid, 'all', true)) {
+ throw new coding_exception('mod_quiz_generator is currently limited to only ' .
+ 'be able to create one attempt for each user. (This should be fixed.)');
+ }
+
+ return quiz_prepare_and_start_new_attempt($quizobj, 1, null, false,
+ $forcedrandomquestions, $forcedvariants);
+ }
+
+ /**
+ * Submit responses to a quiz attempt.
+ *
+ * To be realistic, you should ensure that $USER is set to the user whose attempt
+ * it is before calling this.
+ *
+ * @param int $attemptid the id of the attempt which is being
+ * @param array $responses array responses to submit. See description on
+ * {@link core_question_generator::get_simulated_post_data_for_questions_in_usage()}.
+ * @param bool $checkbutton if simulate a click on the check button for each question, else simulate save.
+ * This should only be used with behaviours that have a check button.
+ * @param bool $finishattempt if true, the attempt will be submitted.
+ */
+ public function submit_responses($attemptid, array $responses, $checkbutton, $finishattempt) {
+ $questiongenerator = $this->datagenerator->get_plugin_generator('core_question');
+
+ $attemptobj = quiz_attempt::create($attemptid);
+
+ $postdata = $questiongenerator->get_simulated_post_data_for_questions_in_usage(
+ $attemptobj->get_question_usage(), $responses, $checkbutton);
+
+ $attemptobj->process_submitted_actions(time(), false, $postdata);
+
+ // Bit if a hack for interactive behaviour.
+ // TODO handle this in a more plugin-friendly way.
+ if ($checkbutton) {
+ $postdata = [];
+ foreach ($responses as $slot => $notused) {
+ $qa = $attemptobj->get_question_attempt($slot);
+ if ($qa->get_behaviour() instanceof qbehaviour_interactive && $qa->get_behaviour()->is_try_again_state()) {
+ $postdata[$qa->get_control_field_name('sequencecheck')] = (string)$qa->get_sequence_check_count();
+ $postdata[$qa->get_flag_field_name()] = (string)(int)$qa->is_flagged();
+ $postdata[$qa->get_behaviour_field_name('tryagain')] = 1;
+ }
+ }
+
+ if ($postdata) {
+ $attemptobj->process_submitted_actions(time(), false, $postdata);
+ }
+ }
+
+ if ($finishattempt) {
+ $attemptobj->process_finish(time(), false);
+ }
+ }
}
$quiz->sumgrades = 10;
$quiz->decimalpoints = 2;
- $this->assertEquals('12.34567%',
+ $this->assertEquals('12.34567 %',
quiz_report_scale_summarks_as_percentage(1.234567, $quiz, false));
- $this->assertEquals('12.35%',
+ $this->assertEquals('12.35 %',
quiz_report_scale_summarks_as_percentage(1.234567, $quiz, true));
$this->assertEquals('-',
quiz_report_scale_summarks_as_percentage('-', $quiz, true));
$percents = array();
$percents[''] = '';
for ($i = 100; $i >= 0; $i--) {
- $percents[$i] = get_string('percents', 'workshopform_numerrors', $i);
+ $percents[$i] = get_string('percents', 'moodle', $i);
}
- $mform->addElement('static', 'mappingzero', 0, get_string('percents', 'workshopform_numerrors', 100));
+ $mform->addElement('static', 'mappingzero', 0, get_string('percents', 'moodle', 100));
for ($i = 1; $i <= $totalweight; $i++) {
$selects = array();
$selects[] = $mform->createElement('select', 'map__idx_'.$i, $i, $percents);
$selects[] = $mform->createElement('static', 'mapdefault__idx_'.$i, '',
- get_string('percents', 'workshopform_numerrors', floor(100 - $i * 100 / $totalweight)));
+ get_string('percents', 'moodle', floor(100 - $i * 100 / $totalweight)));
$mform->addGroup($selects, 'grademapping'.$i, $i, array(' '), false);
$mform->setDefault('map__idx_'.$i, '');
}
$string['grade1default'] = 'Yes';
$string['maperror'] = 'Weighted number of errors is less than or equals';
$string['mapgrade'] = 'Grade for submission';
-$string['percents'] = '{$a} %';
$string['pluginname'] = 'Number of errors';
$string['privacy:metadata'] = 'The Number of errors plugin only stores the details of the assessment form. Actual personal data of how the form has been filled are stored by the Workshop module itself and are attached to exported assessments.';
$string['privacy:export:metadata:map'] = 'If the weighted number of errors reaches {$a->nonegative} then the grade is {$a->grade} percents.';
/**
* @return bool are we are currently in the try_again state.
*/
- protected function is_try_again_state() {
+ public function is_try_again_state() {
$laststep = $this->qa->get_last_step();
return $this->qa->get_state()->is_active() && $laststep->has_behaviour_var('submit') &&
$laststep->has_behaviour_var('_triesleft');
/** @var int Time after which statistics are automatically recomputed. */
const TIME_TO_CACHE = 900; // 15 minutes.
- /**
- * The limit of rows of sub-question and variants rows to display on main page of report before switching to showing min,
- * median and max variants.
- */
- const SUBQ_AND_VARIANT_ROW_LIMIT = 10;
-
/**
* @var object[]
*/
return $errors;
}
- /**
- * Are there too many rows of sub-questions and / or variant rows.
- *
- * @param array $rows the rows we intend to add.
- * @return bool Are there too many?
- */
- protected function too_many_subq_and_or_variant_rows($rows) {
- return (count($rows) > static::SUBQ_AND_VARIANT_ROW_LIMIT);
- }
-
- /**
- * From a number of calculated instances find the three instances with min, median and maximum facility index values.
- *
- * @param calculated[] $questionstats The stats from which to find the ones with minimum, median and maximum facility index.
- * @return calculated[] 3 stat objects with minimum, median and maximum facility index.
- */
- protected function find_min_median_and_max_facility_stats_objects($questionstats) {
- $facilities = array();
- foreach ($questionstats as $key => $questionstat) {
- $facilities[$key] = (float)$questionstat->facility;
- }
- asort($facilities);
- $facilitykeys = array_keys($facilities);
- $keyformin = $facilitykeys[0];
- $keyformedian = $facilitykeys[(int)(round(count($facilitykeys) / 2) - 1)];
- $keyformax = $facilitykeys[count($facilitykeys) - 1];
- $toreturn = array();
- foreach (array($keyformin => 'minimumfacility',
- $keyformedian => 'medianfacility',
- $keyformax => 'maximumfacility') as $key => $stringid) {
- $questionstats[$key]->minmedianmaxnotice = $stringid;
- $toreturn[] = $questionstats[$key];
- }
- return $toreturn;
- }
-
/**
* Return all stats for variants of question in slot $slot.
*
*
* @param int $slot the slot no
* @param bool $limited limit number of variants and sub-questions displayed?
- * @return calculated|calculated_for_subquestion[] stats to display
+ * @return calculated|calculated_for_subquestion|calculated_question_summary[] stats to display
*/
protected function all_subq_and_variant_stats_for_slot($slot, $limited) {
// Random question in this slot?
if ($this->for_slot($slot)->get_sub_question_ids()) {
+ $toreturn = array();
+
if ($limited) {
- $subqvariantstats = $this->all_subq_variants_for_one_slot($slot);
- if ($this->too_many_subq_and_or_variant_rows($subqvariantstats)) {
- // Too many variants from randomly selected questions.
- return $this->find_min_median_and_max_facility_stats_objects($subqvariantstats);
+ $randomquestioncalculated = $this->for_slot($slot);
+
+ if ($subqvariantstats = $this->all_subq_variants_for_one_slot($slot)) {
+ // There are some variants from randomly selected questions.
+ // If we're showing a limited view of the statistics then add a question summary stat
+ // rather than a stat for each subquestion.
+ $summarystat = $this->make_new_calculated_question_summary_stat($randomquestioncalculated, $subqvariantstats);
+
+ $toreturn = array_merge($toreturn, [$summarystat]);
}
- $subqstats = $this->all_subqs_for_one_slot($slot);
- if ($this->too_many_subq_and_or_variant_rows($subqstats)) {
- // Too many randomly selected questions.
- return $this->find_min_median_and_max_facility_stats_objects($subqstats);
+
+ if ($subqstats = $this->all_subqs_for_one_slot($slot)) {
+ // There are some randomly selected questions.
+ // If we're showing a limited view of the statistics then add a question summary stat
+ // rather than a stat for each subquestion.
+ $summarystat = $this->make_new_calculated_question_summary_stat($randomquestioncalculated, $subqstats);
+
+ $toreturn = array_merge($toreturn, [$summarystat]);
}
- }
- $toreturn = array();
- $displaynumber = 1;
- foreach ($this->for_slot($slot)->get_sub_question_ids() as $subqid) {
- $toreturn[] = $this->make_new_subq_stat_for($displaynumber, $slot, $subqid);
- if ($variants = $this->for_subq($subqid)->get_variants()) {
- foreach ($variants as $variant) {
- $toreturn[] = $this->make_new_subq_stat_for($displaynumber, $slot, $subqid, $variant);
+
+ foreach ($toreturn as $index => $calculated) {
+ $calculated->subqdisplayorder = $index;
+ }
+ } else {
+ $displaynumber = 1;
+ foreach ($this->for_slot($slot)->get_sub_question_ids() as $subqid) {
+ $toreturn[] = $this->make_new_subq_stat_for($displaynumber, $slot, $subqid);
+ if ($variants = $this->for_subq($subqid)->get_variants()) {
+ foreach ($variants as $variant) {
+ $toreturn[] = $this->make_new_subq_stat_for($displaynumber, $slot, $subqid, $variant);
+ }
}
+ $displaynumber++;
}
- $displaynumber++;
}
+
return $toreturn;
} else {
$variantstats = $this->all_variant_stats_for_one_slot($slot);
- if ($limited && $this->too_many_subq_and_or_variant_rows($variantstats)) {
- return $this->find_min_median_and_max_facility_stats_objects($variantstats);
+ if ($limited && $variantstats) {
+ $variantquestioncalculated = $this->for_slot($slot);
+
+ // If we're showing a limited view of the statistics then add a question summary stat
+ // rather than a stat for each variation.
+ $summarystat = $this->make_new_calculated_question_summary_stat($variantquestioncalculated, $variantstats);
+
+ return [$summarystat];
} else {
return $variantstats;
}
$slotstat->subqdisplayorder = $displaynumber;
return $slotstat;
}
+
+ /**
+ * Create a summary calculated object for a calculated question. This is used as a placeholder
+ * to indicate that a calculated question has sub questions or variations to show rather than listing each
+ * subquestion or variation directly.
+ *
+ * @param calculated $randomquestioncalculated The calculated instance for the random question slot.
+ * @param calculated[] $subquestionstats The instances of the calculated stats of the questions that are being summarised.
+ * @return calculated_question_summary
+ */
+ protected function make_new_calculated_question_summary_stat($randomquestioncalculated, $subquestionstats) {
+ $question = $randomquestioncalculated->question;
+ $slot = $randomquestioncalculated->slot;
+ $calculatedsummary = new calculated_question_summary($question, $slot, $subquestionstats);
+
+ return $calculatedsummary;
+ }
}
--- /dev/null
+<?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/>.
+
+/**
+ * Question statistics calculations class. Used in the quiz statistics report.
+ *
+ * @package core_question
+ * @copyright 2018 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_question\statistics\questions;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class calculated_question_summary
+ *
+ * This class is used to indicate the statistics for a random question slot should
+ * be rendered with a link to a summary of the displayed questions.
+ *
+ * It's used in the limited view of the statistics calculation in lieu of adding
+ * the stats for each subquestion individually.
+ *
+ * @copyright 2018 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class calculated_question_summary extends calculated {
+
+ /**
+ * @var int only set immediately before display in the table. The order of display in the table.
+ */
+ public $subqdisplayorder;
+
+ /**
+ * @var calculated[] The instances storing the calculated stats of the questions that are being summarised.
+ */
+ protected $subqstats;
+
+ /**
+ * calculated_question_summary constructor.
+ *
+ * @param \stdClass $question
+ * @param int $slot
+ * @param calculated[] $subqstats The instances of the calculated stats of the questions that are being summarised.
+ */
+ public function __construct($question, $slot, $subqstats) {
+ parent::__construct($question, $slot);
+
+ $this->subqstats = $subqstats;
+ $this->subquestions = implode(',', array_column($subqstats, 'questionid'));
+ }
+
+ /**
+ * This is a summary stat so never breakdown by variant.
+ *
+ * @return bool
+ */
+ public function break_down_by_variant() {
+ return false;
+ }
+
+ /**
+ * Returns the minimum and maximum values of the given attribute in the summarised calculated stats.
+ *
+ * @param string $attribute The attribute that we are looking for its extremums.
+ * @return array An array of [min,max]
+ */
+ public function get_min_max_of($attribute) {
+ $getmethod = 'get_min_max_of_' . $attribute;
+ if (method_exists($this, $getmethod)) {
+ return $this->$getmethod();
+ } else {
+ $min = $max = null;
+ $set = false;
+
+ // We cannot simply use min or max functions because, in theory, some attributes might be non-scalar.
+ foreach (array_column($this->subqstats, $attribute) as $value) {
+ if (is_scalar($value) || is_null($value)) {
+ if (!$set) { // It is not good enough to check if (!isset($min)),
+ // because $min might have been set to null in an earlier iteration.
+ $min = $value;
+ $max = $value;
+ $set = true;
+ }
+
+ $min = $this->min($min, $value);
+ $max = $this->max($max, $value);
+ }
+ }
+
+ return [$min, $max];
+ }
+ }
+
+ /**
+ * Returns the minimum and maximum values of the standard deviation in the summarised calculated stats.
+ * @return array An array of [min,max]
+ */
+ protected function get_min_max_of_sd() {
+ $min = $max = null;
+ $set = false;
+
+ foreach ($this->subqstats as $subqstat) {
+ if (isset($subqstat->sd) && $subqstat->maxmark) {
+ $value = $subqstat->sd / $subqstat->maxmark;
+ } else {
+ $value = null;
+ }
+
+ if (!$set) { // It is not good enough to check if (!isset($min)),
+ // because $min might have been set to null in an earlier iteration.
+ $min = $value;
+ $max = $value;
+ $set = true;
+ }
+
+ $min = $this->min($min, $value);
+ $max = $this->max($max, $value);
+ }
+
+ return [$min, $max];
+ }
+
+ /**
+ * Find higher value.
+ * A zero value is almost considered equal to zero in comparisons. The only difference is that when being compared to zero,
+ * zero is higher than null.
+ *
+ * @param float|null $value1
+ * @param float|null $value2
+ * @return float|null
+ */
+ protected function max(float $value1 = null, float $value2 = null) {
+ $temp1 = $value1 ?: 0;
+ $temp2 = $value2 ?: 0;
+
+ $tempmax = max($temp1, $temp2);
+
+ if (!$tempmax && $value1 !== 0 && $value2 !== 0) {
+ $max = null;
+ } else {
+ $max = $tempmax;
+ }
+
+ return $max;
+ }
+
+ /**
+ * Find lower value.
+ * A zero value is almost considered equal to zero in comparisons. The only difference is that when being compared to zero,
+ * zero is lower than null.
+ *
+ * @param float|null $value1
+ * @param float|null $value2
+ * @return mixed|null
+ */
+ protected function min(float $value1 = null, float $value2 = null) {
+ $temp1 = $value1 ?: 0;
+ $temp2 = $value2 ?: 0;
+
+ $tempmin = min($temp1, $temp2);
+
+ if (!$tempmin && $value1 !== 0 && $value2 !== 0) {
+ $min = null;
+ } else {
+ $min = $tempmin;
+ }
+
+ return $min;
+ }
+}
--- /dev/null
+<?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/>.
+
+/**
+ * Unit tests for the calculated_random_question_summary class.
+ *
+ * @package core_question
+ * @category test
+ * @copyright 2018 Shamim Rezaie <shamim@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+use core_question\statistics\questions\calculated_question_summary;
+
+/**
+ * Class core_question_calculated_question_summary_testcase
+ *
+ * @copyright 2018 Shamim Rezaie <shamim@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_question_calculated_question_summary_testcase extends advanced_testcase {
+
+ /**
+ * Provider for test_get_min_max_of.
+ *
+ * @return array
+ */
+ public function get_min_max_provider() {
+ return [
+ 'negative number and null' => [
+ [
+ (object)['questionid' => 1, 'index' => 2],
+ (object)['questionid' => 2, 'index' => -7],
+ (object)['questionid' => 3, 'index' => null],
+ (object)['questionid' => 4, 'index' => 12],
+ ],
+ [-7, 12]
+ ],
+ 'null and negative number' => [
+ [
+ (object)['questionid' => 1, 'index' => 2],
+ (object)['questionid' => 2, 'index' => null],
+ (object)['questionid' => 3, 'index' => -7],
+ (object)['questionid' => 4, 'index' => 12],
+ ],
+ [-7, 12]
+ ],
+ 'negative number and null as maximum' => [
+ [
+ (object)['questionid' => 1, 'index' => -2],
+ (object)['questionid' => 2, 'index' => null],
+ (object)['questionid' => 3, 'index' => -7],
+ ],
+ [-7, null]
+ ],
+ 'zero and null' => [
+ [
+ (object)['questionid' => 1, 'index' => 2],
+ (object)['questionid' => 2, 'index' => 0],
+ (object)['questionid' => 3, 'index' => null],
+ (object)['questionid' => 4, 'index' => 12],
+ ],
+ [0, 12]
+ ],
+ 'null as minimum' => [
+ [
+ (object)['questionid' => 1, 'index' => 2],
+ (object)['questionid' => 2, 'index' => null],
+ (object)['questionid' => 3, 'index' => 12],
+ ],
+ [null, 12]
+ ],
+ 'null and null' => [
+ [
+ (object)['questionid' => 1, 'index' => 2],
+ (object)['questionid' => 2, 'index' => null],
+ (object)['questionid' => 3, 'index' => null],
+ ],
+ [null, 2]
+ ],
+ ];
+ }
+
+ /**
+ * Unit test for get_min_max_of() method.
+ *
+ * @dataProvider get_min_max_provider
+ */
+ public function test_get_min_max_of($subqstats, $expected) {
+ $calculatedsummary = new calculated_question_summary(null, null, $subqstats);
+ $res = $calculatedsummary->get_min_max_of('index');
+ $this->assertEquals($expected, $res);
+ }
+
+ /**
+ * Provider for test_get_min_max_of.
+ *
+ * @return array
+ */
+ public function get_sd_min_max_provider() {
+ return [
+ 'null and number' => [
+ [
+ (object)['questionid' => 1, 'sd' => 0.2, 'maxmark' => 0.5],
+ (object)['questionid' => 2, 'sd' => null, 'maxmark' => 1],
+ (object)['questionid' => 3, 'sd' => 0.1049, 'maxmark' => 1],
+ (object)['questionid' => 4, 'sd' => 0.12, 'maxmark' => 1],
+ ],
+ [null, 0.4]
+ ],
+ 'null and zero' => [
+ [
+ (object)['questionid' => 1, 'sd' => 0.2, 'maxmark' => 0.5],
+ (object)['questionid' => 2, 'sd' => null, 'maxmark' => 1],
+ (object)['questionid' => 3, 'sd' => 0, 'maxmark' => 1],
+ (object)['questionid' => 4, 'sd' => 0.12, 'maxmark' => 1],
+ ],
+ [0, 0.4]
+ ],
+ 'zero mark' => [
+ [
+ (object)['questionid' => 1, 'sd' => 0.2, 'maxmark' => 0],
+ (object)['questionid' => 2, 'sd' => 0.1049, 'maxmark' => 1],
+ ],
+ [null, 0.1049]
+ ],
+ 'nonzero and nonzero' => [
+ [
+ (object)['questionid' => 1, 'sd' => 0.2, 'maxmark' => 0.5],
+ (object)['questionid' => 2, 'sd' => 0.7, 'maxmark' => 2],
+ ],
+ [0.35, 0.4]
+ ],
+ ];
+ }
+
+ /**
+ * Unit test for get_min_max_of_sd() method.
+ *
+ * @dataProvider get_sd_min_max_provider
+ */
+ public function test_get_min_max_of_sd($subqstats, $expected) {
+ $calculatedsummary = new calculated_question_summary(null, null, $subqstats);
+ $res = $calculatedsummary->get_min_max_of('sd');
+ $this->assertEquals($expected, $res);
+ }
+}
return array($category, $course, $qcat, $questions);
}
+
+ /**
+ * This method can construct what the post data would be to simulate a user submitting
+ * responses to a number of questions within a question usage.
+ *
+ * In the responses array, the array keys are the slot numbers for which a response will
+ * be submitted. You can submit a response to any number of responses within the usage.
+ * There is no need to do them all. The values are a string representation of the response.
+ * The exact meaning of that depends on the particular question type. These strings
+ * are passed to the un_summarise_response method of the question to decode.
+ *
+ * @param question_usage_by_activity $quba the question usage.
+ * @param array $responses the resonses to submit, in the format described above.
+ * @param bool $checkbutton if simulate a click on the check button for each question, else simulate save.
+ * This should only be used with behaviours that have a check button.
+ * @return array that can be passed to methods like $quba->process_all_actions as simulated POST data.
+ */
+ public function get_simulated_post_data_for_questions_in_usage(
+ question_usage_by_activity $quba, array $responses, $checkbutton) {
+ $postdata = [];
+
+ foreach ($responses as $slot => $responsesummary) {
+ $postdata += $this->get_simulated_post_data_for_question_attempt(
+ $quba->get_question_attempt($slot), $responsesummary, $checkbutton);
+ }
+
+ return $postdata;
+ }
+
+ /**
+ * This method can construct what the post data would be to simulate a user submitting
+ * responses to one particular question attempt.
+ *
+ * The $responsesummary is a string representation of the response to be submitted.
+ * The exact meaning of that depends on the particular question type. These strings
+ * are passed to the un_summarise_response method of the question to decode.
+ *
+ * @param question_attempt $qa the question attempt for which we are generating POST data.
+ * @param string $responsesummary a textual summary of the response, as described above.
+ * @param bool $checkbutton if simulate a click on the check button, else simulate save.
+ * This should only be used with behaviours that have a check button.
+ * @return array the simulated post data that can be passed to $quba->process_all_actions.
+ */
+ public function get_simulated_post_data_for_question_attempt(
+ question_attempt $qa, $responsesummary, $checkbutton) {
+
+ $question = $qa->get_question();
+ if (!$question instanceof question_with_responses) {
+ return [];
+ }
+
+ $postdata = [];
+ $postdata[$qa->get_control_field_name('sequencecheck')] = (string)$qa->get_sequence_check_count();
+ $postdata[$qa->get_flag_field_name()] = (string)(int)$qa->is_flagged();
+
+ $response = $question->un_summarise_response($responsesummary);
+ foreach ($response as $name => $value) {
+ $postdata[$qa->get_qt_field_name($name)] = (string)$value;
+ }
+
+ // TODO handle behaviour variables better than this.
+ if ($checkbutton) {
+ $postdata[$qa->get_behaviour_field_name('submit')] = 1;
+ }
+
+ return $postdata;
+ }
}
}
}
+ public function un_summarise_response(string $summary) {
+ if (!empty($summary)) {
+ return ['answer' => text_to_html($summary)];
+ } else {
+ return [];
+ }
+ }
+
public function get_correct_response() {
return null;
}
return null;
}
+ public function un_summarise_response(string $response) {
+ return [];
+ }
+
public function classify_response(array $response) {
return array();
}
return $resp;
}
+ public function un_summarise_response(string $summary) {
+ if ($this->has_separate_unit_field()) {
+ throw new coding_exception('Sorry, but at the moment un_summarise_response cannot handle the
+ has_separate_unit_field case for numerical questions.
+ If you need this, you will have to implement it yourself.');
+ }
+
+ if (!empty($summary)) {
+ return ['answer' => $summary];
+ } else {
+ return [];
+ }
+ }
+
public function is_gradable_response(array $response) {
return array_key_exists('answer', $response) &&
($response['answer'] || $response['answer'] === '0' || $response['answer'] === 0);
/**
* Produce a plain text summary of a response.
- * @param $response a response, as might be passed to {@link grade_response()}.
+ * @param array $response a response, as might be passed to {@link grade_response()}.
* @return string a plain text summary of that response, that could be used in reports.
*/
public function summarise_response(array $response);
+ /**
+ * If possible, construct a response that could have lead to the given
+ * response summary. This is basically the opposite of {@link summarise_response()}
+ * but it is intended only to be used for testing.
+ *
+ * @param string $summary a string, which might have come from summarise_response
+ * @return array a response that could have lead to that.
+ */
+ public function un_summarise_response(string $summary);
+
/**
* Categorise the student's response according to the categories defined by
* get_possible_responses.
public function is_gradable_response(array $response) {
return $this->is_complete_response($response);
}
+
+ public function un_summarise_response(string $summary) {
+ throw new coding_exception('This question type (' . get_class($this) .
+ ' does not implement the un_summarise_response testing method.');
+ }
}
}
}
+ public function un_summarise_response(string $summary) {
+ if (!empty($summary)) {
+ return ['answer' => $summary];
+ } else {
+ return [];
+ }
+ }
+
public function is_complete_response(array $response) {
return array_key_exists('answer', $response) &&
($response['answer'] || $response['answer'] === '0');
}
}
+ public function un_summarise_response(string $summary) {
+ if ($summary === get_string('true', 'qtype_truefalse')) {
+ return ['answer' => '1'];
+ } else if ($summary === get_string('false', 'qtype_truefalse')) {
+ return ['answer' => '0'];
+ } else {
+ return [];
+ }
+ }
+
public function classify_response(array $response) {
if (!array_key_exists('answer', $response)) {
return array($this->id => question_classified_response::no_response());