Note:

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

Calculated multiquestions proposal: Difference between revisions

From MoodleDocs
No edit summary
 
(11 intermediate revisions by 2 users not shown)
Line 1: Line 1:
This page will be used to document and structure a project of adding multiquestion capability to the calculated question like the cloze questiontype and allowing the calculated questions to be used as usual calculated question or as multiplechoice question.
Working on it now. If you have ideas - you are welcome!
==Intro==
General idea is: We have general set of variables and need to ask many questions related with some theory and this set of variables.
This imply developping a structure similar to the multiquestion 
i.e adding calculated questions records with the main question as parent. An additional multiplechoice record is necessary to each calculated question.


Actually (august 2007) the general interface to create and edit this improved calculated questiontype is almost completed.
Now I'm working about questions for financial field. Which has now up to 20 variables and up to 10 questions.
The real difficulties are to modify the code related to grade each subquestion and store the session and responses parameters.
If we only allowed single response multichoice question, the code will be a quite straightforward version of the multiquestion one.
==Question Object structure==
Here is the proposed structure of the question object after calling get_question_options
records
*main mdl_question
** for each subquestion an mdl_question with the parent field set to main->id
*** for each mdl_question record
**** a mdl_question_calculated and
**** a mdl_question_multichoice
The usual answers records for each question and subquestion 
All the datasets, data_items records are associated with the main question
All these data are structure like the following example.
Probably to simplify the code the main question (#0) data will be repeated in the subquestions 
stdClass Object
(
    [category] => 1
    [parent] => 0
    [name] => my first multichoice
    [questiontext] => What is the surface of a rectangle of {h} m by {l} m ?
{#0}
What is the perimeter of this rectangle?
{#1}
    [questiontextformat] => 1
    [image] =>
    [generalfeedback] =>
    [defaultgrade] => 1
    [penalty] => 0.1
    [qtype] => calculated
    [length] => 1
    [stamp] => 132.208.141.198+070613190851+gCcidQ
    [version] => 132.208.141.198+070812043753+37cXs7
    [hidden] => 0
    [id] => 382
    [maxgrade] => 1
    [name_prefix] => resp382_
    [options] => stdClass Object
        (
            [answers] => Array
                (
                    [1141] => stdClass Object
                        (
                            [question] => 382
                            [answer] => {h}* {l}
                            [fraction] => 1
                            [feedback] => OK
                            [tolerance] => 0.01
                            [tolerancetype] => 1
                            [correctanswerlength] => 2
                            [correctanswerformat] => 1
                            [id] => 1141
                        )
                    [1142] => stdClass Object
                        (
                            [question] => 382
                            [answer] => 2*({h}+{l})
                            [fraction] => 0.5
                            [feedback] => You give me the perimeter !
                            [tolerance] => 0.01
                            [tolerancetype] => 1
                            [correctanswerlength] => 2
                            [correctanswerformat] => 1
                            [id] => 1142
                        )
                    [1143] => stdClass Object
                        (
                            [question] => 382
                            [answer] => {rr}
                            [fraction] => 0
                            [feedback] => Go back and try again
                            [tolerance] => 0.01
                            [tolerancetype] => 1
                            [correctanswerlength] => 2
                            [correctanswerformat] => 1
                            [id] => 1143
                        )
                )
            [multichoice] => stdClass Object
                (
                    [id] => 257
                    [question] => 382
                    [layout] => 0
                    [answers] => 0
                    [single] => 1
                    [shuffleanswers] => 1
                    [correctfeedback] =>
                    [partiallycorrectfeedback] =>
                    [incorrectfeedback] =>
                    [answernumbering] => abc
                )
            [subquestions] => Array
                (
                    [383] => stdClass Object
                        (
                            [category] => 1
                            [parent] => 382
                            [name] => {#1}
                            [questiontext] => {#1}
                            [questiontextformat] => 1
                            [image] =>
                            [generalfeedback] =>
                            [defaultgrade] => 1
                            [penalty] => 0.1
                            [qtype] => calculated
                            [length] => 1
                            [stamp] => 132.208.141.198+070613203735+MrdwQ3
                            [version] => 132.208.141.198+070812043754+Os3B8Z
                            [hidden] => 0
                            [id] => 383
                            [options] => stdClass Object
                                (
                                    [answers] => Array
                                        (
                                            [1144] => stdClass Object
                                                (
                                                    [question] => 383
                                                    [answer] => 2*({h}+{l})
                                                    [fraction] => 1
                                                    [feedback] => OK 1
                                                    [tolerance] => 0.01
                                                    [tolerancetype] => 1
                                                    [correctanswerlength] => 0
                                                    [correctanswerformat] => 1
                                                    [id] => 1144
                                                )
                                        )
                                    [multichoice] => stdClass Object
                                        (
                                            [id] => 258
                                            [question] => 383
                                            [layout] => 0
                                            [answers] => 0
                                            [single] => 0
                                            [shuffleanswers] => 1
                                            [correctfeedback] =>
                                            [partiallycorrectfeedback] =>
                                            [incorrectfeedback] =>
                                            [answernumbering] => abc
                                        )
                                    [units] => Array
                                        (
                                        )
                                )
                            [editasmultichoice] => 0
                        )
                )
            [units] => Array
                (
                )
        )
    [editasmultichoice] => 0
)
==Create and save sessions==
For calculated questions the '''answer''' field of the mdl_question_states record store the responses with the dataset number  followed by the responses the two being separated by -
    function save_session_and_responses(&$question, &$state) {
        $responses = 'dataset'.$state->options->datasetitem.'-'.
        $state->responses[''];
example 'dataset13-2916m2'
For multichoice questiontype
        // The serialized format for multiple choice quetsions
        // is an optional comma separated list of answer ids (the order of the
        // answers) followed by a colon, followed by another comma separated
        // list of answer ids, which are the radio/checkboxes that were
        // ticked.
        // E.g. 1,3,2,4:2,4 means that the answers were shown in the order
        // 1, 3, 2 and then 4 and the answers 2 and 4 were checked.
For multianswer questiontype


The format is similar to the calculated, the order of the questions followed by the responses the two being separated by - , each answer being separated by ,
But for example could be in Geometry field. '''Right circular cone''' which has ''variables'' such as r(base radius), h(height), <math>\theta</math>(aperture)
        $responses = $state->responses;
        // encode - (hyphen) and , (comma) to &#0045; because they are used as
        // delimiters
        array_walk($responses, create_function('&$val, $key',
                '$val = str_replace(array(",", "-"), array("&#0044;", "&#0045;"), $val);
                $val = "$key-$val";'));
        $responses = implode(',', $responses);
this give answer value like 1-8,2-yes,3-1.23,4-15,5-12.


To identify the subquestion and their corresponding answers, in the calculated question type we will use the question->id because the questions (#0,#1 etc.) can be placed everywhere in the text and possibly we could manage a rewowrking of this order in the case that one of the subquestion is dropped.
And ''many questions'' could be such as:
So each question and subquestions will be separated by , and the first term will be the question_id.
This should be coherent with the existing format. There is however a problem because actually there is no conversion of the - and , in the answer in the actual code.
Given that the answer can contain number and the units, almost any character can be present as typing error.


We need a way to identify the new format.
1. Calculate slant height s? (Formula  <math>s=\sqrt{r^2+h^2} </math> )


The only real characteristic is the word '''dataset''' which could be used to identify records from preceeding calculated questiontype if we set new introductory word to the new questiontype. A spontaneous choice is to use DATASET instead of dataset.
2. Calculate volume V? (Formula <math>V=1/3\cdot \pi\cdot r^2\cdot h </math> )


How do we decide in which format we encode the responses.
3. Calculate surface area S? (Formula <math>S=\sqrt{\pi\cdot r\cdot s} </math> ) (depends on 1 question)
We need a flag from the question object for old type calculated questions that do not contain subquestions and do not have an associated multichoice record.
if (isset($question->options->multichoice) && isset($question->options->subquestion) {
  proceed with the new encoding
}else {
  use the old one
}
==Question states problematic==
Get a detailed view of the of grading and storing the results in the database in order to code correctly for a multianswer (or multiquestion) calculated being possibly a multichoice with more than one answer. The (cloze) multianswer questiontype allows only one answer for the multichoice which simplifies the code.Multiple answers (i.e. roots of second degree equation or  multiple responses in various units) can be useful in calculated question.
A first loading of the code from lib/questionlib.php before elimination of details to get the main process.


    $statefields = 'n.questionid as question, s.*, n.sumpenalty, n.manualcomment';
Forum discussion: Multipart calculated questions using students answers in calculating correct answers
    // Load the newest states (sumpenalty, n.manualcomment) for the questions
[http://moodle.org/mod/forum/discuss.php?d=132225]
    $states = $statefields;
    // Load the newest graded states for the questions
    $gradedstates = $statefields;
    // loop through all questions and set the last_graded states
    foreach ($ids as $i) {
        if (isset($states[$i])) {
            '''restore_question_state'''($questions[$i], $states[$i]);
            if (isset($gradedstates[$i])) {
                '''restore_question_state'''($questions[$i], $gradedstates[$i]);
                $states[$i]->last_graded = $gradedstates[$i];
            } else {
                $states[$i]->last_graded = clone($states[$i]);
            }
        } else {
            // If the new attempt is to be based on a previous attempt get it and clean things
            // Having lastattemptid filled implies that (should we double check?):
            //    $attempt->attempt > 1 and $cmoptions->attemptonlast and !$attempt->preview
            if ($lastattemptid) {
                // find the responses from the previous attempt and save them to the new session
                // Load the last graded state for the question
                $statefields = 'n.questionid as question, s.*, n.sumpenalty';
                if (!$laststate = get_record_sql($sql)) {
                    // Only restore previous responses that have been graded
                    continue;
                }
                // Restore the state so that the responses will be restored
                '''restore_question_state'''($questions[$i], $laststate);
                $states[$i] = clone ($laststate);
            } else {
              // create a new empty state
              $states[$i] = new object;
            }
            // now fill/overide initial values
            $states[$i]->attempt = $attempt->uniqueid;
            $states[$i]->question = (int) $i;
            $states[$i]->seq_number = 0;
            $states[$i]->timestamp = $attempt->timestart;
            $states[$i]->event = ($attempt->timefinish) ? QUESTION_EVENTCLOSE : QUESTION_EVENTOPEN;
            $states[$i]->grade = 0;
            $states[$i]->raw_grade = 0;
            $states[$i]->penalty = 0;
            $states[$i]->sumpenalty = 0;
            $states[$i]->manualcomment = '';
            // if building on last attempt we want to preserve responses 
            if (!$lastattemptid) {
              $states[$i]->responses = array('' => '');
            }
            // Prevent further changes to the session from incrementing the
            // sequence number
            $states[$i]->changed = true;
            if ($lastattemptid) {
                // prepare the previous responses for new processing
                $action = new stdClass;
                $action->responses = $laststate->responses;
                $action->timestamp = $laststate->timestamp;
                $action->event = QUESTION_EVENTSAVE; //emulate save of questions from all pages MDL-7631
                // Process these responses ...
                '''question_process_responses'''($questions[$i], $states[$i], $action, $cmoptions, $attempt);
                // Fix for Bug #5506: When each attempt is built on the last one,
                // preserve the options from any previous attempt.
                if ( isset($laststate->options) ) {
                    $states[$i]->options = $laststate->options;
                }
            } else {
                // Create the empty question type specific information
                if (!$QTYPES[$questions[$i]->qtype]->'''create_session_and_responses'''(
                        $questions[$i], $states[$i], $cmoptions, $attempt)) {
                    return false;
                }
            }
            $states[$i]->last_graded = clone($states[$i]);
        }
    }
    return $states;
}
 
/**
* Creates the run-time fields for the states
*
* Extends the state objects for a question by calling
* {@link restore_session_and_responses()}
* @param object $question The question for which the state is needed
* @param object $state The state as loaded from the database
* @return boolean Represents success or failure
*/
function '''restore_question_state'''(&$question, &$state) {
    // initialise response to the value in the answer field
    $state->responses = array('' => addslashes($state->answer));
    unset($state->answer);
    $state->manualcomment = isset($state->manualcomment) ? addslashes($state->manualcomment) : '';
    // Set the changed field to false; any code which changes the
    // question session must set this to true and must increment
    // ->seq_number. The save_question_session
    // function will save the new state object to the database if the field is
    // set to true.
    $state->changed = false;
    // Load the question type specific data
    return $QTYPES[$question->qtype]
            ->'''restore_session_and_responses'''($question, $state);
}
 
=== '''calculated->restore_session_and_responses'''() should handle ALL the question responses===
The actual parent (datasetdependent/abstractype.php) function extract the datasetitem number and the stored single answer.
    function '''restore_session_and_responses'''(&$question, &$state) {
        if (!ereg('^dataset([0-9]+)[^-]*-(.*)$',
                $state->responses[<nowiki>''</nowiki>], $regs)) {
            notify ("Wrongly formatted raw response answer " .
                  "{$state->responses['']}! Could not restore session for " .
                  " question #{$question->id}.");
            $state->options->datasetitem = 1;
            $state->options->dataset = array();
            $state->responses = array('' => '');
            return false;
        }
 
        // Restore the chosen dataset
        $state->options->datasetitem = $regs[1];
        $state->options->dataset =
        $this->pick_question_dataset($question,$state->options->datasetitem);
        $state->responses = array('' => $regs[2]);
        return true;
    }
If we choose the dataset vs DATASET format to distinguish between old and new format, the code should be changed here. A further testing for DATASET will let us with a $regs[2] that could contain , separated answer data giving the question or subquestions id - separated from the answers either in the calculated format (i.e 2456.4cm2) or in the multichoice format (ex.1,2,3:3,4). This will be valid if in the new format the , - and : has been filtered before saving.
 
==Saving the question session==
 
/**
* Saves the current state of the question session to the database
*
* The state object representing the current state of the session for the
* question is saved to the question_states table with ->responses[ '' ] saved
* to the answer field of the database table. The information in the
* question_sessions table is updated.
* The question type specific data is then saved.
* @return mixed          The id of the saved or updated state or false
* @param object $question The question for which session is to be saved.
* @param object $state    The state information to be saved. In particular the
*                        most recent responses are in ->responses. The object
*                        is updated to hold the new ->id.
*/
function save_question_session(&$question, &$state) {
    // Check if the state has changed
    if (!$state->changed && isset($state->id)) {
        return $state->id;
    }
    // Set the legacy answer field
    $state->answer = isset($state->responses['']) ? $state->responses[''] : '';
 
    // Save the state
    if (!empty($state->update)) { // this forces the old state record to be overwritten
        update_record('question_states', $state);
    } else {
        if (!$state->id = insert_record('question_states', $state)) {
            unset($state->id);
            unset($state->answer);
            return false;
        }
    }
 
    // create or update the session
    if (!$session = get_record('question_sessions', 'attemptid',
            $state->attempt, 'questionid', $question->id)) {
        $session->attemptid = $state->attempt;
        $session->questionid = $question->id;
        $session->newest = $state->id;
        // The following may seem weird, but the newgraded field needs to be set
        // already even if there is no graded state yet.
        $session->newgraded = $state->id;
        $session->sumpenalty = $state->sumpenalty;
        $session->manualcomment = $state->manualcomment;
        if (!insert_record('question_sessions', $session)) {
            error('Could not insert entry in question_sessions');
        }
    } else {
        $session->newest = $state->id;
        if (question_state_is_graded($state) or $state->event == QUESTION_EVENTOPEN) {
            // this state is graded or newly opened, so it goes into the lastgraded field as well
            $session->newgraded = $state->id;
            $session->sumpenalty = $state->sumpenalty;
            $session->manualcomment = $state->manualcomment;
        } else {
            $session->manualcomment = addslashes($session->manualcomment);
        }
        update_record('question_sessions', $session);
    }
    unset($state->answer);
    // Save the question type specific state information and responses
    if (!$QTYPES[$question->qtype]->'''save_session_and_responses'''(
    $question, $state)) {
        return false;
    }
    // Reset the changed flag
    $state->changed = false;
    return $state->id;
}

Latest revision as of 13:04, 14 May 2010

Working on it now. If you have ideas - you are welcome! General idea is: We have general set of variables and need to ask many questions related with some theory and this set of variables.

Now I'm working about questions for financial field. Which has now up to 20 variables and up to 10 questions.

But for example could be in Geometry field. Right circular cone which has variables such as r(base radius), h(height), (aperture)

And many questions could be such as:

1. Calculate slant height s? (Formula )

2. Calculate volume V? (Formula )

3. Calculate surface area S? (Formula ) (depends on 1 question)

Forum discussion: Multipart calculated questions using students answers in calculating correct answers [1]