lib.php 88.8 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16

17
/**
18
19
20
21
22
 * Library of functions for the quiz module.
 *
 * This contains functions that are called also from outside the quiz module
 * Functions that are only called by the quiz module itself are in {@link locallib.php}
 *
23
 * @package    mod_quiz
24
25
 * @copyright  1999 onwards Martin Dougiamas {@link http://moodle.com}
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26
 */
27

28

29
30
defined('MOODLE_INTERNAL') || die();

31
require_once($CFG->dirroot . '/calendar/lib.php');
32

33

34
/**#@+
35
 * Option controlling what options are offered on the quiz settings form.
36
 */
37
38
39
40
define('QUIZ_MAX_ATTEMPT_OPTION', 10);
define('QUIZ_MAX_QPP_OPTION', 50);
define('QUIZ_MAX_DECIMAL_OPTION', 5);
define('QUIZ_MAX_Q_DECIMAL_OPTION', 7);
41
42
/**#@-*/

43
44
45
46
47
48
49
50
51
52
/**#@+
 * Options determining how the grades from individual attempts are combined to give
 * the overall grade for a user
 */
define('QUIZ_GRADEHIGHEST', '1');
define('QUIZ_GRADEAVERAGE', '2');
define('QUIZ_ATTEMPTFIRST', '3');
define('QUIZ_ATTEMPTLAST',  '4');
/**#@-*/

53
/**
54
 * @var int If start and end date for the quiz are more than this many seconds apart
55
56
 * they will be represented by two separate events in the calendar
 */
57
define('QUIZ_MAX_EVENT_LENGTH', 5*24*60*60); // 5 days.
58

59
60
61
62
63
64
65
/**#@+
 * Options for navigation method within quizzes.
 */
define('QUIZ_NAVMETHOD_FREE', 'free');
define('QUIZ_NAVMETHOD_SEQ',  'sequential');
/**#@-*/

66
67
68
69
70
71
/**
 * Event types.
 */
define('QUIZ_EVENT_TYPE_OPEN', 'open');
define('QUIZ_EVENT_TYPE_CLOSE', 'close');

72
73
require_once(__DIR__ . '/deprecatedlib.php');

74
75
/**
 * Given an object containing all the necessary data,
76
 * (defined by the form in mod_form.php) this function
77
78
 * will create a new instance and return the id number
 * of the new instance.
jamiesensei's avatar
   
jamiesensei committed
79
 *
80
 * @param object $quiz the data that came from the form.
81
82
 * @return mixed the id of the new instance on success,
 *          false or a string error message on failure.
83
 */
84
function quiz_add_instance($quiz) {
skodak's avatar
skodak committed
85
    global $DB;
86
    $cmid = $quiz->coursemodule;
87

88
    // Process the options from the form.
89
    $quiz->timecreated = time();
90
91
92
93
    $result = quiz_process_options($quiz);
    if ($result && is_string($result)) {
        return $result;
    }
gustav_delius's avatar
gustav_delius committed
94

95
    // Try to store it in the database.
96
    $quiz->id = $DB->insert_record('quiz', $quiz);
97

98
99
100
101
    // Create the first section for this quiz.
    $DB->insert_record('quiz_sections', array('quizid' => $quiz->id,
            'firstslot' => 1, 'heading' => '', 'shufflequestions' => 0));

102
103
    // Do the processing required after an add or an update.
    quiz_after_add_or_update($quiz);
jamiesensei's avatar
   
jamiesensei committed
104

105
    return $quiz->id;
106
107
}

108
109
/**
 * Given an object containing all the necessary data,
110
 * (defined by the form in mod_form.php) this function
111
 * will update an existing instance with new data.
jamiesensei's avatar
   
jamiesensei committed
112
 *
113
 * @param object $quiz the data that came from the form.
114
 * @return mixed true on success, false or a string error message on failure.
115
 */
116
117
function quiz_update_instance($quiz, $mform) {
    global $CFG, $DB;
118
    require_once($CFG->dirroot . '/mod/quiz/locallib.php');
119

120
    // Process the options from the form.
121
122
123
124
    $result = quiz_process_options($quiz);
    if ($result && is_string($result)) {
        return $result;
    }
125

126
    // Get the current value, so we can see what changed.
127
    $oldquiz = $DB->get_record('quiz', array('id' => $quiz->instance));
128

129
130
131
132
133
    // We need two values from the existing DB record that are not in the form,
    // in some of the function calls below.
    $quiz->sumgrades = $oldquiz->sumgrades;
    $quiz->grade     = $oldquiz->grade;

134
    // Update the database.
135
    $quiz->id = $quiz->instance;
136
    $DB->update_record('quiz', $quiz);
137

138
139
    // Do the processing required after an add or an update.
    quiz_after_add_or_update($quiz);
140

141
142
143
144
145
    if ($oldquiz->grademethod != $quiz->grademethod) {
        quiz_update_all_final_grades($quiz);
        quiz_update_grades($quiz);
    }

146
147
148
149
150
    $quizdateschanged = $oldquiz->timelimit   != $quiz->timelimit
                     || $oldquiz->timeclose   != $quiz->timeclose
                     || $oldquiz->graceperiod != $quiz->graceperiod;
    if ($quizdateschanged) {
        quiz_update_open_attempts(array('quizid' => $quiz->id));
151
152
    }

153
    // Delete any previous preview attempts.
154
    quiz_delete_previews($quiz);
moodler's avatar
moodler committed
155

156
    // Repaginate, if asked to.
157
    if (!empty($quiz->repaginatenow)) {
158
159
160
        quiz_repaginate_questions($quiz->id, $quiz->questionsperpage);
    }

161
    return true;
162
163
}

