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':
310 $this->save_dataset_definitions($form);
313 $this->save_dataset_items($question, $form);
316 error('Incorrect or no wizard page specified!');
322 function save_dataset_items($question, $fromform){
323 //overridden in child classes
326 function get_dataset_definitions($questionid, $newdatasets) {
327 //get the existing datasets for this question
328 $datasetdefs = array();
329 if (!empty($questionid)) {
332 FROM {$CFG->prefix}question_datasets d,
333 {$CFG->prefix}question_dataset_definitions i
334 WHERE d.question = '$questionid'
335 AND d.datasetdefinition = i.id
337 if ($records = get_records_sql($sql)) {
338 foreach ($records as $r) {
339 $datasetdefs["$r->type-$r->category-$r->name"] = $r;
344 foreach ($newdatasets as $dataset) {
346 continue; // The no dataset case...
349 if (!isset($datasetdefs[$dataset])) {
350 //make new datasetdef
351 list($type, $category, $name) = explode('-', $dataset, 3);
352 $datasetdef = new stdClass
;
353 $datasetdef->type
= $type;
354 $datasetdef->name
= $name;
355 $datasetdef->category
= $category;
356 $datasetdef->itemcount
= 0;
357 $datasetdef->options
= 'uniform:1.0:10.0:1';
358 $datasetdefs[$dataset] = clone($datasetdef);
364 function save_dataset_definitions($form) {
366 $datasetdefinitions = $this->get_dataset_definitions($form->id
, $form->dataset
);
367 $tmpdatasets = array_flip($form->dataset
);
368 $defids = array_keys($datasetdefinitions);
369 foreach ($defids as $defid) {
370 $datasetdef = &$datasetdefinitions[$defid];
371 if (isset($datasetdef->id
)) {
372 if (!isset($tmpdatasets[$defid])) {
373 // This dataset is not used any more, delete it
374 delete_records('question_datasets',
375 'question', $form->id
,
376 'datasetdefinition', $datasetdef->id
);
377 if ($datasetdef->category
== 0) { // Question local dataset
378 delete_records('question_dataset_definitions',
379 'id', $datasetdef->id
);
380 delete_records('question_dataset_items',
381 'definition', $datasetdef->id
);
384 // This has already been saved or just got deleted
385 unset($datasetdefinitions[$defid]);
389 if (!$datasetdef->id
= insert_record(
390 'question_dataset_definitions', $datasetdef)) {
391 error("Unable to create dataset $defid");
394 if (0 != $datasetdef->category
) {
395 // We need to look for already existing
396 // datasets in the category.
397 // By first creating the datasetdefinition above we
398 // can manage to automatically take care of
399 // some possible realtime concurrence
400 if ($olderdatasetdefs = get_records_select(
401 'question_dataset_definitions',
402 "type = '$datasetdef->type'
403 AND name = '$datasetdef->name'
404 AND category = '$datasetdef->category'
405 AND id < $datasetdef->id
406 ORDER BY id DESC")) {
408 while ($olderdatasetdef = array_shift($olderdatasetdefs)) {
409 delete_records('question_dataset_definitions',
410 'id', $datasetdef->id
);
411 $datasetdef = $olderdatasetdef;
416 // Create relation to this dataset:
417 $questiondataset = new stdClass
;
418 $questiondataset->question
= $form->id
;
419 $questiondataset->datasetdefinition
= $datasetdef->id
;
420 if (!insert_record('question_datasets',
422 error("Unable to create relation to dataset $name");
424 unset($datasetdefinitions[$defid]);
427 // Remove local obsolete datasets as well as relations
428 // to datasets in other categories:
429 if (!empty($datasetdefinitions)) {
430 foreach ($datasetdefinitions as $def) {
431 delete_records('question_datasets',
432 'question', $form->id
,
433 'datasetdefinition', $def->id
);
435 if ($def->category
== 0) { // Question local dataset
436 delete_records('question_dataset_definitions', 'id', $def->id
);
437 delete_records('question_dataset_items',
438 'definition', $def->id
);
443 /** This function create a copy of the datasets ( definition and dataitems)
444 * from the preceding question if they remain in the new question
445 * otherwise its create the datasets that have been added as in the
446 * save_dataset_definitions()
448 function save_as_new_dataset_definitions($form, $initialid) {
450 // Get the datasets from the intial question
451 $datasetdefinitions = $this->get_dataset_definitions($initialid, $form->dataset
);
452 // $tmpdatasets contains those of the new question
453 $tmpdatasets = array_flip($form->dataset
);
454 $defids = array_keys($datasetdefinitions);// new datasets
455 foreach ($defids as $defid) {
456 $datasetdef = &$datasetdefinitions[$defid];
457 if (isset($datasetdef->id
)) {
458 // This dataset exist in the initial question
459 if (!isset($tmpdatasets[$defid])) {
460 // do not exist in the new question so ignore
461 unset($datasetdefinitions[$defid]);
464 // create a copy but not for category one
465 if (0 == $datasetdef->category
) {
466 $olddatasetid = $datasetdef->id
;
467 $olditemcount = $datasetdef->itemcount
;
468 $datasetdef->itemcount
=0;
469 if (!$datasetdef->id
= insert_record(
470 'question_dataset_definitions', $datasetdef)) {
471 error("Unable to create dataset $defid");
474 $olditems = get_records_sql( // Use number as key!!
475 " SELECT itemnumber, value
476 FROM {$CFG->prefix}question_dataset_items
477 WHERE definition = $olddatasetid ");
478 if (count($olditems) > 0 ) {
480 foreach($olditems as $item ){
481 $item->definition
= $datasetdef->id
;
482 if (!insert_record('question_dataset_items', $item)) {
483 error("Unable to insert dataset item $item->itemnumber with $item->value for $datasetdef->name");
488 $datasetdef->itemcount
=$itemcount;
489 update_record('question_dataset_definitions', $datasetdef);
490 } // end of copy the dataitems
491 }// end of copy the datasetdef
492 // Create relation to the new question with this
493 // copy as new datasetdef from the initial question
494 $questiondataset = new stdClass
;
495 $questiondataset->question
= $form->id
;
496 $questiondataset->datasetdefinition
= $datasetdef->id
;
497 if (!insert_record('question_datasets',
499 error("Unable to create relation to dataset $name");
501 unset($datasetdefinitions[$defid]);
503 }// end of datasetdefs from the initial question
504 // really new one code similar to save_dataset_definitions()
505 if (!$datasetdef->id
= insert_record(
506 'question_dataset_definitions', $datasetdef)) {
507 error("Unable to create dataset $defid");
510 if (0 != $datasetdef->category
) {
511 // We need to look for already existing
512 // datasets in the category.
513 // By first creating the datasetdefinition above we
514 // can manage to automatically take care of
515 // some possible realtime concurrence
516 if ($olderdatasetdefs = get_records_select(
517 'question_dataset_definitions',
518 "type = '$datasetdef->type'
519 AND name = '$datasetdef->name'
520 AND category = '$datasetdef->category'
521 AND id < $datasetdef->id
522 ORDER BY id DESC")) {
524 while ($olderdatasetdef = array_shift($olderdatasetdefs)) {
525 delete_records('question_dataset_definitions',
526 'id', $datasetdef->id
);
527 $datasetdef = $olderdatasetdef;
532 // Create relation to this dataset:
533 $questiondataset = new stdClass
;
534 $questiondataset->question
= $form->id
;
535 $questiondataset->datasetdefinition
= $datasetdef->id
;
536 if (!insert_record('question_datasets',
538 error("Unable to create relation to dataset $name");
540 unset($datasetdefinitions[$defid]);
543 // Remove local obsolete datasets as well as relations
544 // to datasets in other categories:
545 if (!empty($datasetdefinitions)) {
546 foreach ($datasetdefinitions as $def) {
547 delete_records('question_datasets',
548 'question', $form->id
,
549 'datasetdefinition', $def->id
);
551 if ($def->category
== 0) { // Question local dataset
552 delete_records('question_dataset_definitions', 'id', $def->id
);
553 delete_records('question_dataset_items',
554 'definition', $def->id
);
563 function save_question($question, &$form, $course) {
564 // For dataset dependent questions a wizard is used for editing
565 // questions. Therefore saving the question is delayed until
566 // we're through with the whole wizard.
568 $this->validate_form($form);
570 // See where we're coming from
571 switch($form->wizardpage) {
573 unset($SESSION->datasetdependent); // delete any remaining data
574 // from previous wizards
575 if (empty($form->id)) {
576 $SESSION->datasetdependent->question = $form;
577 $question = $this->create_runtime_question($question, $form);
579 $question = parent::save_question($question, $form, $course);
582 case 'datasetdefinitions':
583 $SESSION->datasetdependent->datasetdefinitions = $form;
584 if (empty($form->id)) {
585 $question = $this->create_runtime_question($question, $SESSION->datasetdependent->question);
587 $this->save_dataset_definitions($form);
588 $this->get_question_options($question);
589 //unset($SESSION->datasetdependent->datasetdefinitions);
591 //$this->get_question_options($question);
594 if (!empty($form->addbutton) && isset($SESSION->datasetdependent->question)) {
596 $question = parent::save_question($question, $SESSION->datasetdependent->question, $course);
597 $SESSION->datasetdependent->datasetdefinitions->id = $question->id;
598 $this->save_dataset_definitions($SESSION->datasetdependent->datasetdefinitions);
599 //$this->get_dataset_definitions($question);
600 unset($SESSION->datasetdependent);
603 if (empty($question->id)) {
604 $question = $this->create_runtime_question($question, $SESSION->datasetdependent->question);
606 $this->get_question_options($question);
611 error('Incorrect or no wizard page specified!');
621 /// Dataset functionality
622 function pick_question_dataset($question, $datasetitem) {
623 // Select a dataset in the following format:
624 // An array indexed by the variable names (d.name) pointing to the value
627 if (!$dataset = get_records_sql(
628 "SELECT d.name, i.value
629 FROM {$CFG->prefix}question_dataset_definitions d,
630 {$CFG->prefix}question_dataset_items i,
631 {$CFG->prefix}question_datasets q
632 WHERE q.question = $question->id
633 AND q.datasetdefinition = d.id
634 AND d.id = i.definition
635 AND i.itemnumber = $datasetitem")) {
636 error("Couldn't get the specified dataset for a dataset dependent " .
637 "question! (question: {$question->id}, " .
638 "datasetitem: {$datasetitem})");
640 array_walk($dataset, create_function('&$val', '$val = $val->value;'));
644 function create_virtual_qtype() {
645 error("No virtual question type for question type ".$this->name());
648 function get_virtual_qtype() {
649 if (!$this->virtualqtype
) {
650 $this->virtualqtype
= $this->create_virtual_qtype();
652 return $this->virtualqtype
;
655 function comment_header($question) {
656 // Used by datasetitems.php
657 // Default returns nothing and thus takes away the column
661 function comment_on_datasetitems($question, $data, $number) {
662 // Used by datasetitems.php
663 // Default returns nothing
667 function supports_dataset_item_generation() {
668 // Used by datasetitems.php
669 // Default does not support any item generation
673 function custom_generator_tools($datasetdef) {
674 // Used by datasetitems.php
675 // If there is no generation support,
676 // there cannot possibly be any custom tools either
680 function generate_dataset_item($options) {
681 // Used by datasetitems.php
682 // By default nothing is generated
686 function update_dataset_options($datasetdefs, $form) {
687 // Used by datasetitems.php
688 // Returns the updated datasets
689 // By default the dataset options cannot be updated
693 function dataset_options($form, $name,$prefix='',$langfile='quiz') {
695 // First options - it is not a dataset...
696 $options['0'] = get_string($prefix.'nodataset', $langfile);
698 // Construct question local options
700 $currentdatasetdef = get_record_sql(
702 FROM {$CFG->prefix}question_dataset_definitions a,
703 {$CFG->prefix}question_datasets b
704 WHERE a.id = b.datasetdefinition
705 AND b.question = '$form->id'
706 AND a.name = '$name'")
707 or $currentdatasetdef->type
= '0';
708 foreach (array( LITERAL
, FILE
, LINK
) as $type) {
709 $key = "$type-0-$name";
710 if ($currentdatasetdef->type
== $type
711 and $currentdatasetdef->category
== 0) {
712 $options[$key] = get_string($prefix."keptlocal$type", $langfile);
714 $options[$key] = get_string($prefix."newlocal$type", $langfile);
718 // Construct question category options
719 $categorydatasetdefs = get_records_sql(
721 FROM {$CFG->prefix}question_dataset_definitions a,
722 {$CFG->prefix}question_datasets b
723 WHERE a.id = b.datasetdefinition
724 AND a.category = '$form->category'
725 AND a.name = '$name'");
726 foreach(array( LITERAL
, FILE
, LINK
) as $type) {
727 $key = "$type-$form->category-$name";
728 if (isset($categorydatasetdefs[$type])
729 and $categorydef = $categorydatasetdefs[$type]) {
730 if ($currentdatasetdef->type
== $type
731 and $currentdatasetdef->id
== $categorydef->id
) {
732 $options[$key] = get_string($prefix."keptcategory$type", $langfile);
734 $options[$key] = get_string($prefix."existingcategory$type", $langfile);
737 $options[$key] = get_string($prefix."newcategory$type", $langfile);
742 return array($options, $currentdatasetdef->type
743 ?
"$currentdatasetdef->type-$currentdatasetdef->category-$name"
747 function find_dataset_names($text) {
748 /// Returns the possible dataset names found in the text as an array
749 /// The array has the dataset name for both key and value
750 $datasetnames = array();
751 while (ereg('\\{([[:alpha:]][^>} <{"\']*)\\}', $text, $regs)) {
752 $datasetnames[$regs[1]] = $regs[1];
753 $text = str_replace($regs[0], '', $text);
755 return $datasetnames;
758 function create_virtual_nameprefix($nameprefix, $datasetinput) {
759 if (!ereg('([0-9]+)' . $this->name() . '$', $nameprefix, $regs)) {
760 error("Wrongly formatted nameprefix $nameprefix");
762 $virtualqtype = $this->get_virtual_qtype();
763 return $nameprefix . $regs[1] . $virtualqtype->name();
767 //// END OF CLASS ////