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 uses_quizfile($question, $relativefilepath) {
92 // Check whether the specified file is available by any
93 // dataset item on this question...
95 if (get_record_sql(" SELECT *
96 FROM {$CFG->prefix}question_dataset_items i,
97 {$CFG->prefix}question_dataset_definitions d,
98 {$CFG->prefix}question_datasets q
99 WHERE i.value = '$relativefilepath'
100 AND d.id = i.definition AND d.type = 2
101 AND d.id = q.datasetdefinition
102 AND q.question = $question->id ")) {
106 // Make the check of the parent:
107 return parent
::uses_quizfile($question, $relativefilepath);
111 function finished_edit_wizard(&$form) {
112 return isset($form->backtoquiz
);
115 // This gets called by editquestion.php after the standard question is saved
116 function print_next_wizard_page(&$question, &$form, $course) {
117 global $CFG, $USER, $SESSION, $COURSE;
119 // Catch invalid navigation & reloads
120 if (empty($question->id
) && empty($SESSION->datasetdependent
)) {
121 redirect('edit.php?courseid='.$COURSE->id
, 'The page you are loading has expired.', 3);
124 // See where we're coming from
125 switch($form->wizardpage
) {
127 require("$CFG->dirroot/question/type/datasetdependent/datasetdefinitions.php");
129 case 'datasetdefinitions':
131 require("$CFG->dirroot/question/type/datasetdependent/datasetitems.php");
134 error('Incorrect or no wizard page specified!');
139 // This gets called by question2.php after the standard question is saved
140 function &next_wizard_form($submiturl, $question, $wizardnow){
141 global $CFG, $SESSION, $COURSE;
143 // Catch invalid navigation & reloads
144 if (empty($question->id
) && empty($SESSION->datasetdependent
)) {
145 redirect('edit.php?courseid='.$COURSE->id
, 'The page you are loading has expired. Cannot get next wizard form.', 3);
147 if (empty($question->id
)){
148 $question =& $SESSION->datasetdependent
->questionform
;
151 // See where we're coming from
153 case 'datasetdefinitions':
154 require("$CFG->dirroot/question/type/datasetdependent/datasetdefinitions_form.php");
155 $mform =& new question_dataset_dependent_definitions_form("$submiturl?wizardnow=datasetdefinitions", $question);
158 require("$CFG->dirroot/question/type/datasetdependent/datasetitems_form.php");
159 $regenerate = optional_param('forceregeneration', 0, PARAM_BOOL
);
160 $mform =& new question_dataset_dependent_items_form("$submiturl?wizardnow=datasetitems", $question, $regenerate);
163 error('Incorrect or no wizard page specified!');
171 * This method should be overriden if you want to include a special heading or some other
172 * html on a question editing page besides the question editing form.
174 * @param question_edit_form $mform a child of question_edit_form
175 * @param object $question
176 * @param string $wizardnow is '' for first page.
178 function display_question_editing_page(&$mform, $question, $wizardnow){
181 //on first page default display is fine
182 parent
::display_question_editing_page($mform, $question, $wizardnow);
185 case 'datasetdefinitions':
186 print_heading_with_help(get_string("choosedatasetproperties", "quiz"), "questiondatasets", "quiz");
189 print_heading_with_help(get_string("editdatasets", "quiz"), 'questiondatasets', "quiz");
199 * This method prepare the $datasets in a format similar to dadatesetdefinitions_form.php
200 * so that they can be saved
201 * using the function save_dataset_definitions($form)
202 * when creating a new calculated question or
203 * whenediting an already existing calculated question
204 * or by function save_as_new_dataset_definitions($form, $initialid)
205 * when saving as new an already existing calculated question
207 * @param object $form
208 * @param int $questionfromid default = '0'
210 function preparedatasets(&$form , $questionfromid='0'){
211 // the dataset names present in the edit_question_form and edit_calculated_form are retrieved
212 $possibledatasets = $this->find_dataset_names($form->questiontext
);
213 $mandatorydatasets = array();
214 foreach ($form->answers
as $answer) {
215 $mandatorydatasets +
= $this->find_dataset_names($answer);
217 // if there are identical datasetdefs already saved in the original question.
218 // either when editing a question or saving as new
219 // they are retrieved using $questionfromid
220 if ($questionfromid!='0'){
221 $form->id
= $questionfromid ;
225 // always prepare the mandatorydatasets present in the answers
226 // the $options are not used here
227 foreach ($mandatorydatasets as $datasetname) {
228 if (!isset($datasets[$datasetname])) {
229 list($options, $selected) =
230 $this->dataset_options($form, $datasetname);
231 $datasets[$datasetname]='';
232 $form->dataset
[$key]=$selected ;
236 // do not prepare possibledatasets when creating a question
237 // they will defined and stored with datasetdefinitions_form.php
238 // the $options are not used here
239 if ($questionfromid!='0'){
241 foreach ($possibledatasets as $datasetname) {
242 if (!isset($datasets[$datasetname])) {
243 list($options, $selected) =
244 $this->dataset_options($form, $datasetname,false);
245 $datasets[$datasetname]='';
246 $form->dataset
[$key]=$selected ;
254 * this version save the available data at the different steps of the question editing process
255 * without using global $SESSION as storage between steps
256 * at the first step $wizardnow = 'question'
257 * when creating a new question
258 * when modifying a question
259 * when copying as a new question
260 * the general parameters and answers are saved using parent::save_question
261 * then the datasets are prepared and saved
262 * at the second step $wizardnow = 'datasetdefinitions'
263 * the datadefs final type are defined as private, category or not a datadef
264 * at the third step $wizardnow = 'datasetitems'
265 * the datadefs parameters and the data items are created or defined
267 * @param object question
268 * @param object $form
270 * @param PARAM_ALPHA $wizardnow should be added as we are coming from question2.php
272 function save_question($question, $form, $course) {
273 $wizardnow = optional_param('wizardnow', '', PARAM_ALPHA
);
274 $id = optional_param('id', 0, PARAM_INT
); // question id
275 // in case 'question'
276 // for a new question $form->id is empty
277 // when saving as new question
278 // $question->id = 0, $form is $data from question2.php
279 // and $data->makecopy is defined as $data->id is the initial question id
280 // edit case. If it is a new question we don't necessarily need to
281 // return a valid question object
283 // See where we're coming from
286 case 'question': // coming from the first page, creating the second
287 if (empty($form->id
)) { // for a new question $form->id is empty
288 $question = parent
::save_question($question, $form, $course);
289 //prepare the datasets using default $questionfromid
290 $this->preparedatasets($form);
291 $form->id
= $question->id
;
292 $this->save_dataset_definitions($form);
293 } else if (!empty($form->makecopy
)){
294 $questionfromid = $form->id
;
295 $question = parent
::save_question($question, $form, $course);
296 //prepare the datasets
297 $this->preparedatasets($form,$questionfromid);
298 $form->id
= $question->id
;
299 $this->save_as_new_dataset_definitions($form,$questionfromid );
300 } else {// editing a question
301 $question = parent
::save_question($question, $form, $course);
302 //prepare the datasets
303 $this->preparedatasets($form,$question->id
);
304 $form->id
= $question->id
;
305 $this->save_dataset_definitions($form);
308 case 'datasetdefinitions':
309 $this->save_dataset_definitions($form);
312 $this->save_dataset_items($question, $form);
315 error('Incorrect or no wizard page specified!');
321 function save_dataset_items($question, $fromform){
322 //overridden in child classes
325 function get_dataset_definitions($questionid, $newdatasets) {
326 //get the existing datasets for this question
327 $datasetdefs = array();
328 if (!empty($questionid)) {
331 FROM {$CFG->prefix}question_datasets d,
332 {$CFG->prefix}question_dataset_definitions i
333 WHERE d.question = '$questionid'
334 AND d.datasetdefinition = i.id
336 if ($records = get_records_sql($sql)) {
337 foreach ($records as $r) {
338 $datasetdefs["$r->type-$r->category-$r->name"] = $r;
343 foreach ($newdatasets as $dataset) {
345 continue; // The no dataset case...
348 if (!isset($datasetdefs[$dataset])) {
349 //make new datasetdef
350 list($type, $category, $name) = explode('-', $dataset, 3);
351 $datasetdef = new stdClass
;
352 $datasetdef->type
= $type;
353 $datasetdef->name
= $name;
354 $datasetdef->category
= $category;
355 $datasetdef->itemcount
= 0;
356 $datasetdef->options
= 'uniform:1.0:10.0:1';
357 $datasetdefs[$dataset] = clone($datasetdef);
363 function save_dataset_definitions($form) {
365 $datasetdefinitions = $this->get_dataset_definitions($form->id
, $form->dataset
);
366 $tmpdatasets = array_flip($form->dataset
);
367 $defids = array_keys($datasetdefinitions);
368 foreach ($defids as $defid) {
369 $datasetdef = &$datasetdefinitions[$defid];
370 if (isset($datasetdef->id
)) {
371 if (!isset($tmpdatasets[$defid])) {
372 // This dataset is not used any more, delete it
373 delete_records('question_datasets',
374 'question', $form->id
,
375 'datasetdefinition', $datasetdef->id
);
376 if ($datasetdef->category
== 0) { // Question local dataset
377 delete_records('question_dataset_definitions',
378 'id', $datasetdef->id
);
379 delete_records('question_dataset_items',
380 'definition', $datasetdef->id
);
383 // This has already been saved or just got deleted
384 unset($datasetdefinitions[$defid]);
388 if (!$datasetdef->id
= insert_record(
389 'question_dataset_definitions', $datasetdef)) {
390 error("Unable to create dataset $defid");
393 if (0 != $datasetdef->category
) {
394 // We need to look for already existing
395 // datasets in the category.
396 // By first creating the datasetdefinition above we
397 // can manage to automatically take care of
398 // some possible realtime concurrence
399 if ($olderdatasetdefs = get_records_select(
400 'question_dataset_definitions',
401 "type = '$datasetdef->type'
402 AND name = '$datasetdef->name'
403 AND category = '$datasetdef->category'
404 AND id < $datasetdef->id
405 ORDER BY id DESC")) {
407 while ($olderdatasetdef = array_shift($olderdatasetdefs)) {
408 delete_records('question_dataset_definitions',
409 'id', $datasetdef->id
);
410 $datasetdef = $olderdatasetdef;
415 // Create relation to this dataset:
416 $questiondataset = new stdClass
;
417 $questiondataset->question
= $form->id
;
418 $questiondataset->datasetdefinition
= $datasetdef->id
;
419 if (!insert_record('question_datasets',
421 error("Unable to create relation to dataset $name");
423 unset($datasetdefinitions[$defid]);
426 // Remove local obsolete datasets as well as relations
427 // to datasets in other categories:
428 if (!empty($datasetdefinitions)) {
429 foreach ($datasetdefinitions as $def) {
430 delete_records('question_datasets',
431 'question', $form->id
,
432 'datasetdefinition', $def->id
);
434 if ($def->category
== 0) { // Question local dataset
435 delete_records('question_dataset_definitions', 'id', $def->id
);
436 delete_records('question_dataset_items',
437 'definition', $def->id
);
442 /** This function create a copy of the datasets ( definition and dataitems)
443 * from the preceding question if they remain in the new question
444 * otherwise its create the datasets that have been added as in the
445 * save_dataset_definitions()
447 function save_as_new_dataset_definitions($form, $initialid) {
449 // Get the datasets from the intial question
450 $datasetdefinitions = $this->get_dataset_definitions($initialid, $form->dataset
);
451 // $tmpdatasets contains those of the new question
452 $tmpdatasets = array_flip($form->dataset
);
453 $defids = array_keys($datasetdefinitions);// new datasets
454 foreach ($defids as $defid) {
455 $datasetdef = &$datasetdefinitions[$defid];
456 if (isset($datasetdef->id
)) {
457 // This dataset exist in the initial question
458 if (!isset($tmpdatasets[$defid])) {
459 // do not exist in the new question so ignore
460 unset($datasetdefinitions[$defid]);
463 // create a copy but not for category one
464 if (0 == $datasetdef->category
) {
465 $olddatasetid = $datasetdef->id
;
466 $olditemcount = $datasetdef->itemcount
;
467 $datasetdef->itemcount
=0;
468 if (!$datasetdef->id
= insert_record(
469 'question_dataset_definitions', $datasetdef)) {
470 error("Unable to create dataset $defid");
473 $olditems = get_records_sql( // Use number as key!!
474 " SELECT itemnumber, value
475 FROM {$CFG->prefix}question_dataset_items
476 WHERE definition = $olddatasetid ");
477 if (count($olditems) > 0 ) {
479 foreach($olditems as $item ){
480 $item->definition
= $datasetdef->id
;
481 if (!insert_record('question_dataset_items', $item)) {
482 error("Unable to insert dataset item $item->itemnumber with $item->value for $datasetdef->name");
487 $datasetdef->itemcount
=$itemcount;
488 update_record('question_dataset_definitions', $datasetdef);
489 } // end of copy the dataitems
490 }// end of copy the datasetdef
491 // Create relation to the new question with this
492 // copy as new datasetdef from the initial question
493 $questiondataset = new stdClass
;
494 $questiondataset->question
= $form->id
;
495 $questiondataset->datasetdefinition
= $datasetdef->id
;
496 if (!insert_record('question_datasets',
498 error("Unable to create relation to dataset $name");
500 unset($datasetdefinitions[$defid]);
502 }// end of datasetdefs from the initial question
503 // really new one code similar to save_dataset_definitions()
504 if (!$datasetdef->id
= insert_record(
505 'question_dataset_definitions', $datasetdef)) {
506 error("Unable to create dataset $defid");
509 if (0 != $datasetdef->category
) {
510 // We need to look for already existing
511 // datasets in the category.
512 // By first creating the datasetdefinition above we
513 // can manage to automatically take care of
514 // some possible realtime concurrence
515 if ($olderdatasetdefs = get_records_select(
516 'question_dataset_definitions',
517 "type = '$datasetdef->type'
518 AND name = '$datasetdef->name'
519 AND category = '$datasetdef->category'
520 AND id < $datasetdef->id
521 ORDER BY id DESC")) {
523 while ($olderdatasetdef = array_shift($olderdatasetdefs)) {
524 delete_records('question_dataset_definitions',
525 'id', $datasetdef->id
);
526 $datasetdef = $olderdatasetdef;
531 // Create relation to this dataset:
532 $questiondataset = new stdClass
;
533 $questiondataset->question
= $form->id
;
534 $questiondataset->datasetdefinition
= $datasetdef->id
;
535 if (!insert_record('question_datasets',
537 error("Unable to create relation to dataset $name");
539 unset($datasetdefinitions[$defid]);
542 // Remove local obsolete datasets as well as relations
543 // to datasets in other categories:
544 if (!empty($datasetdefinitions)) {
545 foreach ($datasetdefinitions as $def) {
546 delete_records('question_datasets',
547 'question', $form->id
,
548 'datasetdefinition', $def->id
);
550 if ($def->category
== 0) { // Question local dataset
551 delete_records('question_dataset_definitions', 'id', $def->id
);
552 delete_records('question_dataset_items',
553 'definition', $def->id
);
562 function save_question($question, &$form, $course) {
563 // For dataset dependent questions a wizard is used for editing
564 // questions. Therefore saving the question is delayed until
565 // we're through with the whole wizard.
567 $this->validate_form($form);
569 // See where we're coming from
570 switch($form->wizardpage) {
572 unset($SESSION->datasetdependent); // delete any remaining data
573 // from previous wizards
574 if (empty($form->id)) {
575 $SESSION->datasetdependent->question = $form;
576 $question = $this->create_runtime_question($question, $form);
578 $question = parent::save_question($question, $form, $course);
581 case 'datasetdefinitions':
582 $SESSION->datasetdependent->datasetdefinitions = $form;
583 if (empty($form->id)) {
584 $question = $this->create_runtime_question($question, $SESSION->datasetdependent->question);
586 $this->save_dataset_definitions($form);
587 $this->get_question_options($question);
588 //unset($SESSION->datasetdependent->datasetdefinitions);
590 //$this->get_question_options($question);
593 if (!empty($form->addbutton) && isset($SESSION->datasetdependent->question)) {
595 $question = parent::save_question($question, $SESSION->datasetdependent->question, $course);
596 $SESSION->datasetdependent->datasetdefinitions->id = $question->id;
597 $this->save_dataset_definitions($SESSION->datasetdependent->datasetdefinitions);
598 //$this->get_dataset_definitions($question);
599 unset($SESSION->datasetdependent);
602 if (empty($question->id)) {
603 $question = $this->create_runtime_question($question, $SESSION->datasetdependent->question);
605 $this->get_question_options($question);
610 error('Incorrect or no wizard page specified!');
620 /// Dataset functionality
621 function pick_question_dataset($question, $datasetitem) {
622 // Select a dataset in the following format:
623 // An array indexed by the variable names (d.name) pointing to the value
626 if (!$dataset = get_records_sql(
627 "SELECT d.name, i.value
628 FROM {$CFG->prefix}question_dataset_definitions d,
629 {$CFG->prefix}question_dataset_items i,
630 {$CFG->prefix}question_datasets q
631 WHERE q.question = $question->id
632 AND q.datasetdefinition = d.id
633 AND d.id = i.definition
634 AND i.itemnumber = $datasetitem")) {
635 error("Couldn't get the specified dataset for a dataset dependent " .
636 "question! (question: {$question->id}, " .
637 "datasetitem: {$datasetitem})");
639 array_walk($dataset, create_function('&$val', '$val = $val->value;'));
643 function create_virtual_qtype() {
644 error("No virtual question type for question type ".$this->name());
647 function get_virtual_qtype() {
648 if (!$this->virtualqtype
) {
649 $this->virtualqtype
= $this->create_virtual_qtype();
651 return $this->virtualqtype
;
654 function comment_header($question) {
655 // Used by datasetitems.php
656 // Default returns nothing and thus takes away the column
660 function comment_on_datasetitems($question, $data, $number) {
661 // Used by datasetitems.php
662 // Default returns nothing
666 function supports_dataset_item_generation() {
667 // Used by datasetitems.php
668 // Default does not support any item generation
672 function custom_generator_tools($datasetdef) {
673 // Used by datasetitems.php
674 // If there is no generation support,
675 // there cannot possibly be any custom tools either
679 function generate_dataset_item($options) {
680 // Used by datasetitems.php
681 // By default nothing is generated
685 function update_dataset_options($datasetdefs, $form) {
686 // Used by datasetitems.php
687 // Returns the updated datasets
688 // By default the dataset options cannot be updated
692 function dataset_options($form, $name,$prefix='',$langfile='quiz') {
694 // First options - it is not a dataset...
695 $options['0'] = get_string($prefix.'nodataset', $langfile);
697 // Construct question local options
699 $currentdatasetdef = get_record_sql(
701 FROM {$CFG->prefix}question_dataset_definitions a,
702 {$CFG->prefix}question_datasets b
703 WHERE a.id = b.datasetdefinition
704 AND b.question = '$form->id'
705 AND a.name = '$name'")
706 or $currentdatasetdef->type
= '0';
707 foreach (array( LITERAL
, FILE
, LINK
) as $type) {
708 $key = "$type-0-$name";
709 if ($currentdatasetdef->type
== $type
710 and $currentdatasetdef->category
== 0) {
711 $options[$key] = get_string($prefix."keptlocal$type", $langfile);
713 $options[$key] = get_string($prefix."newlocal$type", $langfile);
717 // Construct question category options
718 $categorydatasetdefs = get_records_sql(
720 FROM {$CFG->prefix}question_dataset_definitions a,
721 {$CFG->prefix}question_datasets b
722 WHERE a.id = b.datasetdefinition
723 AND a.category = '$form->category'
724 AND a.name = '$name'");
725 foreach(array( LITERAL
, FILE
, LINK
) as $type) {
726 $key = "$type-$form->category-$name";
727 if (isset($categorydatasetdefs[$type])
728 and $categorydef = $categorydatasetdefs[$type]) {
729 if ($currentdatasetdef->type
== $type
730 and $currentdatasetdef->id
== $categorydef->id
) {
731 $options[$key] = get_string($prefix."keptcategory$type", $langfile);
733 $options[$key] = get_string($prefix."existingcategory$type", $langfile);
736 $options[$key] = get_string($prefix."newcategory$type", $langfile);
741 return array($options, $currentdatasetdef->type
742 ?
"$currentdatasetdef->type-$currentdatasetdef->category-$name"
746 function find_dataset_names($text) {
747 /// Returns the possible dataset names found in the text as an array
748 /// The array has the dataset name for both key and value
749 $datasetnames = array();
750 while (ereg('\\{([[:alpha:]][^>} <{"\']*)\\}', $text, $regs)) {
751 $datasetnames[$regs[1]] = $regs[1];
752 $text = str_replace($regs[0], '', $text);
754 return $datasetnames;
757 function create_virtual_nameprefix($nameprefix, $datasetinput) {
758 if (!ereg('([0-9]+)' . $this->name() . '$', $nameprefix, $regs)) {
759 error("Wrongly formatted nameprefix $nameprefix");
761 $virtualqtype = $this->get_virtual_qtype();
762 return $nameprefix . $regs[1] . $virtualqtype->name();
766 //// END OF CLASS ////