Skip to content

Commit

Permalink
Implement response analysis methods
Browse files Browse the repository at this point in the history
That is get_possible_responses in the question type class and
classify_response in the question classes.

Co-authored-by: Mahmoud Kassaei <[email protected]>
  • Loading branch information
timhunt and Mahmoud Kassaei committed May 2, 2024
1 parent df09c0e commit 20be882
Show file tree
Hide file tree
Showing 6 changed files with 354 additions and 18 deletions.
1 change: 1 addition & 0 deletions lang/en/qtype_oumatrix.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
$string['rowshdr'] = 'Matrix rows (sub-questions)';
$string['rowanswerlist'] = 'Select answers';
$string['rowx'] = 'Row{$a})';
$string['selected'] = 'Selected';
$string['shuffleanswers'] = 'Shuffle the items?';
$string['shuffleanswers_desc'] = 'Whether options should be randomly shuffled for each attempt by default.';
$string['shuffleanswers_help'] = 'If enabled, the order of the row items is randomly shuffled for each attempt, provided that "Shuffle within questions" in the activity settings is also enabled.';
Expand Down
90 changes: 90 additions & 0 deletions question.php
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,51 @@ public function is_complete_response(array $response): bool {
return true;
}

public function classify_response(array $response): array {
$classifiedresponse = [];
foreach ($this->roworder as $key => $rownumber) {
$row = $this->rows[$rownumber];
$partname = format_string($row->name);
if (!array_key_exists($this->field($key), $response)) {
$classifiedresponse[$partname] = question_classified_response::no_response();
continue;
}

$selectedcolumn = $this->columns[$response[$this->field($key)]];
$classifiedresponse[$partname] = new question_classified_response(
$selectedcolumn->number,
format_string($selectedcolumn->name),
(int) array_key_exists($response[$this->field($key)], $row->correctanswers),
);
}

return $classifiedresponse;
}

public function prepare_simulated_post_data($simulatedresponse): array {
// Expected structure of $simulatedresponse is Row field name => Col name.
// Each row must be present, in order.
$postdata = [];
$subquestions = array_keys($simulatedresponse);
$answers = array_values($simulatedresponse);

foreach ($this->roworder as $key => $rownumber) {
$row = $this->rows[$rownumber];
if ($row->name !== $subquestions[$key]) {
continue;
}
if ($key === ($row->number - 1) && $row->name === $subquestions[$key]) {
foreach ($this->columns as $column) {
if ($column->name !== $answers[$key]) {
continue;
}
$postdata[$this->field($key)] = $column->number;
}
}
}
return $postdata;
}

