MDL-79863 qtype_ordering: qtype/ordering remember ordering question state when moving between pages in a quiz

This commit is contained in:
Gordon Bateson 2015-01-18 00:57:35 +09:00 committed by Mathew May
parent 1cbc814afa
commit dd5db2f3c0
10 changed files with 415 additions and 396 deletions

View File

@ -50,7 +50,7 @@ class backup_qtype_ordering_plugin extends backup_qtype_plugin {
$this->add_question_question_answers($pluginwrapper);
// Now create the qtype own structures
$fields = array('logical', 'studentsee',
$fields = array('selecttype', 'selectcount',
'correctfeedback', 'correctfeedbackformat',
'incorrectfeedback', 'incorrectfeedbackformat',
'partiallycorrectfeedback', 'partiallycorrectfeedbackformat');

View File

@ -4,12 +4,15 @@
<TABLE NAME="question_ordering" COMMENT="Options for ordering questions">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" NEXT="question"/>
<FIELD NAME="question" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="id" NEXT="logical"/>
<FIELD NAME="logical" TYPE="int" LENGTH="2" NOTNULL="true" UNSIGNED="false" DEFAULT="0" SEQUENCE="false" PREVIOUS="question" NEXT="studentsee"/>
<FIELD NAME="studentsee" TYPE="int" LENGTH="2" NOTNULL="true" UNSIGNED="false" DEFAULT="0" SEQUENCE="false" PREVIOUS="logical" NEXT="correctfeedback"/>
<FIELD NAME="correctfeedback" TYPE="text" LENGTH="small" NOTNULL="false" SEQUENCE="false" PREVIOUS="studentsee" NEXT="partiallycorrectfeedback"/>
<FIELD NAME="partiallycorrectfeedback" TYPE="text" LENGTH="small" NOTNULL="false" SEQUENCE="false" PREVIOUS="correctfeedback" NEXT="incorrectfeedback"/>
<FIELD NAME="incorrectfeedback" TYPE="text" LENGTH="small" NOTNULL="false" SEQUENCE="false" PREVIOUS="partiallycorrectfeedback"/>
<FIELD NAME="question" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="id" NEXT="selecttype"/>
<FIELD NAME="selecttype" TYPE="int" LENGTH="4" NOTNULL="true" UNSIGNED="false" DEFAULT="0" SEQUENCE="false" PREVIOUS="question" NEXT="selectcount"/>
<FIELD NAME="selectcount" TYPE="int" LENGTH="4" NOTNULL="true" UNSIGNED="false" DEFAULT="0" SEQUENCE="false" PREVIOUS="selecttype" NEXT="correctfeedback"/>
<FIELD NAME="correctfeedback" TYPE="text" LENGTH="small" NOTNULL="false" SEQUENCE="false" PREVIOUS="selectcount" NEXT="correctfeedbackformat"/>
<FIELD NAME="correctfeedbackformat" TYPE="int" LENGTH="2" NOTNULL="true" UNSIGNED="false" DEFAULT="0" SEQUENCE="false" PREVIOUS="correctfeedback" NEXT="incorrectfeedback"/>
<FIELD NAME="incorrectfeedback" TYPE="text" LENGTH="small" NOTNULL="false" SEQUENCE="false" PREVIOUS="correctfeedbackformat" NEXT="incorrectfeedbackformat"/>
<FIELD NAME="incorrectfeedbackformat" TYPE="int" LENGTH="2" NOTNULL="true" UNSIGNED="false" DEFAULT="0" SEQUENCE="false" PREVIOUS="incorrectfeedback" NEXT="partiallycorrectfeedback"/>
<FIELD NAME="partiallycorrectfeedback" TYPE="text" LENGTH="small" NOTNULL="false" SEQUENCE="false" PREVIOUS="incorrectfeedbackformat" NEXT="partiallycorrectfeedbackformat"/>
<FIELD NAME="partiallycorrectfeedbackformat" TYPE="int" LENGTH="2" NOTNULL="true" UNSIGNED="false" DEFAULT="0" SEQUENCE="false" PREVIOUS="partiallycorrectfeedback"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id" COMMENT="Primary key for question_ordering" NEXT="question"/>

View File

@ -61,15 +61,15 @@ function xmldb_qtype_ordering_upgrade($oldversion) {
upgrade_plugin_savepoint(true, $newversion, 'qtype', 'ordering');
}
$newversion = 2015011408;
$newversion = 2015011711;
if ($oldversion < $newversion) {
// rename "ordering" table for Moodle >= 2.5
$oldname = 'question_ordering';
$newname = 'qtype_ordering_options';
$oldtable = new xmldb_table($oldname);
if ($dbman->table_exists($oldtable)) {
if ($dbman->table_exists($oldname)) {
$oldtable = new xmldb_table($oldname);
if ($dbman->table_exists($newname)) {
$dbman->drop_table($oldtable);
} else {
@ -90,13 +90,18 @@ function xmldb_qtype_ordering_upgrade($oldversion) {
}
}
// add "feedbackformat" fields
// rename "question" -> "questionid"
// rename "logical" -> "selecttype"
// rename "studentsee" -> "selectcount"
// add "(xxx)feedbackformat" fields
$table = new xmldb_table('qtype_ordering_options');
$fields = array(
'questionid' => new xmldb_field('question', XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, null, '0', 'id'),
'correctfeedbackformat' => new xmldb_field('correctfeedbackformat', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, '0', 'correctfeedback'),
'incorrectfeedbackformat' => new xmldb_field('incorrectfeedbackformat', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, '0', 'incorrectfeedback'),
'partiallycorrectfeedbackformat' => new xmldb_field('partiallycorrectfeedbackformat', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, '0', 'partiallycorrectfeedback')
'selecttype' => new xmldb_field('logical', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, '0', 'questionid'),
'selectcount' => new xmldb_field('studentsee', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, '0', 'selecttype'),
'correctfeedbackformat' => new xmldb_field('correctfeedbackformat', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'correctfeedback'),
'incorrectfeedbackformat' => new xmldb_field('incorrectfeedbackformat', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'incorrectfeedback'),
'partiallycorrectfeedbackformat' => new xmldb_field('partiallycorrectfeedbackformat', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'partiallycorrectfeedback')
);
foreach ($fields as $newname => $field) {
$oldexists = $dbman->field_exists($table, $field);

View File

@ -36,8 +36,10 @@ defined('MOODLE_INTERNAL') || die();
*/
class qtype_ordering_edit_form extends question_edit_form {
const NUM_ANS_START = 10;
const NUM_ANS_ADD = 5;
const NUM_ANS_ROWS = 2;
const NUM_ANS_COLS = 60;
const NUM_ANS_START = 6;
const NUM_ANS_ADD = 2;
public function qtype() {
return 'ordering';
@ -50,40 +52,56 @@ class qtype_ordering_edit_form extends question_edit_form {
*/
public function definition_inner($mform) {
// logical
$name = 'logical';
$label = get_string('logicalpossibilities', 'qtype_ordering');
// cache this plugins name
$plugin = 'qtype_ordering';
// selecttype
$name = 'selecttype';
$label = get_string($name, $plugin);
$options = array(
0 => get_string('exactorder', 'qtype_ordering'), // = all ?
1 => get_string('relativeorder', 'qtype_ordering'), // = random subset
2 => get_string('contiguous', 'qtype_ordering') // = contiguous subset
0 => get_string('selectall', $plugin),
1 => get_string('selectrandom', $plugin),
2 => get_string('selectcontiguous', $plugin)
);
$mform->addElement('select', $name, $label, $options);
$mform->addHelpButton($name, $name, $plugin);
$mform->setDefault($name, 0);
// studentsee
$name = 'studentsee';
$label = get_string('itemsforstudent', 'qtype_ordering');
// selectcount
$name = 'selectcount';
$label = get_string($name, $plugin);
$options = array(0 => get_string('all'));
for ($i=3; $i <= 20; $i++) {
$options[] = $i;
$options[$i] = $i;
}
$mform->addElement('select', $name, $label, $options);
$mform->setDefault('studentsee', 0);
$mform->disabledIf($name, 'selecttype', 'eq', 0);
$mform->addHelpButton($name, $name, $plugin);
$mform->setDefault($name, 0);
// answers
// answers (=items)
$elements = array();
$elements[] =& $mform->createElement('header', 'choicehdr', get_string('choiceno', 'qtype_ordering', '{no}'));
$elements[] =& $mform->createElement('textarea', 'answer', get_string('answer', 'qtype_ordering'), 'rows="3" cols="50"');
$name = 'answerheader';
$label = get_string($name, $plugin);
$elements[] =& $mform->createElement('header', $name, $label);
$name = 'answer';
$label = get_string($name, $plugin);
$options = array('rows' => self::NUM_ANS_ROWS, 'cols' => self::NUM_ANS_COLS);
$elements[] =& $mform->createElement('textarea', $name, $label, $options);
if (empty($this->question->options)){
$count = 0;
$start = 0;
} else {
$count = count($this->question->options->answers);
$start = count($this->question->options->answers);
}
$start = max(self::NUM_ANS_START, $count + self::NUM_ANS_ADD);
$options = array('fraction' => array('default' => 0));
$buttontext = get_string('addmoreanswers', 'qtype_ordering');
$this->repeat_elements($elements, $start, $options, 'noanswers', 'addanswers', self::NUM_ANS_ADD, $buttontext);
if ($start < self::NUM_ANS_START) {
$start = self::NUM_ANS_START;
}
$options = array('answerheader' => array('expanded' => true));
$buttontext = get_string('addmoreanswers', $plugin, self::NUM_ANS_ADD);
$this->repeat_elements($elements, $start, $options, 'countanswers', 'addanswers', self::NUM_ANS_ADD, $buttontext);
// feedback
$this->add_ordering_feedback_fields();
@ -113,18 +131,18 @@ class qtype_ordering_edit_form extends question_edit_form {
}
// logical
if (isset($question->options->logical)) {
$question->logical = $question->options->logical;
// selecttype
if (isset($question->options->selecttype)) {
$question->selecttype = $question->options->selecttype;
} else {
$question->logical = 0;
$question->selecttype = 0;
}
// studentsee
if (isset($question->options->studentsee)) {
$question->studentsee = $question->options->studentsee;
// selectcount
if (isset($question->options->selectcount)) {
$question->selectcount = $question->options->selectcount;
} else {
$question->studentsee = max(3, count($question->answer));
$question->selectcount = max(3, count($question->answer));
}
return $question;
@ -132,6 +150,7 @@ class qtype_ordering_edit_form extends question_edit_form {
public function validation($data, $files) {
$errors = array();
$plugin = 'qtype_ordering';
$answercount = 0;
foreach ($data['answer'] as $answer){
@ -142,8 +161,8 @@ class qtype_ordering_edit_form extends question_edit_form {
}
switch ($answercount) {
case 0: $errors['answer[0]'] = get_string('notenoughanswers', 'qtype_ordering', 2);
case 1: $errors['answer[1]'] = get_string('notenoughanswers', 'qtype_ordering', 2);
case 0: $errors['answer[0]'] = get_string('notenoughanswers', $plugin, 2);
case 1: $errors['answer[1]'] = get_string('notenoughanswers', $plugin, 2);
}
return $errors;

View File

@ -15,35 +15,25 @@ $string['pluginnameadding'] = 'Adding an Ordering question';
$string['pluginnameediting'] = 'Editing an Ordering question';
$string['pluginnamesummary'] = 'Put jumbled items into a meaningful order.';
$string['editingordering'] = 'Editing ordering';
$string['addingordering'] = 'Adding a Ordering';
$string['orderingsummary'] = 'Ordering summary';
$string['ordering'] = 'Ordering';
$string['exactorder'] = 'Exact order';
$string['relativeorder'] = 'Relative order';
$string['contiguous'] = 'Contiguous';
$string['logicalpossibilities'] = 'Logical possibilities';
$string['addmoreanswers'] = 'Blank for {no} more answers';
$string['choiceno'] = 'Answer {$a}';
$string['choices'] = 'Available answers';
$string['answer'] = 'Answer';
$string['itemsforstudent'] = 'How many items the student will see';
$string['addmoreanswers'] = 'Add {$a} more items';
$string['answer'] = 'Item text';
$string['answerheader'] = 'Draggable item {no}';
$string['correctorder'] = 'The correct order for these items is as follows:';
$string['defaultquestionname'] = 'Drag the following items into the correct order.';
$string['noresponsedetails'] = 'Sorry, no details of the response to this question are available.';
$string['notenoughanswers'] = 'Ordering questions must have more than {$a} answers.';
$string['selectall'] = 'Select all items';
$string['selectcontiguous'] = 'Select a contiguous subset of items';
$string['selectcount_help'] = 'The number of items that will be displayed when the question is appears in a quiz.';
$string['selectcount'] = 'Size of subset';
$string['selectrandom'] = 'Select a random subset of items';
$string['selecttype_help'] = 'Choose whether to display all the items or a subset of the items.';
$string['selecttype'] = 'Item selection type';
//$string['correctfeedback'] = 'Correct answer';
//$string['incorrectfeedback'] = 'Incorrect answer';
//$string['overallfeedback'] = 'Overall Feedback';
//$string['partiallycorrectfeedback'] = 'Partially correct answer';
$string['notenoughanswers'] = 'not enough answers';
$string['comment'] = 'Drag and drop the items into the correct order.';
// TODO add any other requred strings.
$string['ordering_help'] = 'Ordering help';
$string['defaultquestionname'] = 'Put the following events into the correct order.';
// requred strings for Moodle 2.0
$string['addingordering'] = $string['pluginnameadding'];
$string['editingordering'] = $string['pluginnameediting'];
$string['ordering_help'] = $string['pluginname_help'];
$string['ordering_link'] = $string['pluginname_link'];
$string['ordering'] = $string['pluginname'];
$string['orderingsummary'] = $string['pluginnamesummary'];

View File

@ -27,7 +27,7 @@
* @subpackage questiontypes
*/
require_once($CFG->libdir . '/questionlib.php');
require_once($CFG->libdir.'/questionlib.php');
/**
* This is the base class for Moodle question types.
@ -46,6 +46,7 @@ require_once($CFG->libdir . '/questionlib.php');
* @subpackage questiontypes
*/
class question_type extends default_questiontype {
protected $fileoptions = array(
'subdirs' => false,
'maxfiles' => -1,
@ -708,17 +709,12 @@ class question_type extends default_questiontype {
*/
function get_all_responses(&$question, &$state) {
if (isset($question->options->answers) && is_array($question->options->answers)) {
$answers = array();
foreach ($question->options->answers as $aid=>$answer) {
$r = new stdClass;
$r->answer = $answer->answer;
$r->credit = $answer->fraction;
$answers[$aid] = $r;
$responses = array();
foreach ($question->options->answers as $id => $answer) {
$responses[$id] = (object)array('answer' => $answer->answer,
'credit' => $answer->fraction);
}
$result = new stdClass;
$result->id = $question->id;
$result->responses = $answers;
return $result;
return (object)array('id' => $question->id, 'responses' => $responses);
} else {
return null;
}
@ -1258,28 +1254,28 @@ class question_type extends default_questiontype {
return; // shouldn't happen !!
}
if ($question->options->studentsee==0) { // all items
$question->options->studentsee = count($answers);
if ($question->options->selectcount==0) { // all items
$question->options->selectcount = count($answers);
} else {
// a nasty hack so that "studentsee" is the same
// a nasty hack so that "selectcount" is the same
// as what is displayed by edit_ordering_form.php
$question->options->studentsee += 2;
$question->options->selectcount += 2;
}
switch ($question->options->logical) {
switch ($question->options->selecttype) {
case 0: // all
$answerids = array_keys($answers);
break;
case 1: // random subset
$answerids = array_rand($answers, $question->options->studentsee);
$answerids = array_rand($answers, $question->options->selectcount);
break;
case 2: // contiguous subset
if (count($answers) > $question->options->studentsee) {
$offset = mt_rand(0, count($answers) - $question->options->studentsee);
$answers = array_slice($answers, $offset, $question->options->studentsee, true);
if (count($answers) > $question->options->selectcount) {
$offset = mt_rand(0, count($answers) - $question->options->selectcount);
$answers = array_slice($answers, $offset, $question->options->selectcount, true);
}
$answerids = array_keys($answers);
break;
@ -1867,3 +1863,29 @@ class question_type extends default_questiontype {
}
}
}
abstract class qtype_with_combined_feedback_renderer extends qtype_renderer {
protected function combined_feedback(question_attempt $qa) {
$question = $qa->get_question();
$state = $qa->get_state();
if (!$state->is_finished()) {
$response = $qa->get_last_qt_data();
if (!$qa->get_question()->is_gradable_response($response)) {
return '';
}
list($notused, $state) = $qa->get_question()->grade_response($response);
}
$feedback = '';
$field = $state->get_feedback_class() . 'feedback';
$format = $state->get_feedback_class() . 'feedbackformat';
if ($question->$field) {
$feedback .= $question->format_text($question->$field, $question->$format,
$qa, 'question', $field, $question->id);
}
return $feedback;
}
}

View File

@ -33,12 +33,78 @@ defined('MOODLE_INTERNAL') || die();
*/
class qtype_ordering_question extends question_graded_automatically {
public $answers;
public $options;
public $rightanswer;
/** fields from "qtype_ordering_options" */
public $correctfeedback;
public $correctfeedbackformat;
public $incorrectfeedback;
public $incorrectfeedbackformat;
public $partiallycorrectfeedback;
public $partiallycorrectfeedbackformat;
public function get_response_fieldname() {
return 'response_'.$this->id;
/** records from "question_answers" table */
public $answers;
/** records from "qtype_ordering_options" table */
public $options;
/** array of answerids in correct order */
public $correctresponse;
/** array current order of answerids */
public $currentresponse;
public function start_attempt(question_attempt_step $step, $variant) {
$this->answers = $this->get_ordering_answers();
$this->options = $this->get_ordering_options();
$countanswers = count($this->answers);
// sanitize "selecttype"
$selecttype = $this->options->selecttype;
$selecttype = max(0, $selecttype);
$selecttype = min(2, $selecttype);
// sanitize "selectcount"
$selectcount = $this->options->selectcount;
$selectcount = max(3, $selectcount);
$selectcount = min($countanswers, $selectcount);
// ensure consistency between "selecttype" and "selectcount"
switch (true) {
case ($selecttype==0): $selectcount = $countanswers; break;
case ($selectcount==$countanswers): $selecttype = 0; break;
}
// extract answer ids
switch ($selecttype) {
case 0: // all
$answerids = array_keys($this->answers);
break;
case 1: // random subset
$answerids = array_rand($this->answers, $selectcount);
break;
case 2: // contiguous subset
$answerids = array_keys($this->answers);
$offset = mt_rand(0, $countanswers - $selectcount);
$answerids = array_slice($answerids, $offset, $selectcount, true);
break;
}
$this->correctresponse = $answerids;
$step->set_qt_var('_correctresponse', implode(',', $this->correctresponse));
shuffle($answerids);
$this->currentresponse = $answerids;
$step->set_qt_var('_currentresponse', implode(',', $this->currentresponse));
}
public function apply_attempt_state(question_attempt_step $step) {
$this->answers = $this->get_ordering_answers();
$this->options = $this->get_ordering_options();
$this->correctresponse = explode(',', $step->get_qt_var('_correctresponse'));
$this->currentresponse = explode(',', $step->get_qt_var('_currentresponse'));
}
public function format_questiontext($qa) {
@ -52,24 +118,85 @@ class qtype_ordering_question extends question_graded_automatically {
}
public function get_correct_response() {
if ($this->rightanswer===null) {
$this->rightanswer = $this->get_ordering_answers();
$this->rightanswer = array_keys($this->rightanswer);
$this->rightanswer = implode(',', $this->rightanswer);
$correctresponse = $this->correctresponse;
foreach ($correctresponse as $position => $answerid) {
$answer = $this->answers[$answerid];
$correctresponse[$position] = $answer->md5key;
}
return array('answer' => $this->rightanswer);
$name = $this->get_response_fieldname();
return array($name => implode(',', $correctresponse));
}
public function summarise_response(array $response) {
return '';
}
public function classify_response(array $response) {
if (array_key_exists('answer', $response)) {
$responseclassid = ($response['answer'] ? 1 : 0);
list($fraction) = $this->grade_response($response);
return array($this->id => new question_classified_response($responseclassid, get_string('true', 'qtype_ordering'), $fraction));
return array();
}
public function is_complete_response(array $response) {
return true;
}
public function is_gradable_response(array $response) {
return true;
}
public function get_validation_error(array $response) {
return '';
}
public function is_same_response(array $old, array $new) {
$name = $this->get_response_fieldname();
return (isset($old[$name]) && isset($new[$name]) && $old[$name]==$new[$name]);
}
public function grade_response(array $response) {
$this->update_current_response($response);
$countcorrect = 0;
$countanswers = 0;
$correctresponse = $this->correctresponse;
$currentresponse = $this->currentresponse;
foreach ($currentresponse as $position => $answerid) {
if ($correctresponse[$position]==$answerid) {
$countcorrect++;
}
$countanswers++;
}
if ($countanswers==0) {
$fraction = 0;
} else {
return array($this->id => question_classified_response::no_response());
$fraction = ($countcorrect / $countanswers);
}
return array($fraction, question_state::graded_state_for_fraction($fraction));
}
public function check_file_access($qa, $options, $component, $filearea, $args, $forcedownload) {
return parent::check_file_access($qa, $options, $component, $filearea, $args, $forcedownload);
}
////////////////////////////////////////////////////////////////////
// custom methods
////////////////////////////////////////////////////////////////////
public function get_response_fieldname() {
return 'response_'.$this->id;
}
public function update_current_response($response) {
$name = $this->get_response_fieldname();
if (isset($response[$name])) {
$ids = explode(',', $response[$name]);
foreach ($ids as $i => $id) {
foreach ($this->answers as $answer) {
if ($id==$answer->md5key) {
$ids[$i] = $answer->id;
break;
}
}
}
$this->currentresponse = $ids;
}
}
@ -77,17 +204,17 @@ class qtype_ordering_question extends question_graded_automatically {
global $DB;
if ($this->options===null) {
$this->options = $DB->get_record('qtype_ordering_options', array('questionid' => $this->id));
if ($this->options==false) {
if (empty($this->options)) {
$this->options = (object)array(
'questionid' => $this->id,
'logical' => 0, // require all answers
'studentsee' => 0,
'selecttype' => 0, // all answers
'selectcount' => 0,
'correctfeedback' => '',
'correctfeedbackformat' => 0,
'correctfeedbackformat' => FORMAT_MOODLE, // =0
'incorrectfeedback' => '',
'incorrectfeedbackformat' => 0,
'incorrectfeedbackformat' => FORMAT_MOODLE, // =0
'partiallycorrectfeedback' => '',
'partiallycorrectfeedbackformat' => 0
'partiallycorrectfeedbackformat' => FORMAT_MOODLE // =0
);
$this->options->id = $DB->insert_record('qtype_ordering', $options);
}
@ -114,135 +241,4 @@ class qtype_ordering_question extends question_graded_automatically {
}
return $this->answers;
}
public function is_complete_response(array $response) {
global $CFG, $DB, $SESSION;
$name = $this->get_response_fieldname();
if (isset($response[$name])) {
$responses = $response[$name];
} else {
$responses = optional_param($name, '', PARAM_TEXT);
}
$responses = preg_replace('[^a-zA-Z0-9,_-]', '', $responses);
$responses = explode(',', $responses); // convert to array
$responses = array_filter($responses); // remove blanks
$responses = array_unique($responses); // remove duplicates
foreach ($responses as $i => $response) {
if (substr($response, 0, 14)=='ordering_item_') {
$responses[$i] = substr($response, 14);
} else {
unset($responses[$i]); // remove invalid response
}
}
$ordering = $this->get_ordering_options();
$answers = $this->get_ordering_answers();
if ($ordering->logical==0) {
$total = count($answers); // require all answers
} else {
$total = $ordering->studentsee + 2; // a subset of answers
}
if (isset($CFG->passwordsaltmain)) {
$salt = $CFG->passwordsaltmain;
} else {
$salt = ''; // complex_random_string()
}
$validresponses = array();
foreach ($answers as $answerid => $answer) {
$response = md5($salt.$answer->answer);
$sortorder = intval($answer->fraction);
if (in_array($response, $responses)) {
$answers[$answerid]->sortorder = $sortorder;
$answers[$answerid]->response = $response;
$validresponses[] = $response;
} else {
unset($answers[$answerid]); // this answer is not used
}
}
// convert $answers to sequentially numbered array
$answers = array_values($answers);
// sort $answers by sortorder (not really necessary)
usort($answers, array($this, 'usort_sortorder'));
// remove invalid responses
foreach ($responses as $i => $response) {
if (! in_array($response, $validresponses)) {
unset($responses[$i]);
}
}
unset($validresponses);
$correct = 0;
foreach ($answers as $i => $answer) {
if (isset($responses[$i]) && $answer->response==$responses[$i]) {
$correct++;
}
$i++;
}
if ($total==0) {
$grade = 0;
} else {
$grade = round($correct / $total, 5);
}
// we use $SESSION instead of accessing $_SESSION directly
// $_SESSION['SESSION']->quiz_answer['q'.$this->id] = $grade;
if (empty($SESSION->quiz_answer)) {
$SESSION->quiz_answer = array();
}
$SESSION->quiz_answer['q'.$this->id] = $grade;
return true;
}
public function usort_sortorder($a, $b) {
if ($a->sortorder < $b->sortorder) {
return -1;
}
if ($a->sortorder > $b->sortorder) {
return 1;
}
return 0; // equal values
}
public function is_gradable_response(array $response) {
return true;
}
public function get_validation_error(array $response) {
return '';
}
public function is_same_response(array $prevresponse, array $newresponse) {
}
public function compute_final_grade($responses, $totaltries) {
return 1;
}
public function grade_response(array $response) {
global $SESSION;
if (empty($SESSION->quiz_answer['q'.$this->id])) {
$fraction = 0;
} else {
$fraction = $SESSION->quiz_answer['q'.$this->id];
}
return array($fraction, question_state::graded_state_for_fraction($fraction));
}
public function check_file_access($qa, $options, $component, $filearea, $args, $forcedownload) {
return parent::check_file_access($qa, $options, $component, $filearea, $args, $forcedownload);
}
}

View File

@ -30,7 +30,7 @@ if (class_exists('question_type')) {
$register_questiontype = false;
} else {
$register_questiontype = true; // Moodle 2.0
require_once(dirname(__FILE__).'/legacy/20.php');
require_once($CFG->dirroot.'/question/type/ordering/legacy/20.php');
}
/**
@ -70,8 +70,7 @@ class qtype_ordering extends question_type {
protected function initialise_question_instance(question_definition $question, $questiondata) {
parent::initialise_question_instance($question, $questiondata);
$answers = array_keys($questiondata->options->answers);
$question->rightanswer = implode(',', $answers);
$this->initialise_ordering_feedback($question, $questiondata);
}
public function save_question_options($question) {
@ -119,6 +118,7 @@ class qtype_ordering extends question_type {
return $result;
}
} else {
unset($answer->id);
if (! $answer->id = $DB->insert_record('question_answers', $answer)) {
$result->error = get_string('cannotinsertrecord', 'error', 'question_answers');
return $result;
@ -129,8 +129,8 @@ class qtype_ordering extends question_type {
// create $options for this ordering question
$options = (object)array(
'questionid' => $question->id,
'logical' => $question->logical,
'studentsee' => $question->studentsee
'selecttype' => $question->selecttype,
'selectcount' => $question->selectcount
);
$options = $this->save_ordering_feedback_helper($options, $question, $context, true);
@ -156,17 +156,53 @@ class qtype_ordering extends question_type {
return true;
}
protected function save_ordering_feedback_helper($options, $question, $context, $withparts = false) {
protected function initialise_ordering_feedback($question, $questiondata, $shownumcorrect=false) {
if (method_exists($this, 'initialise_combined_feedback')) {
// Moodle >= 2.1
$options = $this->initialise_combined_feedback($question, $questiondata, $shownumcorrect);
} else {
// Moodle 2.0
$names = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback');
foreach ($names as $name) {
$format = $name.'format';
$question->$name = $questiondata->options->$name;
$question->$format = $questiondata->options->$format;
}
if ($shownumcorrect) {
$question->shownumcorrect = $questiondata->options->shownumcorrect;
}
}
return $options;
}
public function get_possible_responses($questiondata) {
$responses = array();
$question = $this->make_question($questiondata);
foreach ($question->correctresponse as $position => $answerid) {
$responses[] = $position.': '.$question->answers[$answerid]->answer;
}
$responses = array(
0 => question_possible_response::no_response(),
1 => implode(', ', $responses)
);
return ;
}
protected function save_ordering_feedback_helper($options, $question, $context, $shownumcorrect=false) {
if (method_exists($this, 'save_combined_feedback_helper')) {
// Moodle >= 2.1
$options = $this->save_combined_feedback_helper($options, $question, $context, $withparts);
$options = $this->save_combined_feedback_helper($options, $question, $context, $shownumcorrect);
} else {
// Moodle 2.0
$names = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback');
foreach ($names as $name) {
$text = $question->$name;
$format = $name.'format';
$options->$name = $this->import_or_save_files($text, $context, 'qtype_ordering', $name, $question->id);
$options->{$name.'format'} = $text['format'];
$options->$format = $text['format'];
}
if ($shownumcorrect) {
$options->shownumcorrect = (isset($question->shownumcorrect) && $question->shownumcorrect);
}
}
return $options;
@ -196,9 +232,6 @@ class qtype_ordering extends question_type {
return true;
}
// following seems to be unnecessary ...
// initialise_question_instance(question_definition $question, $questiondata)
public function delete_question($questionid, $contextid) {
global $DB;
$DB->delete_records('qtype_ordering_options', array('questionid' => $questionid));
@ -230,8 +263,8 @@ class qtype_ordering extends question_type {
}
$questionname = trim($matches[1]);
$numberofitems = trim($matches[2]);
$extractiontype = trim($matches[3]);
$selectcount = trim($matches[2]);
$selecttype = trim($matches[3]);
$lines = explode(PHP_EOL, $matches[4]);
unset($matches);
@ -241,13 +274,13 @@ class qtype_ordering extends question_type {
// fix empty or long question name
$question->name = $this->fix_questionname($question->name, $questionname);
// set "studentsee" field from $numberofitems
if (is_numeric($numberofitems) && $numberofitems > 2 && $numberofitems <= count($lines)) {
$numberofitems = intval($numberofitems);
// set "selectcount" field from $selectcount
if (is_numeric($selectcount) && $selectcount > 2 && $selectcount <= count($lines)) {
$selectcount = intval($selectcount);
} else {
$numberofitems = min(6, count($lines));
$selectcount = min(6, count($lines));
}
$this->set_num_and_type($question, $numberofitems, $extractiontype);
$this->set_count_and_type($question, $selectcount, $selecttype);
// remove blank items
$lines = array_map('trim', $lines);
@ -276,20 +309,23 @@ class qtype_ordering extends question_type {
}
/**
* extract_num_and_type
* extract_count_and_type
*
* @param stdClass $question
* @todo Finish documenting this function
*/
function extract_num_and_type($question) {
switch ($question->options->logical) {
case 0: $type = 'EXACT'; break; // all
case 1: $type = 'REL'; break; // random subset
case 2: $type = 'CONTIG'; break; // contiguous subset
default: $type = ''; // shouldn't happen !!
function extract_count_and_type($question) {
switch ($question->options->selecttype) {
case 0: $type = 'ALL'; break; // all items
case 1: $type = 'RANDOM'; break; // random subset
case 2: $type = 'CONTIGUOUS'; break; // contiguous subset
default: $type = ''; // shouldn't happen !!
}
$num = $question->options->studentsee + 2;
return array($num, $type);
// Note: this used to be (selectcount + 2)
$count = $question->options->selectcount;
return array($count, $type);
}
/**
@ -301,9 +337,9 @@ class qtype_ordering extends question_type {
* @todo Finish documenting this function
*/
function export_to_gift($question, $format, $extra=null) {
list($num, $type) = $this->extract_num_and_type($question);
list($count, $type) = $this->extract_count_and_type($question);
$expout = $question->questiontext.'{>'.$num.' '.$type.' '."\n";
$expout = $question->questiontext.'{>'.$count.' '.$type.' '."\n";
foreach ($question->options->answers as $answer) {
$expout .= $answer->answer."\n";
}
@ -322,11 +358,11 @@ class qtype_ordering extends question_type {
*/
function export_to_xml($question, qformat_xml $format, $extra=null) {
list($num, $type) = $this->extract_num_and_type($question);
list($count, $type) = $this->extract_count_and_type($question);
$output = '';
$output .= " <logical>$type</logical>\n";
$output .= " <studentsee>$num</studentsee>\n";
$output .= " <selecttype>$type</selecttype>\n";
$output .= " <selectcount>$count</selectcount>\n";
foreach($question->options->answers as $answer) {
$output .= ' <answer fraction="'.$answer->fraction.'" '.$format->format($answer->answerformat).">\n";
@ -367,10 +403,18 @@ class qtype_ordering extends question_type {
// fix empty or long question name
$newquestion->name = $this->fix_questionname($newquestion->name, $newquestion->questiontext);
// extra fields fields fields - "logical" and "studentsee"
$numberofitems = $format->getpath($data, array('#', 'studentsee', 0, '#'), 6);
$extractiontype = $format->getpath($data, array('#', 'logical', 0, '#'), 'RANDOM');
$this->set_num_and_type($newquestion, $numberofitems, $extractiontype);
// extra fields fields fields - "selecttype" and "selectcount"
// (these fields used to be called "logical" and "studentsee")
if (isset($data['#']['selecttype'])) {
$selecttype = 'selecttype';
$selectcount = 'selectcount';
} else {
$selecttype = 'logical';
$selectcount = 'studentsee';
}
$selecttype = $format->getpath($data, array('#', $selecttype, 0, '#'), 'RANDOM');
$selectcount = $format->getpath($data, array('#', $selectcount, 0, '#'), 6);
$this->set_count_and_type($newquestion, $selectcount, $selecttype);
$newquestion->answer = array();
$newquestion->answerformat = array();
@ -421,38 +465,39 @@ class qtype_ordering extends question_type {
}
/*
* set_num_and_type
* set_count_and_type
*
* @param object $question (passed by reference)
* @param integer $num the number of items to display
* @param integer $count the number of items to display
* @param integer $type the extraction type
* @param integer $default_type (optional, default=1)
*/
function set_num_and_type(&$question, $num, $type, $default_type=1) {
function set_count_and_type(&$question, $count, $type, $default_type=1) {
// set "studentsee" from $num(ber of items)
$question->studentsee = ($num - 2);
// set "selectcount" from $count
// this used to be ($count - 2)
$question->selectcount = $count;
// set "logical" from (extraction) $type
// set "selecttype" from $type
switch ($type) {
case 'ALL':
case 'EXACT':
$question->logical = 0;
$question->selecttype = 0;
break;
case 'RANDOM':
case 'REL':
$question->logical = 1;
$question->selecttype = 1;
break;
case 'CONTIGUOUS':
case 'CONTIG':
$question->logical = 2;
$question->selecttype = 2;
break;
// otherwise
default:
$question->logical = $default_type;
$question->selecttype = $default_type;
}
}
}

View File

@ -26,58 +26,35 @@
defined('MOODLE_INTERNAL') || die();
if (! class_exists('qtype_with_combined_feedback_renderer')) {
// Moodle 2.0
require_once($CFG->dirroot.'/question/type/ordering/legacy/20.php');
}
/**
* Generates the output for ORDERING questions
*
* @copyright 2013 Gordon Bateson
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_ordering_renderer extends qtype_renderer {
class qtype_ordering_renderer extends qtype_with_combined_feedback_renderer {
public function formulation_and_controls(question_attempt $qa, question_display_options $options) {
global $CFG, $DB;
$question = $qa->get_question();
$ordering = $question->get_ordering_options();
$answers = $question->get_ordering_answers();
$response = $qa->get_last_qt_data();
$question->update_current_response($response);
$currentresponse = $question->currentresponse;
$correctresponse = $question->correctresponse;
if (empty($ordering) || empty($answers)) {
return ''; // shouldn't happen !!
}
if ($ordering->studentsee==0) { // all items
$ordering->studentsee = count($answers);
} else {
// a nasty hack so that "studentsee" is the same
// as what is displayed by edit_ordering_form.php
$ordering->studentsee += 2;
}
if ($options->correctness) {
list($answerids, $correctorder) = $this->get_response($qa, $question, $answers);
} else {
$correctorder = array();
switch ($ordering->logical) {
case 0: // all
$answerids = array_keys($answers);
break;
case 1: // random subset
$answerids = array_rand($answers, $ordering->studentsee);
break;
case 2: // contiguous subset
if (count($answers) > $ordering->studentsee) {
$offset = mt_rand(0, count($answers) - $ordering->studentsee);
$answers = array_slice($answers, $offset, $ordering->studentsee, true);
}
$answerids = array_keys($answers);
break;
}
shuffle($answerids);
}
$response_name = $qa->get_qt_field_name($question->get_response_fieldname());
// generate fieldnames and ids
// response_fieldname : fieldname: 1_response_319
// response_name : q27:1_response_319
// response_id : id_q27_1_response_319
// sortable_id : id_sortable_q27_1_response_319
$response_fieldname = $question->get_response_fieldname();
$response_name = $qa->get_qt_field_name($response_fieldname);
$response_id = 'id_'.preg_replace('/[^a-zA-Z0-9]+/', '_', $response_name);
$sortable_id = 'id_sortable_'.$question->id;
@ -107,16 +84,16 @@ class qtype_ordering_renderer extends qtype_renderer {
$result .= html_writer::tag('div', $question->format_questiontext($qa), array('class' => 'qtext'));
if (count($answerids)) {
if (count($currentresponse)) {
$result .= html_writer::start_tag('div', array('class' => 'ablock'));
$result .= html_writer::start_tag('div', array('class' => 'answer'));
$result .= html_writer::start_tag('ul', array('class' => 'sortablelist', 'id' => $sortable_id));
// generate ordering items
foreach ($answerids as $position => $answerid) {
if (array_key_exists($answerid, $answers)) {
foreach ($currentresponse as $position => $answerid) {
if (array_key_exists($answerid, $question->answers)) {
if ($options->correctness) {
if ($correctorder[$position]==$answerid) {
if ($correctresponse[$position]==$answerid) {
$class = 'correctposition';
$img = $this->feedback_image(1);
} else {
@ -130,10 +107,10 @@ class qtype_ordering_renderer extends qtype_renderer {
}
// the original "id" revealed the correct order of the answers
// because $answer->fraction holds the correct order number
// $id = 'ordering_item_'.$answerid.'_'.intval($answers[$answerid]->fraction);
$params = array('class' => $class, 'id' => $answers[$answerid]->md5key);
$result .= html_writer::tag('li', $img.$answers[$answerid]->answer, $params);
// $id = 'ordering_item_'.$answerid.'_'.intval($question->answers[$answerid]->fraction);
$answer = $question->answers[$answerid];
$params = array('class' => $class, 'id' => $answer->md5key);
$result .= html_writer::tag('li', $img.$answer->answer, $params);
}
}
@ -151,78 +128,40 @@ class qtype_ordering_renderer extends qtype_renderer {
return $result;
}
public function specific_feedback(question_attempt $qa) {
return $this->combined_feedback($qa);
}
public function correct_response(question_attempt $qa) {
global $DB;
$output = '';
$showcorrect = true;
$showcorrect = false;
if ($step = $qa->get_last_step()) {
switch ($step->get_state()) {
case 'gradedright':
$showcorrect = false;
$msg = get_string('correctfeedback', 'qtype_ordering');
break;
case 'gradedpartial':
$fraction = round($step->get_fraction(), 2);
$msg = get_string('partiallycorrectfeedback', 'qtype_ordering', $fraction);
break;
case 'gradedwrong':
$msg = get_string('incorrectfeedback', 'qtype_ordering');
break;
default:
$showcorrect = false;
$msg = '';
}
if ($msg) {
$output .= html_writer::tag('p', $msg);
case 'gradedright' : $showcorrect = false; break;
case 'gradedpartial': $showcorrect = true; break;
case 'gradedwrong' : $showcorrect = true; break;
}
}
if ($showcorrect) {
$question = $qa->get_question();
$answers = $question->get_ordering_answers();
list($answerids, $correctorder) = $this->get_response($qa, $question, $answers);
if (count($correctorder)) {
if (empty($question->correctresponse)) {
$output .= html_writer::tag('p', get_string('noresponsedetails', 'qtype_ordering'));
} else {
$output .= html_writer::tag('p', get_string('correctorder', 'qtype_ordering'));
$output .= html_writer::start_tag('ol');
foreach ($correctorder as $position => $answerid) {
$output .= html_writer::tag('li', $answers[$answerid]->answer);
$correctresponse = $question->correctresponse;
foreach ($correctresponse as $position => $answerid) {
$answer = $question->answers[$answerid];
$output .= html_writer::tag('li', $answer->answer);
}
$output .= html_writer::end_tag('ol');
} else {
$output .= html_writer::tag('p', get_string('noresponsedetails', 'qtype_ordering'));
}
}
return $output;
}
public function get_response($qa, $question, $answers) {
$answerids = array();
$correctorder = array();
$name = $question->get_response_fieldname();
if ($step = $qa->get_last_step_with_qt_var($name)) {
$response = $step->get_qt_var($name); // "$md5key, ..."
$response = explode(',', $response); // array($position => $md5key, ...)
$response = array_flip($response); // array($md5key => $position, ...)
foreach ($answers as $answer) {
if (array_key_exists($answer->md5key, $response)) {
$position = $response[$answer->md5key];
$sortorder = intval($answer->fraction);
$answerids[$position] = $answer->id;
$correctorder[$sortorder] = $answer->id;
}
}
ksort($answerids);
ksort($correctorder);
$correctorder = array_values($correctorder);
}
return array($answerids, $correctorder);
}
}

View File

@ -30,6 +30,6 @@ defined('MOODLE_INTERNAL') || die();
$plugin->cron = 0;
$plugin->component = 'qtype_ordering';
$plugin->maturity = MATURITY_STABLE; // ALPHA=50, BETA=100, RC=150, STABLE=200
$plugin->release = '2015-01-14 (10)';
$plugin->version = 2015011410;
$plugin->release = '2015-01-17 (11)';
$plugin->version = 2015011711;
$plugin->requires = 2010112400; // Moodle 2.0