Note: You are currently viewing documentation for Moodle 3.7. Up-to-date documentation for the latest stable version of Moodle may be available here: Quiz.

Development:Quiz: Difference between revisions

From MoodleDocs
No edit summary
 
mNo edit summary
 
(52 intermediate revisions by 9 users not shown)
Line 1: Line 1:
The quiz module is a complex module with its own modular structure to allow question type plug-ins. The module has grown organically and in spite of a lot of rewriting for Moodle 1.5 the code is not very simple to understand. Hopefully the information on this page will provide some help.
{{Quiz developer docs}}


==Quiz database structure==
The quiz module is a complex module. The module has grown organically and in spite of a lot of rewriting for Moodle 1.5 the code is not very simple to understand. This page aims to collect useful documentation on how the module works.


The quiz data model has a fairly large pool of database tables, so the first step in explaining them is to provide some order. Conceptually it is possible to distinguish between a static model, which allows defining quizzes and questions, and a runtime model, which stores all the data that is generated when users interact with the statically defined quizzes and questions. Modifying the data in the tables from the static model is only possible for users with teacher privileges (this constraint is not imposed by the database, it is merely an observation). The data in the tables of the runtime model is created when student users interact with quizzes. Therefore these tables usually contain significantly more data than the ones from the static model.
The quiz module uses the Moodle [[Question engine]] which is responsible for the rendering of questions and for the processing of student responses. The quiz module itself is only responsible for assembling questions into quizzes and for presenting the results.


A further simplification is possible by ignoring the questiontype specific tables. They are logical extensions to other tables and therefore are not necessary for understanding the general basic model. However, some information is provided for each questiontype specific table, namely which questiontype it belongs to (although that should be clear from the name), which table it extends and what the additional data is needed for.
==Code documentation==


Using these two criteria the list below puts some order into the collection of tables.
The code is documented according to phpdoc conventions. The explanations here in the wiki are meant to complement this.


*Static model
Inline comments should be used to clarify the flow of the code, and to explain any details that are not clear from reading the code itself (there should be very few of these). The following conventions make it easier to search for comments with special meaning:
** quiz
* use TODO in comments about things that need to be done
** quiz_questions
* use ??? in comments that are questions about the code
** quiz_answers
** quiz_categories
** quiz_question_instances
** quiz_question_versions


*Runtime Model
There are three function libraries:
**quiz_attempts
**quiz_states
**quiz_grades
**quiz_newest_states


*Questiontype specific tables
;lib.php
:All the functions that are sometimes called by the Moodle core.


**quiz_calculated
;editlib.php
**quiz_dataset_definitions
:Functions that are used by the edit page edit.php. This loads locallib.php
**quiz_dataset_items
**quiz_match
**quiz_match_sub
**quiz_multianswers
**quiz_multichoice
**quiz_numerical_units
**quiz_question_datasets
**quiz_randomsamatch
**quiz_rqp
**quiz_rqp_states
**quiz_rqp_types
**quiz_shortanswer
**quiz_truefalse


*Redundant tables
;locallib.php
:All functions that are used only by the quiz module. This loads lib.php and lib/questionlib.php


**quiz_attemptonlast_datasets
The [[Quiz_attempt|attempt.php]] script is one of the most complicated scripts of the quiz module. It is responsible for displaying questions to a user and to evaluate and grade the users' responses. The reason it is complicated is that it has to take a large array of quiz options into account. There is a [[Quiz_attempt|separate page with explanations]] of this script.


These simplifications reduce the number of "interesting" tables significantly, and still some of them are only necessary for understanding very specific aspects of the quiz module. The diagram below shows how the (selected) most important tables are linked to one another.
==Objects==


===Static model===
Most objects in Moodle are simply devices to hold records from the corresponding database table. However the question engine extends some objects with additional properties that do not correspond to database fields. Furthermore the quiz engine uses a few objects that do not correspond to database tables at all. I intend to give descriptions of these below.
====quiz====


The quiz table contains the definition for all the quizzes. Each quiz belongs to a course, reflected by the course id, has a name and a short descriptive text (intro), an opening and a closing time and several fields that store the settings of various quiz options, each of which is explained in the quiz help that is linked to from the quiz settings page. One field that may require additional information is the optionsflag, which... (maybe Gustav can add an explanation here?).
=== The options object ===


The quiz id is used extensively to identify records from various runtime tables, reflecting the fact (surprise) that quizzes are the main players in the quiz module.
The options object is used to carry information about which optional bits of a question should be printed. That can be feedback, correct responses or any other information that a teacher can choose to show or hide to the students during different phases of the quiz lifecycle.


====quiz_questions====
;feedback
:The feedback field holds a boolean value, indicating to the questiontypes' <code>print_question_formulation_and_controls</code> method whether or not to print feedback (if available) for the submitted responses.


This table constitutes the item or question bank, i.e. the repository of defined questions. The quiz_questions table defines the data that is common to questions of all types. It provides each question with a unique id which is used as a foreign key in many other tables. for example the quiz_answers table allows to define an arbitrary number of answers that are part of the question. And many questiontypes have their own tables that hold more information about the question.
;correct_responses
:The correct_responses field holds a boolean value, indicating to the questiontypes' <code>print_question_formulation_and_controls</code> method whether or not to provide the correct responses (if available) inline with the question.


