MDL-47494 gapselect: work-in-progress converting the ddwtos and gapselect qtypes.
authorTim Hunt <T.J.Hunt@open.ac.uk>
Fri, 28 Jan 2011 19:07:33 +0000 (19:07 +0000)
committerTim Hunt <T.J.Hunt@open.ac.uk>
Fri, 28 Jan 2011 19:07:33 +0000 (19:07 +0000)
17 files changed:
question/type/gapselect/db/install.xml [new file with mode: 0755]
question/type/gapselect/edit_form_base.php [new file with mode: 0755]
question/type/gapselect/edit_gapselect_form.php [new file with mode: 0755]
question/type/gapselect/icon.gif [new file with mode: 0755]
question/type/gapselect/lang/en/qtype_gapselect.php [new file with mode: 0755]
question/type/gapselect/question.php [new file with mode: 0755]
question/type/gapselect/questionbase.php [new file with mode: 0755]
question/type/gapselect/questiontype.php [new file with mode: 0755]
question/type/gapselect/questiontypebase.php [new file with mode: 0755]
question/type/gapselect/renderer.php [new file with mode: 0755]
question/type/gapselect/rendererbase.php [new file with mode: 0755]
question/type/gapselect/simpletest/helper.php [new file with mode: 0755]
question/type/gapselect/simpletest/testquestion.php [new file with mode: 0755]
question/type/gapselect/simpletest/testquestiontype.php [new file with mode: 0755]
question/type/gapselect/simpletest/testwalkthrough.php [new file with mode: 0755]
question/type/gapselect/styles.css [new file with mode: 0755]
question/type/gapselect/version.php [new file with mode: 0755]

