Proyectos de Subversion Moodle

Rev

Rev 1 | | 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 :
178
            return "($quizattemptsalias.state = 'finished' AND NOT EXISTS (
179
                           SELECT 1 FROM {quiz_attempts} qa2
180
                            WHERE qa2.quiz = $quizattemptsalias.quiz AND
181
                                qa2.userid = $quizattemptsalias.userid AND
182
                                 qa2.state = 'finished' AND
183
                               qa2.attempt < $quizattemptsalias.attempt))";
184
 
185
        case QUIZ_ATTEMPTLAST :
186
            return "($quizattemptsalias.state = 'finished' AND NOT EXISTS (
187
                           SELECT 1 FROM {quiz_attempts} qa2
188
                            WHERE qa2.quiz = $quizattemptsalias.quiz AND
189
                                qa2.userid = $quizattemptsalias.userid AND
190
                                 qa2.state = 'finished' AND
191
                               qa2.attempt > $quizattemptsalias.attempt))";
192
    }
193
}
194
 
195
/**
196
 * Get the number of students whose score was in a particular band for this quiz.
197
 * @param number $bandwidth the width of each band.
198
 * @param int $bands the number of bands
199
 * @param int $quizid the quiz id.
200
 * @param \core\dml\sql_join $usersjoins (joins, wheres, params) to get enrolled users
201
 * @return array band number => number of users with scores in that band.
202
 */
203
function quiz_report_grade_bands($bandwidth, $bands, $quizid, \core\dml\sql_join $usersjoins = null) {
204
    global $DB;
205
    if (!is_int($bands)) {
206
        debugging('$bands passed to quiz_report_grade_bands must be an integer. (' .
207
                gettype($bands) . ' passed.)', DEBUG_DEVELOPER);
208
        $bands = (int) $bands;
209
    }
210
 
211
    if ($usersjoins && !empty($usersjoins->joins)) {
212
        $userjoin = "JOIN {user} u ON u.id = qg.userid
213
                {$usersjoins->joins}";
214
        $usertest = $usersjoins->wheres;
215
        $params = $usersjoins->params;
216
    } else {
217
        $userjoin = '';
218
        $usertest = '1=1';
219
        $params = [];
220
    }
221
    $sql = "
222
SELECT band, COUNT(1)
223
 
224
FROM (
225
    SELECT FLOOR(qg.grade / :bandwidth) AS band
226
      FROM {quiz_grades} qg
227
    $userjoin
228
    WHERE $usertest AND qg.quiz = :quizid
229
) subquery
230
 
231
GROUP BY
232
    band
233
 
234
ORDER BY
235
    band";
236
 
237
    $params['quizid'] = $quizid;
238
    $params['bandwidth'] = $bandwidth;
239
 
240
    $data = $DB->get_records_sql_menu($sql, $params);
241
 
242
    // We need to create array elements with values 0 at indexes where there is no element.
243
    $data = $data + array_fill(0, $bands + 1, 0);
244
    ksort($data);
245
 
246
    // Place the maximum (perfect grade) into the last band i.e. make last
247
    // band for example 9 <= g <=10 (where 10 is the perfect grade) rather than
248
    // just 9 <= g <10.
249
    $data[$bands - 1] += $data[$bands];
250
    unset($data[$bands]);
251
 
252
    // See MDL-60632. When a quiz participant achieves an overall negative grade the chart fails to render.
253
    foreach ($data as $databand => $datanum) {
254
        if ($databand < 0) {
255
            $data["0"] += $datanum; // Add to band 0.
256
            unset($data[$databand]); // Remove entry below 0.
257
        }
258
    }
259
 
260
    return $data;
261
}
262
 
263
function quiz_report_highlighting_grading_method($quiz, $qmsubselect, $qmfilter) {
264
    if ($quiz->attempts == 1) {
265
        return '<p>' . get_string('onlyoneattemptallowed', 'quiz_overview') . '</p>';
266
 
267
    } else if (!$qmsubselect) {
268
        return '<p>' . get_string('allattemptscontributetograde', 'quiz_overview') . '</p>';
269
 
270
    } else if ($qmfilter) {
271
        return '<p>' . get_string('showinggraded', 'quiz_overview') . '</p>';
272
 
273
    } else {
274
        return '<p>' . get_string('showinggradedandungraded', 'quiz_overview',
275
                '<span class="gradedattempt">' . quiz_get_grading_option_name($quiz->grademethod) .
276
                '</span>') . '</p>';
277
    }
278
}
279
 
280
/**
281
 * Get the feedback text for a grade on this quiz. The feedback is
282
 * processed ready for display.
283
 *
284
 * @param float $grade a grade on this quiz.
285
 * @param int $quizid the id of the quiz object.
286
 * @return string the comment that corresponds to this grade (empty string if there is not one.
287
 */