Most fields are self explanatory, however, there are a few that require additional explanation: the parent field is a means to provide support for wrapped questions (e.g. the multianswer questiontype). When a question wraps around any number of subquestions the subquestions will have their parent id field set to the id of the main question, thus allowing the question to find all its sub-questions (or wrapped questions). A side effect is that any question with a parent id other than "0" is not shown in the list of questions that can be added to a quiz. This side effect is also used for effectively hiding random questions from the question list. Their parent field is simply set to their own id.
;readonly
:The readonly field holds a boolean value, indicating to the questiontypes' <code>print_question_formulation_and_controls</code> method whether the interaction elements should be disabled or not.


It may seem strange then to also have a field called hidden, but that serves a slightly different purpose. Hiding questions is first of all a mechanism to "delete" questions without removing them from the database and thus to restore or "unhide" them at a later stage. Also the (unfinished and disabled) versioning feature uses the hidden field to prevent older versions of a question from cluttering the user interface.
;validation
:The validation field holds a boolean value, indicating to the questiontypes' <code>print_question_formulation_and_controls</code> method whether or not to print question validation information. (This is currently only used by the RQP questiontype.)


A question can also have a length. This defines how many question numbers are required for this question. It is generally set to "1", but the description questiontype, for example, sets it to "0", reflecting the fact that it doesn't have a question number.
;responses
We now deal with the remaining tables from the static model in alphabetical order.
:The responses field holds a boolean value, indicating to the questiontypes' print methods whether or not to print a student's responses. This only makes sense for reviewing.


====quiz_answers====
;scores
:The scores field holds a boolean value, indicating to the questiontypes' print methods whether or not to print the grade awarded to student.


This table allows a common way to define one or more answers for each question. It is not mandatory for a questiontype to make use of this table however. A questiontype may choose to store it's answers in an entirely different way, or even to calculate the correct answer on the fly.
;solutions
:The solutions field holds a boolean value, indicating to the questiontypes' print methods whether or not to show the worked solution provided by the teacher (if any). This only makes sense for reviewing.


