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
use mod_quiz\local\reports\report_base;
18
use mod_quiz\quiz_attempt;
19
 
20
defined('MOODLE_INTERNAL') || die();
21
 
22
require_once($CFG->dirroot . '/mod/quiz/report/grading/gradingsettings_form.php');
23
 
24
/**
25
 * Quiz report to help teachers manually grade questions that need it.
26
 *
27
 * This report basically provides two screens:
28
 * - List question that might need manual grading (or optionally all questions).
29
 * - Provide an efficient UI to grade all attempts at a particular question.
30
 *
31
 * @copyright 2006 Gustav Delius
32
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
33
 */
34
class quiz_grading_report extends report_base {
35
    const DEFAULT_PAGE_SIZE = 5;
36
    const DEFAULT_ORDER = 'random';
37
 
38
    /** @var string Positive integer regular expression. */
39
    const REGEX_POSITIVE_INT = '/^[1-9]\d*$/';
40
 
41
    /** @var array URL parameters for what is being displayed when grading. */
42
    protected $viewoptions = [];
43
 
44
    /** @var int the current group, 0 if none, or NO_GROUPS_ALLOWED. */
45
    protected $currentgroup;
46
 
47
    /** @var array from quiz_report_get_significant_questions. */
48
    protected $questions;
49
 
50
    /** @var stdClass the course settings. */
51
    protected $course;
52
 
53
    /** @var stdClass the course_module settings. */
54
    protected $cm;
55
 
56
    /** @var stdClass the quiz settings. */
57
    protected $quiz;
58
 
59
    /** @var context the quiz context. */
60
    protected $context;
61
 
62
    /** @var quiz_grading_renderer Renderer of Quiz Grading. */
63
    protected $renderer;
64
 
65
    /** @var string fragment of SQL code to restrict to the relevant users. */
66
    protected $userssql;
67
 
68
    /** @var array extra user fields. */
69
    protected $extrauserfields = [];
70
 
71
    public function display($quiz, $cm, $course) {
72
 
73
        $this->quiz = $quiz;
74
        $this->cm = $cm;
75
        $this->course = $course;
76
 
77
        // Get the URL options.
78
        $slot = optional_param('slot', null, PARAM_INT);
79
        $questionid = optional_param('qid', null, PARAM_INT);
80
        $grade = optional_param('grade', null, PARAM_ALPHA);
81
 
82
        $includeauto = optional_param('includeauto', false, PARAM_BOOL);
83
        if (!in_array($grade, ['all', 'needsgrading', 'autograded', 'manuallygraded'])) {
84
            $grade = null;
85
        }
86
        $pagesize = optional_param('pagesize',
87
                get_user_preferences('quiz_grading_pagesize', self::DEFAULT_PAGE_SIZE),
88
                PARAM_INT);
89
        $page = optional_param('page', 0, PARAM_INT);
90
        $order = optional_param('order',
91
                get_user_preferences('quiz_grading_order', self::DEFAULT_ORDER),
92
                PARAM_ALPHAEXT);
93
 
94
        // Assemble the options required to reload this page.
95
        $optparams = ['includeauto', 'page'];
96
        foreach ($optparams as $param) {
97
            if ($$param) {
98
                $this->viewoptions[$param] = $$param;
99
            }
100
        }
101
        if (!data_submitted() && !preg_match(self::REGEX_POSITIVE_INT, $pagesize)) {
102
            // We only validate if the user accesses the page via a cleaned-up GET URL here.
103
            throw new moodle_exception('invalidpagesize');
104
        }
105
        if ($pagesize != self::DEFAULT_PAGE_SIZE) {
106
            $this->viewoptions['pagesize'] = $pagesize;
107
        }
108
        if ($order != self::DEFAULT_ORDER) {
109
            $this->viewoptions['order'] = $order;
110
        }
111
 
112
        // Check permissions.
113
        $this->context = context_module::instance($this->cm->id);
114
        require_capability('mod/quiz:grade', $this->context);
115
        $shownames = has_capability('quiz/grading:viewstudentnames', $this->context);
116
        // Whether the current user can see custom user fields.
117
        $showcustomfields = has_capability('quiz/grading:viewidnumber', $this->context);
118
        $userfieldsapi = \core_user\fields::for_identity($this->context)->with_name();
119
        $customfields = [];
120
        foreach ($userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]) as $field) {
121
            $customfields[] = $field;
122
        }
