Proyectos de Subversion Moodle

Rev

Rev 11 | | Comparar con el anterior | 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
 * Helper functions for the quiz reports.
19
 *
20
 * @package   mod_quiz
21
 * @copyright 2008 Jamie Pratt
22
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
 
26
defined('MOODLE_INTERNAL') || die();
27
 
28
require_once($CFG->dirroot . '/mod/quiz/lib.php');
29
require_once($CFG->libdir . '/filelib.php');
30
 
31
use mod_quiz\question\display_options;
32
 
33
/**
34
 * Takes an array of objects and constructs a multidimensional array keyed by
35
 * the keys it finds on the object.
36
 * @param array $datum an array of objects with properties on the object
37
 * including the keys passed as the next param.
38
 * @param array $keys Array of strings with the names of the properties on the
39
 * objects in datum that you want to index the multidimensional array by.
40
 * @param bool $keysunique If there is not only one object for each
41
 * combination of keys you are using you should set $keysunique to true.
42
 * Otherwise all the object will be added to a zero based array. So the array
43
 * returned will have count($keys) + 1 indexs.
44
 * @return array multidimensional array properly indexed.
45
 */
46
function quiz_report_index_by_keys($datum, $keys, $keysunique = true) {
47
    if (!$datum) {
48
        return [];
49
    }
50
    $key = array_shift($keys);
51
    $datumkeyed = [];
52
    foreach ($datum as $data) {
53
        if ($keys || !$keysunique) {
54
            $datumkeyed[$data->{$key}][]= $data;
55
        } else {
56
            $datumkeyed[$data->{$key}]= $data;
57
        }
58
    }
59
    if ($keys) {
60
        foreach ($datumkeyed as $datakey => $datakeyed) {
61
            $datumkeyed[$datakey] = quiz_report_index_by_keys($datakeyed, $keys, $keysunique);
62
        }
63
    }
64
    return $datumkeyed;
65
}
66
 
67
function quiz_report_unindex($datum) {
68
    if (!$datum) {
69
        return $datum;
70
    }
71
    $datumunkeyed = [];
72
    foreach ($datum as $value) {
73
        if (is_array($value)) {
74
            $datumunkeyed = array_merge($datumunkeyed, quiz_report_unindex($value));
75
        } else {
76
            $datumunkeyed[] = $value;
77
        }
78
    }
79
    return $datumunkeyed;
80
}
81
 
82
/**
83
 * Are there any questions in this quiz?
84
 * @param int $quizid the quiz id.
85
 */
86
function quiz_has_questions($quizid) {
87
    global $DB;
88
    return $DB->record_exists('quiz_slots', ['quizid' => $quizid]);
89
}
90
 
91
/**
92
 * Get the slots of real questions (not descriptions) in this quiz, in order.
93
 * @param stdClass $quiz the quiz.
94
 * @return array of slot => objects with fields
95
 *      ->slot, ->id, ->qtype, ->length, ->number, ->maxmark, ->category (for random questions).
96
 */
97
function quiz_report_get_significant_questions($quiz) {
98
    $quizobj = mod_quiz\quiz_settings::create($quiz->id);
99
    $structure = \mod_quiz\structure::create_for_quiz($quizobj);
100
    $slots = $structure->get_slots();
101
 
102
    $qsbyslot = [];
103
    $number = 1;
104
    foreach ($slots as $slot) {
105
        // Ignore 'questions' of zero length.
106
        if ($slot->length == 0) {
107
            continue;
108
        }
109
 
110
        $slotreport = new \stdClass();
111
        $slotreport->slot = $slot->slot;
112
        $slotreport->id = $slot->questionid;
113
        $slotreport->qtype = $slot->qtype;
114
        $slotreport->length = $slot->length;
115
        $slotreport->number = $number;
11 efrain 116
        $slotreport->displaynumber = $slot->displaynumber ?? $number;
1 efrain 117
        $number += $slot->length;
118
        $slotreport->maxmark = $slot->maxmark;
119
        $slotreport->category = $slot->category;
120
 
121
        $qsbyslot[$slotreport->slot] = $slotreport;
122
    }
123
 
124
    return $qsbyslot;
125
}
126
 
127
/**
128
 * @param stdClass $quiz the quiz settings.
129
 * @return bool whether, for this quiz, it is possible to filter attempts to show
130
 *      only those that gave the final grade.
131
 */
132
function quiz_report_can_filter_only_graded($quiz) {
133
    return $quiz->attempts != 1 && $quiz->grademethod != QUIZ_GRADEAVERAGE;
134
}
135
 
