PageRenderTime 58ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/question/type/questiontype.php

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