Note:

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

Import/export for questiontype plugins: Difference between revisions

From MoodleDocs
 
(28 intermediate revisions by 3 users not shown)
Line 1: Line 1:
''This is a proposal currently in development''
{{Questiontype_developer_docs}}


== Motivation ==
{{Moodle 1.9}}While it is easy enough for import and export formats to support the core question types, it is harder for them to handle any third-party question types that may have been installed. This page describes the mechanism that allows third-party question types to participate in import and export, for formats that support this mechanism. Currently, that means Moodle XML, GIFT and QTI2 formats. QTI2 support is from 1.9.6.


While it is easy enough to add import and export capability for core questiontypes there is no clear way to allow contributed questiontype authors to enhance their plugin with import and export code. This is an outline proposal for including hooks for that functionality. It is primarily aimed at the Moodle XML format (the most comprehensive import/export format) but with a definite goal not to exclude any other format that may wish to use it.
This mechanism was implemented in Moodle 1.9. and back-ported to Moodle 1.8.3+.


== New Methods in Questiontype Class ==
== New methods in questiontype class ==


The main mechanism for this process will be new methods for import and export to be added to the questiontype class. There would be a discrete method for each supported format type. Note that these will not (and should not) be implemented for core question types.
The main mechanism for this process will be new methods for import and export to be added to the questiontype class. There would be a discrete method for each supported format type. Note that these will not (and should not) be implemented for core question types.
Line 11: Line 11:
=== Import ===
=== Import ===


Implement a method for each import format that you wish to handle in the questiontype class. The naming convention is '''import_to_format''' (for example '''import_to_gift''', '''import_to_xml'''). The method is responsible for checking the supplied data and returning a ''question'' object suitable for the ''save_question_options()'' method in the appropriate questiontype class. The method '''must''' check in particular that the data is correct for the questiontype and return a '''false''' if it is not. This is because import methods will be polled to find one to handle the data, as the correct question type cannot always be established in the general case.
Implement a method for each import format that you wish to handle in the questiontype class. The naming convention is '''import_from_format''' (for example '''import_from_gift''', '''import_from_xml'''). The method is responsible for checking the supplied data and returning a ''question'' object suitable for the ''save_question_options()'' method in the appropriate questiontype class. The method '''must''' check in particular that the data is correct for the questiontype and return a '''false''' if it is not. This is because import methods will be polled to find one to handle the data, as the correct question type cannot always be established in the general case.


The template for one of these methods would be (using '''import_to_xml''' as an example) as follows:
'''Warning: It is vital that the function makes some sort of check that the question is valid (for this qtype) and returns false if it is not.'''
 