123
        // Validate order.
124
        $orderoptions = array_merge(['random', 'date', 'studentfirstname', 'studentlastname'], $customfields);
125
        if (!in_array($order, $orderoptions)) {
126
            $order = self::DEFAULT_ORDER;
127
        } else if (!$shownames && ($order == 'studentfirstname' || $order == 'studentlastname')) {
128
            $order = self::DEFAULT_ORDER;
129
        } else if (!$showcustomfields && in_array($order, $customfields)) {
130
            $order = self::DEFAULT_ORDER;
131
        }
132
        if ($order == 'random') {
133
            $page = 0;
134
        }
135
 
136
        // Get the list of questions in this quiz.
137
        $this->questions = quiz_report_get_significant_questions($quiz);
138
        if ($slot && !array_key_exists($slot, $this->questions)) {
139
            throw new moodle_exception('unknownquestion', 'quiz_grading');
140
        }
141
 
142
        // Process any submitted data.
143
        if ($data = data_submitted() && confirm_sesskey() && $this->validate_submitted_marks()) {
144
            // Changes done to handle attempts being missed from grading due to redirecting to new page.
145
            $attemptsgraded = $this->process_submitted_data();
146
 
147
            $nextpagenumber = $page + 1;
148
            // If attempts need grading and one or more have now been graded, then page number should remain the same.
149
            if ($grade == 'needsgrading' && $attemptsgraded) {
150
                $nextpagenumber = $page;
151
            }
152
 
153
            redirect($this->grade_question_url($slot, $questionid, $grade, $nextpagenumber));
154
        }
155
 
156
        // Get the group, and the list of significant users.
157
        $this->currentgroup = $this->get_current_group($cm, $course, $this->context);
158
        if ($this->currentgroup == self::NO_GROUPS_ALLOWED) {
159
            $this->userssql = [];
160
        } else {
161
            $this->userssql = get_enrolled_sql($this->context,
162
                    ['mod/quiz:reviewmyattempts', 'mod/quiz:attempt'], $this->currentgroup);
163
        }
164
 
165
        $hasquestions = quiz_has_questions($this->quiz->id);
166
        if (!$hasquestions) {
167
            $this->print_header_and_tabs($cm, $course, $quiz, 'grading');
168
            echo $this->renderer->render_quiz_no_question_notification($quiz, $cm, $this->context);
169
            return true;
170
        }
171
 
172
        if (!$slot) {
173
            $this->display_index($includeauto);
174
            return true;
175
        }
176
 
177
        // Display the grading UI for one question.
178
 
179
        // Make sure there is something to do.
180
        $counts = null;
181
        $statecounts = $this->get_question_state_summary([$slot]);
182
        foreach ($statecounts as $record) {
183
            if ($record->questionid == $questionid) {
184
                $counts = $record;
185
                break;
186
            }
187
        }
188
 
189
        // If not, redirect back to the list.
190
        if (!$counts || $counts->$grade == 0) {
191
            redirect($this->list_questions_url(), get_string('alldoneredirecting', 'quiz_grading'));
192
        }
193
 
194
        $this->display_grading_interface($slot, $questionid, $grade,
195
                $pagesize, $page, $shownames, $showcustomfields, $order, $counts);
196
        return true;
197
    }
198
 
199
    /**
200
     * Get the JOIN conditions needed so we only show attempts by relevant users.
201
     *
202
     * @return qubaid_join
203
     */
204
    protected function get_qubaids_condition() {
205
 
206
        $where = "quiza.quiz = :mangrquizid AND
207
                quiza.preview = 0 AND
208
                quiza.state = :statefinished";
209
        $params = ['mangrquizid' => $this->cm->instance, 'statefinished' => quiz_attempt::FINISHED];
210
 
211
        $usersjoin = '';
212
        $currentgroup = groups_get_activity_group($this->cm, true);
213
        $enrolleduserscount = count_enrolled_users($this->context,
214
                ['mod/quiz:reviewmyattempts', 'mod/quiz:attempt'], $currentgroup);
215
        if ($currentgroup) {
216
            $userssql = get_enrolled_sql($this->context,
217
                    ['mod/quiz:reviewmyattempts', 'mod/quiz:attempt'], $currentgroup);
218
            if ($enrolleduserscount < 1) {
219
                $where .= ' AND quiza.userid = 0';
220
            } else {
221
                $usersjoin = "JOIN ({$userssql[0]}) AS enr ON quiza.userid = enr.id";
222
                $params += $userssql[1];
223
            }
224
        }
225
 
226
        return new qubaid_join("{quiz_attempts} quiza $usersjoin ", 'quiza.uniqueid', $where, $params);
227
    }
