Note:

If you want to create a new page for developers, you should create it on the Moodle Developer Resource site.

Question Engine 2:Developing the Multianswer (Cloze) Question Type: Difference between revisions

From MoodleDocs
No edit summary
Line 111: Line 111:
The QA Question attempt expects to interact with a multianswer question object but has no initial provision to handle subquestions.
The QA Question attempt expects to interact with a multianswer question object but has no initial provision to handle subquestions.
So all step parameters like 'order' in the case of multichoice questions and 'answer' or 'choice' related to the student answer to a given subquestion has to be identified specifically.
So all step parameters like 'order' in the case of multichoice questions and 'answer' or 'choice' related to the student answer to a given subquestion has to be identified specifically.
We choose to add a prefix _subi- where i is the subquestion index in the question text  
We choose to add a prefix _subi- where i is the subquestion index in the question text.
If a multianswer has a shortanswer subquestion (i=1) followed by a single multichoice in-line (i=2)  
Here an example of the HTML code for a multianswer question which has a shortanswer subquestion (i=1) followed by a single multichoice in-line (i=2).
  This question consists of some text with after that you will have to deal with this short answer  
  This question consists of some text with after that you will have to deal with this short answer  
  <label><span  >
  <label><span  >
  <input type="text" name="q1037,1__sub1-answer" id="q1037,1__sub1-answer" size="37" />
  <input type="text" name="q1037,1_'''_sub1-'''answer" id="q1037,1_'''_sub1-'''answer" size="37" />
  </span></label> and a multichoice embedded right here  
  </span></label> and a multichoice embedded right here  
   <span  ><span class="control">
   <span  ><span class="control">
   <select id="menuq1037,1__sub2-answer" class="select menuq1037,1__sub2-answer" name="q1037,1__sub2-answer" >
   <select id="menuq1037,1_'''_sub2-'''answer" class="select menuq1037,1_'''_sub2-'''answer" name="q1037,1_'''_sub2-'''answer" >
   <option value=""> </option>
   <option value=""> </option>
   <option value="1">Correct answer</option>
   <option value="1">Correct answer</option>
Line 139: Line 141:
; _sub2- : to the name or id of the select element (i.e. name="q1037,1__sub2-answer" )  
; _sub2- : to the name or id of the select element (i.e. name="q1037,1__sub2-answer" )  


So the '''multianswer/question.php''' and'''multianswer/renderer.php''' and the will need to handle correctly
However the '''multianswer/question.php''' and'''multianswer/renderer.php''' will need to handle correctly
the step data
the step data
     [_sub1-answer] => Correct Answer
     [_sub1-answer] => Correct Answer
Line 150: Line 152:
and  '''multichoice/question.php''' expect the data as
and  '''multichoice/question.php''' expect the data as


     [_sub2_order] => 294,293,295,292 giving the answers id order in the select element
     [order] => 294,293,295,292 giving the answers id order in the select element


expect the step data as
and expect the step data as
     [answer] => 1
     [answer] => 1


or in the multianswer multiple choice if the Correct answer was displayed in the first checkbox element
(or as  [choice1] => 1 if multiple answer multiplechoice).
    [choice1] => 1


Various strategies were used to correctly handle the subquestions data and step data
Various strategies were used to correctly handle the subquestions data and step data.


== Multianswer Question functions ==
== Multianswer Question functions ==

Revision as of 23:26, 12 April 2010

Template:Multianswer (Cloze) question

Introduction

This page will describe details about implementing the multianswer ( Cloze) question type in the new question engine code.

The detailed day-to-day interactions between Tim Hunt and me (Pierre Pichet) can be found in MDL-20636 .

Note: This page is a work-in-progress. Feedback and suggested improvements are welcome. Please join the discussion on moodle.org or use the page comments.


The multianswer question type is more an enclosure for subquestions than a real question type as shortanwer or multichoice. The subquestions are enclosed in the multianswer question text using a specific coding i.e