diff --git a/question/type/gapselect/db/install.xml b/question/type/gapselect/db/install.xml
new file mode 100755 (executable)
index 0000000..0eb4f71
--- /dev/null
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<XMLDB PATH="question/type/gapselect/db" VERSION="20080909" COMMENT="XMLDB file for Moodle question/type/gapselect"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:noNamespaceSchemaLocation="../../../../lib/xmldb/xmldb.xsd"
+>
+  <TABLES>
+    <TABLE NAME="question_gapselect" COMMENT="Defines select missing words questions">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" NEXT="questionid"/>
+        <FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="id" NEXT="shuffleanswers"/>
+        <FIELD NAME="shuffleanswers" TYPE="int" LENGTH="4" NOTNULL="true" UNSIGNED="true" DEFAULT="1" SEQUENCE="false" PREVIOUS="questionid" NEXT="correctfeedback"/>
+        <FIELD NAME="correctfeedback" TYPE="text" LENGTH="small" NOTNULL="true" SEQUENCE="false" PREVIOUS="shuffleanswers" NEXT="partiallycorrectfeedback"/>
+        <FIELD NAME="partiallycorrectfeedback" TYPE="text" LENGTH="small" NOTNULL="true" SEQUENCE="false" PREVIOUS="correctfeedback" NEXT="incorrectfeedback"/>
+        <FIELD NAME="incorrectfeedback" TYPE="text" LENGTH="small" NOTNULL="true" SEQUENCE="false" PREVIOUS="partiallycorrectfeedback" NEXT="shownumcorrect"/>
+        <FIELD NAME="shownumcorrect" TYPE="int" LENGTH="2" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="incorrectfeedback"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id" NEXT="questionid"/>
+        <KEY NAME="questionid" TYPE="foreign" FIELDS="questionid" REFTABLE="questions" REFFIELDS="id" PREVIOUS="primary"/>
+      </KEYS>
+    </TABLE>
+  </TABLES>
+</XMLDB>
\ No newline at end of file
diff --git a/question/type/gapselect/edit_form_base.php b/question/type/gapselect/edit_form_base.php
new file mode 100755 (executable)
index 0000000..822eae2
--- /dev/null
@@ -0,0 +1,212 @@
+<?php
+
+
+/**
+ * Elements embedded in question text editing form definition.
+ *
+ * @copyright 2011 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qtype_gapselect_edit_form_base extends question_edit_form {
+
+    /** @var array of HTML tags allowed in choices / drag boxes. */
+    protected $allowedhtmltags = array(
+        'sub',
+        'sup',
+        'b',
+        'i',
+        'em',
+        'strong'
+    );
+
+    /** @var string regex to match HTML open tags. */
+    private $htmltstarttagsandattributes = '/<\s*\w.*?>/';
+
+    /** @var string regex to match HTML close tags or br. */
+    private $htmltclosetags = '~<\s*/\s*\w\s*.*?>|<\s*br\s*>~';
+
+    /** @var string regex to select text like [[cat]] (including the square brackets). */
+    private $squareBracketsRegex = '/\[\[[^]]*?\]\]/';  
+
+    private function get_html_tags($text) {
+        $textarray = array();
+        foreach ($this->allowedhtmltags as $htmltag) {
+            $tagpair = "/<\s*\/?\s*$htmltag\s*.*?>/";
+            preg_match_all($tagpair, $text, $textarray);
+            if ($textarray[0]) {
+                return $textarray[0];
+            }
+        }
+        preg_match_all($this->htmltstarttagsandattributes, $text, $textarray);
+        if ($textarray[0]) {
+            $tag = htmlspecialchars($textarray[0][0]);
+            $allowedtaglist = $this->get_list_of_printable_allowed_tags($this->allowedhtmltags);
+            return $tag . " is not allowed (only $allowedtaglist and corresponsing closing tags are allowed)";
+        }
+        preg_match_all($this->htmltclosetags, $text, $textarray);
+        if ($textarray[0]) {
+            $tag = htmlspecialchars($textarray[0][0]);
+            $allowedtaglist=$this->get_list_of_printable_allowed_tags($this->allowedhtmltags);
+            return $tag . " is not allowed HTML tag! (only $allowedtaglist and corresponsing closing tags are allowed)";
+        }
+        return false;
+    }
+
+    private function get_list_of_printable_allowed_tags($allowedhtmltags) {
+        $allowedtaglist = null;
+        foreach ($allowedhtmltags as $htmltag) {
+            $allowedtaglist .= htmlspecialchars('<'.$htmltag.'>') . ', ';
+        }
+        return $allowedtaglist;
+    }
+
+    /**
+     * definition_inner adds all specific fields to the form.
+     * @param object $mform (the form being built).
+     */
+    function definition_inner(&$mform) {
+        global $CFG;
+
+        //add the answer (choice) fields to the form
+        $this->definition_answer_choice($mform);
+
+        $this->add_combined_feedback_fields(true);
+        $this->add_interactive_settings(true, true);
+    }
+
+    protected function definition_answer_choice(&$mform) {
+        $mform->addElement('header', 'choicehdr',   get_string('choices', 'qtype_gapselect'));
+
+        $mform->addElement('checkbox', 'shuffleanswers',  get_string('shuffle', 'quiz'));
+        $mform->setDefault('shuffleanswers', 0);
+
+        $textboxgroup = array();
+
+        $grouparray = array();
+        $grouparray[] =& $mform->createElement('text', 'answer', get_string('answer', 'qtype_gapselect'), array('size'=>30, 'class'=>'tweakcss'));
+        $grouparray[] =& $mform->createElement('static', '', '',' '.get_string('group', 'qtype_gapselect').' ');
+
+        $grouparray = $this->choice_group($mform, $grouparray);
+        $textboxgroup[] = $mform->createElement('group','choices', 'Choice {no}',$grouparray);
+
+        if (isset($this->question->options)) {
+            $countanswers = count($this->question->options->answers);
+        } else {
+            $countanswers = 0;
+        }
+
+        if ($this->question->formoptions->repeatelements) {
+            $defaultstartnumbers = QUESTION_NUMANS_START*2;
+            $repeatsatstart = max($defaultstartnumbers, QUESTION_NUMANS_START, $countanswers + QUESTION_NUMANS_ADD);
+        } else {
+            $repeatsatstart = $countanswers;
+        }
+
+        $repeatedoptions = $this->repeated_options();
+        $mform->setType('answer', PARAM_RAW);
+        $this->repeat_elements($textboxgroup, $repeatsatstart, $repeatedoptions, 'noanswers', 'addanswers', QUESTION_NUMANS_ADD, get_string('addmorechoiceblanks', 'qtype_gapselect'));
+    }
+
+
+
+    public function set_data($question) {
+        if (isset($question->options)) {
+            $options = $question->options;
+            $default_values = array();
+            if (count($options->answers)) {
+                $key = 0;
+                foreach ($options->answers as $answer) {
+                    $default_values['choices['.$key.'][answer]'] = $answer->answer;
+                    $default_values += $this->default_values_from_feedback_field($answer->feedback, $key);
+                    $key++;
+                }
+            }
+
+            $default_values['shuffleanswers'] =  $question->options->shuffleanswers;
+            $default_values['correctfeedback'] =  $question->options->correctfeedback;
+            $default_values['partiallycorrectfeedback'] =  $question->options->partiallycorrectfeedback;
+            $default_values['incorrectfeedback'] =  $question->options->incorrectfeedback;
+            $default_values['shownumcorrect'] = $question->options->shownumcorrect;
+            $question = (object)((array)$question + $default_values);
+        }
+        parent::set_data($question);
+    }
+
+    public function validation($data, $files) {
+        $errors = parent::validation($data, $files);
+        $questiontext = $data['questiontext'];
+        $choices = $data['choices'];
+
+        //check the whether the slots are valid
+        $errorsinquestiontext = $this->validate_slots($questiontext, $choices);
+        if ($errorsinquestiontext) {
+            $errors['questiontext'] = $errorsinquestiontext;
+        }
+        foreach ($choices as $key => $choice) {
+            $answer = $choice['answer'];
+
+            //check whether the html-tags are allowed tags
+            $validtags = $this->get_html_tags($answer);
+            if (is_array($validtags)) {
+                continue;
+            }
+            if ($validtags) {
+                $errors['choices['.$key.']'] = $validtags;
+            }
+        }
+        return $errors;
+    }
+
+    private function validate_slots($questiontext, $choices) {
+        $error = 'Please check the Question text: ';
+        if (!$questiontext) {
+            return $error . 'The question text is empty!';
+        }
+
+        $matches = array();
+        preg_match_all($this->squareBracketsRegex, $questiontext, $matches);
+        $slots = $matches[0];
+
+        if (!$slots) {
+            return $error . 'The question text is not in the correct format!';
+        }
+
+        $output = array();
+        foreach ($slots as $slot) {
+            // The 2 is for'[[' and 4 is for '[[]]'.
+            $output[] = substr($slot, 2, (strlen($slot)-4));
+        }
+
+        $slots = $output;
+        $found = false;
+        foreach ($slots as $slot) {
+            $found = false;
+            foreach ($choices as $key => $choice) {
+                if ($slot == $key + 1) {
+                    if (!$choice['answer']) {
+                        return " Please check Choices: The choice <b>$slot</b> empty.";
+                    }
+                    $found = true;
+                    break;
+                }
+            }
+            if (!$found) {
+                return $error . "<b>$slot</b> was not found in Choices! (only the choice numbers that exist in choices are allowed to be used a place holders!";
+            }
+        }
+        return false;
+    }
+    function qtype() {
+        return '';
+    }
+
+    protected function default_values_from_feedback_field($feedback, $key) {
+        $default_values = array();
+        return $default_values;
+    }
+
+    protected function repeated_options() {
+        $repeatedoptions = array();
+        return $repeatedoptions;
+    }
+}
\ No newline at end of file
diff --git a/question/type/gapselect/edit_gapselect_form.php b/question/type/gapselect/edit_gapselect_form.php
new file mode 100755 (executable)
index 0000000..62ab9c6
--- /dev/null
@@ -0,0 +1,65 @@
+<?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/>.
+
+
+/**
+ * Defines the editing form for the select missing words question type.
+ *
+ * @package qtype
+ * @subpackage gapselect
+ * @copyright 2011 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once($CFG->dirroot . '/question/type/gapselect/edit_form_base.php');
+
+
+/**
+ * Select from drop down list question editing form definition.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qtype_gapselect_edit_form extends qtype_gapselect_edit_form_base {
+
+    // HTML tags allowed in answers (choices).
+    protected $allowedhtmltags = array();
+
+    function qtype() {
+        return 'gapselect';
+    }
+
+    protected function default_values_from_feedback_field($feedback, $key) {
+        $default_values = array();
+        $default_values['choices['.$key.'][selectgroup]'] = $feedback;
+        return $default_values;
+    }
+
+    protected function repeated_options() {
+        $repeatedoptions = array();
+        $repeatedoptions['selectgroup']['default'] = '1';
+        return $repeatedoptions;
+    }
+    protected function choice_group(&$mform, $grouparray) {
+        $options = array();
+        for ($i = 1; $i <= 8; $i += 1) {
+            $options[$i] = $i;
+        }
+        $grouparray[] =& $mform->createElement('select', 'selectgroup', get_string('group', 'qtype_gapselect'), $options);
+        return $grouparray;
+    }
+}
diff --git a/question/type/gapselect/icon.gif b/question/type/gapselect/icon.gif
new file mode 100755 (executable)
index 0000000..f900852
Binary files /dev/null and b/question/type/gapselect/icon.gif differ
diff --git a/question/type/gapselect/lang/en/qtype_gapselect.php b/question/type/gapselect/lang/en/qtype_gapselect.php
new file mode 100755 (executable)
index 0000000..52db37d
--- /dev/null
@@ -0,0 +1,39 @@
+<?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/>.
+
+
+/**
+ * Language strings for the select missing words question type.
+ *
+ * @package qtype
+ * @subpackage gapselect
+ * @copyright 2011 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+$string['addinggapselect'] = 'Adding a select missing words question';
+$string['addmorechoiceblanks'] = 'Blanks for {no} more choices';
+$string['answer'] = 'Answer';
+$string['choices'] = 'Choices';
+$string['correctansweris'] = 'The correct answer is: $a';
+$string['gapselect'] = 'Select missing words';
+$string['gapselect_help'] = 'Type in some question text like "The [[1]] jumped over the [[2]]", then enter the possible words to go in gaps 1 and 2 underneath.';
+$string['gapselectsummary'] = 'Missing words in some text are filled in using dropdown menus.';
+$string['editinggapselect'] = 'Editing a select missing words question';
+$string['group'] = 'Group';
+$string['pleaseputananswerineachbox'] = 'Please put an answer in each box.';
diff --git a/question/type/gapselect/question.php b/question/type/gapselect/question.php
new file mode 100755 (executable)
index 0000000..9fd15a4
--- /dev/null
@@ -0,0 +1,59 @@
+<?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/>.
+
+
+/**
+ * Select from drop down list question definition class.
+ *
+ * @package qtype
+ * @subpackage gapselect
+ * @copyright 2011 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once($CFG->dirroot . '/question/type/gapselect/questionbase.php');
+
+/**
+ * Represents select missing words question.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qtype_gapselect_question extends qtype_gapselect_question_base {
+//is actually exactly the same.
+}
+
+
+/**
+ * Represents one of the choices (select box option).
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qtype_gapselect_choice {
+    public $text;
+    public $selectgroup;
+
+    public function __construct($text, $selectgroup = 1) {
+        $this->text = $text;
+        $this->selectgroup = $selectgroup;
+    }
+
+    public function choice_group() {
+        return $this->selectgroup;
+    }
+}
diff --git a/question/type/gapselect/questionbase.php b/question/type/gapselect/questionbase.php
new file mode 100755 (executable)
index 0000000..21fbf3b
--- /dev/null
@@ -0,0 +1,286 @@
+<?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/>.
+
+
+/**
+ * Definition class for embedded element in question text question. Parent of drag and drop and select from
+ * drop down list and ?others? *
+ *
+ * @package qtype
+ * @subpackage gapselect
+ * @copyright 2011 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+/**
+ * Represents embedded element in question text question. Parent of drag and drop and select from
+ * drop down list and ?others?
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qtype_gapselect_question_base extends question_graded_automatically_with_countback {
+    /** @var boolean Whether the question stems should be shuffled. */
+    public $shufflechoices;
+
+    public $correctfeedback;
+    public $partiallycorrectfeedback;
+    public $incorrectfeedback;
+
+    /** @var array of arrays. The keys are the choice group numbers. The values
+     * are arrays of qtype_gapselect_choice objects. */
+    public $choices;
+
+    /**
+     * @var array place number => group number of the places in the question
+     * text where choices can be put. Places are numbered from 1.
+     */
+    public $places;
+
+    /**
+     * @var array of strings, one longer than $places, which is achieved by
+     * indexing from 0. The bits of question text that go between the placeholders.
+     */
+    public $textfragments;
+
+    /** @var array index of the right choice for each stem. */
+    public $rightchoices;
+
+    /** @var array shuffled choice indexes. */
+    protected $choiceorder;
+
+    public function init_first_step(question_attempt_step $step) {
+        foreach ($this->choices as $group => $choices) {
+            $varname = '_choiceorder' . $group;
+
+            if ($step->has_qt_var($varname)) {
+                $choiceorder = explode(',', $step->get_qt_var($varname));
+
+            } else {
+                $choiceorder = array_keys($choices);
+                if ($this->shufflechoices) {
+                    shuffle($choiceorder);
+                }
+            }
+
+            foreach ($choiceorder as $key => $value) {
+                $this->choiceorder[$group][$key + 1] = $value;
+            }
+
+            if (!$step->has_qt_var($varname)) {
+                $step->set_qt_var($varname, implode(',', $this->choiceorder[$group]));
+            }
+        }
+    }
+
+    public function get_question_summary() {
+        $question = html_to_text($this->format_questiontext(), 0, false);
+        $groups = array();
+        foreach ($this->choices as $group => $choices) {
+            $cs = array();
+            foreach ($choices as $choice) {
+                $cs[] = html_to_text($this->format_text($choice->text), 0, false);
+            }
+            $groups[] = '[[' . $group . ']] -> {' . implode(' / ', $cs) . '}';
+        }
+        return $question . '; ' . implode('; ', $groups);
+    }
+
+    protected function get_selected_choice($group, $shuffledchoicenumber) {
+        $choiceno = $this->choiceorder[$group][$shuffledchoicenumber];
+        return $this->choices[$group][$choiceno];
+    }
+
+    public function summarise_response(array $response) {
+        $matches = array();
+        $allblank = true;
+        foreach ($this->places as $place => $group) {
+            if (array_key_exists($this->field($place), $response) &&
+                    $response[$this->field($place)]) {
+                $choices[] = '{' . html_to_text($this->format_text($this->get_selected_choice(
+                        $group, $response[$this->field($place)])->text), 0, false) . '}';
+                $allblank = false;
+            } else {
+                $choices[] = '{}';
+            }
+        }
+        if ($allblank) {
+            return null;
+        }
+        return implode(' ', $choices);
+    }
+
+    public function get_random_guess_score() {
+        $accum = 0;
+
+        foreach ($this->places as $placegroup) {
+            $accum += 1 / count($this->choices[$placegroup]);
+        }
+
+        return $accum / count($this->places);
+    }
+
+    public function clear_wrong_from_response(array $response) {
+        foreach ($this->places as $place => $notused) {
+            if (array_key_exists($this->field($place), $response) &&
+                    $response[$this->field($place)] != $this->get_right_choice_for($place)) {
+                $response[$this->field($place)] = '0';
+            }
+        }
+        return $response;
+    }
+
+    public function get_num_parts_right(array $response) {
+        $numright = 0;
+        foreach ($this->places as $place => $notused) {
+            if (!array_key_exists($this->field($place), $response)) {
+                continue;
+            }
+            if ($response[$this->field($place)] == $this->get_right_choice_for($place)) {
+                $numright += 1;
+            }
+        }
+        return array($numright, count($this->places));
+    }
+
+    /**
+     * @param integer $key stem number
+     * @return string the question-type variable name.
+     */
+    public function field($place) {
+        return 'p' . $place;
+    }
+
+    public function get_expected_data() {
+        $vars = array();
+        foreach ($this->places as $place => $notused) {
+            $vars[$this->field($place)] = PARAM_INTEGER;
+        }
+        return $vars;
+    }
+
+    public function get_correct_response() {
+        $response = array();
+        foreach ($this->places as $place => $notused) {
+            $response[$this->field($place)] = $this->get_right_choice_for($place);
+        }
+        return $response;
+    }
+
+    public function get_right_choice_for($place) {
+        $group = $this->places[$place];
+        foreach ($this->choiceorder[$group] as $choicekey => $choiceid) {
+            if ($this->rightchoices[$place] == $choiceid) {
+                return $choicekey;
+            }
+        }
+    }
+
+    public function get_ordered_choices($group) {
+        $choices = array();
+        foreach ($this->choiceorder[$group] as $choicekey => $choiceid) {
+            $choices[$choicekey] = $this->choices[$group][$choiceid];
+        }
+        return $choices;
+    }
+
+    public function is_complete_response(array $response) {
+        $complete = true;
+        foreach ($this->places as $place => $notused) {
+            $complete = $complete && !empty($response[$this->field($place)]);
+        }
+        return $complete;
+    }
+
+    public function is_gradable_response(array $response) {
+        foreach ($this->places as $place => $notused) {
+            if (!empty($response[$this->field($place)])) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public function is_same_response(array $prevresponse, array $newresponse) {
+        foreach ($this->places as $place => $notused) {
+            $fieldname = $this->field($place);
+            if (!question_utils::arrays_same_at_key_integer(
+                    $prevresponse, $newresponse, $fieldname)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public function get_validation_error(array $response) {
+        if ($this->is_complete_response($response)) {
+            return '';
+        }
+        return get_string('pleaseputananswerineachbox', 'qtype_gapselect');
+    }
+
+    public function grade_response(array $response) {
+        list($right, $total) = $this->get_num_parts_right($response);
+        $fraction = $right / $total;
+        return array($fraction, question_state::graded_state_for_fraction($fraction));
+    }
+
+    public function compute_final_grade($responses, $totaltries) {
+        $totalscore = 0;
+        foreach ($this->places as $place => $notused) {
+            $fieldname = $this->field($place);
+
+            $lastwrongindex = -1;
+            $finallyright = false;
+            foreach ($responses as $i => $response) {
+                if (!array_key_exists($fieldname, $response) ||
+                        $response[$fieldname] != $this->get_right_choice_for($place)) {
+                    $lastwrongindex = $i;
+                    $finallyright = false;
+                } else {
+                    $finallyright = true;
+                }
+            }
+
+            if ($finallyright) {
+                $totalscore += max(0, 1 - ($lastwrongindex + 1) * $this->penalty);
+            }
+        }
+
+        return $totalscore / count($this->places);
+    }
+
+    public function classify_response(array $response) {
+        $parts = array();
+        foreach ($this->places as $place => $group) {
+            if (!array_key_exists($this->field($place), $response) ||
+                    !$response[$this->field($place)]) {
+                $parts[$place] = question_classified_response::no_response();
+                continue;
+            }
+
+            $fieldname = $this->field($place);
+            $choiceno = $this->choiceorder[$group][$response[$fieldname]];
+            $choice = $this->choices[$group][$choiceno];
+            $parts[$place] = new question_classified_response(
+                    $choiceno, html_to_text($this->format_text($choice->text), 0, false),
+                    $this->get_right_choice_for($place) == $response[$fieldname]);
+        }
+        return $parts;
+    }
+}
diff --git a/question/type/gapselect/questiontype.php b/question/type/gapselect/questiontype.php
new file mode 100755 (executable)
index 0000000..736eb5c
--- /dev/null
@@ -0,0 +1,207 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+
+/**
+ * Question type class for the select missing words question type.
+ *
+ * @package qtype
+ * @subpackage gapselect
+ * @copyright 2011 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+require_once($CFG->libdir . '/questionlib.php');
+require_once($CFG->dirroot . '/question/engine/lib.php');
+require_once($CFG->dirroot . '/question/format/xml/format.php');
+
+require_once($CFG->dirroot . '/question/type/gapselect/questiontypebase.php');
+
+/**
+ * The select missing words question type class.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qtype_gapselect extends qtype_gapselect_base {
+    protected function choice_options_to_feedback($choice) {
+        return $choice['selectgroup'];
+    }
+
+    protected function make_choice($choicedata) {
+        return new qtype_gapselect_choice($choicedata->answer, $choicedata->feedback);
+    }
+
+    protected function feedback_to_choice_options($feedback) {
+        return array('selectgroup' => $feedback);
+    }
+
+
+    protected function choice_group_key() {
+        return 'selectgroup';
+    }
+
+    function import_from_xml($data, $question, $format, $extra=null) {
+        if (!isset($data['@']['type']) || $data['@']['type'] != 'gapselect') {
+            return false;
+        }
+
+        $question = $format->import_headers($data);
+        $question->qtype = 'gapselect';
+
+        $question->shuffleanswers = $format->trans_single(
+                $format->getpath($data, array('#', 'shuffleanswers', 0, '#'), 1));
+
+        if (!empty($data['#']['selectoption'])) {
+            // Modern XML format.
+            $selectoptions = $data['#']['selectoption'];
+            $question->answer = array();
+            $question->selectgroup = array();
+
+            foreach ($data['#']['selectoption'] as $selectoptionxml) {
+                $question->choices[] = array(
+                    'answer' => $format->getpath($selectoptionxml, array('#', 'text', 0, '#'), '', true),
+                    'selectgroup' => $format->getpath($selectoptionxml, array('#', 'group', 0, '#'), 1),
+                );
+            }
+
+        } else {
+            // Legacy format containing PHP serialisation.
+            foreach ($data['#']['answer'] as $answerxml) {
+                $ans = $format->import_answer($answerxml);
+                $question->choices[] = array(
+                    'answer' => $ans->answer,
+                    'selectgroup' => $ans->feedback,
+                );
+            }
+        }
+
+        $format->import_combined_feedback($question, $data, true);
+        $format->import_hints($question, $data, true);
+
+        return $question;
+    }
+
+    function export_to_xml($question, $format, $extra = null) {
+        $output = '';
+
+        $output .= '    <shuffleanswers>' . $question->options->shuffleanswers . "</shuffleanswers>\n";
+
+        $output .= $format->write_combined_feedback($question->options);
+
+        foreach ($question->options->answers as $answer) {
+            $output .= "    <selectoption>\n";
+            $output .= $format->writetext($answer->answer, 3);
+            $output .= "      <group>{$answer->feedback}</group>\n";
+            $output .= "    </selectoption>\n";
+        }
+
+        return $output;
+    }
+
+    /*
+     * Backup the data in the question
+     *
+     * This is used in question/backuplib.php
+     */
+    public function backup($bf, $preferences, $question, $level = 6) {
+        $status = true;
+        $gapselects = get_records("question_gapselect", "questionid", $question, "id");
+
+        //If there are gapselect
+        if ($gapselects) {
+            //Iterate over each gapselect
+            foreach ($gapselects as $gapselect) {
+                $status = fwrite ($bf,start_tag("SDDLS",$level,true));
+                //Print oumultiresponse contents
+                fwrite ($bf,full_tag("SHUFFLEANSWERS",$level+1,false,$gapselect->shuffleanswers));
+                fwrite ($bf,full_tag("CORRECTFEEDBACK",$level+1,false,$gapselect->correctfeedback));
+                fwrite ($bf,full_tag("PARTIALLYCORRECTFEEDBACK",$level+1,false,$gapselect->partiallycorrectfeedback));
+                fwrite ($bf,full_tag("INCORRECTFEEDBACK",$level+1,false,$gapselect->incorrectfeedback));
+                fwrite ($bf,full_tag("SHOWNUMCORRECT",$level+1,false,$gapselect->shownumcorrect));
+                $status = fwrite ($bf,end_tag("SDDLS",$level,true));
+            }
+
+            //Now print question_answers
+            $status = question_backup_answers($bf,$preferences,$question);
+        }
+        return $status;
+    }
+
+    /**
+     * Restores the data in the question (This is used in question/restorelib.php)
+     *
+     */
+    public function restore($old_question_id,$new_question_id,$info,$restore) {
+        $status = true;
+
+        //Get the gapselect array
+        $gapselects = $info['#']['SDDLS'];
+
+        //Iterate over oumultiresponses
+        for($i = 0; $i < sizeof($gapselects); $i++) {
+            $mul_info = $gapselects[$i];
+
+            //Now, build the question_gapselect record structure
+            $gapselect = new stdClass;
+            $gapselect->questionid = $new_question_id;
+            $gapselect->shuffleanswers = isset($mul_info['#']['SHUFFLEANSWERS']['0']['#'])?backup_todb($mul_info['#']['SHUFFLEANSWERS']['0']['#']):'';
+            if (array_key_exists("CORRECTFEEDBACK", $mul_info['#'])) {
+                $gapselect->correctfeedback = backup_todb($mul_info['#']['CORRECTFEEDBACK']['0']['#']);
+            } else {
+                $gapselect->correctfeedback = '';
+            }
+            if (array_key_exists("PARTIALLYCORRECTFEEDBACK", $mul_info['#'])) {
+                $gapselect->partiallycorrectfeedback = backup_todb($mul_info['#']['PARTIALLYCORRECTFEEDBACK']['0']['#']);
+            } else {
+                $gapselect->partiallycorrectfeedback = '';
+            }
+            if (array_key_exists("INCORRECTFEEDBACK", $mul_info['#'])) {
+                $gapselect->incorrectfeedback = backup_todb($mul_info['#']['INCORRECTFEEDBACK']['0']['#']);
+            } else {
+                $gapselect->incorrectfeedback = '';
+            }
+            if (array_key_exists('SHOWNUMCORRECT', $mul_info['#'])) {
+                $gapselect->shownumcorrect = backup_todb($mul_info['#']['SHOWNUMCORRECT']['0']['#']);
+            } else if (array_key_exists('CORRECTRESPONSESFEEDBACK', $mul_info['#'])) {
+                $gapselect->shownumcorrect = backup_todb($mul_info['#']['CORRECTRESPONSESFEEDBACK']['0']['#']);
+            } else {
+                $gapselect->shownumcorrect = 0;
+            }
+
+            $newid = insert_record ("question_gapselect",$gapselect);
+
+            //Do some output
+            if (($i+1) % 50 == 0) {
+                if (!defined('RESTORE_SILENTLY')) {
+                    echo ".";
+                    if (($i+1) % 1000 == 0) {
+                        echo "<br />";
+                    }
+                }
+                backup_flush(300);
+            }
+
+            if (!$newid) {
+                $status = false;
+            }
+        }
+        return $status;
+    }
+
+}
diff --git a/question/type/gapselect/questiontypebase.php b/question/type/gapselect/questiontypebase.php
new file mode 100755 (executable)
index 0000000..e0cc3d9
--- /dev/null
@@ -0,0 +1,337 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+
+/**
+ * Question type class for the embedded element in question text question types.
+ *
+ * @package qtype
+ * @subpackage gapselect
+ * @copyright 2011 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+require_once($CFG->libdir . '/questionlib.php');
+require_once($CFG->dirroot . '/question/engine/lib.php');
+require_once($CFG->dirroot . '/question/format/xml/format.php');
+
+
+/**
+ * The embedded element in question text question type class.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qtype_gapselect_base extends question_type {
+    /**
+     * Choices are stored in the question_answers table, and any options need to
+     * be put into the feedback field somehow. This method is responsible for
+     * converting all the options to a single string for this purpose. It is used
+     * by {@link save_question_options()}.
+     * @param array $choice the form data relating to this choice.
+     * @return string ready to store in the database.
+     */
+    protected abstract function choice_options_to_feedback($choice);
+
+    public function save_question_options($question) {
+        $result = new stdClass();
+
+        if (!$oldanswers = get_records('question_answers', 'question', $question->id, 'id ASC')) {
+            $oldanswers = array();
+        }
+
+        // Insert all the new answers
+        foreach ($question->choices as $key => $choice) {
+
+            if (trim($choice['answer']) == '') {
+                continue;
+            }
+
+            $feedback = $this->choice_options_to_feedback($choice);
+
+            if ($answer = array_shift($oldanswers)) {  // Existing answer, so reuse it
+                $answer->answer = $choice['answer'];
+                $answer->fraction = 0;
+                $answer->feedback = $feedback;
+                if (!update_record('question_answers', $answer)) {
+                    $result->error = "Could not update question type '".$this->name()."' question answer! (id=$answer->id)";
+                    return $result;
+                }
+            } else {
+                $answer = new stdClass;
+                $answer->answer = $choice['answer'];
+                $answer->question = $question->id;
+                $answer->fraction = 0;
+                $answer->feedback = $feedback;
+                if (!$answer->id = insert_record('question_answers', $answer)) {
+                    $result->error = 'Could not insert question type \''.$this->name().'\'  question answer!';
+                    return $result;
+                }
+            }
+        }
+
+        // Delete old answer records
+        if (!empty($oldanswers)) {
+            foreach($oldanswers as $oa) {
+                delete_records('question_answers', 'id', $oa->id);
+            }
+        }
+
+        $update = true;
+        $options = get_record('question_'.$this->name(), 'questionid', $question->id);
+        if (!$options) {
+            $update = false;
+            $options = new stdClass;
+            $options->questionid = $question->id;
+        }
+
+        $options->shuffleanswers = !empty($question->shuffleanswers);
+        $options->correctfeedback = trim($question->correctfeedback);
+        $options->partiallycorrectfeedback = trim($question->partiallycorrectfeedback);
+        $options->shownumcorrect = !empty($question->shownumcorrect);
+        $options->incorrectfeedback = trim($question->incorrectfeedback);
+
+        if ($update) {
+            if (!update_record('question_'.$this->name(), $options)) {
+                $result->error = "Could not update question type '".$this->name()."' options! (id=$options->id)";
+                return $result;
+            }
+
+        } else {
+            if (!insert_record('question_gapselect', $options)) {
+                $result->error = 'Could not insert question type \''.$this->name().'\' options!';
+                return $result;
+            }
+        }
+
+        $this->save_hints($question, true);
+
+        return true;
+    }
+
+    public function get_question_options($question) {
+        // Get additional information from database and attach it to the question object
+        if (!$question->options = get_record('question_'.$this->name(), 'questionid', $question->id)) {
+            notify('Error: Missing question options for question type \''.$this->name().'\' question '.$question->id.'!');
+            return false;
+        }
+
+        parent::get_question_options($question);
+        return true;
+    }
+
+    public function delete_question($questionid) {
+        delete_records('question_'.$this->name(), 'questionid', $questionid);
+        return parent::delete_question($questionid);
+    }
+
+    /**
+     * Used by {@link initialise_question_instance()} to set up the choice-specific data.
+     * @param object $choicedata as loaded from the question_answers table.
+     * @return object an appropriate object for representing the choice.
+     */
+    protected abstract function make_choice($choicedata);
+
+    protected function initialise_question_instance(question_definition $question, $questiondata) {
+        parent::initialise_question_instance($question, $questiondata);
+
+        $question->shufflechoices = $questiondata->options->shuffleanswers;
+
+        $question->correctfeedback = $questiondata->options->correctfeedback;
+        $question->partiallycorrectfeedback = $questiondata->options->partiallycorrectfeedback;
+        $question->incorrectfeedback = $questiondata->options->incorrectfeedback;
+        $question->shownumcorrect = $questiondata->options->shownumcorrect;
+
+        $question->choices = array();
+        $choiceindexmap= array();
+
+        // Store the choices in arrays by group.
+        $i = 1;
+        foreach ($questiondata->options->answers as $choicedata) {
+            $choice = $this->make_choice($choicedata);
+
+            if (array_key_exists($choice->choice_group(), $question->choices)) {
+                $question->choices[$choice->choice_group()][] = $choice;
+            } else {
+                $question->choices[$choice->choice_group()][1] = $choice;
+            }
+
+            end($question->choices[$choice->choice_group()]);
+            $choiceindexmap[$i] = array($choice->choice_group(),
+                    key($question->choices[$choice->choice_group()]));
+            $i += 1;
+        }
+
+        $question->places = array();
+        $question->textfragments = array();
+        $question->rightchoices = array();
+        // Break up the question text, and store the fragments, places and right answers.
+
+        $bits = preg_split('/\[\[(\d+)]]/', $question->questiontext, null, PREG_SPLIT_DELIM_CAPTURE);
+        $question->textfragments[0] = array_shift($bits);
+        $i = 1;
+
+        while (!empty($bits)) {
+            $choice = array_shift($bits);
+
+            list($group, $choiceindex) = $choiceindexmap[$choice];
+            $question->places[$i] = $group;
+            $question->rightchoices[$i] = $choiceindex;
+
+            $question->textfragments[$i] = array_shift($bits);
+            $i += 1;
+        }
+    }
+
+    protected function make_hint($hint) {
+        return question_hint_with_parts::load_from_record($hint);
+    }
+
+    public function get_random_guess_score($questiondata) {
+        $question = $this->make_question($questiondata);
+        return $question->get_random_guess_score();
+    }
+
+    /**
+     * This function should reverse {@link choice_options_to_feedback()}.
+     * @param string $feedback the data loaded from the database.
+     * @return array the choice options.
+     */
+    protected abstract function feedback_to_choice_options($feedback);
+
+    /**
+     * This method gets the choices (answers)
+     * in a 2 dimentional array.
+     *
+     * @param object $question
+     * @return array of groups
+     */
+    protected function get_array_of_choices($question) {
+        $subquestions = $question->options->answers;
+        $count = 0;
+        foreach ($subquestions as $key=>$subquestion) {
+            $answers[$count]['id'] = $subquestion->id;
+            $answers[$count]['answer'] = $subquestion->answer;
+            $answers[$count]['fraction'] = $subquestion->fraction;
+            $answers[$count] += $this->feedback_to_choice_options($subquestion->feedback);
+            $answers[$count]['choice'] = $count+1;
+            ++$count;
+        }
+        return $answers;
+    }
+
+    /* This method gets the choices (answers) and sort them by groups
+     * in a 2 dimentional array.
+     *
+     * @param object $question
+     * @return array of groups
+     */
+    protected function get_array_of_groups($question, $state) {
+        $answers = $this->get_array_of_choices($question);
+        $arr = array();
+        for($group=1;$group<count($answers);$group++) {
+            $players = $this->get_group_of_players ($question, $state, $answers, $group);
+            if($players) {
+                $arr [$group]= $players;
+            }
+        }
+        return $arr;
+    }
+
+    /**
+     * This method gets the correct answers in a 2 dimentional array.
+     *
+     * @param object $question
+     * @return array of groups
+     */
+    protected function get_correct_answers($question) {
+        $arrayofchoices = $this->get_array_of_choices($question);
+        $arrayofplaceholdeers = $this->get_array_of_placeholders($question);
+
+        $correctplayeers = array();
+        foreach($arrayofplaceholdeers as $ph) {
+            foreach($arrayofchoices as $key=>$choice) {
+                if(($key+1) == $ph) {
+                    $correctplayeers[]= $choice;
+                }
+            }
+        }
+        return $correctplayeers;
+    }
+
+    protected function get_array_of_placeholders($question) {
+        $qtext = $question->questiontext;
+        $error = '<b> ERROR</b>: Please check the form for this question. ';
+        if(!$qtext) {
+            echo $error . 'The question text is empty!';
+            return false;
+        }
+
+        //get the slots
+        $slots = $this->getEmbeddedTextArray($question);
+
+        if(!$slots) {
+            echo $error . 'The question text is not in the correct format!';
+            return false;
+        }
+
+        $output = array();
+        foreach ($slots as $slot) {
+            $output[]=substr($slot, 2, (strlen($slot)-4));//2 is for'[[' and 4 is for '[[]]'
+        }
+        return $output;
+     }
+
+    protected function get_group_of_players ($question, $state, $subquestions, $group) {
+        $goupofanswers=array();
+        foreach($subquestions as $key=>$subquestion) {
+            if($subquestion[$this->choice_group_key()] == $group) {
+                $goupofanswers[] =  $subquestion;
+            }
+        }
+
+        //shuffle answers within this group
+        if ($question->options->shuffleanswers == 1) {
+            srand($state->attempt);
+            shuffle($goupofanswers);
+        }
+        return $goupofanswers;
+    }
+
+    public function get_possible_responses($questiondata) {
+        $question = $this->make_question($questiondata);
+
+        $parts = array();
+        foreach ($question->places as $place => $group) {
+            $choices = array();
+
+            foreach ($question->choices[$group] as $i => $choice) {
+                $choices[$i] = new question_possible_response(
+                        html_to_text($question->format_text($choice->text), 0, false),
+                        $question->rightchoices[$place] == $i);
+            }
+            $choices[null] = question_possible_response::no_response();
+
+            $parts[$place] = $choices;
+        }
+
+        return $parts;
+    }
+
+
+}
diff --git a/question/type/gapselect/renderer.php b/question/type/gapselect/renderer.php
new file mode 100755 (executable)
index 0000000..62f1447
--- /dev/null
@@ -0,0 +1,74 @@
+<?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/>.
+
+
+/**
+ * Select from drop down list question renderer class.
+ *
+ * @package qtype
+ * @subpackage gapselect
+ * @copyright 2011 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once($CFG->dirroot . '/question/type/gapselect/rendererbase.php');
+
+
+/**
+ * Generates the output for select missing words questions.
+ *
+ * @copyright 2011 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qtype_gapselect_renderer extends qtype_elements_embedded_in_question_text_renderer {
+    protected function embedded_element(question_attempt $qa, $place, question_display_options $options) {
+        $question = $qa->get_question();
+        $group = $question->places[$place];
+
+        $fieldname = $question->field($place);
+
+        $value = $qa->get_last_qt_var($question->field($place));
+
+        $attributes = array(
+            'id' => $this->box_id($qa, 'p' . $place, $group),
+            'class' => 'group' . $group
+        );
+
+        if ($options->readonly) {
+            $attributes['disabled'] = 'disabled';
+        }
+
+        $orderedchoices = $question->get_ordered_choices($group);
+        $selectoptions = array();
+        foreach ($orderedchoices as $orderedchoicevalue => $orderedchoice) {
+            $selectoptions[$orderedchoicevalue] = $orderedchoice->text;
+        }
+
+        $feedbackimage = '';
+        if ($options->correctness) {
+            $response = $qa->get_last_qt_data();
+            if (array_key_exists($fieldname, $response)) {
+                $fraction = (int) ($response[$fieldname] == $question->get_right_choice_for($place));
+                $attributes['class'] = $this->feedback_class($fraction);
+                $feedbackimage = $this->feedback_image($fraction);
+            }
+        }
+
+        return html_writer::select($selectoptions, $qa->get_qt_field_name($fieldname), $value, ' ', $attributes) . ' ' . $feedbackimage;
+    }
+
+}
diff --git a/question/type/gapselect/rendererbase.php b/question/type/gapselect/rendererbase.php
new file mode 100755 (executable)
index 0000000..283b5b5
--- /dev/null
@@ -0,0 +1,75 @@
+<?php
+/**
+ * Generates the output for question types where the question includes embedded interactive elements in the
+ * question text.
+ *
+ * @copyright 2010 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class qtype_elements_embedded_in_question_text_renderer extends qtype_with_combined_feedback_renderer {
+    public function formulation_and_controls(question_attempt $qa,
+            question_display_options $options) {
+
+        $question = $qa->get_question();
+
+        $questiontext = '';
+        foreach ($question->textfragments as $i => $fragment) {
+            if ($i > 0) {
+                $questiontext .= $this->embedded_element($qa, $i, $options);
+            }
+            $questiontext .= $fragment;
+        }
+
+
+        $result = '';
+        $result .= html_writer::tag('div', $question->format_text($questiontext),
+                array('class' => $this->qtext_classname(), 'id' => $qa->get_qt_field_name('')));
+
+        $result .= $this->post_qtext_elements($qa, $options);
+
+        if ($qa->get_state() == question_state::$invalid) {
+            $result .= html_writer::nonempty_tag('div',
+                    $question->get_validation_error($qa->get_last_qt_data()),
+                    array('class' => 'validationerror'));
+        }
+
+        return $result;
+    }
+
+    protected function qtext_classname() {
+        return 'qtext';
+    }
+
+    protected abstract function embedded_element(question_attempt $qa, $place, question_display_options $options);
+
+    protected function post_qtext_elements(question_attempt $qa, question_display_options $options) {
+        return '';
+    }
+
+    protected function box_id(question_attempt $qa, $place, $group) {
+        return $qa->get_qt_field_name($place) . '_' . $group;
+    }
+
+    public function specific_feedback(question_attempt $qa) {
+        return $this->combined_feedback($qa);
+    }
+
+    public function correct_response(question_attempt $qa) {
+        $question = $qa->get_question();
+
+        $correctanswer = '';
+        foreach ($question->textfragments as $i => $fragment) {
+            if ($i > 0) {
+                $group = $question->places[$i];
+                $choice = $question->choices[$group][$question->rightchoices[$i]];
+                $correctanswer .= '[' . str_replace('-', '&#x2011;',
+                        $choice->text) . ']';
+            }
+            $correctanswer .= $fragment;
+        }
+
+        if (!empty($correctanswer)) {
+            return get_string('correctansweris', 'qtype_gapselect', $correctanswer);
+        }
+    }
+}
diff --git a/question/type/gapselect/simpletest/helper.php b/question/type/gapselect/simpletest/helper.php
new file mode 100755 (executable)
index 0000000..7a4886f
--- /dev/null
@@ -0,0 +1,106 @@
+<?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/>.
+
+
+/**
+ * Contains the helper class for the select missing words question type tests.
+ *
+ * @package qtype
+ * @subpackage gapselect
+ * @copyright 2011 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+/**
+ * Test helper class for the select missing words question type.
+ *
+ * @copyright 2011 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qtype_gapselect_test_helper {
+    /**
+     * @return qtype_gapselect_question
+     */
+    public static function make_a_gapselect_question() {
+        question_bank::load_question_definition_classes('gapselect');
+        $gapselect = new qtype_gapselect_question();
+
+        test_question_maker::initialise_a_question($gapselect);
+
+        $gapselect->name = 'Selection from drop down list question';
+        $gapselect->questiontext = 'The [[1]] brown [[2]] jumped over the [[3]] dog.';
+        $gapselect->generalfeedback = 'This sentence uses each letter of the alphabet.';
+        $gapselect->qtype = question_bank::get_qtype('gapselect');
+
+        $gapselect->shufflechoices = true;
+
+        test_question_maker::set_standard_combined_feedback_fields($gapselect);
+
+        $gapselect->choices = array(
+            1 => array(
+                1 => new qtype_gapselect_choice('quick', 1),
+                2 => new qtype_gapselect_choice('slow', 1)),
+            2 => array(
+                1 => new qtype_gapselect_choice('fox', 2),
+                2 => new qtype_gapselect_choice('dog', 2)),
+            3 => array(
+                1 => new qtype_gapselect_choice('lazy', 3),
+                2 => new qtype_gapselect_choice('assiduous', 3)),
+        );
+
+        $gapselect->places = array(1 => 1, 2 => 2, 3 => 3);
+        $gapselect->rightchoices = array(1 => 1, 2 => 1, 3 => 1);
+        $gapselect->textfragments = array('The ', ' brown ', ' jumped over the ', ' dog.');
+
+        return $gapselect;
+    }
+
+    /**
+     * @return qtype_gapselect_question
+     */
+    public static function make_a_maths_gapselect_question() {
+        question_bank::load_question_definition_classes('gapselect');
+        $gapselect = new qtype_gapselect_question();
+
+        test_question_maker::initialise_a_question($gapselect);
+
+        $gapselect->name = 'Selection from drop down list question';
+        $gapselect->questiontext = 'Fill in the operators to make this equation work: ' .
+                '7 [[1]] 11 [[2]] 13 [[1]] 17 [[2]] 19 = 3';
+        $gapselect->generalfeedback = 'This sentence uses each letter of the alphabet.';
+        $gapselect->qtype = question_bank::get_qtype('gapselect');
+
+        $gapselect->shufflechoices = true;
+
+        test_question_maker::set_standard_combined_feedback_fields($gapselect);
+
+        $gapselect->choices = array(
+            1 => array(
+                1 => new qtype_gapselect_choice('+', 1, true),
+                2 => new qtype_gapselect_choice('-', 1, true),
+                3 => new qtype_gapselect_choice('*', 1, true),
+                4 => new qtype_gapselect_choice('/', 1, true),
+            ));
+
+        $gapselect->places = array(1 => 1, 2 => 1, 3 => 1, 4 => 1);
+        $gapselect->rightchoices = array(1 => 1, 2 => 2, 3 => 1, 4 => 2);
+        $gapselect->textfragments = array('7 ', ' 11 ', ' 13 ', ' 17 ', ' 19 = 3');
+
+        return $gapselect;
+    }
+}
diff --git a/question/type/gapselect/simpletest/testquestion.php b/question/type/gapselect/simpletest/testquestion.php
new file mode 100755 (executable)
index 0000000..5d916c7
--- /dev/null
@@ -0,0 +1,247 @@
+<?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 select missing words question definition class.
+ *
+ * @package qtype
+ * @subpackage gapselect
+ * @copyright 2011 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once($CFG->dirroot . '/question/engine/simpletest/helpers.php');
+require_once($CFG->dirroot . '/question/type/gapselect/simpletest/helper.php');
+
+
+/**
+ * Unit tests for the matching question definition class.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qtype_gapselect_question_test extends UnitTestCase {
+
+    public function test_get_question_summary() {
+        $gapselect = qtype_gapselect_test_helper::make_a_gapselect_question();
+        $this->assertEqual('The [[1]] brown [[2]] jumped over the [[3]] dog.; [[1]] -> {quick / slow}; [[2]] -> {fox / dog}; [[3]] -> {lazy / assiduous}',
+                $gapselect->get_question_summary());
+    }
+
+    public function test_get_question_summary_maths() {
+        $gapselect = qtype_gapselect_test_helper::make_a_maths_gapselect_question();
+        $this->assertEqual('Fill in the operators to make this equation work: ' .
+                '7 [[1]] 11 [[2]] 13 [[1]] 17 [[2]] 19 = 3; [[1]] -> {+ / - / * / /}',
+                $gapselect->get_question_summary());
+    }
+
+    public function test_summarise_response() {
+        $gapselect = qtype_gapselect_test_helper::make_a_gapselect_question();
+        $gapselect->shufflechoices = false;
+        $gapselect->init_first_step(new question_attempt_step());
+
+        $this->assertEqual('{quick} {fox} {lazy}',
+                $gapselect->summarise_response(array('p1' => '1', 'p2' => '1', 'p3' => '1')));
+    }
+
+    public function test_summarise_response_maths() {
+        $gapselect = qtype_gapselect_test_helper::make_a_maths_gapselect_question();
+        $gapselect->shufflechoices = false;
+        $gapselect->init_first_step(new question_attempt_step());
+
+        $this->assertEqual('{+} {-} {+} {-}',
+                $gapselect->summarise_response(array('p1' => '1', 'p2' => '2', 'p3' => '1', 'p4' => '2')));
+    }
+
+    public function test_get_random_guess_score() {
+        $gapselect = qtype_gapselect_test_helper::make_a_gapselect_question();
+        $this->assertEqual(0.5, $gapselect->get_random_guess_score());
+    }
+
+    public function test_get_random_guess_score_maths() {
+        $gapselect = qtype_gapselect_test_helper::make_a_maths_gapselect_question();
+        $this->assertEqual(0.25, $gapselect->get_random_guess_score());
+    }
+
+    public function test_get_right_choice_for() {
+        $gapselect = qtype_gapselect_test_helper::make_a_gapselect_question();
+        $gapselect->shufflechoices = false;
+        $gapselect->init_first_step(new question_attempt_step());
+
+        $this->assertEqual(1, $gapselect->get_right_choice_for(1));
+        $this->assertEqual(1, $gapselect->get_right_choice_for(2));
+    }
+
+    public function test_get_right_choice_for_maths() {
+        $gapselect = qtype_gapselect_test_helper::make_a_maths_gapselect_question();
+        $gapselect->shufflechoices = false;
+        $gapselect->init_first_step(new question_attempt_step());
+
+        $this->assertEqual(1, $gapselect->get_right_choice_for(1));
+        $this->assertEqual(2, $gapselect->get_right_choice_for(2));
+    }
+
+    public function test_clear_wrong_from_response() {
+        $gapselect = qtype_gapselect_test_helper::make_a_maths_gapselect_question();
+        $gapselect->shufflechoices = false;
+        $gapselect->init_first_step(new question_attempt_step());
+
+        $initialresponse = array('p1' => '1', 'p2' => '1', 'p3' => '1', 'p4' => '1');
+        $this->assertEqual(array('p1' => '1', 'p2' => '0', 'p3' => '1', 'p4' => '0'),
+                $gapselect->clear_wrong_from_response($initialresponse));
+    }
+
+    public function test_get_num_parts_right() {
+        $gapselect = qtype_gapselect_test_helper::make_a_gapselect_question();
+        $gapselect->shufflechoices = false;
+        $gapselect->init_first_step(new question_attempt_step());
+
+        $this->assertEqual(array(2, 3),
+                $gapselect->get_num_parts_right(array('p1' => '1', 'p2' => '1', 'p3' => '2')));
+        $this->assertEqual(array(3, 3),
+                $gapselect->get_num_parts_right(array('p1' => '1', 'p2' => '1', 'p3' => '1')));
+    }
+
+    public function test_get_num_parts_right_maths() {
+        $gapselect = qtype_gapselect_test_helper::make_a_maths_gapselect_question();
+        $gapselect->shufflechoices = false;
+        $gapselect->init_first_step(new question_attempt_step());
+
+        $this->assertEqual(array(2, 4),
+                $gapselect->get_num_parts_right(array('p1' => '1', 'p2' => '1', 'p3' => '1', 'p4' => '1')));
+    }
+
+    public function test_get_expected_data() {
+        $gapselect = qtype_gapselect_test_helper::make_a_gapselect_question();
+        $gapselect->init_first_step(new question_attempt_step());
+
+        $this->assertEqual(array('p1' => PARAM_INT, 'p2' => PARAM_INT, 'p3' => PARAM_INT),
+                $gapselect->get_expected_data());
+    }
+
+    public function test_get_correct_response() {
+        $gapselect = qtype_gapselect_test_helper::make_a_gapselect_question();
+        $gapselect->shufflechoices = false;
+        $gapselect->init_first_step(new question_attempt_step());
+
+        $this->assertEqual(array('p1' => '1', 'p2' => '1', 'p3' => '1'),
+                $gapselect->get_correct_response());
+    }
+
+    public function test_get_correct_response_maths() {
+        $gapselect = qtype_gapselect_test_helper::make_a_maths_gapselect_question();
+        $gapselect->shufflechoices = false;
+        $gapselect->init_first_step(new question_attempt_step());
+
+        $this->assertEqual(array('p1' => '1', 'p2' => '2', 'p3' => '1', 'p4' => '2'),
+                $gapselect->get_correct_response());
+    }
+
+    public function test_is_same_response() {
+        $gapselect = qtype_gapselect_test_helper::make_a_gapselect_question();
+        $gapselect->init_first_step(new question_attempt_step());
+
+        $this->assertTrue($gapselect->is_same_response(
+                array(),
+                array('p1' => '0', 'p2' => '0', 'p3' => '0')));
+
+        $this->assertFalse($gapselect->is_same_response(
+                array(),
+                array('p1' => '1', 'p2' => '0', 'p3' => '0')));
+
+        $this->assertFalse($gapselect->is_same_response(
+                array('p1' => '0', 'p2' => '0', 'p3' => '0'),
+                array('p1' => '1', 'p2' => '0', 'p3' => '0')));
+
+        $this->assertTrue($gapselect->is_same_response(
+                array('p1' => '1', 'p2' => '2', 'p3' => '3'),
+                array('p1' => '1', 'p2' => '2', 'p3' => '3')));
+
+        $this->assertFalse($gapselect->is_same_response(
+                array('p1' => '1', 'p2' => '2', 'p3' => '3'),
+                array('p1' => '1', 'p2' => '2', 'p3' => '2')));
+    }
+    public function test_is_complete_response() {
+        $gapselect = qtype_gapselect_test_helper::make_a_gapselect_question();
+        $gapselect->init_first_step(new question_attempt_step());
+
+        $this->assertFalse($gapselect->is_complete_response(array()));
+        $this->assertFalse($gapselect->is_complete_response(
+                array('p1' => '1', 'p2' => '1', 'p3' => '0')));
+        $this->assertFalse($gapselect->is_complete_response(array('p1' => '1')));
+        $this->assertTrue($gapselect->is_complete_response(
+                array('p1' => '1', 'p2' => '1', 'p3' => '1')));
+    }
+
+    public function test_is_gradable_response() {
+        $gapselect = qtype_gapselect_test_helper::make_a_gapselect_question();
+        $gapselect->init_first_step(new question_attempt_step());
+
+        $this->assertFalse($gapselect->is_gradable_response(array()));
+        $this->assertFalse($gapselect->is_gradable_response(
+                array('p1' => '0', 'p2' => '0', 'p3' => '0')));
+        $this->assertTrue($gapselect->is_gradable_response(
+                array('p1' => '1', 'p2' => '1', 'p3' => '0')));
+        $this->assertTrue($gapselect->is_gradable_response(array('p1' => '1')));
+        $this->assertTrue($gapselect->is_gradable_response(
+                array('p1' => '1', 'p2' => '1', 'p3' => '1')));
+    }
+
+    public function test_grading() {
+        $gapselect = qtype_gapselect_test_helper::make_a_gapselect_question();
+        $gapselect->shufflechoices = false;
+        $gapselect->init_first_step(new question_attempt_step());
+
+        $this->assertEqual(array(1, question_state::$gradedright),
+                $gapselect->grade_response(array('p1' => '1', 'p2' => '1', 'p3' => '1')));
+        $this->assertEqual(array(1/3, question_state::$gradedpartial),
+                $gapselect->grade_response(array('p1' => '1')));
+        $this->assertEqual(array(0, question_state::$gradedwrong),
+                $gapselect->grade_response(array('p1' => '2', 'p2' => '2', 'p3' => '2')));
+    }
+
+    public function test_grading_maths() {
+        $gapselect = qtype_gapselect_test_helper::make_a_maths_gapselect_question();
+        $gapselect->shufflechoices = false;
+        $gapselect->init_first_step(new question_attempt_step());
+
+        $this->assertEqual(array(1, question_state::$gradedright),
+                $gapselect->grade_response(array('p1' => '1', 'p2' => '2', 'p3' => '1', 'p4' => '2')));
+        $this->assertEqual(array(0.5, question_state::$gradedpartial),
+                $gapselect->grade_response(array('p1' => '1', 'p2' => '1', 'p3' => '1', 'p4' => '1')));
+        $this->assertEqual(array(0, question_state::$gradedwrong),
+                $gapselect->grade_response(array('p1' => '0', 'p2' => '1', 'p3' => '2', 'p4' => '1')));
+    }
+
+    public function test_classify_response() {
+        $gapselect = qtype_gapselect_test_helper::make_a_gapselect_question();
+        $gapselect->shufflechoices = false;
+        $gapselect->init_first_step(new question_attempt_step());
+
+        $this->assertEqual(array(
+                    1 => new question_classified_response(1, 'quick', 1),
+                    2 => new question_classified_response(2, 'dog', 0),
+                    3 => new question_classified_response(1, 'lazy', 1),
+                ), $gapselect->classify_response(array('p1' => '1', 'p2' => '2', 'p3' => '1')));
+        $this->assertEqual(array(
+                    1 => question_classified_response::no_response(),
+                    2 => new question_classified_response(1, 'fox', 1),
+                    3 => new question_classified_response(2, 'assiduous', 0),
+                ), $gapselect->classify_response(array('p1' => '0', 'p2' => '1', 'p3' => '2')));
+    }
+}
diff --git a/question/type/gapselect/simpletest/testquestiontype.php b/question/type/gapselect/simpletest/testquestiontype.php
new file mode 100755 (executable)
index 0000000..1cfcfcb
--- /dev/null
@@ -0,0 +1,311 @@
+<?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 select missing words question question definition class.
+ *
+ * @package qtype_gapselect
+ * @copyright 2010 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+require_once($CFG->dirroot . '/question/engine/simpletest/helpers.php');
+require_once($CFG->dirroot . '/question/type/gapselect/simpletest/helper.php');
+
+
+/**
+ * Unit tests for the select missing words question definition class.
+ *
+ * @package qtype
+ * @subpackage gapselect
+ * @copyright 2011 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qtype_gapselect_test extends UnitTestCase {
+    /** @var qtype_gapselect instance of the question type class to test. */
+    protected $qtype;
+
+    public function setUp() {
+        $this->qtype = question_bank::get_qtype('gapselect');;
+    }
+
+    public function tearDown() {
+        $this->qtype = null;
+    }
+
+    public function assert_same_xml($expectedxml, $xml) {
+        $this->assertEqual(str_replace("\r\n", "\n", $expectedxml),
+                str_replace("\r\n", "\n", $xml));
+    }
+
+    /**
+     * @return object the data to construct a question like
+     * {@link qtype_gapselect_test_helper::make_a_gapselect_question()}.
+     */
+    protected function get_test_question_data() {
+        global $USER;
+
+        $gapselect = new stdClass;
+        $gapselect->id = 0;
+        $gapselect->category = 0;
+        $gapselect->parent = 0;
+        $gapselect->questiontextformat = FORMAT_HTML;
+        $gapselect->defaultmark = 1;
+        $gapselect->penalty = 0.3333333;
+        $gapselect->length = 1;
+        $gapselect->stamp = make_unique_id_code();
+        $gapselect->version = make_unique_id_code();
+        $gapselect->hidden = 0;
+        $gapselect->timecreated = time();
+        $gapselect->timemodified = time();
+        $gapselect->createdby = $USER->id;
+        $gapselect->modifiedby = $USER->id;
+
+        $gapselect->name = 'Selection from drop down list question';
+        $gapselect->questiontext = 'The [[1]] brown [[2]] jumped over the [[3]] dog.';
+        $gapselect->generalfeedback = 'This sentence uses each letter of the alphabet.';
+        $gapselect->qtype = 'gapselect';
+
+        $gapselect->options->shuffleanswers = true;
+
+        test_question_maker::set_standard_combined_feedback_fields($gapselect->options);
+
+        $gapselect->options->answers = array(
+            (object) array('answer' => 'quick', 'feedback' => '1'),
+            (object) array('answer' => 'fox', 'feedback' => '2'),
+            (object) array('answer' => 'lazy', 'feedback' => '3'),
+            (object) array('answer' => 'assiduous', 'feedback' => '3'),
+            (object) array('answer' => 'dog', 'feedback' => '2'),
+            (object) array('answer' => 'slow', 'feedback' => '1'),
+        );
+
+        return $gapselect;
+    }
+
+    public function test_name() {
+        $this->assertEqual($this->qtype->name(), 'gapselect');
+    }
+
+    public function test_can_analyse_responses() {
+        $this->assertTrue($this->qtype->can_analyse_responses());
+    }
+
+    public function test_initialise_question_instance() {
+        $qdata = $this->get_test_question_data();
+
+        $expected = qtype_gapselect_test_helper::make_a_gapselect_question();
+        $expected->stamp = $qdata->stamp;
+        $expected->version = $qdata->version;
+
+        $q = $this->qtype->make_question($qdata);
+
+        $this->assertEqual($expected, $q);
+    }
+
+    public function test_get_random_guess_score() {
+        $q = $this->get_test_question_data();
+        $this->assertWithinMargin(0.5, $this->qtype->get_random_guess_score($q), 0.0000001);
+    }
+
+    public function test_get_possible_responses() {
+        $q = $this->get_test_question_data();
+
+        $this->assertEqual(array(
+            1 => array(
+                1 => new question_possible_response('quick', 1),
+                2 => new question_possible_response('slow', 0),
+                null => question_possible_response::no_response()),
+            2 => array(
+                1 => new question_possible_response('fox', 1),
+                2 => new question_possible_response('dog', 0),
+                null => question_possible_response::no_response()),
+            3 => array(
+                1 => new question_possible_response('lazy', 1),
+                2 => new question_possible_response('assiduous', 0),
+                null => question_possible_response::no_response()),
+        ), $this->qtype->get_possible_responses($q));
+    }
+
+    public function test_xml_import() {
+        $xml = '  <question type="gapselect">
+    <name>
+      <text>A select missing words question</text>
+    </name>
+    <questiontext format="moodle_auto_format">
+      <text>Put these in order: [[1]], [[2]], [[3]].</text>
+    </questiontext>
+    <generalfeedback>
+      <text>The answer is Alpha, Beta, Gamma.</text>
+    </generalfeedback>
+    <defaultgrade>3</defaultgrade>
+    <penalty>0.3333333</penalty>
+    <hidden>0</hidden>
+    <shuffleanswers>1</shuffleanswers>
+    <correctfeedback>
+      <text><![CDATA[<p>Your answer is correct.</p>]]></text>
+    </correctfeedback>
+    <partiallycorrectfeedback>
+      <text><![CDATA[<p>Your answer is partially correct.</p>]]></text>
+    </partiallycorrectfeedback>
+    <incorrectfeedback>
+      <text><![CDATA[<p>Your answer is incorrect.</p>]]></text>
+    </incorrectfeedback>
+    <shownumcorrect/>
+    <selectoption>
+      <text>Alpha</text>
+      <group>1</group>
+    </selectoption>
+    <selectoption>
+      <text>Beta</text>
+      <group>1</group>
+    </selectoption>
+    <selectoption>
+      <text>Gamma</text>
+      <group>1</group>
+    </selectoption>
+    <hint>
+      <text>Try again.</text>
+      <shownumcorrect />
+    </hint>
+    <hint>
+      <text>These are the first three letters of the Greek alphabet.</text>
+      <shownumcorrect />
+      <clearwrong />
+    </hint>
+  </question>';
+        $xmldata = xmlize($xml);
+
+        $importer = new qformat_xml();
+        $q = $importer->try_importing_using_qtypes(
+                $xmldata['question'], null, null, 'gapselect');
+
+        $expectedq = new stdClass;
+        $expectedq->qtype = 'gapselect';
+        $expectedq->name = 'A select missing words question';
+        $expectedq->questiontext = 'Put these in order: [[1]], [[2]], [[3]].';
+        $expectedq->questiontextformat = FORMAT_MOODLE;
+        $expectedq->generalfeedback = 'The answer is Alpha, Beta, Gamma.';
+        $expectedq->defaultmark = 3;
+        $expectedq->length = 1;
+        $expectedq->penalty = 0.3333333;
+
+        $expectedq->shuffleanswers = 1;
+        $expectedq->correctfeedback = '<p>Your answer is correct.</p>';
+        $expectedq->partiallycorrectfeedback = '<p>Your answer is partially correct.</p>';
+        $expectedq->shownumcorrect = true;
+        $expectedq->incorrectfeedback = '<p>Your answer is incorrect.</p>';
+
+        $expectedq->choices = array(
+            array('answer' => 'Alpha', 'selectgroup' => 1),
+            array('answer' => 'Beta', 'selectgroup' => 1),
+            array('answer' => 'Gamma', 'selectgroup' => 1),
+        );
+
+        $expectedq->hint = array('Try again.', 'These are the first three letters of the Greek alphabet.');
+        $expectedq->hintshownumcorrect = array(true, true);
+        $expectedq->hintclearwrong = array(false, true);
+
+        $this->assert(new CheckSpecifiedFieldsExpectation($expectedq), $q);
+    }
+
+    public function test_xml_export() {
+        $qdata = new stdClass;
+        $qdata->id = 123;
+        $qdata->qtype = 'gapselect';
+        $qdata->name = 'A select missing words question';
+        $qdata->questiontext = 'Put these in order: [[1]], [[2]], [[3]].';
+        $qdata->questiontextformat = FORMAT_MOODLE;
+        $qdata->generalfeedback = 'The answer is Alpha, Beta, Gamma.';
+        $qdata->defaultmark = 3;
+        $qdata->length = 1;
+        $qdata->penalty = 0.3333333;
+        $qdata->hidden = 0;
+
+        $qdata->options->shuffleanswers = 1;
+        $qdata->options->correctfeedback = '<p>Your answer is correct.</p>';
+        $qdata->options->partiallycorrectfeedback = '<p>Your answer is partially correct.</p>';
+        $qdata->options->shownumcorrect = true;
+        $qdata->options->incorrectfeedback = '<p>Your answer is incorrect.</p>';
+
+        $qdata->options->answers = array(
+            new question_answer('Alpha', 0, '1'),
+            new question_answer('Beta', 0, '1'),
+            new question_answer('Gamma', 0, '1'),
+        );
+
+        $qdata->hints = array(
+            new question_hint_with_parts('Try again.', true, false),
+            new question_hint_with_parts('These are the first three letters of the Greek alphabet.', true, true),
+        );
+
+        $exporter = new qformat_xml();
+        $xml = $exporter->writequestion($qdata);
+
+        $expectedxml = '<!-- question: 123  -->
+  <question type="gapselect">
+    <name>
+      <text>A select missing words question</text>
+    </name>
+    <questiontext format="moodle_auto_format">
+      <text>Put these in order: [[1]], [[2]], [[3]].</text>
+    </questiontext>
+    <generalfeedback>
+      <text>The answer is Alpha, Beta, Gamma.</text>
+    </generalfeedback>
+    <defaultgrade>3</defaultgrade>
+    <penalty>0.3333333</penalty>
+    <hidden>0</hidden>
+    <shuffleanswers>1</shuffleanswers>
+    <correctfeedback>
+      <text><![CDATA[<p>Your answer is correct.</p>]]></text>
+    </correctfeedback>
+    <partiallycorrectfeedback>
+      <text><![CDATA[<p>Your answer is partially correct.</p>]]></text>
+    </partiallycorrectfeedback>
+    <incorrectfeedback>
+      <text><![CDATA[<p>Your answer is incorrect.</p>]]></text>
+    </incorrectfeedback>
+    <shownumcorrect/>
+    <selectoption>
+      <text>Alpha</text>
+      <group>1</group>
+    </selectoption>
+    <selectoption>
+      <text>Beta</text>
+      <group>1</group>
+    </selectoption>
+    <selectoption>
+      <text>Gamma</text>
+      <group>1</group>
+    </selectoption>
+    <hint>
+      <text>Try again.</text>
+      <shownumcorrect/>
+    </hint>
+    <hint>
+      <text>These are the first three letters of the Greek alphabet.</text>
+      <shownumcorrect/>
+      <clearwrong/>
+    </hint>
+  </question>
+';
+
+        $this->assert_same_xml($expectedxml, $xml);
+    }
+}
diff --git a/question/type/gapselect/simpletest/testwalkthrough.php b/question/type/gapselect/simpletest/testwalkthrough.php
new file mode 100755 (executable)
index 0000000..26b0cc6
--- /dev/null
@@ -0,0 +1,37 @@
+<?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
+ * @subpackage gapselect
+ * @copyright 2011 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+require_once($CFG->dirroot . '/question/engine/simpletest/helpers.php');
+require_once($CFG->dirroot . '/question/type/gapselect/simpletest/helper.php');
+
+
+class qtype_gapselect_walkthrough_test extends qbehaviour_walkthrough_test_base {
+
+
+}
diff --git a/question/type/gapselect/styles.css b/question/type/gapselect/styles.css
new file mode 100755 (executable)
index 0000000..09f1c14
--- /dev/null
@@ -0,0 +1,41 @@
+.que.gapselect .qtext {
+    line-height:2em;
+    margin-top: 1px;
+    margin-bottom: 0.5em;
+    display: block;
+}
+
+.que.gapselect .answercontainer {
+    line-height: 2em;
+    margin-bottom:1em;
+    display: block;
+}
+
+.que.gapselect .answertext {
+    padding-bottom: 0.5em;
+}
+
+.que.gapselect .group1 {
+    background-color: #FFFFFF;
+}
+.que.gapselect .group2 {
+    background-color: #DCDCDC;
+}
+.que.gapselect .group3 {
+    background-color: #B0C4DE;
+}
+.que.gapselect .group4 {
+    background-color: #D8BFD8;
+}
+.que.gapselect .group5 {
+    background-color: #87CEFA;
+}
+.que.gapselect .group6 {
+    background-color: #DAA520;
+}
+.que.gapselect .group7 {
+    background-color: #FFD700;
+}
+.que.gapselect .group8 {
+    background-color: #F0E68C;
+}
diff --git a/question/type/gapselect/version.php b/question/type/gapselect/version.php
new file mode 100755 (executable)
index 0000000..43fc3b2
--- /dev/null
@@ -0,0 +1,30 @@
+<?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/>.
+
+
+/**
+ * Version information for the select missing words question type.
+ *
+ * @package qtype
+ * @subpackage gapselect
+ * @copyright 2011 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+$plugin->version  = 2010042800;
+$plugin->requires = 2007101000;