228
 
229
    /**
230
     * Load the quiz_attempts rows corresponding to a list of question_usage ids.
231
     *
232
     * @param int[] $qubaids the question_usage ids of the quiz_attempts to load.
233
     * @return array quiz attempts, with added user name fields.
234
     */
235
    protected function load_attempts_by_usage_ids($qubaids) {
236
        global $DB;
237
 
238
        list($asql, $params) = $DB->get_in_or_equal($qubaids);
239
        $params[] = quiz_attempt::FINISHED;
240
        $params[] = $this->quiz->id;
241
 
242
        $fields = 'quiza.*, ';
243
        $userfieldsapi = \core_user\fields::for_identity($this->context)->with_name();
244
        $userfieldssql = $userfieldsapi->get_sql('u', false, '', 'userid', false);
245
        $fields .= $userfieldssql->selects;
246
        foreach ($userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]) as $userfield) {
247
            $this->extrauserfields[] = s($userfield);
248
        }
249
        $params = array_merge($userfieldssql->params, $params);
250
        $attemptsbyid = $DB->get_records_sql("
251
                SELECT $fields
252
                FROM {quiz_attempts} quiza
253
                JOIN {user} u ON u.id = quiza.userid
254
                {$userfieldssql->joins}
255
                WHERE quiza.uniqueid $asql AND quiza.state = ? AND quiza.quiz = ?",
256
                $params);
257
 
258
        $attempts = [];
259
        foreach ($attemptsbyid as $attempt) {
260
            $attempts[$attempt->uniqueid] = $attempt;
261
        }
262
        return $attempts;
263
    }
264
 
265
    /**
266
     * Get the URL of the front page of the report that lists all the questions.
267
     *
268
     * @return moodle_url the URL.
269
     */
270
    protected function base_url() {
271
        return new moodle_url('/mod/quiz/report.php',
272
                ['id' => $this->cm->id, 'mode' => 'grading']);
273
    }
274
 
275
    /**
276
     * Get the URL of the front page of the report that lists all the questions.
277
     *
278
     * @param bool $includeauto if not given, use the current setting, otherwise,
279
     *      force a particular value of includeauto in the URL.
280
     * @return moodle_url the URL.
281
     */
282
    protected function list_questions_url($includeauto = null) {
283
        $url = $this->base_url();
284
 
285
        $url->params($this->viewoptions);
286
 
287
        if (!is_null($includeauto)) {
288
            $url->param('includeauto', $includeauto);
289
        }
290
 
291
        return $url;
292
    }
293
 
294
    /**
295
     * Get the URL to grade a batch of question attempts.
296
     *
297
     * @param int $slot
298
     * @param int $questionid
299
     * @param string $grade
300
     * @param int|bool $page = true, link to current page. false = omit page.
301
     *      number = link to specific page.
302
     * @return moodle_url
303
     */
304
    protected function grade_question_url($slot, $questionid, $grade, $page = true) {
305
        $url = $this->base_url();
306
        $url->params(['slot' => $slot, 'qid' => $questionid, 'grade' => $grade]);
307
        $url->params($this->viewoptions);
308
 
309
        if (!$page) {
310
            $url->remove_params('page');
311
        } else if (is_integer($page)) {
312
            $url->param('page', $page);
313
        }
314
 
315
        return $url;
316
    }
317
 
318
    /**
319
     * Renders the contents of one cell of the table on the index view.
320
     *
321
     * @param stdClass $counts counts of different types of attempt for this slot.
322
     * @param string $type the type of count to format.
323
     * @param string $string get_string identifier for the grading link text, if required.
324
     * @param string $component get_string component identifier for the grading link text, if required.
325
     * @return string HTML.
326
     */
327
    protected function format_count_for_table($counts, $type, $string, $component) {
328
        $result = $counts->$type;
329
        if ($counts->$type > 0) {
330
            $gradeurl = $this->grade_question_url($counts->slot, $counts->questionid, $type);
331
            $result .= $this->renderer->render_grade_link($counts, $type, $string, $component, $gradeurl);
332
        }
333
        return $result;
334
    }
335
 
