Note:

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

Question behaviours: Difference between revisions

From MoodleDocs
No edit summary
No edit summary
(24 intermediate revisions by 5 users not shown)
Line 1: Line 1:
{{Template:Question_engine_2}}
{{Template:Question_engine_2}}
This page explains how to go about writing a new Question Interaction Model for the new Moodle [[Question Engine 2|question engine]].
This page explains how to go about writing a new question behaviour for the new Moodle [[Question Engine 2|question engine]].


Previous section: [[Overview_of_the_Moodle_question_engine|Overview_of_the_Moodle_question_engine]]
Previous section: [[Overview_of_the_Moodle_question_engine|Overview_of_the_Moodle_question_engine]]


{{Work in progress}}
To learn to write question behaviours, you are highly encouraged to read the code of some of the existing behaviours in the Moodle code base. The code for most behaviours is shorter than these instructions!
 
To learn to write Question Interaction Models, you are highly encouraged to read the code of some of the existing interaction models in the Moodle code base. The code for most interaction models is shorter than these instructions!


Also note that all the question engine code has extensive PHP documenter comments that should explain the purpose of every class and method. This document is supposed to provide an overview of the key points to get you started. It does not attempt to duplicate all the details in the PHPdocs.
Also note that all the question engine code has extensive PHP documenter comments that should explain the purpose of every class and method. This document is supposed to provide an overview of the key points to get you started. It does not attempt to duplicate all the details in the PHPdocs.
Line 15: Line 13:
==File layout==
==File layout==


A question interaction model plugin lives in a folder in <tt>question/interaction</tt>. The layout inside that folder follows the typical layout for a Moodle plugin. For example, inside <tt>question/interaction/mymodel/</tt> we would have:
A question behaviour plugin lives in a folder in <tt>question/behaviour</tt>. The layout inside that folder follows the typical layout for a Moodle plugin. For example, inside <tt>question/behaviour/mybehaviour/</tt> we would have:


; model.php : This contains the definition of the <tt>qim_mymodel</tt> class, which should extend the <tt>question_interaction_model</tt> base class.
; behaviour.php : This contains the definition of the <tt>qbehaviour_mybehaviour</tt> class, which should extend the <tt>question_behaviour</tt> base class.
; renderer.php : This contains the definition of the <tt>qim_mymodel_renderer</tt> class, which should extend the <tt>qim_renderer</tt> base class.
; renderer.php : This contains the definition of the <tt>qbehaviour_mybehaviour_renderer</tt> class, which should extend the <tt>qbehaviour_renderer</tt> base class.
; simpletest/... : Contains the unit tests for this interaction model. You are strongly encouraged to write thorough unit tests to ensure the correctness of your interaction model.
; behaviourtype.php : This contains the definition of the <tt>qbehaviour_mybehaviour_type</tt> class, which should extend the <tt>question_behaviour_type</tt> base class.
; lang/en_utf8/qim_mymodel.php : English language strings for this interaction model. You can, of course, include other languages too. The language file must define at least the string giving the model a name, for example <tt>$string['mymodel'] = 'My model';</tt>.
; tests/... : Contains the [[PHPUnit|unit tests]] and [[Acceptance testing|acceptance tests]] for this behaviour. You are strongly encouraged to write thorough unit tests to ensure the correctness of your behaviour.
; lang/en/qbehaviour_mybehaviour.php : English language strings for this behaviour. The language file must define at least the standard 'pluginname' giving the name of the behaviour, for example <tt>$string['pluginname'] = 'My behaviour';</tt>.


Note that Question Interaction Models are not expected to have their own database tables or capabilities. Therefore, there should not be a db sub-folder.
Note that question behaviours are not expected to have their own database tables or capabilities. Therefore, there should not be a db sub-folder. Similarly, it would probably work to write a cron routine or fire events from your behaviour, but you probably should not do that.


In future, we will probably add the ability for an Interaction Model to have a settings.php file, so that it can have configuration options.


==Question behaviour class==


==Question model class==
One instance of this class will be created for each <tt>question_attempt</tt> that uses this behaviour.


One instance of this class will be created for each <tt>question_attempt</tt> that uses this model.
This documentation just lists the methods that you are likely to need to override. See the PHPdocumentor comments for a complete API documentation.
 
This documentation just lists the metods that you are likely to need to override. See the PHPdocumentor comments for a complete API documentation.




===Class declaration===
===Class declaration===


The class name for the model class must be the plugin name (e.g. <tt>mymodel</tt>) prefixed by <tt>qim_</tt>. You must extend the <tt>question_interaction_model</tt> base class, or another model class.
The class name for the behaviour class must be the plugin name (e.g. <tt>mybehaviour</tt>) prefixed by <tt>qbehaviour_</tt>. You must extend the <tt>question_behaviour</tt> base class, or another behaviour class.


<code php>
<code php>
class qim_mymodel extends question_interaction_model {
class qbehaviour_mybehaviour extends question_behaviour {
     // ...
     // ...
}
}
Line 45: Line 42:


You should not need to override the constructor.
You should not need to override the constructor.


===Fields===
===Fields===


When an interaction model class is created, the fields <tt>qa</tt> and <tt>question</tt> are initialised to point to the question attempt and question that this model is being used by.
When a behaviour class is created, the fields <tt>qa</tt> and <tt>question</tt> are initialised to point to the question attempt and question that this behaviour is being used by.


From the base class code:
From the base class code:
Line 62: Line 58:
         $this->qa = $qa;
         $this->qa = $qa;
         $this->question = $qa->get_question();
         $this->question = $qa->get_question();
        // ...
     }
     }
</code>
</code>


===Methods used during attempts at a question===


===IS_ARCHETYPAL===


