Note:

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

Question Engine 2:How the question engine currently works

From MoodleDocs

This page gives and overview of how the question engine works in Moodle 1.9, and some of the problems inherent in that.

Previous section: Rationale

How this part of the quiz currently works

Before saying what I would like to change, I think it is best to summarise how the question engine currently works.

Database tables

There are three database tables that store data about students' attempts at questions.

question_attempts

This table is basically used to generate unique ids for the other tables relating to question attempts, and allow one to link the data in those tables to the other parts of Moodle that use the question bank, for example from a quiz attempt.

Column Type Comment
id INT(10) NOT NULL AUTO INCREMENT Unique id used to link attempt question data to other things, e.g. quiz_attempts.uniqueid
modulename NOT NULL VARCHAR(20) e.g. 'quiz' the thing linked to.

question_sessions

There is one row in this table for each question in an attempt.

Column Type Comment
id INT(10) NOT NULL AUTO INCREMENT Unique id. Not used much.
attemptid INT(10) NOT NULL REFERENCES question_attempts.id Which attempt this data belongs to.
questionid INT(10) NOT NULL REFERENCES question.id Which question this is the attempt data for.
newest INT(10) NOT NULL REFERENCES question_states.id The latest state of this question in this attempt.
newgraded INT(10) NOT NULL REFERENCES question_states.id The latest graded state of this question in this attempt.
sumpenalty NUMBER(12,7) NOT NULL Used for adaptive mode scoring.
manualcomment TEXT A teacher's manual comment.

(attemptid, questionid) is an alternate key, but that makes MDL-15596 impossible to satisfy.

question_states

There are several rows here for each question session, recording the different states that the question went through as the student attempted it. For example open, save, submit, manual_grade.

Column Type Comment
id INT(10) NOT NULL AUTO INCREMENT Unique id. Not used much.
attempt INT(10) NOT NULL REFERENCES question_attempts.id Which attempt this data belongs to.
question INT(10) NOT NULL REFERENCES question.id Which question this is the attempt data for.
seq_number INT(10) NOT NULL Numbers the states within this question attempt.
answer TEXT NOT NULL A representation of the student's response.
timestamp INT(10) NOT NULL Timestamp of the event that lead to this state.
event INT(4) NOT NULL The type of state this is. One of the QUESTION_EVENTXXX constants from the top of questionlib.php.
grade NUMBER(12,7) NOT NULL The grade that had been earned by this point, after penalties had been taken into account.
raw_grade NUMBER(12,7) NOT NULL The grade that had been earned by this point, before penalties had been taken into account.
penalty NUMBER(12,7) NOT NULL Used by the adaptive mode scoring.

(attempt, question, seq_number) is an alternate key (see the remark in the previous sub-section).

The student's response is stored in the answer column. It is up to each question type to convert the data received from the student into a single string in whatever format seems best, even though the response come from a HTTP post, and so is just an array of key => value pairs, which could easily be stored directly in the database without each question type having to write some nasty string concatenation/parsing code.

Code flow

A part of Moodle, for example the Quiz module, wishes to use questions. First, mod/quiz/attempt.php will render a page of questions by calling

  1. load_questions,
  2. load_question_states, and
  3. print_question one or more times.

All three of these functions delegate part of the work to the appropriate question type.

This outputs each question in its current state. Any form controls that are output have a name="" attribute that is a combination of a prefix determined by the question engine (e.q. q123_), and a main part determined by the question type.

When this page is submitted, the response goes to whichever script the question using code designate. In the case of the quiz it is mod/quiz/processattemptattempt.php. This calls

  1. load_questions,
  2. load_question_states,
  3. question_extract_responses,
  4. question_process_responses, and
  5. save_question_state.
  6. In the case of the quiz, it then saves the quiz_attempts row, which has been updated by some of the above methods.

Again, steps 1., 2., 4. and 5. delegate part of the work to the question types.

Processing manual grading by the teacher is similar. The sequence of calls is:

  1. load_questions,
  2. load_question_states,
  3. question_process_comment, and
  4. save_question_state.
  5. In the case of the quiz, it then saves the quiz_attempts row, which has been updated by some of the above methods.

One thing to note here is that a lot of processing is done before any data can be stored in the database.

Another point to note is that the question type classes are singleton classes. That is, only one instance of each question type is created. All the methods used by the question engine are passed a $question and possibly a $state object to tell the question type which question instance they should be processing. This is good design from several points of view. It works well with batch database queries (efficiency) and the question types to not have state, which might become inconsistent (robustness). However, there is a slight price here, since very rich question types cannot cache state in memory, so they may have to repeat processing (richness <-> efficiency trade off) however, they can store state in the database since they can put whatever they like in the question_states.answer field.

See also

In the next section, Overview, I give the broad outline of how I think this should work in future.