336
    /**
337
     * Display the report front page which summarises the number of attempts to grade.
338
     *
339
     * @param bool $includeauto whether to show automatically-graded questions.
340
     */
341
    protected function display_index($includeauto) {
342
        global $PAGE, $OUTPUT;
343
 
344
        $this->print_header_and_tabs($this->cm, $this->course, $this->quiz, 'grading');
345
 
346
        if ($groupmode = groups_get_activity_groupmode($this->cm)) {
347
            // Groups is being used.
348
            groups_print_activity_menu($this->cm, $this->list_questions_url());
349
        }
350
        // Get the current group for the user looking at the report.
351
        $currentgroup = $this->get_current_group($this->cm, $this->course, $this->context);
352
        if ($currentgroup == self::NO_GROUPS_ALLOWED) {
353
            echo $OUTPUT->notification(get_string('notingroup'));
354
            return;
355
        }
356
        $statecounts = $this->get_question_state_summary(array_keys($this->questions));
357
        if ($includeauto) {
358
            $linktext = get_string('hideautomaticallygraded', 'quiz_grading');
359
        } else {
360
            $linktext = get_string('alsoshowautomaticallygraded', 'quiz_grading');
361
        }
362
        echo $this->renderer->render_display_index_heading($linktext, $this->list_questions_url(!$includeauto));
363
        $data = [];
364
        $header = [];
365
 
366
        $header[] = get_string('qno', 'quiz_grading');
367
        $header[] = get_string('qtypeveryshort', 'question');
368
        $header[] = get_string('questionname', 'quiz_grading');
369
        $header[] = get_string('tograde', 'quiz_grading');
370
        $header[] = get_string('alreadygraded', 'quiz_grading');
371
        if ($includeauto) {
372
            $header[] = get_string('automaticallygraded', 'quiz_grading');
373
        }
374
        $header[] = get_string('total', 'quiz_grading');
375
 
376
        foreach ($statecounts as $counts) {
377
            if ($counts->all == 0) {
378
                continue;
379
            }
380
            if (!$includeauto && $counts->needsgrading == 0 && $counts->manuallygraded == 0) {
381
                continue;
382
            }
383
 
384
            $row = [];
385
 
11 efrain 386
            $row[] = $this->questions[$counts->slot]->displaynumber;
1 efrain 387
 
388
            $row[] = $PAGE->get_renderer('question', 'bank')->qtype_icon($this->questions[$counts->slot]->qtype);
389
 
390
            $row[] = format_string($counts->name);
391
 
392
            $row[] = $this->format_count_for_table($counts, 'needsgrading', 'gradeverb', 'moodle');
393
 
394
            $row[] = $this->format_count_for_table($counts, 'manuallygraded', 'updategrade', 'quiz_grading');
395
 
396
            if ($includeauto) {
397
                $row[] = $this->format_count_for_table($counts, 'autograded', 'updategrade', 'quiz_grading');
398
            }
399
 
400
            $row[] = $this->format_count_for_table($counts, 'all', 'gradeall', 'quiz_grading');
401
 
402
            $data[] = $row;
403
        }
404
        echo $this->renderer->render_questions_table($includeauto, $data, $header);
405
    }
406
 
407
    /**
408
     * Display the UI for grading attempts at one question.
409
     *
410
     * @param int $slot identifies which question to grade.
411
     * @param int $questionid identifies which question to grade.
412
     * @param string $grade type of attempts to grade.
413
     * @param int $pagesize number of questions to show per page.
414
     * @param int $page current page number.
415
     * @param bool $shownames whether student names should be shown.
416
     * @param bool $showcustomfields whether custom field values should be shown.
417
     * @param string $order preferred order of attempts.
418
     * @param stdClass $counts object that stores the number of each type of attempt.
419
     */
