From: kaipe Date: Fri, 30 Jul 2004 14:50:58 +0000 (+0000) Subject: New generic tools that allow the creation of dataset-dependent questions X-Git-Url: http://git.mjollnir.org/gw?a=commitdiff_plain;h=2443a13e754998cdf81d50930e7f4dc0acd3e50b;p=moodle.git New generic tools that allow the creation of dataset-dependent questions as well as the new question type Calculated that uses this generic support. --- diff --git a/mod/quiz/lib.php b/mod/quiz/lib.php index ac805f7291..d4aaf13fbc 100644 --- a/mod/quiz/lib.php +++ b/mod/quiz/lib.php @@ -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 index 0000000000..8b29125e6d --- /dev/null +++ b/mod/quiz/questiontypes/calculated/calculated.html @@ -0,0 +1,177 @@ +
action="question.php"> +
+ +");} ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

:

+ id, true, true, $question->category); ?> +

:

+ + +

:

+
+
+
+

+ +

+
+ "; + } + + print_textarea($usehtmleditor, 15, 60, 630, 300, "questiontext", $question->questiontext); + + if ($usehtmleditor) { /// Trying this out for a while + echo ''; + } else { + echo "
"; + 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 "
"; + } + ?> +

:

+ image", get_string("none"),"",""); + } + ?> +

:

+    + +

:

+ ± +

:

+ tolerance_types(), + 'tolerancetype[]', $answers[0]->tolerancetype, false); ?> +

:

+ '1', '2' => '2', '3' => '3', + '4' => '4', '5' => '5', '6' => '6', + '7' => '7', '8' => '8', '9' => '9', + '10' => '10'), + 'correctanswerlength[]', + $answers[0]->correctanswerlength, false); ?> +

:

+ +

:

+

+ + ()

+

:

+

: + " size="10" + align="RIGHT" name="multiplier[]" + value="multiplier) ?>"/> +    : + " + name="unit[]" + size="5" value="unit) ?>"/>

+
+ + + +"> +
+
+ + + diff --git a/mod/quiz/questiontypes/calculated/editquestion.php b/mod/quiz/questiontypes/calculated/editquestion.php new file mode 100644 index 0000000000..fcc12d0a9b --- /dev/null +++ b/mod/quiz/questiontypes/calculated/editquestion.php @@ -0,0 +1,198 @@ +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 index 0000000000..3f908c8716 --- /dev/null +++ b/mod/quiz/questiontypes/calculated/modifiednumericalqtype.php @@ -0,0 +1,38 @@ +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 index 0000000000..8daa17398e --- /dev/null +++ b/mod/quiz/questiontypes/calculated/questiontype.php @@ -0,0 +1,550 @@ +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 '
' + . ' & ' + . choose_from_menu($lengthoptions, 'calclength[]', + $regs[4], // Selected + '', '', '', true) . '
' + . 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
$strmin
$strmax
$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=| diff --git a/mod/quiz/questiontypes/datasetdependent/abstractqtype.php b/mod/quiz/questiontypes/datasetdependent/abstractqtype.php new file mode 100644 index 0000000000..b63b90ae82 --- /dev/null +++ b/mod/quiz/questiontypes/datasetdependent/abstractqtype.php @@ -0,0 +1,534 @@ +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 = '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 = 'name\">$icon$item->value"; + } + + $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 ''; + + // 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 index 0000000000..8c8b05c446 --- /dev/null +++ b/mod/quiz/questiontypes/datasetdependent/datasetitems.php @@ -0,0 +1,271 @@ +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 "

"; + echo $streditdatasets; + helpbutton("categories", $streditdatasets, "quiz"); + echo "

"; + +/// 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[] = + " + "; + } else { + $columns[] = ''; + } + $columns[] = $number; + foreach ($datasetdefs as $datasetdef) { + $columns[] = + '' + . " + 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 = '
' . $strreuseifpossible + . '
' . $strforceregeneration; + } else { + $forceregeneration = ''; + } + $addline = array('" + . $forceregeneration + , $maxnumber+1); + foreach ($datasetdefs as $datasetdef) { + if ($datasetdef->name) { + $table->head[] = $datasetdef->name; + $addtable->head[] = $datasetdef->name + . ($qtypeobj->supports_dataset_item_generation() + ? '
' . $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[] = + '' + . "id\"/>" + . ( 2 != $datasetdef->type + ? '' + : 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 "
+ + id\"/> + "; + print_table($addtable); + echo '
'; + +// Print form with current datasets + if ($table->data) { + echo "
+ id\"/> + "; + print_table($table); + echo '
'; + } + + echo "


id\"/>
\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 index 0000000000..3493c6b397 --- /dev/null +++ b/mod/quiz/questiontypes/datasetdependent/questiondatasets.html @@ -0,0 +1,58 @@ +
action="question.php"> +
+ + + + + + + + + + $menu) { ?> + + + + + + + + + + + +
{ $tmp){break;} p($name) ?>} -
+ {} + +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ + + + +"> +
+
+