136
/**
137
 * This is a wrapper for {@link quiz_report_grade_method_sql} that takes the whole quiz object instead of just the grading method
138
 * as a param. See definition for {@link quiz_report_grade_method_sql} below.
139
 *
140
 * @param stdClass $quiz
141
 * @param string $quizattemptsalias sql alias for 'quiz_attempts' table
142
 * @return string sql to test if this is an attempt that will contribute towards the grade of the user
143
 */
144
function quiz_report_qm_filter_select($quiz, $quizattemptsalias = 'quiza') {
145
    if ($quiz->attempts == 1) {
146
        // This quiz only allows one attempt.
147
        return '';
148
    }
149
    return quiz_report_grade_method_sql($quiz->grademethod, $quizattemptsalias);
150
}
151
 
152
/**
153
 * Given a quiz grading method return sql to test if this is an
154
 * attempt that will be contribute towards the grade of the user. Or return an
155
 * empty string if the grading method is QUIZ_GRADEAVERAGE and thus all attempts
156
 * contribute to final grade.
157
 *
158
 * @param string $grademethod quiz grading method.
159
 * @param string $quizattemptsalias sql alias for 'quiz_attempts' table
160
 * @return string sql to test if this is an attempt that will contribute towards the graded of the user
161
 */
162
function quiz_report_grade_method_sql($grademethod, $quizattemptsalias = 'quiza') {
163
    switch ($grademethod) {
164
        case QUIZ_GRADEHIGHEST :
165
            return "($quizattemptsalias.state = 'finished' AND NOT EXISTS (
166
                           SELECT 1 FROM {quiz_attempts} qa2
167
                            WHERE qa2.quiz = $quizattemptsalias.quiz AND
168
                                qa2.userid = $quizattemptsalias.userid AND
169
                                 qa2.state = 'finished' AND (
170
                COALESCE(qa2.sumgrades, 0) > COALESCE($quizattemptsalias.sumgrades, 0) OR
171
               (COALESCE(qa2.sumgrades, 0) = COALESCE($quizattemptsalias.sumgrades, 0) AND qa2.attempt < $quizattemptsalias.attempt)
172
                                )))";
173
 
174
        case QUIZ_GRADEAVERAGE :
175
            return '';
176
 
177
        case QUIZ_ATTEMPTFIRST :
1441 ariadna 178
            return "(
179
                $quizattemptsalias.state IN ('finished', 'submitted')
180
                AND NOT EXISTS (
181
                    SELECT 1
182
                      FROM {quiz_attempts} qa2
183
                     WHERE qa2.quiz = $quizattemptsalias.quiz
184
                           AND qa2.userid = $quizattemptsalias.userid
185
                           AND qa2.state IN ('finished', 'submitted')
186
                           AND qa2.attempt < $quizattemptsalias.attempt
187
                )
188
            )";
1 efrain 189
 
190
        case QUIZ_ATTEMPTLAST :
1441 ariadna 191
            return "(
192
                $quizattemptsalias.state IN ('finished', 'submitted')
193
                AND NOT EXISTS (
194
                    SELECT 1
195
                      FROM {quiz_attempts} qa2
196
                     WHERE qa2.quiz = $quizattemptsalias.quiz
197
                           AND qa2.userid = $quizattemptsalias.userid
198
                           AND qa2.state IN ('finished', 'submitted')
199
                           AND qa2.attempt > $quizattemptsalias.attempt
200
                )
201
            )";
1 efrain 202
    }
203
}
204
 
205
/**
206
 * Get the number of students whose score was in a particular band for this quiz.
207
 * @param number $bandwidth the width of each band.
208
 * @param int $bands the number of bands
209
 * @param int $quizid the quiz id.
210
 * @param \core\dml\sql_join $usersjoins (joins, wheres, params) to get enrolled users
211
 * @return array band number => number of users with scores in that band.
212
 */
