weekly release 3.10.4+
[moodle.git] / question / behaviour / adaptive / behaviour.php
CommitLineData
d1b7e03d 1<?php
d1b7e03d
TH
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
d1b7e03d
TH
17/**
18 * Question behaviour for the old adaptive mode.
19 *
aa9bdbe3
TH
20 * @package qbehaviour
21 * @subpackage adaptive
22 * @copyright 2009 The Open University
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
24 */
25
26
a17b297d
TH
27defined('MOODLE_INTERNAL') || die();
28
29
d1b7e03d
TH
30/**
31 * Question behaviour for adaptive mode.
32 *
33 * This is the old version of interactive mode.
34 *
f7970e3c
TH
35 * @copyright 2009 The Open University
36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d 37 */
99caa248 38class qbehaviour_adaptive extends question_behaviour_with_multiple_tries {
d1b7e03d
TH
39 const IS_ARCHETYPAL = true;
40
3d17cd3f
TH
41 public function is_compatible_question(question_definition $question) {
42 return $question instanceof question_automatically_gradable;
d1b7e03d
TH
43 }
44
45 public function get_expected_data() {
46 if ($this->qa->get_state()->is_active()) {
47 return array('submit' => PARAM_BOOL);
48 }
49 return parent::get_expected_data();
50 }
51
88ef9d0b
TH
52 public function get_state_string($showcorrectness) {
53 $laststep = $this->qa->get_last_step();
54 if ($laststep->has_behaviour_var('_try')) {
55 $state = question_state::graded_state_for_fraction(
56 $laststep->get_behaviour_var('_rawfraction'));
57 return $state->default_string(true);
58 }
59
60 $state = $this->qa->get_state();
61 if ($state == question_state::$todo) {
62 return get_string('notcomplete', 'qbehaviour_adaptive');
63 } else {
64 return parent::get_state_string($showcorrectness);
65 }
66 }
67
d1b7e03d
TH
68 public function get_right_answer_summary() {
69 return $this->question->get_right_answer_summary();
70 }
71
72 public function adjust_display_options(question_display_options $options) {
d56ab3b9
JMV
73 // Save some bits so we can put them back later.
74 $save = clone($options);
75
76 // Do the default thing.
d1b7e03d 77 parent::adjust_display_options($options);
d56ab3b9
JMV
78
79 // Then, if they have just Checked an answer, show them the applicable bits of feedback.
d1b7e03d
TH
80 if (!$this->qa->get_state()->is_finished() &&
81 $this->qa->get_last_behaviour_var('_try')) {
d56ab3b9
JMV
82 $options->feedback = $save->feedback;
83 $options->correctness = $save->correctness;
84 $options->numpartscorrect = $save->numpartscorrect;
85
d1b7e03d
TH
86 }
87 }
88
d1b7e03d
TH
89 public function process_action(question_attempt_pending_step $pendingstep) {
90 if ($pendingstep->has_behaviour_var('comment')) {
91 return $this->process_comment($pendingstep);
92 } else if ($pendingstep->has_behaviour_var('finish')) {
93 return $this->process_finish($pendingstep);
94 } else if ($pendingstep->has_behaviour_var('submit')) {
95 return $this->process_submit($pendingstep);
96 } else {
97 return $this->process_save($pendingstep);
98 }
99 }
100
101 public function summarise_action(question_attempt_step $step) {
102 if ($step->has_behaviour_var('comment')) {
103 return $this->summarise_manual_comment($step);
104 } else if ($step->has_behaviour_var('finish')) {
105 return $this->summarise_finish($step);
106 } else if ($step->has_behaviour_var('submit')) {
107 return $this->summarise_submit($step);
108 } else {
109 return $this->summarise_save($step);
110 }
111 }
112
113 public function process_save(question_attempt_pending_step $pendingstep) {
114 $status = parent::process_save($pendingstep);
115 $prevgrade = $this->qa->get_fraction();
116 if (!is_null($prevgrade)) {
117 $pendingstep->set_fraction($prevgrade);
118 }
119 $pendingstep->set_state(question_state::$todo);
120 return $status;
121 }
122
123 protected function adjusted_fraction($fraction, $prevtries) {
124 return $fraction - $this->question->penalty * $prevtries;
125 }
126
127 public function process_submit(question_attempt_pending_step $pendingstep) {
128 $status = $this->process_save($pendingstep);
129
130 $response = $pendingstep->get_qt_data();
096c3016 131 if (!$this->question->is_complete_response($response)) {
d1b7e03d
TH
132 $pendingstep->set_state(question_state::$invalid);
133 if ($this->qa->get_state() != question_state::$invalid) {
134 $status = question_attempt::KEEP;
135 }
136 return $status;
137 }
138
cc9fc649
HB
139 $prevstep = $this->qa->get_last_step_with_behaviour_var('_try');
140 $prevresponse = $prevstep->get_qt_data();
d1b7e03d
TH
141 $prevtries = $this->qa->get_last_behaviour_var('_try', 0);
142 $prevbest = $pendingstep->get_fraction();
143 if (is_null($prevbest)) {
144 $prevbest = 0;
145 }
146
cc9fc649
HB
147 if ($this->question->is_same_response($response, $prevresponse)) {
148 return question_attempt::DISCARD;
149 }
150
d1b7e03d
TH
151 list($fraction, $state) = $this->question->grade_response($response);
152
153 $pendingstep->set_fraction(max($prevbest, $this->adjusted_fraction($fraction, $prevtries)));
cc9fc649
HB
154 if ($prevstep->get_state() == question_state::$complete) {
155 $pendingstep->set_state(question_state::$complete);
156 } else if ($state == question_state::$gradedright) {
d1b7e03d
TH
157 $pendingstep->set_state(question_state::$complete);
158 } else {
159 $pendingstep->set_state(question_state::$todo);
160 }
161 $pendingstep->set_behaviour_var('_try', $prevtries + 1);
162 $pendingstep->set_behaviour_var('_rawfraction', $fraction);
163 $pendingstep->set_new_response_summary($this->question->summarise_response($response));
164
165 return question_attempt::KEEP;
166 }
167
168 public function process_finish(question_attempt_pending_step $pendingstep) {
169 if ($this->qa->get_state()->is_finished()) {
170 return question_attempt::DISCARD;
171 }
172
d1b7e03d 173 $prevtries = $this->qa->get_last_behaviour_var('_try', 0);
cc9fc649 174 $prevbest = $this->qa->get_fraction();
d1b7e03d
TH
175 if (is_null($prevbest)) {
176 $prevbest = 0;
177 }
178
cc9fc649
HB
179 $laststep = $this->qa->get_last_step();
180 $response = $laststep->get_qt_data();
181 if (!$this->question->is_gradable_response($response)) {
182 $state = question_state::$gaveup;
183 $fraction = 0;
184 } else {
d1b7e03d 185
cc9fc649
HB
186 if ($laststep->has_behaviour_var('_try')) {
187 // Last answer was graded, we want to regrade it. Otherwise the answer
188 // has changed, and we are grading a new try.
189 $prevtries -= 1;
190 }
191
192 list($fraction, $state) = $this->question->grade_response($response);
193
194 $pendingstep->set_behaviour_var('_try', $prevtries + 1);
195 $pendingstep->set_behaviour_var('_rawfraction', $fraction);
196 $pendingstep->set_new_response_summary($this->question->summarise_response($response));
197 }
d1b7e03d 198
d1b7e03d 199 $pendingstep->set_state($state);
cc9fc649 200 $pendingstep->set_fraction(max($prevbest, $this->adjusted_fraction($fraction, $prevtries)));
d1b7e03d
TH
201 return question_attempt::KEEP;
202 }
cc9fc649
HB
203
204 /**
205 * Got the most recently graded step. This is mainly intended for use by the
206 * renderer.
207 * @return question_attempt_step the most recently graded step.
208 */
209 public function get_graded_step() {
210 $step = $this->qa->get_last_step_with_behaviour_var('_try');
211 if ($step->has_behaviour_var('_try')) {
212 return $step;
213 } else {
214 return null;
215 }
216 }
217
218 /**
219 * Determine whether a question state represents an "improvable" result,
220 * that is, whether the user can still improve their score.
221 *
222 * @param question_state $state the question state.
223 * @return bool whether the state is improvable
224 */
225 public function is_state_improvable(question_state $state) {
226 return $state == question_state::$todo;
227 }
a3e6ad77
TH
228
229 /**
230 * @return qbehaviour_adaptive_mark_details the information about the current state-of-play, scoring-wise,
231 * for this adaptive attempt.
232 */
233 public function get_adaptive_marks() {
234
235 // Try to find the last graded step.
236 $gradedstep = $this->get_graded_step();
237 if (is_null($gradedstep) || $this->qa->get_max_mark() == 0) {
238 // No score yet.
239 return new qbehaviour_adaptive_mark_details(question_state::$todo);
240 }
241
242 // Work out the applicable state.
243 if ($this->qa->get_state()->is_commented()) {
244 $state = $this->qa->get_state();
245 } else {
246 $state = question_state::graded_state_for_fraction(
247 $gradedstep->get_behaviour_var('_rawfraction'));
248 }
249
250 // Prepare the grading details.
251 $details = $this->adaptive_mark_details_from_step($gradedstep, $state, $this->qa->get_max_mark(), $this->question->penalty);
252 $details->improvable = $this->is_state_improvable($this->qa->get_state());
253 return $details;
254 }
255
256 /**
257 * Actually populate the qbehaviour_adaptive_mark_details object.
258 * @param question_attempt_step $gradedstep the step that holds the relevant mark details.
259 * @param question_state $state the state corresponding to $gradedstep.
260 * @param unknown_type $maxmark the maximum mark for this question_attempt.
261 * @param unknown_type $penalty the penalty for this question, as a fraction.
262 */
263 protected function adaptive_mark_details_from_step(question_attempt_step $gradedstep,
264 question_state $state, $maxmark, $penalty) {
265
266 $details = new qbehaviour_adaptive_mark_details($state);
267 $details->maxmark = $maxmark;
268 $details->actualmark = $gradedstep->get_fraction() * $details->maxmark;
269 $details->rawmark = $gradedstep->get_behaviour_var('_rawfraction') * $details->maxmark;
270
271 $details->currentpenalty = $penalty * $details->maxmark;
272 $details->totalpenalty = $details->currentpenalty * $this->qa->get_last_behaviour_var('_try', 0);
273
274 $details->improvable = $this->is_state_improvable($gradedstep->get_state());
275
276 return $details;
277 }
278}
279
280
281/**
282 * This class encapsulates all the information about the current state-of-play
283 * scoring-wise. It is used to communicate between the beahviour and the renderer.
284 *
285 * @copyright 2012 The Open University
286 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
287 */
288class qbehaviour_adaptive_mark_details {
289 /** @var question_state the current state of the question. */
290 public $state;
291
292 /** @var float the maximum mark for this question. */
293 public $maxmark;
294
295 /** @var float the current mark for this question. */
296 public $actualmark;
297
298 /** @var float the raw mark for this question before penalties were applied. */
299 public $rawmark;
300
301 /** @var float the the amount of additional penalty this attempt attracted. */
302 public $currentpenalty;
303
304 /** @var float the total that will apply to future attempts. */
305 public $totalpenalty;
306
307 /** @var bool whether it is possible for this mark to be improved in future. */
308 public $improvable;
309
310 /**
311 * Constructor.
312 * @param question_state $state
313 */
314 public function __construct($state, $maxmark = null, $actualmark = null, $rawmark = null,
315 $currentpenalty = null, $totalpenalty = null, $improvable = null) {
316 $this->state = $state;
317 $this->maxmark = $maxmark;
318 $this->actualmark = $actualmark;
319 $this->rawmark = $rawmark;
320 $this->currentpenalty = $currentpenalty;
321 $this->totalpenalty = $totalpenalty;
322 $this->improvable = $improvable;
323 }
324
325 /**
326 * Get the marks, formatted to a certain number of decimal places, in the
327 * form required by calls like get_string('gradingdetails', 'qbehaviour_adaptive', $a).
328 * @param int $markdp the number of decimal places required.
329 * @return array ready to substitute into language strings.
330 */
331 public function get_formatted_marks($markdp) {
332 return array(
333 'max' => format_float($this->maxmark, $markdp),
334 'cur' => format_float($this->actualmark, $markdp),
335 'raw' => format_float($this->rawmark, $markdp),
336 'penalty' => format_float($this->currentpenalty, $markdp),
337 'totalpenalty' => format_float($this->totalpenalty, $markdp),
338 );
339 }
d1b7e03d 340}