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
 * Quiz statistics report, table for showing statistics of each question in the quiz.
19
 *
20
 * @package   quiz_statistics
21
 * @copyright 2008 Jamie Pratt
22
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
defined('MOODLE_INTERNAL') || die();
26
 
27
require_once($CFG->libdir.'/tablelib.php');
28
 
29
use \core_question\statistics\questions\calculated_question_summary;
30
 
31
/**
32
 * This table has one row for each question in the quiz, with sub-rows when
33
 * random questions and variants appear.
34
 *
35
 * There are columns for the various item and position statistics.
36
 *
37
 * @copyright 2008 Jamie Pratt
38
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39
 */
40
class quiz_statistics_table extends flexible_table {
41
    /** @var stdClass the quiz settings. */
42
    protected $quiz;
43
 
44
    /** @var integer the quiz course_module id. */
45
    protected $cmid;
46
 
47
    /**
48
     * Constructor.
49
     */
50
    public function __construct() {
51
        parent::__construct('mod-quiz-report-statistics-report');
52
    }
53
 
54
    /**
55
     * Set up the columns and headers and other properties of the table and then
56
     * call flexible_table::setup() method.
57
     *
58
     * @param stdClass $quiz the quiz settings
59
     * @param int $cmid the quiz course_module id
60
     * @param moodle_url $reporturl the URL to redisplay this report.
61
     * @param int $s number of attempts included in the statistics.
62
     */
63
    public function statistics_setup($quiz, $cmid, $reporturl, $s) {
64
        $this->quiz = $quiz;
65
        $this->cmid = $cmid;
66
 
67
        // Define the table columns.
68
        $columns = [];
69
        $headers = [];
70
 
71
        $columns[] = 'number';
72
        $headers[] = get_string('questionnumber', 'quiz_statistics');
73
 
74
        if (!$this->is_downloading()) {
75
            $columns[] = 'icon';
76
            $headers[] = '';
77
            $columns[] = 'actions';
78
            $headers[] = '';
79
        } else {
80
            $columns[] = 'qtype';
81
            $headers[] = get_string('questiontype', 'quiz_statistics');
82
        }
83
 
84
        $columns[] = 'name';
85
        $headers[] = get_string('questionname', 'quiz');
86
 
87
        $columns[] = 's';
88
        $headers[] = get_string('attempts', 'quiz_statistics');
89
 
90
        if ($s > 1) {
91
            $columns[] = 'facility';
92
            $headers[] = get_string('facility', 'quiz_statistics');
93
 
94
            $columns[] = 'sd';
95
            $headers[] = get_string('standarddeviationq', 'quiz_statistics');
96
        }
97
 
98
        $columns[] = 'random_guess_score';
99
        $headers[] = get_string('random_guess_score', 'quiz_statistics');
100
 
101
        $columns[] = 'intended_weight';
102
        $headers[] = get_string('intended_weight', 'quiz_statistics');
103
 
104
        $columns[] = 'effective_weight';
105
        $headers[] = get_string('effective_weight', 'quiz_statistics');
106
 
107
        $columns[] = 'discrimination_index';
108
        $headers[] = get_string('discrimination_index', 'quiz_statistics');
109
 
110
        $columns[] = 'discriminative_efficiency';
111
        $headers[] = get_string('discriminative_efficiency', 'quiz_statistics');
112
 
113
        $this->define_columns($columns);
114
        $this->define_headers($headers);
115
        $this->sortable(false);
116
 
117
        $this->column_class('s', 'numcol');
118
        $this->column_class('facility', 'numcol');
119
        $this->column_class('sd', 'numcol');
120
        $this->column_class('random_guess_score', 'numcol');
121
        $this->column_class('intended_weight', 'numcol');
122
        $this->column_class('effective_weight', 'numcol');
123
        $this->column_class('discrimination_index', 'numcol');
124
        $this->column_class('discriminative_efficiency', 'numcol');
125
 
126
        // Set up the table.
127
        $this->define_baseurl($reporturl->out());
128
 
129
        $this->collapsible(true);
130
 
131
        $this->set_attribute('id', 'questionstatistics');
132
        $this->set_attribute('class', 'generaltable generalbox boxaligncenter');
133
 
134
        parent::setup();
135
    }
136
 
137
    /**
138
     * Open a div tag to wrap statistics table.
139
     */
140
    public function wrap_html_start() {
141
        // Horrible Moodle 2.0 wide-content work-around.
142
        if (!$this->is_downloading()) {
143
            echo html_writer::start_tag('div', ['id' => 'tablecontainer',
144
                    'class' => 'statistics-tablecontainer']);
145
        }
146
    }
147
 
148
    /**
149
     * Close a statistics table div.
150
     */
151
    public function wrap_html_finish() {
152
        if (!$this->is_downloading()) {
153
            echo html_writer::end_tag('div');
154
        }
155
    }
156
 
157
    /**
158
     * The question number.
159
     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
160
     * @return string contents of this table cell.
161
     */
162
    protected function col_number($questionstat) {
163
        if ($this->is_calculated_question_summary($questionstat)) {
164
            return '';
165
        }
166
        if (!isset($questionstat->question->number)) {
167
            return '';
168
        }
169
        $number = $questionstat->question->number;
170
 
171
        if (isset($questionstat->subqdisplayorder)) {
172
            $number = $number . '.'.$questionstat->subqdisplayorder;
173
        }
174
 
175
        if ($questionstat->question->qtype != 'random' && !is_null($questionstat->variant)) {
176
            $number = $number . '.'.$questionstat->variant;
177
        }
178
 
179
        return $number;
180
    }
181
 
182
    /**
183
     * The question type icon.
184
     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
185
     * @return string contents of this table cell.
186
     */
187
    protected function col_icon($questionstat) {
188
        if ($this->is_calculated_question_summary($questionstat)) {
189
            return '';
190
        } else {
191
            $questionobject = $questionstat->question;
192
            return print_question_icon($questionobject);
193
        }
194
    }
195
 
196
    /**
197
     * Actions that can be performed on the question by this user (e.g. edit or preview).
198
     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
199
     * @return string contents of this table cell.
200
     */
201
    protected function col_actions($questionstat) {
202
        if ($this->is_calculated_question_summary($questionstat)) {
203
            return '';
204
        } else if ($questionstat->question->qtype === 'missingtype') {
205
            return '';
206
        } else {
207
            return quiz_question_action_icons($this->quiz, $this->cmid,
208
                    $questionstat->question, $this->baseurl, $questionstat->variant);
209
        }
210
    }
211
 
212
    /**
213
     * The question type name.
214
     *
215
     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
216
     * @return string contents of this table cell.
217
     */
218
    protected function col_qtype($questionstat) {
219
        return question_bank::get_qtype_name($questionstat->question->qtype);
220
    }
221
 
222
    /**
223
     * The question name.
224
     *
225
     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
226
     * @return string contents of this table cell.
227
     */
228
    protected function col_name($questionstat) {
229
        $name = $questionstat->question->name;
230
 
231
        if (!is_null($questionstat->variant)) {
232
            $a = new stdClass();
233
            $a->name = $name;
234
            $a->variant = $questionstat->variant;
235
            $name = get_string('nameforvariant', 'quiz_statistics', $a);
236
        }
237
 
238
        if ($this->is_downloading()) {
239
            return $name;
240
        }
241
 
242
        $baseurl = new moodle_url($this->baseurl);
243
        if (!is_null($questionstat->variant)) {
244
            if ($questionstat->subquestion) {
245
                // Variant of a sub-question.
246
                $url = new moodle_url($baseurl, ['qid' => $questionstat->questionid, 'variant' => $questionstat->variant]);
247
                $name = html_writer::link($url, $name, ['title' => get_string('detailedanalysisforvariant',
248
                                                                                   'quiz_statistics',
249
                                                                                   $questionstat->variant)]);
250
            } else if ($questionstat->slot) {
251
                // Variant of a question in a slot.
252
                $url = new moodle_url($baseurl, ['slot' => $questionstat->slot, 'variant' => $questionstat->variant]);
253
                $name = html_writer::link($url, $name, ['title' => get_string('detailedanalysisforvariant',
254
                                                                                   'quiz_statistics',
255
                                                                                   $questionstat->variant)]);
256
            }
257
        } else {
258
            if ($questionstat->subquestion && !$questionstat->get_variants()) {
259
                // Sub question without variants.
260
                $url = new moodle_url($baseurl, ['qid' => $questionstat->questionid]);
261
                $name = html_writer::link($url, $name, ['title' => get_string('detailedanalysis', 'quiz_statistics')]);
262
            } else if ($baseurl->param('slot') === null && $questionstat->slot) {
263
                // Question in a slot, we are not on a page showing structural analysis of one slot,
264
                // we don't want linking on those pages.
265
                $number = $questionstat->question->number;
266
                $israndomquestion = $questionstat->question->qtype == 'random';
267
                $url = new moodle_url($baseurl, ['slot' => $questionstat->slot]);
268
 
269
                if ($this->is_calculated_question_summary($questionstat)) {
270
                    // Only make the random question summary row name link to the slot structure
271
                    // analysis page with specific text to clearly indicate the link to the user.
272
                    // Random and variant question rows will render the name without a link to improve clarity
273
                    // in the UI.
274
                    $name = html_writer::div(get_string('rangeofvalues', 'quiz_statistics'));
275
                } else if (!$israndomquestion && !$questionstat->get_variants() && !$questionstat->get_sub_question_ids()) {
276
                    // Question cannot be broken down into sub-questions or variants. Link will show response analysis page.
277
                    $name = html_writer::link($url,
278
                                              $name,
279
                                              ['title' => get_string('detailedanalysis', 'quiz_statistics')]);
280
                }
281
            }
282
        }
283
 
284
 
285
        if ($this->is_dubious_question($questionstat)) {
286
            $name = html_writer::tag('div', $name, ['class' => 'dubious']);
287
        }
288
 
289
        if ($this->is_calculated_question_summary($questionstat)) {
290
            $name .= html_writer::link($url, get_string('viewanalysis', 'quiz_statistics'));
291
        } else if (!empty($questionstat->minmedianmaxnotice)) {
292
            $name = get_string($questionstat->minmedianmaxnotice, 'quiz_statistics') . '<br />' . $name;
293
        }
294
 
295
        return $name;
296
    }
297
 
298
    /**
299
     * The number of attempts at this question.
300
     *
301
     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
302
     * @return string contents of this table cell.
303
     */
304
    protected function col_s($questionstat) {
305
        if ($this->is_calculated_question_summary($questionstat)) {
306
            list($min, $max) = $questionstat->get_min_max_of('s');
307
            $min = $min ?: 0;
308
            $max = $max ?: 0;
309
            return $this->format_range($min, $max);
310
        } else if (!isset($questionstat->s)) {
311
            return 0;
312
        } else {
313
            return $questionstat->s;
314
        }
315
    }
316
 
317
    /**
318
     * The facility index (average fraction).
319
     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
320
     * @return string contents of this table cell.
321
     */
322
    protected function col_facility($questionstat) {
323
        if ($this->is_calculated_question_summary($questionstat)) {
324
            list($min, $max) = $questionstat->get_min_max_of('facility');
325
            return $this->format_percentage_range($min, $max);
326
        } else if (is_null($questionstat->facility)) {
327
            return '';
328
        } else {
329
            return $this->format_percentage($questionstat->facility);
330
        }
331
    }
332
 
333
    /**
334
     * The standard deviation of the fractions.
335
     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
336
     * @return string contents of this table cell.
337
     */
338
    protected function col_sd($questionstat) {
339
        if ($this->is_calculated_question_summary($questionstat)) {
340
            list($min, $max) = $questionstat->get_min_max_of('sd');
341
            return $this->format_percentage_range($min, $max);
342
        } else if (is_null($questionstat->sd) || $questionstat->maxmark == 0) {
343
            return '';
344
        } else {
345
            return $this->format_percentage($questionstat->sd / $questionstat->maxmark);
346
        }
347
    }
348
 
349
    /**
350
     * An estimate of the fraction a student would get by guessing randomly.
351
     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
352
     * @return string contents of this table cell.
353
     */
354
    protected function col_random_guess_score($questionstat) {
355
        if ($this->is_calculated_question_summary($questionstat)) {
356
            list($min, $max) = $questionstat->get_min_max_of('randomguessscore');
357
            return $this->format_percentage_range($min, $max);
358
        } else if (is_null($questionstat->randomguessscore)) {
359
            return '';
360
        } else {
361
            return $this->format_percentage($questionstat->randomguessscore);
362
        }
363
    }
364
 
365
    /**
366
     * The intended question weight. Maximum mark for the question as a percentage
367
     * of maximum mark for the quiz. That is, the indended influence this question
368
     * on the student's overall mark.
369
     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
370
     * @return string contents of this table cell.
371
     */
372
    protected function col_intended_weight($questionstat) {
373
        if ($this->is_calculated_question_summary($questionstat)) {
374
            list($min, $max) = $questionstat->get_min_max_of('maxmark');
375
 
376
            if (is_null($min) && is_null($max)) {
377
                return '';
378
            } else {
379
                $min = quiz_report_scale_summarks_as_percentage($min, $this->quiz);
380
                $max = quiz_report_scale_summarks_as_percentage($max, $this->quiz);
381
                return $this->format_range($min, $max);
382
            }
383
        } else {
384
            return quiz_report_scale_summarks_as_percentage($questionstat->maxmark, $this->quiz);
385
        }
386
    }
387
 
388
    /**
389
     * The effective question weight. That is, an estimate of the actual
390
     * influence this question has on the student's overall mark.
391
     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
392
     * @return string contents of this table cell.
393
     */
394
    protected function col_effective_weight($questionstat) {
395
        global $OUTPUT;
396
 
397
        if ($this->is_calculated_question_summary($questionstat)) {
398
            list($min, $max) = $questionstat->get_min_max_of('effectiveweight');
399
 
400
            if (is_null($min) && is_null($max)) {
401
                return '';
402
            } else {
403
                list( , $negcovar) = $questionstat->get_min_max_of('negcovar');
404
                if ($negcovar) {
405
                    $min = get_string('negcovar', 'quiz_statistics');
406
                }
407
 
408
                return $this->format_range($min, $max);
409
            }
410
        } else if (is_null($questionstat->effectiveweight)) {
411
            return '';
412
        } else if ($questionstat->negcovar) {
413
            $negcovar = get_string('negcovar', 'quiz_statistics');
414
 
415
            if (!$this->is_downloading()) {
416
                $negcovar = html_writer::tag('div',
417
                        $negcovar . $OUTPUT->help_icon('negcovar', 'quiz_statistics'),
418
                        ['class' => 'negcovar']);
419
            }
420
 
421
            return $negcovar;
422
        } else {
423
            return $this->format_percentage($questionstat->effectiveweight, false);
424
        }
425
    }
426
 
427
    /**
428
     * Discrimination index. This is the product moment correlation coefficient
429
     * between the fraction for this question, and the average fraction for the
430
     * other questions in this quiz.
431
     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
432
     * @return string contents of this table cell.
433
     */
434
    protected function col_discrimination_index($questionstat) {
435
        if ($this->is_calculated_question_summary($questionstat)) {
436
            list($min, $max) = $questionstat->get_min_max_of('discriminationindex');
437
 
438
            if (isset($max)) {
439
                $min = $min ?: 0;
440
            }
441
 
442
            if (is_numeric($min)) {
443
                $min = $this->format_percentage($min, false);
444
            }
445
            if (is_numeric($max)) {
446
                $max = $this->format_percentage($max, false);
447
            }
448
 
449
            return $this->format_range($min, $max);
450
        } else if (!is_numeric($questionstat->discriminationindex)) {
451
            return $questionstat->discriminationindex;
452
        } else {
453
            return $this->format_percentage($questionstat->discriminationindex, false);
454
        }
455
    }
456
 
457
    /**
458
     * Discrimination efficiency, similar to, but different from, the Discrimination index.
459
     *
460
     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
461
     * @return string contents of this table cell.
462
     */
463
    protected function col_discriminative_efficiency($questionstat) {
464
        if ($this->is_calculated_question_summary($questionstat)) {
465
            list($min, $max) = $questionstat->get_min_max_of('discriminativeefficiency');
466
 
467
            if (!is_numeric($min) && !is_numeric($max)) {
468
                return '';
469
            } else {
470
                return $this->format_percentage_range($min, $max, false);
471
            }
472
        } else if (!is_numeric($questionstat->discriminativeefficiency)) {
473
            return '';
474
        } else {
475
            return $this->format_percentage($questionstat->discriminativeefficiency, false);
476
        }
477
    }
478
 
479
    /**
480
     * This method encapsulates the test for wheter a question should be considered dubious.
481
     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
482
     * @return bool is this question possibly not pulling it's weight?
483
     */
484
    protected function is_dubious_question($questionstat) {
485
        if ($this->is_calculated_question_summary($questionstat)) {
486
            // We only care about the minimum value here.
487
            // If the minimum value is less than the threshold, then we know that there is at least one value below the threshold.
488
            list($discriminativeefficiency) = $questionstat->get_min_max_of('discriminativeefficiency');
489
        } else {
490
            $discriminativeefficiency = $questionstat->discriminativeefficiency;
491
        }
492
 
493
        if (!is_numeric($discriminativeefficiency)) {
494
            return false;
495
        }
496
 
497
        return $discriminativeefficiency < 15;
498
    }
499
 
500
    /**
501
     * Check if the given stats object is an instance of calculated_question_summary.
502
     *
503
     * @param  \core_question\statistics\questions\calculated $questionstat Stats object
504
     * @return bool
505
     */
506
    protected function is_calculated_question_summary($questionstat) {
507
        return $questionstat instanceof calculated_question_summary;
508
    }
509
 
510
    /**
511
     * Format inputs to represent a range between $min and $max.
512
     * This function does not check if $min is less than $max or not.
513
     * If both $min and $max are equal to null, this function returns an empty string.
514
     *
515
     * @param string|null $min The minimum value in the range
516
     * @param string|null $max The maximum value in the range
517
     * @return string
518
     */
519
    protected function format_range(string $min = null, string $max = null) {
520
        if (is_null($min) && is_null($max)) {
521
            return '';
522
        } else {
523
            $a = new stdClass();
524
            $a->min = $min;
525
            $a->max = $max;
526
 
527
            return get_string('rangebetween', 'quiz_statistics', $a);
528
        }
529
    }
530
 
531
    /**
532
     * Format a number to a localised percentage with specified decimal points.
533
     *
534
     * @param float $number The number being formatted
535
     * @param bool $fraction An indicator for whether the number is a fraction or is already multiplied by 100
536
     * @param int $decimals Sets the number of decimal points
537
     * @return string
538
     */
539
    protected function format_percentage(float $number, bool $fraction = true, int $decimals = 2) {
540
        $coefficient = $fraction ? 100 : 1;
541
        return get_string('percents', 'moodle', format_float($number * $coefficient, $decimals));
542
    }
543
 
544
    /**
545
     * Format $min and $max to localised percentages and form a string that represents a range between them.
546
     * This function does not check if $min is less than $max or not.
547
     * If both $min and $max are equal to null, this function returns an empty string.
548
     *
549
     * @param float|null $min The minimum value of the range
550
     * @param float|null $max The maximum value of the range
551
     * @param bool $fraction An indicator for whether min and max are a fractions or are already multiplied by 100
552
     * @param int $decimals Sets the number of decimal points
553
     * @return string A formatted string that represents a range between $min to $max.
554
     */
555
    protected function format_percentage_range(float $min = null, float $max = null, bool $fraction = true, int $decimals = 2) {
556
        if (is_null($min) && is_null($max)) {
557
            return '';
558
        } else {
559
            $min = $min ?: 0;
560
            $max = $max ?: 0;
561
            return $this->format_range(
562
                    $this->format_percentage($min, $fraction, $decimals),
563
                    $this->format_percentage($max, $fraction, $decimals)
564
            );
565
        }
566
    }
567
}