as well as the new question type Calculated that uses this generic support.
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"),
}
}
-
/// FUNCTIONS ///////////////////////////////////////////////////////////////////
function quiz_add_instance($quiz) {
--- /dev/null
+<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 ": ";
+ 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) ?>"/>
+ <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) ?>" />±
+ </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> <?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");
+ }
+?>
+
--- /dev/null
+<?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");
+
+?>
--- /dev/null
+<?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 ////
+
+?>
--- /dev/null
+<?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]\"/> & <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]);
+ }
+}
+
+?>
--- /dev/null
+<?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 );
+ }
+}
+
+?>
--- /dev/null
+<?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
--- /dev/null
+<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>
+