| [ Index ] |
PHP Cross Reference of Moodle 1.9.3 [Build 15-Oct-2008] |
[Summary view] [Print] [Text view]
1 <?php // $Id: questiontype.php,v 1.74.2.13 2008/10/08 10:19:51 jamiesensei Exp $ 2 /** 3 * The default questiontype class. 4 * 5 * @author Martin Dougiamas and many others. This has recently been completely 6 * rewritten by Alex Smith, Julian Sedding and Gustav Delius as part of 7 * the Serving Mathematics project 8 * {@link http://maths.york.ac.uk/serving_maths} 9 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License 10 * @package questionbank 11 * @subpackage questiontypes 12 */ 13 14 require_once($CFG->libdir . '/questionlib.php'); 15 16 /** 17 * This is the base class for Moodle question types. 18 * 19 * There are detailed comments on each method, explaining what the method is 20 * for, and the circumstances under which you might need to override it. 21 * 22 * Note: the questiontype API should NOT be considered stable yet. Very few 23 * question tyeps have been produced yet, so we do not yet know all the places 24 * where the current API is insufficient. I would rather learn from the 25 * experiences of the first few question type implementors, and improve the 26 * interface to meet their needs, rather the freeze the API prematurely and 27 * condem everyone to working round a clunky interface for ever afterwards. 28 * 29 * @package questionbank 30 * @subpackage questiontypes 31 */ 32 class default_questiontype { 33 34 /** 35 * Name of the question type 36 * 37 * The name returned should coincide with the name of the directory 38 * in which this questiontype is located 39 * 40 * @return string the name of this question type. 41 */ 42 function name() { 43 return 'default'; 44 } 45 46 /** 47 * The name this question should appear as in the create new question 48 * dropdown. 49 * 50 * @return mixed the desired string, or false to hide this question type in the menu. 51 */ 52 function menu_name() { 53 $name = $this->name(); 54 $menu_name = get_string($name, 'qtype_' . $name); 55 if ($menu_name[0] == '[') { 56 // Legacy behavior, if the string was not in the proper qtype_name 57 // language file, look it up in the quiz one. 58 $menu_name = get_string($name, 'quiz'); 59 } 60 return $menu_name; 61 } 62 63 /** 64 * @return boolean true if this question can only be graded manually. 65 */ 66 function is_manual_graded() { 67 return false; 68 } 69 70 /** 71 * @return boolean true if this question type can be used by the random question type. 72 */ 73 function is_usable_by_random() { 74 return true; 75 } 76 77 /** 78 * @return whether the question_answers.answer field needs to have 79 * restore_decode_content_links_worker called on it. 80 */ 81 function has_html_answers() { 82 return false; 83 } 84 85 /** 86 * If your question type has a table that extends the question table, and 87 * you want the base class to automatically save, backup and restore the extra fields, 88 * override this method to return an array wherer the first element is the table name, 89 * and the subsequent entries are the column names (apart from id and questionid). 90 * 91 * @return mixed array as above, or null to tell the base class to do nothing. 92 */ 93 function extra_question_fields() { 94 return null; 95 } 96 97 /** 98 * If your question type has a table that extends the question_answers table, 99 * make this method return an array wherer the first element is the table name, 100 * and the subsequent entries are the column names (apart from id and answerid). 101 * 102 * @return mixed array as above, or null to tell the base class to do nothing. 103 */ 104 function extra_answer_fields() { 105 return null; 106 } 107 108 /** 109 * Return an instance of the question editing form definition. This looks for a 110 * class called edit_{$this->name()}_question_form in the file 111 * {$CFG->docroot}/question/type/{$this->name()}/edit_{$this->name()}_question_form.php 112 * and if it exists returns an instance of it. 113 * 114 * @param string $submiturl passed on to the constructor call. 115 * @return object an instance of the form definition, or null if one could not be found. 116 */ 117 function create_editing_form($submiturl, $question, $category, $contexts, $formeditable) { 118 global $CFG; 119 require_once("{$CFG->dirroot}/question/type/edit_question_form.php"); 120 $definition_file = $CFG->dirroot.'/question/type/'.$this->name().'/edit_'.$this->name().'_form.php'; 121 if (!(is_readable($definition_file) && is_file($definition_file))) { 122 return null; 123 } 124 require_once($definition_file); 125 $classname = 'question_edit_'.$this->name().'_form'; 126 if (!class_exists($classname)) { 127 return null; 128 } 129 return new $classname($submiturl, $question, $category, $contexts, $formeditable); 130 } 131 132 /** 133 * @return string the full path of the folder this plugin's files live in. 134 */ 135 function plugin_dir() { 136 global $CFG; 137 return $CFG->dirroot . '/question/type/' . $this->name(); 138 } 139 140 /** 141 * @return string the URL of the folder this plugin's files live in. 142 */ 143 function plugin_baseurl() { 144 global $CFG; 145 return $CFG->wwwroot . '/question/type/' . $this->name(); 146 } 147 148 /** 149 * This method should be overriden if you want to include a special heading or some other 150 * html on a question editing page besides the question editing form. 151 * 152 * @param question_edit_form $mform a child of question_edit_form 153 * @param object $question 154 * @param string $wizardnow is '' for first page. 155 */ 156 function display_question_editing_page(&$mform, $question, $wizardnow){ 157 list($heading, $langmodule) = $this->get_heading(empty($question->id)); 158 print_heading_with_help($heading, $this->name(), $langmodule); 159 $permissionstrs = array(); 160 if (!empty($question->id)){ 161 if ($question->formoptions->canedit){ 162 $permissionstrs[] = get_string('permissionedit', 'question'); 163 } 164 if ($question->formoptions->canmove){ 165 $permissionstrs[] = get_string('permissionmove', 'question'); 166 } 167 if ($question->formoptions->cansaveasnew){ 168 $permissionstrs[] = get_string('permissionsaveasnew', 'question'); 169 } 170 } 171 if (!$question->formoptions->movecontext && count($permissionstrs)){ 172 print_heading(get_string('permissionto', 'question'), 'center', 3); 173 $html = '<ul>'; 174 foreach ($permissionstrs as $permissionstr){ 175 $html .= '<li>'.$permissionstr.'</li>'; 176 } 177 $html .= '</ul>'; 178 print_box($html, 'boxwidthnarrow boxaligncenter generalbox'); 179 } 180 $mform->display(); 181 } 182 183 /** 184 * Method called by display_question_editing_page and by question.php to get heading for breadcrumbs. 185 * 186 * @return array a string heading and the langmodule in which it was found. 187 */ 188 function get_heading($adding = false){ 189 $name = $this->name(); 190 $langmodule = 'qtype_' . $name; 191 if (!$adding){ 192 $strtoget = 'editing' . $name; 193 } else { 194 $strtoget = 'adding' . $name; 195 } 196 $strheading = get_string($strtoget, $langmodule); 197 if ($strheading[0] == '[') { 198 // Legacy behavior, if the string was not in the proper qtype_name 199 // language file, look it up in the quiz one. 200 $langmodule = 'quiz'; 201 $strheading = get_string($strtoget, $langmodule); 202 } 203 return array($strheading, $langmodule); 204 } 205 206 /** 207 * 208 * 209 * @param $question 210 */ 211 function set_default_options(&$question) { 212 } 213 214 /** 215 * Saves or updates a question after editing by a teacher 216 * 217 * Given some question info and some data about the answers 218 * this function parses, organises and saves the question 219 * It is used by {@link question.php} when saving new data from 220 * a form, and also by {@link import.php} when importing questions 221 * This function in turn calls {@link save_question_options} 222 * to save question-type specific options 223 * @param object $question the question object which should be updated 224 * @param object $form the form submitted by the teacher 225 * @param object $course the course we are in 226 * @return object On success, return the new question object. On failure, 227 * return an object as follows. If the error object has an errors field, 228 * display that as an error message. Otherwise, the editing form will be 229 * redisplayed with validation errors, from validation_errors field, which 230 * is itself an object, shown next to the form fields. 231 */ 232 function save_question($question, $form, $course) { 233 global $USER; 234 235 // This default implementation is suitable for most 236 // question types. 237 238 // First, save the basic question itself 239 $question->name = trim($form->name); 240 $question->questiontext = trim($form->questiontext); 241 $question->questiontextformat = $form->questiontextformat; 242 $question->parent = isset($form->parent)? $form->parent : 0; 243 $question->length = $this->actual_number_of_questions($question); 244 $question->penalty = isset($form->penalty) ? $form->penalty : 0; 245 246 if (empty($form->image)) { 247 $question->image = ""; 248 } else { 249 $question->image = $form->image; 250 } 251 252 if (empty($form->generalfeedback)) { 253 $question->generalfeedback = ''; 254 } else { 255 $question->generalfeedback = trim($form->generalfeedback); 256 } 257 258 if (empty($question->name)) { 259 $question->name = shorten_text(strip_tags($question->questiontext), 15); 260 if (empty($question->name)) { 261 $question->name = '-'; 262 } 263 } 264 265 if ($question->penalty > 1 or $question->penalty < 0) { 266 $question->errors['penalty'] = get_string('invalidpenalty', 'quiz'); 267 } 268 269 if (isset($form->defaultgrade)) { 270 $question->defaultgrade = $form->defaultgrade; 271 } 272 273 if (!empty($question->id) && !empty($form->categorymoveto)) { // Question already exists 274 list($movetocategory, $movetocontextid) = explode(',', $form->categorymoveto); 275 if ($movetocategory != $question->category){ 276 question_require_capability_on($question, 'move'); 277 $question->category = $movetocategory; 278 //don't need to test add permission of category we are moving question to. 279 //Only categories that we have permission to add 280 //a question to will get through the form cleaning code for the select box. 281 } 282 // keep existing unique stamp code 283 $question->stamp = get_field('question', 'stamp', 'id', $question->id); 284 $question->modifiedby = $USER->id; 285 $question->timemodified = time(); 286 if (!update_record('question', $question)) { 287 error('Could not update question!'); 288 } 289 } else { // Question is a new one 290 if (isset($form->categorymoveto)){ 291 // Doing save as new question, and we have move rights. 292 list($question->category, $notused) = explode(',', $form->categorymoveto); 293 //don't need to test add permission of category we are moving question to. 294 //Only categories that we have permission to add 295 //a question to will get through the form cleaning code for the select box. 296 } else { 297 // Really a new question. 298 list($question->category, $notused) = explode(',', $form->category); 299 } 300 // Set the unique code 301 $question->stamp = make_unique_id_code(); 302 $question->createdby = $USER->id; 303 $question->modifiedby = $USER->id; 304 $question->timecreated = time(); 305 $question->timemodified = time(); 306 if (!$question->id = insert_record('question', $question)) { 307 print_object($question); 308 error('Could not insert new question!'); 309 } 310 } 311 312 // Now to save all the answers and type-specific options 313 314 $form->id = $question->id; 315 $form->qtype = $question->qtype; 316 $form->category = $question->category; 317 $form->questiontext = $question->questiontext; 318 319 $result = $this->save_question_options($form); 320 321 if (!empty($result->error)) { 322 error($result->error); 323 } 324 325 if (!empty($result->notice)) { 326 notice($result->notice, "question.php?id=$question->id"); 327 } 328 329 if (!empty($result->noticeyesno)) { 330 notice_yesno($result->noticeyesno, "question.php?id=$question->id&courseid={$course->id}", 331 "edit.php?courseid={$course->id}"); 332 print_footer($course); 333 exit; 334 } 335 336 // Give the question a unique version stamp determined by question_hash() 337 if (!set_field('question', 'version', question_hash($question), 'id', $question->id)) { 338 error('Could not update question version field'); 339 } 340 341 return $question; 342 } 343 344 /** 345 * Saves question-type specific options 346 * 347 * This is called by {@link save_question()} to save the question-type specific data 348 * @return object $result->error or $result->noticeyesno or $result->notice 349 * @param object $question This holds the information from the editing form, 350 * it is not a standard question object. 351 */ 352 function save_question_options($question) { 353 $extra_question_fields = $this->extra_question_fields(); 354 355 if (is_array($extra_question_fields)) { 356 $question_extension_table = array_shift($extra_question_fields); 357 358 $function = 'update_record'; 359 $options = get_record($question_extension_table, 'questionid', $question->id); 360 if (!$options) { 361 $function = 'insert_record'; 362 $options = new stdClass; 363 $options->questionid = $question->id; 364 } 365 foreach ($extra_question_fields as $field) { 366 if (!isset($question->$field)) { 367 $result = new stdClass; 368 $result->error = "No data for field $field when saving " . 369 $this->name() . ' question id ' . $question->id; 370 return $result; 371 } 372 $options->$field = $question->$field; 373 } 374 375 if (!$function($question_extension_table, $options)) { 376 $result = new stdClass; 377 $result->error = 'Could not save question options for ' . 378 $this->name() . ' question id ' . $question->id; 379 return $result; 380 } 381 } 382 383 $extra_answer_fields = $this->extra_answer_fields(); 384 // TODO save the answers, with any extra data. 385 386 return null; 387 } 388 389 /** 390 * Changes all states for the given attempts over to a new question 391 * 392 * This is used by the versioning code if the teacher requests that a question 393 * gets replaced by the new version. In order for the attempts to be regraded 394 * properly all data in the states referring to the old question need to be 395 * changed to refer to the new version instead. In particular for question types 396 * that use the answers table the answers belonging to the old question have to 397 * be changed to those belonging to the new version. 398 * 399 * @param integer $oldquestionid The id of the old question 400 * @param object $newquestion The new question 401 * @param array $attempts An array of all attempt objects in whose states 402 * replacement should take place 403 */ 404 function replace_question_in_attempts($oldquestionid, $newquestion, $attemtps) { 405 echo 'Not yet implemented'; 406 return; 407 } 408 409 /** 410 * Loads the question type specific options for the question. 411 * 412 * This function loads any question type specific options for the 413 * question from the database into the question object. This information 414 * is placed in the $question->options field. A question type is 415 * free, however, to decide on a internal structure of the options field. 416 * @return bool Indicates success or failure. 417 * @param object $question The question object for the question. This object 418 * should be updated to include the question type 419 * specific information (it is passed by reference). 420 */ 421 function get_question_options(&$question) { 422 global $CFG; 423 424 if (!isset($question->options)) { 425 $question->options = new object; 426 } 427 428 $extra_question_fields = $this->extra_question_fields(); 429 if (is_array($extra_question_fields)) { 430 $question_extension_table = array_shift($extra_question_fields); 431 $extra_data = get_record($question_extension_table, 'questionid', $question->id, '', '', '', '', implode(', ', $extra_question_fields)); 432 if ($extra_data) { 433 foreach ($extra_question_fields as $field) { 434 $question->options->$field = $extra_data->$field; 435 } 436 } else { 437 notify("Failed to load question options from the table $question_extension_table for questionid " . 438 $question->id); 439 return false; 440 } 441 } 442 443 $extra_answer_fields = $this->extra_answer_fields(); 444 if (is_array($extra_answer_fields)) { 445 $answer_extension_table = array_shift($extra_answer_fields); 446 $question->options->answers = get_records_sql(' 447 SELECT qa.*, qax.' . implode(', qax.', $extra_answer_fields) . ' 448 FROM ' . $CFG->prefix . 'question_answers qa, ' . $CFG->prefix . '$answer_extension_table qax 449 WHERE qa.questionid = ' . $question->id . ' AND qax.answerid = qa.id'); 450 if (!$question->options->answers) { 451 notify("Failed to load question answers from the table $answer_extension_table for questionid " . 452 $question->id); 453 return false; 454 } 455 } else { 456 // Don't check for success or failure because some question types do not use the answers table. 457 $question->options->answers = get_records('question_answers', 'question', $question->id, 'id ASC'); 458 } 459 460 return true; 461 } 462 463 /** 464 * Deletes states from the question-type specific tables 465 * 466 * @param string $stateslist Comma separated list of state ids to be deleted 467 */ 468 function delete_states($stateslist) { 469 /// The default question type does not have any tables of its own 470 // therefore there is nothing to delete 471 472 return true; 473 } 474 475 /** 476 * Deletes a question from the question-type specific tables 477 * 478 * @return boolean Success/Failure 479 * @param object $question The question being deleted 480 */ 481 function delete_question($questionid) { 482 global $CFG; 483 $success = true; 484 485 $extra_question_fields = $this->extra_question_fields(); 486 if (is_array($extra_question_fields)) { 487 $question_extension_table = array_shift($extra_question_fields); 488 $success = $success && delete_records($question_extension_table, 'questionid', $questionid); 489 } 490 491 $extra_answer_fields = $this->extra_answer_fields(); 492 if (is_array($extra_answer_fields)) { 493 $answer_extension_table = array_shift($extra_answer_fields); 494 $success = $success && delete_records_select($answer_extension_table, 495 "answerid IN (SELECT qa.id FROM {$CFG->prefix}question_answers qa WHERE qa.question = $questionid)"); 496 } 497 498 $success = $success && delete_records('question_answers', 'question', $questionid); 499 500 return $success; 501 } 502 503 /** 504 * Returns the number of question numbers which are used by the question 505 * 506 * This function returns the number of question numbers to be assigned 507 * to the question. Most question types will have length one; they will be 508 * assigned one number. The 'description' type, however does not use up a 509 * number and so has a length of zero. Other question types may wish to 510 * handle a bundle of questions and hence return a number greater than one. 511 * @return integer The number of question numbers which should be 512 * assigned to the question. 513 * @param object $question The question whose length is to be determined. 514 * Question type specific information is included. 515 */ 516 function actual_number_of_questions($question) { 517 // By default, each question is given one number 518 return 1; 519 } 520 521 /** 522 * Creates empty session and response information for the question 523 * 524 * This function is called to start a question session. Empty question type 525 * specific session data (if any) and empty response data will be added to the 526 * state object. Session data is any data which must persist throughout the 527 * attempt possibly with updates as the user interacts with the 528 * question. This function does NOT create new entries in the database for 529 * the session; a call to the {@link save_session_and_responses} member will 530 * occur to do this. 531 * @return bool Indicates success or failure. 532 * @param object $question The question for which the session is to be 533 * created. Question type specific information is 534 * included. 535 * @param object $state The state to create the session for. Note that 536 * this will not have been saved in the database so 537 * there will be no id. This object will be updated 538 * to include the question type specific information 539 * (it is passed by reference). In particular, empty 540 * responses will be created in the ->responses 541 * field. 542 * @param object $cmoptions 543 * @param object $attempt The attempt for which the session is to be 544 * started. Questions may wish to initialize the 545 * session in different ways depending on the user id 546 * or time available for the attempt. 547 */ 548 function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) { 549 // The default implementation should work for the legacy question types. 550 // Most question types with only a single form field for the student's response 551 // will use the empty string '' as the index for that one response. This will 552 // automatically be stored in and restored from the answer field in the 553 // question_states table. 554 $state->responses = array( 555 '' => '', 556 ); 557 return true; 558 } 559 560 /** 561 * Restores the session data and most recent responses for the given state 562 * 563 * This function loads any session data associated with the question 564 * session in the given state from the database into the state object. 565 * In particular it loads the responses that have been saved for the given 566 * state into the ->responses member of the state object. 567 * 568 * Question types with only a single form field for the student's response 569 * will not need not restore the responses; the value of the answer 570 * field in the question_states table is restored to ->responses[''] 571 * before this function is called. Question types with more response fields 572 * should override this method and set the ->responses field to an 573 * associative array of responses. 574 * @return bool Indicates success or failure. 575 * @param object $question The question object for the question including any 576 * question type specific information. 577 * @param object $state The saved state to load the session for. This 578 * object should be updated to include the question 579 * type specific session information and responses 580 * (it is passed by reference). 581 */ 582 function restore_session_and_responses(&$question, &$state) { 583 // The default implementation does nothing (successfully) 584 return true; 585 } 586 587 /** 588 * Saves the session data and responses for the given question and state 589 * 590 * This function saves the question type specific session data from the 591 * state object to the database. In particular for most question types it saves the 592 * responses from the ->responses member of the state object. The question type 593 * non-specific data for the state has already been saved in the question_states 594 * table and the state object contains the corresponding id and 595 * sequence number which may be used to index a question type specific table. 596 * 597 * Question types with only a single form field for the student's response 598 * which is contained in ->responses[''] will not have to save this response, 599 * it will already have been saved to the answer field of the question_states table. 600 * Question types with more response fields should override this method to convert 601 * the data the ->responses array into a single string field, and save it in the 602 * database. The implementation in the multichoice question type is a good model to follow. 603 * http://cvs.moodle.org/contrib/plugins/question/type/opaque/questiontype.php?view=markup 604 * has a solution that is probably quite generally applicable. 605 * @return bool Indicates success or failure. 606 * @param object $question The question object for the question including 607 * the question type specific information. 608 * @param object $state The state for which the question type specific 609 * data and responses should be saved. 610 */ 611 function save_session_and_responses(&$question, &$state) { 612 // The default implementation does nothing (successfully) 613 return true; 614 } 615 616 /** 617 * Returns an array of values which will give full marks if graded as 618 * the $state->responses field 619 * 620 * The correct answer to the question in the given state, or an example of 621 * a correct answer if there are many, is returned. This is used by some question 622 * types in the {@link grade_responses()} function but it is also used by the 623 * question preview screen to fill in correct responses. 624 * @return mixed A response array giving the responses corresponding 625 * to the (or a) correct answer to the question. If there is 626 * no correct answer that scores 100% then null is returned. 627 * @param object $question The question for which the correct answer is to 628 * be retrieved. Question type specific information is 629 * available. 630 * @param object $state The state of the question, for which a correct answer is 631 * needed. Question type specific information is included. 632 */ 633 function get_correct_responses(&$question, &$state) { 634 /* The default implementation returns the response for the first answer 635 that gives full marks. */ 636 if ($question->options->answers) { 637 foreach ($question->options->answers as $answer) { 638 if (((int) $answer->fraction) === 1) { 639 return array('' => addslashes($answer->answer)); 640 } 641 } 642 } 643 return null; 644 } 645 646 /** 647 * Return an array of values with the texts for all possible responses stored 648 * for the question 649 * 650 * All answers are found and their text values isolated 651 * @return object A mixed object 652 * ->id question id. Needed to manage random questions: 653 * it's the id of the actual question presented to user in a given attempt 654 * ->responses An array of values giving the responses corresponding 655 * to all answers to the question. Answer ids are used as keys. 656 * The text and partial credit are the object components 657 * @param object $question The question for which the answers are to 658 * be retrieved. Question type specific information is 659 * available. 660 */ 661 // ULPGC ecastro 662 function get_all_responses(&$question, &$state) { 663 if (isset($question->options->answers) && is_array($question->options->answers)) { 664 $answers = array(); 665 foreach ($question->options->answers as $aid=>$answer) { 666 $r = new stdClass; 667 $r->answer = $answer->answer; 668 $r->credit = $answer->fraction; 669 $answers[$aid] = $r; 670 } 671 $result = new stdClass; 672 $result->id = $question->id; 673 $result->responses = $answers; 674 return $result; 675 } else { 676 return null; 677 } 678 } 679 680 /** 681 * Return the actual response to the question in a given state 682 * for the question. 683 * 684 * @return mixed An array containing the response or reponses (multiple answer, match) 685 * given by the user in a particular attempt. 686 * @param object $question The question for which the correct answer is to 687 * be retrieved. Question type specific information is 688 * available. 689 * @param object $state The state object that corresponds to the question, 690 * for which a correct answer is needed. Question 691 * type specific information is included. 692 */ 693 // ULPGC ecastro 694 function get_actual_response($question, $state) { 695 if (!empty($state->responses)) { 696 $responses[] = stripslashes($state->responses['']); 697 } else { 698 $responses[] = ''; 699 } 700 return $responses; 701 } 702 703 // ULPGC ecastro 704 function get_fractional_grade(&$question, &$state) { 705 $maxgrade = $question->maxgrade; 706 $grade = $state->grade; 707 if ($maxgrade) { 708 return (float)($grade/$maxgrade); 709 } else { 710 return (float)$grade; 711 } 712 } 713 714 715 /** 716 * Checks if the response given is correct and returns the id 717 * 718 * @return int The ide number for the stored answer that matches the response 719 * given by the user in a particular attempt. 720 * @param object $question The question for which the correct answer is to 721 * be retrieved. Question type specific information is 722 * available. 723 * @param object $state The state object that corresponds to the question, 724 * for which a correct answer is needed. Question 725 * type specific information is included. 726 */ 727 // ULPGC ecastro 728 function check_response(&$question, &$state){ 729 return false; 730 } 731 732 // Used by the following function, so that it only returns results once per quiz page. 733 var $already_done = false; 734 /** 735 * If this question type requires extra CSS or JavaScript to function, 736 * then this method will return an array of <link ...> tags that reference 737 * those stylesheets. This function will also call require_js() 738 * from ajaxlib.php, to get any necessary JavaScript linked in too. 739 * 740 * The two parameters match the first two parameters of print_question. 741 * 742 * @param object $question The question object. 743 * @param object $state The state object. 744 * 745 * @return an array of bits of HTML to add to the head of pages where 746 * this question is print_question-ed in the body. The array should use 747 * integer array keys, which have no significance. 748 */ 749 function get_html_head_contributions(&$question, &$state) { 750 // By default, we link to any of the files styles.css, styles.php, 751 // script.js or script.php that exist in the plugin folder. 752 // Core question types should not use this mechanism. Their styles 753 // should be included in the standard theme. 754 755 // We only do this once 756 // for this question type, no matter how often this method is called. 757 if ($this->already_done) { 758 return array(); 759 } 760 $this->already_done = true; 761 762 $plugindir = $this->plugin_dir(); 763 $baseurl = $this->plugin_baseurl(); 764 $stylesheets = array(); 765 if (file_exists($plugindir . '/styles.css')) { 766 $stylesheets[] = 'styles.css'; 767 } 768 if (file_exists($plugindir . '/styles.php')) { 769 $stylesheets[] = 'styles.php'; 770 } 771 if (file_exists($plugindir . '/script.js')) { 772 require_js($baseurl . '/script.js'); 773 } 774 if (file_exists($plugindir . '/script.php')) { 775 require_js($baseurl . '/script.php'); 776 } 777 $contributions = array(); 778 foreach ($stylesheets as $stylesheet) { 779 $contributions[] = '<link rel="stylesheet" type="text/css" href="' . 780 $baseurl . '/' . $stylesheet . '" />'; 781 } 782 return $contributions; 783 } 784 785 /** 786 * Prints the question including the number, grading details, content, 787 * feedback and interactions 788 * 789 * This function prints the question including the question number, 790 * grading details, content for the question, any feedback for the previously 791 * submitted responses and the interactions. The default implementation calls 792 * various other methods to print each of these parts and most question types 793 * will just override those methods. 794 * @param object $question The question to be rendered. Question type 795 * specific information is included. The 796 * maximum possible grade is in ->maxgrade. The name 797 * prefix for any named elements is in ->name_prefix. 798 * @param object $state The state to render the question in. The grading 799 * information is in ->grade, ->raw_grade and 800 * ->penalty. The current responses are in 801 * ->responses. This is an associative array (or the 802 * empty string or null in the case of no responses 803 * submitted). The last graded state is in 804 * ->last_graded (hence the most recently graded 805 * responses are in ->last_graded->responses). The 806 * question type specific information is also 807 * included. 808 * @param integer $number The number for this question. 809 * @param object $cmoptions 810 * @param object $options An object describing the rendering options. 811 */ 812 function print_question(&$question, &$state, $number, $cmoptions, $options) { 813 /* The default implementation should work for most question types 814 provided the member functions it calls are overridden where required. 815 The layout is determined by the template question.html */ 816 817 global $CFG; 818 $isgraded = question_state_is_graded($state->last_graded); 819 820 // get the context so we can determine whether some extra links 821 // should be shown. 822 if (!empty($cmoptions->id)) { 823 $cm = get_coursemodule_from_instance('quiz', $cmoptions->id); 824 $context = get_context_instance(CONTEXT_MODULE, $cm->id); 825 $cmorcourseid = '&cmid='.$cm->id; 826 } else if (!empty($cmoptions->course)) { 827 $context = get_context_instance(CONTEXT_COURSE, $cmoptions->course); 828 $cmorcourseid = '&courseid='.$cmoptions->course; 829 } else { 830 error('Need to provide courseid or cmid to print_question.'); 831 } 832 833 // For editing teachers print a link to an editing popup window 834 $editlink = ''; 835 if (question_has_capability_on($question, 'edit')) { 836 $stredit = get_string('edit'); 837 $linktext = '<img src="'.$CFG->pixpath.'/t/edit.gif" alt="'.$stredit.'" />'; 838 $editlink = link_to_popup_window('/question/question.php?inpopup=1&id=' . 839 $question->id . $cmorcourseid, 'editquestion', 840 $linktext, false, false, $stredit, '', true); 841 } 842 843 $generalfeedback = ''; 844 if ($isgraded && $options->generalfeedback) { 845 $generalfeedback = $this->format_text($question->generalfeedback, 846 $question->questiontextformat, $cmoptions); 847 } 848 849 $grade = ''; 850 if ($question->maxgrade and $options->scores) { 851 if ($cmoptions->optionflags & QUESTION_ADAPTIVE) { 852 $grade = !$isgraded ? '--/' : round($state->last_graded->grade, $cmoptions->decimalpoints).'/'; 853 } 854 $grade .= $question->maxgrade; 855 } 856 857 $comment = stripslashes($state->manualcomment); 858 $commentlink = ''; 859 860 if (isset($options->questioncommentlink) && $context && has_capability('mod/quiz:grade', $context)) { 861 $strcomment = get_string('commentorgrade', 'quiz'); 862 $question_to_comment = isset($question->randomquestionid) ? $question->randomquestionid : $question->id; 863 $commentlink = '<div class="commentlink">'.link_to_popup_window ($options->questioncommentlink.'?attempt='.$state->attempt.'&question='.$question_to_comment, 864 'commentquestion', $strcomment, 450, 650, $strcomment, 'none', true).'</div>'; 865 } 866 867 $history = $this->history($question, $state, $number, $cmoptions, $options); 868 869 include "$CFG->dirroot/question/type/question.html"; 870 } 871 872 /* 873 * Print history of responses 874 * 875 * Used by print_question() 876 */ 877 function history($question, $state, $number, $cmoptions, $options) { 878 $history = ''; 879 if(isset($options->history) and $options->history) { 880 if ($options->history == 'all') { 881 // show all states 882 $states = get_records_select('question_states', "attempt = '$state->attempt' AND question = '$question->id' AND event > '0'", 'seq_number ASC'); 883 } else { 884 // show only graded states 885 $states = get_records_select('question_states', "attempt = '$state->attempt' AND question = '$question->id' AND event IN (".QUESTION_EVENTS_GRADED.")", 'seq_number ASC'); 886 } 887 if (count($states) > 1) { 888 $strreviewquestion = get_string('reviewresponse', 'quiz'); 889 $table = new stdClass; 890 $table->width = '100%'; 891 if ($options->scores) { 892 $table->head = array ( 893 get_string('numberabbr', 'quiz'), 894 get_string('action', 'quiz'), 895 get_string('response', 'quiz'), 896 get_string('time'), 897 get_string('score', 'quiz'), 898 //get_string('penalty', 'quiz'), 899 get_string('grade', 'quiz'), 900 ); 901 } else { 902 $table->head = array ( 903 get_string('numberabbr', 'quiz'), 904 get_string('action', 'quiz'), 905 get_string('response', 'quiz'), 906 get_string('time'), 907 ); 908 } 909 910 foreach ($states as $st) { 911 $st->responses[''] = $st->answer; 912 $this->restore_session_and_responses($question, $st); 913 $b = ($state->id == $st->id) ? '<b>' : ''; 914 $be = ($state->id == $st->id) ? '</b>' : ''; 915 if ($state->id == $st->id) { 916 $link = '<b>'.$st->seq_number.'</b>'; 917 } else { 918 if(isset($options->questionreviewlink)) { 919 $link = link_to_popup_window ($options->questionreviewlink.'?state='.$st->id.'&number='.$number, 920 'reviewquestion', $st->seq_number, 450, 650, $strreviewquestion, 'none', true); 921 } else { 922 $link = $st->seq_number; 923 } 924 } 925 if ($options->scores) { 926 $table->data[] = array ( 927 $link, 928 $b.get_string('event'.$st->event, 'quiz').$be, 929 $b.$this->response_summary($question, $st).$be, 930 $b.userdate($st->timestamp, get_string('timestr', 'quiz')).$be, 931 $b.round($st->raw_grade, $cmoptions->decimalpoints).$be, 932 //$b.round($st->penalty, $cmoptions->decimalpoints).$be, 933 $b.round($st->grade, $cmoptions->decimalpoints).$be 934 ); 935 } else { 936 $table->data[] = array ( 937 $link, 938 $b.get_string('event'.$st->event, 'quiz').$be, 939 $b.$this->response_summary($question, $st).$be, 940 $b.userdate($st->timestamp, get_string('timestr', 'quiz')).$be, 941 ); 942 } 943 } 944 $history = print_table($table, true); 945 } 946 } 947 return $history; 948 } 949 950 951 /** 952 * Prints the score obtained and maximum score available plus any penalty 953 * information 954 * 955 * This function prints a summary of the scoring in the most recently 956 * graded state (the question may not have been submitted for marking at 957 * the current state). The default implementation should be suitable for most 958 * question types. 959 * @param object $question The question for which the grading details are 960 * to be rendered. Question type specific information 961 * is included. The maximum possible grade is in 962 * ->maxgrade. 963 * @param object $state The state. In particular the grading information 964 * is in ->grade, ->raw_grade and ->penalty. 965 * @param object $cmoptions 966 * @param object $options An object describing the rendering options. 967 */ 968 function print_question_grading_details(&$question, &$state, $cmoptions, $options) { 969 /* The default implementation prints the number of marks if no attempt 970 has been made. Otherwise it displays the grade obtained out of the 971 maximum grade available and a warning if a penalty was applied for the 972 attempt and displays the overall grade obtained counting all previous 973 responses (and penalties) */ 974 975 if (QUESTION_EVENTDUPLICATE == $state->event) { 976 echo ' '; 977 print_string('duplicateresponse', 'quiz'); 978 } 979 if (!empty($question->maxgrade) && $options->scores) { 980 if (question_state_is_graded($state->last_graded)) { 981 // Display the grading details from the last graded state 982 $grade = new stdClass; 983 $grade->cur = round($state->last_graded->grade, $cmoptions->decimalpoints); 984 $grade->max = $question->maxgrade; 985 $grade->raw = round($state->last_graded->raw_grade, $cmoptions->decimalpoints); 986 987 // let student know wether the answer was correct 988 echo '<div class="correctness '; 989 if ($state->last_graded->raw_grade >= $question->maxgrade/1.01) { // We divide by 1.01 so that rounding errors dont matter. 990 echo ' correct">'; 991 print_string('correct', 'quiz'); 992 } else if ($state->last_graded->raw_grade > 0) { 993 echo ' partiallycorrect">'; 994 print_string('partiallycorrect', 'quiz'); 995 } else { 996 echo ' incorrect">'; 997 print_string('incorrect', 'quiz'); 998 } 999 echo '</div>'; 1000 1001 echo '<div class="gradingdetails">'; 1002 // print grade for this submission 1003 print_string('gradingdetails', 'quiz', $grade); 1004 if ($cmoptions->penaltyscheme) { 1005 // print details of grade adjustment due to penalties 1006 if ($state->last_graded->raw_grade > $state->last_graded->grade){ 1007 echo ' '; 1008 print_string('gradingdetailsadjustment', 'quiz', $grade); 1009 } 1010 // print info about new penalty 1011 // penalty is relevant only if the answer is not correct and further attempts are possible 1012 if (($state->last_graded->raw_grade < $question->maxgrade / 1.01) 1013 and (QUESTION_EVENTCLOSEANDGRADE !== $state->event)) { 1014 1015 if ('' !== $state->last_graded->penalty && ((float)$state->last_graded->penalty) > 0.0) { 1016 // A penalty was applied so display it 1017 echo ' '; 1018 print_string('gradingdetailspenalty', 'quiz', $state->last_graded->penalty); 1019 } else { 1020 /* No penalty was applied even though the answer was 1021 not correct (eg. a syntax error) so tell the student 1022 that they were not penalised for the attempt */ 1023 echo ' '; 1024 print_string('gradingdetailszeropenalty', 'quiz'); 1025 } 1026 } 1027 } 1028 echo '</div>'; 1029 } 1030 } 1031 } 1032 1033 /** 1034 * Prints the main content of the question including any interactions 1035 * 1036 * This function prints the main content of the question including the 1037 * interactions for the question in the state given. The last graded responses 1038 * are printed or indicated and the current responses are selected or filled in. 1039 * Any names (eg. for any form elements) are prefixed with $question->name_prefix. 1040 * This method is called from the print_question method. 1041 * @param object $question The question to be rendered. Question type 1042 * specific information is included. The name 1043 * prefix for any named elements is in ->name_prefix. 1044 * @param object $state The state to render the question in. The grading 1045 * information is in ->grade, ->raw_grade and 1046 * ->penalty. The current responses are in 1047 * ->responses. This is an associative array (or the 1048 * empty string or null in the case of no responses 1049 * submitted). The last graded state is in 1050 * ->last_graded (hence the most recently graded 1051 * responses are in ->last_graded->responses). The 1052 * question type specific information is also 1053 * included. 1054 * The state is passed by reference because some adaptive 1055 * questions may want to update it during rendering 1056 * @param object $cmoptions 1057 * @param object $options An object describing the rendering options. 1058 */ 1059 function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) { 1060 /* This default implementation prints an error and must be overridden 1061 by all question type implementations, unless the default implementation 1062 of print_question has been overridden. */ 1063 1064 notify('Error: Question formulation and input controls has not' 1065 .' been implemented for question type '.$this->name()); 1066 } 1067 1068 /** 1069 * Prints the submit button(s) for the question in the given state 1070 * 1071 * This function prints the submit button(s) for the question in the 1072 * given state. The name of any button created will be prefixed with the 1073 * unique prefix for the question in $question->name_prefix. The suffix 1074 * 'submit' is reserved for the single question submit button and the suffix 1075 * 'validate' is reserved for the single question validate button (for 1076 * question types which support it). Other suffixes will result in a response 1077 * of that name in $state->responses which the printing and grading methods 1078 * can then use. 1079 * @param object $question The question for which the submit button(s) are to 1080 * be rendered. Question type specific information is 1081 * included. The name prefix for any 1082 * named elements is in ->name_prefix. 1083 * @param object $state The state to render the buttons for. The 1084 * question type specific information is also 1085 * included. 1086 * @param object $cmoptions 1087 * @param object $options An object describing the rendering options. 1088 */ 1089 function print_question_submit_buttons(&$question, &$state, $cmoptions, $options) { 1090 /* The default implementation should be suitable for most question 1091 types. It prints a mark button in the case where individual marking is 1092 allowed. */ 1093 1094 if (($cmoptions->optionflags & QUESTION_ADAPTIVE) and !$options->readonly) { 1095 echo '<input type="submit" name="', $question->name_prefix, 'submit" value="', 1096 get_string('mark', 'quiz'), '" class="submit btn" onclick="', 1097 "form.action = form.action + '#q", $question->id, "'; return true;", '" />'; 1098 } 1099 } 1100 1101 /** 1102 * Return a summary of the student response 1103 * 1104 * This function returns a short string of no more than a given length that 1105 * summarizes the student's response in the given $state. This is used for 1106 * example in the response history table. This string should already be, 1107 * for output. 1108 * @return string The summary of the student response 1109 * @param object $question 1110 * @param object $state The state whose responses are to be summarized 1111 * @param int $length The maximum length of the returned string 1112 */ 1113 function response_summary($question, $state, $length=80) { 1114 // This should almost certainly be overridden 1115 $responses = $this->get_actual_response($question, $state); 1116 if (empty($responses) || !is_array($responses)) { 1117 $responses = array(); 1118 } 1119 if (is_array($responses)) { 1120 $responses = implode(',', array_map('s', $responses)); 1121 } 1122 return shorten_text($responses, $length); 1123 } 1124 1125 /** 1126 * Renders the question for printing and returns the LaTeX source produced 1127 * 1128 * This function should render the question suitable for a printed problem 1129 * or solution sheet in LaTeX and return the rendered output. 1130 * @return string The LaTeX output. 1131 * @param object $question The question to be rendered. Question type 1132 * specific information is included. 1133 * @param object $state The state to render the question in. The 1134 * question type specific information is also 1135 * included. 1136 * @param object $cmoptions 1137 * @param string $type Indicates if the question or the solution is to be 1138 * rendered with the values 'question' and 1139 * 'solution'. 1140 */ 1141 function get_texsource(&$question, &$state, $cmoptions, $type) { 1142 // The default implementation simply returns a string stating that 1143 // the question is only available online. 1144 1145 return get_string('onlineonly', 'texsheet'); 1146 } 1147 1148 /** 1149 * Compares two question states for equivalence of the student's responses 1150 * 1151 * The responses for the two states must be examined to see if they represent 1152 * equivalent answers to the question by the student. This method will be 1153 * invoked for each of the previous states of the question before grading 1154 * occurs. If the student is found to have already attempted the question 1155 * with equivalent responses then the attempt at the question is ignored; 1156 * grading does not occur and the state does not change. Thus they are not 1157 * penalized for this case. 1158 * @return boolean 1159 * @param object $question The question for which the states are to be 1160 * compared. Question type specific information is 1161 * included. 1162 * @param object $state The state of the question. The responses are in 1163 * ->responses. This is the only field of $state 1164 * that it is safe to use. 1165 * @param object $teststate The state whose responses are to be 1166 * compared. The state will be of the same age or 1167 * older than $state. If possible, the method should 1168 * only use the field $teststate->responses, however 1169 * any field that is set up by restore_session_and_responses 1170 * can be used. 1171 */ 1172 function compare_responses(&$question, $state, $teststate) { 1173 // The default implementation performs a comparison of the response 1174 // arrays. The ordering of the arrays does not matter. 1175 // Question types may wish to override this (eg. to ignore trailing 1176 // white space or to make "7.0" and "7" compare equal). 1177 1178 // In php neither == nor === compare arrays the way you want. The following 1179 // ensures that the arrays have the same keys, with the same values. 1180 $result = false; 1181 $diff1 = array_diff_assoc($state->responses, $teststate->responses); 1182 if (empty($diff1)) { 1183 $diff2 = array_diff_assoc($teststate->responses, $state->responses); 1184 $result = empty($diff2); 1185 } 1186 1187 return $result; 1188 } 1189 1190 /** 1191 * Checks whether a response matches a given answer 1192 * 1193 * This method only applies to questions that use teacher-defined answers 1194 * 1195 * @return boolean 1196 */ 1197 function test_response(&$question, &$state, $answer) { 1198 $response = isset($state->responses['']) ? $state->responses[''] : ''; 1199 return ($response == $answer->answer); 1200 } 1201 1202 /** 1203 * Performs response processing and grading 1204 * 1205 * This function performs response processing and grading and updates 1206 * the state accordingly. 1207 * @return boolean Indicates success or failure. 1208 * @param object $question The question to be graded. Question type 1209 * specific information is included. 1210 * @param object $state The state of the question to grade. The current 1211 * responses are in ->responses. The last graded state 1212 * is in ->last_graded (hence the most recently graded 1213 * responses are in ->last_graded->responses). The 1214 * question type specific information is also 1215 * included. The ->raw_grade and ->penalty fields 1216 * must be updated. The method is able to 1217 * close the question session (preventing any further 1218 * attempts at this question) by setting 1219 * $state->event to QUESTION_EVENTCLOSEANDGRADE 1220 * @param object $cmoptions 1221 */ 1222 function grade_responses(&$question, &$state, $cmoptions) { 1223 // The default implementation uses the test_response method to 1224 // compare what the student entered against each of the possible 1225 // answers stored in the question, and uses the grade from the 1226 // first one that matches. It also sets the marks and penalty. 1227 // This should be good enought for most simple question types. 1228 1229 $state->raw_grade = 0; 1230 foreach($question->options->answers as $answer) { 1231 if($this->test_response($question, $state, $answer)) { 1232 $state->raw_grade = $answer->fraction; 1233 break; 1234 } 1235 } 1236 1237 // Make sure we don't assign negative or too high marks. 1238 $state->raw_grade = min(max((float) $state->raw_grade, 1239 0.0), 1.0) * $question->maxgrade; 1240 1241 // Update the penalty. 1242 $state->penalty = $question->penalty * $question->maxgrade; 1243 1244 // mark the state as graded 1245 $state->event = ($state->event == QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE; 1246 1247 return true; 1248 } 1249 1250 1251 /** 1252 * Includes configuration settings for the question type on the quiz admin 1253 * page 1254 * 1255 * TODO: It makes no sense any longer to do the admin for question types 1256 * from the quiz admin page. This should be changed. 1257 * Returns an array of objects describing the options for the question type 1258 * to be included on the quiz module admin page. 1259 * Configuration options can be included by setting the following fields in 1260 * the object: 1261 * ->name The name of the option within this question type. 1262 * The full option name will be constructed as 1263 * "quiz_{$this->name()}_$name", the human readable name 1264 * will be displayed with get_string($name, 'quiz'). 1265 * ->code The code to display the form element, help button, etc. 1266 * i.e. the content for the central table cell. Be sure 1267 * to name the element "quiz_{$this->name()}_$name" and 1268 * set the value to $CFG->{"quiz_{$this->name()}_$name"}. 1269 * ->help Name of the string from the quiz module language file 1270 * to be used for the help message in the third column of 1271 * the table. An empty string (or the field not set) 1272 * means to leave the box empty. 1273 * Links to custom settings pages can be included by setting the following 1274 * fields in the object: 1275 * ->name The name of the link text string. 1276 * get_string($name, 'quiz') will be called. 1277 * ->link The filename part of the URL for the link. The full URL 1278 * is contructed as 1279 * "$CFG->wwwroot/question/type/{$this->name()}/$link?sesskey=$sesskey" 1280 * [but with the relavant calls to the s and rawurlencode 1281 * functions] where $sesskey is the sesskey for the user. 1282 * @return array Array of objects describing the configuration options to 1283 * be included on the quiz module admin page. 1284 */ 1285 function get_config_options() { 1286 // No options by default 1287 1288 return false; 1289 } 1290 1291 /** 1292 * Returns true if the editing wizard is finished, false otherwise. 1293 * 1294 * The default implementation returns true, which is suitable for all question- 1295 * types that only use one editing form. This function is used in 1296 * question.php to decide whether we can regrade any states of the edited 1297 * question and redirect to edit.php. 1298 * 1299 * The dataset dependent question-type, which is extended by the calculated 1300 * question-type, overwrites this method because it uses multiple pages (i.e. 1301 * a wizard) to set up the question and associated datasets. 1302 * 1303 * @param object $form The data submitted by the previous page. 1304 * 1305 * @return boolean Whether the wizard's last page was submitted or not. 1306 */ 1307 function finished_edit_wizard(&$form) { 1308 //In the default case there is only one edit page. 1309 return true; 1310 } 1311 1312 /** 1313 * Prints a table of course modules in which the question is used 1314 * 1315 * TODO: This should be made quiz-independent 1316 * 1317 * This function is used near the end of the question edit forms in all question types 1318 * It prints the table of quizzes in which the question is used 1319 * containing checkboxes to allow the teacher to replace the old question version 1320 * 1321 * @param object $question 1322 * @param object $course 1323 * @param integer $cmid optional The id of the course module currently being edited 1324 */ 1325 function print_replacement_options($question, $course, $cmid='0') { 1326 1327 // Disable until the versioning code has been fixed 1328 if (true) { 1329 return; 1330 } 1331 1332 // no need to display replacement options if the question is new 1333 if(empty($question->id)) { 1334 return true; 1335 } 1336 1337 // get quizzes using the question (using the question_instances table) 1338 $quizlist = array(); 1339 if(!$instances = get_records('quiz_question_instances', 'question', $question->id)) { 1340 $instances = array(); 1341 } 1342 foreach($instances as $instance) { 1343 $quizlist[$instance->quiz] = $instance->quiz; 1344 } 1345 $quizlist = implode(',', $quizlist); 1346 if(empty($quizlist) or !$quizzes = get_records_list('quiz', 'id', $quizlist)) { 1347 $quizzes = array(); 1348 } 1349 1350 // do the printing 1351 if(count($quizzes) > 0) { 1352 // print the table 1353 $strquizname = get_string('modulename', 'quiz'); 1354 $strdoreplace = get_string('replace', 'quiz'); 1355 $straffectedstudents = get_string('affectedstudents', 'quiz', $course->students); 1356 echo "<tr valign=\"top\">\n"; 1357 echo "<td align=\"right\"><b>".get_string("replacementoptions", "quiz").":</b></td>\n"; 1358 echo "<td align=\"left\">\n"; 1359 echo "<table cellpadding=\"5\" align=\"left\" class=\"generalbox\" width=\"100%\">\n"; 1360 echo "<tr>\n"; 1361 echo "<th align=\"left\" valign=\"top\" nowrap=\"nowrap\" class=\"generaltableheader c0\" scope=\"col\">$strquizname</th>\n"; 1362 echo "<th align=\"center\" valign=\"top\" nowrap=\"nowrap\" class=\"generaltableheader c0\" scope=\"col\">$strdoreplace</th>\n"; 1363 echo "<th align=\"left\" valign=\"top\" nowrap=\"nowrap\" class=\"generaltableheader c0\" scope=\"col\">$straffectedstudents</th>\n"; 1364 echo "</tr>\n"; 1365 foreach($quizzes as $quiz) { 1366 // work out whethere it should be checked by default 1367 $checked = ''; 1368 if((int)$cmid === (int)$quiz->id 1369 or empty($quiz->usercount)) { 1370 $checked = "checked=\"checked\""; 1371 } 1372 1373 // find how many different students have already attempted this quiz 1374 $students = array(); 1375 if($attempts = get_records_select('quiz_attempts', "quiz = '$quiz->id' AND preview = '0'")) { 1376 foreach($attempts as $attempt) { 1377 if (record_exists('question_states', 'attempt', $attempt->uniqueid, 'question', $question->id, 'originalquestion', 0)) { 1378 $students[$attempt->userid] = 1; 1379 } 1380 } 1381 } 1382 $studentcount = count($students); 1383 1384 $strstudents = $studentcount === 1 ? $course->student : $course->students; 1385 echo "<tr>\n"; 1386 echo "<td align=\"left\" class=\"generaltablecell c0\">".format_string($quiz->name)."</td>\n"; 1387 echo "<td align=\"center\" class=\"generaltablecell c0\"><input name=\"q{$quiz->id}replace\" type=\"checkbox\" ".$checked." /></td>\n"; 1388 echo "<td align=\"left\" class=\"generaltablecell c0\">".(($studentcount) ? $studentcount.' '.$strstudents : '-')."</td>\n"; 1389 echo "</tr>\n"; 1390 } 1391 echo "</table>\n"; 1392 } 1393 echo "</td></tr>\n"; 1394 } 1395 1396 /** 1397 * Call format_text from weblib.php with the options appropriate to question types. 1398 * 1399 * @param string $text the text to format. 1400 * @param integer $text the type of text. Normally $question->questiontextformat. 1401 * @param object $cmoptions the context the string is being displayed in. Only $cmoptions->course is used. 1402 * @return string the formatted text. 1403 */ 1404 function format_text($text, $textformat, $cmoptions = NULL) { 1405 $formatoptions = new stdClass; 1406 $formatoptions->noclean = true; 1407 $formatoptions->para = false; 1408 return format_text($text, $textformat, $formatoptions, $cmoptions === NULL ? NULL : $cmoptions->course); 1409 } 1410 1411 /* 1412 * Find all course / site files linked from a question. 1413 * 1414 * Need to check for links to files in question_answers.answer and feedback 1415 * and in question table in generalfeedback and questiontext fields. Methods 1416 * on child classes will also check extra question specific fields. 1417 * 1418 * Needs to be overriden for child classes that have extra fields containing 1419 * html. 1420 * 1421 * @param string html the html to search 1422 * @param int courseid search for files for courseid course or set to siteid for 1423 * finding site files. 1424 * @return array of url, relative url is key and array with one item = question id as value 1425 * relative url is relative to course/site files directory root. 1426 */ 1427 function find_file_links($question, $courseid){ 1428 $urls = array(); 1429 1430 /// Question image 1431 if ($question->image != ''){ 1432 if (substr(strtolower($question->image), 0, 7) == 'http://') { 1433 $matches = array(); 1434 1435 //support for older questions where we have a complete url in image field 1436 if (preg_match('!^'.question_file_links_base_url($courseid).'(.*)!i', $question->image, $matches)){ 1437 if ($cleanedurl = question_url_check($urls[$matches[2]])){ 1438 $urls[$cleanedurl] = null; 1439 } 1440 } 1441 } else { 1442 if ($question->image != ''){ 1443 if ($cleanedurl = question_url_check($question->image)){ 1444 $urls[$cleanedurl] = null;//will be set later 1445 } 1446 } 1447 1448 } 1449 1450 } 1451 1452 /// Questiontext and general feedback. 1453 $urls += question_find_file_links_from_html($question->questiontext, $courseid); 1454 $urls += question_find_file_links_from_html($question->generalfeedback, $courseid); 1455 1456 /// Answers, if this question uses them. 1457 if (isset($question->options->answers)){ 1458 foreach ($question->options->answers as $answerkey => $answer){ 1459 /// URLs in the answers themselves, if appropriate. 1460 if ($this->has_html_answers()) { 1461 $urls += question_find_file_links_from_html($answer->answer, $courseid); 1462 } 1463 /// URLs in the answer feedback. 1464 $urls += question_find_file_links_from_html($answer->feedback, $courseid); 1465 } 1466 } 1467 1468 /// Set all the values of the array to the question object 1469 if ($urls){ 1470 $urls = array_combine(array_keys($urls), array_fill(0, count($urls), array($question->id))); 1471 } 1472 return $urls; 1473 } 1474 /* 1475 * Find all course / site files linked from a question. 1476 * 1477 * Need to check for links to files in question_answers.answer and feedback 1478 * and in question table in generalfeedback and questiontext fields. Methods 1479 * on child classes will also check extra question specific fields. 1480 * 1481 * Needs to be overriden for child classes that have extra fields containing 1482 * html. 1483 * 1484 * @param string html the html to search 1485 * @param int course search for files for courseid course or set to siteid for 1486 * finding site files. 1487 * @return array of files, file name is key and array with one item = question id as value 1488 */ 1489 function replace_file_links($question, $fromcourseid, $tocourseid, $url, $destination){ 1490 global $CFG; 1491 $updateqrec = false; 1492 1493 /// Question image 1494 if (!empty($question->image)){ 1495 //support for older questions where we have a complete url in image field 1496 if (substr(strtolower($question->image), 0, 7) == 'http://') { 1497 $questionimage = preg_replace('!^'.question_file_links_base_url($fromcourseid).preg_quote($url, '!').'$!i', $destination, $question->image, 1); 1498 } else { 1499 $questionimage = preg_replace('!^'.preg_quote($url, '!').'$!i', $destination, $question->image, 1); 1500 } 1501 if ($questionimage != $question->image){ 1502 $question->image = $questionimage; 1503 $updateqrec = true; 1504 } 1505 } 1506 1507 /// Questiontext and general feedback. 1508 $question->questiontext = question_replace_file_links_in_html($question->questiontext, $fromcourseid, $tocourseid, $url, $destination, $updateqrec); 1509 $question->generalfeedback = question_replace_file_links_in_html($question->generalfeedback, $fromcourseid, $tocourseid, $url, $destination, $updateqrec); 1510 1511 /// If anything has changed, update it in the database. 1512 if ($updateqrec){ 1513 if (!update_record('question', addslashes_recursive($question))){ 1514 error ('Couldn\'t update question '.$question->name); 1515 } 1516 } 1517 1518 1519 /// Answers, if this question uses them. 1520 if (isset($question->options->answers)){ 1521 //answers that do not need updating have been unset 1522 foreach ($question->options->answers as $answer){ 1523 $answerchanged = false; 1524 /// URLs in the answers themselves, if appropriate. 1525 if ($this->has_html_answers()) { 1526 $answer->answer = question_replace_file_links_in_html($answer->answer, $fromcourseid, $tocourseid, $url, $destination, $answerchanged); 1527 } 1528 /// URLs in the answer feedback. 1529 $answer->feedback = question_replace_file_links_in_html($answer->feedback, $fromcourseid, $tocourseid, $url, $destination, $answerchanged); 1530 /// If anything has changed, update it in the database. 1531 if ($answerchanged){ 1532 if (!update_record('question_answers', addslashes_recursive($answer))){ 1533 error ('Couldn\'t update question ('.$question->name.') answer '.$answer->id); 1534 } 1535 } 1536 } 1537 } 1538 } 1539 /** 1540 * @return the best link to pass to print_error. 1541 * @param $cmoptions as passed in from outside. 1542 */ 1543 function error_link($cmoptions) { 1544 global $CFG; 1545 $cm = get_coursemodule_from_instance('quiz', $cmoptions->id); 1546 if (!empty($cm->id)) { 1547 return $CFG->wwwroot . '/mod/quiz/view.php?id=' . $cm->id; 1548 } else if (!empty($cm->course)) { 1549 return $CFG->wwwroot . '/course/view.php?id=' . $cm->course; 1550 } else { 1551 return ''; 1552 } 1553 } 1554 1555 /// BACKUP FUNCTIONS //////////////////////////// 1556 1557 /* 1558 * Backup the data in the question 1559 * 1560 * This is used in question/backuplib.php 1561 */ 1562 function backup($bf,$preferences,$question,$level=6) { 1563 // The default type has nothing to back up 1564 return true; 1565 } 1566 1567 /// RESTORE FUNCTIONS ///////////////// 1568 1569 /* 1570 * Restores the data in the question 1571 * 1572 * This is used in question/restorelib.php 1573 */ 1574 function restore($old_question_id,$new_question_id,$info,$restore) { 1575 // The default question type has nothing to restore 1576 return true; 1577 } 1578 1579 function restore_map($old_question_id,$new_question_id,$info,$restore) { 1580 // There is nothing to decode 1581 return true; 1582 } 1583 1584 function restore_recode_answer($state, $restore) { 1585 // There is nothing to decode 1586 return $state->answer; 1587 } 1588 1589 /** 1590 * Abstract function implemented by each question type. It runs all the code 1591 * required to set up and save a question of any type for testing purposes. 1592 * Alternate DB table prefix may be used to facilitate data deletion. 1593 */ 1594 function generate_test($name, $courseid=null) { 1595 $form = new stdClass(); 1596 $form->name = $name; 1597 $form->questiontextformat = 1; 1598 $form->questiontext = 'test question, generated by script'; 1599 $form->defaultgrade = 1; 1600 $form->penalty = 0.1; 1601 $form->generalfeedback = "Well done"; 1602 1603 $context = get_context_instance(CONTEXT_COURSE, $courseid); 1604 $newcategory = question_make_default_categories(array($context)); 1605 $form->category = $newcategory->id . ',1'; 1606 1607 $question = new stdClass(); 1608 $question->courseid = $courseid; 1609 $question->qtype = $this->qtype; 1610 return array($form, $question); 1611 } 1612 } 1613 ?>
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated: Wed Jan 14 11:33:29 2009 | Cross-referenced by PHPXref 0.7 |