164
165
166
167
168
/**
 * Given an ID of an instance of this module,
 * this function will permanently delete the instance
 * and any data that depends on it.
 *
169
 * @param int $id the id of the quiz to delete.
170
 * @return bool success or failure.
171
 */
172
function quiz_delete_instance($id) {
skodak's avatar
skodak committed
173
    global $DB;
174

175
    $quiz = $DB->get_record('quiz', array('id' => $id), '*', MUST_EXIST);
176

177
    quiz_delete_all_attempts($quiz);
178
    quiz_delete_all_overrides($quiz);
179

180
181
182
183
184
185
186
    // Look for random questions that may no longer be used when this quiz is gone.
    $sql = "SELECT q.id
              FROM {quiz_slots} slot
              JOIN {question} q ON q.id = slot.questionid
             WHERE slot.quizid = ? AND q.qtype = ?";
    $questionids = $DB->get_fieldset_sql($sql, array($quiz->id, 'random'));

187
188
189
    // We need to do the following deletes before we try and delete randoms, otherwise they would still be 'in use'.
    $quizslots = $DB->get_fieldset_select('quiz_slots', 'id', 'quizid = ?', array($quiz->id));
    $DB->delete_records_list('quiz_slot_tags', 'slotid', $quizslots);
190
    $DB->delete_records('quiz_slots', array('quizid' => $quiz->id));
191
    $DB->delete_records('quiz_sections', array('quizid' => $quiz->id));
192
193
194
195
196

    foreach ($questionids as $questionid) {
        question_delete_question($questionid);
    }

197
    $DB->delete_records('quiz_feedback', array('quizid' => $quiz->id));
198

199
200
    quiz_access_manager::delete_settings($quiz);

201
    $events = $DB->get_records('event', array('modulename' => 'quiz', 'instance' => $quiz->id));
202
    foreach ($events as $event) {
203
204
        $event = calendar_event::load($event);
        $event->delete();
205
206
    }

207
    quiz_grade_item_delete($quiz);
208
    // We must delete the module record after we delete the grade item.
209
    $DB->delete_records('quiz', array('id' => $quiz->id));
210

211
212
213
    return true;
}

214
215
216
217
/**
 * Deletes a quiz override from the database and clears any corresponding calendar events
 *
 * @param object $quiz The quiz object.
218
 * @param int $overrideid The id of the override being deleted
219
 * @param bool $log Whether to trigger logs.
220
221
 * @return bool true on success
 */
222
function quiz_delete_override($quiz, $overrideid, $log = true) {
223
224
    global $DB;

225
226
227
228
229
    if (!isset($quiz->cmid)) {
        $cm = get_coursemodule_from_instance('quiz', $quiz->id, $quiz->course);
        $quiz->cmid = $cm->id;
    }

230
    $override = $DB->get_record('quiz_overrides', array('id' => $overrideid), '*', MUST_EXIST);
231

232
    // Delete the events.
233
234
235
236
    if (isset($override->groupid)) {
        // Create the search array for a group override.
        $eventsearcharray = array('modulename' => 'quiz',
            'instance' => $quiz->id, 'groupid' => (int)$override->groupid);
237
        $cachekey = "{$quiz->id}_g_{$override->groupid}";
238
239
240
241
    } else {
        // Create the search array for a user override.
        $eventsearcharray = array('modulename' => 'quiz',
            'instance' => $quiz->id, 'userid' => (int)$override->userid);
242
        $cachekey = "{$quiz->id}_u_{$override->userid}";
243
244
    }
    $events = $DB->get_records('event', $eventsearcharray);
245
    foreach ($events as $event) {
246
247
248
249
250
        $eventold = calendar_event::load($event);
        $eventold->delete();
    }

    $DB->delete_records('quiz_overrides', array('id' => $overrideid));
251
    cache::make('mod_quiz', 'overrides')->delete($cachekey);
252

253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
    if ($log) {
        // Set the common parameters for one of the events we will be triggering.
        $params = array(
            'objectid' => $override->id,
            'context' => context_module::instance($quiz->cmid),
            'other' => array(
                'quizid' => $override->quiz
            )
        );
        // Determine which override deleted event to fire.
        if (!empty($override->userid)) {
            $params['relateduserid'] = $override->userid;
            $event = \mod_quiz\event\user_override_deleted::create($params);
        } else {
            $params['other']['groupid'] = $override->groupid;
            $event = \mod_quiz\event\group_override_deleted::create($params);
        }
270

271
272
273
274
        // Trigger the override deleted event.
        $event->add_record_snapshot('quiz_overrides', $override);
        $event->trigger();
    }
275

276
277
278
279
280
281
282
    return true;
}

