]> git.mjollnir.org Git - moodle.git/commitdiff
New generic tools that allow the creation of dataset-dependent questions
authorkaipe <kaipe>
Fri, 30 Jul 2004 14:50:58 +0000 (14:50 +0000)
committerkaipe <kaipe>
Fri, 30 Jul 2004 14:50:58 +0000 (14:50 +0000)
as well as the new question type Calculated that uses this generic support.

mod/quiz/lib.php
mod/quiz/questiontypes/calculated/calculated.html [new file with mode: 0644]
mod/quiz/questiontypes/calculated/editquestion.php [new file with mode: 0644]
mod/quiz/questiontypes/calculated/modifiednumericalqtype.php [new file with mode: 0644]
mod/quiz/questiontypes/calculated/questiontype.php [new file with mode: 0644]
mod/quiz/questiontypes/datasetdependent/abstractqtype.php [new file with mode: 0644]
mod/quiz/questiontypes/datasetdependent/datasetitems.php [new file with mode: 0644]
mod/quiz/questiontypes/datasetdependent/questiondatasets.html [new file with mode: 0644]

index ac805f729100e3709a44eadd95b439c76047799d..d4aaf13fbc3ce98bdbecfbf10ef7c96a68119a3f 100644 (file)
@@ -22,11 +22,13 @@ define("RANDOMSAMATCH", "6");
 define("DESCRIPTION",   "7");
 define("NUMERICAL",     "8");
 define("MULTIANSWER",   "9");
+define("CALCULATED",   "10");
 
 $QUIZ_QUESTION_TYPE = array ( MULTICHOICE   => get_string("multichoice", "quiz"),
                               TRUEFALSE     => get_string("truefalse", "quiz"),
                               SHORTANSWER   => get_string("shortanswer", "quiz"),
                               NUMERICAL     => get_string("numerical", "quiz"),
+                              CALCULATED    => get_string("calculated", "quiz"),
                               MATCH         => get_string("match", "quiz"),
                               DESCRIPTION   => get_string("description", "quiz"),
                               RANDOM        => get_string("random", "quiz"),
@@ -293,7 +295,6 @@ function quiz_load_questiontypes() {
     }
 }
 