288
function quiz_report_feedback_for_grade($grade, $quizid, $context) {
289
    global $DB;
290
 
291
    static $feedbackcache = [];
292
 
293
    if (!isset($feedbackcache[$quizid])) {
294
        $feedbackcache[$quizid] = $DB->get_records('quiz_feedback', ['quizid' => $quizid]);
295
    }
296
 
297
    // With CBM etc, it is possible to get -ve grades, which would then not match
298
    // any feedback. Therefore, we replace -ve grades with 0.
299
    $grade = max($grade, 0);
300
 
301
    $feedbacks = $feedbackcache[$quizid];
302
    $feedbackid = 0;
303
    $feedbacktext = '';
304
    $feedbacktextformat = FORMAT_MOODLE;
305
    foreach ($feedbacks as $feedback) {
306
        if ($feedback->mingrade <= $grade && $grade < $feedback->maxgrade) {
307
            $feedbackid = $feedback->id;
308
            $feedbacktext = $feedback->feedbacktext;
309
            $feedbacktextformat = $feedback->feedbacktextformat;
310
            break;
311
        }
312
    }
313
 
314
    // Clean the text, ready for display.
315
    $formatoptions = new stdClass();
316
    $formatoptions->noclean = true;
317
    $feedbacktext = file_rewrite_pluginfile_urls($feedbacktext, 'pluginfile.php',
318
            $context->id, 'mod_quiz', 'feedback', $feedbackid);
319
    $feedbacktext = format_text($feedbacktext, $feedbacktextformat, $formatoptions);
320
 
321
    return $feedbacktext;
322
}
323
 
324
/**
325
 * Format a number as a percentage out of $quiz->sumgrades
326
 * @param number $rawgrade the mark to format.
327
 * @param stdClass $quiz the quiz settings
328
 * @param bool $round whether to round the results ot $quiz->decimalpoints.
329
 */
330
function quiz_report_scale_summarks_as_percentage($rawmark, $quiz, $round = true) {
331
    if ($quiz->sumgrades == 0) {
332
        return '';
333
    }
334
    if (!is_numeric($rawmark)) {
335
        return $rawmark;
336
    }
337
 
338
    $mark = $rawmark * 100 / $quiz->sumgrades;
339
    if ($round) {
340
        $mark = quiz_format_grade($quiz, $mark);
341
    }
342
 
343
    return get_string('percents', 'moodle', $mark);
344
}
345
 
346
/**
347
 * Returns an array of reports to which the current user has access to.
348
 * @return array reports are ordered as they should be for display in tabs.
349
 */
350
function quiz_report_list($context) {
351
    global $DB;
352
    static $reportlist = null;
353
    if (!is_null($reportlist)) {
354
        return $reportlist;
355
    }
356
 
357
    $reports = $DB->get_records('quiz_reports', null, 'displayorder DESC', 'name, capability');
358
    $reportdirs = core_component::get_plugin_list('quiz');
359
 
360
    // Order the reports tab in descending order of displayorder.
361
    $reportcaps = [];
362
    foreach ($reports as $key => $report) {
363
        if (array_key_exists($report->name, $reportdirs)) {
364
            $reportcaps[$report->name] = $report->capability;
365
        }
366
    }
367
 
368
    // Add any other reports, which are on disc but not in the DB, on the end.
369
    foreach ($reportdirs as $reportname => $notused) {
370
        if (!isset($reportcaps[$reportname])) {
371
            $reportcaps[$reportname] = null;
372
        }
373
    }
374
    $reportlist = [];
375
    foreach ($reportcaps as $name => $capability) {
376
        if (empty($capability)) {
377
            $capability = 'mod/quiz:viewreports';
378
        }
379
        if (has_capability($capability, $context)) {
380
            $reportlist[] = $name;
381
        }
382
    }
383
    return $reportlist;
384
}
385
 
386
/**
387
 * Create a filename for use when downloading data from a quiz report. It is
388
 * expected that this will be passed to flexible_table::is_downloading, which
389
 * cleans the filename of bad characters and adds the file extension.
390
 * @param string $report the type of report.
391
 * @param string $courseshortname the course shortname.
392
 * @param string $quizname the quiz name.
393
 * @return string the filename.
394
 */
395
function quiz_report_download_filename($report, $courseshortname, $quizname) {
396
    return $courseshortname . '-' . format_string($quizname, true) . '-' . $report;
397
}
398
 
399
/**
400
 * Get the default report for the current user.
401
 * @param stdClass $context the quiz context.
402
 */
403
function quiz_report_default_report($context) {
404
    $reports = quiz_report_list($context);
405
    return reset($reports);
406
}
407
 
408
/**
409
 * Generate a message saying that this quiz has no questions, with a button to
410
 * go to the edit page, if the user has the right capability.
411
 * @param stdClass $quiz the quiz settings.
412
 * @param stdClass $cm the course_module object.
413
 * @param stdClass $context the quiz context.
414
 * @return string HTML to output.
415
 */
416
function quiz_no_questions_message($quiz, $cm, $context) {
417
    global $OUTPUT;
418
 
419
    $output = '';
420
    $output .= $OUTPUT->notification(get_string('noquestions', 'quiz'));
421
    if (has_capability('mod/quiz:manage', $context)) {
422
        $output .= $OUTPUT->single_button(new moodle_url('/mod/quiz/edit.php',
423
        ['cmid' => $cm->id]), get_string('editquiz', 'quiz'), 'get');
424
    }
425
 
426
    return $output;
427
}
428
 
429
/**
430
 * Should the grades be displayed in this report. That depends on the quiz
431
 * display options, and whether the quiz is graded.
432
 * @param stdClass $quiz the quiz settings.
433
 * @param context $context the quiz context.
434
 * @return bool
435
 */
436
function quiz_report_should_show_grades($quiz, context $context) {
437
    if ($quiz->timeclose && time() > $quiz->timeclose) {
438
        $when = display_options::AFTER_CLOSE;
439
    } else {
440
        $when = display_options::LATER_WHILE_OPEN;
441
    }
442
    $reviewoptions = display_options::make_from_quiz($quiz, $when);
443
 
444
    return quiz_has_grades($quiz) &&
445
            ($reviewoptions->marks >= question_display_options::MARK_AND_MAX ||
446
            has_capability('moodle/grade:viewhidden', $context));
447
}