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
No edit summary
 
(26 intermediate revisions by 4 users not shown)
Line 4: Line 4:
Previous section: [[Developing_a_Question_Type|Developing a Question Type]]
Previous section: [[Developing_a_Question_Type|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.
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 is explained on this page. Another, much simpler example to look at is <tt>question/preview.php</tt>, and https://github.com/moodleou/moodle-filter_embedquestion/blob/master/showquestion.php is simpler still.


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.
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.
Line 13: Line 13:
The key class you will be working with is <tt>question_usage_by_activity</tt>. 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.
The key class you will be working with is <tt>question_usage_by_activity</tt>. 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 <tt>question_attempt</tt> 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.
A usage is a collection of <tt>question_attempt</tt> 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 serves 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==
==Starting an attempt at some questions==
Line 23: Line 22:


To create a new usage, ask the question engine:
To create a new usage, ask the question engine:
<code php>
<syntaxhighlight lang="php">
$quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
$quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
$quba->set_preferred_behaviour($quiz->preferredbehaviour);
$quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
</code>
</syntaxhighlight>


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.
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.
Line 36: Line 35:
Once you have a <tt>question_definition</tt> object, you add it to the usage by calling the <tt>add_question</tt> 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.
Once you have a <tt>question_definition</tt> object, you add it to the usage by calling the <tt>add_question</tt> 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
Here is the applicable code from the quiz module (some code to handle random questions and question shuffling omitted)
<code php>
<syntaxhighlight lang="php">
foreach ($quizobj->get_questions() as $i => $questiondata) {
$slot = 0;
     $question = question_bank::make_question($questiondata);
$questions = [];
     $idstoslots[$i] = $quba->add_question($question, $questiondata->maxmark);
$maxmark = [];
$page = [];
foreach ($quizobj->get_questions() as $questiondata) {
     $slot += 1;
    $maxmark[$slot] = $questiondata->maxmark;
     $page[$slot] = $questiondata->page;
    $questions[$slot] = question_bank::make_question($questiondata);
}
}
</code>
</syntaxhighlight>


===Starting the question attempts===
===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:
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:
<code php>
<syntaxhighlight lang="php">
$quba->start_all_questions();
$quba->start_all_questions();
</code>
</syntaxhighlight>
or you can start just one particular question using the <tt>start_question($slot)</tt> method.
or you can start just one particular question using the <tt>start_question($slot)</tt> method.


Line 55: Line 60:


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:
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:
<code php>
<syntaxhighlight lang="php">
question_engine::save_questions_usage_by_activity($quba);
question_engine::save_questions_usage_by_activity($quba);
</code>
</syntaxhighlight>


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


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:
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:
<code php>
<syntaxhighlight lang="php">
$quba = question_engine::load_questions_usage_by_activity($usageid);
$quba = question_engine::load_questions_usage_by_activity($usageid);
</code>
</syntaxhighlight>


The id that you pass to this method is the id you got from <tt>$quba->get_id()</tt> after saving the usage.
The id that you pass to this method is the id you got from <tt>$quba->get_id()</tt> 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 <tt>render_question_head_html</tt> 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 <tt>question_engine::initialise_js()</tt> method, and pass all that as the $meta parameter to print_header.
<code php>
$meta = '';
foreach ($slotsonpage as $slot) {
    $meta .= $quba->render_question_head_html($slot);
}
$meta .= question_engine::initialise_js();
print_header(/* ... lots of args ... */, $meta);
</code>


===The question form===
===The question form===


Within the page, all the questions have to be wrapped in a HTML <tt><nowiki><form></nowiki></tt> tag. This form needs to POST to a scripts that processes the submitted data ([[#Processing_responses|see below]]).
Within the page, all the questions have to be wrapped in a HTML <tt><nowiki><form></nowiki></tt> tag. This form needs to POST to a scripts that processes the submitted data ([[#Processing_responses|see below]]).
<code php>
<syntaxhighlight lang="php">
echo '<form id="responseform" method="post" action="' . $processurl .
echo '<form id="responseform" method="post" action="' . $processurl .
         '" enctype="multipart/form-data" accept-charset="utf-8">', "\n<div>\n";
         '" enctype="multipart/form-data" accept-charset="utf-8">', "\n<div>\n";


print_js_call('question_init_form', array('responseform'));
$PAGE->requires->js_init_call('M.core_question_engine.init_form',
        array('#responseform'), false, 'core_question_engine');


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


The <tt>enctype</tt> and <tt>accept-charset</tt> 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 <tt>question_init_form</tt> function. That function is defined in <tt>question/qengine.js</tt> if you want to find out what it does and why it is important.
The <tt>enctype</tt> and <tt>accept-charset</tt> 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 <tt>question_init_form</tt> function. That function is defined in <tt>question/qengine.js</tt> if you want to find out what it does and why it is important.
Line 113: Line 106:


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 <tt>question_display_options</tt> object that describes what should be visible. For example:
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 <tt>question_display_options</tt> object that describes what should be visible. For example:
<code php>
<syntaxhighlight lang="php">
$options = new question_display_options();
$options = new question_display_options();
$options->marks = question_display_options::MAX_ONLY;
$options->marks = question_display_options::MAX_ONLY;
Line 120: Line 113:
$options->generalfeedback = question_display_options::HIDDEN;
$options->generalfeedback = question_display_options::HIDDEN;
// etc.
// etc.
</code>
</syntaxhighlight>


(In the quiz code, the display options actually come from the quiz settings.) Then you really are ready to output the questions
(In the quiz code, the display options actually come from the quiz settings.) Then you really are ready to output the questions
<code php>
<syntaxhighlight lang="php">
foreach ($slotsonpage as $displaynumber => $slot) {
foreach ($slotsonpage as $displaynumber => $slot) {
     echo $quba->render_question($slot, $options, $displaynumber);
     echo $quba->render_question($slot, $options, $displaynumber);
}
}
</code>
</syntaxhighlight>
 
Here <tt>$displaynumber</tt> 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. It does not have to be a single integer and can also be combined with alphabetic glyphs if you have some other scheme for identifying questions in your activity. For example you could pass '4.3.1', '2a)' '2b)', or '7 of 10' if that is what your activity needs. If you don't pass in a number to display, that is, if you pass null, then the question is printed without a Question X heading.
 
==Processing responses==
 
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.
 


Here <tt>$displaynumber</tt> 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.
===The simple case===


Normally, you just need to do something like:
<syntaxhighlight lang="php">
$timenow = time();
$transaction = $DB->start_delegated_transaction();


==Processing responses==
$quba = question_engine::load_questions_usage_by_activity($usageid);
$quba->process_all_actions($timenow);
question_engine::save_questions_usage_by_activity($quba);
 
$transaction->allow_commit();
</syntaxhighlight>
 
Behind the scenes there is, of course, a lot going on, including the use of the <tt>slots</tt> parameter to control which questions are processed.
 
There may also be additional work you need to do. For example, in the quiz (before the <tt>commit_sql()</tt> line) there is:
 
<syntaxhighlight lang="php">
if ($attempt->timefinish) {
    $attempt->sumgrades = $quba->get_total_mark();
}
$attempt->timemodified = $timenow;
$DB->update_record('quiz_attempts', $attempt);
</syntaxhighlight>
 
===More selective processing===
 
If you want something other than the default processing, you can use code like <tt>$submitteddata = $quba->extract_responses($slot); $quba->process_action($slot, $submitteddata);</tt> or something similar.
 
===Handling scrollpos===
 
You will remember that when we printed the questions, we created a <tt>scrollpos</tt> 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 <tt>scrollpos</tt> parameter, and add it on to the URL you redirect to:


TODO
<syntaxhighlight lang="php">
// With the other required/optional_param calls at the start of processattempt.php.
$scrollpos = optional_param('scrollpos', '', PARAM_RAW);


(Scrollpos)
// ... a bit later in the file
$nexturl = new moodle_url(/* something */);
if ($scrollpos !== '') {
    $nexturl->param('scrollpos', (int) $scrollpos);
}


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


==Reports==
==Reports==


TODO
Places like the quiz reports, we need to get data about a lot of usages at the same time. There are methods on the <tt>question_engine_data_mapper</tt> to do this. You should never access the data in the question engine tables directly.


For example from the quiz grades report. (Again the example code has been simplified.)
<syntaxhighlight lang="php">
// $qubaids is an array of integer ids.
$qubaidscondition = new qubaid_list($qubaids);
$dm = new question_engine_data_mapper();
$latesstepdata = $dm->load_questions_usages_latest_steps(
        $qubaidscondition, $slots);
</syntaxhighlight>
<tt>qubaid_list</tt> is a subclass of the <tt>qubaid_condition</tt> class. This is an efficient way to represent a set of usage ids in a way that can be used in database queries. The other main subclass is <tt>qubaid_join</tt>. That could be used like this:
<syntaxhighlight lang="php">
$qubaidscondition = new qubaid_join('{quiz_attempts} quiza', 'quiza.uniqueid',
        'quiza.quiz = :quizaquiz', array('quizaquiz' => $quizid))
</syntaxhighlight>
Another example, from the quiz manual grading report is
<syntaxhighlight lang="php">
$dm = new question_engine_data_mapper();
$statesummary = $dm->load_questions_usages_question_state_summary(
        $qubaidscondition, $slots);
</syntaxhighlight>
(Perhaps the use of the class <tt>question_engine_data_mapper</tt> should be hidden inside a helper class <tt>question_engine_reporter</tt>?)
==Other methods==
===Methods to help with settings UI===
When you want to present users with a choice of question behaviour (for example, the quiz How questions behave option) use
<syntaxhighlight lang="php">
$options = question_engine::get_behaviour_options();
</syntaxhighlight>
Question marks are stored in the database to a fixed number of decimal places. To get a list of possible decimal place display options, use
<syntaxhighlight lang="php">
$options = question_engine::get_dp_options();
</syntaxhighlight>
===Clean-up methods===
To delete usages you no longer need:
<syntaxhighlight lang="php">
// A single usage ...
question_engine::delete_questions_usage_by_activity($qubaid);
// ... or many.
question_engine::delete_questions_usage_by_activities(
        new qubaid_join('{quiz_attempts} quiza', 'quiza.uniqueid',
        'quiza.quiz = :quizaquiz', array('quizaquiz' => $quizid)));
</syntaxhighlight>
===Changing the score of a question in all attempts===
Suppose you change the number of marks awarded to question 2 in the quiz, you need to change that information in every attempt at that quiz, you can do that with a call like
<syntaxhighlight lang="php">
question_engine::set_max_mark_in_attempts($qubaids, $slot, $newmaxmark);
</syntaxhighlight>
===Re-grading a question===
You only need to re-grade a question when the question definition is edited, for example to change the scoring rules. If you are just changing the maximum possible score for a question, see the previous sub-section.
This code example is adapted from <tt>mod/quiz/report/overview/report.php</tt>. It re-grades selected slots within a quiz attempt.
<syntaxhighlight lang="php">
$transaction = $DB->start_delegated_transaction();
$quba = question_engine::load_questions_usage_by_activity($attempt->uniqueid);
foreach ($slots as $slot) {
    $oldfraction[$slot] = $quba->get_question_fraction($slot);
    $quba->regrade_question($slot);
    $newfraction[$slot] = $quba->get_question_fraction($slot);
}
question_engine::save_questions_usage_by_activity($quba);
$attempt->sumgrades = $quba->get_total_mark();
update_record('quiz_attempts', $attempt);
$transaction->allow_commit();
</syntaxhighlight>


==See also==
==See also==


In the next section, [[Question Engine 2:Implementation plan|Implementation plan]] outlines how I will implement this proposal.
In the next section, [[Question Engine 2:Implementation plan|Implementation plan]] outlines how I will implement this proposal.
* https://github.com/moodleou/moodle-filter_embedquestion/blob/master/showquestion.php is probably the simplest example code that displays a question and lets a user interact with it.


* 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]]

Latest revision as of 10:14, 19 March 2023

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 is explained on this page. Another, much simpler example to look at is question/preview.php, and https://github.com/moodleou/moodle-filter_embedquestion/blob/master/showquestion.php is simpler still.

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 serves 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($quizobj->get_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 (some code to handle random questions and question shuffling omitted)

$slot = 0;
$questions = [];
$maxmark = [];
$page = [];
foreach ($quizobj->get_questions() as $questiondata) {
    $slot += 1;
    $maxmark[$slot] = $questiondata->maxmark;
    $page[$slot] = $questiondata->page;
    $questions[$slot] = question_bank::make_question($questiondata);
}

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.

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<div>\n";

$PAGE->requires->js_init_call('M.core_question_engine.init_form',
        array('#responseform'), false, 'core_question_engine');

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. It does not have to be a single integer and can also be combined with alphabetic glyphs if you have some other scheme for identifying questions in your activity. For example you could pass '4.3.1', '2a)' '2b)', or '7 of 10' if that is what your activity needs. If you don't pass in a number to display, that is, if you pass null, then the question is printed without a Question X heading.

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();
$transaction = $DB->start_delegated_transaction();

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

$transaction->allow_commit();

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 = $quba->get_total_mark();
}
$attempt->timemodified = $timenow;
$DB->update_record('quiz_attempts', $attempt);

More selective processing

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

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 = new moodle_url(/* something */);
if ($scrollpos !== '') {
    $nexturl->param('scrollpos', (int) $scrollpos);
}

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

Reports

Places like the quiz reports, we need to get data about a lot of usages at the same time. There are methods on the question_engine_data_mapper to do this. You should never access the data in the question engine tables directly.

For example from the quiz grades report. (Again the example code has been simplified.)

// $qubaids is an array of integer ids.
$qubaidscondition = new qubaid_list($qubaids);

$dm = new question_engine_data_mapper();
$latesstepdata = $dm->load_questions_usages_latest_steps(
        $qubaidscondition, $slots);

qubaid_list is a subclass of the qubaid_condition class. This is an efficient way to represent a set of usage ids in a way that can be used in database queries. The other main subclass is qubaid_join. That could be used like this:

$qubaidscondition = new qubaid_join('{quiz_attempts} quiza', 'quiza.uniqueid',
        'quiza.quiz = :quizaquiz', array('quizaquiz' => $quizid))

Another example, from the quiz manual grading report is

$dm = new question_engine_data_mapper();
$statesummary = $dm->load_questions_usages_question_state_summary(
        $qubaidscondition, $slots);

(Perhaps the use of the class question_engine_data_mapper should be hidden inside a helper class question_engine_reporter?)


Other methods

Methods to help with settings UI

When you want to present users with a choice of question behaviour (for example, the quiz How questions behave option) use

$options = question_engine::get_behaviour_options();

Question marks are stored in the database to a fixed number of decimal places. To get a list of possible decimal place display options, use

$options = question_engine::get_dp_options();

Clean-up methods

To delete usages you no longer need:

// A single usage ...
question_engine::delete_questions_usage_by_activity($qubaid);

// ... or many.
question_engine::delete_questions_usage_by_activities(
        new qubaid_join('{quiz_attempts} quiza', 'quiza.uniqueid',
        'quiza.quiz = :quizaquiz', array('quizaquiz' => $quizid)));

Changing the score of a question in all attempts

Suppose you change the number of marks awarded to question 2 in the quiz, you need to change that information in every attempt at that quiz, you can do that with a call like

question_engine::set_max_mark_in_attempts($qubaids, $slot, $newmaxmark);

Re-grading a question

You only need to re-grade a question when the question definition is edited, for example to change the scoring rules. If you are just changing the maximum possible score for a question, see the previous sub-section.

This code example is adapted from mod/quiz/report/overview/report.php. It re-grades selected slots within a quiz attempt.

$transaction = $DB->start_delegated_transaction();
$quba = question_engine::load_questions_usage_by_activity($attempt->uniqueid);

foreach ($slots as $slot) {
    $oldfraction[$slot] = $quba->get_question_fraction($slot);
    $quba->regrade_question($slot);
    $newfraction[$slot] = $quba->get_question_fraction($slot);
}

question_engine::save_questions_usage_by_activity($quba);
$attempt->sumgrades = $quba->get_total_mark();
update_record('quiz_attempts', $attempt);
$transaction->allow_commit();

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