420
    protected function display_grading_interface($slot, $questionid, $grade,
421
            $pagesize, $page, $shownames, $showcustomfields, $order, $counts) {
422
 
423
        if ($pagesize * $page >= $counts->$grade) {
424
            $page = 0;
425
        }
426
 
427
        // Prepare the options form.
428
        $hidden = [
429
            'id' => $this->cm->id,
430
            'mode' => 'grading',
431
            'slot' => $slot,
432
            'qid' => $questionid,
433
            'page' => $page,
434
        ];
435
        if (array_key_exists('includeauto', $this->viewoptions)) {
436
            $hidden['includeauto'] = $this->viewoptions['includeauto'];
437
        }
438
        $mform = new quiz_grading_settings_form($hidden, $counts, $shownames, $showcustomfields, $this->context);
439
 
440
        // Tell the form the current settings.
441
        $settings = new stdClass();
442
        $settings->grade = $grade;
443
        $settings->pagesize = $pagesize;
444
        $settings->order = $order;
445
        $mform->set_data($settings);
446
 
447
        if ($mform->is_submitted()) {
448
            if ($mform->is_validated()) {
449
                // If the form was submitted and validated, save the user preferences, and
450
                // redirect to a cleaned-up GET URL.
451
                set_user_preference('quiz_grading_pagesize', $pagesize);
452
                set_user_preference('quiz_grading_order', $order);
453
                redirect($this->grade_question_url($slot, $questionid, $grade, $page));
454
            } else {
455
                // Set the pagesize back to the previous value, so the report page can continue the render
456
                // and the form can show the validation.
457
                $pagesize = get_user_preferences('quiz_grading_pagesize', self::DEFAULT_PAGE_SIZE);
458
            }
459
        }
460
 
461
        list($qubaids, $count) = $this->get_usage_ids_where_question_in_state(
462
                $grade, $slot, $questionid, $order, $page, $pagesize);
463
        $attempts = $this->load_attempts_by_usage_ids($qubaids);
464
 
465
        // Question info.
466
        $questioninfo = new stdClass();
467
        $questioninfo->number = $this->questions[$slot]->number;
468
        $questioninfo->questionname = format_string($counts->name);
469
 
470
        // Paging info.
471
        $paginginfo = new stdClass();
472
        $paginginfo->from = $page * $pagesize + 1;
473
        $paginginfo->to = min(($page + 1) * $pagesize, $count);
474
        $paginginfo->of = $count;
475
        $qubaidlist = implode(',', $qubaids);
476
 
477
        $this->print_header_and_tabs($this->cm, $this->course, $this->quiz, 'grading');
478
 
479
        $gradequestioncontent = '';
480
        foreach ($qubaids as $qubaid) {
481
            $attempt = $attempts[$qubaid];
482
            $quba = question_engine::load_questions_usage_by_activity($qubaid);
483
            $displayoptions = quiz_get_review_options($this->quiz, $attempt, $this->context);
484
            $displayoptions->generalfeedback = question_display_options::HIDDEN;
485
            $displayoptions->history = question_display_options::HIDDEN;
486
            $displayoptions->manualcomment = question_display_options::EDITABLE;
487
 
488
            $gradequestioncontent .= $this->renderer->render_grade_question(
489
                    $quba,
490
                    $slot,
491
                    $displayoptions,
492
                    $this->questions[$slot]->number,
493
                    $this->get_question_heading($attempt, $shownames, $showcustomfields)
494
            );
495
        }
496
 
497
        $pagingbar = new stdClass();
498
        $pagingbar->count = $count;
499
        $pagingbar->page = $page;
500
        $pagingbar->pagesize = $pagesize;
501
        $pagingbar->pagesize = $pagesize;
502
        $pagingbar->order = $order;
503
        $pagingbar->pagingurl = $this->grade_question_url($slot, $questionid, $grade, false);
504
 
505
        $hiddeninputs = [
506
                'qubaids' => $qubaidlist,
507
                'slots' => $slot,
508
                'sesskey' => sesskey()
509
        ];
510
 
511
        echo $this->renderer->render_grading_interface(
512
                $questioninfo,
513
                $this->list_questions_url(),
514
                $mform,
515
                $paginginfo,
516
                $pagingbar,
517
                $this->grade_question_url($slot, $questionid, $grade, $page),
518
                $hiddeninputs,
519
                $gradequestioncontent
520
        );
521
    }
522
 
523
    /**
524
     * When saving a grading page, are all the submitted marks valid?
525
     *
526
     * @return bool true if all valid, else false.
527
     */
528
    protected function validate_submitted_marks() {
529
 
530
        $qubaids = optional_param('qubaids', null, PARAM_SEQUENCE);
531
        if (!$qubaids) {
532
            return false;
533
        }
534
        $qubaids = clean_param_array(explode(',', $qubaids), PARAM_INT);
535
 
536
        $slots = optional_param('slots', '', PARAM_SEQUENCE);
537
        if (!$slots) {
538
            $slots = [];
539
        } else {
540
            $slots = explode(',', $slots);
541
        }
542
 
543
        foreach ($qubaids as $qubaid) {
544
            foreach ($slots as $slot) {
545
                if (!question_engine::is_manual_grade_in_range($qubaid, $slot)) {
546
                    return false;
547
                }
548
            }
549
        }
550
 
551
        return true;
552
    }