The template for one of these methods would be (using '''import_from_xml''' as an example) as follows:


     /**
     /**
Line 23: Line 25:
     * @return object question object suitable for save_options() call or false if cannot handle
     * @return object question object suitable for save_options() call or false if cannot handle
     */
     */
     function import_to_xml( $data, $question, $format, $extra=null ) {
     function import_from_xml( $data, $question, $format, $extra=null ) {
         if ($data->.......
         if ($data->.......
             ....
             ....
Line 55: Line 57:
     }
     }


== New methods is qformat_default class (format.php) ==
The chances are that the detailed coding will be similar to an existing question type. Study the code in question/format/xml/format.php to see how it works.


Two new methods (one for import and one for export) will be provided for the format plugin to call to handle situations that they cannot handle themselves. These would be called from the format classes in an appropriate place (ie, when they "decide" that they cannot handle the question internally)
== New methods in qformat_default class (format.php) ==
 
You do not need to read this section to understand adding this functionality to question type plugins. It is included for completeness/history.
 
Two methods (one for import and one for export) are provided for the format plugin to call to handle situations that they cannot handle themselves. These are called from the format classes (the import/export plugins) in an appropriate place (ie, when they "decide" that they cannot handle the question internally)


=== Import ===
=== Import ===
Line 67: Line 73:
     * @param data mixed the segment of data containing the question
     * @param data mixed the segment of data containing the question
     * @param question object question object processed (so far) by standard import code
     * @param question object question object processed (so far) by standard import code
    * @param format object the format object so that helper methods can be used (in particular error() )
     * @param extra mixed any additional format specific data that may be passed by the format (see format code for info)
     * @param extra mixed any additional format specific data that may be passed by the format (see format code for info)
     * @return object question object suitable for save_options() call or false if cannot handle
     * @return object question object suitable for save_options() call or false if cannot handle
     */
     */
     function import_plugin( $data, $question, $format, $extra=null ) {
     function try_importing_using_qtypes( $data, $question, $extra=null ) {
         global $QTYPES;
         global $QTYPES;
         $formatname = substr(get_class( $this ), length('qformat_'));
         $formatname = substr(get_class( $this ), length('qformat_'));
         $methodname = "import_to_$formatname";
         $methodname = "import_from_$formatname";
         foreach ($QTYPES as $qtype) {
         foreach ($QTYPES as $qtype) {
             if (method_exists( $qtype, $methodname )) {
             if (method_exists( $qtype, $methodname )) {
                 if ($question = $qtype->$methodname( $data, $question, $format, $extra )) {
                 if ($question = $qtype->$methodname( $data, $question, $this, $extra )) {
                     return $question;
                     return $question;
                 }
                 }
Line 93: Line 98:
     * @param name questiontype name
     * @param name questiontype name
     * @param question object the question object
     * @param question object the question object
    * @param format object the format object so that helper methods can be used
     * @param extra mixed any additional format specific data that may be passed by the format (see format code for info)
     * @param extra mixed any additional format specific data that may be passed by the format (see format code for info)
     * @return string the data to append to the output buffer or false if error
     * @return string the data to append to the output buffer or false if error
     */
     */
     function export_plugin( $name, $question, $format, $extra=null ) {
     function try_exporting_using_qtypes( $name, $question, $extra=null ) {
         global $QTYPES;
         global $QTYPES;
         $formatname = substr(get_class( $this ), length('qformat_'));
         $formatname = substr(get_class( $this ), length('qformat_'));
Line 104: Line 108:
             $qtype = $QTYPES[ $name ];
             $qtype = $QTYPES[ $name ];
             if (method_exists( $qtype, $methodname )) {
             if (method_exists( $qtype, $methodname )) {
                 if ( $data = $qtype->$methodname( $question, $format, $extra )) {
                 if ( $data = $qtype->$methodname( $question, $this, $extra )) {
                     return $data;
                     return $data;
                 }
                 }
Line 112: Line 116:
     }
     }


== Error handling ==
The recommended way for import/export formats to report a syntax error is to call the error() method within the format class. The format object is passed to the questiontype method so that $format->error can be called.
== Notes for individual formats ==
This is currently only implemented for the XML and GIFT formats. These are specific notes about how they are implemented and what to expect in the passed parameters.


=== Moodle XML Format ===


== Error handling ==
==== Import ====


The recommended way for import/export formats to report a syntax error is to call the error() method within the format class. The format object is passed to the questiontype method so that $format->error can be called.
For import, try_exporting_using_qtype() is called if the questiontype (as read from the xml file) is not recognised as a core type. The parameters will be set as follows:


== Alternate proposal from Tim ==
* $data = the question object
* $question = null
* $extra = null


I don't think that this proposal is a natural way to do things. If I was writing a 3rd-party question type, then I would expect to have to write some functions of the form
Note that only the question object is passed. The first thing you probably need to do is to test for the questiontype name, thus...


<pre>
    $qtype = $data['@']['type'];
function export_to_gift( $formatversion, $question, $format ) { ... }
function export_to_moodlexml( $formatversion, $question, $format ) { ... }
//etc.
</pre>


Also, I have often felt that most of the existing export format code (some of it very old) is not structured as well as possible. I feel that in, say qformat_gift, there should just be a bunch of functions
You should test this to see if you want to handle it and return '''false''' if not. It is important that you do this step!


<pre>
Next you will want to parse the headers, thus...
function export_shortanswer( $formatversion, $question, $format ) { ... }
function export_numerical( $formatversion, $question, $format ) { ... }
function export_multiplechoice( $formatversion, $question, $format ) { ... }
//etc.
</pre>


which probably all do some qtype-specific work, and then call a helper method to do the bit that is common to all question types. The parameters and return types for these methods would be as Howard proposes above.
    $question = $format->import_headers( $data );


So, what I think this suggests is that for each question type, and each format, there is some function somewhere that does the import or export (or there isn't because that type is not supported for that format).  
The remainder will be specific to your question type plugin.


So each format class (which is only loaded when it is used) should, when it is created, build up two arrays, export_functions, and import_functions. The keys in these arrays would be question type names, and the values would be [http://uk2.php.net/manual/en/language.pseudo-types.php#language.types.callback callbacks] - that is, basically the name of a function or method.
==== Export ====


Howard correctly points out the issues with question type names on import. For the import_functions array, the keys would be the question type names as used in the import file. This assumes that for all formats we might want to support, it is possible for the main question type class to inspect the data for a question, and get some sort of string that can be considered a 'question type'. I'm not sure if this is true. (QTI is the format I am thinking of as a potential problem, but I know of no definite plans to support that in full generality, and it would not be impossible to do even here.)
For export your 'export_to_xml' method will be called correctly if it exists and an instance of the question type exists. The parameters are set as follows:


To set up these arrays, there would be a function in the qformat base class, that would fill in the arrays for the types it supports, and then polls each question type for more information. Something like:
* $question = the question object
* $format = reference to the qformat_xml object. You can use this to call methods on that object if required.  
* $extra = is not used


<pre>
'''NOTE:''' The 'headers' part of the question (the name and question text etc.) will already have been generated prior to this function being called so you do not have to do that.
// In qformat_default
    function name() {
        error('This function must be overriden to return the export format name.');
    }
    var $export_methods = array();
    var $import_methods = array();
    function init() {
        global $QTYPES;
        $methods = get_class_methods($this);
        foreach ($methods as $method) {
            if (strpos($method, 'import_') === 0) {
                $this->import_methods[substr($method, 7)] = array($this, $method);
            }
            if (strpos($method, 'export_') === 0) {
                $this->export_methods[substr($method, 7)] = array($this, $method);
            }
        }
        foreach ($QTYPES as $qtype) {
            $import_methods = $qtype->get_import_methods($this->name());
            foreach ($import_methods as $typename => $method) {
                if (!isset($this->import_methods[$typename])) {
                    $this->import_methods[$typename] = $method;
                }
            }
            if (!isset($this->export_methods[$typename])) {
                if ($import_method = $qtype->get_export_method($this->name())) {
                    $this->export_methods[$qtype->name()] = $import_method;
                }
            }
        }
    }


// In default_questiontype
=== GIFT Format ===
    function get_export_method($formatname) {
        $methodname = 'export_' . $formatname;
        if (method_exists($this, $methodname)) {
            return array($this, $methodname);
        }
        return false;
    }
    function get_import_methods($formatname) {
        $methodname = 'import_' . $formatname;
        if (method_exists($this, $methodname)) {
            return array(
                $this->name() => array($this, $methodname)
            );
        }
        return array();
    }
</pre>


Of course, any of these methods could be overridden by a subclass. In the short term, we might also need to allow some magic value (e.g. '-') in these arrays, instead of a callback, meaning that the particular format class handles this type using the old, differently-structured code, rather than one of the new methods.
For import GIFT is unusual in that the question type cannot be established until the question has been completely parsed. Unlike other formats where try_importing_using_qtypes() is called at the "end" where it has been established that no built in handler exists, GIFT makes this call before built in question handlers are considered. This makes it doubly important that the plugin code checks that the data is appropriate for the qtype and returns '''false''' if not.


Then, the main import and export methods in qformat_default would just need to be (I'm oversimplifying here):
The contents of the parameters are as follows:


<pre>
* $data = the complete text of the question (in a single string)
    function exportprocess() {
* $question = the current question object. You can expect to find the common parts of the question already parsed and included as fields (e.g., name)
        $questions = get_questions_category( $this->category, true );
* $extra = the part of the question between the braces ( {..} ) if it exists (in a single string)
        $expout = '';
        foreach ($questions as $question) {
            if (isset($this->export_functions[$question->type])) {
                $expout .= call_user_func($this->export_functions[$question->type], $version, $question, $this);
            } else {
                // Display some message about unsuported type.
            }
        }
        // Rest of function as before.
    }


    function importprocess() {
For export GIFT is much more as expected. If the questiontype is not handled a call is made to try_exporting_using_qtypes() with the standard parameters.
        $this->openfile();
        $questions = array();
        while ($data = $this->load_data_for_next_question()) {
            $type = $this->deduce_qtype($data);
            if (isset($this->import_functions[$type])) {
                $question = $this->parse_common_parts($data);
                call_user_func($this->import_functions[$type], $version, $type, $data, $question, $this);
                $questions[] = $question;
            } else {
                // Display some message about unsuported type.
            }
        }
        // Rest of function as before.
    }
</pre>


Once all this is in place. All question type authors have to do to support a particular format is write the methods like 'import_gift' and 'export_gift'. That is consistent with my goal of making it as easy as possible to write new question types.
=== QTI2 ===


But to carry this proposal through is a lot of work refactoring the existing import and export classes. Which may be more than Howard bargained for.
The mode of operation and the parameters are exactly the same as the Moodle XML format. QTI support was added leading up to 1.9.6


== Adding Support To Import/Export Plugins ==


(I've just noticed, all the import and export works by doing everything in memory. That is not very scalable, even thought it leads to better error handling. We can save that problem for another day.)
You can, of course, add support for qtype plugins to import/export plugins. You will need to add calls to try_exporting_using_qtypes() and/or try_importing_using_qtypes() as appropriate. Particularly for import, you should consider what is the optimal data to pass as parameters and document that somewhere so qtype plugin writers know what to expect.


== See Also ==
== See Also ==

Latest revision as of 12:39, 25 May 2010

Moodle1.9 While it is easy enough for import and export formats to support the core question types, it is harder for them to handle any third-party question types that may have been installed. This page describes the mechanism that allows third-party question types to participate in import and export, for formats that support this mechanism. Currently, that means Moodle XML, GIFT and QTI2 formats. QTI2 support is from 1.9.6.

This mechanism was implemented in Moodle 1.9. and back-ported to Moodle 1.8.3+.

New methods in questiontype class

The main mechanism for this process will be new methods for import and export to be added to the questiontype class. There would be a discrete method for each supported format type. Note that these will not (and should not) be implemented for core question types.

Import

Implement a method for each import format that you wish to handle in the questiontype class. The naming convention is import_from_format (for example import_from_gift, import_from_xml). The method is responsible for checking the supplied data and returning a question object suitable for the save_question_options() method in the appropriate questiontype class. The method must check in particular that the data is correct for the questiontype and return a false if it is not. This is because import methods will be polled to find one to handle the data, as the correct question type cannot always be established in the general case.

Warning: It is vital that the function makes some sort of check that the question is valid (for this qtype) and returns false if it is not.

The template for one of these methods would be (using import_from_xml as an example) as follows:

   /**
    * Provide import functionality for xml format
    * @param data mixed the segment of data containing the question
    * @param question object question object processed (so far) by standard import code
    * @param format object the format object so that helper methods can be used (in particular error() )
    * @param extra mixed any additional format specific data that may be passed by the format (see format code for info)
    * @return object question object suitable for save_options() call or false if cannot handle
    */
   function import_from_xml( $data, $question, $format, $extra=null ) {
       if ($data->.......
           ....
           return $question;
       }
       else {
           return false;
       }
   } 

Notes:

  • The $question parameter will contain whatever the core routines have managed to work out so far
  • The $format object is the calling object in case there are any useful methods. In particular this method should call $format->error() to report errors properly

Export

For each export format that your question plugin supports write a method called export_to_format (for example, export_to_gift, export_to_xml). The method simply takes the question object (use print_r() to see what it looks like) and returns a string containing the lines to append to the exported file. Returning false will be regarded as an error condition.

The template for one of these methods (using xml format as an example) could be:

   /**
    * Provide export functionality for xml format
    * @param question object the question object
    * @param format object the format object so that helper methods can be used 
    * @param extra mixed any additional format specific data that may be passed by the format (see format code for info)
    * @return string the data to append to the output buffer or false if error
    */
   function export_to_xml( $question, $format, $extra=null ) {
       ....
       return $xmlquestion;
   }

The chances are that the detailed coding will be similar to an existing question type. Study the code in question/format/xml/format.php to see how it works.

New methods in qformat_default class (format.php)

You do not need to read this section to understand adding this functionality to question type plugins. It is included for completeness/history.

Two methods (one for import and one for export) are provided for the format plugin to call to handle situations that they cannot handle themselves. These are called from the format classes (the import/export plugins) in an appropriate place (ie, when they "decide" that they cannot handle the question internally)

Import

As import cannot, in the general sense, matching unhandled question types to the classes (as names may well differ) the installed questiontypes will be polled to find the (first) one that will handle the data. The import/export format should simply call $this->import_plugin() when it is unable to handle the question data itself. It will either return the $question object or false if it fails. The format class retains the responsibility of reporting unhandled question types. This functionality is optional for import/output formats.

  /**
    * Import for questiontype plugins
    * @param data mixed the segment of data containing the question
    * @param question object question object processed (so far) by standard import code
    * @param extra mixed any additional format specific data that may be passed by the format (see format code for info)
    * @return object question object suitable for save_options() call or false if cannot handle
    */
   function try_importing_using_qtypes( $data, $question, $extra=null ) {
       global $QTYPES;
       $formatname = substr(get_class( $this ), length('qformat_'));
       $methodname = "import_from_$formatname";
       foreach ($QTYPES as $qtype) {
           if (method_exists( $qtype, $methodname )) {
               if ($question = $qtype->$methodname( $data, $question, $this, $extra )) {
                   return $question;
               }
           }
       }
       return false;
   }

Export

The export routine simpler, as the questiontype name will always be known. If the export routine encounters a question type that it cannot handle it should call $this->export_plugin(). If the question is handled the function will return a string containing the exported data. If it cannot be handled the function returns false.

   /**
    * Provide export functionality for plugin questiontypes
    * @param name questiontype name
    * @param question object the question object
    * @param extra mixed any additional format specific data that may be passed by the format (see format code for info)
    * @return string the data to append to the output buffer or false if error
    */
   function try_exporting_using_qtypes( $name, $question, $extra=null ) {
       global $QTYPES;
       $formatname = substr(get_class( $this ), length('qformat_'));
       $methodname = "export_to_$formatname";
       if (array_key_exists( $name, $QTYPES )) {
           $qtype = $QTYPES[ $name ];
           if (method_exists( $qtype, $methodname )) {
               if ( $data = $qtype->$methodname( $question, $this, $extra )) {
                   return $data;
               }
           }
       }
       return false;
   }

Error handling

The recommended way for import/export formats to report a syntax error is to call the error() method within the format class. The format object is passed to the questiontype method so that $format->error can be called.

Notes for individual formats

This is currently only implemented for the XML and GIFT formats. These are specific notes about how they are implemented and what to expect in the passed parameters.

Moodle XML Format

Import

For import, try_exporting_using_qtype() is called if the questiontype (as read from the xml file) is not recognised as a core type. The parameters will be set as follows:

  • $data = the question object
  • $question = null
  • $extra = null

Note that only the question object is passed. The first thing you probably need to do is to test for the questiontype name, thus...

   $qtype = $data['@']['type'];

You should test this to see if you want to handle it and return false if not. It is important that you do this step!

Next you will want to parse the headers, thus...

   $question = $format->import_headers( $data );

The remainder will be specific to your question type plugin.

Export

For export your 'export_to_xml' method will be called correctly if it exists and an instance of the question type exists. The parameters are set as follows:

  • $question = the question object
  • $format = reference to the qformat_xml object. You can use this to call methods on that object if required.
  • $extra = is not used

NOTE: The 'headers' part of the question (the name and question text etc.) will already have been generated prior to this function being called so you do not have to do that.

GIFT Format

For import GIFT is unusual in that the question type cannot be established until the question has been completely parsed. Unlike other formats where try_importing_using_qtypes() is called at the "end" where it has been established that no built in handler exists, GIFT makes this call before built in question handlers are considered. This makes it doubly important that the plugin code checks that the data is appropriate for the qtype and returns false if not.

The contents of the parameters are as follows:

  • $data = the complete text of the question (in a single string)
  • $question = the current question object. You can expect to find the common parts of the question already parsed and included as fields (e.g., name)
  • $extra = the part of the question between the braces ( {..} ) if it exists (in a single string)

For export GIFT is much more as expected. If the questiontype is not handled a call is made to try_exporting_using_qtypes() with the standard parameters.

QTI2

The mode of operation and the parameters are exactly the same as the Moodle XML format. QTI support was added leading up to 1.9.6

Adding Support To Import/Export Plugins

You can, of course, add support for qtype plugins to import/export plugins. You will need to add calls to try_exporting_using_qtypes() and/or try_importing_using_qtypes() as appropriate. Particularly for import, you should consider what is the optimal data to pass as parameters and document that somewhere so qtype plugin writers know what to expect.

See Also