Some interaction models define models that should be offered as options in the user interface. For example CBM, interactive mode, and so on. Other models are for internal use. For example <tt>qim_informationitem</tt>, <tt>qim_interactiveadaptedformyqtype</tt>. This is indicated by defining a constant <tt>IS_ARCHETYPAL</tt> in the class. True means that this model will be offered as an option in the UI.


<code php>
====required_question_definition_class()====
    const IS_ARCHETYPAL = true; // or false
</code>


Most behaviours can only work with a particular subclasses of <tt>question_definition</tt>. For example, they may only be able to work work with questions that are <tt>question_automatically_gradable</tt>s. This method lets the behaviour document that. The type of question passed to the constructor is then checked against the class name returned by this function.


===required_question_definition_class===
Example from <tt>qbehaviour_deferredfeedback</tt>:
 
Most interaction models can only work with a particular subclasses of <tt>question_definition</tt>. For example perhpas they can only work with questions that are <tt>question_graded_automatically</tt>s. This method lets the interaction model document that. The type of question passed to the constructor is then checked against the class name returned by this function.
 
Example from <tt>qim_deferredfeedback</tt>:
<code php>
<code php>
     public function required_question_definition_class() {
     public function required_question_definition_class() {
         return 'question_graded_automatically';
         return 'question_automatically_gradable';
     }
     }
</code>
</code>


====get_min_fraction()====


