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 115: Line 115:


In one case we could desing an universal function without refering to the subquestions.
In one case we could desing an universal function without refering to the subquestions.
== 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 .
<code php >
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;
        }
    }
.....
</code>
We could then code the function related to init and expected data.
=== function init_first_step()===
<code php >
  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 "<p> multianswer end of init first step </p> ";
}
</code>
==== function init_first_step() fro subquestions ====
This handle well the init_first_step() from already existing questions (shortanswer, numrical and multichoice ) as here with multichoice
<code php >
    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));
        }
    }
</code>
In the inline select element for multichoice we took the


=== function is_same_response()===
=== function is_same_response()===
In one case we could design an universal function without refering to the subquestions.


<code php >
<code php >
Line 133: Line 217:
     }
     }
</code>
</code>
For the functions that involve directly the $step, Tim design the

Revision as of 17:13, 7 April 2010

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.

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 QA Question attempt expects to interact with a multianswer question object but has no initial provision to handle subquestions.

So the multianswer question functions should redirect the call to the subquestions.

In one case we could desing an universal function without refering to the subquestions.

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() fro subquestions

This handle well the init_first_step() from already existing questions (shortanswer, numrical 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 we took the


function is_same_response()

In one case we could design an universal function without refering to the subquestions.

   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 ;       
   }