Merge branch 'MDL-27414' of git://github.com/jmvedrine/moodle

This commit is contained in:
Dan Poltawski 2014-02-17 12:06:42 +08:00
commit 35a8d26926
18 changed files with 2125 additions and 194 deletions

View File

@ -58,9 +58,7 @@ class qtype_match_renderer extends qtype_with_combined_feedback_renderer {
$result .= html_writer::start_tag('tr', array('class' => 'r' . $parity));
$fieldname = 'sub' . $key;
$result .= html_writer::tag('td', $question->format_text(
$question->stems[$stemid], $question->stemformat[$stemid],
$qa, 'qtype_match', 'subquestion', $stemid),
$result .= html_writer::tag('td', $this->format_stem_text($qa, $stemid),
array('class' => 'text'));
$classes = 'control';
@ -109,6 +107,20 @@ class qtype_match_renderer extends qtype_with_combined_feedback_renderer {
return $this->combined_feedback($qa);
}
/**
* Format each question stem. Overwritten by randomsamatch renderer.
*
* @param question_attempt $qa
* @param integer $stemid stem index
* @return string
*/
public function format_stem_text($qa, $stemid) {
$question = $qa->get_question();
return $question->format_text(
$question->stems[$stemid], $question->stemformat[$stemid],
$qa, 'qtype_match', 'subquestion', $stemid);
}
protected function format_choices($question) {
$choices = array();
foreach ($question->get_choice_order() as $key => $choiceid) {
@ -125,9 +137,7 @@ class qtype_match_renderer extends qtype_with_combined_feedback_renderer {
$choices = $this->format_choices($question);
$right = array();
foreach ($stemorder as $key => $stemid) {
$right[] = $question->format_text($question->stems[$stemid],
$question->stemformat[$stemid], $qa,
'qtype_match', 'subquestion', $stemid) . ' ' .
$right[] = $this->format_stem_text($qa, $stemid) . ' ' .
$choices[$question->get_right_choice_for($stemid)];
}

View File

@ -0,0 +1,71 @@
<?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/>.
/**
* Serve question type files
*
* @package qtype_randomsamatch
* @copyright 2013 Jean-Michel Vedrine
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Random shortanswer matching question type conversion handler.
*
* @copyright 2013 Jean-Michel Vedrine
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
*/
class moodle1_qtype_randomsamatch_handler extends moodle1_qtype_handler {
/**
* Returns the list of paths within one <QUESTION> that this qtype needs to have included
* in the grouped question structure
*
* @return array of strings
*/
public function get_question_subpaths() {
return array(
'RANDOMSAMATCH',
);
}
/**
* Appends the randomsamatch specific information to the question.
*
* @param array $data grouped question data
* @param array $raw grouped raw QUESTION data
*/
public function process_question(array $data, array $raw) {
// Convert match options.
if (isset($data['randomsamatch'])) {
$randomsamatch = $data['randomsamatch'][0];
} else {
$randomsamatch = array('choose' => 4);
}
$randomsamatch['id'] = $this->converter->get_nextid();
$randomsamatch['subcats'] = 1;
$randomsamatch['correctfeedback'] = '';
$randomsamatch['correctfeedbackformat'] = FORMAT_HTML;
$randomsamatch['partiallycorrectfeedback'] = '';
$randomsamatch['partiallycorrectfeedbackformat'] = FORMAT_HTML;
$randomsamatch['incorrectfeedback'] = '';
$randomsamatch['incorrectfeedbackformat'] = FORMAT_HTML;
$this->write_xml('randomsamatch', $randomsamatch, array('/randomsamatch/id'));
}
}

View File

@ -49,14 +49,16 @@ class backup_qtype_randomsamatch_plugin extends backup_qtype_plugin {
// Now create the qtype own structures.
$randomsamatch = new backup_nested_element('randomsamatch', array('id'), array(
'choose'));
'choose', 'subcats', 'correctfeedback', 'correctfeedbackformat',
'partiallycorrectfeedback', 'partiallycorrectfeedbackformat',
'incorrectfeedback', 'incorrectfeedbackformat', 'shownumcorrect'));
// Now the own qtype tree.
$pluginwrapper->add_child($randomsamatch);
// Set source to populate the data.
$randomsamatch->set_source_table('question_randomsamatch',
array('question' => backup::VAR_PARENTID));
$randomsamatch->set_source_table('qtype_randomsamatch_options',
array('questionid' => backup::VAR_PARENTID));
return $plugin;
}

View File

@ -41,7 +41,7 @@ class restore_qtype_randomsamatch_plugin extends restore_qtype_plugin {
$paths = array();
// Add own qtype stuff
// Add own qtype stuff.
$elename = 'randomsamatch';
$elepath = $this->get_pathfor('/randomsamatch');
$paths[] = new restore_path_element($elename, $elepath);
@ -64,14 +64,34 @@ class restore_qtype_randomsamatch_plugin extends restore_qtype_plugin {
$questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
// If the question has been created by restore, we need to create its
// question_randomsamatch too.
// qtype_randomsamatch_options too.
if ($questioncreated) {
// Fill in some field that were added in 2.1, and so which may be missing
// from backups made in older versions of Moodle.
if (!isset($data->subcats)) {
$data->subcats = 1;
}
if (!isset($data->correctfeedback)) {
$data->correctfeedback = '';
$data->correctfeedbackformat = FORMAT_HTML;
}
if (!isset($data->partiallycorrectfeedback)) {
$data->partiallycorrectfeedback = '';
$data->partiallycorrectfeedbackformat = FORMAT_HTML;
}
if (!isset($data->incorrectfeedback)) {
$data->incorrectfeedback = '';
$data->incorrectfeedbackformat = FORMAT_HTML;
}
if (!isset($data->shownumcorrect)) {
$data->shownumcorrect = 0;
}
// Adjust some columns.
$data->question = $newquestionid;
$data->questionid = $newquestionid;
// Insert record.
$newitemid = $DB->insert_record('question_randomsamatch', $data);
$newitemid = $DB->insert_record('qtype_randomsamatch_options', $data);
// Create mapping.
$this->set_mapping('question_randomsamatch', $oldid, $newitemid);
$this->set_mapping('qtype_randomsamatch_options', $oldid, $newitemid);
}
}
@ -82,7 +102,7 @@ class restore_qtype_randomsamatch_plugin extends restore_qtype_plugin {
* answer is one comma separated list of hypen separated pairs
* containing question->id and question_answers->id
*/
public function recode_state_answer($state) {
public function recode_legacy_state_answer($state) {
$answer = $state->answer;
$resultarr = array();
foreach (explode(',', $answer) as $pair) {
@ -95,4 +115,17 @@ class restore_qtype_randomsamatch_plugin extends restore_qtype_plugin {
}
return implode(',', $resultarr);
}
/**
* Return the contents of this qtype to be processed by the links decoder.
*/
public static function define_decode_contents() {
$contents = array();
$fields = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback');
$contents[] = new restore_decode_content('qtype_randomsamatch_options', $fields, 'qtype_randomsamatch_options');
return $contents;
}
}

View File

@ -4,15 +4,23 @@
xsi:noNamespaceSchemaLocation="../../../../lib/xmldb/xmldb.xsd"
>
<TABLES>
<TABLE NAME="question_randomsamatch" COMMENT="Info about a random short-answer matching question">
<TABLE NAME="qtype_randomsamatch_options" COMMENT="Info about a random short-answer matching question">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="question" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Foreign key references question.id."/>
<FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Foreign key references question.id."/>
<FIELD NAME="choose" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="4" SEQUENCE="false" COMMENT="Number of subquestions to randomly generate."/>
<FIELD NAME="subcats" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="1" SEQUENCE="false" COMMENT="Whether to include or not the subcategories."/>
<FIELD NAME="correctfeedback" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Feedback shown for any correct response."/>
<FIELD NAME="correctfeedbackformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="partiallycorrectfeedback" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Feedback shown for any partially correct response."/>
<FIELD NAME="partiallycorrectfeedbackformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="incorrectfeedback" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Feedback shown for any incorrect response."/>
<FIELD NAME="incorrectfeedbackformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="shownumcorrect" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="If true, then when the user gets the question partially correct, tell them how many choices they got correct alongside the feedback."/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="question" TYPE="foreign" FIELDS="question" REFTABLE="question" REFFIELDS="id"/>
<KEY NAME="questionid" TYPE="foreign-unique" FIELDS="questionid" REFTABLE="question" REFFIELDS="id"/>
</KEYS>
</TABLE>
</TABLES>

View File

@ -0,0 +1,190 @@
<?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/>.
/**
* Matching question type upgrade code.
*
* @package qtype_randomsamatch
* @copyright 2013 Jean-Michel Vedrine
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Upgrade code for the random short answer matching question type.
* @param int $oldversion the version we are upgrading from.
*/
function xmldb_qtype_randomsamatch_upgrade($oldversion) {
global $CFG, $DB;
$dbman = $DB->get_manager();
// Moodle v2.2.0 release upgrade line.
// Put any upgrade step following this.
// Moodle v2.3.0 release upgrade line.
// Put any upgrade step following this.
// Moodle v2.4.0 release upgrade line.
// Put any upgrade step following this.
if ($oldversion < 2013110501) {
// Define table question_randomsamatch to be renamed to qtype_randomsamatch_options.
$table = new xmldb_table('question_randomsamatch');
// Launch rename table for qtype_randomsamatch_options.
if ($dbman->table_exists($table)) {
$dbman->rename_table($table, 'qtype_randomsamatch_options');
}
// Record that qtype_randomsamatch savepoint was reached.
upgrade_plugin_savepoint(true, 2013110501, 'qtype', 'randomsamatch');
}
if ($oldversion < 2013110502) {
// Define key question (foreign) to be dropped form qtype_randomsamatch_options.
$table = new xmldb_table('qtype_randomsamatch_options');
$field = new xmldb_field('question', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'id');
if ($dbman->field_exists($table, $field)) {
// Launch drop key question.
$key = new xmldb_key('question', XMLDB_KEY_FOREIGN, array('question'), 'question', array('id'));
$dbman->drop_key($table, $key);
// Launch rename field question.
$dbman->rename_field($table, $field, 'questionid');
$key = new xmldb_key('questionid', XMLDB_KEY_FOREIGN_UNIQUE, array('questionid'), 'question', array('id'));
// Launch add key questionid.
$dbman->add_key($table, $key);
}
// Record that qtype_randomsamatch savepoint was reached.
upgrade_plugin_savepoint(true, 2013110502, 'qtype', 'randomsamatch');
}
if ($oldversion < 2013110503) {
// Add subcats field.
$table = new xmldb_table('qtype_randomsamatch_options');
// Define field subcats to be added to qtype_randomsamatch_options.
$field = new xmldb_field('subcats', XMLDB_TYPE_INTEGER, 2, null,
XMLDB_NOTNULL, null, '1', 'choose');
// Conditionally launch add field subcats.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Add combined feedback fields.
// Define field correctfeedback to be added to qtype_randomsamatch_options.
$field = new xmldb_field('correctfeedback', XMLDB_TYPE_TEXT, 'small', null,
null, null, null, 'subcats');
// Conditionally launch add field correctfeedback.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
// Now fill it with ''.
$DB->set_field('qtype_randomsamatch_options', 'correctfeedback', '');
// Now add the not null constraint.
$field = new xmldb_field('correctfeedback', XMLDB_TYPE_TEXT, 'small', null,
XMLDB_NOTNULL, null, null, 'subcats');
$dbman->change_field_notnull($table, $field);
}
// Define field correctfeedbackformat to be added to qtype_randomsamatch_options.
$field = new xmldb_field('correctfeedbackformat', XMLDB_TYPE_INTEGER, '2', null,
XMLDB_NOTNULL, null, '0', 'correctfeedback');
// Conditionally launch add field correctfeedbackformat.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Define field partiallycorrectfeedback to be added to qtype_randomsamatch_options.
$field = new xmldb_field('partiallycorrectfeedback', XMLDB_TYPE_TEXT, 'small', null,
null, null, null, 'correctfeedbackformat');
// Conditionally launch add field partiallycorrectfeedback.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
// Now fill it with ''.
$DB->set_field('qtype_randomsamatch_options', 'partiallycorrectfeedback', '');
// Now add the not null constraint.
$field = new xmldb_field('partiallycorrectfeedback', XMLDB_TYPE_TEXT, 'small', null,
XMLDB_NOTNULL, null, null, 'correctfeedbackformat');
$dbman->change_field_notnull($table, $field);
}
// Define field partiallycorrectfeedbackformat to be added to qtype_randomsamatch_options.
$field = new xmldb_field('partiallycorrectfeedbackformat', XMLDB_TYPE_INTEGER, '2', null,
XMLDB_NOTNULL, null, '0', 'partiallycorrectfeedback');
// Conditionally launch add field partiallycorrectfeedbackformat.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Define field incorrectfeedback to be added to qtype_randomsamatch_options.
$field = new xmldb_field('incorrectfeedback', XMLDB_TYPE_TEXT, 'small', null,
null, null, null, 'partiallycorrectfeedbackformat');
// Conditionally launch add field incorrectfeedback.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
// Now fill it with ''.
$DB->set_field('qtype_randomsamatch_options', 'incorrectfeedback', '');
// Now add the not null constraint.
$field = new xmldb_field('incorrectfeedback', XMLDB_TYPE_TEXT, 'small', null,
XMLDB_NOTNULL, null, null, 'partiallycorrectfeedbackformat');
$dbman->change_field_notnull($table, $field);
}
// Define field incorrectfeedbackformat to be added to qtype_randomsamatch_options.
$field = new xmldb_field('incorrectfeedbackformat', XMLDB_TYPE_INTEGER, '2', null,
XMLDB_NOTNULL, null, '0', 'incorrectfeedback');
// Conditionally launch add field incorrectfeedbackformat.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Define field shownumcorrect to be added to qtype_randomsamatch_options.
$field = new xmldb_field('shownumcorrect', XMLDB_TYPE_INTEGER, '2', null,
XMLDB_NOTNULL, null, '0', 'incorrectfeedbackformat');
// Conditionally launch add field shownumcorrect.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Record that qtype_randomsamatch savepoint was reached.
upgrade_plugin_savepoint(true, 2013110503, 'qtype', 'randomsamatch');
}
return true;
}

View File

@ -0,0 +1,243 @@
<?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/>.
/**
* Upgrade library code for the randomsamatch question type.
*
* @package qtype_randomsamatch
* @copyright 2013 Jean-Michel Vedrine
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Class for converting attempt data for randomsamatch questions when upgrading
* attempts to the new question engine.
*
* This class is used by the code in question/engine/upgrade/upgradelib.php.
*
* @copyright 2013 Jean-Michel Vedrine
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_randomsamatch_qe2_attempt_updater extends question_qtype_attempt_updater {
/** @var array of question stems. */
protected $stems;
/** @var array of question stems format. */
protected $stemformat;
/** @var array of choices that can be matched to each stem. */
protected $choices;
/** @var array index of the right choice for each stem. */
protected $right;
/** @var array id of the right answer for each stem (used by {@link lookup_choice}). */
protected $rightanswerid;
/** @var array shuffled stem indexes. */
protected $stemorder;
/** @var array shuffled choice indexes. */
protected $choiceorder;
/** @var array flipped version of the choiceorder array. */
protected $flippedchoiceorder;
public function question_summary() {
return ''; // Done later, after we know which shortanswer questions are used.
}
public function right_answer() {
return ''; // Done later, after we know which shortanswer questions are used.
}
/**
* Explode the answer saved as a string in state
*
* @param string $answer comma separated list of dash separated pairs
* @return array
*/
protected function explode_answer($answer) {
if (!$answer) {
return array();
}
$bits = explode(',', $answer);
$selections = array();
foreach ($bits as $bit) {
list($stem, $choice) = explode('-', $bit);
$selections[$stem] = $choice;
}
return $selections;
}
protected function make_summary($pairs) {
$bits = array();
foreach ($pairs as $stem => $answer) {
$bits[] = $stem . ' -> ' . $answer;
}
return implode('; ', $bits);
}
/**
* Find the index corresponding to a choice
*
* @param integer $choice
* @return integer
*/
protected function lookup_choice($choice) {
if (array_key_exists($choice, $this->choices)) {
// Easy case: choice is a key in the choices array.
return $choice;
} else {
// But choice can also be the id of a shortanser correct answer
// without been a key of the choices array, in that case we need
// to first find the shortanswer id, then find the choices index
// associated to it.
$questionid = array_search($choice, $this->rightanswerid);
if ($questionid) {
return $this->right[$questionid];
}
}
return null;
}
public function response_summary($state) {
$choices = $this->explode_answer($state->answer);
if (empty($choices)) {
return null;
}
$pairs = array();
foreach ($choices as $stemid => $choicekey) {
if (array_key_exists($stemid, $this->stems) && $choices[$stemid]) {
$choiceid = $this->lookup_choice($choicekey);
if ($choiceid) {
$pairs[$this->stems[$stemid]] = $this->choices[$choiceid];
} else {
$this->logger->log_assumption("Dealing with a place where the
student selected a choice that was later deleted for
randomsamatch question {$this->question->id}");
$pairs[$this->stems[$stemid]] = '[CHOICE THAT WAS LATER DELETED]';
}
}
}
if ($pairs) {
return $this->make_summary($pairs);
} else {
return '';
}
}
public function was_answered($state) {
$choices = $this->explode_answer($state->answer);
foreach ($choices as $choice) {
if ($choice) {
return true;
}
}
return false;
}
public function set_first_step_data_elements($state, &$data) {
$this->stems = array();
$this->stemformat = array();
$this->choices = array();
$this->right = array();
$this->rightanswer = array();
$choices = $this->explode_answer($state->answer);
$this->stemorder = array();
foreach ($choices as $key => $notused) {
$this->stemorder[] = $key;
}
$wrappedquestions = array();
// TODO test what happen when some questions are missing.
foreach ($this->stemorder as $questionid) {
$wrappedquestions[] = $this->load_question($questionid);
}
foreach ($wrappedquestions as $wrappedquestion) {
// We only take into account the first correct answer.
$foundcorrect = false;
foreach ($wrappedquestion->options->answers as $answer) {
if ($foundcorrect || $answer->fraction != 1.0) {
unset($wrappedquestion->options->answers[$answer->id]);
} else if (!$foundcorrect) {
$foundcorrect = true;
// Store right answer id, so we can use it later in lookup_choice.
$this->rightanswerid[$wrappedquestion->id] = $answer->id;
$key = array_search($answer->answer, $this->choices);
if ($key === false) {
$key = $answer->id;
$this->choices[$key] = $answer->answer;
$data['_choice_' . $key] = $answer->answer;
}
$this->stems[$wrappedquestion->id] = $wrappedquestion->questiontext;
$this->stemformat[$wrappedquestion->id] = $wrappedquestion->questiontextformat;
$this->right[$wrappedquestion->id] = $key;
$this->rightanswer[$wrappedquestion->id] = $answer->answer;
$data['_stem_' . $wrappedquestion->id] = $wrappedquestion->questiontext;
$data['_stemformat_' . $wrappedquestion->id] = $wrappedquestion->questiontextformat;
$data['_right_' . $wrappedquestion->id] = $key;
}
}
}
$this->choiceorder = array_keys($this->choices);
// We don't shuffle the choices as that seems unnecessary for old upgraded attempts.
$this->flippedchoiceorder = array_combine(
array_values($this->choiceorder), array_keys($this->choiceorder));
$data['_stemorder'] = implode(',', $this->stemorder);
$data['_choiceorder'] = implode(',', $this->choiceorder);
$this->updater->qa->questionsummary = $this->to_text($this->question->questiontext) . ' {' .
implode('; ', $this->stems) . '} -> {' . implode('; ', $this->choices) . '}';
$answer = array();
foreach ($this->stems as $key => $stem) {
$answer[$stem] = $this->choices[$this->right[$key]];
}
$this->updater->qa->rightanswer = $this->make_summary($answer);
}
public function supply_missing_first_step_data(&$data) {
throw new coding_exception('qtype_randomsamatch_updater::supply_missing_first_step_data ' .
'not tested');
$data['_stemorder'] = array();
$data['_choiceorder'] = array();
}
public function set_data_elements_for_step($state, &$data) {
$choices = $this->explode_answer($state->answer);
foreach ($this->stemorder as $i => $key) {
if (empty($choices[$key])) {
$data['sub' . $i] = 0;
continue;
}
$choice = $this->lookup_choice($choices[$key]);
if (array_key_exists($choice, $this->flippedchoiceorder)) {
$data['sub' . $i] = $this->flippedchoiceorder[$choice] + 1;
} else {
$data['sub' . $i] = 0;
}
}
}
public function load_question($questionid) {
return $this->qeupdater->load_question($questionid);
}
}

View File

@ -17,8 +17,7 @@
/**
* Defines the editing form for the randomsamatch question type.
*
* @package qtype
* @subpackage randomsamatch
* @package qtype_randomsamatch
* @copyright 2007 Jamie Pratt me@jamiep.org
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
*/
@ -41,20 +40,37 @@ class qtype_randomsamatch_edit_form extends question_edit_form {
}
$mform->addElement('select', 'choose',
get_string('randomsamatchnumber', 'quiz'), $questionstoselect);
get_string('randomsamatchnumber', 'qtype_randomsamatch'), $questionstoselect);
$mform->setType('feedback', PARAM_RAW);
$mform->addElement('advcheckbox', 'subcats',
get_string('subcats', 'qtype_randomsamatch'), null, null, array(0, 1));
$mform->addHelpButton('subcats', 'subcats', 'qtype_randomsamatch');
$mform->setDefault('subcats', 1);
$mform->addElement('hidden', 'fraction', 0);
$mform->setType('fraction', PARAM_RAW);
$this->add_combined_feedback_fields(true);
$this->add_interactive_settings(true, true);
}
protected function data_preprocessing($question) {
$question = parent::data_preprocessing($question);
$question = $this->data_preprocessing_combined_feedback($question, true);
$question = $this->data_preprocessing_hints($question, true, true);
if (!empty($question->options)) {
$question->choose = $question->options->choose;
$question->subcats = $question->options->subcats;
}
if (empty($question->name)) {
$question->name = get_string('randomsamatch', 'quiz');
$question->name = get_string('randomsamatch', 'qtype_randomsamatch');
}
if (empty($question->questiontext)) {
$question->questiontext = get_string('randomsamatchintro', 'quiz');
$question->questiontext = get_string('randomsamatchintro', 'qtype_randomsamatch');
}
return $question;
}
@ -66,12 +82,14 @@ class qtype_randomsamatch_edit_form extends question_edit_form {
public function validation($data, $files) {
global $DB;
$errors = parent::validation($data, $files);
if (isset($data->categorymoveto)) {
list($category) = explode(',', $data['categorymoveto']);
} else {
list($category) = explode(',', $data['category']);
}
$saquestions = question_bank::get_qtype('randomsamatch')->get_sa_candidates($category);
$saquestions = question_bank::get_qtype('randomsamatch')->get_available_saquestions_from_category(
$category, $data['subcats']);
$numberavailable = count($saquestions);
if ($saquestions === false) {
$a = new stdClass();

View File

@ -23,6 +23,7 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['insufficientoptions'] = 'Insufficient selection options are available for this question, therefore it is not available in this quiz. Please inform your teacher.';
$string['nosaincategory'] = 'There are no short answer questions in the category that you chose \'{$a->catname}\'. Choose a different category, make some questions in this category.';
$string['notenoughsaincategory'] = 'There is/are only {$a->nosaquestions} short answer questions in the category that you chose \'{$a->catname}\'. Choose a different category, make some more questions in this category or reduce the amount of questions you\'ve selected.';
$string['pluginname'] = 'Random short-answer matching';
@ -31,3 +32,8 @@ $string['pluginname_link'] = 'question/type/randomsamatch';
$string['pluginnameadding'] = 'Adding a Random short-answer matching question';
$string['pluginnameediting'] = 'Editing a Random short-answer matching question';
$string['pluginnamesummary'] = 'Like a Matching question, but created randomly from the short answer questions in a particular category.';
$string['randomsamatchnumber'] = 'Number of questions to select';
$string['randomsamatch'] = 'Random short-answer matching';
$string['randomsamatchintro'] = 'For each of the following questions, select the matching answer from the menu.';
$string['subcats'] = 'Include subcategories';
$string['subcats_help'] = 'If checked, questions will be choosen from subcategories too.';

View File

@ -0,0 +1,47 @@
<?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/>.
/**
* Serve question type files
*
* @since 2.0
* @package qtype_randomsamatch
* @copyright 2013 Jean-Michel Vedrine
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Checks file access for random short answer matching questions.
* @package qtype_randomsamatch
* @category files
* @param stdClass $course course object
* @param stdClass $cm course module object
* @param stdClass $context context object
* @param string $filearea file area
* @param array $args extra arguments
* @param bool $forcedownload whether or not force download
* @param array $options additional options affecting the file serving
* @return bool
*/
function qtype_randomsamatch_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) {
global $DB, $CFG;
require_once($CFG->libdir . '/questionlib.php');
question_pluginfile($course, $context, 'qtype_randomsamatch', $filearea, $args, $forcedownload, $options);
}

View File

@ -0,0 +1,140 @@
<?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/>.
/**
* Matching question definition class.
*
* @package qtype_randomsamatch
* @copyright 2013 Jean-Michel Vedrine
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/question/type/match/question.php');
/**
* Represents a randomsamatch question.
*
* @copyright 22013 Jean-Michel Vedrine
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_randomsamatch_question extends qtype_match_question {
/** @var qtype_randomsamatch_question_loader helper for loading the shortanswer questions. */
public $questionsloader;
public function start_attempt(question_attempt_step $step, $variant) {
$saquestions = $this->questionsloader->load_questions();
foreach ($saquestions as $wrappedquestion) {
// Store and save stem text and format.
$this->stems[$wrappedquestion->id] = $wrappedquestion->questiontext;
$this->stemformat[$wrappedquestion->id] = $wrappedquestion->questiontextformat;
$step->set_qt_var('_stem_' . $wrappedquestion->id, $this->stems[$wrappedquestion->id]);
$step->set_qt_var('_stemformat_' . $wrappedquestion->id, $this->stemformat[$wrappedquestion->id]);
// Find, store and save right choice id.
$key = $this->find_right_answer($wrappedquestion);
$this->right[$wrappedquestion->id] = $key;
$step->set_qt_var('_right_' . $wrappedquestion->id, $key);
// No need to save saquestions, it will be saved by parent class in _stemorder.
}
// Save all the choices.
foreach ($this->choices as $key => $answer) {
$step->set_qt_var('_choice_' . $key, $answer);
}
parent::start_attempt($step, $variant);
}
/**
* Find the corresponding choice id of the first correct answer of a shortanswer question.
* choice is added to the randomsamatch question if it doesn't already exist.
* @param object $wrappedquestion short answer question.
* @return int correct choice id.
*/
public function find_right_answer($wrappedquestion) {
// We only take into account *one* (the first) correct answer.
while ($answer = array_shift($wrappedquestion->answers)) {
if (!question_state::graded_state_for_fraction(
$answer->fraction)->is_incorrect()) {
// Store this answer as a choice, only if this is a new one.
$key = array_search($answer->answer, $this->choices);
if ($key === false) {
$key = $answer->id;
$this->choices[$key] = $answer->answer;
}
return $key;
}
}
// We should never get there.
throw new coding_exception('shortanswerquestionwithoutrightanswer', $wrappedquestion->id);
}
public function apply_attempt_state(question_attempt_step $step) {
$saquestions = explode(',', $step->get_qt_var('_stemorder'));
foreach ($saquestions as $questionid) {
$this->stems[$questionid] = $step->get_qt_var('_stem_' . $questionid);
$this->stemformat[$questionid] = $step->get_qt_var('_stemformat_' . $questionid);
$key = $step->get_qt_var('_right_' . $questionid);
$this->right[$questionid] = $key;
$this->choices[$key] = $step->get_qt_var('_choice_' . $key);
}
parent::apply_attempt_state($step);
}
}
/**
* This class is responsible for loading the questions that a question needs from the database.
*
* @copyright 2013 Jean-Michel vedrine
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_randomsamatch_question_loader {
/** @var array hold available shortanswers questionid to choose from. */
protected $availablequestions;
/** @var int how many questions to load. */
protected $choose;
/**
* Constructor
* @param array $availablequestions array of available question ids.
* @param int $choose how many questions to load.
*/
public function __construct($availablequestions, $choose) {
$this->availablequestions = $availablequestions;
$this->choose = $choose;
}
/**
* Choose and load the desired number of questions.
* @return array of short answer questions.
*/
public function load_questions() {
if ($this->choose > count($this->availablequestions)) {
throw new coding_exception('notenoughtshortanswerquestions');
}
$questionids = draw_rand_array($this->availablequestions, $this->choose);
$questions = array();
foreach ($questionids as $questionid) {
$questions[] = question_bank::load_question($questionid);
}
return $questions;
}
}

View File

@ -17,8 +17,7 @@
/**
* Question type class for the randomsamatch question type.
*
* @package qtype
* @subpackage randomsamatch
* @package qtype_randomsamatch
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
@ -26,6 +25,8 @@
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/question/type/questionbase.php');
require_once($CFG->dirroot . '/question/type/numerical/question.php');
/**
* The randomsamatch question type class.
@ -37,34 +38,30 @@ defined('MOODLE_INTERNAL') || die();
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_randomsamatch extends question_type {
/**
* Cache of available shortanswer question ids from a particular category.
* @var array two-dimensional array. The first key is a category id, the
* second key is wether subcategories should be included.
*/
private $availablesaquestionsbycategory = array();
const MAX_SUBQUESTIONS = 10;
public function requires_qtypes() {
return array('shortanswer', 'match');
}
public function is_usable_by_random() {
return false;
}
public function get_question_options($question) {
global $DB;
$question->options = $DB->get_record('question_randomsamatch',
array('question' => $question->id), '*', MUST_EXIST);
parent::get_question_options($question);
$question->options = $DB->get_record('qtype_randomsamatch_options',
array('questionid' => $question->id));
// This could be included as a flag in the database. It's already
// supported by the code.
// Recurse subcategories: 0 = no recursion, 1 = recursion .
$question->options->subcats = 1;
return true;
}
public function save_question_options($question) {
global $DB;
$options = new stdClass();
$options->question = $question->id;
$options->choose = $question->choose;
if (2 > $question->choose) {
$result = new stdClass();
@ -72,174 +69,93 @@ class qtype_randomsamatch extends question_type {
return $result;
}
if ($existing = $DB->get_record('question_randomsamatch',
array('question' => $options->question))) {
$options->id = $existing->id;
$DB->update_record('question_randomsamatch', $options);
} else {
$DB->insert_record('question_randomsamatch', $options);
$context = $question->context;
// Save the question options.
$options = $DB->get_record('qtype_randomsamatch_options', array('questionid' => $question->id));
if (!$options) {
$options = new stdClass();
$options->questionid = $question->id;
$options->correctfeedback = '';
$options->partiallycorrectfeedback = '';
$options->incorrectfeedback = '';
$options->id = $DB->insert_record('qtype_randomsamatch_options', $options);
}
$options->choose = $question->choose;
$options->subcats = $question->subcats;
$options = $this->save_combined_feedback_helper($options, $question, $context, true);
$DB->update_record('qtype_randomsamatch_options', $options);
$this->save_hints($question, true);
return true;
}
protected function make_hint($hint) {
return question_hint_with_parts::load_from_record($hint);
}
public function delete_question($questionid, $contextid) {
global $DB;
$DB->delete_records('question_randomsamatch', array('question' => $questionid));
$DB->delete_records('qtype_randomsamatch_options', array('questionid' => $questionid));
parent::delete_question($questionid, $contextid);
}
public function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
// Choose a random shortanswer question from the category:
// We need to make sure that no question is used more than once in the
// quiz. Therfore the following need to be excluded:
// 1. All questions that are explicitly assigned to the quiz
// 2. All random questions
// 3. All questions that are already chosen by an other random question.
global $QTYPES, $OUTPUT, $USER;
if (!isset($cmoptions->questionsinuse)) {
$cmoptions->questionsinuse = $cmoptions->questions;
}
public function move_files($questionid, $oldcontextid, $newcontextid) {
parent::move_files($questionid, $oldcontextid, $newcontextid);
if ($question->options->subcats) {
// Recurse into subcategories.
$categorylist = question_categorylist($question->category);
} else {
$categorylist = array($question->category);
}
$saquestions = $this->get_sa_candidates($categorylist, $cmoptions->questionsinuse);
$count = count($saquestions);
$wanted = $question->options->choose;
if ($count < $wanted) {
$question->questiontext = "Insufficient selection options are
available for this question, therefore it is not available in this
quiz. Please inform your teacher.";
// Treat this as a description from this point on.
$question->qtype = 'description';
return true;
}
$saquestions =
draw_rand_array($saquestions, $question->options->choose); // From bug 1889.
foreach ($saquestions as $key => $wrappedquestion) {
if (!$QTYPES[$wrappedquestion->qtype]
->get_question_options($wrappedquestion)) {
return false;
}
// Now we overwrite the $question->options->answers field to only
// *one* (the first) correct answer. This loop can be deleted to
// take all answers into account (i.e. put them all into the
// drop-down menu.
$foundcorrect = false;
foreach ($wrappedquestion->options->answers as $answer) {
if ($foundcorrect || $answer->fraction != 1.0) {
unset($wrappedquestion->options->answers[$answer->id]);
} else if (!$foundcorrect) {
$foundcorrect = true;
}
}
if (!$QTYPES[$wrappedquestion->qtype]
->create_session_and_responses($wrappedquestion, $state, $cmoptions,
$attempt)) {
return false;
}
$wrappedquestion->name_prefix = $question->name_prefix;
$wrappedquestion->maxgrade = $question->maxgrade;
$cmoptions->questionsinuse .= ",$wrappedquestion->id";
$state->options->subquestions[$key] = clone($wrappedquestion);
}
// Shuffle the answers (Do this always because this is a random question type).
$subquestionids = array_values(array_map(create_function('$val',
'return $val->id;'), $state->options->subquestions));
$subquestionids = swapshuffle($subquestionids);
// Create empty responses.
foreach ($subquestionids as $val) {
$state->responses[$val] = '';
}
return true;
$this->move_files_in_combined_feedback($questionid, $oldcontextid, $newcontextid);
$this->move_files_in_hints($questionid, $oldcontextid, $newcontextid);
}
function restore_session_and_responses(&$question, &$state) {
global $DB;
global $QTYPES, $OUTPUT;
static $wrappedquestions = array();
if (empty($state->responses[''])) {
$question->questiontext = "Insufficient selection options are
available for this question, therefore it is not available in this
quiz. Please inform your teacher.";
// Treat this as a description from this point on.
$question->qtype = 'description';
} else {
$responses = explode(',', $state->responses['']);
$responses = array_map(create_function('$val',
'return explode("-", $val);'), $responses);
protected function delete_files($questionid, $contextid) {
parent::delete_files($questionid, $contextid);
// Restore the previous responses.
$state->responses = array();
foreach ($responses as $response) {
$wqid = $response[0];
$state->responses[$wqid] = $response[1];
if (!isset($wrappedquestions[$wqid])) {
if (!$wrappedquestions[$wqid] = $DB->get_record('question', array('id' => $wqid))) {
echo $OUTPUT->notification("Couldn't get question (id=$wqid)!");
return false;
}
if (!$QTYPES[$wrappedquestions[$wqid]->qtype]
->get_question_options($wrappedquestions[$wqid])) {
echo $OUTPUT->notification("Couldn't get question options (id=$response[0])!");
return false;
}
// Now we overwrite the $question->options->answers field to only
// *one* (the first) correct answer. This loop can be deleted to
// take all answers into account (i.e. put them all into the
// drop-down menu.
$foundcorrect = false;
foreach ($wrappedquestions[$wqid]->options->answers as $answer) {
if ($foundcorrect || $answer->fraction != 1.0) {
unset($wrappedquestions[$wqid]->options->answers[$answer->id]);
} else if (!$foundcorrect) {
$foundcorrect = true;
}
}
}
$wrappedquestion = clone($wrappedquestions[$wqid]);
if (!$QTYPES[$wrappedquestion->qtype]
->restore_session_and_responses($wrappedquestion, $state)) {
echo $OUTPUT->notification("Couldn't restore session of question (id=$response[0])!");
return false;
}
$wrappedquestion->name_prefix = $question->name_prefix;
$wrappedquestion->maxgrade = $question->maxgrade;
$state->options->subquestions[$wrappedquestion->id] =
clone($wrappedquestion);
}
}
return true;
$this->delete_files_in_combined_feedback($questionid, $contextid);
$this->delete_files_in_hints($questionid, $contextid);
}
public function get_sa_candidates($categorylist, $questionsinuse = 0) {
global $DB;
list ($usql, $params) = $DB->get_in_or_equal($categorylist);
list ($ques_usql, $ques_params) = $DB->get_in_or_equal(explode(',', $questionsinuse),
SQL_PARAMS_QM, null, false);
$params = array_merge($params, $ques_params);
return $DB->get_records_select('question',
"qtype = 'shortanswer' " .
"AND category $usql " .
"AND parent = '0' " .
"AND hidden = '0'" .
"AND id $ques_usql", $params);
protected function initialise_question_instance(question_definition $question, $questiondata) {
parent::initialise_question_instance($question, $questiondata);
$availablesaquestions = $this->get_available_saquestions_from_category(
$question->category, $questiondata->options->subcats);
$question->shufflestems = false;
$question->stems = array();
$question->choices = array();
$question->right = array();
$this->initialise_combined_feedback($question, $questiondata);
$question->questionsloader = new qtype_randomsamatch_question_loader(
$availablesaquestions, $questiondata->options->choose);
}
public function can_analyse_responses() {
return false;
}
/**
* Get all the usable shortanswer questions from a particular question category.
*
* @param integer $categoryid the id of a question category.
* @param bool $subcategories whether to include questions from subcategories.
* @return array of question records.
*/
public function get_available_saquestions_from_category($categoryid, $subcategories) {
if (isset($this->availablesaquestionsbycategory[$categoryid][$subcategories])) {
return $this->availablesaquestionsbycategory[$categoryid][$subcategories];
}
if ($subcategories) {
$categoryids = question_categorylist($categoryid);
} else {
$categoryids = array($categoryid);
}
$questionids = question_bank::get_finder()->get_questions_from_categories(
$categoryids, "qtype = 'shortanswer'");
$this->availablesaquestionsbycategory[$categoryid][$subcategories] = $questionids;
return $questionids;
}
/**
@ -251,4 +167,68 @@ class qtype_randomsamatch extends question_type {
public function get_random_guess_score($question) {
return 1/$question->options->choose;
}
/**
* Defines the table which extends the question table. This allows the base questiontype
* to automatically save, backup and restore the extra fields.
*
* @return an array with the table name (first) and then the column names (apart from id and questionid)
*/
public function extra_question_fields() {
return array('qtype_randomsamatch_options',
'choose', // Number of shortanswer questions to choose.
'subcats', // Questions can be choosen from subcategories.
);
}
/**
* Imports the question from Moodle XML format.
*
* @param array $xml structure containing the XML data
* @param object $fromform question object to fill: ignored by this function (assumed to be null)
* @param qformat_xml $format format class exporting the question
* @param object $extra extra information (not required for importing this question in this format)
* @return object question object
*/
public function import_from_xml($xml, $fromform, qformat_xml $format, $extra=null) {
// Return if data type is not our own one.
if (!isset($xml['@']['type']) || $xml['@']['type'] != $this->name()) {
return false;
}
// Import the common question headers and set the corresponding field.
$fromform = $format->import_headers($xml);
$fromform->qtype = $this->name();
$format->import_combined_feedback($fromform, $xml, true);
$format->import_hints($fromform, $xml, true);
$extras = $this->extra_question_fields();
array_shift($extras);
foreach ($extras as $extra) {
$fromform->$extra = $format->getpath($xml, array('#', $extra, 0, '#'), '', true);
}
return $fromform;
}
/**
* Exports the question to Moodle XML format.
*
* @param object $question question to be exported into XML format
* @param qformat_xml $format format class exporting the question
* @param object $extra extra information (not required for exporting this question in this format)
* @return string containing the question data in XML format
*/
public function export_to_xml($question, qformat_xml $format, $extra=null) {
$expout = '';
$expout .= $format->write_combined_feedback($question->options,
$question->id,
$question->contextid);
$extraquestionfields = $this->extra_question_fields();
array_shift($extraquestionfields);
foreach ($extraquestionfields as $extra) {
$expout .= " <$extra>" . $question->options->$extra . "</$extra>\n";
}
return $expout;
}
}

View File

@ -0,0 +1,43 @@
<?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/>.
/**
* Matching question renderer class.
*
* @package qtype_randomsamatch
* @copyright 2013 Jean-Michel Vedrine
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/question/type/match/renderer.php');
/**
* Generates the output for randomsamatch questions.
*
* @copyright 2013 Jean-Michel Vedrine
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_randomsamatch_renderer extends qtype_match_renderer {
public function format_stem_text($qa, $stemid) {
$question = $qa->get_question();
return $question->format_text(
$question->stems[$stemid], $question->stemformat[$stemid],
$qa, 'question', 'questiontext', $stemid);
}
}

View File

@ -0,0 +1,139 @@
<?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/>.
/**
* Test helpers for the randomsamatch question type.
*
* @package qtype_randomsamatch
* @copyright 2013 Jean-Michel Vedrine
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/question/type/randomsamatch/question.php');
require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
/**
* Test helper class for the randomsamatch question type.
*
* @copyright 2013 Jean-Michel Vedrine
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_randomsamatch_test_helper extends question_test_helper {
public function get_test_questions() {
return array('animals');
}
/**
* Makes a randomsamatch question similar to the match question returned
* by {@link make_a_matching_question}, but with no 'insect' distractor.
* @return qtype_randomsamatch_question
*/
public function make_randomsamatch_question_animals() {
question_bank::load_question_definition_classes('randomsamatch');
$q = new qtype_randomsamatch_question();
test_question_maker::initialise_a_question($q);
$q->name = 'Random shortanswer matching question';
$q->questiontext = 'Classify the animals.';
$q->generalfeedback = 'Frogs and toads are amphibians, the others are mammals.';
$q->qtype = question_bank::get_qtype('randomsamatch');
test_question_maker::set_standard_combined_feedback_fields($q);
$q->shufflestems = false;
$q->stems = array();
$q->choices = array();
$q->right = array();
// Now we create 4 shortanswers question,
// but we just fill the needed fields.
question_bank::load_question_definition_classes('shortanswer');
$sa1 = new qtype_shortanswer_question();
test_question_maker::initialise_a_question($sa1);
$sa1->id = 25;
$sa1->questiontext = 'Dog';
$sa1->answers = array(
13 => new question_answer(13, 'Mammal', 1.0, 'Correct.', FORMAT_HTML),
14 => new question_answer(14, 'Animal', 0.5, 'There is a betterresponse.', FORMAT_HTML),
15 => new question_answer(15, '*', 0.0, 'That is a bad answer.', FORMAT_HTML),
);
$sa1->qtype = question_bank::get_qtype('shortanswer');
$sa2 = new qtype_shortanswer_question();
test_question_maker::initialise_a_question($sa2);
$sa2->id = 26;
$sa2->questiontext = 'Frog';
$sa2->answers = array(
16 => new question_answer(16, 'Amphibian', 1.0, 'Correct.', FORMAT_HTML),
17 => new question_answer(17, 'A Prince', 1.0, 'Maybe.', FORMAT_HTML),
18 => new question_answer(18, '*', 0.0, 'That is a bad answer.', FORMAT_HTML),
);
$sa2->qtype = question_bank::get_qtype('shortanswer');
$sa3 = new qtype_shortanswer_question();
test_question_maker::initialise_a_question($sa3);
$sa3->id = 27;
$sa3->questiontext = 'Toad';
$sa3->answers = array(
19 => new question_answer(19, 'Amphibian', 1.0, 'Correct.', FORMAT_HTML),
20 => new question_answer(20, '*', 0.0, 'That is a bad answer.', FORMAT_HTML),
);
$sa3->qtype = question_bank::get_qtype('shortanswer');
$sa4 = new qtype_shortanswer_question();
test_question_maker::initialise_a_question($sa4);
$sa4->id = 28;
$sa4->questiontext = 'Cat';
$sa4->answers = array(
21 => new question_answer(21, 'Mammal', 1.0, 'Correct.', FORMAT_HTML),
);
$sa4->qtype = question_bank::get_qtype('shortanswer');
$q->questionsloader = new qtype_randomsamatch_test_question_loader(array(), 4, array($sa1, $sa2, $sa3, $sa4));
return $q;
}
}
/**
* Test implementation of {@link qtype_randomsamatch_question_loader}. Gets the questions
* from an array passed to the constructor, rather than querying the database.
*
* @copyright 2013 Jean-Michel Vedrine
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_randomsamatch_test_question_loader extends qtype_randomsamatch_question_loader {
/** @var array hold available shortanswers questions to choose from. */
protected $questions;
/**
* Constructor
* @param array $availablequestions not used for tests.
* @param int $choose how many questions to load (not used here).
* @param array $questions array of questions to use.
*/
public function __construct($availablequestions, $choose, $questions) {
parent::__construct($availablequestions, $choose);
$this->questions = $questions;
}
/**
* Just return the shortanswers questions passed to the constructor.
* @return array of short answer questions.
*/
public function load_questions() {
return $this->questions;
}
}

View File

@ -0,0 +1,153 @@
<?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 radom shortanswer matching question definition classes.
*
* @package qtype_randomsamatch
* @copyright 2013 Jean-Michel Vedrine
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
/**
* Unit tests for the random shortanswer matching question definition class.
*
* @copyright 2013 Jean-Michel Vedrine
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_randomsamatch_question_test extends advanced_testcase {
public function test_get_expected_data() {
$question = test_question_maker::make_question('randomsamatch');
$question->start_attempt(new question_attempt_step(), 1);
$this->assertEquals(array('sub0' => PARAM_INT, 'sub1' => PARAM_INT,
'sub2' => PARAM_INT, 'sub3' => PARAM_INT), $question->get_expected_data());
}
public function test_is_complete_response() {
$question = test_question_maker::make_question('randomsamatch');
$question->start_attempt(new question_attempt_step(), 1);
$this->assertFalse($question->is_complete_response(array()));
$this->assertFalse($question->is_complete_response(
array('sub0' => '1', 'sub1' => '1', 'sub2' => '1', 'sub3' => '0')));
$this->assertFalse($question->is_complete_response(array('sub1' => '1')));
$this->assertTrue($question->is_complete_response(
array('sub0' => '1', 'sub1' => '1', 'sub2' => '1', 'sub3' => '1')));
}
public function test_is_gradable_response() {
$question = test_question_maker::make_question('randomsamatch');
$question->start_attempt(new question_attempt_step(), 1);
$this->assertFalse($question->is_gradable_response(array()));
$this->assertFalse($question->is_gradable_response(
array('sub0' => '0', 'sub1' => '0', 'sub2' => '0', 'sub3' => '0')));
$this->assertTrue($question->is_gradable_response(
array('sub0' => '1', 'sub1' => '0', 'sub2' => '0', 'sub3' => '0')));
$this->assertTrue($question->is_gradable_response(array('sub1' => '1')));
$this->assertTrue($question->is_gradable_response(
array('sub0' => '1', 'sub1' => '1', 'sub2' => '3', 'sub3' => '1')));
}
public function test_is_same_response() {
$question = test_question_maker::make_question('randomsamatch');
$question->start_attempt(new question_attempt_step(), 1);
$this->assertTrue($question->is_same_response(
array(),
array('sub0' => '0', 'sub1' => '0', 'sub2' => '0', 'sub3' => '0')));
$this->assertTrue($question->is_same_response(
array('sub0' => '0', 'sub1' => '0', 'sub2' => '0', 'sub3' => '0'),
array('sub0' => '0', 'sub1' => '0', 'sub2' => '0', 'sub3' => '0')));
$this->assertFalse($question->is_same_response(
array('sub0' => '0', 'sub1' => '0', 'sub2' => '0', 'sub3' => '0'),
array('sub0' => '1', 'sub1' => '2', 'sub2' => '3', 'sub3' => '1')));
$this->assertTrue($question->is_same_response(
array('sub0' => '1', 'sub1' => '2', 'sub2' => '3', 'sub3' => '1'),
array('sub0' => '1', 'sub1' => '2', 'sub2' => '3', 'sub3' => '1')));
$this->assertFalse($question->is_same_response(
array('sub0' => '2', 'sub1' => '2', 'sub2' => '3', 'sub3' => '1'),
array('sub0' => '1', 'sub1' => '2', 'sub2' => '3', 'sub3' => '1')));
}
public function test_grading() {
$question = test_question_maker::make_question('randomsamatch');
$question->start_attempt(new question_attempt_step(), 1);
$choiceorder = $question->get_choice_order();
$orderforchoice = array_combine(array_values($choiceorder), array_keys($choiceorder));
$this->assertEquals(array(1, question_state::$gradedright),
$question->grade_response(array('sub0' => $orderforchoice[13],
'sub1' => $orderforchoice[16], 'sub2' => $orderforchoice[16],
'sub3' => $orderforchoice[13])));
$this->assertEquals(array(0.25, question_state::$gradedpartial),
$question->grade_response(array('sub0' => $orderforchoice[13])));
$this->assertEquals(array(0, question_state::$gradedwrong),
$question->grade_response(array('sub0' => $orderforchoice[16],
'sub1' => $orderforchoice[13], 'sub2' => $orderforchoice[13],
'sub3' => $orderforchoice[16])));
}
public function test_get_correct_response() {
$question = test_question_maker::make_question('randomsamatch');
$question->start_attempt(new question_attempt_step(), 1);
$choiceorder = $question->get_choice_order();
$orderforchoice = array_combine(array_values($choiceorder), array_keys($choiceorder));
$this->assertEquals(array('sub0' => $orderforchoice[13], 'sub1' => $orderforchoice[16],
'sub2' => $orderforchoice[16], 'sub3' => $orderforchoice[13]),
$question->get_correct_response());
}
public function test_get_question_summary() {
$question = test_question_maker::make_question('randomsamatch');
$question->start_attempt(new question_attempt_step(), 1);
$qsummary = $question->get_question_summary();
$this->assertRegExp('/' . preg_quote($question->questiontext, '/') . '/', $qsummary);
foreach ($question->stems as $stem) {
$this->assertRegExp('/' . preg_quote($stem, '/') . '/', $qsummary);
}
foreach ($question->choices as $choice) {
$this->assertRegExp('/' . preg_quote($choice, '/') . '/', $qsummary);
}
}
public function test_summarise_response() {
$question = test_question_maker::make_question('randomsamatch');
$question->shufflestems = false;
$question->start_attempt(new question_attempt_step(), 1);
$summary = $question->summarise_response(array('sub0' => 2, 'sub1' => 1));
$this->assertRegExp('/Dog -> \w+; Frog -> \w+/', $summary);
}
}

View File

@ -0,0 +1,406 @@
<?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/>.
/**
* Tests of the upgrade to the new Moodle question engine for attempts at
* randomsamatch questions.
*
* @package qtype_randomsamatch
* @copyright 2013 Jean-Michel Vedrine
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/question/engine/upgrade/tests/helper.php');
/**
* Testing the upgrade of randomsamatch question attempts.
*
* @copyright 2013 Jean-Michel Vedrine
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_randomsamatch_attempt_upgrader_test extends question_attempt_upgrader_test_base {
public function test_randomsamatch_deferredfeedback_qsession1() {
$quiz = (object) array(
'id' => '1',
'course' => '2',
'name' => 'random short answer matching deferred quiz',
'intro' => '<p>To test random shortanswer matching questions.</p>',
'introformat' => '1',
'timeopen' => '0',
'timeclose' => '0',
'attempts' => '0',
'attemptonlast' => '0',
'grademethod' => '1',
'decimalpoints' => '2',
'questiondecimalpoints' => '-1',
'review' => '4459503',
'questionsperpage' => '1',
'shufflequestions' => '0',
'shuffleanswers' => '1',
'questions' => '5,0',
'sumgrades' => '1.00000',
'grade' => '100.00000',
'timecreated' => '0',
'timemodified' => '1368446711',
'timelimit' => '0',
'password' => '',
'subnet' => '',
'popup' => '0',
'delay1' => '0',
'delay2' => '0',
'showuserpicture' => '0',
'showblocks' => '0',
'preferredbehaviour' => 'deferredfeedback',
);
$attempt = (object) array(
'id' => '1',
'uniqueid' => '1',
'quiz' => '1',
'userid' => '3',
'attempt' => '1',
'sumgrades' => '0.66667',
'timestart' => '1368446755',
'timefinish' => '1368446789',
'timemodified' => '1368446789',
'layout' => '5,0',
'preview' => '0',
'needsupgradetonewqe' => 1,
);
$question = (object) array(
'id' => '5',
'category' => '1',
'parent' => '0',
'name' => 'Random shortanswer matching question animals',
'questiontext' => 'For each of the following questions, select the matching answer from the menu.',
'questiontextformat' => '1',
'generalfeedback' => '',
'generalfeedbackformat' => '1',
'penalty' => '0.1000000',
'qtype' => 'randomsamatch',
'length' => '1',
'stamp' => 'localhost+130513115611+72Efbk',
'version' => 'localhost+130513115611+0REXHW',
'hidden' => '0',
'timecreated' => '1368446171',
'timemodified' => '1368446171',
'createdby' => '2',
'modifiedby' => '2',
'maxmark' => '1.0000000',
'options' => (object) array(
'id' => '1',
'question' => '5',
'choose' => '3',
'subcats' => 1,
),
'defaultmark' => '1.0000000',
);
$qsession = (object) array(
'id' => '1',
'attemptid' => '1',
'questionid' => '5',
'newest' => '3',
'newgraded' => '3',
'sumpenalty' => '0.1000000',
'manualcomment' => '',
'manualcommentformat' => '1',
'flagged' => '0',
);
$qstates = array(
1 => (object) array(
'id' => '1',
'attempt' => '1',
'question' => '5',
'seq_number' => '0',
'answer' => '2-0,3-0,6-0',
'timestamp' => '1368446755',
'event' => '0',
'grade' => '0.0000000',
'raw_grade' => '0.0000000',
'penalty' => '0.0000000',
),
2 => (object) array(
'id' => '2',
'attempt' => '1',
'question' => '5',
'seq_number' => '1',
'answer' => '2-3,3-5,6-3',
'timestamp' => '1368446783',
'event' => '2',
'grade' => '0.0000000',
'raw_grade' => '0.6666667',
'penalty' => '0.1000000',
),
3 => (object) array(
'id' => '3',
'attempt' => '1',
'question' => '5',
'seq_number' => '2',
'answer' => '2-3,3-5,6-3',
'timestamp' => '1368446783',
'event' => '6',
'grade' => '0.6666667',
'raw_grade' => '0.6666667',
'penalty' => '0.1000000',
),
);
$sa1 = (object) array(
'id' => '2',
'category' => '1',
'parent' => '0',
'name' => 'animal 1',
'questiontext' => 'Dog',
'questiontextformat' => '1',
'defaultmark' => '1',
'penalty' => '0.1',
'qtype' => 'shortanswer',
'length' => '1',
'stamp' => 'localhost+090227173002+mbdE0X',
'version' => 'localhost+090304190917+xAB5Nf',
'hidden' => '0',
'generalfeedback' => '',
'generalfeedbackformat' => '1',
'timecreated' => '1235755802',
'timemodified' => '1236193757',
'createdby' => '25299',
'modifiedby' => '25299',
'unlimited' => '0',
'options' => (object) array(
'id' => '15211',
'question' => '2',
'layout' => '0',
'answers' => array(
7 => (object) array(
'question' => '2',
'answer' => 'Amphibian',
'fraction' => '0',
'feedback' => '',
'id' => 7,
),
3 => (object) array(
'question' => '2',
'answer' => 'Mammal',
'fraction' => '1',
'feedback' => '',
'id' => 3,
),
22 => (object) array(
'question' => '2',
'answer' => '*',
'fraction' => '0',
'feedback' => '',
'id' => 22,
),
),
'single' => '1',
'shuffleanswers' => '1',
'correctfeedback' => 'Your answer is correct. Well done.',
'partiallycorrectfeedback' => '',
'incorrectfeedback' => 'Your answer is incorrect. The correct answer is: Mammal.',
'answernumbering' => 'abc',
),
);
$sa2 = (object) array(
'id' => '3',
'category' => '1',
'parent' => '0',
'name' => 'animal 2',
'questiontext' => 'Frog',
'questiontextformat' => '1',
'defaultmark' => '1',
'penalty' => '0.1',
'qtype' => 'shortanswer',
'length' => '1',
'stamp' => 'localhost+090227173002+mbdE0X',
'version' => 'localhost+090304190917+xAB5Nf',
'hidden' => '0',
'generalfeedback' => '',
'generalfeedbackformat' => '1',
'timecreated' => '1235755802',
'timemodified' => '1236193757',
'createdby' => '25299',
'modifiedby' => '25299',
'unlimited' => '0',
'options' => (object) array(
'id' => '15214',
'question' => '3',
'layout' => '0',
'answers' => array(
5 => (object) array(
'question' => '3',
'answer' => 'Amphibian',
'fraction' => '1',
'feedback' => '',
'id' => 5,
),
11 => (object) array(
'question' => '3',
'answer' => 'Mammal',
'fraction' => '0',
'feedback' => '',
'id' => 11,
),
27 => (object) array(
'question' => '3',
'answer' => '*',
'fraction' => '0',
'feedback' => '',
'id' => 27,
),
),
'single' => '1',
'shuffleanswers' => '1',
'correctfeedback' => 'Your answer is correct. Well done.',
'partiallycorrectfeedback' => '',
'incorrectfeedback' => 'Your answer is incorrect. The correct answer is: Mammal.',
'answernumbering' => 'abc',
),
);
$sa3 = (object) array(
'id' => '6',
'category' => '1',
'parent' => '0',
'name' => 'animal 3',
'questiontext' => 'Toad',
'questiontextformat' => '1',
'defaultmark' => '1',
'penalty' => '0.1',
'qtype' => 'shortanswer',
'length' => '1',
'stamp' => 'localhost+090227173002+mbdE0X',
'version' => 'localhost+090304190917+xAB5Nf',
'hidden' => '0',
'generalfeedback' => '',
'generalfeedbackformat' => '1',
'timecreated' => '1235755802',
'timemodified' => '1236193757',
'createdby' => '25299',
'modifiedby' => '25299',
'unlimited' => '0',
'options' => (object) array(
'id' => '4578',
'question' => '6',
'layout' => '0',
'answers' => array(
9 => (object) array(
'question' => '6',
'answer' => 'Amphibian',
'fraction' => '1',
'feedback' => '',
'id' => 9,
),
18 => (object) array(
'question' => '6',
'answer' => 'Mammal',
'fraction' => '0',
'feedback' => '',
'id' => 18,
),
32 => (object) array(
'question' => '6',
'answer' => '*',
'fraction' => '0',
'feedback' => '',
'id' => 32,
),
),
'single' => '1',
'shuffleanswers' => '1',
'correctfeedback' => 'Your answer is correct. Well done.',
'partiallycorrectfeedback' => '',
'incorrectfeedback' => 'Your answer is incorrect. The correct answer is: Mammal.',
'answernumbering' => 'abc',
),
);
$this->loader->put_question_in_cache($sa2);
$this->loader->put_question_in_cache($sa1);
$this->loader->put_question_in_cache($sa3);
$qa = $this->updater->convert_question_attempt($quiz, $attempt, $question, $qsession, $qstates);
$expectedqa = (object) array(
'behaviour' => 'deferredfeedback',
'questionid' => 5,
'variant' => 1,
'maxmark' => 1.0000000,
'minfraction' => 0,
'maxfraction' => 1,
'flagged' => 0,
'questionsummary' => 'For each of the following questions, select the matching answer from the menu.{Dog;Frog;Toad}->{Mammal;Amphibian}',
'rightanswer' => 'Dog -> Mammal; Frog -> Amphibian; Toad -> Amphibian',
'responsesummary' => 'Dog->Mammal;Frog->Amphibian;Toad->Mammal',
'timemodified' => 1368446783,
'steps' => array(
0 => (object) array(
'sequencenumber' => 0,
'state' => 'todo',
'fraction' => null,
'timecreated' => 1368446755,
'userid' => 3,
'data' => array(
'_choice_3' => 'Mammal',
'_stem_2' => 'Dog',
'_stemformat_2' => '1',
'_right_2' => 3,
'_choice_5' => 'Amphibian',
'_stem_3' => 'Frog',
'_stemformat_3' => '1',
'_right_3' => 5,
'_stem_6' => 'Toad',
'_stemformat_6' => '1',
'_right_6' => 5,
'_stemorder' => '2,3,6',
'_choiceorder' => '3,5',
),
),
1 => (object) array(
'sequencenumber' => 1,
'state' => 'complete',
'fraction' => null,
'timecreated' => 1368446783,
'userid' => 3,
'data' => array(
'sub0' => 1,
'sub1' => 2,
'sub2' => 1,
),
),
2 => (object) array(
'sequencenumber' => 2,
'state' => 'gradedpartial',
'fraction' => 0.6666667,
'timecreated' => 1368446783,
'userid' => 3,
'data' => array(
'sub0' => 1,
'sub1' => 2,
'sub2' => 1,
'-finish' => 1,
),
),
),
);
$this->compare_qas($expectedqa, $qa);
}
}

View File

@ -0,0 +1,435 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This file contains tests that walks a question through the interactive
* behaviour.
*
* @package qtype_randomsamatch
* @copyright 2013 Jean-Michel Vedrine
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
/**
* Unit tests for the randomsamatch question type.
*
* @copyright 2013 Jean-Michel Vedrine
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_randomsamatch_walkthrough_test extends qbehaviour_walkthrough_test_base {
public function test_deferred_feedback_unanswered() {
// Create a randomsamatch question.
$m = test_question_maker::make_question('randomsamatch');
$this->start_attempt_at_question($m, 'deferredfeedback', 4);
$choiceorder = $m->get_choice_order();
$orderforchoice = array_combine(array_values($choiceorder), array_keys($choiceorder));
$choices = array(0 => get_string('choose') . '...');
foreach ($choiceorder as $key => $choice) {
$choices[$key] = $m->choices[$choice];
}
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_select_expectation('sub0', $choices, null, true),
$this->get_contains_select_expectation('sub1', $choices, null, true),
$this->get_contains_select_expectation('sub2', $choices, null, true),
$this->get_contains_select_expectation('sub3', $choices, null, true),
$this->get_contains_question_text_expectation($m),
$this->get_does_not_contain_feedback_expectation());
$this->check_step_count(1);
// Save a blank response.
$this->process_submission(array('sub0' => '0', 'sub1' => '0',
'sub2' => '0', 'sub3' => '0'));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_select_expectation('sub0', $choices, null, true),
$this->get_contains_select_expectation('sub1', $choices, null, true),
$this->get_contains_select_expectation('sub2', $choices, null, true),
$this->get_contains_select_expectation('sub3', $choices, null, true),
$this->get_contains_question_text_expectation($m),
$this->get_does_not_contain_feedback_expectation());
$this->check_step_count(1);
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gaveup);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_select_expectation('sub0', $choices, null, false),
$this->get_contains_select_expectation('sub1', $choices, null, false),
$this->get_contains_select_expectation('sub2', $choices, null, false),
$this->get_contains_select_expectation('sub3', $choices, null, false));
}
public function test_deferred_feedback_partial_answer() {
// Create a randomsamatching question.
$m = test_question_maker::make_question('randomsamatch');
$m->shufflestems = false;
$this->start_attempt_at_question($m, 'deferredfeedback', 4);
$choiceorder = $m->get_choice_order();
$orderforchoice = array_combine(array_values($choiceorder), array_keys($choiceorder));
$choices = array(0 => get_string('choose') . '...');
foreach ($choiceorder as $key => $choice) {
$choices[$key] = $m->choices[$choice];
}
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_select_expectation('sub0', $choices, null, true),
$this->get_contains_select_expectation('sub1', $choices, null, true),
$this->get_contains_select_expectation('sub2', $choices, null, true),
$this->get_contains_select_expectation('sub3', $choices, null, true),
$this->get_contains_question_text_expectation($m),
$this->get_does_not_contain_feedback_expectation());
// Save a partial response.
$this->process_submission(array('sub0' => $orderforchoice[13],
'sub1' => $orderforchoice[16], 'sub2' => '0', 'sub3' => '0'));
// Verify.
$this->check_current_state(question_state::$invalid);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_select_expectation('sub0', $choices, $orderforchoice[13], true),
$this->get_contains_select_expectation('sub1', $choices, $orderforchoice[16], true),
$this->get_contains_select_expectation('sub2', $choices, null, true),
$this->get_contains_select_expectation('sub3', $choices, null, true),
$this->get_contains_question_text_expectation($m),
$this->get_does_not_contain_feedback_expectation());
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedpartial);
$this->check_current_mark(2);
$this->check_current_output(
$this->get_contains_select_expectation('sub0', $choices, $orderforchoice[13], false),
$this->get_contains_select_expectation('sub1', $choices, $orderforchoice[16], false),
$this->get_contains_select_expectation('sub2', $choices, null, false),
$this->get_contains_select_expectation('sub3', $choices, null, false),
$this->get_contains_partcorrect_expectation());
}
public function test_interactive_correct_no_submit() {
// Create a randomsamatching question.
$m = test_question_maker::make_question('randomsamatch');
$m->hints = array(
new question_hint_with_parts(11, 'This is the first hint.', FORMAT_HTML, false, false),
new question_hint_with_parts(12, 'This is the second hint.', FORMAT_HTML, true, true),
);
$m->shufflestems = false;
$this->start_attempt_at_question($m, 'interactive', 4);
$choiceorder = $m->get_choice_order();
$orderforchoice = array_combine(array_values($choiceorder), array_keys($choiceorder));
$choices = array(0 => get_string('choose') . '...');
foreach ($choiceorder as $key => $choice) {
$choices[$key] = $m->choices[$choice];
}
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_select_expectation('sub0', $choices, null, true),
$this->get_contains_select_expectation('sub1', $choices, null, true),
$this->get_contains_select_expectation('sub2', $choices, null, true),
$this->get_contains_select_expectation('sub3', $choices, null, true),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_feedback_expectation(),
$this->get_tries_remaining_expectation(3),
$this->get_no_hint_visible_expectation());
// Save the right answer.
$this->process_submission(array('sub0' => $orderforchoice[13],
'sub1' => $orderforchoice[16], 'sub2' => $orderforchoice[16],
'sub3' => $orderforchoice[13]));
// Finish the attempt without clicking check.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(4);
$this->check_current_output(
$this->get_contains_select_expectation('sub0', $choices, $orderforchoice[13], false),
$this->get_contains_select_expectation('sub1', $choices, $orderforchoice[16], false),
$this->get_contains_select_expectation('sub2', $choices, $orderforchoice[16], false),
$this->get_contains_select_expectation('sub3', $choices, $orderforchoice[13], false),
$this->get_contains_submit_button_expectation(false),
$this->get_contains_correct_expectation(),
$this->get_no_hint_visible_expectation());
}
public function test_interactive_partial_no_submit() {
// Create a randomsamatching question.
$m = test_question_maker::make_question('randomsamatch');
$m->hints = array(
new question_hint_with_parts(11, 'This is the first hint.', FORMAT_HTML, false, false),
new question_hint_with_parts(12, 'This is the second hint.', FORMAT_HTML, true, true),
);
$m->shufflestems = false;
$this->start_attempt_at_question($m, 'interactive', 4);
$choiceorder = $m->get_choice_order();
$orderforchoice = array_combine(array_values($choiceorder), array_keys($choiceorder));
$choices = array(0 => get_string('choose') . '...');
foreach ($choiceorder as $key => $choice) {
$choices[$key] = $m->choices[$choice];
}
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_select_expectation('sub0', $choices, null, true),
$this->get_contains_select_expectation('sub1', $choices, null, true),
$this->get_contains_select_expectation('sub2', $choices, null, true),
$this->get_contains_select_expectation('sub3', $choices, null, true),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_feedback_expectation(),
$this->get_tries_remaining_expectation(3),
$this->get_no_hint_visible_expectation());
// Save the right answer.
$this->process_submission(array('sub0' => $orderforchoice[13],
'sub1' => $orderforchoice[16], 'sub2' => $orderforchoice[13],
'sub3' => '0'));
// Finish the attempt without clicking check.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedpartial);
$this->check_current_mark(2);
$this->check_current_output(
$this->get_contains_select_expectation('sub0', $choices, $orderforchoice[13], false),
$this->get_contains_select_expectation('sub1', $choices, $orderforchoice[16], false),
$this->get_contains_select_expectation('sub2', $choices, $orderforchoice[13], false),
$this->get_contains_select_expectation('sub3', $choices, null, false),
$this->get_contains_submit_button_expectation(false),
$this->get_contains_partcorrect_expectation(),
$this->get_no_hint_visible_expectation());
}
public function test_interactive_with_invalid() {
// Create a randomsamatching question.
$m = test_question_maker::make_question('randomsamatch');
$m->hints = array(
new question_hint_with_parts(11, 'This is the first hint.', FORMAT_HTML, false, false),
new question_hint_with_parts(12, 'This is the second hint.', FORMAT_HTML, true, true),
);
$m->shufflestems = false;
$this->start_attempt_at_question($m, 'interactive', 4);
$choiceorder = $m->get_choice_order();
$orderforchoice = array_combine(array_values($choiceorder), array_keys($choiceorder));
$choices = array(0 => get_string('choose') . '...');
foreach ($choiceorder as $key => $choice) {
$choices[$key] = $m->choices[$choice];
}
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_select_expectation('sub0', $choices, null, true),
$this->get_contains_select_expectation('sub1', $choices, null, true),
$this->get_contains_select_expectation('sub2', $choices, null, true),
$this->get_contains_select_expectation('sub3', $choices, null, true),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_feedback_expectation(),
$this->get_tries_remaining_expectation(3),
$this->get_no_hint_visible_expectation());
// Try to submit an invalid answer.
$this->process_submission(array('sub0' => '0',
'sub1' => '0', 'sub2' => '0',
'sub3' => '0', '-submit' => '1'));
// Verify.
$this->check_current_state(question_state::$invalid);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_select_expectation('sub0', $choices, null, true),
$this->get_contains_select_expectation('sub1', $choices, null, true),
$this->get_contains_select_expectation('sub2', $choices, null, true),
$this->get_contains_select_expectation('sub3', $choices, null, true),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_feedback_expectation(),
$this->get_invalid_answer_expectation(),
$this->get_no_hint_visible_expectation());
// Now submit the right answer.
$this->process_submission(array('sub0' => $orderforchoice[13],
'sub1' => $orderforchoice[16], 'sub2' => $orderforchoice[16],
'sub3' => $orderforchoice[13], '-submit' => '1'));
// Verify.
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(4);
$this->check_current_output(
$this->get_contains_select_expectation('sub0', $choices, $orderforchoice[13], false),
$this->get_contains_select_expectation('sub1', $choices, $orderforchoice[16], false),
$this->get_contains_select_expectation('sub2', $choices, $orderforchoice[16], false),
$this->get_contains_select_expectation('sub3', $choices, $orderforchoice[13], false),
$this->get_contains_submit_button_expectation(false),
$this->get_contains_correct_expectation(),
$this->get_no_hint_visible_expectation());
}
public function test_randomsamatch_clear_wrong() {
// Create a randomsamatching question.
$m = test_question_maker::make_question('randomsamatch');
$m->hints = array(
new question_hint_with_parts(11, 'This is the first hint.', FORMAT_HTML, false, true),
new question_hint_with_parts(12, 'This is the second hint.', FORMAT_HTML, true, true),
);
$m->shufflestems = false;
$this->start_attempt_at_question($m, 'interactive', 4);
$choiceorder = $m->get_choice_order();
$orderforchoice = array_combine(array_values($choiceorder), array_keys($choiceorder));
$choices = array(0 => get_string('choose') . '...');
foreach ($choiceorder as $key => $choice) {
$choices[$key] = $m->choices[$choice];
}
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_select_expectation('sub0', $choices, null, true),
$this->get_contains_select_expectation('sub1', $choices, null, true),
$this->get_contains_select_expectation('sub2', $choices, null, true),
$this->get_contains_select_expectation('sub3', $choices, null, true),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_feedback_expectation(),
$this->get_tries_remaining_expectation(3),
$this->get_no_hint_visible_expectation());
// Submit a completely wrong response.
$this->process_submission(array('sub0' => $orderforchoice[16],
'sub1' => $orderforchoice[13], 'sub2' => $orderforchoice[13],
'sub3' => $orderforchoice[16], '-submit' => 1));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_select_expectation('sub0', $choices, $orderforchoice[16], false),
$this->get_contains_select_expectation('sub1', $choices, $orderforchoice[13], false),
$this->get_contains_select_expectation('sub2', $choices, $orderforchoice[13], false),
$this->get_contains_select_expectation('sub3', $choices, $orderforchoice[16], false),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'sub0', '0'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'sub1', '0'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'sub2', '0'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'sub3', '0'),
$this->get_contains_submit_button_expectation(false),
$this->get_contains_hint_expectation('This is the first hint.'));
// Try again.
$this->process_submission(array('sub0' => 0,
'sub1' => 0, 'sub2' => 0,
'sub3' => 0, '-tryagain' => 1));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_select_expectation('sub0', $choices, null, true),
$this->get_contains_select_expectation('sub1', $choices, null, true),
$this->get_contains_select_expectation('sub2', $choices, null, true),
$this->get_contains_select_expectation('sub3', $choices, null, true),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_feedback_expectation(),
$this->get_tries_remaining_expectation(2),
$this->get_no_hint_visible_expectation());
// Submit a partially wrong response.
$this->process_submission(array('sub0' => $orderforchoice[16],
'sub1' => $orderforchoice[13], 'sub2' => $orderforchoice[16],
'sub3' => $orderforchoice[13], '-submit' => 1));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_select_expectation('sub0', $choices, $orderforchoice[16], false),
$this->get_contains_select_expectation('sub1', $choices, $orderforchoice[13], false),
$this->get_contains_select_expectation('sub2', $choices, $orderforchoice[16], false),
$this->get_contains_select_expectation('sub3', $choices, $orderforchoice[13], false),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'sub0', '0'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'sub1', '0'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'sub2', $orderforchoice[16]),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'sub3', $orderforchoice[13]),
$this->get_contains_submit_button_expectation(false),
$this->get_contains_hint_expectation('This is the second hint.'));
// Try again.
$this->process_submission(array('sub0' => 0,
'sub1' => 0, 'sub2' => $orderforchoice[16],
'sub3' => $orderforchoice[13], '-tryagain' => 1));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_select_expectation('sub0', $choices, null, true),
$this->get_contains_select_expectation('sub1', $choices, null, true),
$this->get_contains_select_expectation('sub2', $choices, $orderforchoice[16], true),
$this->get_contains_select_expectation('sub3', $choices, $orderforchoice[13], true),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_feedback_expectation(),
$this->get_tries_remaining_expectation(1),
$this->get_no_hint_visible_expectation());
}
}

View File

@ -17,14 +17,21 @@
/**
* Version information for the randomsamatch question type.
*
* @package qtype
* @subpackage randomsamatch
* @package qtype_randomsamatch
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2013110500;
$plugin->version = 2013110510;
$plugin->requires = 2013110500;
$plugin->component = 'qtype_randomsamatch';
$plugin->dependencies = array(
'qtype_match' => 2013110500,
'qtype_shortanswer' => 2013110500,
);
$plugin->maturity = MATURITY_STABLE;