553
 
554
    /**
555
     * Save all submitted marks to the database.
556
     *
557
     * @return bool returns true if some attempts or all are graded. False, if none of the attempts are graded.
558
     */
559
    protected function process_submitted_data(): bool {
560
        global $DB;
561
 
562
        $qubaids = optional_param('qubaids', null, PARAM_SEQUENCE);
563
        $assumedslotforevents = optional_param('slot', null, PARAM_INT);
564
 
565
        if (!$qubaids) {
566
            return false;
567
        }
568
 
569
        $qubaids = clean_param_array(explode(',', $qubaids), PARAM_INT);
570
        $attempts = $this->load_attempts_by_usage_ids($qubaids);
571
        $events = [];
572
 
573
        $transaction = $DB->start_delegated_transaction();
574
        $attemptsgraded = false;
575
        foreach ($qubaids as $qubaid) {
576
            $attempt = $attempts[$qubaid];
577
            $attemptobj = new quiz_attempt($attempt, $this->quiz, $this->cm, $this->course);
578
 
579
            // State of the attempt before grades are changed.
580
            $attemptoldtstate = $attemptobj->get_question_state($assumedslotforevents);
581
 
582
            $attemptobj->process_submitted_actions(time());
583
 
584
            // Get attempt state after grades are changed.
585
            $attemptnewtstate = $attemptobj->get_question_state($assumedslotforevents);
586
 
587
            // Check if any attempts are graded.
588
            if (!$attemptsgraded && $attemptoldtstate->is_graded() != $attemptnewtstate->is_graded()) {
589
                $attemptsgraded = true;
590
            }
591
 
592
            // Add the event we will trigger later.
593
            $params = [
594
                'objectid' => $attemptobj->get_question_attempt($assumedslotforevents)->get_question_id(),
595
                'courseid' => $attemptobj->get_courseid(),
596
                'context' => context_module::instance($attemptobj->get_cmid()),
597
                'other' => [
598
                    'quizid' => $attemptobj->get_quizid(),
599
                    'attemptid' => $attemptobj->get_attemptid(),
600
                    'slot' => $assumedslotforevents,
601
                ],
602
            ];
603
            $events[] = \mod_quiz\event\question_manually_graded::create($params);
604
        }
605
        $transaction->allow_commit();
606
 
607
        // Trigger events for all the questions we manually marked.
608
        foreach ($events as $event) {
609
            $event->trigger();
610
        }
611
 
612
        return $attemptsgraded;
613
    }
614
 
615
    /**
616
     * Load information about the number of attempts at various questions in each
617
     * summarystate.
618
     *
619
     * The results are returned as an two dimensional array $qubaid => $slot => $dataobject
620
     *
621
     * @param array $slots A list of slots for the questions you want to konw about.
622
     * @return array The array keys are slot,qestionid. The values are objects with
623
     * fields $slot, $questionid, $inprogress, $name, $needsgrading, $autograded,
624
     * $manuallygraded and $all.
625
     */
626
    protected function get_question_state_summary($slots) {
627
        $dm = new question_engine_data_mapper();
628
        return $dm->load_questions_usages_question_state_summary(
629
                $this->get_qubaids_condition(), $slots);
630
    }
631
 
632
    /**
633
     * Get a list of usage ids where the question with slot $slot, and optionally
634
     * also with question id $questionid, is in summary state $summarystate. Also
635
     * return the total count of such states.
636
     *
637
     * Only a subset of the ids can be returned by using $orderby, $limitfrom and
638
     * $limitnum. A special value 'random' can be passed as $orderby, in which case
639
     * $limitfrom is ignored.
640
     *
641
     * @param int $slot The slot for the questions you want to konw about.
642
     * @param int $questionid (optional) Only return attempts that were of this specific question.
643
     * @param string $summarystate 'all', 'needsgrading', 'autograded' or 'manuallygraded'.
644
     * @param string $orderby 'random', 'date', 'student' or 'idnumber'.
645
     * @param int $page implements paging of the results.
646
     *      Ignored if $orderby = random or $pagesize is null.
647
     * @param int $pagesize implements paging of the results. null = all.
648
     * @return array with two elements, an array of usage ids, and a count of the total number.
649
     */