/**
 * Deletes all quiz overrides from the database and clears any corresponding calendar events
 *
 * @param object $quiz The quiz object.
283
 * @param bool $log Whether to trigger logs.
284
 */
285
function quiz_delete_all_overrides($quiz, $log = true) {
286
287
288
289
    global $DB;

    $overrides = $DB->get_records('quiz_overrides', array('quiz' => $quiz->id), 'id');
    foreach ($overrides as $override) {
290
        quiz_delete_override($quiz, $override->id, $log);
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
    }
}

/**
 * Updates a quiz object with override information for a user.
 *
 * Algorithm:  For each quiz setting, if there is a matching user-specific override,
 *   then use that otherwise, if there are group-specific overrides, return the most
 *   lenient combination of them.  If neither applies, leave the quiz setting unchanged.
 *
 *   Special case: if there is more than one password that applies to the user, then
 *   quiz->extrapasswords will contain an array of strings giving the remaining
 *   passwords.
 *
 * @param object $quiz The quiz object.
306
 * @param int $userid The userid.
307
308
309
310
311
 * @return object $quiz The updated quiz object.
 */
function quiz_update_effective_access($quiz, $userid) {
    global $DB;

312
    // Check for user override.
313
314
315
    $override = $DB->get_record('quiz_overrides', array('quiz' => $quiz->id, 'userid' => $userid));

    if (!$override) {
316
        $override = new stdClass();
317
318
319
320
321
322
323
        $override->timeopen = null;
        $override->timeclose = null;
        $override->timelimit = null;
        $override->attempts = null;
        $override->password = null;
    }

324
    // Check for group overrides.
325
326
    $groupings = groups_get_user_groups($quiz->course, $userid);

327
    if (!empty($groupings[0])) {
328
        // Select all overrides that apply to the User's groups.
329
        list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0]));
330
331
        $sql = "SELECT * FROM {quiz_overrides}
                WHERE groupid $extra AND quiz = ?";
Tim Hunt's avatar
Tim Hunt committed
332
        $params[] = $quiz->id;
333
334
        $records = $DB->get_records_sql($sql, $params);

335
        // Combine the overrides.
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
        $opens = array();
        $closes = array();
        $limits = array();
        $attempts = array();
        $passwords = array();

        foreach ($records as $gpoverride) {
            if (isset($gpoverride->timeopen)) {
                $opens[] = $gpoverride->timeopen;
            }
            if (isset($gpoverride->timeclose)) {
                $closes[] = $gpoverride->timeclose;
            }
            if (isset($gpoverride->timelimit)) {
                $limits[] = $gpoverride->timelimit;
            }
            if (isset($gpoverride->attempts)) {
                $attempts[] = $gpoverride->attempts;
            }
            if (isset($gpoverride->password)) {
                $passwords[] = $gpoverride->password;
            }
        }
359
        // If there is a user override for a setting, ignore the group override.
360
        if (is_null($override->timeopen) && count($opens)) {
361
            $override->timeopen = min($opens);
362
363
        }
        if (is_null($override->timeclose) && count($closes)) {
364
365
366
367
368
            if (in_array(0, $closes)) {
                $override->timeclose = 0;
            } else {
                $override->timeclose = max($closes);
            }
369
370
        }
        if (is_null($override->timelimit) && count($limits)) {
371
372
373
374
375
            if (in_array(0, $limits)) {
                $override->timelimit = 0;
            } else {
                $override->timelimit = max($limits);
            }
376
377
        }
        if (is_null($override->attempts) && count($attempts)) {
378
379
380
381
382
            if (in_array(0, $attempts)) {
                $override->attempts = 0;
            } else {
                $override->attempts = max($attempts);
            }
383
384
        }
        if (is_null($override->password) && count($passwords)) {
385
            $override->password = array_shift($passwords);
386
            if (count($passwords)) {
387
                $override->extrapasswords = $passwords;
388
389
390
391
392
            }
        }

    }

393
    // Merge with quiz defaults.
394
    $keys = array('timeopen', 'timeclose', 'timelimit', 'attempts', 'password', 'extrapasswords');
