Note:

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

Using the question engine from module: Difference between revisions

From MoodleDocs
Line 136: Line 136:
Once again, the example here are inspired by real code from the quiz module, in this case <tt>mod/quiz/processattempt.php</tt>, but simplified to remove mention of the <tt>quiz_attempt</tt> class.
Once again, the example here are inspired by real code from the quiz module, in this case <tt>mod/quiz/processattempt.php</tt>, but simplified to remove mention of the <tt>quiz_attempt</tt> class.


It is really quite simple:


===The simple case===
Normally, you just need to do something like:
<code php>
<code php>
$timenow = time();
$timenow = time();
Line 160: Line 162:
update_record('quiz_attempts', $this->attempt);
update_record('quiz_attempts', $this->attempt);
</code>
</code>
===More selective processing===
If you want something other than the default processing, you can use <tt>$submitteddata = $quba->extract_responses($slot); $quba->process_action($slot, $submitteddata);</tt>.


===Handling scrollpos===
===Handling scrollpos===

Revision as of 14:08, 2 December 2010

This page explains how to use the new Moodle question engine in an activity module you are developing.

Previous section: Developing a Question Type

The first example of a module that uses the question engine is the quiz module. Looking at how the quiz code works will let you see a real working example of what explained on this page.

Note that all the question engine code has extensive PHP documentor 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.


Overview

The key class you will be working with is question_usage_by_activity. This tracks an attempt at some questions, for example a quiz attempt. The usage should provide all the interface methods you need to do things with the question engine. You should rarely need to dive deeper into the inner workings of the question engine than this.

A usage is a collection of question_attempt objects. A question attempt represents the state of the user's attempt at one question within the usage. When you add a question to the usage, you get back a slot number. This server to index that question within the attempt. Using slot numbers like this means that the same question can be added more than once to the same usage. A lot of the usage API calls take this slot number to indicate which question attempt to operate on. Other API methods let you perform batch actions on all the question attempts within the usage.


Starting an attempt at some questions

The example code in this section all came from mod/quiz/startattempt.php.

Creating the usage

To create a new usage, ask the question engine: $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); $quba->set_preferred_behaviour($quiz->preferredbehaviour);

You will see that the constructor takes the plugin name (using the Moodle 2.0 'frankenstyle' naming convention) and the context that owns the usage. Usages should be cleaned up automatically when a context is deleted or a plugin un-installed. You also have to tell the usage which behaviour should be used when creating the attempt at each question.

Adding questions

Having got a usage, you will probably then want to add some questions to it. The question data you loaded from the database (probably using the get_question_options function) come in a form suitable for data handling like import/export, backup/restore, and so on. We need to convert that to an question_definition object. That can be done using the question_bank::make_question method. Alternatively, you could just use the question_bank::load_question method, but that can only load one question at a time.

Once you have a question_definition object, you add it to the usage by calling the add_question method passing the question definition, and also the score you want this question marked out of. You get back the slot number that has been assigned to this question. The slot numbers are allocated in order, starting from 1. You can rely on that numbering convention. For example, the quiz module uses the slot numbers to make sure the grade for each question lines up properly in the reports.

Here is the applicable code from the quiz module foreach ($quizobj->get_questions() as $i => $questiondata) {

   $question = question_bank::make_question($questiondata);
   $idstoslots[$i] = $quba->add_question($question, $questiondata->maxmark);

}

Starting the question attempts

The usage does not do much when the question is added. Before the user can start interacting with the question, you have to start it. You can either do that in bulk: $quba->start_all_questions(); or you can start just one particular question using the start_question($slot) method.

Saving the usage

So far, all these method calls have just built up a data structure in memory. If you want to keep it, you have to save it in the database. Fortunately, that is easy: question_engine::save_questions_usage_by_activity($quba);

The usage is stored as a row in the question_usages table (plus rows in other tables). To get the id of the usage, call $quba->get_id() after you have saved it.


Displaying questions

Suppose that you wish to display a page that contains several questions from the usage. The questions to display will be the ones whose slot numbers are in an array called $slotsonpage.

The example code in this section is derived from code used by mod/quiz/attempt.php. However, the quiz code is split between mod/quiz/attempt.php and the quiz_attempt class in mod/quiz/attemptlib.php. That separation means that if I used the real quiz code in this tutorial, it would be unnecessarily hard to understand. Instead, I have simplified the examples below. If you are interested, you should be able to match up what you see below with the real quiz code.

Loading the usage

