Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
/**
18
 * This file contains classes for handling the different question behaviours
19
 * during upgrade.
20
 *
21
 * @package    moodlecore
22
 * @subpackage questionengine
23
 * @copyright  2010 The Open University
24
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25
 */
26
 
27
 
28
defined('MOODLE_INTERNAL') || die();
29
 
30
 
31
/**
32
 * Base class for managing the upgrade of a question using a particular behaviour.
33
 *
34
 * This class takes as input:
35
 * 1. Various backgroud data like $quiz, $attempt and $question.
36
 * 2. The data about the question session to upgrade $qsession and $qstates.
37
 * Working through that data, it builds up
38
 * 3. The equivalent new data $qa. This has roughly the same data as a
39
 * question_attempt object belonging to the new question engine would have, but
40
 * $this->qa is built up from stdClass objects.
41
 *
42
 * @copyright  2010 The Open University
43
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
44
 */
45
abstract class question_behaviour_attempt_updater {
46
    /** @var question_qtype_attempt_updater */
47
    protected $qtypeupdater;
48
    /** @var question_engine_assumption_logger */
49
    protected $logger;
50
    /** @var question_engine_attempt_upgrader */
51
    protected $qeupdater;
52
 
53
    /**
54
     * @var object this is the data for the upgraded questions attempt that
55
     * we are building.
56
     */
57
    public $qa;
58
 
59
    /** @var object the quiz settings. */
60
    protected $quiz;
61
    /** @var object the quiz attempt data. */
62
    protected $attempt;
63
    /** @var object the question definition data. */
64
    protected $question;
65
    /** @var object the question session to be upgraded. */
66
    protected $qsession;
67
    /** @var array the question states for the session to be upgraded. */
68
    protected $qstates;
69
    /** @var stdClass */
70
    protected $startstate;
71
 
72
    /**
73
     * @var int counts the question_steps as they are converted to
74
     * question_attempt_steps.
75
     */
76
    protected $sequencenumber;
77
    /** @var object pointer to the state that has already finished this attempt. */
78
    protected $finishstate;
79
 
80
    public function __construct($quiz, $attempt, $question, $qsession, $qstates, $logger, $qeupdater) {
81
        $this->quiz = $quiz;
82
        $this->attempt = $attempt;
83
        $this->question = $question;
84
        $this->qsession = $qsession;
85
        $this->qstates = $qstates;
86
        $this->logger = $logger;
87
        $this->qeupdater = $qeupdater;
88
    }
89
 
90
    public function discard() {
91
        // Help the garbage collector, which seems to be struggling.
92
        $this->quiz = null;
93
        $this->attempt = null;
94
        $this->question = null;
95
        $this->qsession = null;
96
        $this->qstates = null;
97
        $this->qa = null;
98
        $this->qtypeupdater->discard();
99
        $this->qtypeupdater = null;
100
        $this->logger = null;
101
        $this->qeupdater = null;
102
    }
103
 
104
    abstract protected function behaviour_name();
105
 
106
    public function get_converted_qa() {
107
        $this->initialise_qa();
108
        $this->convert_steps();
109
        return $this->qa;
110
    }
111
 
112
    protected function create_missing_first_step() {
113
        $step = new stdClass();
114
        $step->state = 'todo';
115
        $step->data = array();
116
        $step->fraction = null;
117
        $step->timecreated = $this->attempt->timestart ? $this->attempt->timestart : time();
118
        $step->userid = $this->attempt->userid;
119
        $this->qtypeupdater->supply_missing_first_step_data($step->data);
120
        return $step;
121
    }
122
 
123
    public function supply_missing_qa() {
124
        $this->initialise_qa();
125
        $this->qa->timemodified = $this->attempt->timestart;
126
        $this->sequencenumber = 0;
127
        $this->add_step($this->create_missing_first_step());
128
        return $this->qa;
129
    }
130
 
131
    protected function initialise_qa() {
132
        $this->qtypeupdater = $this->make_qtype_updater();
133
 
134
        $qa = new stdClass();
135
        $qa->questionid = $this->question->id;
136
        $qa->variant = 1;
137
        $qa->behaviour = $this->behaviour_name();
138
        $qa->questionsummary = $this->qtypeupdater->question_summary($this->question);
139
        $qa->rightanswer = $this->qtypeupdater->right_answer($this->question);
140
        $qa->maxmark = $this->question->maxmark;
141
        $qa->minfraction = 0;
142
        $qa->maxfraction = 1;
143
        $qa->flagged = 0;
144
        $qa->responsesummary = '';
145
        $qa->timemodified = 0;
146
        $qa->steps = array();
147
 
148
        $this->qa = $qa;
149
    }
150
 
151
    protected function convert_steps() {
152
        $this->finishstate = null;
153
        $this->startstate = null;
154
        $this->sequencenumber = 0;
155
        foreach ($this->qstates as $state) {
156
            $this->process_state($state);
157
        }
158
        $this->finish_up();
159
    }
160
 
161
    protected function process_state($state) {
162
        $step = $this->make_step($state);
163
        $method = 'process' . $state->event;
164
        $this->$method($step, $state);
165
    }
166
 
167
    protected function finish_up() {
168
    }
169
 
170
    protected function add_step($step) {
171
        $step->sequencenumber = $this->sequencenumber;
172
        $this->qa->steps[] = $step;
173
        $this->sequencenumber++;
174
    }
175
 
176
    protected function discard_last_state() {
177
        array_pop($this->qa->steps);
178
        $this->sequencenumber--;
179
    }
180
 
181
    protected function unexpected_event($state) {
182
        throw new coding_exception("Unexpected event {$state->event} in state {$state->id} in question session {$this->qsession->id}.");
183
    }
184
 
185
    protected function process0($step, $state) {
186
        if ($this->startstate) {
187
            if ($state->answer == reset($this->qstates)->answer) {
188
                return;
189
            } else if ($this->quiz->attemptonlast && $this->sequencenumber == 1) {
190
                // There was a bug in attemptonlast in the past, which meant that
191
                // it created two inconsistent open states, with the second taking
192
                // priority. Simulate that be discarding the first open state, then
193
                // continuing.
194
                $this->logger->log_assumption("Ignoring bogus state in attempt at question {$state->question}");
195
                $this->sequencenumber = 0;
196
                $this->qa->steps = array();
197
            } else if ($this->qtypeupdater->is_blank_answer($state)) {
198
                $this->logger->log_assumption("Ignoring second start state with blank answer in attempt at question {$state->question}");
199
                return;
200
            } else {
201
                throw new coding_exception("Two inconsistent open states for question session {$this->qsession->id}.");
202
            }
203
        }
204
        $step->state = 'todo';
205
        $this->startstate = $state;
206
        $this->add_step($step);
207
    }
208
 
209
    protected function process1($step, $state) {
210
        $this->unexpected_event($state);
211
    }
212
 
213
    protected function process2($step, $state) {
214
        if ($this->qtypeupdater->was_answered($state)) {
215
            $step->state = 'complete';
216
        } else {
217
            $step->state = 'todo';
218
        }
219
        $this->add_step($step);
220
    }
221
 
222
    protected function process3($step, $state) {
223
        return $this->process6($step, $state);
224
    }
225
 
226
    protected function process4($step, $state) {
227
        $this->unexpected_event($state);
228
    }
229
 
230
    protected function process5($step, $state) {
231
        $this->unexpected_event($state);
232
    }
233
 
234
    abstract protected function process6($step, $state);
235
    abstract protected function process7($step, $state);
236
 
237
    protected function process8($step, $state) {
238
        return $this->process6($step, $state);
239
    }
240
 
241
    protected function process9($step, $state) {
242
        if (!$this->finishstate) {
243
            $submitstate = clone($state);
244
            $submitstate->event = 8;
245
            $submitstate->grade = 0;
246
            $this->process_state($submitstate);
247
        }
248
 
249
        $step->data['-comment'] = $this->qsession->manualcomment;
250
        if ($this->question->maxmark > 0) {
251
            $step->fraction = $state->grade / $this->question->maxmark;
252
            $step->state = $this->manual_graded_state_for_fraction($step->fraction);
253
            $step->data['-mark'] = $state->grade;
254
            $step->data['-maxmark'] = $this->question->maxmark;
255
        } else {
256
            $step->state = 'manfinished';
257
        }
258
        unset($step->data['answer']);
259
        $step->userid = null;
260
        $this->add_step($step);
261
    }
262
 
263
    /**
264
     * @param object $question a question definition
265
     * @return qtype_updater
266
     */
267
    protected function make_qtype_updater() {
268
        global $CFG;
269
 
270
        if ($this->question->qtype == 'deleted') {
271
            return new question_deleted_question_attempt_updater(
272
                    $this, $this->question, $this->logger, $this->qeupdater);
273
        }
274
 
275
        $path = $CFG->dirroot . '/question/type/' . $this->question->qtype . '/db/upgradelib.php';
276
        if (!is_readable($path)) {
277
            throw new coding_exception("Question type {$this->question->qtype}
278
                    is missing important code (the file {$path})
279
                    required to run the upgrade to the new question engine.");
280
        }
281
        include_once($path);
282
        $class = 'qtype_' . $this->question->qtype . '_qe2_attempt_updater';
283
        if (!class_exists($class)) {
284
            throw new coding_exception("Question type {$this->question->qtype}
285
                    is missing important code (the class {$class})
286
                    required to run the upgrade to the new question engine.");
287
        }
288
        return new $class($this, $this->question, $this->logger, $this->qeupdater);
289
    }
290
 
291
    public function to_text($html) {
292
        return trim(html_to_text($html, 0, false));
293
    }
294
 
295
    protected function graded_state_for_fraction($fraction) {
296
        if ($fraction < 0.000001) {
297
            return 'gradedwrong';
298
        } else if ($fraction > 0.999999) {
299
            return 'gradedright';
300
        } else {
301
            return 'gradedpartial';
302
        }
303
    }
304
 
305
    protected function manual_graded_state_for_fraction($fraction) {
306
        if ($fraction < 0.000001) {
307
            return 'mangrwrong';
308
        } else if ($fraction > 0.999999) {
309
            return 'mangrright';
310
        } else {
311
            return 'mangrpartial';
312
        }
313
    }
314
 
315
    protected function make_step($state){
316
        $step = new stdClass();
317
        $step->data = array();
318
 
319
        if ($state->event == 0 || $this->sequencenumber == 0) {
320
            $this->qtypeupdater->set_first_step_data_elements($state, $step->data);
321
        } else {
322
            $this->qtypeupdater->set_data_elements_for_step($state, $step->data);
323
        }
324
 
325
        $step->fraction = null;
326
        $step->timecreated = $state->timestamp ? $state->timestamp : time();
327
        $step->userid = $this->attempt->userid;
328
 
329
        $summary = $this->qtypeupdater->response_summary($state);
330
        if (!is_null($summary)) {
331
            $this->qa->responsesummary = $summary;
332
        }
333
        $this->qa->timemodified = max($this->qa->timemodified, $state->timestamp);
334
 
335
        return $step;
336
    }
337
}
338
 
339
 
340
class qbehaviour_deferredfeedback_converter extends question_behaviour_attempt_updater {
341
 
342
    protected function behaviour_name() {
343
        return 'deferredfeedback';
344
    }
345
 
346
    protected function process6($step, $state) {
347
        if (!$this->startstate) {
348
            $this->logger->log_assumption("Ignoring bogus submit before open in attempt at question {$state->question}");
349
            // WTF, but this has happened a few times in our DB. It seems it is safe to ignore.
350
            return;
351
        }
352
 
353
        if ($this->finishstate) {
354
            if ($this->finishstate->answer != $state->answer ||
355
                    $this->finishstate->grade != $state->grade ||
356
                    $this->finishstate->raw_grade != $state->raw_grade ||
357
                    $this->finishstate->penalty != $state->penalty) {
358
                $this->logger->log_assumption("Two inconsistent finish states found for question session {$this->qsession->id} in attempt at question {$state->question} keeping the later one.");
359
                $this->discard_last_state();
360
            } else {
361
                $this->logger->log_assumption("Ignoring extra finish states in attempt at question {$state->question}");
362
                return;
363
            }
364
        }
365
 
366
        if ($this->question->maxmark > 0) {
367
            $step->fraction = $state->grade / $this->question->maxmark;
368
            $step->state = $this->graded_state_for_fraction($step->fraction);
369
        } else {
370
            $step->state = 'finished';
371
        }
372
        $step->data['-finish'] = '1';
373
        $this->finishstate = $state;
374
        $this->add_step($step);
375
    }
376
 
377
    protected function process7($step, $state) {
378
        $this->unexpected_event($state);
379
    }
380
}
381
 
382
 
383
class qbehaviour_manualgraded_converter extends question_behaviour_attempt_updater {
384
    protected function behaviour_name() {
385
        return 'manualgraded';
386
    }
387
 
388
    protected function process6($step, $state) {
389
        $step->state = 'needsgrading';
390
        if (!$this->finishstate) {
391
            $step->data['-finish'] = '1';
392
            $this->finishstate = $state;
393
        }
394
        $this->add_step($step);
395
    }
396
 
397
    protected function process7($step, $state) {
398
        return $this->process2($step, $state);
399
    }
400
}
401
 
402
 
403
class qbehaviour_informationitem_converter extends question_behaviour_attempt_updater {
404
 
405
    protected function behaviour_name() {
406
        return 'informationitem';
407
    }
408
 
409
    protected function process0($step, $state) {
410
        if ($this->startstate) {
411
            return;
412
        }
413
        $step->state = 'todo';
414
        $this->startstate = $state;
415
        $this->add_step($step);
416
    }
417
 
418
    protected function process2($step, $state) {
419
        $this->unexpected_event($state);
420
    }
421
 
422
    protected function process3($step, $state) {
423
        $this->unexpected_event($state);
424
    }
425
 
426
    protected function process6($step, $state) {
427
        if ($this->finishstate) {
428
            return;
429
        }
430
 
431
        $step->state = 'finished';
432
        $step->data['-finish'] = '1';
433
        $this->finishstate = $state;
434
        $this->add_step($step);
435
    }
436
 
437
    protected function process7($step, $state) {
438
        return $this->process6($step, $state);
439
    }
440
 
441
    protected function process8($step, $state) {
442
        return $this->process6($step, $state);
443
    }
444
}
445
 
446
 
447
class qbehaviour_adaptive_converter extends question_behaviour_attempt_updater {
448
    protected $try;
449
    protected $laststepwasatry = false;
450
    protected $finished = false;
451
    protected $bestrawgrade = 0;
452
 
453
    protected function behaviour_name() {
454
        return 'adaptive';
455
    }
456
 
457
    protected function finish_up() {
458
        parent::finish_up();
459
        if ($this->finishstate || !$this->attempt->timefinish) {
460
            return;
461
        }
462
 
463
        $state = end($this->qstates);
464
        $step = $this->make_step($state);
465
        $this->process6($step, $state);
466
    }
467
 
468
    protected function process0($step, $state) {
469
        $this->try = 1;
470
        $this->laststepwasatry = false;
471
        parent::process0($step, $state);
472
    }
473
 
474
    protected function process2($step, $state) {
475
        if ($this->finishstate) {
476
            $this->logger->log_assumption("Ignoring bogus save after submit in an " .
477
                    "adaptive attempt at question {$state->question} " .
478
                    "(question session {$this->qsession->id})");
479
            return;
480
        }
481
 
482
        if ($this->question->maxmark > 0) {
483
            $step->fraction = $state->grade / $this->question->maxmark;
484
        }
485
 
486
        $this->laststepwasatry = false;
487
        parent::process2($step, $state);
488
    }
489
 
490
    protected function process3($step, $state) {
491
        if ($this->question->maxmark > 0) {
492
            $step->fraction = $state->grade / $this->question->maxmark;
493
            if ($this->graded_state_for_fraction($step->fraction) == 'gradedright') {
494
                $step->state = 'complete';
495
            } else {
496
                $step->state = 'todo';
497
            }
498
        } else {
499
            $step->state = 'complete';
500
        }
501
 
502
        $this->bestrawgrade = max($state->raw_grade, $this->bestrawgrade);
503
 
504
        $step->data['-_try'] = $this->try;
505
        $this->try += 1;
506
        $this->laststepwasatry = true;
507
        if ($this->question->maxmark > 0) {
508
            $step->data['-_rawfraction'] = $state->raw_grade / $this->question->maxmark;
509
        } else {
510
            $step->data['-_rawfraction'] = 0;
511
        }
512
        $step->data['-submit'] = 1;
513
 
514
        $this->add_step($step);
515
    }
516
 
517
    protected function process6($step, $state) {
518
        if ($this->finishstate) {
519
            if (!$this->qtypeupdater->compare_answers($this->finishstate->answer, $state->answer) ||
520
                    $this->finishstate->grade != $state->grade ||
521
                    $this->finishstate->raw_grade != $state->raw_grade ||
522
                    $this->finishstate->penalty != $state->penalty) {
523
                throw new coding_exception("Two inconsistent finish states found for question session {$this->qsession->id}.");
524
            } else {
525
                $this->logger->log_assumption("Ignoring extra finish states in attempt at question {$state->question}");
526
                return;
527
            }
528
        }
529
 
530
        $this->bestrawgrade = max($state->raw_grade, $this->bestrawgrade);
531
 
532
        if ($this->question->maxmark > 0) {
533
            $step->fraction = $state->grade / $this->question->maxmark;
534
            $step->state = $this->graded_state_for_fraction(
535
                    $this->bestrawgrade / $this->question->maxmark);
536
        } else {
537
            $step->state = 'finished';
538
        }
539
 
540
        $step->data['-finish'] = 1;
541
        if ($this->laststepwasatry) {
542
            $this->try -= 1;
543
        }
544
        $step->data['-_try'] = $this->try;
545
        if ($this->question->maxmark > 0) {
546
            $step->data['-_rawfraction'] = $state->raw_grade / $this->question->maxmark;
547
        } else {
548
            $step->data['-_rawfraction'] = 0;
549
        }
550
 
551
        $this->finishstate = $state;
552
        $this->add_step($step);
553
    }
554
 
555
    protected function process7($step, $state) {
556
        $this->unexpected_event($state);
557
    }
558
}
559
 
560
 
561
class qbehaviour_adaptivenopenalty_converter extends qbehaviour_adaptive_converter {
562
    protected function behaviour_name() {
563
        return 'adaptivenopenalty';
564
    }
565
}