395
396
397
398
399
400
401
402
403
    foreach ($keys as $key) {
        if (isset($override->{$key})) {
            $quiz->{$key} = $override->{$key};
        }
    }

    return $quiz;
}

404
405
/**
 * Delete all the attempts belonging to a quiz.
406
407
 *
 * @param object $quiz The quiz object.
408
409
410
 */
function quiz_delete_all_attempts($quiz) {
    global $CFG, $DB;
411
412
    require_once($CFG->dirroot . '/mod/quiz/locallib.php');
    question_engine::delete_questions_usage_by_activities(new qubaids_for_quiz($quiz->id));
413
414
    $DB->delete_records('quiz_attempts', array('quiz' => $quiz->id));
    $DB->delete_records('quiz_grades', array('quiz' => $quiz->id));
415
416
}

417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
/**
 * Delete all the attempts belonging to a user in a particular quiz.
 *
 * @param object $quiz The quiz object.
 * @param object $user The user object.
 */
function quiz_delete_user_attempts($quiz, $user) {
    global $CFG, $DB;
    require_once($CFG->dirroot . '/mod/quiz/locallib.php');
    question_engine::delete_questions_usage_by_activities(new qubaids_for_quiz_user($quiz->get_quizid(), $user->id));
    $params = [
        'quiz' => $quiz->get_quizid(),
        'userid' => $user->id,
    ];
    $DB->delete_records('quiz_attempts', $params);
    $DB->delete_records('quiz_grades', $params);
}

435
436
437
438
/**
 * Get the best current grade for a particular user in a quiz.
 *
 * @param object $quiz the quiz settings.
439
 * @param int $userid the id of the user.
440
 * @return float the user's current grade for this quiz, or null if this user does
441
442
443
444
 * not have a grade on this quiz.
 */
function quiz_get_best_grade($quiz, $userid) {
    global $DB;
445
446
    $grade = $DB->get_field('quiz_grades', 'grade',
            array('quiz' => $quiz->id, 'userid' => $userid));
447

448
    // Need to detect errors/no result, without catching 0 grades.
449
450
451
452
453
454
455
456
457
458
459
460
461
    if ($grade === false) {
        return null;
    }

    return $grade + 0; // Convert to number.
}

/**
 * Is this a graded quiz? If this method returns true, you can assume that
 * $quiz->grade and $quiz->sumgrades are non-zero (for example, if you want to
 * divide by them).
 *
 * @param object $quiz a row from the quiz table.
462
 * @return bool whether this is a graded quiz.
463
464
465
466
467
 */
function quiz_has_grades($quiz) {
    return $quiz->grade >= 0.000005 && $quiz->sumgrades >= 0.000005;
}

468
469
470
471
472
473
474
475
476
477
/**
 * Does this quiz allow multiple tries?
 *
 * @return bool
 */
function quiz_allows_multiple_tries($quiz) {
    $bt = question_engine::get_behaviour_type($quiz->preferredbehaviour);
    return $bt->allows_multiple_submitted_responses();
}

478
479
480
481
482
483
484
485
486
487
488
489
490
/**
 * Return a small object with summary information about what a
 * user has done with a given particular instance of this module
 * Used for user activity reports.
 * $return->time = the time they did it
 * $return->info = a short text description
 *
 * @param object $course
 * @param object $user
 * @param object $mod
 * @param object $quiz
 * @return object|null
 */
491
function quiz_user_outline($course, $user, $mod, $quiz) {
492
    global $DB, $CFG;
493
    require_once($CFG->libdir . '/gradelib.php');
494
495
496
497
498
499
    $grades = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id, $user->id);

    if (empty($grades->items[0]->grades)) {
        return null;
    } else {
        $grade = reset($grades->items[0]->grades);
500
501
    }

502
    $result = new stdClass();
503
504
505
    // If the user can't see hidden grades, don't return that information.
    $gitem = grade_item::fetch(array('id' => $grades->items[0]->id));
    if (!$gitem->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) {
506
        $result->info = get_string('gradenoun') . ': ' . $grade->str_long_grade;
507
    } else {
508
        $result->info = get_string('gradenoun') . ': ' . get_string('hidden', 'grades');
509
    }
510

511
    $result->time = grade_get_date_for_user_grade($grade, $user);
512

513
    return $result;
514
}
515

516
517
518
519
520
521
522
523
524
525
/**
 * Print a detailed representation of what a  user has done with
 * a given particular instance of this module, for user activity reports.
 *
 * @param object $course
 * @param object $user
 * @param object $mod
 * @param object $quiz
 * @return bool
 */