1441 ariadna 213
function quiz_report_grade_bands($bandwidth, $bands, $quizid, ?\core\dml\sql_join $usersjoins = null) {
1 efrain 214
    global $DB;
215
    if (!is_int($bands)) {
216
        debugging('$bands passed to quiz_report_grade_bands must be an integer. (' .
217
                gettype($bands) . ' passed.)', DEBUG_DEVELOPER);
218
        $bands = (int) $bands;
219
    }
220
 
221
    if ($usersjoins && !empty($usersjoins->joins)) {
222
        $userjoin = "JOIN {user} u ON u.id = qg.userid
223
                {$usersjoins->joins}";
224
        $usertest = $usersjoins->wheres;
225
        $params = $usersjoins->params;
226
    } else {
227
        $userjoin = '';
228
        $usertest = '1=1';
229
        $params = [];
230
    }
231
    $sql = "
232
SELECT band, COUNT(1)
233
 
234
FROM (
235
    SELECT FLOOR(qg.grade / :bandwidth) AS band
236
      FROM {quiz_grades} qg
237
    $userjoin
238
    WHERE $usertest AND qg.quiz = :quizid
239
) subquery
240
 
241
GROUP BY
242
    band
243
 
244
ORDER BY
245
    band";
246
 
247
    $params['quizid'] = $quizid;
248
    $params['bandwidth'] = $bandwidth;
249
 
250
    $data = $DB->get_records_sql_menu($sql, $params);
251
 
252
    // We need to create array elements with values 0 at indexes where there is no element.
253
    $data = $data + array_fill(0, $bands + 1, 0);
254
    ksort($data);
255
 
256
    // Place the maximum (perfect grade) into the last band i.e. make last
257
    // band for example 9 <= g <=10 (where 10 is the perfect grade) rather than
258
    // just 9 <= g <10.
259
    $data[$bands - 1] += $data[$bands];
260
    unset($data[$bands]);
261
 
262
    // See MDL-60632. When a quiz participant achieves an overall negative grade the chart fails to render.
263
    foreach ($data as $databand => $datanum) {
264
        if ($databand < 0) {
265
            $data["0"] += $datanum; // Add to band 0.
266
            unset($data[$databand]); // Remove entry below 0.
267
        }
268
    }
269
 
270
    return $data;
271
}
272
 
273
function quiz_report_highlighting_grading_method($quiz, $qmsubselect, $qmfilter) {
274
    if ($quiz->attempts == 1) {
275
        return '<p>' . get_string('onlyoneattemptallowed', 'quiz_overview') . '</p>';
276
 
277
    } else if (!$qmsubselect) {
278
        return '<p>' . get_string('allattemptscontributetograde', 'quiz_overview') . '</p>';
279
 
280
    } else if ($qmfilter) {
281
        return '<p>' . get_string('showinggraded', 'quiz_overview') . '</p>';
282
 
283
    } else {
284
        return '<p>' . get_string('showinggradedandungraded', 'quiz_overview',
285
                '<span class="gradedattempt">' . quiz_get_grading_option_name($quiz->grademethod) .
286
                '</span>') . '</p>';
287
    }
288
}
289
 
290
/**
291
 * Get the feedback text for a grade on this quiz. The feedback is
292
 * processed ready for display.
293
 *
294
 * @param float $grade a grade on this quiz.
295
 * @param int $quizid the id of the quiz object.
296
 * @return string the comment that corresponds to this grade (empty string if there is not one.
297
 */
298
function quiz_report_feedback_for_grade($grade, $quizid, $context) {
299
    global $DB;
300
 
301
    static $feedbackcache = [];
302
 
303
    if (!isset($feedbackcache[$quizid])) {
304
        $feedbackcache[$quizid] = $DB->get_records('quiz_feedback', ['quizid' => $quizid]);
305
    }
306
 
307
    // With CBM etc, it is possible to get -ve grades, which would then not match
308
    // any feedback. Therefore, we replace -ve grades with 0.
309
    $grade = max($grade, 0);
310
 
311
    $feedbacks = $feedbackcache[$quizid];
312
    $feedbackid = 0;
313
    $feedbacktext = '';
314
    $feedbacktextformat = FORMAT_MOODLE;
315
    foreach ($feedbacks as $feedback) {
316
        if ($feedback->mingrade <= $grade && $grade < $feedback->maxgrade) {
317
            $feedbackid = $feedback->id;
318
            $feedbacktext = $feedback->feedbacktext;
319
            $feedbacktextformat = $feedback->feedbacktextformat;
320
            break;
321
        }
322
    }
323
 
324
    // Clean the text, ready for display.
325
    $formatoptions = new stdClass();
326
    $formatoptions->noclean = true;
327
    $feedbacktext = file_rewrite_pluginfile_urls($feedbacktext, 'pluginfile.php',
328
            $context->id, 'mod_quiz', 'feedback', $feedbackid);
329
    $feedbacktext = format_text($feedbacktext, $feedbacktextformat, $formatoptions);
330
 
331
    return $feedbacktext;
332
}
333
 
334
/**
335
 * Format a number as a percentage out of $quiz->sumgrades
336
 * @param number $rawgrade the mark to format.
337
 * @param stdClass $quiz the quiz settings
338
 * @param bool $round whether to round the results ot $quiz->decimalpoints.
339
 */
