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