526
function quiz_user_complete($course, $user, $mod, $quiz) {
527
    global $DB, $CFG, $OUTPUT;
528
    require_once($CFG->libdir . '/gradelib.php');
529
    require_once($CFG->dirroot . '/mod/quiz/locallib.php');
530

531
532
533
    $grades = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id, $user->id);
    if (!empty($grades->items[0]->grades)) {
        $grade = reset($grades->items[0]->grades);
534
535
536
        // If the user can't see hidden grades, don't return that information.
        $gitem = grade_item::fetch(array('id' => $grades->items[0]->id));
        if (!$gitem->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) {
537
            echo $OUTPUT->container(get_string('gradenoun').': '.$grade->str_long_grade);
538
539
540
541
            if ($grade->str_feedback) {
                echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback);
            }
        } else {
542
            echo $OUTPUT->container(get_string('gradenoun') . ': ' . get_string('hidden', 'grades'));
543
544
545
            if ($grade->str_feedback) {
                echo $OUTPUT->container(get_string('feedback').': '.get_string('hidden', 'grades'));
            }
546
547
        }
    }
548

549
550
    if ($attempts = $DB->get_records('quiz_attempts',
            array('userid' => $user->id, 'quiz' => $quiz->id), 'attempt')) {
551
        foreach ($attempts as $attempt) {
552
            echo get_string('attempt', 'quiz', $attempt->attempt) . ': ';
553
554
            if ($attempt->state != quiz_attempt::FINISHED) {
                echo quiz_attempt_state_name($attempt->state);
555
            } else {
556
557
558
559
560
561
562
563
564
565
566
567
568
                if (!isset($gitem)) {
                    if (!empty($grades->items[0]->grades)) {
                        $gitem = grade_item::fetch(array('id' => $grades->items[0]->id));
                    } else {
                        $gitem = new stdClass();
                        $gitem->hidden = true;
                    }
                }
                if (!$gitem->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) {
                    echo quiz_format_grade($quiz, $attempt->sumgrades) . '/' . quiz_format_grade($quiz, $quiz->sumgrades);
                } else {
                    echo get_string('hidden', 'grades');
                }
569
                echo ' - '.userdate($attempt->timefinish).'<br />';
570
571
572
            }
        }
    } else {
573
        print_string('noattempts', 'quiz');
574
575
    }

576
577
578
579
    return true;
}


580
/**
581
 * @param int|array $quizids A quiz ID, or an array of quiz IDs.
582
 * @param int $userid the userid.
583
 * @param string $status 'all', 'finished' or 'unfinished' to control
584
 * @param bool $includepreviews
585
586
 * @return an array of all the user's attempts at this quiz. Returns an empty
 *      array if there are none.
587
 */
588
function quiz_get_user_attempts($quizids, $userid, $status = 'finished', $includepreviews = false) {
589
590
591
592
593
594
    global $DB, $CFG;
    // TODO MDL-33071 it is very annoying to have to included all of locallib.php
    // just to get the quiz_attempt::FINISHED constants, but I will try to sort
    // that out properly for Moodle 2.4. For now, I will just do a quick fix for
    // MDL-33048.
    require_once($CFG->dirroot . '/mod/quiz/locallib.php');
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614

    $params = array();
    switch ($status) {
        case 'all':
            $statuscondition = '';
            break;

        case 'finished':
            $statuscondition = ' AND state IN (:state1, :state2)';
            $params['state1'] = quiz_attempt::FINISHED;
            $params['state2'] = quiz_attempt::ABANDONED;
            break;

        case 'unfinished':
            $statuscondition = ' AND state IN (:state1, :state2)';
            $params['state1'] = quiz_attempt::IN_PROGRESS;
            $params['state2'] = quiz_attempt::OVERDUE;
            break;
    }

615
616
617
618
619
    $quizids = (array) $quizids;
    list($insql, $inparams) = $DB->get_in_or_equal($quizids, SQL_PARAMS_NAMED);
    $params += $inparams;
    $params['userid'] = $userid;

620
621
622
623
    $previewclause = '';
    if (!$includepreviews) {
        $previewclause = ' AND preview = 0';
    }
624

625
    return $DB->get_records_select('quiz_attempts',
626
            "quiz $insql AND userid = :userid" . $previewclause . $statuscondition,
627
            $params, 'quiz, attempt ASC');
628
}
moodler's avatar
moodler committed
629

630
631
632
633
634
/**
 * Return grade for given user or all users.
 *
 * @param int $quizid id of quiz
 * @param int $userid optional user id, 0 means all users
635
636
 * @return array array of grades, false if none. These are raw grades. They should
 * be processed with quiz_format_grade for display.
637
 */