340
function quiz_report_scale_summarks_as_percentage($rawmark, $quiz, $round = true) {
341
    if ($quiz->sumgrades == 0) {
342
        return '';
343
    }
344
    if (!is_numeric($rawmark)) {
345
        return $rawmark;
346
    }
347
 
348
    $mark = $rawmark * 100 / $quiz->sumgrades;
349
    if ($round) {
350
        $mark = quiz_format_grade($quiz, $mark);
351
    }
352
 
353
    return get_string('percents', 'moodle', $mark);
354
}
355
 
356
/**
357
 * Returns an array of reports to which the current user has access to.
358
 * @return array reports are ordered as they should be for display in tabs.
359
 */
360
function quiz_report_list($context) {
361
    global $DB;
362
    static $reportlist = null;
363
    if (!is_null($reportlist)) {
364
        return $reportlist;
365
    }
366
 
367
    $reports = $DB->get_records('quiz_reports', null, 'displayorder DESC', 'name, capability');
368
    $reportdirs = core_component::get_plugin_list('quiz');
369
 
370
    // Order the reports tab in descending order of displayorder.
371
    $reportcaps = [];
372
    foreach ($reports as $key => $report) {
373
        if (array_key_exists($report->name, $reportdirs)) {
374
            $reportcaps[$report->name] = $report->capability;
375
        }
376
    }
377
 
378
    // Add any other reports, which are on disc but not in the DB, on the end.
379
    foreach ($reportdirs as $reportname => $notused) {
380
        if (!isset($reportcaps[$reportname])) {
381
            $reportcaps[$reportname] = null;
382
        }
383
    }
384
    $reportlist = [];
385
    foreach ($reportcaps as $name => $capability) {
386
        if (empty($capability)) {
387
            $capability = 'mod/quiz:viewreports';
388
        }
389
        if (has_capability($capability, $context)) {
390
            $reportlist[] = $name;
391
        }
392
    }
393
    return $reportlist;
394
}
395
 
396
/**
397
 * Create a filename for use when downloading data from a quiz report. It is
398
 * expected that this will be passed to flexible_table::is_downloading, which
399
 * cleans the filename of bad characters and adds the file extension.
400
 * @param string $report the type of report.
401
 * @param string $courseshortname the course shortname.
402
 * @param string $quizname the quiz name.
403
 * @return string the filename.
404
 */
405
function quiz_report_download_filename($report, $courseshortname, $quizname) {
406
    return $courseshortname . '-' . format_string($quizname, true) . '-' . $report;
407
}
408
 
409
/**
410
 * Get the default report for the current user.
411
 * @param stdClass $context the quiz context.
412
 */
413
function quiz_report_default_report($context) {
414
    $reports = quiz_report_list($context);
415
    return reset($reports);
416
}
417
 
418
/**
419
 * Generate a message saying that this quiz has no questions, with a button to
420
 * go to the edit page, if the user has the right capability.
421
 * @param stdClass $quiz the quiz settings.
422
 * @param stdClass $cm the course_module object.
423
 * @param stdClass $context the quiz context.
424
 * @return string HTML to output.
425
 */
426
function quiz_no_questions_message($quiz, $cm, $context) {
427
    global $OUTPUT;
428
 
429
    $output = '';
430
    $output .= $OUTPUT->notification(get_string('noquestions', 'quiz'));
431
    if (has_capability('mod/quiz:manage', $context)) {
432
        $output .= $OUTPUT->single_button(new moodle_url('/mod/quiz/edit.php',
433
        ['cmid' => $cm->id]), get_string('editquiz', 'quiz'), 'get');
434
    }
435
 
436
    return $output;
437
}
438
 
439
/**
440
 * Should the grades be displayed in this report. That depends on the quiz
441
 * display options, and whether the quiz is graded.
442
 * @param stdClass $quiz the quiz settings.
443
 * @param context $context the quiz context.
444
 * @return bool
445
 */
446
function quiz_report_should_show_grades($quiz, context $context) {
447
    if ($quiz->timeclose && time() > $quiz->timeclose) {
448
        $when = display_options::AFTER_CLOSE;
449
    } else {
450
        $when = display_options::LATER_WHILE_OPEN;
451
    }
452
    $reviewoptions = display_options::make_from_quiz($quiz, $when);
453
 
454
    return quiz_has_grades($quiz) &&
455
            ($reviewoptions->marks >= question_display_options::MARK_AND_MAX ||
456
            has_capability('moodle/grade:viewhidden', $context));
457
}