===get_min_fraction===
Returns the smallest possible [[Question_Engine_2:Design#Words_related_to_grades|fraction]] that this behaviour, applied to this question, may give. Normally this will be the min fraction of the question type, however some behaviours (for example CBM) manipulate the question scores, and so need to return an adjusted min_fraction.
 
Returns the smallest possible [[Question_Engine_2:Design#Words_related_to_grades|fraction]] that this model, applied to this question, may give. Normally this will be the min fraction or the question type, however some models (for example CBM) manipulate the question scores, and so need to return an adjusted min_fraction.


For example, from <tt>qim_deferredfeedback</tt>:
For example, from <tt>qbehaviour_deferredfeedback</tt>:
<code php>
<code php>
     public function get_min_fraction() {
     public function get_min_fraction() {
Line 98: Line 88:
</code>
</code>


From <tt>qim_qim_deferredcbm</tt>:
From <tt>qbehaviour_deferredcbm</tt>:
<code php>
<code php>
     public function get_min_fraction() {
     public function get_min_fraction() {
Line 106: Line 96:
</code>
</code>


====get_expected_data()====


===get_expected_data===
Some behaviours display additional form controls. When the question form is submitted, these submitted values are read using the standard optional_param function. The question engine needs to know what parameters to look for, and with what types.


Some interaction models display additional form controls. When the question from is submitted, these submitted values are read using the standard optional_param function. The question engine needs to know what parameters to ask for, and with what types.
The <tt>get_expected_data</tt> function should return an array of expected parameters, with the corresponding param type. Note that the array of expected parameters may depend on the current state of the question attempt. For example, from <tt>qbehaviour_deferredcbm</tt>
 
The <tt>get_expected_data</tt> function should return an array of expected parameters, with the corresponding param type. Note that the array of expected parameters may depend on the current state of the question attempt. For example, from <tt>qim_deferredcbm</tt>
<code>
<code>
     public function get_expected_data() {
     public function get_expected_data() {
Line 117: Line 106:
             return array('certainty' => PARAM_INT);
             return array('certainty' => PARAM_INT);
         }
         }
         return array();
         return parent::get_expected_data();
     }
     }
</code>
</code>


====init_first_step()====


===init_first_step===
You are unlikely to need to override this method. This is called when the question attempt is actually stared. It gives the behaviour a chance to do any necessary initialisation, including passing on the request to the question type. This is, for example, how the choices in a multiple choices question are shuffled.
 
You are unlikely to need to override this method. This is called when the question attempt is actually stared. It gives the model a chance to do any necessary initialisation, including passing on the request to the question type. This is, for example, how the choices in a multiple choices question are shuffled.


From the base class:
From the base class:
Line 134: Line 122:




===process_action===
====process_action()====


This is the most important method. It controls what happens when someone does something with the question (other than starting it, which is handled by <tt>init_first_step</tt>.)
This is the most important method. It controls what happens when someone does something with the question (other than starting it, which is handled by <tt>init_first_step</tt>.)


When the method is called, a pending attempt step is passed in. This method can either return false, to indicate nothing interesting happened, and the pending step should be discarded. Or, it can update that step, and return true, to have the new step retained.
When the method is called, a pending attempt step is passed in. This method can either return <tt>question_attempt::DISCARD</tt>, to indicate nothing interesting happened, and the pending step should be discarded. Or, it can update that step, and return <tt>question_attempt::KEEP</tt>, to have the new step retained.


Typically, the method is implemented by examining the current state of the attempt, and the pending step, to decide what sort of action this is, and then delegating to a more specific method.
Typically, the method is implemented by examining the current state of the attempt, and the pending step, to decide what sort of action this is, and then dispatching to a more specific method.


For example, from <tt>qim_deferredfeedback</tt>
For example, from <tt>qbehaviour_deferredfeedback</tt>
<code php>
<code php>
     public function process_action(question_attempt_step $pendingstep) {
     public function process_action(question_attempt_step $pendingstep) {
         if ($pendingstep->has_im_var('comment')) {
         if ($pendingstep->has_behaviour_var('comment')) {
             return $this->process_comment($pendingstep);
             return $this->process_comment($pendingstep);
         } else if ($pendingstep->has_im_var('finish')) {
         } else if ($pendingstep->has_behaviour_var('finish')) {
             return $this->process_finish($pendingstep);
             return $this->process_finish($pendingstep);
         } else {
         } else {
Line 155: Line 143:
</code>
</code>


Note that there are some special actions that every model must be able to handle:
Note that there are some special actions that every behaviour must be able to handle:
 
; behaviour_var comment => '... ''the comment'' ...' : If this question has marks, then there will also be a behaviour_var mark and a behaviour_var maxmark. This is the action that is fired when a user manually grades a question.
; behaviour_var finish => 1 : This is the action that is generated when the user submits and finishes a whole usage. For example when they click ''''Submit all and finish'''' in a quiz.
 
The base class implements the <tt>process_comment</tt> method in a way that should be suitable for most behaviours. There is also a subclass <tt>question_behaviour_with_save</tt> of <tt>question_behaviour</tt> which provides a <tt>process_save</tt> implementation that may be suitable for most behaviours. You may find it helps to extend this class instead of <tt>question_behaviour</tt>.
 
====process_...()====
 
So, the process_action function has dispatched to a more specific process_... function. What should that function do? Normally, it has to delegate some processing to the question type, and based on the results of that, upgrade the <tt>$pendingstep</tt>.
 
For example, from <tt>qbehaviour_deferredfeedback</tt>
<code php>
    public function process_finish(question_attempt_pending_step $pendingstep) {
        if ($this->qa->get_state()->is_finished()) {
            return question_attempt::DISCARD;
        }


; im_var comment => 'A manual comment' : This may also have im_var mark and im_var maxmark, if the question is graded. This is the action that is fired when a user manually grades a question.
        $response = $this->qa->get_last_step()->get_qt_data();
; im_var finish => 1 : This is the action that is generated when the user submits and finishes a whole usage. For example when they click ''''Submit all and finish'''' in a quiz.
        if (!$this->question->is_gradable_response($response)) {
            $pendingstep->set_state(question_state::$gaveup);
        } else {
            list($fraction, $state) = $this->question->grade_response($response);
            $pendingstep->set_fraction($fraction);
            $pendingstep->set_state($state);
        }
        $pendingstep->set_new_response_summary($this->question->summarise_response($response));
        return question_attempt::KEEP;
    }
</code>


The base class implements the <tt>process_comment</tt> method in a way that should be suitable for most models. It also provides a <tt>process_save</tt> implementation that may be suitable for most models.
First, if the question attempt has already been finished, there is nothing to do, so <tt>question_attempt::DISCARD</tt> is returned.


If there is something to do, the response data is extracted from the <tt>$pendingstep</tt>. The question type is asked whether the data there is complete enough to be graded. If not, the state is set to 'gave up'. If there is, the question type is asked to compute the fraction, and that and the state are put into the <tt>$pendingstep</tt>. Finally, the response summary, which is displayed in places like the quiz reports is updated and <tt>question_attempt::KEEP</tt> is returned.


===adjust_display_options===
====adjust_display_options()====


This method is called before a question is rendered, so that the interaction model can change the display options depending on the current state of the question. So, for example, after the question is finished, it should be displayed in read-only mode. Before the student had submitted an answer, no feedback should be displayed.
This method is called before a question is rendered, so that the behaviour can change the display options depending on the current state of the question. So, for example, after the question is finished, it should be displayed in read-only mode. Before the student had submitted an answer, no feedback should be displayed.


For example, the base class does
For example, the base class does
<code php>
<code php>
     public function adjust_display_options(question_display_options $options) {
     public function adjust_display_options(question_display_options $options) {
         if (question_state::is_finished($this->qa->get_state())) {
         if ($this->qa->get_state()->is_finished())) {
             $options->readonly = true;
             $options->readonly = true;
            $options->numpartscorrect = $options->numpartscorrect &&
                    $this->qa->get_state()->is_partially_correct() &&
                    !empty($this->question->shownumcorrect);
         } else {
         } else {
             $options->hide_all_feedback();
             $options->hide_all_feedback();
         }
         }
    }
</code>
====get_state_string()====
In the info box that starts each question, the is a few words summary of the current state of the question. That summary is generated by this method. It is not often necessary to override the base class implementation:
<code php>
    public function get_state_string($showcorrectness) {
        return $this->qa->get_state()->default_string($showcorrectness);
    }
</code>
An example where it is overridden is <tt>qbehaviour_interactive</tt>, if you want to look.
====get_our_resume_data()====
This method is required to make the quiz feature '''Each attempt builds on last''' work. It needs to return any behaviour variable from the current attempt that would be needed to start a new attempt in the same state.
For example, from <tt>qbehaviour_deferredcbm</tt>:
<code php>
    protected function get_our_resume_data() {
        $lastcertainty = $this->qa->get_last_behaviour_var('certainty');
        if ($lastcertainty) {
            return array('-certainty' => $lastcertainty);
        } else {
            return array();
        }
    }
    protected function get_our_resume_data() {
        return array();
    }
</code>
===Methods used for reporting===
As well as just processing a single attempt as it happens, we need to be able to run reports of what happened across lots of attempts, for example to determine whether questions are performing well or badly.
Some of these methods in this section are called during the attempt, and the result stored in the database at that time, rather than when the report is run. That is just a performance consideration. Logically, these methods are all for reporting, so I list them here.
====get_correct_response()====
In fact, this first method is not even used for the reports. It is used in the question preview pop-up window to make the 'Fill in correct response' button work. It returns the behaviour data that needs to be submitted as part of a correct response. Obviously, this is then combined with data from the question type.
For example, from <tt>qbehaviour_deferredcbm</tt>
<code php>
    public function get_correct_response() {
        if ($this->qa->get_state()->is_active()) {
            return array('certainty' => question_cbm::HIGH);
        }
        return array();
    }
</code>
====get_question_summary()====
This method returns a plain-text summary of the question that was asked. This is used, for example, by the quiz Responses report. Normally the base-class implementation that just delegates to the question type is all you need.
<code php>
    public function get_question_summary() {
        return $this->question->get_question_summary();
    }
</code>
====get_right_answer_summary()====
This should return a plain-text summary of what the right answer to the question is. For example, in the base class:
<code php>
    public function get_right_answer_summary() {
        return $this->question->get_right_answer_summary();
    }
</code>
and in <tt>qbehaviour_deferredcbm</tt>:
<code php>
    public function get_right_answer_summary() {
        $summary = parent::get_right_answer_summary();
        return $summary . ' [' . question_cbm::get_string(question_cbm::HIGH) . ']';
    }
</code>
(Hmm. I probably should not be concatenating language strings like that.)
====classify_response()====
This is used by the response analysis in the quiz statistics report: At the moment, no behaviours do more than delegate to the question type:
<code php>
    public function classify_response() {
        return $this->question->classify_response($this->qa->get_last_qt_data());
    }
</code>
====summarise_action()====
This is used by the response history table that is shown underneath questions on the quiz review page, in the Action column. It should return a plain text representation of the action the student took in this step. For example, from <tt>qbehaviour_deferredfeedback</tt>:
<code php>
    public function summarise_action(question_attempt_step $step) {
        if ($step->has_behaviour_var('comment')) {
            return $this->summarise_manual_comment($step);
        } else if ($step->has_behaviour_var('finish')) {
            return $this->summarise_finish($step);
        } else {
            return $this->summarise_save($step);
        }
    }
</code>
from <tt>question_behaviour_with_save</tt>
<code php>
    public function summarise_save(question_attempt_step $step) {
        $data = $step->get_submitted_data();
        if (empty($data)) {
            return $this->summarise_start($step);
        }
        return get_string('saved', 'question',
                $this->question->summarise_response($step->get_qt_data()));
    }
</code>
and from the base class:
<code php>
    public function summarise_start($step) {
        return get_string('started', 'question');
    }
    public function summarise_finish($step) {
        return get_string('attemptfinished', 'question');
     }
     }
</code>
</code>
Line 181: Line 321:
==Renderer class==
==Renderer class==


Renderers are all about generating HTML output. The overall output of questions is controlled by the <tt>core_question_renderer::question(...)</tt>, which in turn calls other methods of itself, the question type renderer and the interaction model renderer, to generate the various bits of output. See [[Question_Engine_2:Overview#What_are_the_parts_of_a_question.3F|this overview of the parts of a question]].
Renderers are all about generating HTML output. The overall output of questions is controlled by the <tt>core_question_renderer::question(...)</tt> method, which in turn calls other methods of the core question renderer, the behaviour renderer and the question type renderer, to generate the various bits of output. See [[Question_Engine_2:Overview#What_are_the_parts_of_a_question.3F|this overview of the parts of a question]].
 
Note that most of the methods take a <tt>question_display_options</tt> object that specifies what should and should not be visible. For example, should feedback, or marks information, be displayed. It is important that your renderer respects these options.


Once again, in what follows, I only list the methods your are most likely to want to override.
Once again, in what follows, I only list the methods your are most likely to want to override.
Line 190: Line 332:
This is a block of output in the question formulation area. It goes after all the question type output in this area.
This is a block of output in the question formulation area. It goes after all the question type output in this area.


Two examples. This method is used by the interactive model to show the '''Submit''' button (if the question is in an appropriate state). It is used by the CBM model to show the certainty choices.
Two examples. This method is used by the interactive behaviour to show the '''Submit''' button (if the question is in an appropriate state). It is used by the CBM behaviour to show the certainty choices. Here is the interactive behaviour code:
<code php>
    public function controls(question_attempt $qa, question_display_options $options) {
        return $this->submit_button($qa, $options);
    }
</code>
<tt>submit_button</tt> is a helper method provided by the base class.




===feedback===
===feedback===


This is a block of output in the feedback area of the question. It is used, for example in interactive mode, to show the '''Try again''' button.
This is a block of output in the feedback area of the question. It is used, for example, in interactive behaviour, to show the '''Try again''' button, and in the CBM behaviour to output some text that explains how the score was adjusted. Here is the interactive behaviour code:
<code php>
    public function feedback(question_attempt $qa, question_display_options $options) {
        if (!$qa->get_state()->is_active() || !$options->readonly) {
            return '';
        }
 
        // Set up $attributes array ...
 
        $output = html_writer::empty_tag('input', $attributes);
        if (empty($attributes['disabled'])) {
            $output .= print_js_call('question_init_submit_button',
                    array($attributes['id'], $qa->get_slot()), true);
        }
        return $output;
</code>
 
Note that we set up a call to the JavaScript function <tt>question_init_submit_button</tt>, passing the button id, and the question slot number. You should do that for any button you output that will cause the question to be submitted.
 
 
==Question behaviour type class==
 
The behaviour type class provides information about the behaviour in general. In contrast the behaviour class handles the behaviour applied to one particular question_attempt.
 
Note that, before Moodle 2.6, there was not a separate behaviour type class. These methods either did not exist, or were static methods on the behaviour class.
 
 
===Methods related to the settings UI===
 
An activity using questions needs to provide various options to the user that relate to behaviours. For example, on the quiz settings form, we need to display a menu '''How questions behave''' where the teacher can choose which behaviour they want. The quiz does this by calling <tt>question_engine::get_behaviour_options()</tt>. Similarly, when selecting the display options '''Users may review''', '''During the attempt''', not all options are relevant to all behaviours, The quiz can get the appropriate dependencies by calling <tt>question_engine::get_behaviour_unused_display_options()</tt>. Naturally, the question engine needs to consult the behaviours to implement these methods This is done using the following two things.
 
====is_archetypal()====
 
Some behaviours should be offered as options in the user interface. For example CBM, interactive, and so on. Others are for internal use. For example <tt>qbehaviour_informationitem</tt>, <tt>qbehaviour_interactiveadaptedformyqtype</tt>. This is indicated by the return value of the is_archetypal method. True means that this behaviour will be offered as an option in the UI. This defaults to false in the base class, so you need to override the method if you want your behaviour to be an option that users select.
 
<code php>
    public function is_archetypal() {
        return true;
    }
</code>
 
====get_unused_display_options()====
 
This method should return a list of <tt>question_display_options</tt> field that are irrelevant before <tt>question_usage_by_activity::finish_all_questions()</tt> is called.
 
For example, from <tt>qbehaviour_deferredfeedback_type</tt>:
<code php>
    public static function get_unused_display_options() {
        return array('correctness', 'marks', 'specificfeedback',
                'generalfeedback', 'rightanswer');
    }
</code>
 
 
===Things related to reporting===
 
====adjust_random_guess_score()====
 
Questions are able to report an estimate of the mark a student would get by guessing. Naturally, question behaviours that modify the marks need to be able to manipulate that. For example in <tt>qbehaviour_deferredcbm_type</tt>:
<code php>
    public static function adjust_random_guess_score($fraction) {
        return question_cbm::adjust_fraction($fraction, question_cbm::default_certainty());
    }
</code>
 
====summarise_usage()====


This method is an opportunity for the behaviour to provide some overall summary information about all the <tt>question_attempts</tt> in a <tt>question_usage_by_activity</tt>. An example of where this is done is in <tt>qbehaviour_deferredcbm_type</tt>. To see one place (currently the only place) where that is used, have a look at the quiz review page for a quiz attempt using one of the CBM behaviours.


==Unit tests==
==Unit tests==


For a general introduction, see the [[Unit_tests|Moodle unit testing documentation]].
For a general introduction, see the [[PHPUnit|Moodle unit testing documentation]].


Most of the interaction models are currently tested by working one or more test questions through a sequence of example inputs. To do this they subclass <tt>qim_walkthrough_test_base</tt> which provides a lot of helper methods to facilitate writing tests like that.
Most of the behaviours are currently tested by working one or more test questions through a sequence of example inputs and testing the results. To do this they sub-class <tt>qbehaviour_walkthrough_test_base</tt> which provides a lot of helper methods to facilitate writing tests like that.


The best way to start is probably to look at the tests for some of the standard interaction models.
The best way to start is probably to look at the tests for some of the standard behaviours. For example <tt>qbehaviour_deferredfeedback_walkthrough_test::test_deferredfeedback_feedback_multichoice_single</tt>
<code php>
    public function test_deferredfeedback_feedback_multichoice_single() {


        // Create a true-false question with correct answer true.
        $mc = test_question_maker::make_a_multichoice_single_question();
        $rightindex = $this->get_mc_right_answer_index($mc);
        // Start a deferred feedback attempt and add the question to it.
        $this->start_attempt_at_question($mc, 'deferredfeedback', 3);
        // Verify.
        $this->check_current_state(question_state::$todo);
        $this->check_current_mark(null);
        $this->check_current_output(
                $this->get_contains_question_text_expectation($mc),
                $this->get_contains_mc_radio_expectation(0, true, false),
                $this->get_contains_mc_radio_expectation(1, true, false),
                $this->get_contains_mc_radio_expectation(2, true, false),
                $this->get_does_not_contain_feedback_expectation());
        // Process the data extracted for this question.
        $this->process_submission(array('answer' => $rightindex));
        // Verify.
        $this->check_current_state(question_state::$complete);
        $this->check_current_mark(null);
        $this->check_current_output(
                $this->get_contains_mc_radio_expectation($rightindex, true, true),
                $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, true, false),
                $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, true, false),
                $this->get_does_not_contain_correctness_expectation(),
                $this->get_does_not_contain_feedback_expectation());
        // Finish the attempt.
        $this->quba->finish_all_questions();
        // Verify.
        $this->check_current_state(question_state::$gradedright);
        $this->check_current_mark(3);
        $this->check_current_output(
                $this->get_contains_mc_radio_expectation($rightindex, false, true),
                $this->get_contains_correct_expectation());
        // Now change the correct answer to the question, and regrade.
        $mc->answers[13]->fraction = -0.33333333;
        $mc->answers[14]->fraction = 1;
        $this->quba->regrade_all_questions();
        // Verify.
        $this->check_current_state(question_state::$gradedwrong);
        $this->check_current_mark(-1);
        $this->check_current_output(
                $this->get_contains_incorrect_expectation());
    }
</code>
As you can see, the helper methods hide a lot of the details, like starting an attempt, but if you browse the code, you will be able to see exactly what those (quite simple) helper functions do. I was going to summarise here what the test does, but I think the comments in the code already do that well enough.


==See also==
==See also==
Line 213: Line 483:
* The PHP documenter comments that explain the purposes of every method in the question engine code.
* The PHP documenter comments that explain the purposes of every method in the question engine code.
* Back to [[Question_Engine_2|Question Engine 2]]
* Back to [[Question_Engine_2|Question Engine 2]]
[[Category:Plugins]]

Revision as of 14:41, 27 November 2013

This page explains how to go about writing a new question behaviour for the new Moodle question engine.

Previous section: Overview_of_the_Moodle_question_engine

To learn to write question behaviours, you are highly encouraged to read the code of some of the existing behaviours in the Moodle code base. The code for most behaviours is shorter than these instructions!

Also note that all the question engine code has extensive PHP documenter comments that should explain the purpose of every class and method. This document is supposed to provide an overview of the key points to get you started. It does not attempt to duplicate all the details in the PHPdocs.

This document assumes that you understand the data model, where a question attempt comprises a number of steps, and each step has some properties like a state and a mark, and an array of submitted data. This is explained in the overview_of_the_Moodle_question_engine.


File layout

A question behaviour plugin lives in a folder in question/behaviour. The layout inside that folder follows the typical layout for a Moodle plugin. For example, inside question/behaviour/mybehaviour/ we would have:

behaviour.php
This contains the definition of the qbehaviour_mybehaviour class, which should extend the question_behaviour base class.
renderer.php
This contains the definition of the qbehaviour_mybehaviour_renderer class, which should extend the qbehaviour_renderer base class.
behaviourtype.php
This contains the definition of the qbehaviour_mybehaviour_type class, which should extend the question_behaviour_type base class.
tests/...
Contains the unit tests and acceptance tests for this behaviour. You are strongly encouraged to write thorough unit tests to ensure the correctness of your behaviour.
lang/en/qbehaviour_mybehaviour.php
English language strings for this behaviour. The language file must define at least the standard 'pluginname' giving the name of the behaviour, for example $string['pluginname'] = 'My behaviour';.

Note that question behaviours are not expected to have their own database tables or capabilities. Therefore, there should not be a db sub-folder. Similarly, it would probably work to write a cron routine or fire events from your behaviour, but you probably should not do that.


Question behaviour class

One instance of this class will be created for each question_attempt that uses this behaviour.

This documentation just lists the methods that you are likely to need to override. See the PHPdocumentor comments for a complete API documentation.


Class declaration

The class name for the behaviour class must be the plugin name (e.g. mybehaviour) prefixed by qbehaviour_. You must extend the question_behaviour base class, or another behaviour class.

class qbehaviour_mybehaviour extends question_behaviour {

   // ...

}

You should not need to override the constructor.

Fields

When a behaviour class is created, the fields qa and question are initialised to point to the question attempt and question that this behaviour is being used by.

From the base class code:

   // From the base class.
   /** @var question_attempt */
   protected $qa;
   /** @var question_definition */
   protected $question;
   public function __construct(question_attempt $qa) {
       $this->qa = $qa;
       $this->question = $qa->get_question();
       // ...
   }

Methods used during attempts at a question

required_question_definition_class()

Most behaviours can only work with a particular subclasses of question_definition. For example, they may only be able to work work with questions that are question_automatically_gradables. This method lets the behaviour document that. The type of question passed to the constructor is then checked against the class name returned by this function.

Example from qbehaviour_deferredfeedback:

   public function required_question_definition_class() {
       return 'question_automatically_gradable';
   }

get_min_fraction()

Returns the smallest possible fraction that this behaviour, applied to this question, may give. Normally this will be the min fraction of the question type, however some behaviours (for example CBM) manipulate the question scores, and so need to return an adjusted min_fraction.

For example, from qbehaviour_deferredfeedback:

   public function get_min_fraction() {
       return $this->question->get_min_fraction();
   }

From qbehaviour_deferredcbm:

   public function get_min_fraction() {
       return question_cbm::adjust_fraction(
               parent::get_min_fraction(), question_cbm::HIGH);
   }

get_expected_data()

Some behaviours display additional form controls. When the question form is submitted, these submitted values are read using the standard optional_param function. The question engine needs to know what parameters to look for, and with what types.

The get_expected_data function should return an array of expected parameters, with the corresponding param type. Note that the array of expected parameters may depend on the current state of the question attempt. For example, from qbehaviour_deferredcbm

   public function get_expected_data() {
       if (question_state::is_active($this->qa->get_state())) {
           return array('certainty' => PARAM_INT);
       }
       return parent::get_expected_data();
   }

init_first_step()

You are unlikely to need to override this method. This is called when the question attempt is actually stared. It gives the behaviour a chance to do any necessary initialisation, including passing on the request to the question type. This is, for example, how the choices in a multiple choices question are shuffled.

From the base class:

   public function init_first_step(question_attempt_step $step) {
       $this->question->init_first_step($step);
   }


process_action()

This is the most important method. It controls what happens when someone does something with the question (other than starting it, which is handled by init_first_step.)

When the method is called, a pending attempt step is passed in. This method can either return question_attempt::DISCARD, to indicate nothing interesting happened, and the pending step should be discarded. Or, it can update that step, and return question_attempt::KEEP, to have the new step retained.

Typically, the method is implemented by examining the current state of the attempt, and the pending step, to decide what sort of action this is, and then dispatching to a more specific method.

For example, from qbehaviour_deferredfeedback

   public function process_action(question_attempt_step $pendingstep) {
       if ($pendingstep->has_behaviour_var('comment')) {
           return $this->process_comment($pendingstep);
       } else if ($pendingstep->has_behaviour_var('finish')) {
           return $this->process_finish($pendingstep);
       } else {
           return $this->process_save($pendingstep);
       }
   }

Note that there are some special actions that every behaviour must be able to handle:

behaviour_var comment => '... the comment ...'
If this question has marks, then there will also be a behaviour_var mark and a behaviour_var maxmark. This is the action that is fired when a user manually grades a question.
behaviour_var finish => 1
This is the action that is generated when the user submits and finishes a whole usage. For example when they click 'Submit all and finish' in a quiz.

The base class implements the process_comment method in a way that should be suitable for most behaviours. There is also a subclass question_behaviour_with_save of question_behaviour which provides a process_save implementation that may be suitable for most behaviours. You may find it helps to extend this class instead of question_behaviour.

process_...()

So, the process_action function has dispatched to a more specific process_... function. What should that function do? Normally, it has to delegate some processing to the question type, and based on the results of that, upgrade the $pendingstep.

For example, from qbehaviour_deferredfeedback

   public function process_finish(question_attempt_pending_step $pendingstep) {
       if ($this->qa->get_state()->is_finished()) {
           return question_attempt::DISCARD;
       }
       $response = $this->qa->get_last_step()->get_qt_data();
       if (!$this->question->is_gradable_response($response)) {
           $pendingstep->set_state(question_state::$gaveup);
       } else {
           list($fraction, $state) = $this->question->grade_response($response);
           $pendingstep->set_fraction($fraction);
           $pendingstep->set_state($state);
       }
       $pendingstep->set_new_response_summary($this->question->summarise_response($response));
       return question_attempt::KEEP;
   }

First, if the question attempt has already been finished, there is nothing to do, so question_attempt::DISCARD is returned.

If there is something to do, the response data is extracted from the $pendingstep. The question type is asked whether the data there is complete enough to be graded. If not, the state is set to 'gave up'. If there is, the question type is asked to compute the fraction, and that and the state are put into the $pendingstep. Finally, the response summary, which is displayed in places like the quiz reports is updated and question_attempt::KEEP is returned.

adjust_display_options()

This method is called before a question is rendered, so that the behaviour can change the display options depending on the current state of the question. So, for example, after the question is finished, it should be displayed in read-only mode. Before the student had submitted an answer, no feedback should be displayed.

For example, the base class does

   public function adjust_display_options(question_display_options $options) {
       if ($this->qa->get_state()->is_finished())) {
           $options->readonly = true;
           $options->numpartscorrect = $options->numpartscorrect &&
                   $this->qa->get_state()->is_partially_correct() &&
                   !empty($this->question->shownumcorrect);
       } else {
           $options->hide_all_feedback();
       }
   }

get_state_string()

In the info box that starts each question, the is a few words summary of the current state of the question. That summary is generated by this method. It is not often necessary to override the base class implementation:

   public function get_state_string($showcorrectness) {
       return $this->qa->get_state()->default_string($showcorrectness);
   }

An example where it is overridden is qbehaviour_interactive, if you want to look.

get_our_resume_data()

This method is required to make the quiz feature Each attempt builds on last work. It needs to return any behaviour variable from the current attempt that would be needed to start a new attempt in the same state.

For example, from qbehaviour_deferredcbm:

   protected function get_our_resume_data() {
       $lastcertainty = $this->qa->get_last_behaviour_var('certainty');
       if ($lastcertainty) {
           return array('-certainty' => $lastcertainty);
       } else {
           return array();
       }
   }
   protected function get_our_resume_data() {
       return array();
   }


Methods used for reporting

As well as just processing a single attempt as it happens, we need to be able to run reports of what happened across lots of attempts, for example to determine whether questions are performing well or badly.

Some of these methods in this section are called during the attempt, and the result stored in the database at that time, rather than when the report is run. That is just a performance consideration. Logically, these methods are all for reporting, so I list them here.

get_correct_response()

In fact, this first method is not even used for the reports. It is used in the question preview pop-up window to make the 'Fill in correct response' button work. It returns the behaviour data that needs to be submitted as part of a correct response. Obviously, this is then combined with data from the question type.

For example, from qbehaviour_deferredcbm

   public function get_correct_response() {
       if ($this->qa->get_state()->is_active()) {
           return array('certainty' => question_cbm::HIGH);
       }
       return array();
   }

get_question_summary()

This method returns a plain-text summary of the question that was asked. This is used, for example, by the quiz Responses report. Normally the base-class implementation that just delegates to the question type is all you need.

   public function get_question_summary() {
       return $this->question->get_question_summary();
   }

get_right_answer_summary()

This should return a plain-text summary of what the right answer to the question is. For example, in the base class:

   public function get_right_answer_summary() {
       return $this->question->get_right_answer_summary();
   }

and in qbehaviour_deferredcbm:

   public function get_right_answer_summary() {
       $summary = parent::get_right_answer_summary();
       return $summary . ' [' . question_cbm::get_string(question_cbm::HIGH) . ']';
   }

(Hmm. I probably should not be concatenating language strings like that.)

classify_response()

This is used by the response analysis in the quiz statistics report: At the moment, no behaviours do more than delegate to the question type:

   public function classify_response() {
       return $this->question->classify_response($this->qa->get_last_qt_data());
   }

summarise_action()

This is used by the response history table that is shown underneath questions on the quiz review page, in the Action column. It should return a plain text representation of the action the student took in this step. For example, from qbehaviour_deferredfeedback:

   public function summarise_action(question_attempt_step $step) {
       if ($step->has_behaviour_var('comment')) {
           return $this->summarise_manual_comment($step);
       } else if ($step->has_behaviour_var('finish')) {
           return $this->summarise_finish($step);
       } else {
           return $this->summarise_save($step);
       }
   }

from question_behaviour_with_save

   public function summarise_save(question_attempt_step $step) {
       $data = $step->get_submitted_data();
       if (empty($data)) {
           return $this->summarise_start($step);
       }
       return get_string('saved', 'question',
               $this->question->summarise_response($step->get_qt_data()));
   }

and from the base class:

   public function summarise_start($step) {
       return get_string('started', 'question');
   }
   public function summarise_finish($step) {
       return get_string('attemptfinished', 'question');
   }


Renderer class

Renderers are all about generating HTML output. The overall output of questions is controlled by the core_question_renderer::question(...) method, which in turn calls other methods of the core question renderer, the behaviour renderer and the question type renderer, to generate the various bits of output. See this overview of the parts of a question.

Note that most of the methods take a question_display_options object that specifies what should and should not be visible. For example, should feedback, or marks information, be displayed. It is important that your renderer respects these options.

Once again, in what follows, I only list the methods your are most likely to want to override.


controls

This is a block of output in the question formulation area. It goes after all the question type output in this area.

Two examples. This method is used by the interactive behaviour to show the Submit button (if the question is in an appropriate state). It is used by the CBM behaviour to show the certainty choices. Here is the interactive behaviour code:

   public function controls(question_attempt $qa, question_display_options $options) {
       return $this->submit_button($qa, $options);
   }

submit_button is a helper method provided by the base class.


feedback

This is a block of output in the feedback area of the question. It is used, for example, in interactive behaviour, to show the Try again button, and in the CBM behaviour to output some text that explains how the score was adjusted. Here is the interactive behaviour code:

   public function feedback(question_attempt $qa, question_display_options $options) {
       if (!$qa->get_state()->is_active() || !$options->readonly) {
           return ;
       }
       // Set up $attributes array ...
       $output = html_writer::empty_tag('input', $attributes);
       if (empty($attributes['disabled'])) {
           $output .= print_js_call('question_init_submit_button',
                   array($attributes['id'], $qa->get_slot()), true);
       }
       return $output;

Note that we set up a call to the JavaScript function question_init_submit_button, passing the button id, and the question slot number. You should do that for any button you output that will cause the question to be submitted.


Question behaviour type class

The behaviour type class provides information about the behaviour in general. In contrast the behaviour class handles the behaviour applied to one particular question_attempt.

Note that, before Moodle 2.6, there was not a separate behaviour type class. These methods either did not exist, or were static methods on the behaviour class.


Methods related to the settings UI

An activity using questions needs to provide various options to the user that relate to behaviours. For example, on the quiz settings form, we need to display a menu How questions behave where the teacher can choose which behaviour they want. The quiz does this by calling question_engine::get_behaviour_options(). Similarly, when selecting the display options Users may review, During the attempt, not all options are relevant to all behaviours, The quiz can get the appropriate dependencies by calling question_engine::get_behaviour_unused_display_options(). Naturally, the question engine needs to consult the behaviours to implement these methods This is done using the following two things.

is_archetypal()

Some behaviours should be offered as options in the user interface. For example CBM, interactive, and so on. Others are for internal use. For example qbehaviour_informationitem, qbehaviour_interactiveadaptedformyqtype. This is indicated by the return value of the is_archetypal method. True means that this behaviour will be offered as an option in the UI. This defaults to false in the base class, so you need to override the method if you want your behaviour to be an option that users select.

   public function is_archetypal() {
       return true;
   }

get_unused_display_options()

This method should return a list of question_display_options field that are irrelevant before question_usage_by_activity::finish_all_questions() is called.

For example, from qbehaviour_deferredfeedback_type:

   public static function get_unused_display_options() {
       return array('correctness', 'marks', 'specificfeedback',
               'generalfeedback', 'rightanswer');
   }


Things related to reporting

adjust_random_guess_score()

Questions are able to report an estimate of the mark a student would get by guessing. Naturally, question behaviours that modify the marks need to be able to manipulate that. For example in qbehaviour_deferredcbm_type:

   public static function adjust_random_guess_score($fraction) {
       return question_cbm::adjust_fraction($fraction, question_cbm::default_certainty());
   }

summarise_usage()

This method is an opportunity for the behaviour to provide some overall summary information about all the question_attempts in a question_usage_by_activity. An example of where this is done is in qbehaviour_deferredcbm_type. To see one place (currently the only place) where that is used, have a look at the quiz review page for a quiz attempt using one of the CBM behaviours.

Unit tests

For a general introduction, see the Moodle unit testing documentation.

Most of the behaviours are currently tested by working one or more test questions through a sequence of example inputs and testing the results. To do this they sub-class qbehaviour_walkthrough_test_base which provides a lot of helper methods to facilitate writing tests like that.

The best way to start is probably to look at the tests for some of the standard behaviours. For example qbehaviour_deferredfeedback_walkthrough_test::test_deferredfeedback_feedback_multichoice_single

   public function test_deferredfeedback_feedback_multichoice_single() {
       // Create a true-false question with correct answer true.
       $mc = test_question_maker::make_a_multichoice_single_question();
       $rightindex = $this->get_mc_right_answer_index($mc);
       // Start a deferred feedback attempt and add the question to it.
       $this->start_attempt_at_question($mc, 'deferredfeedback', 3);
       // Verify.
       $this->check_current_state(question_state::$todo);
       $this->check_current_mark(null);
       $this->check_current_output(
               $this->get_contains_question_text_expectation($mc),
               $this->get_contains_mc_radio_expectation(0, true, false),
               $this->get_contains_mc_radio_expectation(1, true, false),
               $this->get_contains_mc_radio_expectation(2, true, false),
               $this->get_does_not_contain_feedback_expectation());
       // Process the data extracted for this question.
       $this->process_submission(array('answer' => $rightindex));
       // Verify.
       $this->check_current_state(question_state::$complete);
       $this->check_current_mark(null);
       $this->check_current_output(
               $this->get_contains_mc_radio_expectation($rightindex, true, true),
               $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, true, false),
               $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, true, false),
               $this->get_does_not_contain_correctness_expectation(),
               $this->get_does_not_contain_feedback_expectation());
       // Finish the attempt.
       $this->quba->finish_all_questions();
       // Verify.
       $this->check_current_state(question_state::$gradedright);
       $this->check_current_mark(3);
       $this->check_current_output(
               $this->get_contains_mc_radio_expectation($rightindex, false, true),
               $this->get_contains_correct_expectation());
       // Now change the correct answer to the question, and regrade.
       $mc->answers[13]->fraction = -0.33333333;
       $mc->answers[14]->fraction = 1;
       $this->quba->regrade_all_questions();
       // Verify.
       $this->check_current_state(question_state::$gradedwrong);
       $this->check_current_mark(-1);
       $this->check_current_output(
               $this->get_contains_incorrect_expectation());
   }

As you can see, the helper methods hide a lot of the details, like starting an attempt, but if you browse the code, you will be able to see exactly what those (quite simple) helper functions do. I was going to summarise here what the test does, but I think the comments in the code already do that well enough.

See also

In the next section, Developing a Question Type I describe what a developer will need to do to create a Question Type plugin for the new system.

  • The PHP documenter comments that explain the purposes of every method in the question engine code.
  • Back to Question Engine 2