638
function quiz_get_user_grades($quiz, $userid = 0) {
639
    global $CFG, $DB;
640

641
    $params = array($quiz->id);
642
    $usertest = '';
643
644
    if ($userid) {
        $params[] = $userid;
645
646
        $usertest = 'AND u.id = ?';
    }
647
    return $DB->get_records_sql("
648
649
650
651
652
653
654
655
656
657
658
            SELECT
                u.id,
                u.id AS userid,
                qg.grade AS rawgrade,
                qg.timemodified AS dategraded,
                MAX(qa.timefinish) AS datesubmitted

            FROM {user} u
            JOIN {quiz_grades} qg ON u.id = qg.userid
            JOIN {quiz_attempts} qa ON qa.quiz = qg.quiz AND qa.userid = u.id

659
660
661
            WHERE qg.quiz = ?
            $usertest
            GROUP BY u.id, qg.grade, qg.timemodified", $params);
662
663
}

664
665
666
667
668
/**
 * Round a grade to to the correct number of decimal places, and format it for display.
 *
 * @param object $quiz The quiz table row, only $quiz->decimalpoints is used.
 * @param float $grade The grade to round.
669
 * @return float
670
671
 */
function quiz_format_grade($quiz, $grade) {
672
673
674
    if (is_null($grade)) {
        return get_string('notyetgraded', 'quiz');
    }
675
676
677
    return format_float($grade, $quiz->decimalpoints);
}

678
/**
679
 * Determine the correct number of decimal places required to format a grade.
680
681
 *
 * @param object $quiz The quiz table row, only $quiz->decimalpoints is used.
682
 * @return integer
683
 */
684
function quiz_get_grade_format($quiz) {
685
686
687
    if (empty($quiz->questiondecimalpoints)) {
        $quiz->questiondecimalpoints = -1;
    }
688

689
    if ($quiz->questiondecimalpoints == -1) {
690
        return $quiz->decimalpoints;
691
    }
692
693
694
695
696
697
698
699
700
701
702
703
704

    return $quiz->questiondecimalpoints;
}

/**
 * Round a grade to the correct number of decimal places, and format it for display.
 *
 * @param object $quiz The quiz table row, only $quiz->decimalpoints is used.
 * @param float $grade The grade to round.
 * @return float
 */
function quiz_format_question_grade($quiz, $grade) {
    return format_float($grade, quiz_get_grade_format($quiz));
705
706
}

707
708
709
/**
 * Update grades in central gradebook
 *
710
 * @category grade
711
712
 * @param object $quiz the quiz settings.
 * @param int $userid specific user only, 0 means all users.
713
 * @param bool $nullifnone If a single user is specified and $nullifnone is true a grade item with a null rawgrade will be inserted
714
 */
715
function quiz_update_grades($quiz, $userid = 0, $nullifnone = true) {
716
    global $CFG, $DB;
717
    require_once($CFG->libdir . '/gradelib.php');
moodler's avatar
moodler committed
718

719
720
    if ($quiz->grade == 0) {
        quiz_grade_item_update($quiz);
721

722
723
    } else if ($grades = quiz_get_user_grades($quiz, $userid)) {
        quiz_grade_item_update($quiz, $grades);
724

725
    } else if ($userid && $nullifnone) {
726
        $grade = new stdClass();
727
728
        $grade->userid = $userid;
        $grade->rawgrade = null;
729
        quiz_grade_item_update($quiz, $grade);
730
731

    } else {
732
733
734
        quiz_grade_item_update($quiz);
    }
}
735

736
/**
737
 * Create or update the grade item for given quiz
738
 *
739
 * @category grade
740
 * @param object $quiz object with extra cmidnumber
741
 * @param mixed $grades optional array/object of grade(s); 'reset' means reset grades in gradebook
742
743
 * @return int 0 if ok, error code otherwise
 */
744
function quiz_grade_item_update($quiz, $grades = null) {
745
    global $CFG, $OUTPUT;
746
    require_once($CFG->dirroot . '/mod/quiz/locallib.php');
747
    require_once($CFG->libdir . '/gradelib.php');
748

749
    if (property_exists($quiz, 'cmidnumber')) { // May not be always present.
750
        $params = array('itemname' => $quiz->name, 'idnumber' => $quiz->cmidnumber);
751
    } else {
752
        $params = array('itemname' => $quiz->name);
753
754
755
756
757
758
759
760
761
762
763
    }

    if ($quiz->grade > 0) {
        $params['gradetype'] = GRADE_TYPE_VALUE;
        $params['grademax']  = $quiz->grade;
        $params['grademin']  = 0;

    } else {
        $params['gradetype'] = GRADE_TYPE_NONE;
    }

764
    // What this is trying to do:
765
766
767
768
769
770
    // 1. If the quiz is set to not show grades while the quiz is still open,
    //    and is set to show grades after the quiz is closed, then create the
    //    grade_item with a show-after date that is the quiz close date.
    // 2. If the quiz is set to not show grades at either of those times,
    //    create the grade_item as hidden.
    // 3. If the quiz is set to show grades, create the grade_item visible.
771
772
773
774
    $openreviewoptions = mod_quiz_display_options::make_from_quiz($quiz,
            mod_quiz_display_options::LATER_WHILE_OPEN);
    $closedreviewoptions = mod_quiz_display_options::make_from_quiz($quiz,
            mod_quiz_display_options::AFTER_CLOSE);
775
776
    if ($openreviewoptions->marks < question_display_options::MARK_AND_MAX &&
            $closedreviewoptions->marks < question_display_options::MARK_AND_MAX) {
777
778
        $params['hidden'] = 1;

779
780
    } else if ($openreviewoptions->marks < question_display_options::MARK_AND_MAX &&
            $closedreviewoptions->marks >= question_display_options::MARK_AND_MAX) {
781
782
783
784
785
786
787
        if ($quiz->timeclose) {
            $params['hidden'] = $quiz->timeclose;
        } else {
            $params['hidden'] = 1;
        }

    } else {
788
        // Either
789
        // a) both open and closed enabled
790
        // b) open enabled, closed disabled - we can not "hide after",
791
        //    grades are kept visible even after closing.
792
793
794
        $params['hidden'] = 0;
    }

795
796
797
    if (!$params['hidden']) {
        // If the grade item is not hidden by the quiz logic, then we need to
        // hide it if the quiz is hidden from students.
798
799
800
801
802
803
804
        if (property_exists($quiz, 'visible')) {
            // Saving the quiz form, and cm not yet updated in the database.
            $params['hidden'] = !$quiz->visible;
        } else {
            $cm = get_coursemodule_from_instance('quiz', $quiz->id);
            $params['hidden'] = !$cm->visible;
        }
805
806
    }

807
808
    if ($grades  === 'reset') {
        $params['reset'] = true;
809
        $grades = null;
810
    }
811

812
    $gradebook_grades = grade_get_grades($quiz->course, 'mod', 'quiz', $quiz->id);
813
814
815
    if (!empty($gradebook_grades->items)) {
        $grade_item = $gradebook_grades->items[0];
        if ($grade_item->locked) {
816
            // NOTE: this is an extremely nasty hack! It is not a bug if this confirmation fails badly. --skodak.
817
818
            $confirm_regrade = optional_param('confirm_regrade', 0, PARAM_INT);
            if (!$confirm_regrade) {
819
820
821
822
823
824
825
826
827
828
829
830
831
                if (!AJAX_SCRIPT) {
                    $message = get_string('gradeitemislocked', 'grades');
                    $back_link = $CFG->wwwroot . '/mod/quiz/report.php?q=' . $quiz->id .
                            '&amp;mode=overview';
                    $regrade_link = qualified_me() . '&amp;confirm_regrade=1';
                    echo $OUTPUT->box_start('generalbox', 'notice');
                    echo '<p>'. $message .'</p>';
                    echo $OUTPUT->container_start('buttons');
                    echo $OUTPUT->single_button($regrade_link, get_string('regradeanyway', 'grades'));
                    echo $OUTPUT->single_button($back_link,  get_string('cancel'));
                    echo $OUTPUT->container_end();
                    echo $OUTPUT->box_end();
                }
832
833
                return GRADE_UPDATE_ITEM_LOCKED;
            }
834
835
        }
    }
836

837
    return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0, $grades, $params);
838
839
840
841
842
}