public function grade_response(array $response): array {
// Retrieve the number of right responses and the total number of responses.
[$numrightparts, $total] = $this->get_num_parts_right($response);
Expand Down Expand Up @@ -367,6 +412,51 @@ public function is_complete_response(array $response): bool {
return true;
}

public function classify_response(array $response) {
$classifiedresponse = [];
foreach ($this->roworder as $rowkey => $rownumber) {
$row = $this->rows[$rownumber];
$rowname = format_string($row->name);

foreach ($this->columns as $column) {
if ($this->is_choice_selected($response, $rowkey, $column->number)) {
$classifiedresponse[$rowname . ': ' . format_string($column->name)] =
new question_classified_response(
1,
get_string('selected', 'qtype_oumatrix'),
array_key_exists($column->number, $row->correctanswers) / count($row->correctanswers),
);
}
}
}

return $classifiedresponse;
}

public function prepare_simulated_post_data($simulatedresponse): array {
$postdata = [];
$subquestions = array_keys($simulatedresponse);
$answers = array_values($simulatedresponse);
foreach ($this->roworder as $key => $rowid) {
$row = $this->rows[$rowid];
$rowanswers = $answers[$key];
if ($key === ($row->number - 1) && $row->name === $subquestions[$key]) {
foreach ($this->columns as $colid => $column) {
// Set the field to '0' initially.
$postdata[$this->field($key, $column->number)] = '0';
foreach ($rowanswers as $colnumber => $colname) {
if ($row->name === $subquestions[$key] &&
$column->number === $colnumber && $column->name === $colname) {
// Set the field to '1' if it has been ticked..
$postdata[$this->field($key, $column->number)] = '1';
}
}
}
}
}
return $postdata;
}

public function grade_response(array $response): array {
// Retrieve the number of right responses and the total number of responses.
if ($this->grademethod == 'allnone') {
Expand Down
60 changes: 43 additions & 17 deletions questiontype.php
Original file line number Diff line number Diff line change
Expand Up @@ -316,30 +316,56 @@ public function get_num_correct_choices(stdClass $questiondata): int {
}

public function get_possible_responses($questiondata) {
if ($questiondata->options->single) {
$responses = [];
if ($questiondata->options->inputtype == 'single') {
return $this->get_possible_responses_single($questiondata);
} else {
return $this->get_possible_responses_multiple($questiondata);
}
}

// TODO: Sort out this funtion to work with rows and columns, etc.
foreach ($questiondata->options->answers as $aid => $answer) {
$responses[$aid] = new question_possible_response(
question_utils::to_plain_text($answer->answer, $answer->answerformat),
$answer->fraction);
/**
* Do the radio button case of get_possible_responses.
*
* @param stdClass $questiondata the question definition data.
* @return array as for get_possible_responses.
*/
protected function get_possible_responses_single(stdClass $questiondata): array {
$parts = [];
foreach ($questiondata->rows as $row) {
$responses = [];
foreach ($questiondata->columns as $column) {
$responses[$column->number] = new question_possible_response(
format_string($column->name),
(int) ($column->number == $row->correctanswers)
);
}

$responses[null] = question_possible_response::no_response();
return [$questiondata->id => $responses];
} else {
$parts = [];
$parts[format_string($row->name)] = $responses;
}
return $parts;
}

foreach ($questiondata->options->answers as $aid => $answer) {
$parts[$aid] = [
$aid => new question_possible_response(question_utils::to_plain_text(
$answer->answer, $answer->answerformat), $answer->fraction),
/**
* Do the checkbox button case of get_possible_responses.
*
* @param stdClass $questiondata the question definition data.
* @return array as for get_possible_responses.
*/
protected function get_possible_responses_multiple(stdClass $questiondata): array {
$parts = [];
foreach ($questiondata->rows as $row) {
$rowname = format_string($row->name);
$correctanswer = explode(',', $row->correctanswers);
foreach ($questiondata->columns as $column) {
$parts[$rowname . ': ' . format_string($column->name)] = [
1 => new question_possible_response(
get_string('selected', 'qtype_oumatrix'),
in_array($column->number, $correctanswer) / count($correctanswer),
),
];
}

return $parts;
}
return $parts;
}

public function import_from_xml($data, $question, qformat_xml $format, $extra = null) {
Expand Down
74 changes: 74 additions & 0 deletions tests/question_multiple_test.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

use question_attempt_step;
use question_state;
use question_classified_response;

defined('MOODLE_INTERNAL') || die();

Expand Down Expand Up @@ -100,6 +101,79 @@ public function test_is_gradable_response(): void {
$this->assertTrue($question->is_gradable_response($response), $question->is_complete_response($response));
}

public function test_classify_response_multiple(): void {
$this->resetAfterTest();
$question = \test_question_maker::make_question('oumatrix', 'food_multiple');
$question->shuffleanswers = 0;
$question->start_attempt(new question_attempt_step(), 1);

// Test a correct response.
$response = $question->prepare_simulated_post_data([
'Proteins' => [1 => 'Chicken breast', 3 => 'Salmon fillet', 6 => 'Steak'],
'Vegetables' => [2 => 'Carrot', 4 => 'Asparagus', 7 => 'Potato'],
'Fats' => [5 => 'Olive oil']]);
$this->assertEquals([
"Proteins: Chicken breast" => new question_classified_response(1, 'Selected', 1 / 3),
"Proteins: Salmon fillet" => new question_classified_response(1, 'Selected', 1 / 3),
"Proteins: Steak" => new question_classified_response(1, 'Selected', 1 / 3),
"Vegetables: Carrot" => new question_classified_response(1, 'Selected', 1 / 3),
"Vegetables: Asparagus" => new question_classified_response(1, 'Selected', 1 / 3),
"Vegetables: Potato" => new question_classified_response(1, 'Selected', 1 / 3),
"Fats: Olive oil" => new question_classified_response(1, 'Selected', 1),
], $question->classify_response($response));

// Test a partial response.
$response = $question->prepare_simulated_post_data([
'Proteins' => [1 => 'Chicken breast', 4 => 'Asparagus'],
'Vegetables' => [2 => 'Carrot', 1 => 'Chicken breast'],
'Fats' => [5 => 'Olive oil']]);
$this->assertEquals([
"Proteins: Chicken breast" => new question_classified_response(1, 'Selected', 1 / 3),
"Proteins: Asparagus" => new question_classified_response(1, 'Selected', 0),
"Vegetables: Chicken breast" => new question_classified_response(1, 'Selected', 0),
"Vegetables: Carrot" => new question_classified_response(1, 'Selected', 1 / 3),
"Fats: Olive oil" => new question_classified_response(1, 'Selected', 1),
], $question->classify_response($response));
}

public function test_prepare_simulated_post_data_multiple(): void {
$this->resetAfterTest();
$question = \test_question_maker::make_question('oumatrix', 'food_multiple');
$question->shuffleanswers = 0;
$question->start_attempt(new question_attempt_step(), 1);

$response = ['Proteins' => [1 => 'Chicken breast', 3 => 'Salmon fillet', 6 => 'Steak'],
'Vegetables' => [2 => 'Carrot', 4 => 'Asparagus', 7 => 'Potato'], 'Fats' => [5 => 'Olive oil']];

$expected = [
'rowanswers0_1' => '1',
'rowanswers0_2' => '0',
'rowanswers0_3' => '1',
'rowanswers0_4' => '0',
'rowanswers0_5' => '0',
'rowanswers0_6' => '1',
'rowanswers0_7' => '0',

'rowanswers1_1' => '0',
'rowanswers1_2' => '1',
'rowanswers1_3' => '0',
'rowanswers1_4' => '1',
'rowanswers1_5' => '0',
'rowanswers1_6' => '0',
'rowanswers1_7' => '1',

'rowanswers2_1' => '0',
'rowanswers2_2' => '0',
'rowanswers2_3' => '0',
'rowanswers2_4' => '0',
'rowanswers2_5' => '1',
'rowanswers2_6' => '0',
'rowanswers2_7' => '0',
];
$this->assertEquals($expected, $question->prepare_simulated_post_data($response));
}


public function test_is_same_response(): void {
$question = \test_question_maker::make_question('oumatrix', 'food_multiple');
$question->start_attempt(new question_attempt_step(), 1);
Expand Down
68 changes: 68 additions & 0 deletions tests/question_single_test.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

namespace qtype_oumatrix;

use question_attempt_step;
use question_state;
use question_classified_response;

defined('MOODLE_INTERNAL') || die();

Expand Down Expand Up @@ -60,6 +62,72 @@ public function test_is_gradable_response(): void {
$this->assertEquals($question->is_gradable_response($response), $question->is_complete_response($response));
}

public function test_classify_response_single(): void {
$this->resetAfterTest();
$question = \test_question_maker::make_question('oumatrix', 'animals_single');
$question->shuffleanswers = 0;
$question->start_attempt(new question_attempt_step(), 1);

// All sub-questions are answered correctly.
$response = $question->prepare_simulated_post_data(
['Bee' => 'Insects', 'Salmon' => 'Fish', 'Seagull' => 'Birds', 'Dog' => 'Mammals']);
$this->assertEquals([
'Bee' => new question_classified_response(1, 'Insects', 1),
'Salmon' => new question_classified_response(2, 'Fish', 1),
'Seagull' => new question_classified_response(3, 'Birds', 1),
'Dog' => new question_classified_response(4, 'Mammals', 1),
], $question->classify_response($response));

// Three sub-questions are answered correctly and one incorrectly.
$response = $question->prepare_simulated_post_data(
['Bee' => 'Insects', 'Salmon' => 'Birds', 'Seagull' => 'Birds', 'Dog' => 'Mammals']);
$this->assertEquals([
'Bee' => new question_classified_response(1, 'Insects', 1),
'Salmon' => new question_classified_response(3, 'Birds', 0),
'Seagull' => new question_classified_response(3, 'Birds', 1),
'Dog' => new question_classified_response(4, 'Mammals', 1),
], $question->classify_response($response));

// Two sub-questions are answered correctly and two incorrectly.
$response = $question->prepare_simulated_post_data(
['Bee' => 'Insects', 'Salmon' => 'Birds', 'Seagull' => 'Birds', 'Dog' => 'Insects']);
$this->assertEquals([
'Bee' => new question_classified_response(1, 'Insects', 1),
'Salmon' => new question_classified_response(3, 'Birds', 0),
'Seagull' => new question_classified_response(3, 'Birds', 1),
'Dog' => new question_classified_response(1, 'Insects', 0),
], $question->classify_response($response));

// Two sub-questions are answered correctly, one incorrectly, and the second sub-question is not answered.
$response = $question->prepare_simulated_post_data(
['Bee' => 'Insects', 'Salmon' => '', 'Seagull' => 'Birds', 'Dog' => 'Insects']);
$this->assertEquals([
'Bee' => new question_classified_response(1, 'Insects', 1),
'Salmon' => question_classified_response::no_response(),
'Seagull' => new question_classified_response(3, 'Birds', 1),
'Dog' => new question_classified_response(1, 'Insects', 0),
], $question->classify_response($response));
}

public function test_prepare_simulated_post_data_single(): void {
$this->resetAfterTest();
$question = \test_question_maker::make_question('oumatrix', 'animals_single');
$question->shuffleanswers = 0;
$question->start_attempt(new question_attempt_step(), 1);

$response = ['Bee' => 'Insects', 'Salmon' => 'Fish', 'Seagull' => 'Birds', 'Dog' => 'Mammals'];
$expected = ['rowanswers0' => 1, 'rowanswers1' => 2, 'rowanswers2' => 3, 'rowanswers3' => 4];
$this->assertEquals($expected, $question->prepare_simulated_post_data($response));

$response = ['Bee' => 'Insects', 'Salmon' => 'Birds', 'Seagull' => 'Birds', 'Dog' => 'Mammals'];
$expected = ['rowanswers0' => 1, 'rowanswers1' => 3, 'rowanswers2' => 3, 'rowanswers3' => 4];
$this->assertEquals($expected, $question->prepare_simulated_post_data($response));

$response = ['Bee' => 'Insects', 'Salmon' => 'Birds', 'Seagull' => 'Birds', 'Dog' => 'Insects'];
$expected = ['rowanswers0' => 1, 'rowanswers1' => 3, 'rowanswers2' => 3, 'rowanswers3' => 1];
$this->assertEquals($expected, $question->prepare_simulated_post_data($response));
}

public function test_is_same_response(): void {
$question = \test_question_maker::make_question('oumatrix');
$question->start_attempt(new question_attempt_step(), 1);
Expand Down
Loading

0 comments on commit 20be882

Please sign in to comment.