http://docs.moodle.org/en/Development:Plans_for_enhancing_import/export_in_questiontype_plugins
Please excuse the snappy title
Yeh - I struggled with those method names. The import_from is a very good point though. I'll change that.
What about simply(ish)...
import_from_questiontype_plugin()
export_to_questiontype_plugin()
??
I think that the question type import_from_xml and export_to_xml functions sound great.
I don't know that much about import/export, so maybe these aren't problems, but I had a couple questions:
1) Would import_plugin (whatever it gets called) re-try all the core qtypes? I thought that core types were handled separately and if it got to import_plugin, we would know that the core qtypes have all failed (which is why it's import_plugin, not import_qtypes). Is this not the case? (I guess core qtypes wouldn't have the import function, so they'd get skipped anyway. Maybe this isn't really a problem.)
2) I'm a little concerned about how failure in the various cases can be detected consistently, especially if one question type is an extension of another. What if one question type finds all the subfields it's expecting and thinks it's successful, but another question type is really the right one and handles all the subfields?
Maybe import_plugin could get named something like attempt_import_for_plugin_qtypes?
Hi Howard,
I have started to play with the new import/export format for plugin question types in HEAD, with my REGEXP question type. So far I have implemented the export_to_xml method and am glad to report that it was quite easy to implement and that it does work fine! In file question/type/regexp/questiontype.php, inside class question_regexp_qtype extends default_questiontype {} I simply added the following:
function export_to_xml( $question, $format, $extra=null ) { $expout = " <usehint>{$question->options->usehint}</usehint>\n "; foreach($question->options->answers as $answer) { $percent = 100 * $answer->fraction; $expout .= " <answer fraction=\"$percent\">\n"; $expout .= $this->writetext( $answer->answer,3,false ); $expout .= " <feedback>\n"; $expout .= $this->writetext( $answer->feedback,4,false ); $expout .= " </feedback>\n"; $expout .= " </answer>\n"; } return $expout; }
I also needed to copy from question/format/xml/format.php function writetext( $raw, $ilev=0, $short=true) {} because it seems I cannot link to that file from my own question type, but it's only a few lines.
I will now work on the import_from_xml method, and test backup/restore and then report here. I hope the recent changes by Jamie Pratt to the question bank will not interfere too much with your import/export methods. If you want to take a look at my REGEXP question type you can download it (version 1.8 is OK for 1.9/HEAD) from the plugins download here.
Many thanks for making these methods available to "plugin question types".
Joseph
Yes it does! Thanks Tim. Does this mean that the function function writetext( $raw, $ilev=0, $short=true) {} in question/format/xml/format.php is no longer needed?
Joseph
Another possibility is as follows. In question/import.php line 71 if I replace:
$validcats = question_category_options( $cmid, false, true );
with
$validcats = question_category_options( $courseid, false, true );
everything is working fine...
what is $cmid doing in 1.9 instead of $courseid (as in 1.8)?
Joseph
Howard, if I compare question/import.php and question/export.php in 1.9 latest version, both have this line:
list($thispageurl, $courseid, $cmid, $cm, $module, $pagevars) = question_edit_setup();
If I try to echo or notify $thispageurl I get a blank page and no error message. What on earth is this $thispageurl meant to be like?
Anyway, in both question/import.php and question/export.php, the value of $cmid is 0. In export.php you do not use $cmid at all when testing the validity of category, so export works fine:
// check category is valid
$validcats = question_category_options( $course->id, true, false );
if (!array_key_exists( $categoryid, $validcats)) {
print_error( 'invalidcategory','quiz' );
}
In import.php, however, as mentioned in my previous post, you do use $cmid: // check category is valid (against THIS courseid, before we change it)
$validcats = question_category_options( $cmid, false, true );
if (!array_key_exists( $params->category, $validcats )) {
print_error( 'invalidcategory', 'quiz' );
}
And of course, because $cmid = 0, the category is never valid, hence the error. Although I do not understand the relationship between this error and the "library for constructing URLs" mentioned in your post, I suggest doing the simple replacement mentioned in my post in import.php, i.e. using $courseid instead of $cmid in the category validity test. What do you think?
Joseph
PS.- Better swear at your computer than at real people. He/she?/it won't mind. Keep up the good work.
Hmm... Not for me... I am conducting my tests on a clean new 1.9 install, where I create a couple of courses, a couple of quizzes in each, a couple of categories and a couple of questions in each category.
In import.php, right after
list($thispageurl, $courseid, $cmid, $cm, $module, $pagevars) = question_edit_setup(false, false);I add this line:
echo("HERE IS IMPORT.PHP courseid = $courseid; cmid = $cmid");In course ID 2, if I go directly to the questions bank and click on Import, then import.php echoes "HERE IS IMPORT PHP courseid = 2; cmid = 0".
In course ID 2, if I edit quiz ID 6, and, on the editing quiz screen I click on Import, , then import.php echoes "HERE IS IMPORT PHP courseid = 2; cmid = 6".
As far as I can gather, $cmid contains the id of the quiz being currently edited, not the course id. So in import.php, the test $validcats = question_category_options( $cmid, false, true ) can't return a correct list of categories in the actual course. I maintain that it does work however with $validcats = question_category_options( $courseid, false, true );
Can anyone confirm this?
Joseph
The matching question probably doesn't need it (but of course still be fine for testing), since there is already import/export for it. I'll take a stab at the ordering question.
Another question type that works with HEAD is the file response question. It only has one extra database table, so the XML would be relatively simple.
I've attached the updated ordering question with XML import/export added. Because it is very similar to the matching question, it was very easy to do.
One question: I added a sanity check to make sure the answers are indeed a sequence of numbers. If the check fails, I'd rather have a different error message than "order is not supported in xml", but I wasn't sure what the best way to handle that would be. Would it be okay to add an error() instead of return false?
I thought that some people were still using GIFT to write and import questions since it's relatively simple to do in a text editor. The only proposal I've seen for ordering questions was for an ordering question written for 1.6 (post here, but I haven't actually tried to use it) and it was basically:
Question text. {>Item 1, Item 2, Item 3}
I decided to use the following format. Since '>' is a special character, it should be escaped if it's used as part of the text somewhere. I check for the '>' at the very beginning and for at least three items, which are separated by '='.
::Ordering Question Name::This is an ordering question. {> =aaa =bbb =ccc }
I've attached the updated ordering question so you can try it out. I found a small bug in the updated gift/format.php due to the moved check, which is reported here. The existing question types fail right and left as it stands, but the plugin type works!
function import_from_xml($data, $question, $format, $extra=null) { // get common parts $question = $format->import_headers($data); // header parts particular to matching $question->qtype = 'renderedmatch'; $question->shuffleanswers = $format->getpath( $data, array( '#','shuffleanswers',0,'#' ), 1 ); // get subquestions $subquestions = $data['#']['subquestion']; $question->subquestions = array(); $question->subanswers = array(); // run through subquestions foreach ($subquestions as $subquestion) { $question->subquestions[] = $format->getpath( $subquestion, array('#','text',0,'#'), '', true ); $question->subanswers[] = $format->getpath( $subquestion, array('#','answer',0,'#','text',0,'#'), '', true); } return $question; } function export_to_xml($question, $format, $extra=null) { $expout = ''; foreach($question->options->subquestions as $subquestion) { $expout .= "<subquestion>\n"; $expout .= $format->writetext( $subquestion->questiontext ); $expout .= "<answer>".$format->writetext( $subquestion->answertext )."</answer>\n"; $expout .= "</subquestion>\n"; } return $expout; }It works but I want to add a check to the import function to be sure to only parse the question if it is a renderedmatch question. What is the better vay to do this ? Can I test $question because I think qtype is already parsed when my import function is called ? Am I right ? Do you see a better way ?