Proyectos de Subversion Moodle

Rev

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