3 ///////////////////////////////////////////////////////////////
4 /// ABSTRACT SUPERCLASS FOR QUSTION TYPES THAT USE DATASETS ///
5 ///////////////////////////////////////////////////////////////
7 * @package questionbank
8 * @subpackage questiontypes
11 require_once($CFG->libdir
.'/filelib.php');
13 define("LITERAL", "1");
17 class question_dataset_dependent_questiontype
extends default_questiontype
{
19 var $virtualqtype = false;
22 return 'datasetdependent';
25 function save_question_options($question) {
26 // Default does nothing...
30 function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
31 // Find out how many datasets are available
33 if(!$maxnumber = (int)get_field_sql(
34 "SELECT MIN(a.itemcount)
35 FROM {$CFG->prefix}question_dataset_definitions a,
36 {$CFG->prefix}question_datasets b
37 WHERE b.question = $question->id
38 AND a.id = b.datasetdefinition")) {
39 error("Couldn't get the specified dataset for a calculated " .
40 "question! (question: {$question->id}");
43 // Choose a random dataset
44 $state->options
->datasetitem
= rand(1, $maxnumber);
45 $state->options
->dataset
=
46 $this->pick_question_dataset($question,$state->options
->datasetitem
);
47 $state->responses
= array('' => '');
51 function restore_session_and_responses(&$question, &$state) {
52 if (!ereg('^dataset([0-9]+)[^-]*-(.*)$',
53 $state->responses
[''], $regs)) {
54 notify ("Wrongly formatted raw response answer " .
55 "{$state->responses['']}! Could not restore session for " .
56 " question #{$question->id}.");
57 $state->options
->datasetitem
= 1;
58 $state->options
->dataset
= array();
59 $state->responses
= array('' => '');
63 // Restore the chosen dataset
64 $state->options
->datasetitem
= $regs[1];
65 $state->options
->dataset
=
66 $this->pick_question_dataset($question,$state->options
->datasetitem
);
67 $state->responses
= array('' => $regs[2]);
71 function save_session_and_responses(&$question, &$state) {
72 $responses = 'dataset'.$state->options
->datasetitem
.'-'.
73 $state->responses
[''];
75 // Set the legacy answer field
76 if (!set_field('question_states', 'answer', $responses, 'id',
84 function substitute_variables($str, $dataset) {
85 foreach ($dataset as $name => $value) {
86 $str = str_replace('{'.$name.'}', $value, $str);
91 function finished_edit_wizard(&$form) {
92 return isset($form->backtoquiz
);
95 // This gets called by editquestion.php after the standard question is saved
96 function print_next_wizard_page(&$question, &$form, $course) {
97 global $CFG, $USER, $SESSION, $COURSE;
99 // Catch invalid navigation & reloads
100 if (empty($question->id
) && empty($SESSION->datasetdependent
)) {
101 redirect('edit.php?courseid='.$COURSE->id
, 'The page you are loading has expired.', 3);
104 // See where we're coming from
105 switch($form->wizardpage
) {
107 require("$CFG->dirroot/question/type/datasetdependent/datasetdefinitions.php");
109 case 'datasetdefinitions':
111 require("$CFG->dirroot/question/type/datasetdependent/datasetitems.php");
114 error('Incorrect or no wizard page specified!');
119 // This gets called by question2.php after the standard question is saved
120 function &next_wizard_form($submiturl, $question, $wizardnow){
121 global $CFG, $SESSION, $COURSE;
123 // Catch invalid navigation & reloads
124 if (empty($question->id
) && empty($SESSION->datasetdependent
)) {
125 redirect('edit.php?courseid='.$COURSE->id
, 'The page you are loading has expired. Cannot get next wizard form.', 3);
127 if (empty($question->id
)){
128 $question =& $SESSION->datasetdependent
->questionform
;
131 // See where we're coming from
133 case 'datasetdefinitions':
134 require("$CFG->dirroot/question/type/datasetdependent/datasetdefinitions_form.php");
135 $mform =& new question_dataset_dependent_definitions_form("$submiturl?wizardnow=datasetdefinitions", $question);
138 require("$CFG->dirroot/question/type/datasetdependent/datasetitems_form.php");
139 $regenerate = optional_param('forceregeneration', 0, PARAM_BOOL
);
140 $mform =& new question_dataset_dependent_items_form("$submiturl?wizardnow=datasetitems", $question, $regenerate);
143 error('Incorrect or no wizard page specified!');
151 * This method should be overriden if you want to include a special heading or some other
152 * html on a question editing page besides the question editing form.
154 * @param question_edit_form $mform a child of question_edit_form
155 * @param object $question
156 * @param string $wizardnow is '' for first page.
158 function display_question_editing_page(&$mform, $question, $wizardnow){
161 //on first page default display is fine
162 parent
::display_question_editing_page($mform, $question, $wizardnow);
165 case 'datasetdefinitions':
166 print_heading_with_help(get_string("choosedatasetproperties", "quiz"), "questiondatasets", "quiz");
169 print_heading_with_help(get_string("editdatasets", "quiz"), 'questiondatasets', "quiz");
179 * This method prepare the $datasets in a format similar to dadatesetdefinitions_form.php
180 * so that they can be saved
181 * using the function save_dataset_definitions($form)
182 * when creating a new calculated question or
183 * whenediting an already existing calculated question
184 * or by function save_as_new_dataset_definitions($form, $initialid)
185 * when saving as new an already existing calculated question
187 * @param object $form
188 * @param int $questionfromid default = '0'
190 function preparedatasets(&$form , $questionfromid='0'){
191 // the dataset names present in the edit_question_form and edit_calculated_form are retrieved
192 $possibledatasets = $this->find_dataset_names($form->questiontext
);
193 $mandatorydatasets = array();
194 foreach ($form->answers
as $answer) {
195 $mandatorydatasets +
= $this->find_dataset_names($answer);
197 // if there are identical datasetdefs already saved in the original question.
198 // either when editing a question or saving as new
199 // they are retrieved using $questionfromid
200 if ($questionfromid!='0'){
201 $form->id
= $questionfromid ;
205 // always prepare the mandatorydatasets present in the answers
206 // the $options are not used here
207 foreach ($mandatorydatasets as $datasetname) {
208 if (!isset($datasets[$datasetname])) {
209 list($options, $selected) =
210 $this->dataset_options($form, $datasetname);
211 $datasets[$datasetname]='';
212 $form->dataset
[$key]=$selected ;
216 // do not prepare possibledatasets when creating a question
217 // they will defined and stored with datasetdefinitions_form.php
218 // the $options are not used here
219 if ($questionfromid!='0'){
221 foreach ($possibledatasets as $datasetname) {
222 if (!isset($datasets[$datasetname])) {
223 list($options, $selected) =
224 $this->dataset_options($form, $datasetname,false);
225 $datasets[$datasetname]='';
226 $form->dataset
[$key]=$selected ;
234 * this version save the available data at the different steps of the question editing process
235 * without using global $SESSION as storage between steps
236 * at the first step $wizardnow = 'question'
237 * when creating a new question
238 * when modifying a question
239 * when copying as a new question
240 * the general parameters and answers are saved using parent::save_question
241 * then the datasets are prepared and saved
242 * at the second step $wizardnow = 'datasetdefinitions'
243 * the datadefs final type are defined as private, category or not a datadef
244 * at the third step $wizardnow = 'datasetitems'
245 * the datadefs parameters and the data items are created or defined
247 * @param object question
248 * @param object $form
250 * @param PARAM_ALPHA $wizardnow should be added as we are coming from question2.php
252 function save_question($question, $form, $course) {
253 $wizardnow = optional_param('wizardnow', '', PARAM_ALPHA
);
254 $id = optional_param('id', 0, PARAM_INT
); // question id
255 // in case 'question'
256 // for a new question $form->id is empty
257 // when saving as new question
258 // $question->id = 0, $form is $data from question2.php
259 // and $data->makecopy is defined as $data->id is the initial question id
260 // edit case. If it is a new question we don't necessarily need to
261 // return a valid question object
263 // See where we're coming from
266 case 'question': // coming from the first page, creating the second
267 if (empty($form->id
)) { // for a new question $form->id is empty
268 $question = parent
::save_question($question, $form, $course);
269 //prepare the datasets using default $questionfromid
270 $this->preparedatasets($form);
271 $form->id
= $question->id
;
272 $this->save_dataset_definitions($form);
273 } else if (!empty($form->makecopy
)){
274 $questionfromid = $form->id
;
275 $question = parent
::save_question($question, $form, $course);
276 //prepare the datasets
277 $this->preparedatasets($form,$questionfromid);
278 $form->id
= $question->id
;
279 $this->save_as_new_dataset_definitions($form,$questionfromid );
280 } else {// editing a question
281 $question = parent
::save_question($question, $form, $course);
282 //prepare the datasets
283 $this->preparedatasets($form,$question->id
);
284 $form->id
= $question->id
;
285 $this->save_dataset_definitions($form);
288 case 'datasetdefinitions':
290 $this->save_dataset_definitions($form);
293 $this->save_dataset_items($question, $form);
296 error('Incorrect or no wizard page specified!');
302 function save_dataset_items($question, $fromform){
303 //overridden in child classes
306 function get_dataset_definitions($questionid, $newdatasets) {
307 //get the existing datasets for this question
308 $datasetdefs = array();
309 if (!empty($questionid)) {
312 FROM {$CFG->prefix}question_datasets d,
313 {$CFG->prefix}question_dataset_definitions i
314 WHERE d.question = '$questionid'
315 AND d.datasetdefinition = i.id
317 if ($records = get_records_sql($sql)) {
318 foreach ($records as $r) {
319 $datasetdefs["$r->type-$r->category-$r->name"] = $r;
324 foreach ($newdatasets as $dataset) {
326 continue; // The no dataset case...
329 if (!isset($datasetdefs[$dataset])) {
330 //make new datasetdef
331 list($type, $category, $name) = explode('-', $dataset, 3);
332 $datasetdef = new stdClass
;
333 $datasetdef->type
= $type;
334 $datasetdef->name
= $name;
335 $datasetdef->category
= $category;
336 $datasetdef->itemcount
= 0;
337 $datasetdef->options
= 'uniform:1.0:10.0:1';
338 $datasetdefs[$dataset] = clone($datasetdef);
344 function save_dataset_definitions($form) {
346 $datasetdefinitions = $this->get_dataset_definitions($form->id
, $form->dataset
);
347 $tmpdatasets = array_flip($form->dataset
);
348 $defids = array_keys($datasetdefinitions);
349 foreach ($defids as $defid) {
350 $datasetdef = &$datasetdefinitions[$defid];
351 if (isset($datasetdef->id
)) {
352 if (!isset($tmpdatasets[$defid])) {
353 // This dataset is not used any more, delete it
354 delete_records('question_datasets',
355 'question', $form->id
,
356 'datasetdefinition', $datasetdef->id
);
357 if ($datasetdef->category
== 0) { // Question local dataset
358 delete_records('question_dataset_definitions',
359 'id', $datasetdef->id
);
360 delete_records('question_dataset_items',
361 'definition', $datasetdef->id
);
364 // This has already been saved or just got deleted
365 unset($datasetdefinitions[$defid]);
369 if (!$datasetdef->id
= insert_record(
370 'question_dataset_definitions', $datasetdef)) {
371 error("Unable to create dataset $defid");
374 if (0 != $datasetdef->category
) {
375 // We need to look for already existing
376 // datasets in the category.
377 // By first creating the datasetdefinition above we
378 // can manage to automatically take care of
379 // some possible realtime concurrence
380 if ($olderdatasetdefs = get_records_select(
381 'question_dataset_definitions',
382 "type = '$datasetdef->type'
383 AND name = '$datasetdef->name'
384 AND category = '$datasetdef->category'
385 AND id < $datasetdef->id
386 ORDER BY id DESC")) {
388 while ($olderdatasetdef = array_shift($olderdatasetdefs)) {
389 delete_records('question_dataset_definitions',
390 'id', $datasetdef->id
);
391 $datasetdef = $olderdatasetdef;
396 // Create relation to this dataset:
397 $questiondataset = new stdClass
;
398 $questiondataset->question
= $form->id
;
399 $questiondataset->datasetdefinition
= $datasetdef->id
;
400 if (!insert_record('question_datasets',
402 error("Unable to create relation to dataset $name");
404 unset($datasetdefinitions[$defid]);
407 // Remove local obsolete datasets as well as relations
408 // to datasets in other categories:
409 if (!empty($datasetdefinitions)) {
410 foreach ($datasetdefinitions as $def) {
411 delete_records('question_datasets',
412 'question', $form->id
,
413 'datasetdefinition', $def->id
);
415 if ($def->category
== 0) { // Question local dataset
416 delete_records('question_dataset_definitions', 'id', $def->id
);
417 delete_records('question_dataset_items',
418 'definition', $def->id
);
423 /** This function create a copy of the datasets ( definition and dataitems)
424 * from the preceding question if they remain in the new question
425 * otherwise its create the datasets that have been added as in the
426 * save_dataset_definitions()
428 function save_as_new_dataset_definitions($form, $initialid) {
430 // Get the datasets from the intial question
431 $datasetdefinitions = $this->get_dataset_definitions($initialid, $form->dataset
);
432 // $tmpdatasets contains those of the new question
433 $tmpdatasets = array_flip($form->dataset
);
434 $defids = array_keys($datasetdefinitions);// new datasets
435 foreach ($defids as $defid) {
436 $datasetdef = &$datasetdefinitions[$defid];
437 if (isset($datasetdef->id
)) {
438 // This dataset exist in the initial question
439 if (!isset($tmpdatasets[$defid])) {
440 // do not exist in the new question so ignore
441 unset($datasetdefinitions[$defid]);
444 // create a copy but not for category one
445 if (0 == $datasetdef->category
) {
446 $olddatasetid = $datasetdef->id
;
447 $olditemcount = $datasetdef->itemcount
;
448 $datasetdef->itemcount
=0;
449 if (!$datasetdef->id
= insert_record(
450 'question_dataset_definitions', $datasetdef)) {
451 error("Unable to create dataset $defid");
454 $olditems = get_records_sql( // Use number as key!!
455 " SELECT itemnumber, value
456 FROM {$CFG->prefix}question_dataset_items
457 WHERE definition = $olddatasetid ");
458 if (count($olditems) > 0 ) {
460 foreach($olditems as $item ){
461 $item->definition
= $datasetdef->id
;
462 if (!insert_record('question_dataset_items', $item)) {
463 error("Unable to insert dataset item $item->itemnumber with $item->value for $datasetdef->name");
468 $datasetdef->itemcount
=$itemcount;
469 update_record('question_dataset_definitions', $datasetdef);
470 } // end of copy the dataitems
471 }// end of copy the datasetdef
472 // Create relation to the new question with this
473 // copy as new datasetdef from the initial question
474 $questiondataset = new stdClass
;
475 $questiondataset->question
= $form->id
;
476 $questiondataset->datasetdefinition
= $datasetdef->id
;
477 if (!insert_record('question_datasets',
479 error("Unable to create relation to dataset $name");
481 unset($datasetdefinitions[$defid]);
483 }// end of datasetdefs from the initial question
484 // really new one code similar to save_dataset_definitions()
485 if (!$datasetdef->id
= insert_record(
486 'question_dataset_definitions', $datasetdef)) {
487 error("Unable to create dataset $defid");
490 if (0 != $datasetdef->category
) {
491 // We need to look for already existing
492 // datasets in the category.
493 // By first creating the datasetdefinition above we
494 // can manage to automatically take care of
495 // some possible realtime concurrence
496 if ($olderdatasetdefs = get_records_select(
497 'question_dataset_definitions',
498 "type = '$datasetdef->type'
499 AND name = '$datasetdef->name'
500 AND category = '$datasetdef->category'
501 AND id < $datasetdef->id
502 ORDER BY id DESC")) {
504 while ($olderdatasetdef = array_shift($olderdatasetdefs)) {
505 delete_records('question_dataset_definitions',
506 'id', $datasetdef->id
);
507 $datasetdef = $olderdatasetdef;
512 // Create relation to this dataset:
513 $questiondataset = new stdClass
;
514 $questiondataset->question
= $form->id
;
515 $questiondataset->datasetdefinition
= $datasetdef->id
;
516 if (!insert_record('question_datasets',
518 error("Unable to create relation to dataset $name");
520 unset($datasetdefinitions[$defid]);
523 // Remove local obsolete datasets as well as relations
524 // to datasets in other categories:
525 if (!empty($datasetdefinitions)) {
526 foreach ($datasetdefinitions as $def) {
527 delete_records('question_datasets',
528 'question', $form->id
,
529 'datasetdefinition', $def->id
);
531 if ($def->category
== 0) { // Question local dataset
532 delete_records('question_dataset_definitions', 'id', $def->id
);
533 delete_records('question_dataset_items',
534 'definition', $def->id
);
543 function save_question($question, &$form, $course) {
544 // For dataset dependent questions a wizard is used for editing
545 // questions. Therefore saving the question is delayed until
546 // we're through with the whole wizard.
548 $this->validate_form($form);
550 // See where we're coming from
551 switch($form->wizardpage) {
553 unset($SESSION->datasetdependent); // delete any remaining data
554 // from previous wizards
555 if (empty($form->id)) {
556 $SESSION->datasetdependent->question = $form;
557 $question = $this->create_runtime_question($question, $form);
559 $question = parent::save_question($question, $form, $course);
562 case 'datasetdefinitions':
563 $SESSION->datasetdependent->datasetdefinitions = $form;
564 if (empty($form->id)) {
565 $question = $this->create_runtime_question($question, $SESSION->datasetdependent->question);
567 $this->save_dataset_definitions($form);
568 $this->get_question_options($question);
569 //unset($SESSION->datasetdependent->datasetdefinitions);
571 //$this->get_question_options($question);
574 if (!empty($form->addbutton) && isset($SESSION->datasetdependent->question)) {
576 $question = parent::save_question($question, $SESSION->datasetdependent->question, $course);
577 $SESSION->datasetdependent->datasetdefinitions->id = $question->id;
578 $this->save_dataset_definitions($SESSION->datasetdependent->datasetdefinitions);
579 //$this->get_dataset_definitions($question);
580 unset($SESSION->datasetdependent);
583 if (empty($question->id)) {
584 $question = $this->create_runtime_question($question, $SESSION->datasetdependent->question);
586 $this->get_question_options($question);
591 error('Incorrect or no wizard page specified!');
601 /// Dataset functionality
602 function pick_question_dataset($question, $datasetitem) {
603 // Select a dataset in the following format:
604 // An array indexed by the variable names (d.name) pointing to the value
607 if (!$dataset = get_records_sql(
608 "SELECT d.name, i.value
609 FROM {$CFG->prefix}question_dataset_definitions d,
610 {$CFG->prefix}question_dataset_items i,
611 {$CFG->prefix}question_datasets q
612 WHERE q.question = $question->id
613 AND q.datasetdefinition = d.id
614 AND d.id = i.definition
615 AND i.itemnumber = $datasetitem")) {
616 error("Couldn't get the specified dataset for a dataset dependent " .
617 "question! (question: {$question->id}, " .
618 "datasetitem: {$datasetitem})");
620 array_walk($dataset, create_function('&$val', '$val = $val->value;'));
624 function create_virtual_qtype() {
625 error("No virtual question type for question type ".$this->name());
628 function get_virtual_qtype() {
629 if (!$this->virtualqtype
) {
630 $this->virtualqtype
= $this->create_virtual_qtype();
632 return $this->virtualqtype
;
635 function comment_header($question) {
636 // Used by datasetitems.php
637 // Default returns nothing and thus takes away the column
641 function comment_on_datasetitems($question, $data, $number) {
642 // Used by datasetitems.php
643 // Default returns nothing
647 function supports_dataset_item_generation() {
648 // Used by datasetitems.php
649 // Default does not support any item generation
653 function custom_generator_tools($datasetdef) {
654 // Used by datasetitems.php
655 // If there is no generation support,
656 // there cannot possibly be any custom tools either
660 function generate_dataset_item($options) {
661 // Used by datasetitems.php
662 // By default nothing is generated
666 function update_dataset_options($datasetdefs, $form) {
667 // Used by datasetitems.php
668 // Returns the updated datasets
669 // By default the dataset options cannot be updated
673 function dataset_options($form, $name,$prefix='',$langfile='quiz') {
675 // First options - it is not a dataset...
676 $options['0'] = get_string($prefix.'nodataset', $langfile);
678 // Construct question local options
680 $currentdatasetdef = get_record_sql(
682 FROM {$CFG->prefix}question_dataset_definitions a,
683 {$CFG->prefix}question_datasets b
684 WHERE a.id = b.datasetdefinition
685 AND b.question = '$form->id'
686 AND a.name = '$name'")
687 or $currentdatasetdef->type
= '0';
688 foreach (array( LITERAL
, FILE
, LINK
) as $type) {
689 $key = "$type-0-$name";
690 if ($currentdatasetdef->type
== $type
691 and $currentdatasetdef->category
== 0) {
692 $options[$key] = get_string($prefix."keptlocal$type", $langfile);
694 $options[$key] = get_string($prefix."newlocal$type", $langfile);
698 // Construct question category options
699 $categorydatasetdefs = get_records_sql(
701 FROM {$CFG->prefix}question_dataset_definitions a,
702 {$CFG->prefix}question_datasets b
703 WHERE a.id = b.datasetdefinition
704 AND a.category = '$form->category'
705 AND a.name = '$name'");
706 foreach(array( LITERAL
, FILE
, LINK
) as $type) {
707 $key = "$type-$form->category-$name";
708 if (isset($categorydatasetdefs[$type])
709 and $categorydef = $categorydatasetdefs[$type]) {
710 if ($currentdatasetdef->type
== $type
711 and $currentdatasetdef->id
== $categorydef->id
) {
712 $options[$key] = get_string($prefix."keptcategory$type", $langfile);
714 $options[$key] = get_string($prefix."existingcategory$type", $langfile);
717 $options[$key] = get_string($prefix."newcategory$type", $langfile);
722 return array($options, $currentdatasetdef->type
723 ?
"$currentdatasetdef->type-$currentdatasetdef->category-$name"
727 function find_dataset_names($text) {
728 /// Returns the possible dataset names found in the text as an array
729 /// The array has the dataset name for both key and value
730 $datasetnames = array();
731 while (ereg('\\{([[:alpha:]][^>} <{"\']*)\\}', $text, $regs)) {
732 $datasetnames[$regs[1]] = $regs[1];
733 $text = str_replace($regs[0], '', $text);
735 return $datasetnames;
738 function create_virtual_nameprefix($nameprefix, $datasetinput) {
739 if (!ereg('([0-9]+)' . $this->name() . '$', $nameprefix, $regs)) {
740 error("Wrongly formatted nameprefix $nameprefix");
742 $virtualqtype = $this->get_virtual_qtype();
743 return $nameprefix . $regs[1] . $virtualqtype->name();
747 //// END OF CLASS ////