The question id links each answer to a question. The sequence number field may be used to store the order of the different answers, it can be set to "0" though if the order is of no importance. The answer field stores whatever constitutes an answer for the concerned questiontype (each questiontype can make whatever it wants off it's answers), the fraction field stores the assigned score for the question (range 0..1) and the feedback field allows defining some feedback, to be displayed when the student's answer agrees with the defined answer record.
==Time limit==


====quiz_categories====
A quiz can have a time limit. This is stored in minutes in $quiz->timelimit. So before using this in time calculations it always has to be multiplied by 60 to turn it into seconds like all other timestamps in moodle and php. If $quiz->timelimit is zero it means there is no timelimit.


Categories are provided as a way to organize questions. Each category has a name and a descriptive text (info) and the sortorder as metadata. Categories allow hierarchical nesting via the parent id and can be private or published, i.e. they can be made available to teachers in other courses.
If a student asks to start an attempt on view.php for a quiz with a timelimit then he is shown a javascript message alerting him to the timelimit and is asked to confirm.


Since categories are simply a means for organising questions they are not vital for understanding how the quiz module works.
For quizzes with timelimit attempt.php shows a javascript timer that counts down and automatically submits and closes the attempt when the time is up.


====quiz_question_instances====
Confusingly there are two javascript timers in the quiz module. jsclock.php provides a countdown in the title bar that counts down to the quiz closing time if this is less than a day away. This has nothing to do with the timelimit. jstimer.php provides the countdown timer that implements the timelimit. It in turn uses timer.js.


Questions can have different grades assigned in different quizzes. These are stored in the quiz_question_instances table. While, after a small extension, this table could also fulfill the purpose of storing the order of the questions in a quiz, this is currently still done in the questions field in the quiz table.
The time a response was submitted by the student is recorded by attempt.php right at the top of the page and is then passed on to quiz_process_responses in $action->timestamp. This puts it into $state->timestamp. Finally, after the responses have been graded, the function quiz_apply_penalty_and_timelimit() checks that the responses are within the timelimit to within 5% and if not it sets the grade to zero (or the previously obtained grade, if that is higher).


====quiz_question_versions====
==Pagination==


This feature is not finished and disabled. The table structure may still change.
Quiz attempts can be paginated, i.e., spread over several pages. The student can navigate between the pages using the standard Moodle paging bar. When the student navigates to a different quiz page the answers on the current page are automatically submitted for saving (but not grading).


===Runtime Model===
To do this automatic submission the paging bar needs some javascript. It is therefore not produced with Moodle's standard print_paging_bar() function from weblib.php but with quiz_print_navigation_panel() which is defined in mod/quiz/locallib.php and produces something that looks the same.


====quiz_attempts====
The teacher has complete control via the edit interface on edit.php over where the page breaks should occur. He can repaginate the quiz with any chosen number of questions per page. He can also move the page-breaks up and down using the arrows.


In the quiz_attempts table a record is created each time when a user starts an attempt at a quiz. It is possible for a user to attempt a quiz several times, therefore the number of the attempt is stored in the attempt field. The sumgrade field records the (unscaled) grade for the attempt, i.e. if the grades assigned to the questions add up to 8, but the maximum grade for the quiz is set to 10, then the sumgrades field can contain 8 at maximum.
Internally page breaks are stored in the $quiz->questions field (which now should really be called $quiz->layout). This field contains a comma separated list of questionids and pagebreaks where the pagebreaks are represented by the id 0. For example 23,12,0,11, 0 means that the two questions with ids 23 and 12 are on the first page and the question with id 11 is on the second page. The last page break is invisible and Moodle sometimes puts it there itself for its own convenience.


The timestart field is set to the current time when an attempt is started and is never changed afterwards. The timefinish field is set to "0" initially and to the current time when the attempt is closed. This is exploited at several places in the code to determine whether an attempt has been closed or not (i.e. closed = timefinish > 0). For all other modifications of an attempt record the timemodified field should be changed as well.
Because the quiz has an option $quiz->shufflequestions to shuffle questions the layout that the student sees in a particular attempt does not necessarily have to be the same as that stored in $quiz->questions. Therefore each attempt has its own $attemp->layout field. If $quiz->shufflequestions is false then this just contains a copy of $quiz->questions but if it is true then during the creation of a new attempt by quiz_create_attempt() the function quiz_repaginate() is used to produce a layout with $quiz->questionsperpage number of questions per page that are randomly ordered.


Finally, there are the layout and preview fields. The preview field is a flag that marks a teacher preview (i.e. an attempt by a user with teacher privileges) that may be automatically deleted when the quiz is previewed again, and which is not taken into account when viewing statistics. The layout field contains a comma separated list of question ids, with a "0" denoting a page break. Usually the comma separated list ends with ",0".
Both attempt.php and review.php use the $attempt->layout field to determine what questions to show on a particular page. That way we can guarantee that the student will, for a particular attempt, always see the questions in the same order and with the same pagination, both while attempting and during review. Also a teacher when reviewing a student's attempt sees the pages the same way they were shown to the student. However the teacher is also given the option to see all questions on one page.


====quiz_states====
There are some functions in locallib.php dedicated to handling the layout fields: quiz_questions_on_page(), quiz_questions_in_quiz(), quiz_number_of_pages(), quiz_first_questionnumber(), quiz_repaginate(). They are very short functions. The function quiz_first_questionnumber() that determines the number of the first question on a particular page makes use of the $question->length field. To allow this calculation to be fast is the main reason why that field is in the question table even though it could also be determined easily from the question type.


States are saved for each interaction with a question. This allows to review the complete history of a user's attempts on individual questions. The seq_number field stores the order of this history, the answer field stores a questiontype specific string unless the questiontype stores its answers differently. The event field stores an integer which can be one of the named constant defined in the file locallib.php denoting for example a saving or a grading interaction amongst others. For more details see eventtypes.html


Of further interest are the grade, raw_grade and penalty fields. The raw_grade field stores the grade that was achieved for the question scaled to the question's weight or grade as assigned in the quiz_question_instances table. The grade field stores the actual achieved grade after deduction of the penalty. And in the penalty field the penalty for that state is saved. This is different from the cumulative penalty, which is stored in the quiz_newest_states table.
==Question versioning==


The originalquestion field is a construct that will be used by the versioning code. The question ids in the states will be changed to the ids of the new versions of the questions and the id of the question, which was used for the actual attempt, will be stored in the originalquestion field.
'''Note:''' Question versioning is currently disabled until it is re-developed to fix all reported issues.
We now deal with the remaining tables from the runtime model in alphabetical order.


====quiz_grades====
When questions that were already attempted by a student are edited, it can be important to keep a copy of the question as it was before editing in order to reconstruct the quiz as it was seen by the student. To provide this functionality a question versioning mechanism was implemented.


The quiz_grades table merely stores a student's awarded grade for a quiz. Since it is possible to allow several attempts on a quiz, the grade stored is calculated depending on the quiz setting grademethod. This table exists mainly for convenience, because the values of its fields can be recalculated.
The first goal, namely keeping around old questions, is easily achieved. They are just not deleted any more. However, this is not enough; it is also necessary to store which questions are versions of others. To achieve this goal, there is an additional table, which stores the versioning information: quiz_question_versions.


====quiz_newest_states====
When a question is replaced for which there are already student attempts then all the attempt data gets associated to the new version of the question and is re-graded. This requires the question ids in the quiz_attempts, quiz_states and quiz_newest_states tables to be replaced by the new id. However we do also want to be able to reconstruct the quiz the way the student saw it when he gave his answers. For that purpose the id of the original question is always preserved in the 'originalquestion' field of the quiz_states table.


This table exists only for efficiency reasons:
If all old versions of questions are kept around this could horribly clutter the editing interface. Therefore a field called hidden was added to the quiz_questions table and all old versions of edited questions are automatically hidden. When this flag is set to 1 the question is not displayed in the list of available questions, unless the user chooses to show them.
#Via its 'newest' and 'newgraded' fields it gives attempt.php a way to quickly find the newest state and the newest graded state for an attempt. It allows the construction of SQL to select all the states that need to be loaded on attempt.php or review.php.
#Via its 'sumpenalty' field it gives quiz_apply_penalty() a quick way for getting at the accumulated penalty that needs to be applied. Without this field the penalties from all previous graded states would have to be added up each time. Not a big deal actually because this could be achieved with a single SQL query (using SUM) but this field was introduced when we still had the multiplicative penalty scheme around which would have been more difficult to recompute.


This table was introduced in Moodle 1.5 and is not populated for all states during the upgrade because on sites with a lot of existing states that could take too long. Rather it is done whenever needed by quiz_upgrade_states().
While the mechanism above should work as described, there is some additional complexity in order to minimise the number of versions created. If a question is created and has not been attempted by a student yet (this excludes teacher previews of the individual question and the quiz!), the database record will be reused (i.e. overwritten) and no new version will be created. This is especially important when the question is created and the first 2 or 3 mistakes are only noticed during preview.


===Questiontype specific tables===
On the editing screen for questions an additional set of options was introduced (see image).
Replacement Options
It shows which quizzes use the edited question and how many students have attempted it in a particular quiz. Based on this information it is then possible to choose in which quizzes the new version of the question should be used and in which ones the old one should remain.


====quiz_calculated====
By default the 'replace' checkbox for all quizzes that don't have any students' attempts are checked and in addition, if the question is edited out of a quiz context (i.e. not in the category question list), the 'replace' option is checked for that quiz as well.


The quiz_calculated table is an extension to the quiz_questions table by the calculated questiontype. However, it would be more suitable to change that to be an extension of the quiz_answers table, which, from a data perspective, is already possible, since an answer id is stored in the answer field. The questiontype code would need some changes to take this into account, however.
===Database===


====quiz_dataset_definitions====
The changes to the database structure are limited to an added field (hidden) in the quiz_questions table and an additional table called quiz_question_versions. However, dealing with the quiz_questions table has become slightly more complicated.


The quiz_dataset_definitions table belongs to the abstract datasetdependent questiontype, which is currently only used by the calculated questiontype. It is an indirect extension to the quiz_questions table, because the quiz_question_datasets table can link a question to one or more datasets. Each dataset represents a variable, that is used either in the questiontext or in the answer to a dataset dependent question.
The hidden field in the quiz_questions table has no implications for the core functionality. It is only used to determine, as the name implies, whether the question is shown in the category list or not.


====quiz_dataset_items====
The table quiz_question_versions stores information about the actual change. This information includes the ids of the old question and the new question, the id of the user who did the change and a timestamp. Quite importantly, the id of the quiz, in which the question was replaced is also stored. This means that the versions table provides a history of the different states the quiz went through until it was edited to be at the current state. The information allows to recreate a quiz as it was at any point in time (from a data perspective - this possibility is not used extensively by the code).


Dataset items can be created for each dataset. The quiz_dataset_items table stores these possible values for the variables defined in the quiz_dataset_definitions table.
===Adjustments to the Data===


====quiz_match====
When a question is replaced by a newer version, database records are updated in the order shown below (compare with question.php):


The quiz_match table belongs to the match questiontype and extends the quiz_questions table. It is only used in the code for saving matching questions and can therefore be considered redundant.
* First a new record is inserted into the quiz_question_versions table for each affected quiz (i.e. each quiz in which the question was replaced).
* Then, for each affected quiz, the comma separated list of question ids in the question field is updated by replacing the old question id with the new one.
* In the quiz_question_instances table the record that links the old question to the quiz is also updated to point to the new question.
* In all attempts belonging to the old question the comma-separated list of question ids in the layout field are changed by replacing the old id by the new one.
* All states belonging to the old question are made to belong to the new version by changing the id in the 'question' field. However if we are replacing the original question then the id of this original version is stored in the originalquestion field.
* We have to change the questionid field in quiz_newest_states.
* Finally we have to do any question-type specific changes. For example question types that store student responses by storing the id of the answer in the quiz_answers table will have to recode these ids in all the states to point to the corresponding answers in the new version. This is handled by the function replace_question_in_attempts() in the question type class.


====quiz_match_sub====
===Affected Code and Functionality===


The quiz_match_sub table belongs to the match questiontype and extends the quiz_questions table. It stores the pairs of questions and answers (as strings) that need to be matched for a correct solution.
Note: This section should still be considered under construction until the question mark behind bug #3311 is taken off.


====quiz_multianswers====
In the file review.php and potentially also in the file attempt.php, if a question is edited during a student's attempt, the data from quiz_question_versions needs to be taken into account. If a student has attempted a quiz and a question was changed afterwards (i.e. a new version of that question was created), the question id of the old version remains in the comma separated list inside the attempt->layout field. However, since the records in the quiz_question_instances table get updated, we need to go forward in the question history, by looping through entries from the quiz_question_versions table, to find out the id of the question version that is currently used in the quiz.


The quiz_multianswers table belongs to the multianswer questiontype and is an extension of the quiz_questions table. It merely stores a comma separated list of question ids in the sequence field, which is important, because that's the only way to know which sub question belongs to which position in the questiontext.
Suggestion: With a fairly simple change to the convention of what is stored in the quiz_question_versions table we could get rid of the requirement of looping through all the versions. If in the newquestion field we store the id of the question that is currently used in the quiz, it would be possible to get the complete history for a question quite simply by selecting by quiz id and newquestion.


====quiz_multichoice====
It should be fairly simple to write an upgrade script for this change. Additionally, another set_field would need to be added to question.php to change the newquestion field to the new question id. The benefits would be a much simpler handling of the question history, resulting in more efficient code than the current fix for bug #3311 in review.php.


The quiz_multichoice table belongs to the multichoice questiontype and is an extension of the quiz_questions table. The layout field does not seem to be used, the answers field stores the order of the answers (should be superseded by the seq_number field in the quiz_answers table) and the single field is a flag signaling, whether only one option or multiple options can be chosen.
The place where all the versioning actually takes place is question.php. Here the changes described in Adjustments to the Data are carried out.


====quiz_numerical====
Obviously the backup and restore scripts also take quiz_question_versions into account, however, they don't need to be concerned with the ways the data is used.


The quiz_numerical table belongs to the numerical questiontype and is an extension of the quiz_answers table, defining a tolerance value for each answer.
==Changes for Moodle 1.5: adaptive questions==


====quiz_numerical_units====
During the first half of 2005 the quiz module code has undergone a considerable rewrite to allow for adaptive questions in which a question session can consist of several sequential student responses. The question can adapt itself to the student answers. For example in response to certain answers the question could provide feedback or hints and then ask the student to answer again or give the student a simpler or related question.


The quiz_numerical_units table is used by the numerical questiontype and the calculated questionype. It extends the quiz_questions table, defining an arbitrary number of units that can be used in the responses.
Unfortunately many changes had to be made to the question type methods. This has however resulted in improved efficiency and has made the writing of question types easier. It also allows question types with more powerful features and has fixed some bugs / annoying behaviour.  


====quiz_question_datasets====
For details see:
*[[Quiz rewrite|Quiz module rewrite]]
*[[Development:Quiz code methods|How to convert existing question types]] with tips about changing methods.


The quiz_question_datasets table is used by dataset dependent questionypes (i.e. calculated) to link datasets to questions.
Of course there were countless other changes to the quiz module going from Moodle 1.4 to 1.5, especially to the teacher interface. However in spite of the fact that these changes are a lot more visible they were much less drastic from the point of view of the code. Here is a very incomplete list of changes:


====quiz_randomsamatch====
* New quiz results overview page
* Reform of the quiz edit page: Changes on the quiz edit page are saved straightaway, not only after Save button is pressed.
* Copying questions: The teacher can create a new question using a previous one as template.
* Moving questions: The teacher can now move selected questions to a different category.
* Re-marking after question editing: if a teacher corrects a question that students have already attempted the teacher can request a remark.
* Teacher preview tab.
* Detailed teacher control over what students can see during review.


This extension to the quiz_questions table simply stores how many shortanswer questions should be randomly chosen to build this randomsamatch question.
==Changes for Moodle 1.6: separating questions from quizzes==


====quiz_rqp====
The quiz module is not the only activity module in Moodle that uses questions. The lesson does too and potentially questions could be useful in many modules. Therefore we have started to rewrite some  of the quiz module functions and move them from locallib.php to questionlib.php so that eventually they could be moved into a central library and be used by other modules.


No information.
Module developers who want to use the
quiz module questions in their own module should take a look at the simple questiondemo module that you can find in CVS at [http://moodle.cvs.sourceforge.net/moodle/contrib/questiondemo contrib/questiondemo]. The idea is that it will be simpler
to understand this demonstration module than the code in the quiz module which
is very complicated due to the many options and features. The interesting code in the questiondemo module is in view.php. There you can see how to
both render and score questions. This module requires the version of the quiz module from Moodle 1.6dev or later.


====quiz_rqp_states====
The details of the changes are explained on the page [[Separating questions from quizzes]].


No information.
==Future plans==


====quiz_rqp_types====
The features below are in no particular order:


No information.
* Editing questions: This is about editing questions after students have already attempted them. There needs to be a mechanism to keep the old versions of the question around for auditing purposes.
* New quiz statistics pages?: These pages should be built by using functions defined by the individual question types.
* Manual grade override: Teachers should be able to override the automatically calculated grades and should be able to make comments.
* Off-line questions: The answers to these are handed in off-line in the conventional way (e.g., on paper) and teachers enter marks on Moodle.
* Batch printing of quiz sheets: We want to be able to hand out question sheets to students so they can start working on the questions before going to the computer.
* Question preview from question edit page: so the teacher can try the question already before saving the changes.
* Show table of questions on view.php: gives teachers and students a bit of an overview of the quiz.
* Extending deadlines for individual students: for example when a student misses a deadline for good reasons.
* Filtering questions by quiz and by search: More ways to restrict which questions are shown on the quiz editing page.
* Re-open quizzes for revision: After the due date has passed the quiz could allow practice attempts.


====quiz_shortanswer====
==UI design / usability==
See : [[Development:Quiz_Usability_portal|Quiz usability portal]]


The quiz_shortanswer table belongs to the shortanswer questiontype and is an extension of the quiz_questions table. The answers field stores a comma separated list of answer ids, which is redundant. The only valuable piece of information contained in this table is the usecase field, which is used to decide whether to do a case sensitive or case insensitive comparison for grading.
[[Category:Developer|Quiz]]
quiz_truefalse
[[Category:Quiz]]
 
An extension of the quiz_questions table the quiz_truefalse table stores the answer ids for the true and for the false answers.

Latest revision as of 16:57, 13 May 2010

Template:Quiz developer docs

The quiz module is a complex module. The module has grown organically and in spite of a lot of rewriting for Moodle 1.5 the code is not very simple to understand. This page aims to collect useful documentation on how the module works.

The quiz module uses the Moodle Question engine which is responsible for the rendering of questions and for the processing of student responses. The quiz module itself is only responsible for assembling questions into quizzes and for presenting the results.

Code documentation

The code is documented according to phpdoc conventions. The explanations here in the wiki are meant to complement this.

Inline comments should be used to clarify the flow of the code, and to explain any details that are not clear from reading the code itself (there should be very few of these). The following conventions make it easier to search for comments with special meaning:

  • use TODO in comments about things that need to be done
  • use ??? in comments that are questions about the code

There are three function libraries:

lib.php
All the functions that are sometimes called by the Moodle core.
editlib.php
Functions that are used by the edit page edit.php. This loads locallib.php
locallib.php
All functions that are used only by the quiz module. This loads lib.php and lib/questionlib.php

The attempt.php script is one of the most complicated scripts of the quiz module. It is responsible for displaying questions to a user and to evaluate and grade the users' responses. The reason it is complicated is that it has to take a large array of quiz options into account. There is a separate page with explanations of this script.

Objects

Most objects in Moodle are simply devices to hold records from the corresponding database table. However the question engine extends some objects with additional properties that do not correspond to database fields. Furthermore the quiz engine uses a few objects that do not correspond to database tables at all. I intend to give descriptions of these below.

The options object

The options object is used to carry information about which optional bits of a question should be printed. That can be feedback, correct responses or any other information that a teacher can choose to show or hide to the students during different phases of the quiz lifecycle.

feedback
The feedback field holds a boolean value, indicating to the questiontypes' print_question_formulation_and_controls method whether or not to print feedback (if available) for the submitted responses.
correct_responses
The correct_responses field holds a boolean value, indicating to the questiontypes' print_question_formulation_and_controls method whether or not to provide the correct responses (if available) inline with the question.
readonly
The readonly field holds a boolean value, indicating to the questiontypes' print_question_formulation_and_controls method whether the interaction elements should be disabled or not.
validation
The validation field holds a boolean value, indicating to the questiontypes' print_question_formulation_and_controls method whether or not to print question validation information. (This is currently only used by the RQP questiontype.)
responses
The responses field holds a boolean value, indicating to the questiontypes' print methods whether or not to print a student's responses. This only makes sense for reviewing.
scores
The scores field holds a boolean value, indicating to the questiontypes' print methods whether or not to print the grade awarded to student.
solutions
The solutions field holds a boolean value, indicating to the questiontypes' print methods whether or not to show the worked solution provided by the teacher (if any). This only makes sense for reviewing.

Time limit

A quiz can have a time limit. This is stored in minutes in $quiz->timelimit. So before using this in time calculations it always has to be multiplied by 60 to turn it into seconds like all other timestamps in moodle and php. If $quiz->timelimit is zero it means there is no timelimit.

If a student asks to start an attempt on view.php for a quiz with a timelimit then he is shown a javascript message alerting him to the timelimit and is asked to confirm.

For quizzes with timelimit attempt.php shows a javascript timer that counts down and automatically submits and closes the attempt when the time is up.

Confusingly there are two javascript timers in the quiz module. jsclock.php provides a countdown in the title bar that counts down to the quiz closing time if this is less than a day away. This has nothing to do with the timelimit. jstimer.php provides the countdown timer that implements the timelimit. It in turn uses timer.js.

The time a response was submitted by the student is recorded by attempt.php right at the top of the page and is then passed on to quiz_process_responses in $action->timestamp. This puts it into $state->timestamp. Finally, after the responses have been graded, the function quiz_apply_penalty_and_timelimit() checks that the responses are within the timelimit to within 5% and if not it sets the grade to zero (or the previously obtained grade, if that is higher).

Pagination

Quiz attempts can be paginated, i.e., spread over several pages. The student can navigate between the pages using the standard Moodle paging bar. When the student navigates to a different quiz page the answers on the current page are automatically submitted for saving (but not grading).

To do this automatic submission the paging bar needs some javascript. It is therefore not produced with Moodle's standard print_paging_bar() function from weblib.php but with quiz_print_navigation_panel() which is defined in mod/quiz/locallib.php and produces something that looks the same.

The teacher has complete control via the edit interface on edit.php over where the page breaks should occur. He can repaginate the quiz with any chosen number of questions per page. He can also move the page-breaks up and down using the arrows.

Internally page breaks are stored in the $quiz->questions field (which now should really be called $quiz->layout). This field contains a comma separated list of questionids and pagebreaks where the pagebreaks are represented by the id 0. For example 23,12,0,11, 0 means that the two questions with ids 23 and 12 are on the first page and the question with id 11 is on the second page. The last page break is invisible and Moodle sometimes puts it there itself for its own convenience.

Because the quiz has an option $quiz->shufflequestions to shuffle questions the layout that the student sees in a particular attempt does not necessarily have to be the same as that stored in $quiz->questions. Therefore each attempt has its own $attemp->layout field. If $quiz->shufflequestions is false then this just contains a copy of $quiz->questions but if it is true then during the creation of a new attempt by quiz_create_attempt() the function quiz_repaginate() is used to produce a layout with $quiz->questionsperpage number of questions per page that are randomly ordered.

Both attempt.php and review.php use the $attempt->layout field to determine what questions to show on a particular page. That way we can guarantee that the student will, for a particular attempt, always see the questions in the same order and with the same pagination, both while attempting and during review. Also a teacher when reviewing a student's attempt sees the pages the same way they were shown to the student. However the teacher is also given the option to see all questions on one page.

There are some functions in locallib.php dedicated to handling the layout fields: quiz_questions_on_page(), quiz_questions_in_quiz(), quiz_number_of_pages(), quiz_first_questionnumber(), quiz_repaginate(). They are very short functions. The function quiz_first_questionnumber() that determines the number of the first question on a particular page makes use of the $question->length field. To allow this calculation to be fast is the main reason why that field is in the question table even though it could also be determined easily from the question type.


Question versioning

Note: Question versioning is currently disabled until it is re-developed to fix all reported issues.

When questions that were already attempted by a student are edited, it can be important to keep a copy of the question as it was before editing in order to reconstruct the quiz as it was seen by the student. To provide this functionality a question versioning mechanism was implemented.

The first goal, namely keeping around old questions, is easily achieved. They are just not deleted any more. However, this is not enough; it is also necessary to store which questions are versions of others. To achieve this goal, there is an additional table, which stores the versioning information: quiz_question_versions.

When a question is replaced for which there are already student attempts then all the attempt data gets associated to the new version of the question and is re-graded. This requires the question ids in the quiz_attempts, quiz_states and quiz_newest_states tables to be replaced by the new id. However we do also want to be able to reconstruct the quiz the way the student saw it when he gave his answers. For that purpose the id of the original question is always preserved in the 'originalquestion' field of the quiz_states table.

If all old versions of questions are kept around this could horribly clutter the editing interface. Therefore a field called hidden was added to the quiz_questions table and all old versions of edited questions are automatically hidden. When this flag is set to 1 the question is not displayed in the list of available questions, unless the user chooses to show them.

While the mechanism above should work as described, there is some additional complexity in order to minimise the number of versions created. If a question is created and has not been attempted by a student yet (this excludes teacher previews of the individual question and the quiz!), the database record will be reused (i.e. overwritten) and no new version will be created. This is especially important when the question is created and the first 2 or 3 mistakes are only noticed during preview.

On the editing screen for questions an additional set of options was introduced (see image). Replacement Options It shows which quizzes use the edited question and how many students have attempted it in a particular quiz. Based on this information it is then possible to choose in which quizzes the new version of the question should be used and in which ones the old one should remain.

By default the 'replace' checkbox for all quizzes that don't have any students' attempts are checked and in addition, if the question is edited out of a quiz context (i.e. not in the category question list), the 'replace' option is checked for that quiz as well.

Database

The changes to the database structure are limited to an added field (hidden) in the quiz_questions table and an additional table called quiz_question_versions. However, dealing with the quiz_questions table has become slightly more complicated.

The hidden field in the quiz_questions table has no implications for the core functionality. It is only used to determine, as the name implies, whether the question is shown in the category list or not.

The table quiz_question_versions stores information about the actual change. This information includes the ids of the old question and the new question, the id of the user who did the change and a timestamp. Quite importantly, the id of the quiz, in which the question was replaced is also stored. This means that the versions table provides a history of the different states the quiz went through until it was edited to be at the current state. The information allows to recreate a quiz as it was at any point in time (from a data perspective - this possibility is not used extensively by the code).

Adjustments to the Data

When a question is replaced by a newer version, database records are updated in the order shown below (compare with question.php):

  • First a new record is inserted into the quiz_question_versions table for each affected quiz (i.e. each quiz in which the question was replaced).
  • Then, for each affected quiz, the comma separated list of question ids in the question field is updated by replacing the old question id with the new one.
  • In the quiz_question_instances table the record that links the old question to the quiz is also updated to point to the new question.
  • In all attempts belonging to the old question the comma-separated list of question ids in the layout field are changed by replacing the old id by the new one.
  • All states belonging to the old question are made to belong to the new version by changing the id in the 'question' field. However if we are replacing the original question then the id of this original version is stored in the originalquestion field.
  • We have to change the questionid field in quiz_newest_states.
  • Finally we have to do any question-type specific changes. For example question types that store student responses by storing the id of the answer in the quiz_answers table will have to recode these ids in all the states to point to the corresponding answers in the new version. This is handled by the function replace_question_in_attempts() in the question type class.

Affected Code and Functionality

Note: This section should still be considered under construction until the question mark behind bug #3311 is taken off.

In the file review.php and potentially also in the file attempt.php, if a question is edited during a student's attempt, the data from quiz_question_versions needs to be taken into account. If a student has attempted a quiz and a question was changed afterwards (i.e. a new version of that question was created), the question id of the old version remains in the comma separated list inside the attempt->layout field. However, since the records in the quiz_question_instances table get updated, we need to go forward in the question history, by looping through entries from the quiz_question_versions table, to find out the id of the question version that is currently used in the quiz.

Suggestion: With a fairly simple change to the convention of what is stored in the quiz_question_versions table we could get rid of the requirement of looping through all the versions. If in the newquestion field we store the id of the question that is currently used in the quiz, it would be possible to get the complete history for a question quite simply by selecting by quiz id and newquestion.

It should be fairly simple to write an upgrade script for this change. Additionally, another set_field would need to be added to question.php to change the newquestion field to the new question id. The benefits would be a much simpler handling of the question history, resulting in more efficient code than the current fix for bug #3311 in review.php.

The place where all the versioning actually takes place is question.php. Here the changes described in Adjustments to the Data are carried out.

Obviously the backup and restore scripts also take quiz_question_versions into account, however, they don't need to be concerned with the ways the data is used.

Changes for Moodle 1.5: adaptive questions

During the first half of 2005 the quiz module code has undergone a considerable rewrite to allow for adaptive questions in which a question session can consist of several sequential student responses. The question can adapt itself to the student answers. For example in response to certain answers the question could provide feedback or hints and then ask the student to answer again or give the student a simpler or related question.

Unfortunately many changes had to be made to the question type methods. This has however resulted in improved efficiency and has made the writing of question types easier. It also allows question types with more powerful features and has fixed some bugs / annoying behaviour.

For details see:

Of course there were countless other changes to the quiz module going from Moodle 1.4 to 1.5, especially to the teacher interface. However in spite of the fact that these changes are a lot more visible they were much less drastic from the point of view of the code. Here is a very incomplete list of changes:

  • New quiz results overview page
  • Reform of the quiz edit page: Changes on the quiz edit page are saved straightaway, not only after Save button is pressed.
  • Copying questions: The teacher can create a new question using a previous one as template.
  • Moving questions: The teacher can now move selected questions to a different category.
  • Re-marking after question editing: if a teacher corrects a question that students have already attempted the teacher can request a remark.
  • Teacher preview tab.
  • Detailed teacher control over what students can see during review.

Changes for Moodle 1.6: separating questions from quizzes

The quiz module is not the only activity module in Moodle that uses questions. The lesson does too and potentially questions could be useful in many modules. Therefore we have started to rewrite some of the quiz module functions and move them from locallib.php to questionlib.php so that eventually they could be moved into a central library and be used by other modules.

Module developers who want to use the quiz module questions in their own module should take a look at the simple questiondemo module that you can find in CVS at contrib/questiondemo. The idea is that it will be simpler to understand this demonstration module than the code in the quiz module which is very complicated due to the many options and features. The interesting code in the questiondemo module is in view.php. There you can see how to both render and score questions. This module requires the version of the quiz module from Moodle 1.6dev or later.

The details of the changes are explained on the page Separating questions from quizzes.

Future plans

The features below are in no particular order:

  • Editing questions: This is about editing questions after students have already attempted them. There needs to be a mechanism to keep the old versions of the question around for auditing purposes.
  • New quiz statistics pages?: These pages should be built by using functions defined by the individual question types.
  • Manual grade override: Teachers should be able to override the automatically calculated grades and should be able to make comments.
  • Off-line questions: The answers to these are handed in off-line in the conventional way (e.g., on paper) and teachers enter marks on Moodle.
  • Batch printing of quiz sheets: We want to be able to hand out question sheets to students so they can start working on the questions before going to the computer.
  • Question preview from question edit page: so the teacher can try the question already before saving the changes.
  • Show table of questions on view.php: gives teachers and students a bit of an overview of the quiz.
  • Extending deadlines for individual students: for example when a student misses a deadline for good reasons.
  • Filtering questions by quiz and by search: More ways to restrict which questions are shown on the quiz editing page.
  • Re-open quizzes for revision: After the due date has passed the quiz could allow practice attempts.

UI design / usability

See : Quiz usability portal