/**
 * Delete grade item for given quiz
 *
843
 * @category grade
844
845
846
847
848
 * @param object $quiz object
 * @return object quiz
 */
function quiz_grade_item_delete($quiz) {
    global $CFG;
849
    require_once($CFG->libdir . '/gradelib.php');
850

851
852
    return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0,
            null, array('deleted' => 1));
853
854
}

855
856
857
858
859
860
861
862
/**
 * This standard function will check all instances of this module
 * and make sure there are up-to-date events created for each of them.
 * If courseid = 0, then every quiz event in the site is checked, else
 * only quiz events belonging to the course specified are checked.
 * This function is used, in its new format, by restore_refresh_events()
 *
 * @param int $courseid
863
864
 * @param int|stdClass $instance Quiz module instance or ID.
 * @param int|stdClass $cm Course module object or ID (not used in this module).
865
866
 * @return bool
 */
867
function quiz_refresh_events($courseid = 0, $instance = null, $cm = null) {
868
    global $DB;
moodler's avatar
moodler committed
869

870
871
872
873
874
875
876
877
878
    // If we have instance information then we can just update the one event instead of updating all events.
    if (isset($instance)) {
        if (!is_object($instance)) {
            $instance = $DB->get_record('quiz', array('id' => $instance), '*', MUST_EXIST);
        }
        quiz_update_events($instance);
        return true;
    }

moodler's avatar
moodler committed
879
    if ($courseid == 0) {
880
        if (!$quizzes = $DB->get_records('quiz')) {
moodler's avatar
moodler committed
881
882
883
            return true;
        }
    } else {
884
        if (!$quizzes = $DB->get_records('quiz', array('course' => $courseid))) {
moodler's avatar
moodler committed
885
886
887
            return true;
        }
    }
888

moodler's avatar
moodler committed
889
    foreach ($quizzes as $quiz) {
890
        quiz_update_events($quiz);
moodler's avatar
moodler committed
891
    }
892

moodler's avatar
moodler committed
893
894
895
    return true;
}

