/
behaviour.php
279 lines (242 loc) · 12.6 KB
/
behaviour.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
<?php
// This file is part of POAS question and related behaviours - https://code.google.com/p/oasychev-moodle-plugins/
//
// POAS question is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// POAS question is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
defined('MOODLE_INTERNAL') || die();
/**
* Question behaviour for question with hints in adaptive mode.
*
* Behaviour variables:
* _try - number of submissions (inherited from adaptive)
* _rawfraction - fraction for the step without penalties (inherited from adaptive)
* _hashint - there was hint requested in the step
* _resp_hintbtns, _nonresp_hintbtns - variables are set if buttons for response-based and non-response based hints should be rendered in the step
* _render_<hintname> - true if hint with hintname should be rendered when rendering question next time
* _penalty - penalty added in this state (used for rendering and summarising mainly)
* _totalpenalties - sum of all penalties already done
*
* Behaviour controls:
* submit - submit answer to grading (inherited from adaptive)
* <hintname>btn - buttons to get hint <hintname>
*
* @copyright 2011 Oleg Sychev Volgograd State Technical University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once($CFG->dirroot . '/question/behaviour/adaptive/behaviour.php');
class qbehaviour_adaptivehints extends qbehaviour_adaptive implements qtype_poasquestion\behaviour_with_hints {
const IS_ARCHETYPAL = false;
public function is_compatible_question(question_definition $question) {
return ($question instanceof question_automatically_gradable) && ($question instanceof qtype_poasquestion\question_with_hints);
}
public function get_expected_data() {
$expected = parent::get_expected_data();
$step = $this->qa->get_last_step();
if ($this->qa->get_state()->is_active()) {// Returning an array of hint buttons.
foreach ($this->question->hints_available_for_student($step->get_qt_data()) as $hintkey) {
$hintkey = $this->adjust_hintkey($hintkey);
$expected[$hintkey.'btn'] = PARAM_BOOL;
}
}
return $expected;
}
public function adjust_display_options(question_display_options $options) {
parent::adjust_display_options($options);// There seems to nothing to be done until question_display_options will be passed to specific_feedback function of question renderer.
}
/**
* Adjust hintkey, adding number for sequential multiple instance hints.
*
* Passed hintkey should ends with # character to be appended with number.
*/
public function adjust_hintkey($hintkey) {
if (substr($hintkey, -1) == '#') {
$i = 0;
while ($this->qa->get_last_behaviour_var('_render_' . $hintkey . $i) !== null) {
$i++;
}
$hintkey = $hintkey . $i;
}
return $hintkey;
}
/**
* Adjust hints array, replacing every hintkey that ends with # with a whole
* bunch of numbers up to max used in this attempt.
*/
public function adjust_hints($hints) {
$result = array();
foreach ($hints as $hintkey) {
if (substr($hintkey, -1) == '#') {
$adjustedkey = $this->adjust_hintkey($hintkey);
$maxnumber = substr($adjustedkey, strpos($adjustedkey, '#') + 1);
for ($i = 0; $i <= $maxnumber; $i++) {
$key = $hintkey . $i;
$result[] = $key;
}
} else {
$result[] = $hintkey;
}
}
return $result;
}
// Summarise functions.
public function summarise_action(question_attempt_step $step) {
// Summarise hint action.
foreach ($this->question->hints_available_for_student($step->get_qt_data()) as $hintkey) {
$hintkey = $this->adjust_hintkey($hintkey);
if ($step->has_behaviour_var($hintkey.'btn')) {
return $this->summarise_hint($step, $hintkey);
}
}
return parent::summarise_action($step);
}
public function summarise_hint(question_attempt_step $step, $hintkey) {
$response = $step->get_qt_data();
$hintobj = $this->question->hint_object($hintkey, $step->get_qt_data());
$hintdescription = $hintobj->hint_description();
$a = new stdClass();
$a->hint = $hintdescription;
$a->response = $this->question->summarise_response($response);
$a->penalty = $hintobj->penalty_for_specific_hint($response);
return get_string('hintused', 'qbehaviour_adaptivehints', $a);
}
// We should init first step to show non-response based hint buttons.
public function init_first_step(question_attempt_step $step, $variant) {
parent::init_first_step($step, $variant);
$step->set_behaviour_var('_nonresp_hintbtns', true);
}
// Process functions.
public function process_action(question_attempt_pending_step $pendingstep) {
$result = null;
// Process hint button press.
foreach ($this->question->hints_available_for_student($pendingstep->get_qt_data()) as $hintkey) {
$hintkey = $this->adjust_hintkey($hintkey);
if ($pendingstep->has_behaviour_var($hintkey.'btn')) {
$result = $this->process_hint($pendingstep, $hintkey);
}
}
// Proces all actions.
if ($result === null) {
$result = parent::process_action($pendingstep);
}
// Compute variables to show question it should render it's hint buttons.
if (!$this->qa->get_state()->is_finished()) {
$pendingstep->set_behaviour_var('_nonresp_hintbtns', true);
$response = $pendingstep->get_qt_data();
if ($this->question->is_complete_response($response)) {
$pendingstep->set_behaviour_var('_resp_hintbtns', true);
}
}
return $result;
}
public function process_hint(question_attempt_pending_step $pendingstep, $hintkey) {
$status = $this->process_save($pendingstep);
$response = $pendingstep->get_qt_data();
$hintobj = $this->question->hint_object($hintkey, $response);
if (!$hintobj->hint_available($response)) {// Couldn't compute hint for such response.
return question_attempt::DISCARD;
}
// Process data from last graded state (e.g. submit).
$prevstep = $this->get_graded_step();
if (!is_null($prevstep)) {// TODO - deal with situation where hint requested for response that is correct already.
if ($prevstep->get_state() == question_state::$complete) {
$pendingstep->set_state(question_state::$complete);
} else {
$pendingstep->set_state(question_state::$todo);
}
$pendingstep->set_behaviour_var('_rawfraction', $prevstep->get_behaviour_var('_rawfraction'));
} else {// Hint requested before submitting anything.
$pendingstep->set_fraction(0);
$pendingstep->set_behaviour_var('_rawfraction', 0);
$pendingstep->set_state(question_state::$todo);
}
// Set hint variables.
$pendingstep->set_behaviour_var('_hashint',true);
$prevtotal = $this->qa->get_last_behaviour_var('_totalpenalties', 0);
$penalty = $hintobj->penalty_for_specific_hint($response);
$pendingstep->set_behaviour_var('_penalty', $penalty);
$newtotal = $prevtotal + $penalty;
$pendingstep->set_behaviour_var('_totalpenalties', $newtotal);
$pendingstep->set_behaviour_var('_render_'.$hintkey, true);
// Copy previous _render_hintxxx variables if previous state is hint state and response is same.
$prevhintstep = $this->qa->get_last_step();
if ($prevhintstep->has_behaviour_var('_hashint') && $this->is_same_response($pendingstep)) {
$prevhints = $this->adjust_hints($this->question->hints_available_for_student($pendingstep->get_qt_data()));
foreach ($prevhints as $prevhintkey) {
if ($prevhintstep->has_behaviour_var('_render_'.$prevhintkey)) {
$pendingstep->set_behaviour_var('_render_'.$prevhintkey, true);
}
}
}
$prevbest = $pendingstep->get_fraction();
if (is_null($prevbest)) {
$prevbest = 0;
}
// The fraction = rawfraction - totalpenalties (already collected).
$pendingstep->set_fraction(max($prevbest, $this->adjusted_fraction($pendingstep->get_behaviour_var('_rawfraction'), $newtotal)));
$pendingstep->set_new_response_summary($this->question->summarise_response($response));
return question_attempt::KEEP;
}
// Overload process_submit to recalculate fraction and add _totalpenalties.
public function process_submit(question_attempt_pending_step $pendingstep) {
// Must find out prevbest before parent function get in it's fraction.
$prevbest = $pendingstep->get_fraction();
if (is_null($prevbest)) {
$prevbest = 0;
}
$status = parent::process_submit($pendingstep);
$response = $pendingstep->get_qt_data();
if ($this->question->is_gradable_response($response) && $status == question_attempt::KEEP) {// State was graded.
$prevtotal = $this->qa->get_last_behaviour_var('_totalpenalties', 0);
// The fraction = rawfraction - totalpenalties (already collected).
$pendingstep->set_fraction(max($prevbest, $this->adjusted_fraction($pendingstep->get_behaviour_var('_rawfraction'), $prevtotal)));
$pendingstep->set_behaviour_var('_totalpenalties', $prevtotal + $this->question->penalty);// For submit penalty is added after fraction is calculated.
$pendingstep->set_behaviour_var('_penalty', $this->question->penalty);
}
return $status;
}
// Overload process_finish to recalculate fraction and add _totalpenalties.
public function process_finish(question_attempt_pending_step $pendingstep) {
// Must find out prevbest before parent function get in it's fraction.
$prevbest = $this->qa->get_fraction();
if (is_null($prevbest)) {
$prevbest = 0;
}
$status = parent::process_finish($pendingstep);
if ($pendingstep->get_state() != question_state::$gaveup) {// State was graded.
$laststep = $this->qa->get_last_step();
$total = $this->qa->get_last_behaviour_var('_totalpenalties', 0);
if (!$laststep->has_behaviour_var('_try')) {// Submitting ( not previous grading) resulted in finishing, so need to apply penalty.
$total += $this->question->penalty;
$pendingstep->set_behaviour_var('_penalty', $this->question->penalty);
}
$pendingstep->set_behaviour_var('_totalpenalties', $total);
// Must substract by one submission penalty less , to account for one lawful submission.
$pendingstep->set_fraction(max($prevbest, $this->adjusted_fraction($pendingstep->get_behaviour_var('_rawfraction'), $total - $this->question->penalty)));
}
return question_attempt::KEEP;
}
// Overloading this to have easy 'no penalties' adaptive version.
protected function adjusted_fraction($fraction, $penalty) {
return $fraction - $penalty;
}
// Overload get_graded_step since hinting changes grade too, we need to use last one with grade.
public function get_graded_step() {
// Variable _totalpenalties is set only when grading, i.e. on hinting, finishing and submitting.
$step = $this->qa->get_last_step_with_behaviour_var('_totalpenalties');
if ($step->has_behaviour_var('_totalpenalties')) {
return $step;
} else {
return null;
}
}
}