mirror of
https://github.com/moodle/moodle.git
synced 2025-01-19 06:18:28 +01:00
Merge branch 'MDL-27414' of git://github.com/jmvedrine/moodle
This commit is contained in:
commit
35a8d26926
@ -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)];
|
||||
}
|
||||
|
||||
|
71
question/type/randomsamatch/backup/moodle1/lib.php
Normal file
71
question/type/randomsamatch/backup/moodle1/lib.php
Normal 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'));
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
190
question/type/randomsamatch/db/upgrade.php
Normal file
190
question/type/randomsamatch/db/upgrade.php
Normal 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;
|
||||
}
|
243
question/type/randomsamatch/db/upgradelib.php
Normal file
243
question/type/randomsamatch/db/upgradelib.php
Normal 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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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.';
|
||||
|
47
question/type/randomsamatch/lib.php
Normal file
47
question/type/randomsamatch/lib.php
Normal 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);
|
||||
}
|
140
question/type/randomsamatch/question.php
Normal file
140
question/type/randomsamatch/question.php
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
43
question/type/randomsamatch/renderer.php
Normal file
43
question/type/randomsamatch/renderer.php
Normal 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);
|
||||
}
|
||||
}
|
139
question/type/randomsamatch/tests/helper.php
Normal file
139
question/type/randomsamatch/tests/helper.php
Normal 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;
|
||||
}
|
||||
}
|
153
question/type/randomsamatch/tests/question_test.php
Normal file
153
question/type/randomsamatch/tests/question_test.php
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
406
question/type/randomsamatch/tests/upgradelibnewqe_test.php
Normal file
406
question/type/randomsamatch/tests/upgradelibnewqe_test.php
Normal 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);
|
||||
}
|
||||
}
|
435
question/type/randomsamatch/tests/walkthrough_test.php
Normal file
435
question/type/randomsamatch/tests/walkthrough_test.php
Normal 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());
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user