896
897
898
/**
 * Returns all quiz graded users since a given time for specified quiz
 */
899
900
function quiz_get_recent_mod_activity(&$activities, &$index, $timestart,
        $courseid, $cmid, $userid = 0, $groupid = 0) {
901
    global $CFG, $USER, $DB;
902
    require_once($CFG->dirroot . '/mod/quiz/locallib.php');
903

904
    $course = get_course($courseid);
905
    $modinfo = get_fast_modinfo($course);
906

907
    $cm = $modinfo->cms[$cmid];
908
    $quiz = $DB->get_record('quiz', array('id' => $cm->instance));
909

910
    if ($userid) {
911
912
        $userselect = "AND u.id = :userid";
        $params['userid'] = $userid;
913
    } else {
914
        $userselect = '';
915
    }
916

917
    if ($groupid) {
918
919
920
        $groupselect = 'AND gm.groupid = :groupid';
        $groupjoin   = 'JOIN {groups_members} gm ON  gm.userid=u.id';
        $params['groupid'] = $groupid;
921
    } else {
922
923
924
925
        $groupselect = '';
        $groupjoin   = '';
    }

926
927
928
    $params['timestart'] = $timestart;
    $params['quizid'] = $quiz->id;

929
    $userfieldsapi = \core_user\fields::for_userpic();
930
    $ufields = $userfieldsapi->get_sql('u', false, '', 'useridagain', false)->selects;
931
932
    if (!$attempts = $DB->get_records_sql("
              SELECT qa.*,
933
                     {$ufields}
934
935
936
937
938
939
940
941
942
943
944
945
                FROM {quiz_attempts} qa
                     JOIN {user} u ON u.id = qa.userid
                     $groupjoin
               WHERE qa.timefinish > :timestart
                 AND qa.quiz = :quizid
                 AND qa.preview = 0
                     $userselect
                     $groupselect
            ORDER BY qa.timefinish ASC", $params)) {
        return;
    }

946
    $context         = context_module::instance($cm->id);
947
948
    $accessallgroups = has_capability('moodle/site:accessallgroups', $context);
    $viewfullnames   = has_capability('moodle/site:viewfullnames', $context);
949
    $grader          = has_capability('mod/quiz:viewreports', $context);
950
    $groupmode       = groups_get_activity_groupmode($cm, $course);
951

952
    $usersgroups = null;
953
    $aname = format_string($cm->name, true);
954
955
956
    foreach ($attempts as $attempt) {
        if ($attempt->userid != $USER->id) {
            if (!$grader) {
957
                // Grade permission required.
958
959
                continue;
            }
960

961
            if ($groupmode == SEPARATEGROUPS and !$accessallgroups) {
962
963
964
965
                $usersgroups = groups_get_all_groups($course->id,
                        $attempt->userid, $cm->groupingid);
                $usersgroups = array_keys($usersgroups);
                if (!array_intersect($usersgroups, $modinfo->get_groups($cm->groupingid))) {
966
967
968
                    continue;
                }
            }
969
970
        }

971
        $options = quiz_get_review_options($quiz, $attempt, $context);
972

973
        $tmpactivity = new stdClass();
974

975
976
977
978
979
        $tmpactivity->type       = 'quiz';
        $tmpactivity->cmid       = $cm->id;
        $tmpactivity->name       = $aname;
        $tmpactivity->sectionnum = $cm->sectionnum;
        $tmpactivity->timestamp  = $attempt->timefinish;
980

981
        $tmpactivity->content = new stdClass();
982
983
        $tmpactivity->content->attemptid = $attempt->id;
        $tmpactivity->content->attempt   = $attempt->attempt;
984
        if (quiz_has_grades($quiz) && $options->marks >= question_display_options::MARK_AND_MAX) {
985
986
987
988
989
990
            $tmpactivity->content->sumgrades = quiz_format_grade($quiz, $attempt->sumgrades);
            $tmpactivity->content->maxgrade  = quiz_format_grade($quiz, $quiz->sumgrades);
        } else {
            $tmpactivity->content->sumgrades = null;
            $tmpactivity->content->maxgrade  = null;
        }
991

992
        $tmpactivity->user = user_picture::unalias($attempt, null, 'useridagain');
993
        $tmpactivity->user->fullname  = fullname($tmpactivity->user, $viewfullnames);
994

995
        $activities[$index++] = $tmpactivity;
996
997
998
    }
}

999
function quiz_print_recent_mod_activity($activity, $courseid, $detail, $modnames) {