-
 /// FUNCTIONS ///////////////////////////////////////////////////////////////////
 
 function quiz_add_instance($quiz) {
diff --git a/mod/quiz/questiontypes/calculated/calculated.html b/mod/quiz/questiontypes/calculated/calculated.html
new file mode 100644 (file)
index 0000000..8b29125
--- /dev/null
@@ -0,0 +1,177 @@
+<FORM name="theform" method="post" <?php echo $onsubmit ?> action="question.php">
+<CENTER>
+<INPUT type="hidden" name="nextwizardpage" value="<?php p($nextwizardpage)?>"/>
+<?php foreach ($calculatedmessages as $message) {formerr("$message<br/>");} ?>
+<TABLE cellpadding=5>
+<TR valign=top>
+    <TD align=right><P><B><?php  print_string("category", "quiz") ?>:</B></P></TD>
+    <TD>
+    <?php   quiz_category_select_menu($course->id, true, true, $question->category); ?>
+    </TD>
+</TR>
+<TR valign=top>
+    <TD align=right><P><B><?php  print_string("questionname", "quiz") ?>:</B></P></TD>
+    <TD>
+        <INPUT type="text" name="name" size=50 value="<?php  p($question->name) ?>">
+        <?php  if (isset($err["name"])) formerr($err["name"]); ?>
+    </TD>
+</TR>
+<tr valign=top>
+    <td align="right"><p><b><?php  print_string("question", "quiz") ?>:</b></p>
+    <br />
+    <br />
+    <br />
+    <p><font SIZE="1">
+    <?php
+           if ($usehtmleditor) {
+               helpbutton("richtext", get_string("helprichtext"), "moodle", true, true);
+           } else {
+               helpbutton("text", get_string("helptext"), "moodle", true, true);
+           }
+    ?>
+    </font></p>
+    </td>
+    <td>
+        <?php  if (isset($err["questiontext"])) {
+               formerr($err["questiontext"]); 
+               echo "<br />";
+           }
+
+           print_textarea($usehtmleditor, 15, 60, 630, 300, "questiontext", $question->questiontext);
+
+           if ($usehtmleditor) {   /// Trying this out for a while
+               echo '<input type="hidden" name="questiontextformat" value="'.FORMAT_HTML.'">';
+           } else {
+               echo "<div align=right>";
+               print_string("formattexttype");
+               echo ":&nbsp;";
+               if (!isset($question->questiontextformat)) {
+                   $question->questiontextformat = FORMAT_MOODLE;
+               }
+               choose_from_menu(format_text_menu(), "questiontextformat", $question->questiontextformat, "");
+               helpbutton("textformat", get_string("helpformatting"));
+               echo "</div>";
+           }
+        ?>
+    </td>
+</tr>
+<TR valign=top>
+    <TD align=right><P><B><?php  print_string("imagedisplay", "quiz") ?>:</B></P></TD>
+    <TD>
+    <?php   if (empty($images)) {
+            print_string("noimagesyet");
+        } else {
+            choose_from_menu($images, "image", "$question->image", get_string("none"),"","");
+        }
+    ?>
+    </TD>
+</TR>
+
+<TR valign=top>
+<TD align=right><P><B><?php  print_string("correctanswerformula", "quiz") ?>:</B></P></TD>
+    <TD>
+    <INPUT align="LEFT" type="text" id="formula0" name="answer[]" size="20" value="<?php  p($answers[0]->answer) ?>"/>&nbsp;&nbsp;
+    <INPUT type="hidden" name="fraction[]" value="1.0"/>
+    </TD>
+</TR>
+<TR valign=top>
+    <TD align=right><P><B><?php  print_string("tolerance", "quiz"); ?>:</B></P></TD>
+    <TD>
+    <INPUT align="LEFT" type="text" id=tolerance0" name="tolerance[]" size="15" value="<?php  p($answers[0]->tolerance) ?>" />&plusmn;
+    </TD>
+</TR>
+<TR valign=top>
+<TD align=right><P><B><?php  print_string("tolerancetype", "quiz"); ?>:</B></P></TD>
+    <TD>
+    <?php choose_from_menu($qtypeobj->tolerance_types(),
+            'tolerancetype[]', $answers[0]->tolerancetype, false); ?>
+    </TD>
+</TR>
+<TR valign=top>
+<TD align=right><P><B><?php  print_string("correctanswerlength", "quiz"); ?>:</B></P></TD>
+    <TD>
+    <?php choose_from_menu(array('1' => '1', '2' => '2', '3' => '3',
+                                 '4' => '4', '5' => '5', '6' => '6',
+                                 '7' => '7', '8' => '8', '9' => '9',
+                                 '10' => '10'),
+                           'correctanswerlength[]',
+                           $answers[0]->correctanswerlength, false); ?>
+    </TD>
+</TR>
+<TR valign=top>
+    <TD align=right><P><B><?php  print_string("feedback", "quiz") ?>:</B></P></TD>
+    <TD>
+        <textarea name="feedback[]" rows=2 cols=50 wrap="virtual"><?php  p($answers[0]->feedback) ?></textarea>
+    </TD>
+</TR>
+<TR valign=top>
+<TD align=right><P><B><?php  print_string("unit", "quiz") ?>:</B></P></TD>
+    <TD>
+        <P><INPUT type="HIDDEN" name="multiplier[]" value="1.0"/>
+        <INPUT align="LEFT" type="text" id="defaultunit" name="unit[]"
+                size="5" value="<?php  p($units[0]->unit) ?>"/>
+        <B>(<?php  print_string("optional", "quiz") ?>)</B></P>
+    </TD>
+</TR>
+<TR valign=top>
+<td></td>
+<TD align=left><P><B><?php  print_string("alternativeunits", "quiz") ?>:</B></P></TD>
+<td></td>
+</TR>
+<?PHP
+for ($i=1; $i<count($units); $i++) {
+    $unit=$units[$i];
+?>
+<TR valign=top>
+<td></td>
+    <TD align=left>
+        <P><B><?php  print_string("multiplier", "quiz") ?>:</B>
+        <INPUT type="text" id="<?php  p("multiplier$i") ?>" size="10"
+                align="RIGHT" name="multiplier[]"
+                value="<?php  p($unit->multiplier) ?>"/>
+        <B>&nbsp;&nbsp;&nbsp;<?php  print_string("unit", "quiz") ?>:</B>
+        <INPUT align="LEFT" type="text" id="<?php  p("unit$i") ?>"
+                name="unit[]"
+                size="5" value="<?php  p($unit->unit) ?>"/></P>
+    </TD>
+</TR>
+<?PHP
+} /// END for
+?>
+</TABLE>
+
+<INPUT type="hidden" name=id value="<?php  p($question->id) ?>">
+<INPUT type="hidden" name=qtype value="<?php  p($question->qtype) ?>">
+<INPUT type="submit" onClick="return determineMinAndMax();"  value="<?php  print_string("savechanges") ?>">
+</CENTER>
+</FORM>
+<SCRIPT language="JAVASCRIPT">
+function determineMinAndMax() {
+    // This client-side script will determine the values for min and max
+    // based on the input for answer and acceptederror.
+    with(document.theform) {
+        if (formula0.value=='') {
+            alert('<?php  print_string("missingformula","quiz") ?>');
+            return false;
+            /* It could perhaps be an idea to parse the formula here
+             * but as it is necessary at the server anyway, we can
+             * leave like this for the moment. */
+            
+        } else if (''!=defaultunit.value && !isNaN(defaultunit.value)) {
+            alert('<?php  print_string("unitmustnotbenumeric","quiz") ?>');
+            return false;
+        } else if (isNaN(tolerance0.value)) {
+            alert('<?php  print_string("tolerancemustbenumeric","quiz") ?>');
+            return false;
+        } else {
+            return true;
+        }
+    }
+}
+</SCRIPT>
+<?php  
+   if ($usehtmleditor) { 
+       print_richedit_javascript("theform", "questiontext", "no");
+   }
+?>
+
diff --git a/mod/quiz/questiontypes/calculated/editquestion.php b/mod/quiz/questiontypes/calculated/editquestion.php
new file mode 100644 (file)
index 0000000..fcc12d0
--- /dev/null
@@ -0,0 +1,198 @@
+<?PHP // $Id$
+
+// Get a handle to the question type we are dealing with here
+$qtypeobj = $QUIZ_QTYPES[CALCULATED];
+if (isset($form->editdatasets) && $form->editdatasets) {
+    require("$CFG->dirroot/mod/quiz/questiontypes/datasetdependent/datasetitems.php");
+    exit();
+}
+
+$calculatedmessages = array();
+if ($form) {
+
+    // Verify the quality of the question properties
+    if (empty($question->name)) {
+        $calculatedmessages[] = get_string('missingname', 'quiz');
+    }
+    if (empty($question->questiontext)) {
+        $calculatedmessages[] = get_string('missingquestiontext', 'quiz');
+    }
+
+    // Formula stuff (verify some of them)
+    $answers[0]->answer = trim(array_shift($form->answer))
+    and false===($formulaerrors =
+            quiz_qtype_calculated_find_formula_errors($answers[0]->answer))
+    or $answers[0]->answer
+    and $calculatedmessages[] = $formulaerrors
+    or $calculatedmesages[] = get_string('missingformula', 'quiz');
+    
+    $answers[0]->tolerance = array_shift($form->tolerance)
+    or $answers[0]->tolerance = 0.0;
+    is_numeric($answers[0]->tolerance)
+    or $calculatedmessages[] = get_string('tolerancemustbenumeric', 'quiz');
+
+    $answers[0]->feedback = array_shift($form->feedback);
+    
+    // Let's trust the drop down menus.
+
+    $answers[0]->tolerancetype = array_shift($form->tolerancetype);
+    $answers[0]->correctanswerlength = array_shift($form->correctanswerlength);
+    $answers[0]->fraction = array_shift($form->fraction);
+
+    // Fill with remaining answers, in case calculated.html
+    // supports multiple formulas.
+    $i = 1;
+    foreach ($form->answer as $key => $answer) {
+        if (trim($answer)) {
+            $answers[$i]->answer = trim($answer);
+            $answers[$i]->tolerance = $form->tolerance[$key]
+            or $answers[$i]->tolerance = 0.0;
+            $answers[$i]->tolerancetype = $form->tolerancetype[$key];
+            $answers[$i]->correctanswerlength =
+                    $form->correctanswerlength[$key];
+
+            $answers[$i]->fraction = $form->fraction[$key];
+            $answers[$i]->feedback = $form->feedback[$key];
+
+            // Check for errors:
+            false === ($formulaerrors =
+                    quiz_qtype_calculated_find_formula_errors($answer))
+            or $calculatedmessages[] = $formulaerrors;
+            is_numeric($answers[$i]->tolerance)
+            or $calculatedmessages[] = get_string('tolerancemustbenumeric',
+                                                  'quiz');
+            // Increase answer count
+            ++$i;
+        }
+    }
+
+    // Finally the units:
+
+    // Start with the default units...
+    $units[0]->unit = array_shift($form->unit);
+    array_shift($form->multiplier); // In case it is not 1.0
+    $units[0]->multiplier = 1.0; // Must!
+    
+    // Accept other units if they have legal multipliers
+    $i = 1;
+    foreach ($form->multiplier as $key => $multiplier) {
+        if ($multiplier && is_numeric($multiplier)) {
+            $units[$i]->multiplier = $multiplier;
+            $units[$i]->unit = $form->unit[$key];
+            ++$i;
+        }
+    }
+
+
+    if (empty($calculatedmessages)) {
+    // First page calculated.html passed all right!
+
+        if (!empty($form->dataset)) {
+            // Dataset definitions have been set
+            // Save question!
+            $subtypeoptions->answers = $answers;
+            $subtypeoptions->units = $units;
+            $question = $qtypeobj->save_question
+                    ($question, $form, $course, $subtypeoptions);
+            require("$CFG->dirroot/mod/quiz/questiontypes/datasetdependent/datasetitems.php");
+            exit();
+        } else {
+            $datasetmessage = '';
+        }
+        
+        // Now continue by preparing for the second page questiondatasets.html
+        
+        $possibledatasets = $qtypeobj->find_dataset_names(
+                $question->questiontext);
+    
+        $mandatorydatasets = array();
+        foreach ($answers as $answer) {
+            $mandatorydatasets += $qtypeobj
+                    ->find_dataset_names($answer->answer);
+        }
+        
+        $datasets = $qtypeobj->construct_dataset_menus(
+                $question, $mandatorydatasets, $possibledatasets);
+        print_heading_with_help(get_string("choosedatasetproperties", "quiz"), "questiondatasets", "quiz");
+        require("$CFG->dirroot/mod/quiz/questiontypes/datasetdependent/questiondatasets.html");
+        exit();
+    }
+    
+} else {
+// First page in question wizard - calculated.html!
+
+    // The layout of the editing page will only support
+    // one formula alternative for calculated questions.
+    // However, the code behind supports up to six formulas
+    // and the database store and attempt/review framework 
+    // does not have any limit.
+    if (!empty($question->id)) {
+        $answersraw= $qtypeobj->get_answers($question);
+    }
+    $answers= array();
+    for ($i=0; $i<6; $i++) {
+        // Make answer slots with default values
+        $answers[$i]->answer              = "";
+        $answers[$i]->feedback            = "";
+        $answers[$i]->fraction            = "1.0";
+        $answers[$i]->tolerance           = "0.01";
+        $answers[$i]->tolerancetype       = "1";
+        $answers[$i]->correctanswerlength = "2";
+    }
+    if (!empty($answersraw)) {
+        $i=0;
+        foreach ($answersraw as $answer) {
+            $answers[$i] = $answer;
+            $i++;
+        }
+    }
+
+    // Units are handled the same way
+    // as for numerical questions
+    $units = array();
+    for ($i=0 ; $i<6 ; $i++) {
+        // Make unit slots, default as blank...
+        $units[$i]->multiplier = '';
+        $units[$i]->unit = '';
+    }
+    if (!empty($question->id) and $unitsraw = get_records(
+            'quiz_numerical_units', 'question', $question->id)) {
+        /// Find default unit and have it put in the zero slot
+        /// This procedure might be overridden later when
+        /// the unit is stripped form an answer...
+        foreach ($unitsraw as $key => $unit) {
+            if (1.0 == $unit->multiplier) {
+                /// Default unit found:
+                $units[0] = $unit;
+                unset($unitsraw[$key]);
+                break;
+            }
+        }
+        /// Fill remaining answer slots with whatsever left
+        if (!empty($unitsraw)) {
+            $i = 1; // The zero slot got the default unit...
+            foreach ($unitsraw as $unit) {
+                $units[$i] = $unit;
+                $i++;
+            }
+        }
+    } else {
+        $units[0]->multiplier = 1.0;
+    }
+
+    // Strip trailing zeros from multipliers
+    foreach ($units as $i => $unit) {
+        if (ereg('^(.*\\..(.*[^0])?)0+$', $unit->multiplier, $regs1)) {
+            if (ereg('^(.+)\\.0$', $regs1[1], $regs2)) {
+                $units[$i]->multiplier = $regs2[1];
+            } else {
+                $units[$i]->multiplier = $regs1[1];
+            }
+        }
+    }
+}
+
+print_heading_with_help(get_string("editingcalculated", "quiz"), "calculated", "quiz");
+require("calculated.html");
+
+?>
diff --git a/mod/quiz/questiontypes/calculated/modifiednumericalqtype.php b/mod/quiz/questiontypes/calculated/modifiednumericalqtype.php
new file mode 100644 (file)
index 0000000..3f908c8
--- /dev/null
@@ -0,0 +1,38 @@
+<?PHP  // $Id$
+
+///////////////////////////////
+/// CALCULATED HELPER CLASS ///
+///////////////////////////////
+
+/// OVERRIDDEN EDITION OF THE CLASS FOR QUESTION TYPE NUMERICAL ///
+
+class quiz_calculated_qtype_numerical_helper extends quiz_numerical_qtype {
+/// A question with qtype=CALCULATED will appear as a NUMERICAL
+/// question in a quiz and it is therefore desirable to reuse
+/// most of the grading and printing framework.
+/// However, the CALCULATED functions will be fed with data
+/// that differs from what the NUMERICAL qtype can handle.
+/// Therefore the CALCULATED qtype often act by modifying the data
+/// it has been fed and then pass it on to the NUMERICAL equivalent.
+
+/// The NUMERICAL equivalent are called through an instance of this class,
+/// for which the method get_answers has been modified so that its
+/// caller will be fed with data fed to the qtype CALCULATED and then
+/// modified to fit qtype NUMERICAL.
+
+    // This solution assumes a single-threaded environment
+    // for each instance...
+
+    var $answers = false;
+
+    function get_answers($question, $addedcondition='') {
+        return $this->answers;
+    }
+
+    function set_answers($calculatedanswers) {
+        $this->answers = $calculatedanswers;
+    }
+}
+//// END OF CLASS ////
+
+?>
diff --git a/mod/quiz/questiontypes/calculated/questiontype.php b/mod/quiz/questiontypes/calculated/questiontype.php
new file mode 100644 (file)
index 0000000..8daa173
--- /dev/null
@@ -0,0 +1,550 @@
+<?PHP  // $Id$
+
+/////////////////
+/// CALCULATED ///
+/////////////////
+
+/// QUESTION TYPE CLASS //////////////////
+
+require_once("$CFG->dirroot/mod/quiz/questiontypes/datasetdependent/abstractqtype.php");
+class quiz_calculated_qtype extends quiz_dataset_dependent_questiontype {
+
+    // Used by the function custom_generator_tools:
+    var $calcgenerateidhasbeenadded = false;
+    
+    function get_answers($question) {
+        global $CFG;
+        return get_records_sql(
+            "SELECT a.*, c.tolerance, c.tolerancetype, c.correctanswerlength, c.id calcid
+               FROM {$CFG->prefix}quiz_answers a,
+                    {$CFG->prefix}quiz_calculated c
+              WHERE c.question = $question->id AND a.id = c.answer");
+    }
+    
+    function name() {
+        return 'calculated';
+    }
+    
+    function create_virtual_qtype() {
+        require('modifiednumericalqtype.php');
+        return new quiz_calculated_qtype_numerical_helper();
+    }
+
+    function supports_dataset_item_generation() {
+    // Calcualted support generation of randomly distributed number data
+        return true;
+    }
+
+    function custom_generator_tools($datasetdef) {
+        if (ereg('^(uniform|loguniform):([^-]*):([^-]*):([0-9]*)$',
+                $datasetdef->options, $regs)) {
+            for ($i = 0 ; $i<10 ; ++$i) {
+                $lengthoptions[$i] = get_string(($regs[1] == 'uniform'
+                                                ? 'decimals'
+                                                : 'significantfigures'), 'quiz', $i);
+            }
+            return '<INPUT TYPE="submit" onClick="'
+                    . "document.addform.regenerateddefid.value='$datasetdef->id'; return true;"
+                    .'" VALUE="'. get_string('generatevalue', 'quiz') . '"/><br/>'
+                    . '<INPUT TYPE="text" SIZE="3" NAME="calcmin[]" '
+                    . " VALUE=\"$regs[2]\"/> &amp; <INPUT NAME=\"calcmax[]\" "
+                    . ' TYPE="text" SIZE="3" VALUE="' . $regs[3] .'"/> '
+                    . choose_from_menu($lengthoptions, 'calclength[]',
+                                       $regs[4], // Selected
+                                       '', '', '', true) . '<br/>'
+                    . choose_from_menu(array('uniform' => get_string('uniform', 'quiz'),
+                                             'loguniform' => get_string('loguniform', 'quiz')),
+                                       'calcdistribution[]',
+                                       $regs[1], // Selected
+                                       '', '', '', true);
+        } else {
+            return '';
+        }
+    }
+
+    function update_dataset_options($datasetdefs, $form) {
+        // Do we have informatin about new options???
+        if (empty($form->definition) || empty($form->calcmin)
+                || empty($form->calcmax) || empty($form->calclength)
+                || empty($form->calcdistribution)) {
+            // I gues not:
+
+        } else {
+            // Looks like we just could have some new information here
+            foreach ($form->definition as $key => $defid) {
+                if (isset($datasetdefs[$defid])
+                        && is_numeric($form->calcmin[$key])
+                        && is_numeric($form->calcmax[$key])
+                        && is_numeric($form->calclength[$key])) {
+                    switch     ($form->calcdistribution[$key]) {
+                        case 'uniform': case 'loguniform':
+                            $datasetdefs[$defid]->options =
+                                    $form->calcdistribution[$key] . ':'
+                                    . $form->calcmin[$key] . ':'
+                                    . $form->calcmax[$key] . ':'
+                                    . $form->calclength[$key];
+                            break;
+                        default:
+                            notify("Unexpected distribution $form->calcdistribution[$key]");
+                    }
+                }
+            }
+        }
+
+        // Look for empty options, on which we set default values
+        foreach ($datasetdefs as $def) {
+            if (empty($def->options)) {
+                $datasetdefs[$def->id]->options = 'uniform:1.0:10.0:1';
+            }
+        }
+        return $datasetdefs;
+    }
+
+    function generate_dataset_item($options) {
+        if (!ereg('^(uniform|loguniform):([^-]*):([^-]*):([0-9]*)$',
+                $options, $regs)) {
+            // Unknown options...
+            return false;
+        }
+        if ($regs[1] == 'uniform') {
+            $nbr = $regs[2] + ($regs[3]-$regs[2])*mt_rand()/mt_getrandmax();
+            return round($nbr, $regs[4]);
+
+        } else if ($regs[1] == 'loguniform') {
+            $log0 = log(abs($regs[2])); // It would have worked the other way to
+            $nbr = exp($log0 + (log(abs($regs[3])) - $log0)*mt_rand()/mt_getrandmax());
+
+            // Reformat according to the precision $regs[4]:
+
+            // Determine the format 0.[1-9][0-9]* for the nbr... 
+            $p10 = 0;
+            while ($nbr < 1) {
+                --$p10;
+                $nbr *= 10;
+            }
+            while ($nbr >= 1) {
+                ++$p10;
+                $nbr /= 10;
+            }
+            // ... and have the nbr rounded of to the correct length
+            $nbr = round($nbr, $regs[4]);
+
+            // Have the nbr written on a suitable format,
+            // Either scientific or plain numeric
+            if (-2 > $p10 || 4 < $p10) {
+                // Use scientific format:
+                $eX = 'e'.--$p10;
+                $nbr *= 10;
+                if (1 == $regs[4]) {
+                    $nbr = $nbr.$eX;
+                } else {
+                    // Attach additional zeros at the end of $nbr,
+                    $nbr .= (1==strlen($nbr) ? '.' : '')
+                            . '00000000000000000000000000000000000000000x';
+                    $nbr = substr($nbr, 0, $regs[4] +1).$eX;
+                }
+            } else {
+                // Stick to plain numeric format
+                $nbr *= "1e$p10";
+                if (0.1 <= $nbr / "1e$regs[4]") {
+                    $nbr = $nbr;
+                } else {
+                    // Could be an idea to add some zeros here
+                    $nbr .= (ereg('^[0-9]*$', $nbr) ? '.' : '')
+                            . '00000000000000000000000000000000000000000x';
+                    $oklen = $regs[4] + ($p10 < 1 ? 2-$p10 : 1);
+                    $nbr = substr($nbr, 0, $oklen);
+                }
+            }
+
+            // The larger of the values decide the sign in case the 
+            // have equal different signs (which they really must not have)
+            if ($regs[2] + $regs[3] > 0) {
+                return $nbr;
+            } else {
+                return -$nbr;
+            }
+
+        } else {
+            error("The distribution $regs[1] caused problems");
+        }
+        return '';
+    }
+
+    function comment_header($question) {
+        $answers = $this->get_answers($question);
+        $strheader = '';
+        $delimiter = '';
+        foreach ($answers as $answer) {
+            $strheader .= $delimiter.$answer->answer;
+            $delimiter = ',';
+        }
+        return $strheader;
+    }
+
+    function comment_on_datasetitems($question, $data, $number) {
+
+        /// Find a default unit:
+        if ($unit = get_record('quiz_numerical_units',
+                'question', $question->id, 'multiplier', 1.0)) {
+            $unit = $unit->unit;
+        } else {
+            $unit = '';
+        }
+
+        // Get answers
+        $answers = $this->get_answers($question);
+        $stranswers = get_string('answer', 'quiz');
+        $strmin = get_string('min', 'quiz');
+        $strmax = get_string('max', 'quiz');
+        $errors = '';
+        $delimiter = ': ';
+        foreach ($answers as $answer) {
+            $calculated = quiz_qtype_calculated_calculate_answer(
+                    $answer->answer, $data, $answer->tolerance,
+                    $answer->tolerancetype, $answer->correctanswerlength, $unit);
+            if ($calculated->min === '') {
+                // This should mean that something is wrong
+                $errors .= " -$calculated->answer";
+                $stranswers .= $delimiter;
+            } else {
+                $stranswers .= $delimiter.$calculated->answer;
+            }
+            $strmin .= $delimiter.$calculated->min;
+            $strmax .= $delimiter.$calculated->max;
+            $delimiter = ', ';
+        }
+        return "$stranswers<br/>$strmin<br/>$strmax<br/>$errors";
+    }
+
+    function tolerance_types() {
+        return array('1'  => get_string('relative', 'quiz'),
+                     '2'  => get_string('nominal', 'quiz'),
+                     '3'  => get_string('geometric', 'quiz'));
+    }
+
+    function save_question_options($question, $options) {
+        // Get old answers:
+        $oldanswers = $this->get_answers($question)
+        or $oldanswers = array(); // if there are none
+
+        // Update with new answers
+        $answerrec->question = $calcrec->question = $question->id;
+        foreach ($options->answers as $newanswer) {
+            $answerrec->answer = $newanswer->answer;
+            $answerrec->fraction = $newanswer->fraction;
+            $answerrec->feedback = $newanswer->feedback;
+            $calcrec->tolerance = $newanswer->tolerance;
+            $calcrec->tolerancetype = $newanswer->tolerancetype;
+            $calcrec->correctanswerlength = $newanswer->correctanswerlength;
+            if ($oldanswer = array_shift($oldanswers)) {
+                // Reuse old record:
+                $calcrec->answer = $answerrec->id = $oldanswer->id;
+                $calcrec->id = $oldanswer->calcid;
+                if (!update_record('quiz_answers', $answerrec)) {
+                    error("Unable to update answer for calculated question $question->name");
+                } else {
+                    // notify("Answer updated successfully for calculated question $question->name");
+                }
+                if (!update_record('quiz_calculated', $calcrec)) {
+                    error("Unable to update options calculared question $question->name");
+                } else {
+                    // notify("Options updated successfully for calculated question $question->name");
+                }
+            } else {
+                unset($answerrec->id);
+                unset($calcrec->id);
+                if (!($calcrec->answer = insert_record('quiz_answers',
+                                                       $answerrec))) {
+                    error("Unable to insert answer for calculated question $question->name");
+                } else {
+                    // notify("Answer inserted successfully for calculated question $question->name");
+                }
+                if (!insert_record('quiz_calculated', $calcrec)) {
+                    error("Unable to insert options calculared question $question->name");
+                } else {
+                    // notify("Options inserted successfully for calculated question $question->name");
+                }
+            }
+        }
+
+        // Delete excessive records:
+        foreach ($oldanswers as $oldanswer) {
+            if (!delete_records('quiz_answers', 'id', $oldanswer->id)) {
+                error("Unable to delete old answers for calculated question $question->name");
+            } else {
+                // notify("Old answers deleted successfully for calculated question $question->name");
+            }
+            if (!delete_records('quiz_calculated', 'id', $oldanswer->calcid)) {
+                error("Unable to delete old options for calculated question $question->name");
+            } else {
+                // notify("Old options deleted successfully for calculated question $question->name");
+            }
+        }
+
+        // Get old units (just like for numerical questions)
+        $oldunits = get_records('quiz_numerical_units',
+                                'question', $question->id)
+        or $oldunits = array(); // if there are none
+        if (1 == count($options->units) && !$options->units[0]->unit) {
+            /// Only default unit and it is empty, so drop it:
+            $options->units = array();
+        }
+        foreach ($options->units as $newunit) {
+            $newunit->question = $question->id;
+            if ($oldunit = array_shift($oldunits)) {
+                $newunit->id = $oldunit->id;
+                if (!update_record('quiz_numerical_units', $newunit)) {
+                    error("Unable to update unit $newunit->unit for $question->name");
+                } else {
+                    // notify("Unit $newunit->unit was updated successfully for $question->name");
+                }
+            } else {
+                if (!insert_record('quiz_numerical_units', $newunit)) {
+                    error("Unable to insert unit $newunit->unit for $question->name");
+                } else {
+                    // notify("Unit $newunit->unit was inserted successfully for question $question->name");
+                }
+            }
+        }
+        
+        // Delete excessive unit records
+        foreach ($oldunits as $oldunit) {
+            if (!delete_records('quiz_numerical_units', 'id', $oldunit->id)) {
+                error("Unable to delete old unit $oldunit->unit for question $question->name");
+            } else {
+                notify("Deleted old unit $oldunit->unit successfully for question $question->name");
+            }
+        }
+
+        return true;
+    }
+    
+    function dataset_options($question, $name, $renameabledatasets=false) {
+    // Takes datasets from the parent implementation but
+    // filters options that are currently not accepted by calculated
+    // It also determines a default selection...
+        list($options, $selected) = parent::dataset_options($question, $name);
+        foreach ($options as $key => $whatever) {
+            if (!ereg('^'.LITERAL.'-', $key) && $key != '0') {
+                unset($options[$key]);
+            }
+        }
+        if (!$selected) {
+            $selected = LITERAL . "-0-$name"; // Default
+        }
+        return array($options, $selected);
+    }
+
+    function construct_dataset_menus($question, $mandatorydatasets,
+                                     $optionaldatasets) {
+        $datasetmenus = array();
+        foreach ($mandatorydatasets as $datasetname) {
+            if (!isset($datasetmenus[$datasetname])) {
+                list($options, $selected) =
+                        $this->dataset_options($question, $datasetname);
+                unset($options['0']); // Mandatory...
+                $datasetmenus[$datasetname] = choose_from_menu ($options,
+                        'dataset[]', $selected, '', '', "0", true);
+            }
+        }
+        foreach ($optionaldatasets as $datasetname) {
+            if (!isset($datasetmenus[$datasetname])) {
+                list($options, $selected) =
+                        $this->dataset_options($question, $datasetname);
+                $datasetmenus[$datasetname] = choose_from_menu ($options,
+                        'dataset[]', $selected, '', '', "0", true);
+            }
+        }
+        return $datasetmenus;
+    }
+
+    function grade_response($question, $nameprefix) {
+        /// Determines the answers and then lets the
+        /// NUMERICAL question type take care of the
+        /// grading...
+        
+        list($datasetnumber, $individualdata) =
+        $this->parse_datasetinput($question->response[$nameprefix]);
+
+        // find the raw answer material
+        global $CFG;
+        if (!($answers = $this->get_answers($question))) {
+            notify("Error no answers found for question $question->id");
+        }
+        
+        /// Find a default unit:
+        if ($unit = get_record('quiz_numerical_units',
+                'question', $question->id, 'multiplier', 1.0)) {
+            $unit = $unit->unit;
+        } else {
+            $unit = '';
+        }
+        
+        // Construct answers for the numerical question type
+        foreach ($answers as $aid => $answer) {
+            $answernumerical = quiz_qtype_calculated_calculate_answer(
+                    $answer->answer, $individualdata,
+                    $answer->tolerance, $answer->tolerancetype,
+                    $answer->correctanswerlength, $unit);
+            $answers[$aid]->answer = $answernumerical->answer;
+            $answers[$aid]->min = $answernumerical->min;
+            $answers[$aid]->max = $answernumerical->max;
+        }
+
+        // Forward the grading to the virtual qtype
+        $virtualnameprefix = $this->create_virtual_nameprefix(
+                $nameprefix, $question->response[$nameprefix]);
+        unset($question->response[$nameprefix]);
+        $virtualqtype = $this->get_virtual_qtype();
+        $virtualqtype->set_answers($answers);
+        return $virtualqtype->grade_response($question,
+                                             $virtualnameprefix);
+    }
+}
+//// END OF CLASS ////
+
+//////////////////////////////////////////////////////////////////////////
+//// INITIATION - Without this line the question type is not in use... ///
+//////////////////////////////////////////////////////////////////////////
+$QUIZ_QTYPES[CALCULATED]= new quiz_calculated_qtype();
+
+function quiz_qtype_calculated_calculate_answer($formula, $individualdata,
+        $tolerance, $tolerancetype, $answerlength, $unit='') {
+/// The return value has these properties:
+/// ->answer    the correct answer
+/// ->min       the lower bound for an acceptable response
+/// ->max       the upper bound for an accetpable response
+    
+    /// Exchange formula variables with the correct values...
+    foreach ($individualdata as $name => $value) {
+        $formula = str_replace('{'.$name.'}', "($value)", $formula);
+    }
+    if (ereg('\\{([^}]*)\\}', $formula, $regs)) {
+        // This is the normal case for a recently added question.
+        // Return a notification about it
+        $calculated->answer = $calculated->min = $calculated->max = '';
+        return $calculated;
+        
+    } else if (ereg('^(.*([^- 0-9+*./()eE])).*$', $formula, $regs)) {
+        $calculated->answer = get_string('syntaxerror', 'quiz', $regs[1]);
+        $calculated->min = $calculated->max = '';
+        return $calculated;
+    }
+
+    /// Calculate the correct answer
+    eval('$answer = '.$formula.';');
+
+    /// Calculate min and max
+    switch ($tolerancetype) {
+        case '1': case 'relative':
+            /// Recalculate the tolerance and fall through
+            /// to the nominal case:
+            $tolerance = $answer * $tolerance;
+            
+            // Falls through to the nominal case -
+        case '2': case 'nominal':
+            $tolerance = abs($tolerance); // important
+            $max = $answer + $tolerance;
+            $min = $answer - $tolerance;
+            break;
+
+        case '3': case 'geometric':
+            $quotient = 1 + abs($tolerance);
+            if ($answer >= 0) {
+                $max = $answer * $quotient;
+                $min = $answer / $quotient;
+            } else {
+                $min = $answer * $quotient;
+                $max = $answer / $quotient;
+            }
+            break;
+
+        default:
+            error("Unknown tolerance type $tolerancetype");
+    }
+    $calculated->min    = $min;
+    $calculated->max    = $max;
+
+    /// Adjust the number of significant digits for the correct answer
+    if ($answer) { // Applies only if the result is non-zero
+
+        // Convert to positive answer...
+        if ($answer < 0) {
+            $answer = -$answer;
+            $sign = '-';
+        } else {
+            $sign = '';
+        }
+
+        // Determine the format 0.[1-9][0-9]* for the answer... 
+        $p10 = 0;
+        while ($answer < 1) {
+            --$p10;
+            $answer *= 10;
+        }
+        while ($answer >= 1) {
+            ++$p10;
+            $answer /= 10;
+        }
+        // ... and have the answer rounded of to the correct length
+        $answer = round($answer, $answerlength);
+
+        // Have the answer written on a suitable format,
+        // Either scientific or plain numeric
+        if (-2 > $p10 || 4 < $p10) {
+            // Use scientific format:
+            $eX = 'e'.--$p10;
+            $answer *= 10;
+            if (1 == $answerlength) {
+                $calculated->answer = $sign.$answer.$eX.$unit;
+            } else {
+                // Attach additional zeros at the end of $answer,
+                $answer .= (1==strlen($answer) ? '.' : '')
+                        . '00000000000000000000000000000000000000000x';
+                $calculated->answer = $sign
+                        .substr($answer, 0, $answerlength +1).$eX.$unit;
+            }
+        } else {
+            // Stick to plain numeric format
+            $answer *= "1e$p10";
+            if (0.1 <= $answer / "1e$answerlength") {
+                $calculated->answer = $sign.$answer.$unit;
+            } else {
+                // Could be an idea to add some zeros here
+                $answer .= (ereg('^[0-9]*$', $answer) ? '.' : '')
+                        . '00000000000000000000000000000000000000000x';
+                $oklen = $answerlength + ($p10 < 1 ? 2-$p10 : 1);
+                $calculated->answer = $sign.substr($answer, 0, $oklen).$unit;
+            }
+        }
+    } else {
+        $calculated->answer = 0.0;
+    }
+    
+    /// Return the result
+    return $calculated;
+}
+
+function quiz_qtype_calculated_find_formula_errors($formula) {
+/// Validates the formula submitted from the question edit page.
+/// Returns false if everything is alright.
+/// Otherwise it constructs an error message
+
+    // Strip away dataset names
+    while (ereg('\\{[[:alpha:]][^>} <{"\']*\\}', $formula, $regs)) {
+        $formula = str_replace($regs[0], '1', $formula);
+    }
+
+
+    if (!ereg('[^- 0-9+*/:.(>?)!e=|<E&]+', $formula, $regs)) {
+        // Need to work more on this check -
+        // It is too strict and still not very effetive...
+        return false; // Need to work more on this one
+        
+    } else {
+        return get_string('illegalformulasyntax', 'quiz', $regs[0]);
+    }
+}
+
+?>
diff --git a/mod/quiz/questiontypes/datasetdependent/abstractqtype.php b/mod/quiz/questiontypes/datasetdependent/abstractqtype.php
new file mode 100644 (file)
index 0000000..b63b90a
--- /dev/null
@@ -0,0 +1,534 @@
+<?PHP  // $Id$
+
+///////////////////////////////////////////////////////////////
+/// ABSTRACT SUPERCLASS FOR QUSTION TYPES THAT USE DATASETS ///
+///////////////////////////////////////////////////////////////
+
+define("LITERAL", "1");
+define("FILE", "2");
+define("LINK", "3");
+
+class quiz_dataset_dependent_questiontype extends quiz_default_questiontype {
+
+    var $virtualqtype= false;
+
+    // Contains picked dataset numbers by category. The idea is to
+    // reuse dataset number for each category within a quiz.
+    var $datasetnumbers = array();
+
+    function name() {
+        return 'datasetdependent';
+    }
+    
+    function create_virtual_qtype() {
+        error("No vitrual question type for question type ".$this->name());
+    }
+
+    function get_virtual_qtype() {
+        if (!$this->virtualqtype) {
+            $this->virtualqtype = $this->create_virtual_qtype();
+        }
+        return $this->virtualqtype;
+    }
+
+    function comment_header($question) {
+    // Used by datasetitems.php
+        // Default returns nothing and thus takes away the column
+        return '';
+    }
+
+    function comment_on_datasetitems($question, $data, $number) {
+    // Used by datasetitems.php
+        // Default returns nothing
+        return '';
+    }
+
+    function supports_dataset_item_generation() {
+    // Used by datasetitems.php
+        // Default does not support any item generation
+        return false;
+    }
+
+    function custom_generator_tools($datasetdef) {
+    // Used by datasetitems.php
+        // If there is no generation support,
+        // there cannot possibly be any custom tools either
+        return '';
+    }
+
+    function generate_dataset_item($options) {
+    // Used by datasetitems.php
+        // By default nothing is generated
+        return '';
+    }
+
+    function update_dataset_options($datasetdefs, $form) {
+    // Used by datasetitems.php
+    // Returns the updated datasets
+        // By default the dataset options cannot be updated
+        return $datasetdefs;
+    }
+
+    function dataset_options($question, $name) {
+        
+        // First options - it is not a dataset...
+        $options['0'] = get_string('nodataset', 'quiz');
+        
+        // Construct question local options
+        global $CFG;
+        $currentdatasetdef = get_record_sql(
+                "SELECT a.*
+                   FROM {$CFG->prefix}quiz_dataset_definitions a,
+                        {$CFG->prefix}quiz_question_datasets b
+                  WHERE a.id = b.datasetdefinition
+                    AND b.question = '$question->id'
+                    AND a.name = '$name'")
+        or $currentdatasetdef->type = '0';        
+        foreach (array( LITERAL, FILE, LINK) as $type) {
+            $key = "$type-0-$name";
+            if ($currentdatasetdef->type == $type
+                    and $currentdatasetdef->category == 0) {
+                $options[$key] = get_string('keptlocal', 'quiz', $type);
+            } else {
+                $options[$key] = get_string('newlocal', 'quiz', $type);
+            }
+        }
+        
+        // Construct question category options
+        $categorydatasetdefs = get_records_sql(
+                "SELECT a.type, a.id
+                   FROM {$CFG->prefix}quiz_dataset_definitions a,
+                        {$CFG->prefix}quiz_question_datasets b
+                  WHERE a.id = b.datasetdefinition
+                    AND a.category = '$question->category'
+                    AND a.name = '$name'");
+        foreach(array( LITERAL, FILE, LINK) as $type) {
+            $key = "$type-$question->category-$name";
+            if (isset($categorydatasetdefs[$type])
+                    and $categorydef = $categorydatasetdefs[$type]) {
+                if ($currentdatasetdef->type == $type
+                        and $currentdatasetdef->id == $categorydef->id) {
+                    $options[$key] = get_string('keptcategory', 'quiz', $type);
+                } else {
+                    $options[$key] = get_string('existingcategory',
+                                                'quiz', $type);
+                }
+            } else {
+                $options[$key] = get_string('newcategory', 'quiz', $type);
+            }
+        }
+        
+        // All done!
+        return array($options, $currentdatasetdef->type
+                ? "$currentdatasetdef->type-$currentdatasetdef->category-$name"
+                : '');
+    }
+
+    function save_question_options($question, $options) {
+        // Default does nothing...
+        return true;
+    }
+    
+    function save_question($question, $form, $course, $subtypeoptions=false) {
+    // For dataset dependent questions a wizard is used for editing
+    // questions. Therefore calls from question.php are ignored.
+    // Instead questions are saved when this method is called by
+    // editquestion.php
+    
+        if ($subtypeoptions) {
+            // Let's save the question
+            // We need to save the question first (in order to get the id)
+            // We then save the dataset definitions and finally we
+            // save the subtype options...
+            
+            // Save question
+            if (!empty($question->id)) { // Question already exists
+                $question->version++;  // Update version number of question
+                if (!update_record("quiz_questions", $question)) {
+                    error("Could not update question!");
+                }
+            } else {   // Question is a new one
+                // Set the unique code (not to be changed)
+                $question->stamp = make_unique_id_code();
+                $question->version = 1;
+                if (!$question->id=insert_record("quiz_questions", $question)) {
+                    error("Could not insert new question!");
+                }
+            }
+            
+            // Save datasets
+            global $CFG;
+            $datasetdefinitions = get_records_sql( // Indexed by name...
+                "SELECT a.name, a.id, a.type, a.category
+                  FROM {$CFG->prefix}quiz_dataset_definitions a,
+                       {$CFG->prefix}quiz_question_datasets b
+                  WHERE a.id = b.datasetdefinition
+                    AND b.question = $question->id");
+
+            foreach ($form->dataset as $dataset) {
+                if (!$dataset) {
+                    continue; // The no dataset case...
+                }
+                
+                list($type, $category, $name) = explode('-', $dataset, 3);
+
+                if (isset($datasetdefinitions[$name])
+                        and $datasetdefinitions[$name]->type == $type
+                        and $datasetdefinitions[$name]->category == $category) {
+                    // Keep this dataset as it already fulfills our dreams
+                    // by preventing it from being deleted
+                    unset($datasetdefinitions[$name]);
+                    continue;
+                }
+
+                // We need to create a new datasetdefinition
+                unset ($datasetdef);
+                $datasetdef->type = $type;
+                $datasetdef->name = $name;
+                $datasetdef->category = $category;
+
+                if (!$datasetdef->id = insert_record(
+                        'quiz_dataset_definitions', $datasetdef)) {
+                    error("Unable to create dataset $name");
+                }
+
+                if ($category) {
+                    // We need to look for already existing 
+                    // datasets in the category.
+                    // By first creating the datasetdefinition above we
+                    // can manage to automatically take care of
+                    // some possible realtime concurrence
+                    while ($olderdatasetdef = get_record_select(
+                            'quiz_dataset_definitions',
+                            " type = '$type' AND name = '$name'
+                            AND category = '$category'
+                            AND id < $datasetdef->id ")) {
+                        // Use older dataset instead:
+                        delete_records('quiz_dataset_definitions',
+                                       'id', $datasetdef->id);
+                        $datasetdef = $olderdatasetdef;
+                    }
+                }
+                
+                // Create relation to this dataset:
+                unset($questiondataset);
+                $questiondataset->question = $question->id;
+                $questiondataset->datasetdefinition = $datasetdef->id;
+                if (!insert_record('quiz_question_datasets',
+                                   $questiondataset)) {
+                    error("Unable to create relation to dataset $name");
+                }
+            }
+
+            // Remove local obsolete datasets as well as relations
+            // to datasets in other categories:
+            if (!empty($datasetdefinitions)) {
+                foreach ($datasetdefinitions as $def) {
+                    delete_records('quiz_question_datasets',
+                                   'question', $question->id,
+                                   'datasetdefinition', $def->id);
+
+                    if ($def->category == 0) { // Question local dataset
+                        delete_records('quiz_dataset_definitions', 'id', $def->id);
+                        delete_records('quiz_dataset_items',
+                                       'definition', $def->id);
+                    }
+                }
+            }
+            
+            // Save subtype options
+            $this->save_question_options($question, $subtypeoptions);
+            return $question;
+            
+        } else if (empty($form->editdatasets)) {
+            // Parse for common question entries and
+            // continue with editquestion.php by returning the question
+            $question->name               = $form->name;
+            $question->questiontext       = $form->questiontext;
+            $question->questiontextformat = $form->questiontextformat;
+            if (empty($form->image)) {
+                $question->image = "";
+            } else {
+                $question->image = $form->image;
+            }
+            if (isset($form->defaultgrade)) {
+                $question->defaultgrade = $form->defaultgrade;
+            }
+            return $question;
+        } else {
+            return $question;
+        }
+    }
+
+    function find_dataset_names($text) {
+    /// Returns the possible dataset names found in the text as an array
+    /// The array has the dataset name for both key and value
+        $datasetnames = array();
+        while (ereg('\\{([[:alpha:]][^>} <{"\']*)\\}', $text, $regs)) {
+            $datasetnames[$regs[1]] = $regs[1];
+            $text = str_replace($regs[0], '', $text);
+        }
+        return $datasetnames;
+    }
+
+    function convert_to_response_answer_field($questionresponse) {
+        // It does not look like all platforms support the ksort strategi
+        // so gotta try something else...
+        foreach ($questionresponse as $key => $response) {
+            if (!isset($shortestkey)
+                    || strlen($shortestkey) > strlen($key)) {
+                $shortestkey = $key;
+            }
+        }
+        $dataset = $questionresponse[$shortestkey];
+        unset($questionresponse[$shortestkey]);
+        $virtualqtype = $this->get_virtual_qtype();
+        return "dataset$dataset-" . $virtualqtype
+                ->convert_to_response_answer_field($questionresponse);
+    }
+
+    function create_response($question, $nameprefix, $questionsinuse) {
+    /// This method must pick a dataset and have its number and
+    /// data injected in the response keys
+
+
+        // First we retrieve the dataset definitions for this questions
+        // and check how many datasets we have available ($maxnumber)
+        global $CFG;
+        $datasetdefinitions = get_records_sql(
+            "SELECT a.* FROM {$CFG->prefix}quiz_dataset_definitions a,
+                             {$CFG->prefix}quiz_question_datasets b
+             WHERE a.id = b.datasetdefinition
+               AND b.question = $question->id");
+        $definitionids = $delimiter = '';
+        foreach ($datasetdefinitions as $datasetdef) {
+            $definitionids .= $delimiter.$datasetdef->id;
+            $delimiter = ',';
+            if (!isset($maxnumber) || $datasetdef->itemcount < $maxnumber) {
+                $maxnumber = $datasetdef->itemcount;
+            }
+        }
+        
+        // We then pick dataset number and retrieve the datasetitems
+        if (!isset($maxnumber) || 0 == $maxnumber) {
+            notify("Error: Question $question->id does not
+                    have items for its datasets");
+            $datasetinput = 0;
+            $datasetitems = array();
+            
+        } else {
+            isset($this->datasetnumbers[$question->category])
+            and   $this->datasetnumbers[$question->category] <= $maxnumber
+            or    $this->datasetnumbers[$question->category] =
+                    quiz_qtype_dataset_pick_new($question->category,
+                                                $maxnumber);
+            $datasetinput = $this->datasetnumbers[$question->category];
+            $datasetitems = get_records_select('quiz_dataset_items',
+                    "definition in ($definitionids)
+                    AND number = $datasetinput");
+        }
+        
+        // Build the rest of $datasetinput
+        foreach ($datasetitems as $item) {
+            $datasetdef = $datasetdefinitions[$item->definition];
+            
+            // We here need to pay attention to whether the
+            // data item is a link or an ordinary literal
+            if ($datasetdef->type == LITERAL) {
+                // The ordinary simple case
+                $value = $item->value;
+
+            } else {
+                $icon = '<IMG SRC="../../files/pix/'
+                        . mimeinfo('icon', $item->value) . '/>';
+                if (substr(strtolower($item->value), 0, 7)=='http://') {
+                    $link = $item->value;
+                        
+                } else {
+                    global $quiz; // Try to reach this info globally
+                    if ($CFG->slasharguments) {
+                        // Use this method if possible for better caching
+                        $link = "$CFG->wwwroot/mod/quiz/quizfile.php/"
+                                . "$quiz->id/$question->id/$item->value";
+
+                    } else {
+                        $link = "$CFG->wwwroot/mod/quiz/quizfile.php?file=/"
+                                . "$quiz->id/$question->id/$item->value";
+                    }
+                }
+                $value = '<a target="_blank" href="' . $link
+                . "\" title=\"$datasetdef->name\">$icon$item->value</a>";
+            }
+
+            $datasetinput .= ';' . base64_encode($datasetdef->name)
+                          . ':' . base64_encode($value);
+        }
+
+        // Use the virtual question type and have it ->create_response:
+        $virtualqtype = $this->get_virtual_qtype();
+        $response = $virtualqtype->create_response($question,
+                $this->create_virtual_nameprefix($nameprefix, $datasetinput),
+                $questionsinuse);
+        $response[$nameprefix] = $datasetinput;
+        return $response;
+    }
+
+    function create_virtual_nameprefix($nameprefix, $datasetinput) {
+    // This default implementation is sometimes overridden
+        if (!ereg('([0-9]+)' . $this->name() . '$', $nameprefix, $regs)) {
+            error("Malformed nameprefix $nameprefix");
+        }
+        $virtualqtype = $this->get_virtual_qtype();
+        return $nameprefix . $regs[1] . $virtualqtype->name();
+    }
+        // Default implementation that sometimes can overridden
+
+    function extract_response($rawresponse, $nameprefix) {
+        if (!ereg('^dataset([;:0-9A-Za-z+/=]+)-(.*)$',
+                $rawresponse->answer, $regs)) {
+            error ("Malformated raw response answer $rawresponse->answer");
+        }
+        
+        // Truncate raw response to fit the virtual qtype
+        $rawresponse->answer = $regs[2];
+                
+        $virtualqtype = $this->get_virtual_qtype();
+        $response = $virtualqtype->extract_response($rawresponse,
+                $this->create_virtual_nameprefix($nameprefix, $regs[1]));
+        $response[$nameprefix] = $regs[1];
+        return $response;
+    }
+    
+    function print_question_formulation_and_controls($question,
+            $quiz, $readonly, $answers, $correctanswers, $nameprefix) {
+        
+        // Replace wild-cards with dataset items
+        $datasetinput = $question->response[$nameprefix];
+        list($datasetnumber, $data) =
+                $this->parse_datasetinput($datasetinput);
+        foreach ($data as $name => $value) {
+            $question->questiontext = str_replace
+                    ('{'.$name.'}', $value, $question->questiontext);
+        }
+
+        // Print hidden field with dataset info
+        echo '<input type="hidden" name="' . $nameprefix
+                . '" value="' . $datasetinput . '"/>';
+
+        // Forward to the virtual qtype
+        unset($question->response[$nameprefix]);
+        $virtualqtype = $this->get_virtual_qtype();
+        $virtualqtype->print_question_formulation_and_controls(
+                $question, $quiz, $readonly, $answers, $correctanswers,
+                $this->create_virtual_nameprefix($nameprefix, $datasetinput));
+    }
+
+    function parse_datasetinput($datasetinput) {
+    /// Returns an array consisting of three pieces of information
+    /// In [0] there is the dataset number
+    /// In [1] there is an array where the data items are mapped by name
+    
+    /// The dataset related part of the response key
+    /// follows right after the response key and end with :
+    /// In order to avoid any conflict that can occur whenever anyone
+    /// wish to use : in the data, the data dependent part
+    /// has been converted to base64 in two steps
+
+        $rawdata = split('[:;]', $datasetinput);
+        $rawlength = count($rawdata);
+        $i = 0;
+        $data = array();
+        while (++$i < $rawlength) {
+            $data[base64_decode($rawdata[$i])] =
+                    base64_decode($rawdata[++$i]);
+        }
+        return array($rawdata[0], $data);
+    }
+}
+//// END OF CLASS ////
+
+function quiz_qtype_dataset_pick_new($category, $maxnumber) {
+//// Function used by ->create_response
+//// It takes care of picking a new datasetnumber for
+//// the user in the specified question category
+    
+    // We need to know whether the attempt builds on the last or
+    // not. It can not be determined by the function args to
+    // ->create_response.
+    // Instead of adding that argument to all implementations of
+    // create_response we try to reach $quiz globally. That
+    // should work because this function is only used by
+    // attempt.php when starting an attempt.
+    global $quiz;  //// PATTERN VIOLATION ////
+    if ($quiz->attemptonlast) {
+
+        // Dataset numbers for attemptonlast quizes are stored
+        // in quiz_attemptonlast_datasets
+        global $USER;
+        if (!($attemptonlastdataset = get_record(
+                'quiz_attemptonlast_datasets',
+                'userid', $USER->id, 'category', $category))
+                or $attemptonlastdataset->datasetnumber > $maxnumber) {
+
+            // No suitable $attemptonlastdataset
+
+            if ($attemptonlastdataset) {
+                // Remove the unsuitable:
+                delete_records('quiz_attemptonlast_datasets',
+                               'id', $attemptonlastdataset->id);
+                unset($attemptonlastdataset->id);
+                unset($attemptonlastdataset->datasetnumber);
+                
+            } else {
+                $attemptonlastdataset->userid = $USER->id;
+                $attemptonlastdataset->category = $category;
+            }
+
+            // Create without setting datasetnumber
+            // so that this user gets its id
+            $attemptonlastdataset->id = insert_record(
+                    'quiz_attemptonlast_datasets', $attemptonlastdataset);
+            
+            // Pick the datasetnumber in a thread safe way
+            // so that this can be done without having
+            // concurrent users get the same datasetnumber!
+            // The chosen pattern relies on
+            // synchronization for autoincrement on the id
+            // when the previous insert_record statement
+            // was executed.
+            if (!($latestdatasetpick = get_record_select(
+                    'quiz_attemptonlast_datasets',
+                    "category = $category AND datasetnumber > 0
+                    AND id < $attemptonlastdataset->id",
+                    ' max(id) id '))) {
+                // Smells like the current user is first:
+                $latestdatasetpick->id = 0;
+            }
+            $latestattemptonlasts = get_records_select(
+                    'quiz_attemptonlast_datasets',
+                    "$latestdatasetpick->id <= id
+                    AND id < $attemptonlastdataset->id
+                    AND category = $category");
+            $attemptonlastdataset->datasetnumber =
+                    (count($latestattemptonlasts)
+                    + (isset($latestattemptonlasts[$latestdatasetpick->id])
+                        ? $latestattemptonlasts[$latestdatasetpick->id]
+                                ->datasetnumber - 1
+                        : 0))
+                    % $maxnumber + 1;
+            if (!update_record('quiz_attemptonlast_datasets',
+                               $attemptonlastdataset)) {
+                notify("Error unable to save the picked datasetnumber in
+                        quiz_attemptonlast_datasets for user $USER-id");
+            }
+        }
+        return $attemptonlastdataset->datasetnumber;
+        
+    } else {
+        // When it is not an attemptonlast
+        // we pick the dataset number randomly
+        return  rand ( 1 , $maxnumber );
+    }
+}
+
+?>
diff --git a/mod/quiz/questiontypes/datasetdependent/datasetitems.php b/mod/quiz/questiontypes/datasetdependent/datasetitems.php
new file mode 100644 (file)
index 0000000..8c8b05c
--- /dev/null
@@ -0,0 +1,271 @@
+<?PHP // $Id$
+
+// Allows a teacher to create, edit and delete categories
+
+/// Print headings
+
+    $strdatasetnumber = get_string("datasetnumber", "quiz");
+    $strnumberinfo = get_string("categoryinfo", "quiz");
+    $strquestions = get_string("questions", "quiz");
+    $strpublish = get_string("publish", "quiz");
+    $strdelete = get_string("remove", "quiz");
+    $straction = get_string("action");
+    $stradd = get_string("add");
+    $strcancel = get_string("cancel");
+    $strsavechanges = get_string("savechanges");
+    $strbacktoquiz = get_string("backtoquiz", "quiz");
+
+    $streditingquiz = get_string("editingquiz", "quiz");
+    $streditdatasets = get_string("editdatasets", "quiz");
+    $strreuseifpossible = get_string('reuseifpossible', 'quiz');
+    $strforceregeneration = get_string('forceregeneration', 'quiz');
+
+// Get datasetdefinitions:
+    $datasetdefs = get_records_sql(
+                "SELECT a.* FROM {$CFG->prefix}quiz_dataset_definitions a,
+                                 {$CFG->prefix}quiz_question_datasets b
+                  WHERE a.id = b.datasetdefinition
+                    AND b.question = $question->id");
+    if (empty($datasetdefs)) {
+        redirect('edit.php');
+    }
+    foreach($datasetdefs as $datasetdef) {
+        if (!isset($maxnumber) || $datasetdef->itemcount < $maxnumber) {
+            $maxnumber = $datasetdef->itemcount;
+        }        
+    }
+
+/// Print heading
+
+    echo "<P ALIGN=CENTER><FONT SIZE=3>";
+    echo $streditdatasets;
+    helpbutton("categories", $streditdatasets, "quiz");
+    echo "</FONT></P>";
+
+/// If data submitted, then process and store.
+    if ($form = data_submitted()) {
+        if (isset($form->addbutton) && $form->addbutton &&
+                $maxnumber + 1 == $form->numbertoadd) { // This twisted condition should effectively stop resubmits caused by reloads
+            $addeditem->number = $form->numbertoadd;
+            foreach ($form->definition as $key => $itemdef) {
+                $addeditem->definition = $itemdef;
+                $addeditem->value = $form->value[$key];
+                if ($form->itemid[$key]) {
+                    // Reuse an previously used record
+                    $addeditem->id = $form->itemid[$key];
+                    if (!update_record('quiz_dataset_items', $addeditem)) {
+                        error("Error: Unable to update dataset item");
+                    }
+                } else {
+                    unset($addeditem->id);
+                    if (!insert_record('quiz_dataset_items', $addeditem)) {
+                        error("Error: Unable to insert dataset item");
+                    }
+                }
+                if ($datasetdefs[$itemdef]->itemcount <= $maxnumber) {
+                    $datasetdefs[$itemdef]->itemcount = $maxnumber+1;
+                    if (!update_record('quiz_dataset_definitions',
+                                       $datasetdefs[$itemdef])) {
+                         error("Error: Unable to update itemcount");
+                    }
+                }
+            }
+            // else Success:
+            $maxnumber = $addeditem->number;
+
+        } else if (isset($form->deletebutton) && $form->deletebutton
+                   and $maxnumber == $form->numbertodelete)
+        {
+            // Simply decrease itemcount where == $maxnumber
+            foreach ($datasetdefs as $datasetdef) {
+                if ($datasetdef->itemcount == $maxnumber) {
+                    $datasetdef->itemcount--;
+                    if (!update_record('quiz_dataset_definitions',
+                                       $datasetdef)) {
+                         error("Error: Unable to update itemcount");
+                    }
+                }
+            }
+            --$maxnumber;
+        }
+
+        // Handle generator options...
+        $olddatasetdefs = $datasetdefs;
+        $datasetdefs = $qtypeobj->update_dataset_options($olddatasetdefs, $form);
+        foreach ($datasetdefs as $key => $newdef) {
+            if ($newdef->options != $olddatasetdefs[$key]->options) {
+                // Save the new value for options
+                update_record('quiz_dataset_definitions', $newdef);
+            }
+        }
+    }
+
+    make_upload_directory("$course->id");  // Just in case
+    $grosscoursefiles = get_directory_list("$CFG->dataroot/$course->id",
+                                       "$CFG->moddata");
+
+// Have $coursefiles indexed by file paths:
+    $coursefiles = array();
+    foreach ($grosscoursefiles as $coursefile) {
+        $coursefiles[$coursefile] = $coursefile;
+    }
+
+
+// Get question header if any
+    $strquestionheader = $qtypeobj->comment_header($question);
+
+// Get the data set definition and items:
+    foreach ($datasetdefs as $key => $datasetdef) {
+        $datasetdefs[$key]->items = get_records_sql( // Use number as key!!
+                    " SELECT number, definition, id, value
+                      FROM {$CFG->prefix}quiz_dataset_items
+                      WHERE definition = $datasetdef->id ");
+    }
+
+    $table->data = array();
+    for ($number = $maxnumber ; $number > 0  ; --$number) {
+        $columns = array();
+        if ($maxnumber == $number) {
+            $columns[] =
+                    "<INPUT TYPE=\"hidden\" name=\"numbertodelete\" value=\"$number\"/>
+                     <INPUT TYPE=\"submit\" name=\"deletebutton\" value=\"$strdelete\"/>";
+        } else {
+            $columns[] = '';
+        }
+        $columns[] = $number;
+        foreach ($datasetdefs as $datasetdef) {
+            $columns[] =
+                    '<INPUT TYPE="hidden" name="itemid[]" value="'. $datasetdef->items[$number]->id .'"/>'
+                    . "<INPUT TYPE=\"hidden\" name=\"number[]\" value=\"$number\"/>
+                    <INPUT TYPE=\"hidden\" name=\"definition[]\" value=\"$datasetdef->id\"/>"
+                    . // Set $data:
+                    ($data[$datasetdef->name] = $datasetdef->items[$number]->value) ;
+
+        }
+        if ($strquestionheader) {
+            $columns[] = $qtypeobj->comment_on_datasetitems($question, $data, $number);
+        }
+        $table->data[] = $columns;
+    }
+
+    $table->head = array($straction, $strdatasetnumber);
+    $table->align = array("CENTER", "CENTER");
+    $addtable->head = $table->head;
+    if ($qtypeobj->supports_dataset_item_generation()) {
+        if (isset($form->forceregeneration) && $form->forceregeneration) {
+            $force = ' checked="checked" ';
+            $reuse = '';
+        } else {
+            $force = '';
+            $reuse = ' checked="checked" ';
+        }
+        $forceregeneration = '<br/><INPUT type="radio" name="forceregeneration" '
+                . $reuse . ' value="0"/>' . $strreuseifpossible
+                . '<br/><INPUT type="radio" name="forceregeneration" value="1" '
+                . $force . ' />' . $strforceregeneration;
+    } else {
+        $forceregeneration = '';
+    }
+    $addline = array('<INPUT TYPE="hidden" name="numbertoadd" value="'
+            . ($maxnumber+1)
+            . "\"/><INPUT TYPE=\"submit\" name=\"addbutton\" value=\"$stradd\"/>"
+            . $forceregeneration
+            , $maxnumber+1);
+    foreach ($datasetdefs as $datasetdef) {
+        if ($datasetdef->name) {
+            $table->head[] = $datasetdef->name;
+            $addtable->head[] = $datasetdef->name
+                    . ($qtypeobj->supports_dataset_item_generation()
+                    ?  '<br/>' . $qtypeobj->custom_generator_tools($datasetdef)
+                    : '');
+            $table->align[] = "CENTER";
+
+            // THE if-statement IS FOR BUT ONE THING
+            // - to determine an item value for the input field
+            // - this is tried in a number of different way...
+            if (isset($form->regenerateddefid) && $form->regenerateddefid) {
+                // Regeneration clicked...
+                if ($form->regenerateddefid == $datasetdef->id) {
+                    //...for this item...
+                    $itemvalue = $qtypeobj
+                            ->generate_dataset_item($datasetdef->options);
+                } else {
+                    // ...but not for this, keep unchanged!
+                    foreach ($form->definition as $key => $itemdef) {
+                        if ($datasetdef->id == $itemdef) {
+                            $itemvalue = $form->value[$key];
+                            break;
+                        }
+                    }
+                }
+            } else if (isset($form->forceregeneration)
+                    && $form->forceregeneration) {
+                // Can only mean a an "Add operation with forced regeneration:
+                $itemvalue = $qtypeobj->generate_dataset_item($datasetdef->options);
+
+            } else if (isset($datasetdef->items[$maxnumber + 1])) {
+                // Looks like we do have an old value to use here:
+                $itemvalue = $datasetdef->items[$maxnumber + 1]->value;
+
+            } else {
+                // We're getting getting desperate -
+                // is there any chance to determine a value somehow
+                // Let's just try anything now...
+
+                $qtypeobj->supports_dataset_item_generation() and '' !== (
+                    // Generation could work if the options are alright:
+                    $itemvalue = $qtypeobj->generate_dataset_item($datasetdef->options))
+
+                or ereg('(.*)'.($maxnumber).'(.*)',
+                        $datasetdef->items[$maxnumber]->value, $valueregs)
+                    // Looks like this trivial generator does it:
+                and $itemvalue = $valueregs[1].($maxnumber+1).$valueregs[2]
+                
+                or // Let's just pick the dataset number, better than nothing:
+                    $itemvalue = $maxnumber + 1;
+            }
+
+            $addline[] = 
+                    '<INPUT TYPE="hidden" name="itemid[]" value="'. $datasetdef->items[$maxnumber + 1]->id .'"/>'
+                    . "<INPUT TYPE=\"hidden\" name=\"definition[]\" value=\"$datasetdef->id\"/>"
+                    . ( 2 != $datasetdef->type
+                      ? '<INPUT TYPE="text" size="20" name="value[]" value="'
+                            . $itemvalue
+                            . '"/>'
+                      : choose_from_menu($coursefiles, 'value[]',
+                            $itemvalue,
+                            '', '', '', true));
+            $data[$datasetdef->name] = $itemvalue;
+        }
+    }
+    if ($strquestionheader) {
+        $table->head[] = $strquestionheader;
+        $addtable->head[] = $strquestionheader;
+        $table->align[] = "CENTER";
+        $addline[] = $qtypeobj->comment_on_datasetitems($question, $data, $maxnumber + 1);
+    }
+
+// Print form for adding one more dataset
+    $addtable->align = $table->align;
+    $addtable->data = array($addline);
+    echo "<FORM NAME=\"addform\" METHOD=\"post\" ACTION=\"question.php\">
+            <INPUT TYPE=\"hidden\" NAME=\"regenerateddefid\" VALUE=\"0\"/>
+            <INPUT TYPE=\"hidden\" NAME=\"id\" VALUE=\"$question->id\"/>
+            <INPUT TYPE=\"hidden\" NAME=\"editdatasets\" VALUE=\"1\"/>";
+    print_table($addtable);
+    echo '</FORM>';
+    
+// Print form with current datasets
+    if ($table->data) {
+        echo "<FORM METHOD=\"post\" ACTION=\"question.php\">
+            <INPUT TYPE=\"hidden\" NAME=\"id\" VALUE=\"$question->id\"/>
+            <INPUT TYPE=\"hidden\" NAME=\"editdatasets\" VALUE=\"1\"/>";
+        print_table($table);
+        echo '</FORM>';
+    }
+
+    echo "<center><BR><BR><FORM METHOD=\"get\" ACTION=\"edit.php\"><INPUT TYPE=\"hidden\" NAME=\"question\" VALUE=\"$question->id\"/><INPUT TYPE=submit NAME=backtoquiz VALUE=\"$strbacktoquiz\"></FORM></center>\n";
+
+    print_footer();
+
+?>
\ No newline at end of file
diff --git a/mod/quiz/questiontypes/datasetdependent/questiondatasets.html b/mod/quiz/questiontypes/datasetdependent/questiondatasets.html
new file mode 100644 (file)
index 0000000..3493c6b
--- /dev/null
@@ -0,0 +1,58 @@
+<FORM name="theform" method="post" <?php echo $onsubmit ?> action="question.php">
+<CENTER>
+<INPUT type="hidden" name="nextwizardpage" value="calculated.html"/>
+<TABLE cellpadding=5>
+<?php if (!empty($datasets)) { ?>
+<TR valign="top">
+    <TD align="right"><B><?php  print_string("wildcard", "quiz") ?> </B></TD>
+    <TD align="right">{<?php foreach($datasets as $name => $tmp){break;} p($name) ?>}</TD>
+    <TD><B> - <?php  print_string("substitutedby", "quiz") ?></B></TD>
+    <TD align="left"><?php echo array_shift($datasets) ?></TD>
+</TR>
+<?php   foreach ($datasets as $name => $menu) { ?>    
+<TR valign="top">
+    <TD/>
+    <TD align="right">{<?php p($name) ?>}</TD>
+    <TD/>
+    <TD align="left"><?php echo $menu ?>
+</TR>
+<?php   }
+      } else { ?>
+<TR>
+<TD colspan="4"><?php print_string('nopossibledatasets', 'quiz') ?>
+<INPUT type="hidden" name="dataset[]" value="0"/>
+</TD>
+</TR>
+<?php } ?>
+<TR valign=top>
+    <TD/>
+    <TD/>
+    <TD/>
+    <TD>
+        <INPUT type="hidden" name="name"               value="<?php  p($question->name) ?>"/>
+        <INPUT type="hidden" name="questiontext"       value="<?php  p($question->questiontext) ?>"/>
+        <INPUT type="hidden" name="questiontextformat" value="<?php  p($question->questiontextformat) ?>"/>
+        <INPUT type="hidden" name="image"              value="<?php  p($question->image) ?>"/>
+        <?php foreach ($answers as $answer) { ?>
+        <INPUT type="hidden" name="answer[]"              value="<?php  p($answer->answer) ?>"/>
+        <INPUT type="hidden" name="fraction[]"            value="<?php  p($answer->fraction) ?>"/>
+        <INPUT type="hidden" name="feedback[]"            value="<?php  p($answer->feedback) ?>"/>
+        <INPUT type="hidden" name="tolerance[]"           value="<?php  p($answer->tolerance) ?>"/>
+        <INPUT type="hidden" name="tolerancetype[]"       value="<?php  p($answer->tolerancetype) ?>"/>
+        <INPUT type="hidden" name="correctanswerlength[]" value="<?php  p($answer->correctanswerlength) ?>"/>
+        <?php } ?>
+        <?php foreach ($units as $unit) { ?>
+        <INPUT type="hidden" name="multiplier[]"   value="<?php  p($unit->multiplier) ?>"/>
+        <INPUT type="hidden" name="unit[]"         value="<?php  p($unit->unit) ?>"/>
+        <?php } ?>
+    </TD>
+</TR>
+</TABLE>
+
+<INPUT type="hidden" name=id value="<?php  p($question->id) ?>">
+<INPUT type="hidden" name=qtype value="<?php  p($question->qtype) ?>">
+<INPUT type="hidden" name="category" value="<?php  p($question->category) ?>"/>
+<INPUT type="submit"      value="<?php  print_string("savechanges") ?>">
+</CENTER>
+</FORM>
+