650
    protected function get_usage_ids_where_question_in_state($summarystate, $slot,
651
            $questionid = null, $orderby = 'random', $page = 0, $pagesize = null) {
652
        $dm = new question_engine_data_mapper();
653
        $extraselect = '';
654
        if ($pagesize && $orderby != 'random') {
655
            $limitfrom = $page * $pagesize;
656
        } else {
657
            $limitfrom = 0;
658
        }
659
 
660
        $qubaids = $this->get_qubaids_condition();
661
 
662
        $params = [];
663
        $userfieldsapi = \core_user\fields::for_identity($this->context)->with_name();
664
        $userfieldssql = $userfieldsapi->get_sql('u', true, '', 'userid', true);
665
        $params = array_merge($params, $userfieldssql->params);
666
        $customfields = [];
667
        foreach ($userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]) as $field) {
668
            $customfields[] = $field;
669
        }
670
        if ($orderby === 'date') {
671
            list($statetest, $params) = $dm->in_summary_state_test(
672
                    'manuallygraded', false, 'mangrstate');
673
            $extraselect = "(
674
                    SELECT MAX(sortqas.timecreated)
675
                    FROM {question_attempt_steps} sortqas
676
                    WHERE sortqas.questionattemptid = qa.id
677
                        AND sortqas.state $statetest
678
                    ) as tcreated";
679
            $orderby = "tcreated";
680
        } else if ($orderby === 'studentfirstname' || $orderby === 'studentlastname' || in_array($orderby, $customfields)) {
681
            $qubaids->from .= " JOIN {user} u ON quiza.userid = u.id {$userfieldssql->joins}";
682
            // For name sorting, map orderby form value to
683
            // actual column names; 'idnumber' maps naturally.
684
            if ($orderby === "studentlastname") {
685
                $orderby = "u.lastname, u.firstname";
686
            } else if ($orderby === "studentfirstname") {
687
                $orderby = "u.firstname, u.lastname";
688
            } else if (in_array($orderby, $customfields)) { // Sort order by current custom user field.
689
                $orderby = $userfieldssql->mappings[$orderby];
690
            }
691
        }
692
 
693
        return $dm->load_questions_usages_where_question_in_state($qubaids, $summarystate,
694
                $slot, $questionid, $orderby, $params, $limitfrom, $pagesize, $extraselect);
695
    }
696
 
697
    /**
698
     * Initialise some parts of $PAGE and start output.
699
     *
700
     * @param stdClass $cm the course_module information.
701
     * @param stdClass $course the course settings.
702
     * @param stdClass $quiz the quiz settings.
703
     * @param string $reportmode the report name.
704
     */
705
    public function print_header_and_tabs($cm, $course, $quiz, $reportmode = 'overview') {
706
        global $PAGE;
707
        $this->renderer = $PAGE->get_renderer('quiz_grading');
708
        parent::print_header_and_tabs($cm, $course, $quiz, $reportmode);
709
    }
710
 
711
    /**
712
     * Get question heading.
713
     *
714
     * @param stdClass $attempt An instance of quiz_attempt.
715
     * @param bool $shownames True to show the student first/lastnames.
716
     * @param bool $showcustomfields Whether custom field values should be shown.
717
     * @return string The string text for the question heading.
718
     */
719
    protected function get_question_heading(stdClass $attempt, bool $shownames, bool $showcustomfields): string {
720
        global $DB;
721
        $a = new stdClass();
722
        $a->attempt = $attempt->attempt;
723
        $a->fullname = fullname($attempt);
724
 
725
        $customfields = [];
726
        foreach ($this->extrauserfields as $field) {
727
            if (strval($attempt->{$field}) !== '') {
728
                $customfields[] = s($attempt->{$field});
729
            }
730
        }
731
 
732
        $a->customfields = implode(', ', $customfields);
733
 
734
        if ($shownames && $showcustomfields) {
735
            return get_string('gradingattemptwithcustomfields', 'quiz_grading', $a);
736
        } else if ($shownames) {
737
            return get_string('gradingattempt', 'quiz_grading', $a);
738
        } else if ($showcustomfields) {
739
            $a->fullname = $a->customfields;
740
            return get_string('gradingattempt', 'quiz_grading', $a);
741
        } else {
742
            return '';
743
        }
744
    }
745
}