{1:MULTICHOICE:Wrong answer#Feedback for this wrong answer
~=Correct answer#Feedback for correct answer
~%50%Answer that gives half the credit#Feedback for half credit answer}

In the pre engine multianswer the following subquestuons are allowed

  • short answers (SHORTANSWER or SA or MW), case is unimportant,
  • short answers (SHORTANSWER_C or SAC or MWC), case must match,
  • numerical answers (NUMERICAL or NM),
  • multiple choice (MULTICHOICE or MC), represented as a dropdown menu in-line in the text
  • multiple choice (MULTICHOICE_V or MCV), represented a vertical column of radio buttons, or
  • multiple choice (MULTICHOICE_H or MCH), represented as a horizontal row of radio-buttons.

As the new engine allow to store easily more subquestions parameters we decide to implement MULTICHOICE Multi answer and shuffling for multichoice subquestions. So we add the following types

  • multiple choice multi answer (M_MULTICHOICE_V or MMCV), represented a vertical column of radio buttons, or
  • multiple choice multi answer (M_MULTICHOICE_H or MMCH), represented as a horizontal row of radio-buttons

and the shuffled anwers types

  • multiple choice (MULTICHOICE_S or MCS), represented as a dropdown menu in-line in the text
  • multiple choice (MULTICHOICE_V_S or MCVS), represented a vertical column of radio buttons, or
  • multiple choice (MULTICHOICE_H_S or MCHS), represented as a horizontal row of radio-buttons.
  • multiple choice multi answer (M_MULTICHOICE_V_S or MCVS), represented a vertical column of radio buttons, or
  • multiple choice multi answer (M_MULTICHOICE_H_S or MMCHS), represented as a horizontal row of radio-buttons.

This give thirteen variants.

The new engine is built with only one question that is handle by the correponding renderer and the various interacton model.

The attempt object has no initial provision for subquestions although match question type could be considered as having subquestions.

We need first a way to idenitfiy the or other answers coming from the different subquestions which are identified in the cloze questions by there appearance order in the question text ( i.e 1,2, 3, etc.). The identifier chosen was _sub_subquestion_index giving _sub3_answer parameter for the answer step value of the 3rd subquestion.

question classes

The question engine initialise_question_instance in question/type/multianswer/questiontype.php is

   protected function initialise_question_instance(question_definition $question, $questiondata) {
....
    parent::initialise_question_instance($question, $questiondata);
    foreach($questiondata->options->questions as $key =>$wrapped) {
    $class = "";
    switch ($wrapped->qtype){
       case "shortanswer":$class = "qtype_multianswer_shortanswer_question";
                          ....
                          break;
       case "numerical": $class = "qtype_multianswer_numerical_question";
                          ....
                          break;                                           
       case "multichoice":
            if ($wrapped->options->single) {                                    
               switch ($wrapped->options->layout){
                    case VERTICAL   : 
                    case HORIZONTAL : $class = "qtype_multianswer_multichoice_single_question";
                                      break;
                    default : $class = "qtype_multianswer_multichoice_single_inline_question";
               }
                       
             } else {
                   $class = "qtype_multianswer_multichoice_multi_question";
             }

$question->subquestions[$key]->shuffleanswers handle the shuffling options. 

The 5 classes qtype_multianswer_..._question in addition to the qtype_multianswer_question are defined in question/type/multianswer/question.php.

renderer classes

With careful design we could limit the renderer to 4 types. The main class qtype_multianswer_renderer extends qtype_renderer { whose public function formulation_and_controls() follows the same coding as the function print_question_formulation_and_controls of the question/type/multianswer/questiontype.php

       $qtextremaining = $question->format_questiontext();

       // The regex will recognize text snippets of type {#X}
       // where the X can be any text not containg } or white-space characters.

       while (ereg('\{#([^[:space:]}]*)}', $qtextremaining, $regs)) {
           $qtextsplits = explode($regs[0], $qtextremaining, 2);
           $result .= $qtextsplits[0];
           $qtextremaining = $qtextsplits[1];
           $positionkey = $regs[1];
      // transfer to the specific subquestion renderer                 
           if (isset($question->subquestions[$positionkey]) &&
               $question->subquestions[$positionkey] != ){
                  $wrapped = &$question->subquestions[$positionkey];
                  $qout = $wrapped->get_renderer();
                  $qa->subquestionindex = $positionkey ;
                  $result .= $qout->formulation_and_controls($qa,$options);

Shortanswer and numerical use the same renderer

The regular in-line select element multichoice has its own renderer.

The multianswer or single answer multiple choice (vertical or horizontal display) use the same renderer.

Back and forth between 'answer' and '_subi-answer'

The QA Question attempt expects to interact with a multianswer question object but has no initial provision to handle subquestions. So all step parameters like 'order' in the case of multichoice questions and 'answer' or 'choice' related to the student answer to a given subquestion has to be identified specifically. We choose to add a prefix _subi- where i is the subquestion index in the question text.

Here an example of the HTML code for a multianswer question which has a shortanswer subquestion (i=1) followed by a single multichoice in-line (i=2).

This question consists of some text with after that you will have to deal with this short answer 
<label>
<input type="text" name="q1037,1__sub1-answer" id="q1037,1__sub1-answer" size="37" />
</label> and a multichoice embedded right here 
 
 <select id="menuq1037,1__sub2-answer" class="select menuq1037,1__sub2-answer" name="q1037,1__sub2-answer" >
  <option value=""> </option>
  <option value="1">Correct answer</option>
  <option value="2">Another wrong answer</option>
  <option value="3">Wrong answer</option>
  <option value="4">Answer that gives half the credit</option>
 </select>

the attempt data will be something like

   [_sub2_order] => 294,293,295,292 giving the answers id  order in the select element

the step data if the student answer Correct response for the shortanswer and select first choice for the multiple choice will be

   [_sub1-answer] => Correct Answer
   [_sub2-answer] => 1

So the renderer.php will need to add

_sub1-
to the name or id of the input text element (i.e. name="q1037,1__sub1-answer" )
_sub2-
to the name or id of the select element (i.e. name="q1037,1__sub2-answer" )

However the multianswer/question.php andmultianswer/renderer.php will need to handle correctly the step data

   [_sub1-answer] => Correct Answer
   [_sub2-answer] => 1

knowing that the shortanswer/question.php expect the step data as

   [answer] => Correct Answer

and multichoice/question.php expect the data as

   [order] => 294,293,295,292 giving the answers id order in the select element

and expect the step data as

   [answer] => 1

(or as [choice1] => 1 if multiple answer multiplechoice).

Various strategies were used to correctly handle the subquestions data and step data.

Multianswer Question functions

The main difficulties where how to code the various functions ( i.e. get_correct_response(), is_complete_response() etc.) to the specifics of each subquestion types.

The main strategy for multianswer question functions ( i.e. get_correct_response(), is_complete_response() etc.) is to redirect the call to the subquestions and assembling the subquestion results correctly

Specific attempt_step_subquestion_adapter

The QA Question attempt expects to interact with a multianswer question object but has no initial provision to handle subquestions. So Tim create a specific question_attempt_step class .

For the functions that involve directly the $step, Tim design the
/**
* This is an adapter class that wraps a {@link question_attempt_step} and
* modifies the get/set_*_data methods so that they operate only on the parts
* that belong to a particular subquestion, as indicated by an extra prefix.
*
* @copyright © 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class question_attempt_step_subquestion_adapter extends question_attempt_step {
   /** @var question_attempt_step the step we are wrapping. */
   protected $realqas;
   /** @var string the exta prefix on fields we work with. */
   protected $extraprefix;
   /**
    * 
    * @param question_attempt_step $realqas the step to wrap.
    * @param unknown_type $extraprefix the extra prefix that is used for date fields.
    */
   public function __construct(question_attempt_step $realqas, $extraprefix) {
       $this->realqas = $realqas;
       $this->extraprefix = $extraprefix;
   }
   /**
    * Add the extra prefix to a field name.
    * @param string $field the plain field name.
    * @return string the field name with the extra bit of prefix added.
    */
   protected function adjust_field_name($field) {
       if (substr($field, 0, 2) == '!_') {
           return '!_' . $this->extraprefix . substr($field, 2);
       } else if (substr($field, 0, 2) == '!') {
           return '!' . $this->extraprefix . substr($field, 1);
       } else if (substr($field, 0, 2) == '_') {
           return '_' . $this->extraprefix . substr($field, 1);
       } else {
           return $this->extraprefix . $field;
       }
   }
.....

We could then code the function related to init and expected data.

function init_first_step()

  public function init_first_step(question_attempt_step $step) {
   foreach ($this->subquestions as $i => $subq) {
       $substep = new question_attempt_step_subquestion_adapter($step, '_sub' . $i);
       $subq->init_first_step($substep);
       
     //  $subq->init_first_step($step);
   }

// echo "

multianswer end of init first step

";

}

function init_first_step() for subquestions

This handle well the init_first_step() from already existing questions (shortanswer, numerical and multichoice ) as here with multichoice

   public function init_first_step(question_attempt_step $step) {
       if ($step->has_qt_var('_order')) {
           $this->order = explode(',', $step->get_qt_var('_order'));
       } else {
           $this->order = array_keys($this->answers);
           if ($this->shuffleanswers) {
               shuffle($this->order);
           }
           $step->set_qt_var('_order', implode(',', $this->order));
       }
   }

In the inline select element for multichoice the first select empty element cannot be shuffled so we adapt the code from the match question to obtain

   public function init_first_step(question_attempt_step $step) {
       if ($step->has_qt_var('_order')) {
           $choiceorder = explode(',', $step->get_qt_var('_order'));
       } else {
           $choiceorder = array_keys($this->answers);
           if ($this->shuffleanswers) {
               shuffle($choiceorder);
           }             
           $step->set_qt_var('_order', implode(',', $choiceorder));
       }
       $this->order = array();
       foreach ($choiceorder as $key => $value) {
           $this->order[$key + 1] = $value;
       }
   }

and

       protected function init_order(question_attempt $qa) {
       if (is_null($this->order)) {
           $choiceorder = explode(',', $step->get_qt_var($this->fieldid.'_order'));
           $this->order = array();
           foreach ($choiceorder as $key => $value) {
               $this->order[$key + 1] = $value;
           }            
       }
   }

Question grading

In the interaction model, after a submit from the student, there is a test ( is_same_response()) that controls if there are some new elements that should be managed. For this function we were able to design an universal function without refering to the subquestions.

function is_same_response()

   public function is_same_response(array $prevresponse, array $newresponse) {
       if ( $newresponse ==  || count($prevresponse) != count($newresponse)){
           return false ;
       }
       foreach($newresponse as $arraykey => $value){
           if($value ==  ||  ! array_key_exists($arraykey, $prevresponse) 
               || $value !== $prevresponse[$arraykey]){
              return false ;
         }
       }
       return true ;       
   }

If the test is false i.e. something new, then one of the process will be to grade the new responses using is_gradable_response() which generally refers to is_complete_response(). If is_gradable_response() true then grade_response() can be called (shortanswer,numerical and multichoice are all gradable questions).

One main difficulty is related to the harcoding of 'answer' as identifier for the student answer stored in the step data in the shortanswer, numerical and single answer multichoice questions. In the multianswer they are stored with the prefix _sub_i where i is the subquestion index. So we need to remove this prefix before transferring the data to a subquestion function.

Here the case for

function is_gradable_response()

function is_gradable_response(array $response) {
    // Split responses into the separate bits for each question.
   $subresponses = array();
   foreach ($response as $key => $value) {
       if (preg_match('/^_sub(\d+)-(.*)/', $key, $matches)) {
           $subresponses[$matches[1]][$matches[2]] = $value;
       }
   }
   $result = false ;

   foreach ($this->subquestions as $i => $subq) {
       if (!empty($subresponses[$i])) {
           $result = $result || $subq->is_gradable_response($subresponses[$i]);
       }
   }
       return $result;
   }

For single answer question type ( shortanswer, numerical and single answer multichoice questions) is_gradable_response() tranfer to is_complete_response().

is_complete_response(array $response)

   public function is_complete_response(array $response) { // used
   // Split responses into the separate bits for each question.
   $subresponses = array();
   foreach ($response as $key => $value) {
    if (preg_match('/^_sub(\d+)-(.*)/', $key, $matches)) {
           $subresponses[$matches[1]][$matches[2]] = $value;
       }
   }
   
   // Now test each subquestion and  combine.
   // return positive is at least one subquestion test positive
   // so that it could be graded at the next step
   $result = false ;
   foreach ($this->subquestions as $i => $subq) {
       if (isset($subresponses[$i])) {   // empty test is bad if 0 is one value 
           if($subq->is_complete_response($subresponses[$i])){
               return true ;
           }
       }
   }
   return $result ;
}

grade_response(array $response)

Then grade_response(array $response) which uses the same stategy

   public function grade_response(array $response) {
   $subresponses = array();
   foreach ($response as $key => $value) {
       if (preg_match('/^_sub(\d+)-(.*)/', $key, $matches)) {
           $subresponses[$matches[1]][$matches[2]] = $value;
       }
   }    
   // Now grade each bit and  combine.
   $totfraction = 0 ;
   foreach ($this->subquestions as $i => $subq) { 
       if (!empty($subresponses[$i])) {
           $res = $subq->grade_response($subresponses[$i]); // fraction=>state 
           $subgrade= $res[0]* $subq->defaultmark ;
           $subq->subgrade = $subgrade ;               // store the resulting grade
           $totfraction += $res[0] * $subq->defaultmark/$this->defaultmark ;
       }
   }
       return array($totfraction, question_state::graded_state_for_fraction($totfraction));
   }

Subquestion functions

Shortanswer

Numerical

Multichoice