behaviour.php 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. <?php
  2. // This file is part of Moodle - http://moodle.org/
  3. //
  4. // Moodle is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // Moodle is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * Question behaviour where the student can submit questions one at a
  18. * time for immediate feedback.
  19. *
  20. * @package qbehaviour
  21. * @subpackage interactive
  22. * @copyright 2009 The Open University
  23. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24. */
  25. defined('MOODLE_INTERNAL') || die();
  26. /**
  27. * Question behaviour for the interactive model.
  28. *
  29. * Each question has a submit button next to it which the student can use to
  30. * submit it. Once the qustion is submitted, it is not possible for the
  31. * student to change their answer any more, but the student gets full feedback
  32. * straight away.
  33. *
  34. * @copyright 2009 The Open University
  35. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  36. */
  37. class qbehaviour_interactive extends question_behaviour_with_multiple_tries {
  38. /**
  39. * Special value used for {@link question_display_options::$readonly when
  40. * we are showing the try again button to the student during an attempt.
  41. * The particular number was chosen randomly. PHP will treat it the same
  42. * as true, but in the renderer we reconginse it display the try again
  43. * button enabled even though the rest of the question is disabled.
  44. * @var integer
  45. */
  46. const READONLY_EXCEPT_TRY_AGAIN = 23485299;
  47. public function is_compatible_question(question_definition $question) {
  48. return $question instanceof question_automatically_gradable;
  49. }
  50. public function can_finish_during_attempt() {
  51. return true;
  52. }
  53. public function get_right_answer_summary() {
  54. return $this->question->get_right_answer_summary();
  55. }
  56. /**
  57. * @return bool are we are currently in the try_again state.
  58. */
  59. protected function is_try_again_state() {
  60. $laststep = $this->qa->get_last_step();
  61. return $this->qa->get_state()->is_active() && $laststep->has_behaviour_var('submit') &&
  62. $laststep->has_behaviour_var('_triesleft');
  63. }
  64. public function adjust_display_options(question_display_options $options) {
  65. // We only need different behaviour in try again states.
  66. if (!$this->is_try_again_state()) {
  67. parent::adjust_display_options($options);
  68. if ($this->qa->get_state() == question_state::$invalid &&
  69. $options->marks == question_display_options::MARK_AND_MAX) {
  70. $options->marks = question_display_options::MAX_ONLY;
  71. }
  72. return;
  73. }
  74. // Let the hint adjust the options.
  75. $hint = $this->get_applicable_hint();
  76. if (!is_null($hint)) {
  77. $hint->adjust_display_options($options);
  78. }
  79. // Now call the base class method, but protect some fields from being overwritten.
  80. $save = clone($options);
  81. parent::adjust_display_options($options);
  82. $options->feedback = $save->feedback;
  83. $options->numpartscorrect = $save->numpartscorrect;
  84. // In a try-again state, everything except the try again button
  85. // Should be read-only. This is a mild hack to achieve this.
  86. if (!$options->readonly) {
  87. $options->readonly = self::READONLY_EXCEPT_TRY_AGAIN;
  88. }
  89. }
  90. public function get_applicable_hint() {
  91. if (!$this->is_try_again_state()) {
  92. return null;
  93. }
  94. return $this->question->get_hint(count($this->question->hints) -
  95. $this->qa->get_last_behaviour_var('_triesleft'), $this->qa);
  96. }
  97. public function get_expected_data() {
  98. if ($this->is_try_again_state()) {
  99. return array(
  100. 'tryagain' => PARAM_BOOL,
  101. );
  102. } else if ($this->qa->get_state()->is_active()) {
  103. return array(
  104. 'submit' => PARAM_BOOL,
  105. );
  106. }
  107. return parent::get_expected_data();
  108. }
  109. public function get_expected_qt_data() {
  110. $hint = $this->get_applicable_hint();
  111. if (!empty($hint->clearwrong)) {
  112. return $this->question->get_expected_data();
  113. }
  114. return parent::get_expected_qt_data();
  115. }
  116. public function get_state_string($showcorrectness) {
  117. $state = $this->qa->get_state();
  118. if (!$state->is_active() || $state == question_state::$invalid) {
  119. return parent::get_state_string($showcorrectness);
  120. }
  121. return get_string('triesremaining', 'qbehaviour_interactive',
  122. $this->qa->get_last_behaviour_var('_triesleft'));
  123. }
  124. public function init_first_step(question_attempt_step $step, $variant) {
  125. parent::init_first_step($step, $variant);
  126. $step->set_behaviour_var('_triesleft', count($this->question->hints) + 1);
  127. }
  128. public function process_action(question_attempt_pending_step $pendingstep) {
  129. if ($pendingstep->has_behaviour_var('finish')) {
  130. return $this->process_finish($pendingstep);
  131. }
  132. if ($this->is_try_again_state()) {
  133. if ($pendingstep->has_behaviour_var('tryagain')) {
  134. return $this->process_try_again($pendingstep);
  135. } else {
  136. return question_attempt::DISCARD;
  137. }
  138. } else {
  139. if ($pendingstep->has_behaviour_var('comment')) {
  140. return $this->process_comment($pendingstep);
  141. } else if ($pendingstep->has_behaviour_var('submit')) {
  142. return $this->process_submit($pendingstep);
  143. } else {
  144. return $this->process_save($pendingstep);
  145. }
  146. }
  147. }
  148. public function summarise_action(question_attempt_step $step) {
  149. if ($step->has_behaviour_var('comment')) {
  150. return $this->summarise_manual_comment($step);
  151. } else if ($step->has_behaviour_var('finish')) {
  152. return $this->summarise_finish($step);
  153. } else if ($step->has_behaviour_var('tryagain')) {
  154. return get_string('tryagain', 'qbehaviour_interactive');
  155. } else if ($step->has_behaviour_var('submit')) {
  156. return $this->summarise_submit($step);
  157. } else {
  158. return $this->summarise_save($step);
  159. }
  160. }
  161. public function process_try_again(question_attempt_pending_step $pendingstep) {
  162. $pendingstep->set_state(question_state::$todo);
  163. return question_attempt::KEEP;
  164. }
  165. public function process_submit(question_attempt_pending_step $pendingstep) {
  166. if ($this->qa->get_state()->is_finished()) {
  167. return question_attempt::DISCARD;
  168. }
  169. if (!$this->is_complete_response($pendingstep)) {
  170. $pendingstep->set_state(question_state::$invalid);
  171. } else {
  172. $triesleft = $this->qa->get_last_behaviour_var('_triesleft');
  173. $response = $pendingstep->get_qt_data();
  174. list($fraction, $state) = $this->question->grade_response($response);
  175. if ($state == question_state::$gradedright || $triesleft == 1) {
  176. $pendingstep->set_state($state);
  177. $pendingstep->set_fraction($this->adjust_fraction($fraction, $pendingstep));
  178. } else {
  179. $pendingstep->set_behaviour_var('_triesleft', $triesleft - 1);
  180. $pendingstep->set_state(question_state::$todo);
  181. }
  182. $pendingstep->set_new_response_summary($this->question->summarise_response($response));
  183. }
  184. return question_attempt::KEEP;
  185. }
  186. protected function adjust_fraction($fraction, question_attempt_pending_step $pendingstep) {
  187. $totaltries = $this->qa->get_step(0)->get_behaviour_var('_triesleft');
  188. $triesleft = $this->qa->get_last_behaviour_var('_triesleft');
  189. $fraction -= ($totaltries - $triesleft) * $this->question->penalty;
  190. $fraction = max($fraction, 0);
  191. return $fraction;
  192. }
  193. public function process_finish(question_attempt_pending_step $pendingstep) {
  194. if ($this->qa->get_state()->is_finished()) {
  195. return question_attempt::DISCARD;
  196. }
  197. $response = $this->qa->get_last_qt_data();
  198. if (!$this->question->is_gradable_response($response)) {
  199. $pendingstep->set_state(question_state::$gaveup);
  200. } else {
  201. list($fraction, $state) = $this->question->grade_response($response);
  202. $pendingstep->set_fraction($this->adjust_fraction($fraction, $pendingstep));
  203. $pendingstep->set_state($state);
  204. }
  205. $pendingstep->set_new_response_summary($this->question->summarise_response($response));
  206. return question_attempt::KEEP;
  207. }
  208. public function process_save(question_attempt_pending_step $pendingstep) {
  209. $status = parent::process_save($pendingstep);
  210. if ($status == question_attempt::KEEP &&
  211. $pendingstep->get_state() == question_state::$complete) {
  212. $pendingstep->set_state(question_state::$todo);
  213. }
  214. return $status;
  215. }
  216. }