At the end of the last section, you had just saved the usage to the database. To display the questions as part of a different script, they must be loaded from the database again. Once again you ask the question engine to do this: $quba = question_engine::load_questions_usage_by_activity($usageid);

The id that you pass to this method is the id you got from $quba->get_id() after saving the usage.

Before print_header

One subtlety is that different question types may rely on JavaScript and CSS to work. In order for this, particularly the links to the CSS, to be included in the page you are outputting, you must call the render_question_head_html method for each question you will be outputting on this page. You need to collect all the HTML returned, and also the return value from question_engine::initialise_js() method, and pass all that as the $meta parameter to print_header.

$meta = ; foreach ($slotsonpage as $slot) {

   $meta .= $quba->render_question_head_html($slot);

} $meta .= question_engine::initialise_js(); print_header(/* ... lots of args ... */, $meta);

The question form

Within the page, all the questions have to be wrapped in a HTML <form> tag. This form needs to POST to a scripts that processes the submitted data (see below). echo '<form id="responseform" method="post" action="' . $processurl .

'" enctype="multipart/form-data" accept-charset="utf-8">', "\n

\n";

print_js_call('question_init_form', array('responseform'));

echo '<input type="hidden" name="slots" value="' . implode(',', $slotsonpage) . "\" />\n"; echo '<input type="hidden" name="scrollpos" value="" />';

The enctype and accept-charset parameters are important. Please copy them. The form must have an id, and you must then write some JavaScript that passes that id to to the question_init_form function. That function is defined in question/qengine.js if you want to find out what it does and why it is important.

You should output a hidden form field slots that contains a comma-separated list of the slot numbers that appear on this page. That is not absolutely necessary, but it will make processing the form submission much more efficient.

Another nicety is the scrollpos hidden field. Some question behaviours, for example the interactive behaviour, have submit buttons that do something to the question. When the user clicks this button, it is nice if when the page re-displays, it is scrolled to exactly the same place where it was. The question engine has JavaScript which, if it finds a scrollpos hidden field, will automatically save the scroll position to it when a button is clicked, and cause the page to scroll to that position when it reloads.


Outputting the questions

Finally, you can actually print the questions. Well, there is one more thing to take care of first. To control how the question appears, you must prepare a question_display_options object that describes what should be visible. For example: $options = new question_display_options(); $options->marks = question_display_options::MAX_ONLY; $options->markdp = 2; // Display marks to 2 decimal places. $options->feedback = question_display_options::VISIBLE; $options->generalfeedback = question_display_options::HIDDEN; // etc.

(In the quiz code, the display options actually come from the quiz settings.) Then you really are ready to output the questions foreach ($slotsonpage as $displaynumber => $slot) {

   echo $quba->render_question($slot, $options, $displaynumber);

}

Here $displaynumber is the 'question number' you want printed next to each question. This is not necessarily the same as the slot number. For example, in the quiz, Description 'questions' are not numbered, although they take up a slot. If you don't pass in a number to display, then the question is just printed without one.


Processing responses

Once again, the example here are inspired by real code from the quiz module, in this case mod/quiz/processattempt.php, but simplified to remove mention of the quiz_attempt class.


The simple case

Normally, you just need to do something like: $timenow = time(); begine_sql();

$quba = question_engine::load_questions_usage_by_activity($usageid); $quba->process_all_actions($timenow); question_engine::save_questions_usage_by_activity($quba);

commit_sql();

Behind the scenes there is, of course, a lot going on, including the use of the slots parameter to control which questions are processed.

There may also be additional work you need to do. For example, in the quiz (before the commit_sql() line) there is:

if ($attempt->timefinish) {

   $attempt->sumgrades = $this->quba->get_total_mark();

} $attempt->timemodified = $timenow; update_record('quiz_attempts', $this->attempt);

More selective processing

If you want something other than the default processing, you can use $submitteddata = $quba->extract_responses($slot); $quba->process_action($slot, $submitteddata);.

Handling scrollpos

You will remember that when we printed the questions, we created a scrollpos hidden field. Presumably, after you have processed the submission, you will redirect the user back to a page to display the questions again. In that case, you need to get the scrollpos parameter, and add it on to the URL you redirect to:

// With the other required/optional_param calls at the start of processattempt.php. $scrollpos = optional_param('scrollpos', , PARAM_RAW);

// ... a bit later in the file $nexturl = /* something */; if ($scrollpos !== ) {

   $nexturl .= '&scrollpos=' . ((int) $scrollpos);

}

// ... after all the processing is done. redirect($nexturl);

Reports

TODO


See also

In the next section, Implementation plan outlines how I will implement this proposal.

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