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
Line 87: Line 87:
===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 or 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 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 <tt>qbehaviour_deferredfeedback</tt>:
For example, from <tt>qbehaviour_deferredfeedback</tt>:
Line 103: Line 103:
     }
     }
</code>
</code>


===get_expected_data===
===get_expected_data===

Revision as of 14:46, 10 November 2010

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.
simpletest/...
Contains the unit tests for this behaviour. You are strongly encouraged to write thorough unit tests to ensure the correctness of your behaviour.
lang/en_utf8/qbehaviour_mybehaviour.php
English language strings for this behaviour. You can, of course, include other languages too. The language file must define at least the string giving the model a name, for example $string['mybehaviour'] = '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.

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


Question behaviour class

One instance of this class will be created for each question_attempt 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.


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 model class.

class qbehaviour_mymodel 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 model 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();
   }


IS_ARCHETYPAL

Some behaviours define behaviours that should be offered as options in the user interface. For example CBM, interactive, and so on. Other behaviours are for internal use. For example qbehaviour_informationitem, qbehaviour_interactiveadaptedformyqtype. This is indicated by defining a constant IS_ARCHETYPAL in the class. True means that this behaviour will be offered as an option in the UI.

   const IS_ARCHETYPAL = true; // or false


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_graded_automaticallys. 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_graded_automatically';
   }


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 from 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 array();
   }


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 delegating 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 => 'A manual comment'
This may also have behaviour_var mark and behaviour_var maxmark, if the question is graded. 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::DISCARD 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 (question_state::is_finished($this->qa->get_state())) {
           $options->readonly = true;
       } else {
           $options->hide_all_feedback();
       }
   }


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 model to show the certainty choices.


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 model to output some text that explains how the score was adjusted.


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 subclass 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.


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