Proyectos de Subversion Moodle

Rev

Rev 1267 | | 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
 * This file contains the definition for the class assignment
19
 *
20
 * This class provides all the functionality for the new assign module.
21
 *
22
 * @package   mod_assign
23
 * @copyright 2012 NetSpot {@link http://www.netspot.com.au}
24
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25
 */
26
 
27
defined('MOODLE_INTERNAL') || die();
28
 
29
// Assignment submission statuses.
30
define('ASSIGN_SUBMISSION_STATUS_NEW', 'new');
31
define('ASSIGN_SUBMISSION_STATUS_REOPENED', 'reopened');
32
define('ASSIGN_SUBMISSION_STATUS_DRAFT', 'draft');
33
define('ASSIGN_SUBMISSION_STATUS_SUBMITTED', 'submitted');
34
 
35
// Search filters for grading page.
36
define('ASSIGN_FILTER_NONE', 'none');
37
define('ASSIGN_FILTER_SUBMITTED', 'submitted');
38
define('ASSIGN_FILTER_NOT_SUBMITTED', 'notsubmitted');
39
define('ASSIGN_FILTER_SINGLE_USER', 'singleuser');
40
define('ASSIGN_FILTER_REQUIRE_GRADING', 'requiregrading');
1441 ariadna 41
define('ASSIGN_FILTER_GRADED', 'graded');
1 efrain 42
define('ASSIGN_FILTER_GRANTED_EXTENSION', 'grantedextension');
43
define('ASSIGN_FILTER_DRAFT', 'draft');
44
 
45
// Marker filter for grading page.
46
define('ASSIGN_MARKER_FILTER_NO_MARKER', -1);
47
 
48
// Reopen attempt methods.
1441 ariadna 49
/**
50
 * ASSIGN_ATTEMPT_REOPEN_METHOD_NONE - Reopening attempts is not allowed.
51
 *
52
 * @deprecated since Moodle 4.4
53
 * @todo MDL-81977 This will be deleted in Moodle 4.8.
54
 */
1 efrain 55
define('ASSIGN_ATTEMPT_REOPEN_METHOD_NONE', 'none');
56
define('ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL', 'manual');
1441 ariadna 57
define('ASSIGN_ATTEMPT_REOPEN_METHOD_AUTOMATIC', 'automatic');
1 efrain 58
define('ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS', 'untilpass');
59
 
60
// Special value means allow unlimited attempts.
61
define('ASSIGN_UNLIMITED_ATTEMPTS', -1);
62
 
63
// Special value means no grade has been set.
64
define('ASSIGN_GRADE_NOT_SET', -1);
65
 
66
// Grading states.
67
define('ASSIGN_GRADING_STATUS_GRADED', 'graded');
68
define('ASSIGN_GRADING_STATUS_NOT_GRADED', 'notgraded');
69
 
70
// Marking workflow states.
71
define('ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED', 'notmarked');
72
define('ASSIGN_MARKING_WORKFLOW_STATE_INMARKING', 'inmarking');
73
define('ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW', 'readyforreview');
74
define('ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW', 'inreview');
75
define('ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE', 'readyforrelease');
76
define('ASSIGN_MARKING_WORKFLOW_STATE_RELEASED', 'released');
77
 
78
/** ASSIGN_MAX_EVENT_LENGTH = 432000 ; 5 days maximum */
79
define("ASSIGN_MAX_EVENT_LENGTH", "432000");
80
 
81
// Name of file area for intro attachments.
82
define('ASSIGN_INTROATTACHMENT_FILEAREA', 'introattachment');
83
 
84
// Name of file area for activity attachments.
85
define('ASSIGN_ACTIVITYATTACHMENT_FILEAREA', 'activityattachment');
86
 
87
// Event types.
88
define('ASSIGN_EVENT_TYPE_DUE', 'due');
89
define('ASSIGN_EVENT_TYPE_GRADINGDUE', 'gradingdue');
90
define('ASSIGN_EVENT_TYPE_OPEN', 'open');
91
define('ASSIGN_EVENT_TYPE_CLOSE', 'close');
1441 ariadna 92
define('ASSIGN_EVENT_TYPE_EXTENSION', 'extension');
1 efrain 93
 
94
require_once($CFG->libdir . '/accesslib.php');
95
require_once($CFG->libdir . '/formslib.php');
96
require_once($CFG->dirroot . '/repository/lib.php');
97
require_once($CFG->dirroot . '/mod/assign/mod_form.php');
98
require_once($CFG->libdir . '/gradelib.php');
99
require_once($CFG->dirroot . '/grade/grading/lib.php');
100
require_once($CFG->dirroot . '/mod/assign/feedbackplugin.php');
101
require_once($CFG->dirroot . '/mod/assign/submissionplugin.php');
102
require_once($CFG->dirroot . '/mod/assign/renderable.php');
103
require_once($CFG->dirroot . '/mod/assign/gradingtable.php');
104
require_once($CFG->libdir . '/portfolio/caller.php');
105
 
106
use mod_assign\event\submission_removed;
107
use mod_assign\event\submission_status_updated;
108
use \mod_assign\output\grading_app;
109
use \mod_assign\output\assign_header;
110
use \mod_assign\output\assign_submission_status;
111
use mod_assign\output\timelimit_panel;
112
use mod_assign\downloader;
113
 
114
/**
115
 * Standard base class for mod_assign (assignment types).
116
 *
117
 * @package   mod_assign
118
 * @copyright 2012 NetSpot {@link http://www.netspot.com.au}
119
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
120
 */
1441 ariadna 121
class assign {
1 efrain 122
 
123
    /** @var stdClass the assignment record that contains the global settings for this assign instance */
124
    private $instance;
125
 
126
    /** @var array $var array an array containing per-user assignment records, each having calculated properties (e.g. dates) */
127
    private $userinstances = [];
128
 
129
    /** @var grade_item the grade_item record for this assign instance's primary grade item. */
130
    private $gradeitem;
131
 
132
    /** @var context the context of the course module for this assign instance
133
     *               (or just the course if we are creating a new one)
134
     */
135
    private $context;
136
 
137
    /** @var stdClass the course this assign instance belongs to */
138
    private $course;
139
 
140
    /** @var stdClass the admin config for all assign instances  */
141
    private $adminconfig;
142
 
143
    /** @var assign_renderer the custom renderer for this module */
144
    private $output;
145
 
146
    /** @var cm_info the course module for this assign instance */
147
    private $coursemodule;
148
 
149
    /** @var array cache for things like the coursemodule name or the scale menu -
150
     *             only lives for a single request.
151
     */
152
    private $cache;
153
 
1441 ariadna 154
    /** @var assign_submission_plugin[] list of the installed submission plugins */
1 efrain 155
    private $submissionplugins;
156
 
1441 ariadna 157
    /** @var assign_feedback_plugin[] list of the installed feedback plugins */
1 efrain 158
    private $feedbackplugins;
159
 
160
    /** @var string action to be used to return to this page
161
     *              (without repeating any form submissions etc).
162
     */
163
    private $returnaction = 'view';
164
 
165
    /** @var array params to be used to return to this page */
166
    private $returnparams = array();
167
 
168
    /** @var string modulename prevents excessive calls to get_string */
169
    private static $modulename = null;
170
 
171
    /** @var string modulenameplural prevents excessive calls to get_string */
172
    private static $modulenameplural = null;
173
 
174
    /** @var array of marking workflow states for the current user */
175
    private $markingworkflowstates = null;
176
 
177
    /** @var array of all marking workflow states */
178
    private $allmarkingworkflowstates = null;
179
 
180
    /** @var bool whether to exclude users with inactive enrolment */
181
    private $showonlyactiveenrol = null;
182
 
183
    /** @var string A key used to identify userlists created by this object. */
184
    private $useridlistid = null;
185
 
186
    /** @var array cached list of participants for this assignment. The cache key will be group, showactive and the context id */
187
    private $participants = array();
188
 
189
    /** @var array cached list of user groups when team submissions are enabled. The cache key will be the user. */
190
    private $usersubmissiongroups = array();
191
 
192
    /** @var array cached list of user groups. The cache key will be the user. */
193
    private $usergroups = array();
194
 
195
    /** @var array cached list of IDs of users who share group membership with the user. The cache key will be the user. */
196
    private $sharedgroupmembers = array();
197
 
198
    /**
199
     * @var stdClass The most recent team submission. Used to determine additional attempt numbers and whether
200
     * to update the gradebook.
201
     */
202
    private $mostrecentteamsubmission = null;
203
 
204
    /** @var array Array of error messages encountered during the execution of assignment related operations. */
205
    private $errors = array();
206
 
207
    /** @var mixed This var can vary between false for no overrides to a stdClass of the overrides for a group */
208
    private $overridedata;
209
 
210
    /** @var float grade value. */
211
    public $grade;
212
 
1441 ariadna 213
    /** @var array $usersearch The content that the current user is looking for. */
214
    protected array $usersearch = [];
215
 
1 efrain 216
    /**
217
     * Constructor for the base assign class.
218
     *
219
     * Note: For $coursemodule you can supply a stdclass if you like, but it
220
     * will be more efficient to supply a cm_info object.
221
     *
222
     * @param mixed $coursemodulecontext context|null the course module context
223
     *                                   (or the course context if the coursemodule has not been
224
     *                                   created yet).
225
     * @param mixed $coursemodule the current course module if it was already loaded,
226
     *                            otherwise this class will load one from the context as required.
227
     * @param mixed $course the current course  if it was already loaded,
228
     *                      otherwise this class will load one from the context as required.
229
     */
1441 ariadna 230
    public function __construct($coursemodulecontext, $coursemodule, $course) {
1 efrain 231
        $this->context = $coursemodulecontext;
232
        $this->course = $course;
233
 
234
        // Ensure that $this->coursemodule is a cm_info object (or null).
235
        $this->coursemodule = cm_info::create($coursemodule);
236
 
237
        // Temporary cache only lives for a single request - used to reduce db lookups.
238
        $this->cache = array();
239
 
240
        $this->submissionplugins = $this->load_plugins('assignsubmission');
241
        $this->feedbackplugins = $this->load_plugins('assignfeedback');
242
 
243
        // Extra entropy is required for uniqid() to work on cygwin.
244
        $this->useridlistid = clean_param(uniqid('', true), PARAM_ALPHANUM);
245
    }
246
 
247
    /**
248
     * Set the action and parameters that can be used to return to the current page.
249
     *
250
     * @param string $action The action for the current page
251
     * @param array $params An array of name value pairs which form the parameters
252
     *                      to return to the current page.
253
     * @return void
254
     */
1441 ariadna 255
    public function register_return_link($action, $params) {
1 efrain 256
        global $PAGE;
257
        $params['action'] = $action;
258
        $cm = $this->get_course_module();
259
        if ($cm) {
260
            $currenturl = new moodle_url('/mod/assign/view.php', array('id' => $cm->id));
261
        } else {
262
            $currenturl = new moodle_url('/mod/assign/index.php', array('id' => $this->get_course()->id));
263
        }
264
 
265
        $currenturl->params($params);
266
        $PAGE->set_url($currenturl);
267
    }
268
 
269
    /**
270
     * Return an action that can be used to get back to the current page.
271
     *
272
     * @return string action
273
     */
1441 ariadna 274
    public function get_return_action() {
1 efrain 275
        global $PAGE;
276
 
277
        // Web services don't set a URL, we should avoid debugging when ussing the url object.
278
        if (!WS_SERVER) {
279
            $params = $PAGE->url->params();
280
        }
281
 
282
        if (!empty($params['action'])) {
283
            return $params['action'];
284
        }
285
        return '';
286
    }
287
 
288
    /**
289
     * Based on the current assignment settings should we display the intro.
290
     *
291
     * @return bool showintro
292
     */
1441 ariadna 293
    public function show_intro() {
294
        if ($this->get_instance()->alwaysshowdescription ||
295
                time() > $this->get_instance()->allowsubmissionsfromdate) {
1 efrain 296
            return true;
297
        }
298
        return false;
299
    }
300
 
301
    /**
302
     * Return a list of parameters that can be used to get back to the current page.
303
     *
304
     * @return array params
305
     */
1441 ariadna 306
    public function get_return_params() {
1 efrain 307
        global $PAGE;
308
 
309
        $params = array();
310
        if (!WS_SERVER) {
311
            $params = $PAGE->url->params();
312
        }
313
        unset($params['id']);
314
        unset($params['action']);
315
        return $params;
316
    }
317
 
318
    /**
319
     * Set the submitted form data.
320
     *
321
     * @param stdClass $data The form data (instance)
322
     */
1441 ariadna 323
    public function set_instance(stdClass $data) {
1 efrain 324
        $this->instance = $data;
325
    }
326
 
327
    /**
328
     * Set the context.
329
     *
330
     * @param context $context The new context
331
     */
1441 ariadna 332
    public function set_context(context $context) {
1 efrain 333
        $this->context = $context;
334
    }
335
 
336
    /**
337
     * Set the course data.
338
     *
339
     * @param stdClass $course The course data
340
     */
1441 ariadna 341
    public function set_course(stdClass $course) {
1 efrain 342
        $this->course = $course;
343
    }
344
 
345
    /**
1441 ariadna 346
     * Set usersearch to limit results when getting list of participants.
347
     *
348
     * @param int|null $userid User id to search for.
349
     * @param int|null $groupid Group id to limit resuts to specific group.
350
     * @param string $usersearch Search string to limit results.
351
     */
352
    public function set_usersearch(?int $userid, ?int $groupid, string $usersearch = ''): void {
353
        $usersearcharray = [];
354
        $usersearcharray['userid'] = $userid;
355
        $usersearcharray['groupid'] = $groupid;
356
        $usersearcharray['usersearch'] = $usersearch;
357
        $this->usersearch = $usersearcharray;
358
    }
359
 
360
    /**
1 efrain 361
     * Set error message.
362
     *
363
     * @param string $message The error message
364
     */
1441 ariadna 365
    protected function set_error_message(string $message) {
1 efrain 366
        $this->errors[] = $message;
367
    }
368
 
369
    /**
370
     * Get error messages.
371
     *
372
     * @return array The array of error messages
373
     */
1441 ariadna 374
    public function get_error_messages(): array {
1 efrain 375
        return $this->errors;
376
    }
377
 
378
    /**
379
     * Get list of feedback plugins installed.
380
     *
381
     * @return array
382
     */
1441 ariadna 383
    public function get_feedback_plugins() {
1 efrain 384
        return $this->feedbackplugins;
385
    }
386
 
387
    /**
388
     * Get list of submission plugins installed.
389
     *
390
     * @return array
391
     */
1441 ariadna 392
    public function get_submission_plugins() {
1 efrain 393
        return $this->submissionplugins;
394
    }
395
 
396
    /**
397
     * Is blind marking enabled and reveal identities not set yet?
398
     *
399
     * @return bool
400
     */
1441 ariadna 401
    public function is_blind_marking() {
1 efrain 402
        return $this->get_instance()->blindmarking && !$this->get_instance()->revealidentities;
403
    }
404
 
405
    /**
406
     * Is hidden grading enabled?
407
     *
408
     * This just checks the assignment settings. Remember to check
409
     * the user has the 'showhiddengrader' capability too
410
     *
411
     * @return bool
412
     */
1441 ariadna 413
    public function is_hidden_grader() {
1 efrain 414
        return $this->get_instance()->hidegrader;
415
    }
416
 
417
    /**
418
     * Does an assignment have submission(s) or grade(s) already?
419
     *
420
     * @return bool
421
     */
1441 ariadna 422
    public function has_submissions_or_grades() {
1 efrain 423
        $allgrades = $this->count_grades();
424
        $allsubmissions = $this->count_submissions();
425
        if (($allgrades == 0) && ($allsubmissions == 0)) {
426
            return false;
427
        }
428
        return true;
429
    }
430
 
431
    /**
432
     * Get a specific submission plugin by its type.
433
     *
434
     * @param string $subtype assignsubmission | assignfeedback
435
     * @param string $type
436
     * @return mixed assign_plugin|null
437
     */
1441 ariadna 438
    public function get_plugin_by_type($subtype, $type) {
1 efrain 439
        $shortsubtype = substr($subtype, strlen('assign'));
440
        $name = $shortsubtype . 'plugins';
441
        if ($name != 'feedbackplugins' && $name != 'submissionplugins') {
442
            return null;
443
        }
444
        $pluginlist = $this->$name;
445
        foreach ($pluginlist as $plugin) {
446
            if ($plugin->get_type() == $type) {
447
                return $plugin;
448
            }
449
        }
450
        return null;
451
    }
452
 
453
    /**
454
     * Get a feedback plugin by type.
455
     *
456
     * @param string $type - The type of plugin e.g comments
457
     * @return mixed assign_feedback_plugin|null
458
     */
1441 ariadna 459
    public function get_feedback_plugin_by_type($type) {
1 efrain 460
        return $this->get_plugin_by_type('assignfeedback', $type);
461
    }
462
 
463
    /**
464
     * Get a submission plugin by type.
465
     *
466
     * @param string $type - The type of plugin e.g comments
467
     * @return mixed assign_submission_plugin|null
468
     */
1441 ariadna 469
    public function get_submission_plugin_by_type($type) {
1 efrain 470
        return $this->get_plugin_by_type('assignsubmission', $type);
471
    }
472
 
473
    /**
474
     * Load the plugins from the sub folders under subtype.
475
     *
476
     * @param string $subtype - either submission or feedback
477
     * @return array - The sorted list of plugins
478
     */
1441 ariadna 479
    public function load_plugins($subtype) {
1 efrain 480
        global $CFG;
481
        $result = array();
482
 
483
        $names = core_component::get_plugin_list($subtype);
484
 
485
        foreach ($names as $name => $path) {
486
            if (file_exists($path . '/locallib.php')) {
487
                require_once($path . '/locallib.php');
488
 
489
                $shortsubtype = substr($subtype, strlen('assign'));
490
                $pluginclass = 'assign_' . $shortsubtype . '_' . $name;
491
 
492
                $plugin = new $pluginclass($this, $name);
493
 
494
                if ($plugin instanceof assign_plugin) {
495
                    $idx = $plugin->get_sort_order();
496
                    while (array_key_exists($idx, $result)) {
1441 ariadna 497
                        $idx +=1;
1 efrain 498
                    }
499
                    $result[$idx] = $plugin;
500
                }
501
            }
502
        }
503
        ksort($result);
504
        return $result;
505
    }
506
 
507
    /**
508
     * Display the assignment, used by view.php
509
     *
510
     * The assignment is displayed differently depending on your role,
511
     * the settings for the assignment and the status of the assignment.
512
     *
513
     * @param string $action The current action if any.
514
     * @param array $args Optional arguments to pass to the view (instead of getting them from GET and POST).
515
     * @return string - The page output.
516
     */
1441 ariadna 517
    public function view($action='', $args = array()) {
1 efrain 518
        global $PAGE;
519
 
520
        $o = '';
521
        $mform = null;
522
        $notices = array();
523
        $nextpageparams = array();
524
 
525
        if (!empty($this->get_course_module()->id)) {
526
            $nextpageparams['id'] = $this->get_course_module()->id;
527
        }
528
 
529
        // Handle form submissions first.
530
        if ($action == 'savesubmission') {
531
            $action = 'editsubmission';
532
            if ($this->process_save_submission($mform, $notices)) {
533
                $action = 'redirect';
534
                if ($this->can_grade()) {
535
                    $nextpageparams['action'] = 'grading';
536
                } else {
537
                    $nextpageparams['action'] = 'view';
538
                }
539
            }
540
        } else if ($action == 'editprevioussubmission') {
541
            $action = 'editsubmission';
542
            if ($this->process_copy_previous_attempt($notices)) {
543
                $action = 'redirect';
544
                $nextpageparams['action'] = 'editsubmission';
545
            }
546
        } else if ($action == 'lock') {
547
            $this->process_lock_submission();
548
            $action = 'redirect';
549
            $nextpageparams['action'] = 'grading';
550
        } else if ($action == 'removesubmission') {
551
            $this->process_remove_submission();
552
            $action = 'redirect';
553
            if ($this->can_grade()) {
554
                $nextpageparams['action'] = 'grading';
555
            } else {
556
                $nextpageparams['action'] = 'view';
557
            }
558
        } else if ($action == 'addattempt') {
559
            $this->process_add_attempt(required_param('userid', PARAM_INT));
560
            $action = 'redirect';
561
            $nextpageparams['action'] = 'grading';
562
        } else if ($action == 'reverttodraft') {
563
            $this->process_revert_to_draft();
564
            $action = 'redirect';
565
            $nextpageparams['action'] = 'grading';
566
        } else if ($action == 'unlock') {
567
            $this->process_unlock_submission();
568
            $action = 'redirect';
569
            $nextpageparams['action'] = 'grading';
570
        } else if ($action == 'setbatchmarkingworkflowstate') {
571
            $this->process_set_batch_marking_workflow_state();
572
            $action = 'redirect';
573
            $nextpageparams['action'] = 'grading';
574
        } else if ($action == 'setbatchmarkingallocation') {
575
            $this->process_set_batch_marking_allocation();
576
            $action = 'redirect';
577
            $nextpageparams['action'] = 'grading';
578
        } else if ($action == 'confirmsubmit') {
579
            $action = 'submit';
580
            if ($this->process_submit_for_grading($mform, $notices)) {
581
                $action = 'redirect';
582
                $nextpageparams['action'] = 'view';
583
            } else if ($notices) {
584
                $action = 'viewsubmitforgradingerror';
585
            }
586
        } else if ($action == 'submitotherforgrading') {
587
            if ($this->process_submit_other_for_grading($mform, $notices)) {
588
                $action = 'redirect';
589
                $nextpageparams['action'] = 'grading';
590
            } else {
591
                $action = 'viewsubmitforgradingerror';
592
            }
593
        } else if ($action == 'gradingbatchoperation') {
1441 ariadna 594
            $action = $this->process_grading_batch_operation();
1 efrain 595
            if ($action == 'grading') {
596
                $action = 'redirect';
597
                $nextpageparams['action'] = 'grading';
598
            }
599
        } else if ($action == 'submitgrade') {
600
            if (optional_param('saveandshownext', null, PARAM_RAW)) {
601
                // Save and show next.
602
                $action = 'grade';
603
                if ($this->process_save_grade($mform)) {
604
                    $action = 'redirect';
605
                    $nextpageparams['action'] = 'grade';
606
                    $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) + 1;
607
                    $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
608
                }
609
            } else if (optional_param('nosaveandprevious', null, PARAM_RAW)) {
610
                $action = 'redirect';
611
                $nextpageparams['action'] = 'grade';
612
                $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) - 1;
613
                $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
614
            } else if (optional_param('nosaveandnext', null, PARAM_RAW)) {
615
                $action = 'redirect';
616
                $nextpageparams['action'] = 'grade';
617
                $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) + 1;
618
                $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
619
            } else if (optional_param('savegrade', null, PARAM_RAW)) {
620
                // Save changes button.
621
                $action = 'grade';
622
                if ($this->process_save_grade($mform)) {
623
                    $action = 'redirect';
624
                    $nextpageparams['action'] = 'savegradingresult';
625
                }
626
            } else {
627
                // Cancel button.
628
                $action = 'redirect';
629
                $nextpageparams['action'] = 'grading';
630
            }
631
        } else if ($action == 'quickgrade') {
632
            $message = $this->process_save_quick_grades();
633
            $action = 'quickgradingresult';
634
        } else if ($action == 'saveextension') {
635
            $action = 'grantextension';
636
            if ($this->process_save_extension($mform)) {
637
                $action = 'redirect';
638
                $nextpageparams['action'] = 'grading';
639
            }
640
        } else if ($action == 'revealidentitiesconfirm') {
641
            $this->process_reveal_identities();
642
            $action = 'redirect';
643
            $nextpageparams['action'] = 'grading';
644
        }
645
 
1441 ariadna 646
        $returnparams = array('rownum'=>optional_param('rownum', 0, PARAM_INT),
647
                              'useridlistid' => optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM));
1 efrain 648
        $this->register_return_link($action, $returnparams);
649
 
650
        // Include any page action as part of the body tag CSS id.
651
        if (!empty($action)) {
652
            $PAGE->set_pagetype('mod-assign-' . $action);
653
        }
654
        // Now show the right view page.
655
        if ($action == 'redirect') {
656
            $nextpageurl = new moodle_url('/mod/assign/view.php', $nextpageparams);
657
            $messages = '';
658
            $messagetype = \core\output\notification::NOTIFY_INFO;
659
            $errors = $this->get_error_messages();
660
            if (!empty($errors)) {
661
                $messages = html_writer::alist($errors, ['class' => 'mb-1 mt-1']);
662
                $messagetype = \core\output\notification::NOTIFY_ERROR;
663
            }
664
            redirect($nextpageurl, $messages, null, $messagetype);
665
            return;
666
        } else if ($action == 'savegradingresult') {
667
            $message = get_string('gradingchangessaved', 'assign');
668
            $o .= $this->view_savegrading_result($message);
669
        } else if ($action == 'quickgradingresult') {
670
            $mform = null;
671
            $o .= $this->view_quickgrading_result($message);
672
        } else if ($action == 'gradingpanel') {
673
            $o .= $this->view_single_grading_panel($args);
674
        } else if ($action == 'grade') {
675
            $o .= $this->view_single_grade_page($mform);
676
        } else if ($action == 'viewpluginassignfeedback') {
677
            $o .= $this->view_plugin_content('assignfeedback');
678
        } else if ($action == 'viewpluginassignsubmission') {
679
            $o .= $this->view_plugin_content('assignsubmission');
680
        } else if ($action == 'editsubmission') {
681
            $PAGE->add_body_class('limitedwidth');
682
            $o .= $this->view_edit_submission_page($mform, $notices);
683
        } else if ($action == 'grader') {
684
            $o .= $this->view_grader();
685
        } else if ($action == 'grading') {
686
            $o .= $this->view_grading_page();
687
        } else if ($action == 'downloadall') {
688
            $o .= $this->download_submissions();
689
        } else if ($action == 'submit') {
690
            $PAGE->add_body_class('limitedwidth');
691
            $o .= $this->check_submit_for_grading($mform);
692
        } else if ($action == 'grantextension') {
693
            $o .= $this->view_grant_extension($mform);
694
        } else if ($action == 'revealidentities') {
1441 ariadna 695
            $o .= $this->view_reveal_identities_confirm();
1 efrain 696
        } else if ($action == 'removesubmissionconfirm') {
697
            $PAGE->add_body_class('limitedwidth');
698
            $o .= $this->view_remove_submission_confirm();
699
        } else if ($action == 'plugingradingbatchoperation') {
1441 ariadna 700
            $o .= $this->view_plugin_grading_batch_operation();
1 efrain 701
        } else if ($action == 'viewpluginpage') {
1441 ariadna 702
             $o .= $this->view_plugin_page();
1 efrain 703
        } else if ($action == 'viewbatchsetmarkingworkflowstate') {
1441 ariadna 704
             $o .= $this->view_batch_set_workflow_state();
1 efrain 705
        } else if ($action == 'viewbatchmarkingallocation') {
1441 ariadna 706
            $o .= $this->view_batch_markingallocation();
1 efrain 707
        } else if ($action == 'viewsubmitforgradingerror') {
708
            $o .= $this->view_error_page(get_string('submitforgrading', 'assign'), $notices);
709
        } else if ($action == 'fixrescalednullgrades') {
710
            $o .= $this->view_fix_rescaled_null_grades();
711
        } else {
712
            $PAGE->add_body_class('limitedwidth');
713
            $o .= $this->view_submission_page();
714
        }
715
 
716
        return $o;
717
    }
718
 
719
    /**
720
     * Add this instance to the database.
721
     *
722
     * @param stdClass $formdata The data submitted from the form
723
     * @param bool $callplugins This is used to skip the plugin code
724
     *             when upgrading an old assignment to a new one (the plugins get called manually)
725
     * @return mixed false if an error occurs or the int id of the new instance
726
     */
1441 ariadna 727
    public function add_instance(stdClass $formdata, $callplugins) {
1 efrain 728
        global $DB;
729
        $adminconfig = $this->get_admin_config();
730
 
731
        $err = '';
732
 
733
        // Add the database record.
734
        $update = new stdClass();
735
        $update->name = $formdata->name;
736
        $update->timemodified = time();
737
        $update->timecreated = time();
738
        $update->course = $formdata->course;
739
        $update->courseid = $formdata->course;
740
        $update->intro = $formdata->intro;
741
        $update->introformat = $formdata->introformat;
742
        $update->alwaysshowdescription = !empty($formdata->alwaysshowdescription);
743
        if (isset($formdata->activityeditor)) {
744
            $update->activity = $this->save_editor_draft_files($formdata);
745
            $update->activityformat = $formdata->activityeditor['format'];
746
        }
747
        if (isset($formdata->submissionattachments)) {
748
            $update->submissionattachments = $formdata->submissionattachments;
749
        }
750
        $update->submissiondrafts = $formdata->submissiondrafts;
751
        $update->requiresubmissionstatement = $formdata->requiresubmissionstatement;
752
        $update->sendnotifications = $formdata->sendnotifications;
753
        $update->sendlatenotifications = $formdata->sendlatenotifications;
754
        $update->sendstudentnotifications = $adminconfig->sendstudentnotifications;
755
        if (isset($formdata->sendstudentnotifications)) {
756
            $update->sendstudentnotifications = $formdata->sendstudentnotifications;
757
        }
758
        $update->duedate = $formdata->duedate;
759
        $update->cutoffdate = $formdata->cutoffdate;
760
        $update->gradingduedate = $formdata->gradingduedate;
761
        if (isset($formdata->timelimit)) {
762
            $update->timelimit = $formdata->timelimit;
763
        }
764
        $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate;
765
        $update->grade = $formdata->grade;
766
        $update->completionsubmit = !empty($formdata->completionsubmit);
767
        $update->teamsubmission = $formdata->teamsubmission;
768
        $update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit;
769
        if (isset($formdata->teamsubmissiongroupingid)) {
770
            $update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid;
771
        }
772
        $update->blindmarking = $formdata->blindmarking;
773
        if (isset($formdata->hidegrader)) {
774
            $update->hidegrader = $formdata->hidegrader;
775
        }
1441 ariadna 776
        $update->maxattempts = $formdata->maxattempts ?? 1;
777
        $update->attemptreopenmethod = $formdata->attemptreopenmethod ?? ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS;
1 efrain 778
        if (isset($formdata->preventsubmissionnotingroup)) {
779
            $update->preventsubmissionnotingroup = $formdata->preventsubmissionnotingroup;
780
        }
781
        $update->markingworkflow = $formdata->markingworkflow;
782
        $update->markingallocation = $formdata->markingallocation;
783
        if (empty($update->markingworkflow)) { // If marking workflow is disabled, make sure allocation is disabled.
784
            $update->markingallocation = 0;
785
        }
786
        if (isset($formdata->markinganonymous)) {
787
            // If marking workflow is disabled, or anonymous submissions is disabled then make sure marking anonymous is disabled.
788
            if (empty($update->markingworkflow) || empty($update->blindmarking)) {
789
                $update->markinganonymous = 0;
790
            } else {
791
                $update->markinganonymous = $formdata->markinganonymous;
792
            }
793
        }
1441 ariadna 794
 
795
        // Grade penalties.
796
        $update->gradepenalty = $formdata->gradepenalty ?? 0;
797
 
1 efrain 798
        $returnid = $DB->insert_record('assign', $update);
1441 ariadna 799
        $this->instance = $DB->get_record('assign', array('id'=>$returnid), '*', MUST_EXIST);
1 efrain 800
        // Cache the course record.
1441 ariadna 801
        $this->course = $DB->get_record('course', array('id'=>$formdata->course), '*', MUST_EXIST);
1 efrain 802
 
803
        $this->save_intro_draft_files($formdata);
804
        $this->save_editor_draft_files($formdata);
805
 
806
        if ($callplugins) {
807
            // Call save_settings hook for submission plugins.
808
            foreach ($this->submissionplugins as $plugin) {
809
                if (!$this->update_plugin_instance($plugin, $formdata)) {
810
                    throw new \moodle_exception($plugin->get_error());
811
                    return false;
812
                }
813
            }
814
            foreach ($this->feedbackplugins as $plugin) {
815
                if (!$this->update_plugin_instance($plugin, $formdata)) {
816
                    throw new \moodle_exception($plugin->get_error());
817
                    return false;
818
                }
819
            }
820
 
821
            // In the case of upgrades the coursemodule has not been set,
822
            // so we need to wait before calling these two.
823
            $this->update_calendar($formdata->coursemodule);
824
            if (!empty($formdata->completionexpected)) {
1441 ariadna 825
                \core_completion\api::update_completion_date_event($formdata->coursemodule, 'assign', $this->instance,
826
                        $formdata->completionexpected);
1 efrain 827
            }
828
            $this->update_gradebook(false, $formdata->coursemodule);
1441 ariadna 829
 
1 efrain 830
        }
831
 
832
        $update = new stdClass();
833
        $update->id = $this->get_instance()->id;
1441 ariadna 834
        $update->nosubmissions = (!$this->is_any_submission_plugin_enabled()) ? 1: 0;
1 efrain 835
        $DB->update_record('assign', $update);
836
 
837
        return $returnid;
838
    }
839
 
840
    /**
841
     * Delete all grades from the gradebook for this assignment.
842
     *
843
     * @return bool
844
     */
1441 ariadna 845
    protected function delete_grades() {
1 efrain 846
        global $CFG;
847
 
1441 ariadna 848
        $result = grade_update('mod/assign',
849
                               $this->get_course()->id,
850
                               'mod',
851
                               'assign',
852
                               $this->get_instance()->id,
853
                               0,
854
                               null,
855
                               array('deleted'=>1));
1 efrain 856
        return $result == GRADE_UPDATE_OK;
857
    }
858
 
859
    /**
860
     * Delete this instance from the database.
861
     *
862
     * @return bool false if an error occurs
863
     */
1441 ariadna 864
    public function delete_instance() {
1 efrain 865
        global $DB;
866
        $result = true;
867
 
868
        foreach ($this->submissionplugins as $plugin) {
869
            if (!$plugin->delete_instance()) {
870
                throw new \moodle_exception($plugin->get_error());
871
                $result = false;
872
            }
873
        }
874
        foreach ($this->feedbackplugins as $plugin) {
875
            if (!$plugin->delete_instance()) {
876
                throw new \moodle_exception($plugin->get_error());
877
                $result = false;
878
            }
879
        }
880
 
881
        // Delete files associated with this assignment.
882
        $fs = get_file_storage();
1441 ariadna 883
        if (! $fs->delete_area_files($this->context->id) ) {
1 efrain 884
            $result = false;
885
        }
886
 
887
        $this->delete_all_overrides();
888
 
889
        // Delete_records will throw an exception if it fails - so no need for error checking here.
890
        $DB->delete_records('assign_submission', array('assignment' => $this->get_instance()->id));
891
        $DB->delete_records('assign_grades', array('assignment' => $this->get_instance()->id));
892
        $DB->delete_records('assign_plugin_config', array('assignment' => $this->get_instance()->id));
893
        $DB->delete_records('assign_user_flags', array('assignment' => $this->get_instance()->id));
894
        $DB->delete_records('assign_user_mapping', array('assignment' => $this->get_instance()->id));
895
 
896
        // Delete items from the gradebook.
897
        if (! $this->delete_grades()) {
898
            $result = false;
899
        }
900
 
901
        // Delete the instance.
902
        // We must delete the module record after we delete the grade item.
1441 ariadna 903
        $DB->delete_records('assign', array('id'=>$this->get_instance()->id));
1 efrain 904
 
905
        return $result;
906
    }
907
 
908
    /**
909
     * Deletes a assign override from the database and clears any corresponding calendar events
910
     *
911
     * @param int $overrideid The id of the override being deleted
912
     * @return bool true on success
913
     */
1441 ariadna 914
    public function delete_override($overrideid) {
1 efrain 915
        global $CFG, $DB;
916
 
917
        require_once($CFG->dirroot . '/calendar/lib.php');
918
 
919
        $cm = $this->get_course_module();
920
        if (empty($cm)) {
921
            $instance = $this->get_instance();
922
            $cm = get_coursemodule_from_instance('assign', $instance->id, $instance->course);
923
        }
924
 
925
        $override = $DB->get_record('assign_overrides', array('id' => $overrideid), '*', MUST_EXIST);
926
 
927
        // Delete the events.
928
        $conds = array('modulename' => 'assign', 'instance' => $this->get_instance()->id);
929
        if (isset($override->userid)) {
930
            $conds['userid'] = $override->userid;
931
            $cachekey = "{$cm->instance}_u_{$override->userid}";
932
        } else {
933
            $conds['groupid'] = $override->groupid;
934
            $cachekey = "{$cm->instance}_g_{$override->groupid}";
935
        }
936
        $events = $DB->get_records('event', $conds);
937
        foreach ($events as $event) {
938
            $eventold = calendar_event::load($event);
939
            $eventold->delete();
940
        }
941
 
942
        $DB->delete_records('assign_overrides', array('id' => $overrideid));
943
        cache::make('mod_assign', 'overrides')->delete($cachekey);
944
 
945
        // Set the common parameters for one of the events we will be triggering.
946
        $params = array(
947
            'objectid' => $override->id,
948
            'context' => context_module::instance($cm->id),
949
            'other' => array(
950
                'assignid' => $override->assignid
951
            )
952
        );
953
        // Determine which override deleted event to fire.
954
        if (!empty($override->userid)) {
955
            $params['relateduserid'] = $override->userid;
956
            $event = \mod_assign\event\user_override_deleted::create($params);
957
        } else {
958
            $params['other']['groupid'] = $override->groupid;
959
            $event = \mod_assign\event\group_override_deleted::create($params);
960
        }
961
 
962
        // Trigger the override deleted event.
963
        $event->add_record_snapshot('assign_overrides', $override);
964
        $event->trigger();
965
 
966
        return true;
967
    }
968
 
969
    /**
970
     * Deletes all assign overrides from the database and clears any corresponding calendar events
971
     */
1441 ariadna 972
    public function delete_all_overrides() {
1 efrain 973
        global $DB;
974
 
975
        $overrides = $DB->get_records('assign_overrides', array('assignid' => $this->get_instance()->id), 'id');
976
        foreach ($overrides as $override) {
977
            $this->delete_override($override->id);
978
        }
979
    }
980
 
981
    /**
982
     * Updates the assign properties with override information for a user.
983
     *
984
     * Algorithm:  For each assign setting, if there is a matching user-specific override,
985
     *   then use that otherwise, if there are group-specific overrides, return the most
986
     *   lenient combination of them.  If neither applies, leave the assign setting unchanged.
987
     *
988
     * @param int $userid The userid.
989
     */
1441 ariadna 990
    public function update_effective_access($userid) {
1 efrain 991
 
992
        $override = $this->override_exists($userid);
993
 
994
        // Merge with assign defaults.
995
        $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate', 'timelimit');
996
        foreach ($keys as $key) {
997
            if (isset($override->{$key})) {
998
                $this->get_instance($userid)->{$key} = $override->{$key};
999
            }
1000
        }
1441 ariadna 1001
 
1 efrain 1002
    }
1003
 
1004
    /**
1005
     * Returns whether an assign has any overrides.
1006
     *
1007
     * @return true if any, false if not
1008
     */
1441 ariadna 1009
    public function has_overrides() {
1 efrain 1010
        global $DB;
1011
 
1012
        $override = $DB->record_exists('assign_overrides', array('assignid' => $this->get_instance()->id));
1013
 
1014
        if ($override) {
1015
            return true;
1016
        }
1017
 
1018
        return false;
1019
    }
1020
 
1021
    /**
1022
     * Returns user override
1023
     *
1024
     * Algorithm:  For each assign setting, if there is a matching user-specific override,
1025
     *   then use that otherwise, if there are group-specific overrides, use the one with the
1026
     *   lowest sort order. If neither applies, leave the assign setting unchanged.
1027
     *
1028
     * @param int $userid The userid.
1029
     * @return stdClass The override
1030
     */
1441 ariadna 1031
    public function override_exists($userid) {
1 efrain 1032
        global $DB;
1033
 
1034
        // Gets an assoc array containing the keys for defined user overrides only.
1441 ariadna 1035
        $getuseroverride = function($userid) use ($DB) {
1 efrain 1036
            $useroverride = $DB->get_record('assign_overrides', ['assignid' => $this->get_instance()->id, 'userid' => $userid]);
1037
            return $useroverride ? get_object_vars($useroverride) : [];
1038
        };
1039
 
1040
        // Gets an assoc array containing the keys for defined group overrides only.
1441 ariadna 1041
        $getgroupoverride = function($userid) use ($DB) {
1 efrain 1042
            $groupings = groups_get_user_groups($this->get_instance()->course, $userid);
1043
 
1044
            if (empty($groupings[0])) {
1045
                return [];
1046
            }
1047
 
1048
            // Select all overrides that apply to the User's groups.
1049
            list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0]));
1050
            $sql = "SELECT * FROM {assign_overrides}
1051
                    WHERE groupid $extra AND assignid = ? ORDER BY sortorder ASC";
1052
            $params[] = $this->get_instance()->id;
1053
            $groupoverride = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE);
1054
 
1055
            return $groupoverride ? get_object_vars($groupoverride) : [];
1056
        };
1057
 
1058
        // Later arguments clobber earlier ones with array_merge. The two helper functions
1059
        // return arrays containing keys for only the defined overrides. So we get the
1060
        // desired behaviour as per the algorithm.
1061
        return (object)array_merge(
1062
            ['timelimit' => null, 'duedate' => null, 'cutoffdate' => null, 'allowsubmissionsfromdate' => null],
1063
            $getgroupoverride($userid),
1064
            $getuseroverride($userid)
1065
        );
1066
    }
1067
 
1068
    /**
1069
     * Check if the given calendar_event is either a user or group override
1070
     * event.
1071
     *
1072
     * @return bool
1073
     */
1441 ariadna 1074
    public function is_override_calendar_event(\calendar_event $event) {
1 efrain 1075
        global $DB;
1076
 
1077
        if (!isset($event->modulename)) {
1078
            return false;
1079
        }
1080
 
1081
        if ($event->modulename != 'assign') {
1082
            return false;
1083
        }
1084
 
1085
        if (!isset($event->instance)) {
1086
            return false;
1087
        }
1088
 
1089
        if (!isset($event->userid) && !isset($event->groupid)) {
1090
            return false;
1091
        }
1092
 
1093
        $overrideparams = [
1094
            'assignid' => $event->instance
1095
        ];
1096
 
1097
        if (isset($event->groupid)) {
1098
            $overrideparams['groupid'] = $event->groupid;
1099
        } else if (isset($event->userid)) {
1100
            $overrideparams['userid'] = $event->userid;
1101
        }
1102
 
1103
        if ($DB->get_record('assign_overrides', $overrideparams)) {
1104
            return true;
1105
        } else {
1106
            return false;
1107
        }
1108
    }
1109
 
1110
    /**
1111
     * This function calculates the minimum and maximum cutoff values for the timestart of
1112
     * the given event.
1113
     *
1114
     * It will return an array with two values, the first being the minimum cutoff value and
1115
     * the second being the maximum cutoff value. Either or both values can be null, which
1116
     * indicates there is no minimum or maximum, respectively.
1117
     *
1118
     * If a cutoff is required then the function must return an array containing the cutoff
1119
     * timestamp and error string to display to the user if the cutoff value is violated.
1120
     *
1121
     * A minimum and maximum cutoff return value will look like:
1122
     * [
1123
     *     [1505704373, 'The due date must be after the sbumission start date'],
1124
     *     [1506741172, 'The due date must be before the cutoff date']
1125
     * ]
1126
     *
1127
     * If the event does not have a valid timestart range then [false, false] will
1128
     * be returned.
1129
     *
1130
     * @param calendar_event $event The calendar event to get the time range for
1131
     * @return array
1132
     */
1441 ariadna 1133
    function get_valid_calendar_event_timestart_range(\calendar_event $event) {
1 efrain 1134
        $instance = $this->get_instance();
1135
        $submissionsfromdate = $instance->allowsubmissionsfromdate;
1136
        $cutoffdate = $instance->cutoffdate;
1137
        $duedate = $instance->duedate;
1138
        $gradingduedate = $instance->gradingduedate;
1139
        $mindate = null;
1140
        $maxdate = null;
1141
 
1142
        if ($event->eventtype == ASSIGN_EVENT_TYPE_DUE) {
1143
            // This check is in here because due date events are currently
1144
            // the only events that can be overridden, so we can save a DB
1145
            // query if we don't bother checking other events.
1146
            if ($this->is_override_calendar_event($event)) {
1147
                // This is an override event so there is no valid timestart
1148
                // range to set it to.
1149
                return [false, false];
1150
            }
1151
 
1152
            if ($submissionsfromdate) {
1153
                $mindate = [
1154
                    $submissionsfromdate,
1155
                    get_string('duedatevalidation', 'assign'),
1156
                ];
1157
            }
1158
 
1159
            if ($cutoffdate) {
1160
                $maxdate = [
1161
                    $cutoffdate,
1162
                    get_string('cutoffdatevalidation', 'assign'),
1163
                ];
1164
            }
1165
 
1166
            if ($gradingduedate) {
1167
                // If we don't have a cutoff date or we've got a grading due date
1168
                // that is earlier than the cutoff then we should use that as the
1169
                // upper limit for the due date.
1170
                if (!$cutoffdate || $gradingduedate < $cutoffdate) {
1171
                    $maxdate = [
1172
                        $gradingduedate,
1173
                        get_string('gradingdueduedatevalidation', 'assign'),
1174
                    ];
1175
                }
1176
            }
1177
        } else if ($event->eventtype == ASSIGN_EVENT_TYPE_GRADINGDUE) {
1178
            if ($duedate) {
1179
                $mindate = [
1180
                    $duedate,
1181
                    get_string('gradingdueduedatevalidation', 'assign'),
1182
                ];
1183
            } else if ($submissionsfromdate) {
1184
                $mindate = [
1185
                    $submissionsfromdate,
1186
                    get_string('gradingduefromdatevalidation', 'assign'),
1187
                ];
1188
            }
1189
        }
1190
 
1191
        return [$mindate, $maxdate];
1192
    }
1193
 
1194
    /**
1195
     * Actual implementation of the reset course functionality, delete all the
1196
     * assignment submissions for course $data->courseid.
1197
     *
1198
     * @param stdClass $data the data submitted from the reset course.
1199
     * @return array status array
1200
     */
1441 ariadna 1201
    public function reset_userdata($data) {
1 efrain 1202
        global $CFG, $DB;
1203
 
1204
        $componentstr = get_string('modulenameplural', 'assign');
1441 ariadna 1205
        $status = [];
1 efrain 1206
 
1207
        $fs = get_file_storage();
1208
        if (!empty($data->reset_assign_submissions)) {
1209
            // Delete files associated with this assignment.
1210
            foreach ($this->submissionplugins as $plugin) {
1441 ariadna 1211
                $fileareas = [];
1 efrain 1212
                $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type();
1213
                $fileareas = $plugin->get_file_areas();
1214
                foreach ($fileareas as $filearea => $notused) {
1215
                    $fs->delete_area_files($this->context->id, $plugincomponent, $filearea);
1216
                }
1217
 
1218
                if (!$plugin->delete_instance()) {
1441 ariadna 1219
                    $status[] = [
1254 ariadna 1220
                        'component' => $componentstr,
1221
                        'item' => get_string('deleteallsubmissions', 'assign'),
1441 ariadna 1222
                        'error' => $plugin->get_error(),
1223
                    ];
1 efrain 1224
                }
1225
            }
1226
 
1227
            foreach ($this->feedbackplugins as $plugin) {
1228
                $fileareas = array();
1229
                $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type();
1230
                $fileareas = $plugin->get_file_areas();
1231
                foreach ($fileareas as $filearea => $notused) {
1232
                    $fs->delete_area_files($this->context->id, $plugincomponent, $filearea);
1233
                }
1234
 
1235
                if (!$plugin->delete_instance()) {
1441 ariadna 1236
                    $status[] = [
1254 ariadna 1237
                        'component' => $componentstr,
1441 ariadna 1238
                        'item ' => get_string('deleteallsubmissions', 'assign'),
1239
                        'error' => $plugin->get_error(),
1240
                    ];
1 efrain 1241
                }
1242
            }
1243
 
1441 ariadna 1244
            $assignids = $DB->get_records('assign', ['course' => $data->courseid], '', 'id');
1 efrain 1245
            list($sql, $params) = $DB->get_in_or_equal(array_keys($assignids));
1246
 
1247
            $DB->delete_records_select('assign_submission', "assignment $sql", $params);
1248
            $DB->delete_records_select('assign_user_flags', "assignment $sql", $params);
1249
 
1441 ariadna 1250
            $status[] = [
1254 ariadna 1251
                'component' => $componentstr,
1252
                'item' => get_string('deleteallsubmissions', 'assign'),
1441 ariadna 1253
                'error' => false,
1254
            ];
1 efrain 1255
 
1256
            if (!empty($data->reset_gradebook_grades)) {
1257
                $DB->delete_records_select('assign_grades', "assignment $sql", $params);
1258
                // Remove all grades from gradebook.
1254 ariadna 1259
                require_once($CFG->dirroot . '/mod/assign/lib.php');
1 efrain 1260
                assign_reset_gradebook($data->courseid);
1261
            }
1262
 
1263
            // Reset revealidentities for assign if blindmarking is enabled.
1264
            if ($this->get_instance()->blindmarking) {
1441 ariadna 1265
                $DB->set_field('assign', 'revealidentities', 0, ['id' => $this->get_instance()->id]);
1 efrain 1266
            }
1267
        }
1268
 
1269
        $purgeoverrides = false;
1270
 
1271
        // Remove user overrides.
1272
        if (!empty($data->reset_assign_user_overrides)) {
1441 ariadna 1273
            $DB->delete_records_select('assign_overrides',
1274
                'assignid IN (SELECT id FROM {assign} WHERE course = ?) AND userid IS NOT NULL', [$data->courseid]);
1275
            $status[] = [
1 efrain 1276
                'component' => $componentstr,
1441 ariadna 1277
                'item' => get_string('useroverrides', 'assign'),
1278
                'error' => false,
1279
            ];
1 efrain 1280
            $purgeoverrides = true;
1281
        }
1282
        // Remove group overrides.
1283
        if (!empty($data->reset_assign_group_overrides)) {
1441 ariadna 1284
            $DB->delete_records_select('assign_overrides',
1285
                'assignid IN (SELECT id FROM {assign} WHERE course = ?) AND groupid IS NOT NULL', [$data->courseid]);
1286
            $status[] = [
1 efrain 1287
                'component' => $componentstr,
1441 ariadna 1288
                'item' => get_string('groupoverrides', 'assign'),
1289
                'error' => false,
1290
            ];
1 efrain 1291
            $purgeoverrides = true;
1292
        }
1293
 
1294
        // Updating dates - shift may be negative too.
1295
        if ($data->timeshift) {
1441 ariadna 1296
            $sql = "UPDATE {assign_overrides}
1297
                       SET allowsubmissionsfromdate = allowsubmissionsfromdate + ?
1298
                     WHERE assignid = ? AND allowsubmissionsfromdate <> 0";
1254 ariadna 1299
            $DB->execute(
1441 ariadna 1300
                $sql,
1301
                [$data->timeshift, $this->get_instance()->id],
1254 ariadna 1302
            );
1441 ariadna 1303
            $sql = "UPDATE {assign_overrides}
1304
                       SET duedate = duedate + ?
1305
                     WHERE assignid = ? AND duedate <> 0";
1254 ariadna 1306
            $DB->execute(
1441 ariadna 1307
                $sql,
1308
                [$data->timeshift, $this->get_instance()->id],
1254 ariadna 1309
            );
1441 ariadna 1310
            $sql = "UPDATE {assign_overrides}
1311
                       SET cutoffdate = cutoffdate + ?
1312
                     WHERE assignid =? AND cutoffdate <> 0";
1254 ariadna 1313
            $DB->execute(
1441 ariadna 1314
                $sql,
1315
                [$data->timeshift, $this->get_instance()->id],
1254 ariadna 1316
            );
1 efrain 1317
 
1318
            $purgeoverrides = true;
1319
 
1320
            // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
1321
            // See MDL-9367.
1441 ariadna 1322
            shift_course_mod_dates('assign', [
1323
                'allowsubmissionsfromdate',
1324
                'duedate',
1325
                'cutoffdate',
1326
                'gradingduedate',
1327
            ], $data->timeshift, $data->courseid, $this->get_instance()->id);
1328
 
1329
            $status[] = [
1254 ariadna 1330
                'component' => $componentstr,
1441 ariadna 1331
                'item' => get_string('date'),
1332
                'error' => false,
1333
            ];
1 efrain 1334
        }
1335
 
1336
        if ($purgeoverrides) {
1337
            cache::make('mod_assign', 'overrides')->purge();
1338
        }
1339
 
1340
        return $status;
1341
    }
1342
 
1343
    /**
1344
     * Update the settings for a single plugin.
1345
     *
1346
     * @param assign_plugin $plugin The plugin to update
1347
     * @param stdClass $formdata The form data
1348
     * @return bool false if an error occurs
1349
     */
1441 ariadna 1350
    protected function update_plugin_instance(assign_plugin $plugin, stdClass $formdata) {
1 efrain 1351
        if ($plugin->is_visible()) {
1352
            $enabledname = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
1353
            if (!empty($formdata->$enabledname)) {
1354
                $plugin->enable();
1355
                if (!$plugin->save_settings($formdata)) {
1356
                    throw new \moodle_exception($plugin->get_error());
1357
                    return false;
1358
                }
1359
            } else {
1360
                $plugin->disable();
1361
            }
1362
        }
1363
        return true;
1364
    }
1365
 
1366
    /**
1367
     * Update the gradebook information for this assignment.
1368
     *
1369
     * @param bool $reset If true, will reset all grades in the gradbook for this assignment
1370
     * @param int $coursemoduleid This is required because it might not exist in the database yet
1371
     * @return bool
1372
     */
1441 ariadna 1373
    public function update_gradebook($reset, $coursemoduleid) {
1 efrain 1374
        global $CFG;
1375
 
1441 ariadna 1376
        require_once($CFG->dirroot.'/mod/assign/lib.php');
1 efrain 1377
        $assign = clone $this->get_instance();
1378
        $assign->cmidnumber = $coursemoduleid;
1379
 
1380
        // Set assign gradebook feedback plugin status (enabled and visible).
1381
        $assign->gradefeedbackenabled = $this->is_gradebook_feedback_enabled();
1382
 
1383
        $param = null;
1384
        if ($reset) {
1385
            $param = 'reset';
1386
        }
1387
 
1388
        return assign_grade_item_update($assign, $param);
1389
    }
1390
 
1391
    /**
1392
     * Get the marking table page size
1393
     *
1394
     * @return integer
1395
     */
1441 ariadna 1396
    public function get_assign_perpage() {
1 efrain 1397
        $perpage = (int) get_user_preferences('assign_perpage', 10);
1398
        $adminconfig = $this->get_admin_config();
1399
        $maxperpage = -1;
1400
        if (isset($adminconfig->maxperpage)) {
1401
            $maxperpage = $adminconfig->maxperpage;
1402
        }
1441 ariadna 1403
        if (isset($maxperpage) &&
1 efrain 1404
            $maxperpage != -1 &&
1441 ariadna 1405
            ($perpage == -1 || $perpage > $maxperpage)) {
1 efrain 1406
            $perpage = $maxperpage;
1407
        }
1408
        return $perpage;
1409
    }
1410
 
1411
    /**
1412
     * Load and cache the admin config for this module.
1413
     *
1414
     * @return stdClass the plugin config
1415
     */
1441 ariadna 1416
    public function get_admin_config() {
1 efrain 1417
        if ($this->adminconfig) {
1418
            return $this->adminconfig;
1419
        }
1420
        $this->adminconfig = get_config('assign');
1421
        return $this->adminconfig;
1422
    }
1423
 
1424
    /**
1425
     * Update the calendar entries for this assignment.
1426
     *
1427
     * @param int $coursemoduleid - Required to pass this in because it might
1428
     *                              not exist in the database yet.
1429
     * @return bool
1430
     */
1441 ariadna 1431
    public function update_calendar($coursemoduleid) {
1 efrain 1432
        global $DB, $CFG;
1441 ariadna 1433
        require_once($CFG->dirroot.'/calendar/lib.php');
1 efrain 1434
 
1435
        // Special case for add_instance as the coursemodule has not been set yet.
1436
        $instance = $this->get_instance();
1437
 
1438
        // Start with creating the event.
1439
        $event = new stdClass();
1440
        $event->modulename  = 'assign';
1441
        $event->courseid = $instance->course;
1442
        $event->groupid = 0;
1443
        $event->userid  = 0;
1444
        $event->instance  = $instance->id;
1445
        $event->type = CALENDAR_EVENT_TYPE_ACTION;
1446
 
1447
        // Convert the links to pluginfile. It is a bit hacky but at this stage the files
1448
        // might not have been saved in the module area yet.
1449
        $intro = $instance->intro;
1450
        if ($draftid = file_get_submitted_draft_itemid('introeditor')) {
1451
            $intro = file_rewrite_urls_to_pluginfile($intro, $draftid);
1452
        }
1453
 
1454
        // We need to remove the links to files as the calendar is not ready
1455
        // to support module events with file areas.
1456
        $intro = strip_pluginfile_content($intro);
1457
        if ($this->show_intro()) {
1458
            $event->description = array(
1459
                'text' => $intro,
1460
                'format' => $instance->introformat
1461
            );
1462
        } else {
1463
            $event->description = array(
1464
                'text' => '',
1465
                'format' => $instance->introformat
1466
            );
1467
        }
1468
 
1469
        $eventtype = ASSIGN_EVENT_TYPE_DUE;
1470
        if ($instance->duedate) {
1471
            $event->name = get_string('calendardue', 'assign', $instance->name);
1472
            $event->eventtype = $eventtype;
1473
            $event->timestart = $instance->duedate;
1474
            $event->timesort = $instance->duedate;
1475
            $select = "modulename = :modulename
1476
                       AND instance = :instance
1477
                       AND eventtype = :eventtype
1478
                       AND groupid = 0
1479
                       AND courseid <> 0";
1480
            $params = array('modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => $eventtype);
1481
            $event->id = $DB->get_field_select('event', 'id', $select, $params);
1482
 
1483
            // Now process the event.
1484
            if ($event->id) {
1485
                $calendarevent = calendar_event::load($event->id);
1486
                $calendarevent->update($event, false);
1487
            } else {
1488
                calendar_event::create($event, false);
1489
            }
1490
        } else {
1441 ariadna 1491
            $DB->delete_records('event', array('modulename' => 'assign', 'instance' => $instance->id,
1492
                'eventtype' => $eventtype));
1 efrain 1493
        }
1494
 
1495
        $eventtype = ASSIGN_EVENT_TYPE_GRADINGDUE;
1496
        if ($instance->gradingduedate) {
1497
            $event->name = get_string('calendargradingdue', 'assign', $instance->name);
1498
            $event->eventtype = $eventtype;
1499
            $event->timestart = $instance->gradingduedate;
1500
            $event->timesort = $instance->gradingduedate;
1441 ariadna 1501
            $event->id = $DB->get_field('event', 'id', array('modulename' => 'assign',
1502
                'instance' => $instance->id, 'eventtype' => $event->eventtype));
1 efrain 1503
 
1504
            // Now process the event.
1505
            if ($event->id) {
1506
                $calendarevent = calendar_event::load($event->id);
1507
                $calendarevent->update($event, false);
1508
            } else {
1509
                calendar_event::create($event, false);
1510
            }
1511
        } else {
1441 ariadna 1512
            $DB->delete_records('event', array('modulename' => 'assign', 'instance' => $instance->id,
1513
                'eventtype' => $eventtype));
1 efrain 1514
        }
1515
 
1516
        return true;
1517
    }
1518
 
1519
    /**
1520
     * Update this instance in the database.
1521
     *
1522
     * @param stdClass $formdata - the data submitted from the form
1523
     * @return bool false if an error occurs
1524
     */
1441 ariadna 1525
    public function update_instance($formdata) {
1 efrain 1526
        global $DB;
1527
        $adminconfig = $this->get_admin_config();
1528
 
1529
        $update = new stdClass();
1530
        $update->id = $formdata->instance;
1531
        $update->name = $formdata->name;
1532
        $update->timemodified = time();
1533
        $update->course = $formdata->course;
1534
        $update->intro = $formdata->intro;
1535
        $update->introformat = $formdata->introformat;
1536
        $update->alwaysshowdescription = !empty($formdata->alwaysshowdescription);
1537
        if (isset($formdata->activityeditor)) {
1538
            $update->activity = $this->save_editor_draft_files($formdata);
1539
            $update->activityformat = $formdata->activityeditor['format'];
1540
        }
1541
        if (isset($formdata->submissionattachments)) {
1542
            $update->submissionattachments = $formdata->submissionattachments;
1543
        }
1544
        $update->submissiondrafts = $formdata->submissiondrafts;
1545
        $update->requiresubmissionstatement = $formdata->requiresubmissionstatement;
1546
        $update->sendnotifications = $formdata->sendnotifications;
1547
        $update->sendlatenotifications = $formdata->sendlatenotifications;
1548
        $update->sendstudentnotifications = $adminconfig->sendstudentnotifications;
1549
        if (isset($formdata->sendstudentnotifications)) {
1550
            $update->sendstudentnotifications = $formdata->sendstudentnotifications;
1551
        }
1552
        $update->duedate = $formdata->duedate;
1553
        $update->cutoffdate = $formdata->cutoffdate;
1554
        if (isset($formdata->timelimit)) {
1555
            $update->timelimit = $formdata->timelimit;
1556
        }
1557
        $update->gradingduedate = $formdata->gradingduedate;
1558
        $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate;
1559
        $update->grade = $formdata->grade;
1560
        if (!empty($formdata->completionunlocked)) {
1561
            $update->completionsubmit = !empty($formdata->completionsubmit);
1562
        }
1563
        $update->teamsubmission = $formdata->teamsubmission;
1564
        $update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit;
1565
        if (isset($formdata->teamsubmissiongroupingid)) {
1566
            $update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid;
1567
        }
1568
        if (isset($formdata->hidegrader)) {
1569
            $update->hidegrader = $formdata->hidegrader;
1570
        }
1571
        $update->blindmarking = $formdata->blindmarking;
1441 ariadna 1572
        $update->maxattempts = $formdata->maxattempts ?? 1;
1573
        $update->attemptreopenmethod = $formdata->attemptreopenmethod ?? ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS;
1 efrain 1574
        if (isset($formdata->preventsubmissionnotingroup)) {
1575
            $update->preventsubmissionnotingroup = $formdata->preventsubmissionnotingroup;
1576
        }
1577
        $update->markingworkflow = $formdata->markingworkflow;
1578
        $update->markingallocation = $formdata->markingallocation;
1579
        if (empty($update->markingworkflow)) { // If marking workflow is disabled, make sure allocation is disabled.
1580
            $update->markingallocation = 0;
1581
        }
1582
        $update->markinganonymous = $formdata->markinganonymous;
1583
        // If marking workflow is disabled, or blindmarking is disabled then make sure marking anonymous is disabled.
1584
        if (empty($update->markingworkflow) || empty($update->blindmarking)) {
1585
            $update->markinganonymous = 0;
1586
        }
1587
 
1441 ariadna 1588
        // Grade penalties.
1589
        $update->gradepenalty = $formdata->gradepenalty ?? 0;
1590
 
1 efrain 1591
        $result = $DB->update_record('assign', $update);
1441 ariadna 1592
        $this->instance = $DB->get_record('assign', array('id'=>$update->id), '*', MUST_EXIST);
1 efrain 1593
 
1594
        $this->save_intro_draft_files($formdata);
1595
 
1596
        // Load the assignment so the plugins have access to it.
1597
 
1598
        // Call save_settings hook for submission plugins.
1599
        foreach ($this->submissionplugins as $plugin) {
1600
            if (!$this->update_plugin_instance($plugin, $formdata)) {
1601
                throw new \moodle_exception($plugin->get_error());
1602
                return false;
1603
            }
1604
        }
1605
        foreach ($this->feedbackplugins as $plugin) {
1606
            if (!$this->update_plugin_instance($plugin, $formdata)) {
1607
                throw new \moodle_exception($plugin->get_error());
1608
                return false;
1609
            }
1610
        }
1611
 
1612
        $this->update_calendar($this->get_course_module()->id);
1613
        $completionexpected = (!empty($formdata->completionexpected)) ? $formdata->completionexpected : null;
1441 ariadna 1614
        \core_completion\api::update_completion_date_event($this->get_course_module()->id, 'assign', $this->instance,
1615
                $completionexpected);
1 efrain 1616
        $this->update_gradebook(false, $this->get_course_module()->id);
1617
 
1618
        $update = new stdClass();
1619
        $update->id = $this->get_instance()->id;
1441 ariadna 1620
        $update->nosubmissions = (!$this->is_any_submission_plugin_enabled()) ? 1: 0;
1 efrain 1621
        $DB->update_record('assign', $update);
1622
 
1441 ariadna 1623
        // Check if we need to recalculate penalty for existing grades.
1624
        if (!empty($formdata->recalculatepenalty) && $formdata->recalculatepenalty === 'yes') {
1625
            $assign = clone $this->get_instance();
1626
            $assign->cmidnumber = $this->get_course_module()->idnumber;
1627
            assign_update_grades($assign);
1628
        }
1629
 
1 efrain 1630
        return $result;
1631
    }
1632
 
1633
    /**
1634
     * Save the attachments in the intro description.
1635
     *
1636
     * @param stdClass $formdata
1637
     */
1441 ariadna 1638
    protected function save_intro_draft_files($formdata) {
1 efrain 1639
        if (isset($formdata->introattachments)) {
1441 ariadna 1640
            file_save_draft_area_files($formdata->introattachments, $this->get_context()->id,
1641
                                       'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0);
1 efrain 1642
        }
1643
    }
1644
 
1645
    /**
1646
     * Save the attachments in the editor description.
1647
     *
1648
     * @param stdClass $formdata
1649
     */
1441 ariadna 1650
    protected function save_editor_draft_files($formdata): string {
1 efrain 1651
        $text = '';
1652
        if (isset($formdata->activityeditor)) {
1653
            $text = $formdata->activityeditor['text'];
1654
            if (isset($formdata->activityeditor['itemid'])) {
1441 ariadna 1655
                $text = file_save_draft_area_files($formdata->activityeditor['itemid'], $this->get_context()->id,
1656
                    'mod_assign', ASSIGN_ACTIVITYATTACHMENT_FILEAREA,
1657
                    0, array('subdirs' => true), $formdata->activityeditor['text']);
1 efrain 1658
            }
1659
        }
1660
        return $text;
1661
    }
1662
 
1663
 
1664
    /**
1665
     * Add elements in grading plugin form.
1666
     *
1667
     * @param mixed $grade stdClass|null
1668
     * @param MoodleQuickForm $mform
1669
     * @param stdClass $data
1670
     * @param int $userid - The userid we are grading
1671
     * @return void
1672
     */
1441 ariadna 1673
    protected function add_plugin_grade_elements($grade, MoodleQuickForm $mform, stdClass $data, $userid) {
1 efrain 1674
        foreach ($this->feedbackplugins as $plugin) {
1675
            if ($plugin->is_enabled() && $plugin->is_visible()) {
1676
                $plugin->get_form_elements_for_user($grade, $mform, $data, $userid);
1677
            }
1678
        }
1679
    }
1680
 
1681
 
1682
 
1683
    /**
1684
     * Add one plugins settings to edit plugin form.
1685
     *
1686
     * @param assign_plugin $plugin The plugin to add the settings from
1687
     * @param MoodleQuickForm $mform The form to add the configuration settings to.
1688
     *                               This form is modified directly (not returned).
1689
     * @param array $pluginsenabled A list of form elements to be added to a group.
1690
     *                              The new element is added to this array by this function.
1691
     * @return void
1692
     */
1441 ariadna 1693
    protected function add_plugin_settings(assign_plugin $plugin, MoodleQuickForm $mform, & $pluginsenabled) {
1 efrain 1694
        global $CFG;
1695
        if ($plugin->is_visible() && !$plugin->is_configurable() && $plugin->is_enabled()) {
1696
            $name = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
1697
            $pluginsenabled[] = $mform->createElement('hidden', $name, 1);
1698
            $mform->setType($name, PARAM_BOOL);
1699
            $plugin->get_settings($mform);
1700
        } else if ($plugin->is_visible() && $plugin->is_configurable()) {
1701
            $name = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
1702
            $label = $plugin->get_name();
1703
            $pluginsenabled[] = $mform->createElement('checkbox', $name, '', $label);
1704
            $helpicon = $this->get_renderer()->help_icon('enabled', $plugin->get_subtype() . '_' . $plugin->get_type());
1705
            $pluginsenabled[] = $mform->createElement('static', '', '', $helpicon);
1706
 
1707
            $default = get_config($plugin->get_subtype() . '_' . $plugin->get_type(), 'default');
1708
            if ($plugin->get_config('enabled') !== false) {
1709
                $default = $plugin->is_enabled();
1710
            }
1711
            $mform->setDefault($plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled', $default);
1712
 
1713
            $plugin->get_settings($mform);
1441 ariadna 1714
 
1 efrain 1715
        }
1716
    }
1717
 
1718
    /**
1719
     * Add settings to edit plugin form.
1720
     *
1721
     * @param MoodleQuickForm $mform The form to add the configuration settings to.
1722
     *                               This form is modified directly (not returned).
1723
     * @return void
1724
     */
1441 ariadna 1725
    public function add_all_plugin_settings(MoodleQuickForm $mform) {
1 efrain 1726
        $mform->addElement('header', 'submissiontypes', get_string('submissiontypes', 'assign'));
1727
 
1728
        $submissionpluginsenabled = array();
1729
        $group = $mform->addGroup(array(), 'submissionplugins', get_string('submissiontypes', 'assign'), array(' '), false);
1730
        foreach ($this->submissionplugins as $plugin) {
1731
            $this->add_plugin_settings($plugin, $mform, $submissionpluginsenabled);
1732
        }
1733
        $group->setElements($submissionpluginsenabled);
1734
 
1735
        $mform->addElement('header', 'feedbacktypes', get_string('feedbacktypes', 'assign'));
1736
        $feedbackpluginsenabled = array();
1737
        $group = $mform->addGroup(array(), 'feedbackplugins', get_string('feedbacktypes', 'assign'), array(' '), false);
1738
        foreach ($this->feedbackplugins as $plugin) {
1739
            $this->add_plugin_settings($plugin, $mform, $feedbackpluginsenabled);
1740
        }
1741
        $group->setElements($feedbackpluginsenabled);
1742
        $mform->setExpanded('submissiontypes');
1743
    }
1744
 
1745
    /**
1746
     * Allow each plugin an opportunity to update the defaultvalues
1747
     * passed in to the settings form (needed to set up draft areas for
1748
     * editor and filemanager elements)
1749
     *
1750
     * @param array $defaultvalues
1751
     */
1441 ariadna 1752
    public function plugin_data_preprocessing(&$defaultvalues) {
1 efrain 1753
        foreach ($this->submissionplugins as $plugin) {
1754
            if ($plugin->is_visible()) {
1755
                $plugin->data_preprocessing($defaultvalues);
1756
            }
1757
        }
1758
        foreach ($this->feedbackplugins as $plugin) {
1759
            if ($plugin->is_visible()) {
1760
                $plugin->data_preprocessing($defaultvalues);
1761
            }
1762
        }
1763
    }
1764
 
1765
    /**
1441 ariadna 1766
     * Allow each plugin to validiate the data from the assignments settings form.
1767
     *
1768
     * @param array $data as passed to mod_assign_mod_form::validation().
1769
     * @param array $files as passed to mod_assign_mod_form::validation().
1770
     * @return array and validation errors that should be displayed.
1771
     */
1772
    public function plugin_settings_validation(array $data, array $files): array {
1773
        $errors = [];
1774
 
1775
        foreach ($this->submissionplugins as $plugin) {
1776
            if ($plugin->is_visible()) {
1777
                $errors = array_merge($errors, $plugin->settings_validation($data, $files));
1778
            }
1779
        }
1780
 
1781
        foreach ($this->feedbackplugins as $plugin) {
1782
            if ($plugin->is_visible()) {
1783
                $errors = array_merge($errors, $plugin->settings_validation($data, $files));
1784
            }
1785
        }
1786
 
1787
        return $errors;
1788
    }
1789
 
1790
    /**
1 efrain 1791
     * Get the name of the current module.
1792
     *
1793
     * @return string the module name (Assignment)
1794
     */
1441 ariadna 1795
    protected function get_module_name() {
1 efrain 1796
        if (isset(self::$modulename)) {
1797
            return self::$modulename;
1798
        }
1799
        self::$modulename = get_string('modulename', 'assign');
1800
        return self::$modulename;
1801
    }
1802
 
1803
    /**
1804
     * Get the plural name of the current module.
1805
     *
1806
     * @return string the module name plural (Assignments)
1807
     */
1441 ariadna 1808
    protected function get_module_name_plural() {
1 efrain 1809
        if (isset(self::$modulenameplural)) {
1810
            return self::$modulenameplural;
1811
        }
1812
        self::$modulenameplural = get_string('modulenameplural', 'assign');
1813
        return self::$modulenameplural;
1814
    }
1815
 
1816
    /**
1817
     * Has this assignment been constructed from an instance?
1818
     *
1819
     * @return bool
1820
     */
1441 ariadna 1821
    public function has_instance() {
1 efrain 1822
        return $this->instance || $this->get_course_module();
1823
    }
1824
 
1825
    /**
1826
     * Get the settings for the current instance of this assignment.
1827
     *
1828
     * @return stdClass The settings
1829
     * @throws dml_exception
1830
     */
1441 ariadna 1831
    public function get_default_instance() {
1 efrain 1832
        global $DB;
1833
        if (!$this->instance && $this->get_course_module()) {
1834
            $params = array('id' => $this->get_course_module()->instance);
1835
            $this->instance = $DB->get_record('assign', $params, '*', MUST_EXIST);
1836
 
1837
            $this->userinstances = [];
1838
        }
1839
        return $this->instance;
1840
    }
1841
 
1842
    /**
1843
     * Get the settings for the current instance of this assignment
1844
     * @param int|null $userid the id of the user to load the assign instance for.
1845
     * @return stdClass The settings
1846
     */
1441 ariadna 1847
    public function get_instance(?int $userid = null): stdClass {
1 efrain 1848
        global $USER;
1849
        $userid = $userid ?? $USER->id;
1850
 
1851
        $this->instance = $this->get_default_instance();
1852
 
1853
        // If we have the user instance already, just return it.
1854
        if (isset($this->userinstances[$userid])) {
1855
            return $this->userinstances[$userid];
1856
        }
1857
 
1858
        // Calculate properties which vary per user.
1859
        $this->userinstances[$userid] = $this->calculate_properties($this->instance, $userid);
1860
        return $this->userinstances[$userid];
1861
    }
1862
 
1863
    /**
1864
     * Calculates and updates various properties based on the specified user.
1865
     *
1866
     * @param stdClass $record the raw assign record.
1867
     * @param int $userid the id of the user to calculate the properties for.
1868
     * @return stdClass a new record having calculated properties.
1869
     */
1441 ariadna 1870
    private function calculate_properties(\stdClass $record, int $userid): \stdClass {
1 efrain 1871
        $record = clone ($record);
1872
 
1873
        // Relative dates.
1874
        if (!empty($record->duedate)) {
1875
            $course = $this->get_course();
1876
            $usercoursedates = course_get_course_dates_for_user_id($course, $userid);
1877
            if ($usercoursedates['start']) {
1878
                $userprops = ['duedate' => $record->duedate + $usercoursedates['startoffset']];
1879
                $record = (object) array_merge((array) $record, (array) $userprops);
1880
            }
1881
        }
1882
        return $record;
1883
    }
1884
 
1885
    /**
1886
     * Get the primary grade item for this assign instance.
1887
     *
1888
     * @return grade_item The grade_item record
1889
     */
1441 ariadna 1890
    public function get_grade_item() {
1 efrain 1891
        if ($this->gradeitem) {
1892
            return $this->gradeitem;
1893
        }
1894
        $instance = $this->get_instance();
1441 ariadna 1895
        $params = array('itemtype' => 'mod',
1896
                        'itemmodule' => 'assign',
1897
                        'iteminstance' => $instance->id,
1898
                        'courseid' => $instance->course,
1899
                        'itemnumber' => 0);
1 efrain 1900
        $this->gradeitem = grade_item::fetch($params);
1901
        if (!$this->gradeitem) {
1902
            throw new coding_exception('Improper use of the assignment class. ' .
1441 ariadna 1903
                                       'Cannot load the grade item.');
1 efrain 1904
        }
1905
        return $this->gradeitem;
1906
    }
1907
 
1908
    /**
1909
     * Get the context of the current course.
1910
     *
1911
     * @return mixed context|null The course context
1912
     */
1441 ariadna 1913
    public function get_course_context() {
1 efrain 1914
        if (!$this->context && !$this->course) {
1915
            throw new coding_exception('Improper use of the assignment class. ' .
1441 ariadna 1916
                                       'Cannot load the course context.');
1 efrain 1917
        }
1918
        if ($this->context) {
1919
            return $this->context->get_course_context();
1920
        } else {
1921
            return context_course::instance($this->course->id);
1922
        }
1923
    }
1924
 
1925
 
1926
    /**
1927
     * Get the current course module.
1928
     *
1929
     * @return cm_info|null The course module or null if not known
1930
     */
1441 ariadna 1931
    public function get_course_module() {
1 efrain 1932
        if ($this->coursemodule) {
1933
            return $this->coursemodule;
1934
        }
1935
        if (!$this->context) {
1936
            return null;
1937
        }
1938
 
1939
        if ($this->context->contextlevel == CONTEXT_MODULE) {
1940
            $modinfo = get_fast_modinfo($this->get_course());
1941
            $this->coursemodule = $modinfo->get_cm($this->context->instanceid);
1942
            return $this->coursemodule;
1943
        }
1944
        return null;
1945
    }
1946
 
1947
    /**
1948
     * Get context module.
1949
     *
1950
     * @return context
1951
     */
1441 ariadna 1952
    public function get_context() {
1 efrain 1953
        return $this->context;
1954
    }
1955
 
1956
    /**
1957
     * Get the current course.
1958
     *
1959
     * @return mixed stdClass|null The course
1960
     */
1441 ariadna 1961
    public function get_course() {
1 efrain 1962
        global $DB;
1963
 
1964
        if ($this->course && is_object($this->course)) {
1965
            return $this->course;
1966
        }
1967
 
1968
        if (!$this->context) {
1969
            return null;
1970
        }
1971
        $params = array('id' => $this->get_course_context()->instanceid);
1972
        $this->course = $DB->get_record('course', $params, '*', MUST_EXIST);
1973
 
1974
        return $this->course;
1975
    }
1976
 
1977
    /**
1978
     * Count the number of intro attachments.
1979
     *
1980
     * @return int
1981
     */
1441 ariadna 1982
    protected function count_attachments() {
1 efrain 1983
 
1984
        $fs = get_file_storage();
1441 ariadna 1985
        $files = $fs->get_area_files($this->get_context()->id, 'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA,
1986
                        0, 'id', false);
1 efrain 1987
 
1988
        return count($files);
1989
    }
1990
 
1991
    /**
1992
     * Are there any intro attachments to display?
1993
     *
1994
     * @return boolean
1995
     */
1441 ariadna 1996
    protected function has_visible_attachments() {
1 efrain 1997
        return ($this->count_attachments() > 0);
1998
    }
1999
 
2000
    /**
2001
     * Check if the intro attachments should be provided to the user.
2002
     *
2003
     * @param int $userid User id.
2004
     * @return bool
2005
     */
1441 ariadna 2006
    public function should_provide_intro_attachments(int $userid): bool {
1 efrain 2007
        $instance = $this->get_instance($userid);
2008
 
2009
        // Check if user has permission to view attachments regardless of assignment settings.
2010
        if (has_capability('moodle/course:manageactivities', $this->get_context())) {
2011
            return true;
2012
        }
2013
 
2014
        // If assignment does not show intro, we never provide intro attachments.
2015
        if (!$this->show_intro()) {
2016
            return false;
2017
        }
2018
 
2019
        // If intro attachments should only be shown when submission is started, check if there is an open submission.
2020
        if (!empty($instance->submissionattachments) && !$this->submissions_open($userid, true)) {
2021
            return false;
2022
        }
2023
 
2024
        return true;
2025
    }
2026
 
2027
    /**
2028
     * Return a grade in user-friendly form, whether it's a scale or not.
2029
     *
2030
     * @param mixed $grade int|null
2031
     * @param boolean $editing Are we allowing changes to this grade?
2032
     * @param int $userid The user id the grade belongs to
2033
     * @param int $modified Timestamp from when the grade was last modified
1441 ariadna 2034
     * @param float $deductedmark The deducted mark if penalty is applied
1 efrain 2035
     * @return string User-friendly representation of grade
2036
     */
1441 ariadna 2037
    public function display_grade($grade, $editing, $userid = 0, $modified = 0, float $deductedmark = 0) {
2038
        global $DB, $PAGE;
1 efrain 2039
 
2040
        static $scalegrades = array();
2041
 
2042
        $o = '';
2043
 
2044
        if ($this->get_instance()->grade >= 0) {
2045
            // Normal number.
2046
            if ($editing && $this->get_instance()->grade > 0) {
2047
                if ($grade < 0) {
2048
                    $displaygrade = '';
2049
                } else {
2050
                    $displaygrade = format_float($grade, $this->get_grade_item()->get_decimals());
2051
                }
2052
                $o .= '<label class="accesshide" for="quickgrade_' . $userid . '">' .
1441 ariadna 2053
                       get_string('usergrade', 'assign') .
2054
                       '</label>';
1 efrain 2055
                $o .= '<input type="text"
2056
                              id="quickgrade_' . $userid . '"
2057
                              name="quickgrade_' . $userid . '"
2058
                              value="' .  $displaygrade . '"
2059
                              size="6"
2060
                              maxlength="10"
2061
                              class="quickgrade"/>';
2062
                $o .= '&nbsp;/&nbsp;' . format_float($this->get_instance()->grade, $this->get_grade_item()->get_decimals());
2063
            } else {
2064
                if ($grade == -1 || $grade === null) {
2065
                    $o .= '-';
2066
                } else {
2067
                    $item = $this->get_grade_item();
2068
                    $o .= grade_format_gradevalue($grade, $item);
2069
                    if ($item->get_displaytype() == GRADE_DISPLAY_TYPE_REAL) {
2070
                        // If displaying the raw grade, also display the total value.
2071
                        $o .= '&nbsp;/&nbsp;' . format_float($this->get_instance()->grade, $item->get_decimals());
2072
                    }
2073
                }
2074
            }
1441 ariadna 2075
 
2076
            // Add penalty indicator, icon only.
2077
            $penaltyindicator = '';
2078
            if ($deductedmark > 0) {
2079
                $gradegrade = new \grade_grade();
2080
                $gradegrade->deductedmark = $deductedmark;
2081
                $gradegrade->overridden = $userid > 0 ? $this->get_grade_item()->get_grade($userid)->overridden : 0;
2082
                $penaltyindicator = \core_grades\penalty_manager::show_penalty_indicator($gradegrade);
2083
            }
2084
 
2085
            return $penaltyindicator . $o;
1 efrain 2086
        } else {
2087
            // Scale.
2088
            if (empty($this->cache['scale'])) {
1441 ariadna 2089
                if ($scale = $DB->get_record('scale', array('id'=>-($this->get_instance()->grade)))) {
1 efrain 2090
                    $this->cache['scale'] = make_menu_from_list($scale->scale);
2091
                } else {
2092
                    $o .= '-';
2093
                    return $o;
2094
                }
2095
            }
2096
            if ($editing) {
2097
                $o .= '<label class="accesshide"
2098
                              for="quickgrade_' . $userid . '">' .
1441 ariadna 2099
                      get_string('usergrade', 'assign') .
2100
                      '</label>';
1 efrain 2101
                $o .= '<select name="quickgrade_' . $userid . '" class="quickgrade">';
2102
                $o .= '<option value="-1">' . get_string('nograde') . '</option>';
2103
                foreach ($this->cache['scale'] as $optionid => $option) {
2104
                    $selected = '';
2105
                    if ($grade == $optionid) {
2106
                        $selected = 'selected="selected"';
2107
                    }
2108
                    $o .= '<option value="' . $optionid . '" ' . $selected . '>' . $option . '</option>';
2109
                }
2110
                $o .= '</select>';
2111
                return $o;
2112
            } else {
2113
                $scaleid = (int)$grade;
2114
                if (isset($this->cache['scale'][$scaleid])) {
2115
                    $o .= $this->cache['scale'][$scaleid];
2116
                    return $o;
2117
                }
2118
                $o .= '-';
2119
                return $o;
2120
            }
2121
        }
2122
    }
2123
 
2124
    /**
2125
     * Get the submission status/grading status for all submissions in this assignment for the
2126
     * given paticipants.
2127
     *
2128
     * These statuses match the available filters (requiregrading, submitted, notsubmitted, grantedextension).
2129
     * If this is a group assignment, group info is also returned.
2130
     *
2131
     * @param array $participants an associative array where the key is the participant id and
2132
     *                            the value is the participant record.
2133
     * @return array an associative array where the key is the participant id and the value is
2134
     *               the participant record.
2135
     */
1441 ariadna 2136
    private function get_submission_info_for_participants($participants) {
1 efrain 2137
        global $DB;
2138
 
2139
        if (empty($participants)) {
2140
            return $participants;
2141
        }
2142
 
2143
        list($insql, $params) = $DB->get_in_or_equal(array_keys($participants), SQL_PARAMS_NAMED);
2144
 
2145
        $assignid = $this->get_instance()->id;
2146
        $params['assignmentid1'] = $assignid;
2147
        $params['assignmentid2'] = $assignid;
2148
        $params['assignmentid3'] = $assignid;
2149
 
2150
        $fields = 'SELECT u.id, s.status, s.timemodified AS stime, g.timemodified AS gtime, g.grade, uf.extensionduedate';
2151
        $from = ' FROM {user} u
2152
                         LEFT JOIN {assign_submission} s
2153
                                ON u.id = s.userid
2154
                               AND s.assignment = :assignmentid1
2155
                               AND s.latest = 1
2156
                         LEFT JOIN {assign_grades} g
2157
                                ON u.id = g.userid
2158
                               AND g.assignment = :assignmentid2
2159
                               AND g.attemptnumber = s.attemptnumber
2160
                         LEFT JOIN {assign_user_flags} uf
2161
                                ON u.id = uf.userid
2162
                               AND uf.assignment = :assignmentid3
2163
            ';
2164
        $where = ' WHERE u.id ' . $insql;
2165
 
2166
        if (!empty($this->get_instance()->blindmarking)) {
2167
            $from .= 'LEFT JOIN {assign_user_mapping} um
2168
                             ON u.id = um.userid
2169
                            AND um.assignment = :assignmentid4 ';
2170
            $params['assignmentid4'] = $assignid;
2171
            $fields .= ', um.id as recordid ';
2172
        }
2173
 
2174
        $sql = "$fields $from $where";
2175
 
2176
        $records = $DB->get_records_sql($sql, $params);
2177
 
2178
        if ($this->get_instance()->teamsubmission) {
2179
            // Get all groups.
1441 ariadna 2180
            $allgroups = groups_get_all_groups($this->get_course()->id,
2181
                                               array_keys($participants),
2182
                                               $this->get_instance()->teamsubmissiongroupingid,
2183
                                               'DISTINCT g.id, g.name');
2184
 
1 efrain 2185
        }
2186
        foreach ($participants as $userid => $participant) {
2187
            $participants[$userid]->fullname = $this->fullname($participant);
2188
            $participants[$userid]->submitted = false;
2189
            $participants[$userid]->requiregrading = false;
2190
            $participants[$userid]->grantedextension = false;
2191
            $participants[$userid]->submissionstatus = '';
2192
        }
2193
 
2194
        foreach ($records as $userid => $submissioninfo) {
2195
            // These filters are 100% the same as the ones in the grading table SQL.
2196
            $submitted = false;
2197
            $requiregrading = false;
2198
            $grantedextension = false;
2199
            $submissionstatus = !empty($submissioninfo->status) ? $submissioninfo->status : '';
2200
 
2201
            if (!empty($submissioninfo->stime) && $submissioninfo->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
2202
                $submitted = true;
2203
            }
2204
 
2205
            if ($submitted && ($submissioninfo->stime >= $submissioninfo->gtime ||
1441 ariadna 2206
                    empty($submissioninfo->gtime) ||
2207
                    $submissioninfo->grade === null)) {
1 efrain 2208
                $requiregrading = true;
2209
            }
2210
 
2211
            if (!empty($submissioninfo->extensionduedate)) {
2212
                $grantedextension = true;
2213
            }
2214
 
2215
            $participants[$userid]->submitted = $submitted;
2216
            $participants[$userid]->requiregrading = $requiregrading;
2217
            $participants[$userid]->grantedextension = $grantedextension;
2218
            $participants[$userid]->submissionstatus = $submissionstatus;
2219
            if ($this->get_instance()->teamsubmission) {
2220
                $group = $this->get_submission_group($userid);
2221
                if ($group) {
2222
                    $participants[$userid]->groupid = $group->id;
2223
                    $participants[$userid]->groupname = $group->name;
2224
                }
2225
            }
2226
        }
2227
        return $participants;
2228
    }
2229
 
2230
    /**
2231
     * Get the submission status/grading status for all submissions in this assignment.
2232
     * These statuses match the available filters (requiregrading, submitted, notsubmitted, grantedextension).
2233
     * If this is a group assignment, group info is also returned.
2234
     *
2235
     * @param int $currentgroup
2236
     * @param boolean $tablesort Apply current user table sorting preferences.
2237
     * @return array List of user records with extra fields 'submitted', 'notsubmitted', 'requiregrading', 'grantedextension',
2238
     *               'groupid', 'groupname'
2239
     */
1441 ariadna 2240
    public function list_participants_with_filter_status_and_group($currentgroup, $tablesort = false) {
1 efrain 2241
        $participants = $this->list_participants($currentgroup, false, $tablesort);
2242
 
2243
        if (empty($participants)) {
2244
            return $participants;
2245
        } else {
2246
            return $this->get_submission_info_for_participants($participants);
2247
        }
2248
    }
2249
 
2250
    /**
2251
     * Return a valid order by segment for list_participants that matches
2252
     * the sorting of the current grading table. Not every field is supported,
2253
     * we are only concerned with a list of users so we can't search on anything
2254
     * that is not part of the user information (like grading statud or last modified stuff).
2255
     *
2256
     * @return string Order by clause for list_participants
2257
     */
1441 ariadna 2258
    private function get_grading_sort_sql() {
1 efrain 2259
        $usersort = flexible_table::get_sort_for_table('mod_assign_grading');
2260
        // TODO Does not support custom user profile fields (MDL-70456).
2261
        $userfieldsapi = \core_user\fields::for_identity($this->context, false)->with_userpic();
2262
        $userfields = $userfieldsapi->get_required_fields();
2263
        $orderfields = explode(',', $usersort);
2264
        $validlist = [];
2265
 
2266
        foreach ($orderfields as $orderfield) {
2267
            $orderfield = trim($orderfield);
2268
            foreach ($userfields as $field) {
2269
                $parts = explode(' ', $orderfield);
2270
                if ($parts[0] == $field) {
2271
                    // Prepend the user table prefix and count this as a valid order field.
2272
                    array_push($validlist, 'u.' . $orderfield);
2273
                }
2274
            }
2275
        }
2276
        // Produce a final list.
2277
        $result = implode(',', $validlist);
2278
        if (empty($result)) {
2279
            // Fall back ordering when none has been set.
2280
            $result = 'u.lastname, u.firstname, u.id';
2281
        }
2282
 
2283
        return $result;
2284
    }
2285
 
2286
    /**
2287
     * Returns array with sql code and parameters returning all ids of users who have submitted an assignment.
2288
     *
2289
     * @param int $group The group that the query is for.
2290
     * @return array list($sql, $params)
2291
     */
1441 ariadna 2292
    protected function get_submitted_sql($group = 0) {
1 efrain 2293
        // We need to guarentee unique table names.
2294
        static $i = 0;
2295
        $i++;
2296
        $prefix = 'sa' . $i . '_';
2297
        $params = [
2298
            "{$prefix}assignment" => (int) $this->get_instance()->id,
2299
            "{$prefix}status" => ASSIGN_SUBMISSION_STATUS_NEW,
2300
        ];
2301
        $capjoin = get_enrolled_with_capabilities_join($this->context, $prefix, '', $group, $this->show_only_active_users());
2302
        $params += $capjoin->params;
2303
        $sql = "SELECT {$prefix}s.userid
2304
                  FROM {assign_submission} {$prefix}s
2305
                  JOIN {user} {$prefix}u ON {$prefix}u.id = {$prefix}s.userid
2306
                  $capjoin->joins
2307
                 WHERE {$prefix}s.assignment = :{$prefix}assignment
2308
                   AND {$prefix}s.status <> :{$prefix}status
2309
                   AND $capjoin->wheres";
2310
        return array($sql, $params);
2311
    }
2312
 
2313
    /**
2314
     * Load a list of users enrolled in the current course with the specified permission and group.
2315
     * 0 for no group.
2316
     * Apply any current sort filters from the grading table.
2317
     *
2318
     * @param int $currentgroup
2319
     * @param bool $idsonly
2320
     * @param bool $tablesort
2321
     * @return array List of user records
2322
     */
1441 ariadna 2323
    public function list_participants($currentgroup, $idsonly, $tablesort = false) {
1 efrain 2324
        global $DB, $USER;
2325
 
2326
        // Get the last known sort order for the grading table.
2327
 
2328
        if (empty($currentgroup)) {
2329
            $currentgroup = 0;
2330
        }
2331
 
2332
        $key = $this->context->id . '-' . $currentgroup . '-' . $this->show_only_active_users();
2333
        if (!isset($this->participants[$key])) {
1441 ariadna 2334
            list($esql, $params) = get_enrolled_sql($this->context, 'mod/assign:submit', $currentgroup,
2335
                    $this->show_only_active_users());
1 efrain 2336
            list($ssql, $sparams) = $this->get_submitted_sql($currentgroup);
2337
            $params += $sparams;
2338
 
2339
            $fields = 'u.*';
2340
            $orderby = 'u.lastname, u.firstname, u.id';
2341
 
2342
            $additionaljoins = '';
2343
            $additionalfilters = '';
2344
            $instance = $this->get_instance();
2345
            if (!empty($instance->blindmarking)) {
2346
                $additionaljoins .= " LEFT JOIN {assign_user_mapping} um
2347
                                  ON u.id = um.userid
2348
                                 AND um.assignment = :assignmentid1
2349
                           LEFT JOIN {assign_submission} s
2350
                                  ON u.id = s.userid
2351
                                 AND s.assignment = :assignmentid2
2352
                                 AND s.latest = 1
2353
                        ";
2354
                $params['assignmentid1'] = (int) $instance->id;
2355
                $params['assignmentid2'] = (int) $instance->id;
2356
                $fields .= ', um.id as recordid ';
2357
 
2358
                // Sort by submission time first, then by um.id to sort reliably by the blind marking id.
2359
                // Note, different DBs have different ordering of NULL values.
2360
                // Therefore we coalesce the current time into the timecreated field, and the max possible integer into
2361
                // the ID field.
2362
                if (empty($tablesort)) {
2363
                    $orderby = "COALESCE(s.timecreated, " . time() . ") ASC, COALESCE(s.id, " . PHP_INT_MAX . ") ASC, um.id ASC";
2364
                }
2365
            }
2366
 
1441 ariadna 2367
            if ($instance->markingworkflow &&
2368
                    $instance->markingallocation &&
2369
                    !has_capability('mod/assign:manageallocations', $this->get_context()) &&
2370
                    has_capability('mod/assign:grade', $this->get_context())) {
1 efrain 2371
 
2372
                $additionaljoins .= ' LEFT JOIN {assign_user_flags} uf
2373
                                     ON u.id = uf.userid
2374
                                     AND uf.assignment = :assignmentid3';
2375
 
2376
                $params['assignmentid3'] = (int) $instance->id;
2377
 
2378
                $additionalfilters .= ' AND uf.allocatedmarker = :markerid';
2379
                $params['markerid'] = $USER->id;
2380
            }
2381
 
1441 ariadna 2382
            // A user wants to view a particular user rather than a set of users.
2383
            if ($this->usersearch) {
2384
                if (isset($this->usersearch['userid'])) {
2385
                    $additionalfilters .= " AND u.id = :uid";
2386
                    $params['uid'] = $this->usersearch['userid'];
2387
                } else if ($this->usersearch['usersearch'] !== '') { // A user wants to view a subset of learners that match the search criteria.
2388
                    [
2389
                        'where' => $keywordswhere,
2390
                        'params' => $keywordsparams,
2391
                    ] = \core_user::get_users_search_sql($this->context, $this->usersearch['usersearch']);
2392
                    $additionalfilters .= " AND $keywordswhere";
2393
                    $params = array_merge($params, $keywordsparams);
2394
                }
2395
            }
2396
 
2397
            // Exclude suspended users from the list of participants.
2398
            $additionalfilters .= " AND u.suspended = 0 AND u.auth <> 'nologin'";
2399
 
1 efrain 2400
            $sql = "SELECT $fields
2401
                      FROM {user} u
2402
                      JOIN ($esql UNION $ssql) je ON je.id = u.id
2403
                           $additionaljoins
2404
                     WHERE u.deleted = 0
2405
                           $additionalfilters
2406
                  ORDER BY $orderby";
2407
 
2408
            $users = $DB->get_records_sql($sql, $params);
2409
 
2410
            $cm = $this->get_course_module();
2411
            $info = new \core_availability\info_module($cm);
2412
            $users = $info->filter_user_list($users);
2413
 
2414
            $this->participants[$key] = $users;
2415
        }
2416
 
2417
        if ($tablesort) {
2418
            // Resort the user list according to the grading table sort and filter settings.
2419
            $sortedfiltereduserids = $this->get_grading_userid_list(true, '');
2420
            $sortedfilteredusers = [];
2421
            foreach ($sortedfiltereduserids as $nextid) {
2422
                $nextid = intval($nextid);
2423
                if (isset($this->participants[$key][$nextid])) {
2424
                    $sortedfilteredusers[$nextid] = $this->participants[$key][$nextid];
2425
                }
2426
            }
2427
            $this->participants[$key] = $sortedfilteredusers;
2428
        }
2429
 
2430
        if ($idsonly) {
2431
            $idslist = array();
2432
            foreach ($this->participants[$key] as $id => $user) {
2433
                $idslist[$id] = new stdClass();
2434
                $idslist[$id]->id = $id;
2435
            }
2436
            return $idslist;
2437
        }
2438
        return $this->participants[$key];
2439
    }
2440
 
2441
    /**
2442
     * Load a user if they are enrolled in the current course. Populated with submission
2443
     * status for this assignment.
2444
     *
2445
     * @param int $userid
2446
     * @return null|stdClass user record
2447
     */
1441 ariadna 2448
    public function get_participant($userid) {
1 efrain 2449
        global $DB, $USER;
2450
 
2451
        if ($userid == $USER->id) {
2452
            $participant = clone ($USER);
2453
        } else {
2454
            $participant = $DB->get_record('user', array('id' => $userid));
2455
        }
2456
        if (!$participant) {
2457
            return null;
2458
        }
2459
 
2460
        if (!is_enrolled($this->context, $participant, '', $this->show_only_active_users())) {
2461
            return null;
2462
        }
2463
 
2464
        $result = $this->get_submission_info_for_participants(array($participant->id => $participant));
2465
 
2466
        $submissioninfo = $result[$participant->id];
2467
        if (!$submissioninfo->submitted && !has_capability('mod/assign:submit', $this->context, $userid)) {
2468
            return null;
2469
        }
2470
 
2471
        return $submissioninfo;
2472
    }
2473
 
2474
    /**
2475
     * Load a count of valid teams for this assignment.
2476
     *
2477
     * @param int $activitygroup Activity active group
2478
     * @return int number of valid teams
2479
     */
1441 ariadna 2480
    public function count_teams($activitygroup = 0) {
1 efrain 2481
 
2482
        $count = 0;
2483
 
2484
        $participants = $this->list_participants($activitygroup, true);
2485
 
2486
        // If a team submission grouping id is provided all good as all returned groups
2487
        // are the submission teams, but if no team submission grouping was specified
2488
        // $groups will contain all participants groups.
2489
        if ($this->get_instance()->teamsubmissiongroupingid) {
2490
 
2491
            // We restrict the users to the selected group ones.
1441 ariadna 2492
            $groups = groups_get_all_groups($this->get_course()->id,
2493
                                            array_keys($participants),
2494
                                            $this->get_instance()->teamsubmissiongroupingid,
2495
                                            'DISTINCT g.id, g.name');
1 efrain 2496
 
2497
            $count = count($groups);
2498
 
2499
            // When a specific group is selected we don't count the default group users.
2500
            if ($activitygroup == 0) {
2501
                if (empty($this->get_instance()->preventsubmissionnotingroup)) {
2502
                    // See if there are any users in the default group.
2503
                    $defaultusers = $this->get_submission_group_members(0, true);
2504
                    if (count($defaultusers) > 0) {
2505
                        $count += 1;
2506
                    }
2507
                }
2508
            } else if ($activitygroup != 0 && empty($groups)) {
2509
                // Set count to 1 if $groups returns empty.
2510
                // It means the group is not part of $this->get_instance()->teamsubmissiongroupingid.
2511
                $count = 1;
2512
            }
2513
        } else {
2514
            // It is faster to loop around participants if no grouping was specified.
2515
            $groups = array();
2516
            foreach ($participants as $participant) {
2517
                if ($group = $this->get_submission_group($participant->id)) {
2518
                    $groups[$group->id] = true;
2519
                } else if (empty($this->get_instance()->preventsubmissionnotingroup)) {
2520
                    $groups[0] = true;
2521
                }
2522
            }
2523
 
2524
            $count = count($groups);
2525
        }
2526
 
2527
        return $count;
2528
    }
2529
 
2530
    /**
2531
     * Load a count of active users enrolled in the current course with the specified permission and group.
2532
     * 0 for no group.
2533
     *
2534
     * @param int $currentgroup
2535
     * @return int number of matching users
2536
     */
1441 ariadna 2537
    public function count_participants($currentgroup) {
1 efrain 2538
        return count($this->list_participants($currentgroup, true));
2539
    }
2540
 
2541
    /**
2542
     * Load a count of active users submissions in the current module that require grading
2543
     * This means the submission modification time is more recent than the
2544
     * grading modification time and the status is SUBMITTED.
2545
     *
2546
     * @param mixed $currentgroup int|null the group for counting (if null the function will determine it)
2547
     * @return int number of matching submissions
2548
     */
1441 ariadna 2549
    public function count_submissions_need_grading($currentgroup = null) {
1 efrain 2550
        global $DB;
2551
 
2552
        if ($this->get_instance()->teamsubmission) {
2553
            // This does not make sense for group assignment because the submission is shared.
2554
            return 0;
2555
        }
2556
 
2557
        if ($currentgroup === null) {
2558
            $currentgroup = groups_get_activity_group($this->get_course_module(), true);
2559
        }
2560
        list($esql, $params) = get_enrolled_sql($this->get_context(), '', $currentgroup, true);
2561
 
2562
        $params['assignid'] = $this->get_instance()->id;
2563
        $params['submitted'] = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
2564
        $sqlscalegrade = $this->get_instance()->grade < 0 ? ' OR g.grade = -1' : '';
2565
 
2566
        $sql = 'SELECT COUNT(s.userid)
2567
                   FROM {assign_submission} s
2568
                   LEFT JOIN {assign_grades} g ON
2569
                        s.assignment = g.assignment AND
2570
                        s.userid = g.userid AND
2571
                        g.attemptnumber = s.attemptnumber
2572
                   JOIN(' . $esql . ') e ON e.id = s.userid
2573
                   WHERE
2574
                        s.latest = 1 AND
2575
                        s.assignment = :assignid AND
2576
                        s.timemodified IS NOT NULL AND
2577
                        s.status = :submitted AND
2578
                        (s.timemodified >= g.timemodified OR g.timemodified IS NULL OR g.grade IS NULL '
1441 ariadna 2579
                            . $sqlscalegrade . ')';
1 efrain 2580
 
2581
        return $DB->count_records_sql($sql, $params);
2582
    }
2583
 
2584
    /**
2585
     * Load a count of grades.
2586
     *
2587
     * @return int number of grades
2588
     */
1441 ariadna 2589
    public function count_grades() {
1 efrain 2590
        global $DB;
2591
 
2592
        if (!$this->has_instance()) {
2593
            return 0;
2594
        }
2595
 
2596
        $currentgroup = groups_get_activity_group($this->get_course_module(), true);
2597
        list($esql, $params) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
2598
 
2599
        $params['assignid'] = $this->get_instance()->id;
2600
 
2601
        $sql = 'SELECT COUNT(g.userid)
2602
                   FROM {assign_grades} g
2603
                   JOIN(' . $esql . ') e ON e.id = g.userid
2604
                   WHERE g.assignment = :assignid';
2605
 
2606
        return $DB->count_records_sql($sql, $params);
2607
    }
2608
 
2609
    /**
2610
     * Load a count of submissions.
2611
     *
2612
     * @param bool $includenew When true, also counts the submissions with status 'new'.
2613
     * @return int number of submissions
2614
     */
1441 ariadna 2615
    public function count_submissions($includenew = false) {
1 efrain 2616
        global $DB;
2617
 
2618
        if (!$this->has_instance()) {
2619
            return 0;
2620
        }
2621
 
2622
        $params = array();
2623
        $sqlnew = '';
2624
 
2625
        if (!$includenew) {
2626
            $sqlnew = ' AND s.status <> :status ';
2627
            $params['status'] = ASSIGN_SUBMISSION_STATUS_NEW;
2628
        }
2629
 
2630
        if ($this->get_instance()->teamsubmission) {
2631
            // We cannot join on the enrolment tables for group submissions (no userid).
2632
            $sql = 'SELECT COUNT(DISTINCT s.groupid)
2633
                        FROM {assign_submission} s
2634
                        WHERE
2635
                            s.assignment = :assignid AND
2636
                            s.timemodified IS NOT NULL AND
2637
                            s.userid = :groupuserid' .
1441 ariadna 2638
                            $sqlnew;
1 efrain 2639
 
2640
            $params['assignid'] = $this->get_instance()->id;
2641
            $params['groupuserid'] = 0;
2642
        } else {
2643
            $currentgroup = groups_get_activity_group($this->get_course_module(), true);
2644
            list($esql, $enrolparams) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
2645
 
2646
            $params = array_merge($params, $enrolparams);
2647
            $params['assignid'] = $this->get_instance()->id;
2648
 
2649
            $sql = 'SELECT COUNT(DISTINCT s.userid)
2650
                       FROM {assign_submission} s
2651
                       JOIN(' . $esql . ') e ON e.id = s.userid
2652
                       WHERE
2653
                            s.assignment = :assignid AND
2654
                            s.timemodified IS NOT NULL ' .
1441 ariadna 2655
                            $sqlnew;
2656
 
1 efrain 2657
        }
2658
 
2659
        return $DB->count_records_sql($sql, $params);
2660
    }
2661
 
2662
    /**
2663
     * Load a count of submissions with a specified status.
2664
     *
2665
     * @param string $status The submission status - should match one of the constants
2666
     * @param mixed $currentgroup int|null the group for counting (if null the function will determine it)
2667
     * @return int number of matching submissions
2668
     */
1441 ariadna 2669
    public function count_submissions_with_status($status, $currentgroup = null) {
1 efrain 2670
        global $DB;
2671
 
2672
        if ($currentgroup === null) {
2673
            $currentgroup = groups_get_activity_group($this->get_course_module(), true);
2674
        }
2675
        list($esql, $params) = get_enrolled_sql($this->get_context(), '', $currentgroup, true);
2676
 
2677
        $params['assignid'] = $this->get_instance()->id;
2678
        $params['assignid2'] = $this->get_instance()->id;
2679
        $params['submissionstatus'] = $status;
2680
 
2681
        if ($this->get_instance()->teamsubmission) {
2682
 
2683
            $groupsstr = '';
2684
            if ($currentgroup != 0) {
2685
                // If there is an active group we should only display the current group users groups.
2686
                $participants = $this->list_participants($currentgroup, true);
1441 ariadna 2687
                $groups = groups_get_all_groups($this->get_course()->id,
2688
                                                array_keys($participants),
2689
                                                $this->get_instance()->teamsubmissiongroupingid,
2690
                                                'DISTINCT g.id, g.name');
1 efrain 2691
                if (empty($groups)) {
2692
                    // If $groups is empty it means it is not part of $this->get_instance()->teamsubmissiongroupingid.
2693
                    // All submissions from students that do not belong to any of teamsubmissiongroupingid groups
2694
                    // count towards groupid = 0. Setting to true as only '0' key matters.
2695
                    $groups = [true];
2696
                }
2697
                list($groupssql, $groupsparams) = $DB->get_in_or_equal(array_keys($groups), SQL_PARAMS_NAMED);
2698
                $groupsstr = 's.groupid ' . $groupssql . ' AND';
2699
                $params = $params + $groupsparams;
2700
            }
2701
            $sql = 'SELECT COUNT(s.groupid)
2702
                        FROM {assign_submission} s
2703
                        WHERE
2704
                            s.latest = 1 AND
2705
                            s.assignment = :assignid AND
2706
                            s.timemodified IS NOT NULL AND
2707
                            s.userid = :groupuserid AND '
1441 ariadna 2708
                            . $groupsstr . '
1 efrain 2709
                            s.status = :submissionstatus';
2710
            $params['groupuserid'] = 0;
2711
        } else {
2712
            $sql = 'SELECT COUNT(s.userid)
2713
                        FROM {assign_submission} s
2714
                        JOIN(' . $esql . ') e ON e.id = s.userid
2715
                        WHERE
2716
                            s.latest = 1 AND
2717
                            s.assignment = :assignid AND
2718
                            s.timemodified IS NOT NULL AND
2719
                            s.status = :submissionstatus';
1441 ariadna 2720
 
1 efrain 2721
        }
2722
 
2723
        return $DB->count_records_sql($sql, $params);
2724
    }
2725
 
2726
    /**
2727
     * Utility function to get the userid for every row in the grading table
2728
     * so the order can be frozen while we iterate it.
2729
     *
2730
     * @param boolean $cached If true, the cached list from the session could be returned.
2731
     * @param string $useridlistid String value used for caching the participant list.
2732
     * @return array An array of userids
2733
     */
1441 ariadna 2734
    protected function get_grading_userid_list($cached = false, $useridlistid = '') {
1 efrain 2735
        global $SESSION;
2736
 
2737
        if ($cached) {
2738
            if (empty($useridlistid)) {
2739
                $useridlistid = $this->get_useridlist_key_id();
2740
            }
2741
            $useridlistkey = $this->get_useridlist_key($useridlistid);
2742
            if (empty($SESSION->mod_assign_useridlist[$useridlistkey])) {
2743
                $SESSION->mod_assign_useridlist[$useridlistkey] = $this->get_grading_userid_list(false, '');
2744
            }
2745
            return $SESSION->mod_assign_useridlist[$useridlistkey];
2746
        }
2747
        $filter = get_user_preferences('assign_filter', '');
2748
        $table = new assign_grading_table($this, 0, $filter, 0, false);
2749
 
2750
        $useridlist = $table->get_column_data('userid');
2751
 
2752
        return $useridlist;
2753
    }
2754
 
2755
    /**
2756
     * Is user id filtered by user filters and table preferences.
2757
     *
2758
     * @param int $userid User id that needs to be checked.
2759
     * @return bool
2760
     */
1441 ariadna 2761
    public function is_userid_filtered($userid) {
1 efrain 2762
        $users = $this->get_grading_userid_list();
2763
        return in_array($userid, $users);
2764
    }
2765
 
2766
    /**
2767
     * Finds all assignment notifications that have yet to be mailed out, and mails them.
2768
     *
2769
     * Cron function to be run periodically according to the moodle cron.
2770
     *
2771
     * @return bool
2772
     */
1441 ariadna 2773
    public static function cron() {
1 efrain 2774
        global $DB;
2775
 
2776
        // Only ever send a max of one days worth of updates.
2777
        $yesterday = time() - (24 * 3600);
2778
        $timenow   = time();
2779
        $task = \core\task\manager::get_scheduled_task(mod_assign\task\cron_task::class);
2780
        $lastruntime = $task->get_last_run_time();
2781
 
2782
        // Collect all submissions that require mailing.
2783
        // Submissions are included if all are true:
2784
        //   - The assignment is visible in the gradebook.
2785
        //   - No previous notification has been sent.
2786
        //   - The grader was a real user, not an automated process.
2787
        //   - The grade was updated in the past 24 hours.
2788
        //   - If marking workflow is enabled, the workflow state is at 'released'.
2789
        $sql = "SELECT g.id as gradeid, a.course, a.name, a.blindmarking, a.revealidentities, a.hidegrader,
2790
                       g.*, g.timemodified as lastmodified, cm.id as cmid, um.id as recordid
2791
                 FROM {assign} a
2792
                 JOIN {assign_grades} g ON g.assignment = a.id
2793
            LEFT JOIN {assign_user_flags} uf ON uf.assignment = a.id AND uf.userid = g.userid
2794
                 JOIN {course_modules} cm ON cm.course = a.course AND cm.instance = a.id
2795
                 JOIN {modules} md ON md.id = cm.module AND md.name = 'assign'
2796
                 JOIN {grade_items} gri ON gri.iteminstance = a.id AND gri.courseid = a.course AND gri.itemmodule = md.name
2797
            LEFT JOIN {assign_user_mapping} um ON g.id = um.userid AND um.assignment = a.id
2798
                 WHERE (a.markingworkflow = 0 OR (a.markingworkflow = 1 AND uf.workflowstate = :wfreleased)) AND
2799
                       g.grader > 0 AND uf.mailed = 0 AND gri.hidden = 0 AND
2800
                       g.timemodified >= :yesterday AND g.timemodified <= :today
2801
              ORDER BY a.course, cm.id";
2802
 
2803
        $params = array(
2804
            'yesterday' => $yesterday,
2805
            'today' => $timenow,
2806
            'wfreleased' => ASSIGN_MARKING_WORKFLOW_STATE_RELEASED,
2807
        );
2808
        $submissions = $DB->get_records_sql($sql, $params);
2809
 
2810
        if (!empty($submissions)) {
2811
 
2812
            mtrace('Processing ' . count($submissions) . ' assignment submissions ...');
2813
 
2814
            // Preload courses we are going to need those.
2815
            $courseids = array();
2816
            foreach ($submissions as $submission) {
2817
                $courseids[] = $submission->course;
2818
            }
2819
 
2820
            // Filter out duplicates.
2821
            $courseids = array_unique($courseids);
2822
            $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
2823
            list($courseidsql, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
2824
            $sql = 'SELECT c.*, ' . $ctxselect .
1441 ariadna 2825
                      ' FROM {course} c
1 efrain 2826
                 LEFT JOIN {context} ctx ON ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel
2827
                     WHERE c.id ' . $courseidsql;
2828
 
2829
            $params['contextlevel'] = CONTEXT_COURSE;
2830
            $courses = $DB->get_records_sql($sql, $params);
2831
 
2832
            // Clean up... this could go on for a while.
2833
            unset($courseids);
2834
            unset($ctxselect);
2835
            unset($courseidsql);
2836
            unset($params);
2837
 
2838
            // Message students about new feedback.
2839
            foreach ($submissions as $submission) {
2840
 
2841
                mtrace("Processing assignment submission $submission->id ...");
2842
 
2843
                // Do not cache user lookups - could be too many.
1441 ariadna 2844
                if (!$user = $DB->get_record('user', array('id'=>$submission->userid))) {
1 efrain 2845
                    mtrace('Could not find user ' . $submission->userid);
2846
                    continue;
2847
                }
2848
 
2849
                // Use a cache to prevent the same DB queries happening over and over.
2850
                if (!array_key_exists($submission->course, $courses)) {
2851
                    mtrace('Could not find course ' . $submission->course);
2852
                    continue;
2853
                }
2854
                $course = $courses[$submission->course];
2855
                if (isset($course->ctxid)) {
2856
                    // Context has not yet been preloaded. Do so now.
2857
                    context_helper::preload_from_record($course);
2858
                }
2859
 
2860
                // Override the language and timezone of the "current" user, so that
2861
                // mail is customised for the receiver.
2862
                \core\cron::setup_user($user, $course);
2863
 
2864
                // Context lookups are already cached.
2865
                $coursecontext = context_course::instance($course->id);
2866
                if (!is_enrolled($coursecontext, $user->id)) {
1441 ariadna 2867
                    $courseshortname = format_string($course->shortname,
2868
                                                     true,
2869
                                                     array('context' => $coursecontext));
1 efrain 2870
                    mtrace(fullname($user) . ' not an active participant in ' . $courseshortname);
2871
                    continue;
2872
                }
2873
 
1441 ariadna 2874
                if (!$grader = $DB->get_record('user', array('id'=>$submission->grader))) {
1 efrain 2875
                    mtrace('Could not find grader ' . $submission->grader);
2876
                    continue;
2877
                }
2878
 
2879
                $modinfo = get_fast_modinfo($course, $user->id);
2880
                $cm = $modinfo->get_cm($submission->cmid);
2881
                // Context lookups are already cached.
2882
                $contextmodule = context_module::instance($cm->id);
2883
 
2884
                if (!$cm->uservisible) {
2885
                    // Hold mail notification for assignments the user cannot access until later.
2886
                    continue;
2887
                }
2888
 
2889
                // Notify the student. Default to the non-anon version.
2890
                $messagetype = 'feedbackavailable';
2891
                // Message type needs 'anon' if "hidden grading" is enabled and the student
2892
                // doesn't have permission to see the grader.
2893
                if ($submission->hidegrader && !has_capability('mod/assign:showhiddengrader', $contextmodule, $user)) {
2894
                    $messagetype = 'feedbackavailableanon';
2895
                    // There's no point in having an "anonymous grader" if the notification email
2896
                    // comes from them. Send the email from the noreply user instead.
2897
                    $grader = core_user::get_noreply_user();
2898
                }
2899
 
2900
                $eventtype = 'assign_notification';
2901
                $updatetime = $submission->lastmodified;
2902
                $modulename = get_string('modulename', 'assign');
2903
 
2904
                $uniqueid = 0;
2905
                if ($submission->blindmarking && !$submission->revealidentities) {
2906
                    if (empty($submission->recordid)) {
2907
                        $uniqueid = self::get_uniqueid_for_user_static($submission->assignment, $grader->id);
2908
                    } else {
2909
                        $uniqueid = $submission->recordid;
2910
                    }
2911
                }
2912
                $showusers = $submission->blindmarking && !$submission->revealidentities;
1441 ariadna 2913
                self::send_assignment_notification($grader,
2914
                                                   $user,
2915
                                                   $messagetype,
2916
                                                   $eventtype,
2917
                                                   $updatetime,
2918
                                                   $cm,
2919
                                                   $contextmodule,
2920
                                                   $course,
2921
                                                   $modulename,
2922
                                                   $submission->name,
2923
                                                   $showusers,
2924
                                                   $uniqueid);
1 efrain 2925
 
1441 ariadna 2926
                $flags = $DB->get_record('assign_user_flags', array('userid'=>$user->id, 'assignment'=>$submission->assignment));
1 efrain 2927
                if ($flags) {
2928
                    $flags->mailed = 1;
2929
                    $DB->update_record('assign_user_flags', $flags);
2930
                } else {
2931
                    $flags = new stdClass();
2932
                    $flags->userid = $user->id;
2933
                    $flags->assignment = $submission->assignment;
2934
                    $flags->mailed = 1;
2935
                    $DB->insert_record('assign_user_flags', $flags);
2936
                }
2937
 
2938
                mtrace('Done');
2939
            }
2940
            mtrace('Done processing ' . count($submissions) . ' assignment submissions');
2941
 
2942
            \core\cron::setup_user();
2943
 
2944
            // Free up memory just to be sure.
2945
            unset($courses);
2946
        }
2947
 
2948
        // Update calendar events to provide a description.
2949
        $sql = 'SELECT id
2950
                    FROM {assign}
2951
                    WHERE
2952
                        allowsubmissionsfromdate >= :lastruntime AND
2953
                        allowsubmissionsfromdate <= :timenow AND
2954
                        alwaysshowdescription = 0';
2955
        $params = array('lastruntime' => $lastruntime, 'timenow' => $timenow);
2956
        $newlyavailable = $DB->get_records_sql($sql, $params);
2957
        foreach ($newlyavailable as $record) {
2958
            $cm = get_coursemodule_from_instance('assign', $record->id, 0, false, MUST_EXIST);
2959
            $context = context_module::instance($cm->id);
2960
 
2961
            $assignment = new assign($context, null, null);
2962
            $assignment->update_calendar($cm->id);
2963
        }
2964
 
2965
        return true;
2966
    }
2967
 
2968
    /**
2969
     * Mark in the database that this grade record should have an update notification sent by cron.
2970
     *
2971
     * @param stdClass $grade a grade record keyed on id
2972
     * @param bool $mailedoverride when true, flag notification to be sent again.
2973
     * @return bool true for success
2974
     */
1441 ariadna 2975
    public function notify_grade_modified($grade, $mailedoverride = false) {
1 efrain 2976
        global $DB;
2977
 
2978
        $flags = $this->get_user_flags($grade->userid, true);
2979
        if ($flags->mailed != 1 || $mailedoverride) {
2980
            $flags->mailed = 0;
2981
        }
2982
 
2983
        return $this->update_user_flags($flags);
2984
    }
2985
 
2986
    /**
2987
     * Update user flags for this user in this assignment.
2988
     *
2989
     * @param stdClass $flags a flags record keyed on id
2990
     * @return bool true for success
2991
     */
1441 ariadna 2992
    public function update_user_flags($flags) {
1 efrain 2993
        global $DB;
2994
        if ($flags->userid <= 0 || $flags->assignment <= 0 || $flags->id <= 0) {
2995
            return false;
2996
        }
2997
 
2998
        $result = $DB->update_record('assign_user_flags', $flags);
2999
        return $result;
3000
    }
3001
 
3002
    /**
3003
     * Update a grade in the grade table for the assignment and in the gradebook.
3004
     *
3005
     * @param stdClass $grade a grade record keyed on id
3006
     * @param bool $reopenattempt If the attempt reopen method is manual, allow another attempt at this assignment.
3007
     * @return bool true for success
3008
     */
1441 ariadna 3009
    public function update_grade($grade, $reopenattempt = false) {
1 efrain 3010
        global $DB;
3011
 
3012
        $grade->timemodified = time();
3013
 
3014
        if (!empty($grade->workflowstate)) {
3015
            $validstates = $this->get_marking_workflow_states_for_current_user();
3016
            if (!array_key_exists($grade->workflowstate, $validstates)) {
3017
                return false;
3018
            }
3019
        }
3020
 
3021
        if ($grade->grade && $grade->grade != -1) {
3022
            if ($this->get_instance()->grade > 0) {
3023
                if (!is_numeric($grade->grade)) {
3024
                    return false;
3025
                } else if ($grade->grade > $this->get_instance()->grade) {
3026
                    return false;
3027
                } else if ($grade->grade < 0) {
3028
                    return false;
3029
                }
3030
            } else {
3031
                // This is a scale.
1441 ariadna 3032
                if ($scale = $DB->get_record('scale', array('id' => -($this->get_instance()->grade)))) {
1 efrain 3033
                    $scaleoptions = make_menu_from_list($scale->scale);
3034
                    if (!array_key_exists((int) $grade->grade, $scaleoptions)) {
3035
                        return false;
3036
                    }
3037
                }
3038
            }
3039
        }
3040
 
3041
        if (empty($grade->attemptnumber)) {
3042
            // Set it to the default.
3043
            $grade->attemptnumber = 0;
3044
        }
3045
        $DB->update_record('assign_grades', $grade);
3046
 
3047
        $submission = null;
3048
        if ($this->get_instance()->teamsubmission) {
3049
            if (isset($this->mostrecentteamsubmission)) {
3050
                $submission = $this->mostrecentteamsubmission;
3051
            } else {
3052
                $submission = $this->get_group_submission($grade->userid, 0, false);
3053
            }
3054
        } else {
3055
            $submission = $this->get_user_submission($grade->userid, false);
3056
        }
3057
 
3058
        // Only push to gradebook if the update is for the most recent attempt.
3059
        if ($submission && $submission->attemptnumber != $grade->attemptnumber) {
3060
            return true;
3061
        }
3062
 
3063
        if ($this->gradebook_item_update(null, $grade)) {
3064
            \mod_assign\event\submission_graded::create_from_grade($this, $grade)->trigger();
3065
        }
3066
 
3067
        // If the conditions are met, allow another attempt.
3068
        if ($submission) {
1441 ariadna 3069
            $isreopened = $this->reopen_submission_if_required($grade->userid,
3070
                    $submission,
3071
                    $reopenattempt);
1 efrain 3072
            if ($isreopened) {
3073
                $completion = new completion_info($this->get_course());
1441 ariadna 3074
                if ($completion->is_enabled($this->get_course_module()) &&
3075
                    $this->get_instance()->completionsubmit) {
1 efrain 3076
                    $completion->update_state($this->get_course_module(), COMPLETION_INCOMPLETE, $grade->userid);
3077
                }
3078
            }
3079
        }
3080
 
3081
        return true;
3082
    }
3083
 
3084
    /**
3085
     * View the grant extension date page.
3086
     *
3087
     * Uses url parameters 'userid'
3088
     * or from parameter 'selectedusers'
3089
     *
3090
     * @param moodleform $mform - Used for validation of the submitted data
3091
     * @return string
3092
     */
1441 ariadna 3093
    protected function view_grant_extension($mform) {
1 efrain 3094
        global $CFG;
3095
        require_once($CFG->dirroot . '/mod/assign/extensionform.php');
3096
 
3097
        $o = '';
3098
 
3099
        $data = new stdClass();
3100
        $data->id = $this->get_course_module()->id;
3101
 
3102
        $formparams = array(
3103
            'instance' => $this->get_instance(),
3104
            'assign' => $this
3105
        );
3106
 
3107
        $users = optional_param('userid', 0, PARAM_INT);
3108
        if (!$users) {
3109
            $users = required_param('selectedusers', PARAM_SEQUENCE);
3110
        }
3111
        $userlist = explode(',', $users);
3112
 
3113
        $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate');
3114
        $maxoverride = array('allowsubmissionsfromdate' => 0, 'duedate' => 0, 'cutoffdate' => 0);
3115
        foreach ($userlist as $userid) {
3116
            // To validate extension date with users overrides.
3117
            $override = $this->override_exists($userid);
3118
            foreach ($keys as $key) {
3119
                if ($override->{$key}) {
3120
                    if ($maxoverride[$key] < $override->{$key}) {
3121
                        $maxoverride[$key] = $override->{$key};
3122
                    }
3123
                } else if ($maxoverride[$key] < $this->get_instance()->{$key}) {
3124
                    $maxoverride[$key] = $this->get_instance()->{$key};
3125
                }
3126
            }
3127
        }
3128
        foreach ($keys as $key) {
3129
            if ($maxoverride[$key]) {
3130
                $this->get_instance()->{$key} = $maxoverride[$key];
3131
            }
3132
        }
3133
 
3134
        $formparams['userlist'] = $userlist;
3135
 
3136
        $data->selectedusers = $users;
3137
        $data->userid = 0;
3138
 
3139
        if (empty($mform)) {
3140
            $mform = new mod_assign_extension_form(null, $formparams);
3141
        }
3142
        $mform->set_data($data);
1441 ariadna 3143
        $header = new assign_header($this->get_instance(),
3144
                                    $this->get_context(),
3145
                                    $this->show_intro(),
3146
                                    $this->get_course_module()->id,
3147
                                    get_string('grantextension', 'assign'));
1 efrain 3148
        $o .= $this->get_renderer()->render($header);
3149
        $o .= $this->get_renderer()->render(new assign_form('extensionform', $mform));
3150
        $o .= $this->view_footer();
3151
        return $o;
3152
    }
3153
 
3154
    /**
3155
     * Get a list of the users in the same group as this user.
3156
     *
3157
     * @param int $groupid The id of the group whose members we want or 0 for the default group
3158
     * @param bool $onlyids Whether to retrieve only the user id's
3159
     * @param bool $excludesuspended Whether to exclude suspended users
3160
     * @return array The users (possibly id's only)
3161
     */
1441 ariadna 3162
    public function get_submission_group_members($groupid, $onlyids, $excludesuspended = false) {
1 efrain 3163
        $members = array();
3164
        if ($groupid != 0) {
3165
            $allusers = $this->list_participants($groupid, $onlyids);
3166
            foreach ($allusers as $user) {
3167
                if ($this->get_submission_group($user->id)) {
3168
                    $members[] = $user;
3169
                }
3170
            }
3171
        } else {
3172
            $allusers = $this->list_participants(null, $onlyids);
3173
            foreach ($allusers as $user) {
3174
                if ($this->get_submission_group($user->id) == null) {
3175
                    $members[] = $user;
3176
                }
3177
            }
3178
        }
3179
        // Exclude suspended users, if user can't see them.
3180
        if ($excludesuspended || !has_capability('moodle/course:viewsuspendedusers', $this->context)) {
3181
            foreach ($members as $key => $member) {
3182
                if (!$this->is_active_user($member->id)) {
3183
                    unset($members[$key]);
3184
                }
3185
            }
3186
        }
3187
 
3188
        return $members;
3189
    }
3190
 
3191
    /**
3192
     * Get a list of the users in the same group as this user that have not submitted the assignment.
3193
     *
3194
     * @param int $groupid The id of the group whose members we want or 0 for the default group
3195
     * @param bool $onlyids Whether to retrieve only the user id's
3196
     * @return array The users (possibly id's only)
3197
     */
1441 ariadna 3198
    public function get_submission_group_members_who_have_not_submitted($groupid, $onlyids) {
1 efrain 3199
        $instance = $this->get_instance();
3200
        if (!$instance->teamsubmission || !$instance->requireallteammemberssubmit) {
3201
            return array();
3202
        }
3203
        $members = $this->get_submission_group_members($groupid, $onlyids);
3204
 
3205
        foreach ($members as $id => $member) {
3206
            $submission = $this->get_user_submission($member->id, false);
3207
            if ($submission && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
3208
                unset($members[$id]);
3209
            } else {
3210
                if ($this->is_blind_marking()) {
3211
                    $members[$id]->alias = get_string('hiddenuser', 'assign') .
1441 ariadna 3212
                                           $this->get_uniqueid_for_user($id);
1 efrain 3213
                }
3214
            }
3215
        }
3216
        return $members;
3217
    }
3218
 
3219
    /**
3220
     * Load the group submission object for a particular user, optionally creating it if required.
3221
     *
3222
     * @param int $userid The id of the user whose submission we want
3223
     * @param int $groupid The id of the group for this user - may be 0 in which
3224
     *                     case it is determined from the userid.
3225
     * @param bool $create If set to true a new submission object will be created in the database
3226
     *                     with the status set to "new".
3227
     * @param int $attemptnumber - -1 means the latest attempt
3228
     * @return stdClass|false The submission
3229
     */
1441 ariadna 3230
    public function get_group_submission($userid, $groupid, $create, $attemptnumber=-1) {
1 efrain 3231
        global $DB;
3232
 
3233
        if ($groupid == 0) {
3234
            $group = $this->get_submission_group($userid);
3235
            if ($group) {
3236
                $groupid = $group->id;
3237
            }
3238
        }
3239
 
3240
        // Now get the group submission.
1441 ariadna 3241
        $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
1 efrain 3242
        if ($attemptnumber >= 0) {
3243
            $params['attemptnumber'] = $attemptnumber;
3244
        }
3245
 
3246
        // Only return the row with the highest attemptnumber.
3247
        $submission = null;
3248
        $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', '*', 0, 1);
3249
        if ($submissions) {
3250
            $submission = reset($submissions);
3251
        }
3252
 
3253
        if ($submission) {
3254
            if ($create) {
3255
                $action = optional_param('action', '', PARAM_TEXT);
3256
                if ($action == 'editsubmission') {
3257
                    if (empty($submission->timestarted) && $this->get_instance()->timelimit) {
3258
                        $submission->timestarted = time();
3259
                        $DB->update_record('assign_submission', $submission);
3260
                    }
3261
                }
3262
            }
3263
            return $submission;
3264
        }
3265
        if ($create) {
3266
            $submission = new stdClass();
3267
            $submission->assignment = $this->get_instance()->id;
3268
            $submission->userid = 0;
3269
            $submission->groupid = $groupid;
3270
            $submission->timecreated = time();
3271
            $submission->timemodified = $submission->timecreated;
3272
            if ($attemptnumber >= 0) {
3273
                $submission->attemptnumber = $attemptnumber;
3274
            } else {
3275
                $submission->attemptnumber = 0;
3276
            }
3277
            // Work out if this is the latest submission.
3278
            $submission->latest = 0;
1441 ariadna 3279
            $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
1 efrain 3280
            if ($attemptnumber == -1) {
3281
                // This is a new submission so it must be the latest.
3282
                $submission->latest = 1;
3283
            } else {
3284
                // We need to work this out.
3285
                $result = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', 'attemptnumber', 0, 1);
3286
                if ($result) {
3287
                    $latestsubmission = reset($result);
3288
                }
3289
                if (!$latestsubmission || ($attemptnumber == $latestsubmission->attemptnumber)) {
3290
                    $submission->latest = 1;
3291
                }
3292
            }
3293
            $transaction = $DB->start_delegated_transaction();
3294
            if ($submission->latest) {
3295
                // This is the case when we need to set latest to 0 for all the other attempts.
3296
                $DB->set_field('assign_submission', 'latest', 0, $params);
3297
            }
3298
            $submission->status = ASSIGN_SUBMISSION_STATUS_NEW;
3299
            $sid = $DB->insert_record('assign_submission', $submission);
3300
            $transaction->allow_commit();
3301
            return $DB->get_record('assign_submission', array('id' => $sid));
3302
        }
3303
        return false;
3304
    }
3305
 
3306
    /**
3307
     * View a page rendered by a plugin.
3308
     *
3309
     * Uses url parameters 'pluginaction', 'pluginsubtype', 'plugin', and 'id'.
3310
     *
3311
     * @return string
3312
     */
1441 ariadna 3313
    protected function view_plugin_page() {
1 efrain 3314
        global $USER;
3315
 
3316
        $o = '';
3317
 
3318
        $pluginsubtype = required_param('pluginsubtype', PARAM_ALPHA);
3319
        $plugintype = required_param('plugin', PARAM_PLUGIN);
3320
        $pluginaction = required_param('pluginaction', PARAM_ALPHA);
3321
 
3322
        $plugin = $this->get_plugin_by_type($pluginsubtype, $plugintype);
3323
        if (!$plugin) {
3324
            throw new \moodle_exception('invalidformdata', '');
3325
            return;
3326
        }
3327
 
3328
        $o .= $plugin->view_page($pluginaction);
3329
 
3330
        return $o;
3331
    }
3332
 
3333
 
3334
    /**
3335
     * This is used for team assignments to get the group for the specified user.
3336
     * If the user is a member of multiple or no groups this will return false
3337
     *
3338
     * @param int $userid The id of the user whose submission we want
3339
     * @return mixed The group or false
3340
     */
1441 ariadna 3341
    public function get_submission_group($userid) {
1 efrain 3342
 
3343
        if (isset($this->usersubmissiongroups[$userid])) {
3344
            return $this->usersubmissiongroups[$userid];
3345
        }
3346
 
3347
        $groups = $this->get_all_groups($userid);
3348
        if (count($groups) != 1) {
3349
            $return = false;
3350
        } else {
3351
            $return = array_pop($groups);
3352
        }
3353
 
3354
        // Cache the user submission group.
3355
        $this->usersubmissiongroups[$userid] = $return;
3356
 
3357
        return $return;
3358
    }
3359
 
3360
    /**
3361
     * Gets all groups the user is a member of.
3362
     *
3363
     * @param int $userid Teh id of the user who's groups we are checking
3364
     * @return array The group objects
3365
     */
1441 ariadna 3366
    public function get_all_groups($userid) {
1 efrain 3367
        if (isset($this->usergroups[$userid])) {
3368
            return $this->usergroups[$userid];
3369
        }
3370
 
3371
        $grouping = $this->get_instance()->teamsubmissiongroupingid;
3372
        $return = groups_get_all_groups($this->get_course()->id, $userid, $grouping, 'g.*', false, true);
3373
 
3374
        $this->usergroups[$userid] = $return;
3375
 
3376
        return $return;
3377
    }
3378
 
3379
 
3380
    /**
3381
     * Display the submission that is used by a plugin.
3382
     *
3383
     * Uses url parameters 'sid', 'gid' and 'plugin'.
3384
     *
3385
     * @param string $pluginsubtype
3386
     * @return string
3387
     */
1441 ariadna 3388
    protected function view_plugin_content($pluginsubtype) {
1 efrain 3389
        $o = '';
3390
 
3391
        $submissionid = optional_param('sid', 0, PARAM_INT);
3392
        $gradeid = optional_param('gid', 0, PARAM_INT);
3393
        $plugintype = required_param('plugin', PARAM_PLUGIN);
3394
        $item = null;
3395
        if ($pluginsubtype == 'assignsubmission') {
3396
            $plugin = $this->get_submission_plugin_by_type($plugintype);
3397
            if ($submissionid <= 0) {
3398
                throw new coding_exception('Submission id should not be 0');
3399
            }
3400
            $item = $this->get_submission($submissionid);
3401
 
3402
            // Check permissions.
3403
            if (empty($item->userid)) {
3404
                // Group submission.
3405
                $this->require_view_group_submission($item->groupid);
3406
            } else {
3407
                $this->require_view_submission($item->userid);
3408
            }
1441 ariadna 3409
            $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
3410
                                                              $this->get_context(),
3411
                                                              $this->show_intro(),
3412
                                                              $this->get_course_module()->id,
3413
                                                              $plugin->get_name()));
3414
            $o .= $this->get_renderer()->render(new assign_submission_plugin_submission($plugin,
3415
                                                              $item,
3416
                                                              assign_submission_plugin_submission::FULL,
3417
                                                              $this->get_course_module()->id,
3418
                                                              $this->get_return_action(),
3419
                                                              $this->get_return_params()));
1 efrain 3420
 
3421
            // Trigger event for viewing a submission.
3422
            \mod_assign\event\submission_viewed::create_from_submission($this, $item)->trigger();
1441 ariadna 3423
 
1 efrain 3424
        } else {
3425
            $plugin = $this->get_feedback_plugin_by_type($plugintype);
3426
            if ($gradeid <= 0) {
3427
                throw new coding_exception('Grade id should not be 0');
3428
            }
3429
            $item = $this->get_grade($gradeid);
3430
            // Check permissions.
3431
            $this->require_view_submission($item->userid);
1441 ariadna 3432
            $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
3433
                                                              $this->get_context(),
3434
                                                              $this->show_intro(),
3435
                                                              $this->get_course_module()->id,
3436
                                                              $plugin->get_name()));
3437
            $o .= $this->get_renderer()->render(new assign_feedback_plugin_feedback($plugin,
3438
                                                              $item,
3439
                                                              assign_feedback_plugin_feedback::FULL,
3440
                                                              $this->get_course_module()->id,
3441
                                                              $this->get_return_action(),
3442
                                                              $this->get_return_params()));
1 efrain 3443
 
3444
            // Trigger event for viewing feedback.
3445
            \mod_assign\event\feedback_viewed::create_from_grade($this, $item)->trigger();
3446
        }
3447
 
3448
        $o .= $this->view_return_links();
3449
 
3450
        $o .= $this->view_footer();
3451
 
3452
        return $o;
3453
    }
3454
 
3455
    /**
3456
     * Rewrite plugin file urls so they resolve correctly in an exported zip.
3457
     *
3458
     * @param string $text - The replacement text
3459
     * @param stdClass $user - The user record
3460
     * @param assign_plugin $plugin - The assignment plugin
3461
     */
1441 ariadna 3462
    public function download_rewrite_pluginfile_urls($text, $user, $plugin) {
1 efrain 3463
        // The groupname prefix for the urls doesn't depend on the group mode of the assignment instance.
3464
        // Rather, it should be determined by checking the group submission settings of the instance,
3465
        // which is what download_submission() does when generating the file name prefixes.
3466
        $groupname = '';
3467
        if ($this->get_instance()->teamsubmission) {
3468
            $submissiongroup = $this->get_submission_group($user->id);
3469
            if ($submissiongroup) {
3470
                $groupname = $submissiongroup->name . '-';
3471
            } else {
3472
                $groupname = get_string('defaultteam', 'assign') . '-';
3473
            }
3474
        }
3475
 
3476
        if ($this->is_blind_marking()) {
3477
            $prefix = $groupname . get_string('participant', 'assign');
3478
            $prefix = str_replace('_', ' ', $prefix);
3479
            $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($user->id) . '_');
3480
        } else {
3481
            $prefix = $groupname . fullname($user);
3482
            $prefix = str_replace('_', ' ', $prefix);
3483
            $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($user->id) . '_');
3484
        }
3485
 
3486
        // Only prefix files if downloadasfolders user preference is NOT set.
3487
        if (!get_user_preferences('assign_downloadasfolders', 1)) {
3488
            $subtype = $plugin->get_subtype();
3489
            $type = $plugin->get_type();
3490
            $prefix = $prefix . $subtype . '_' . $type . '_';
3491
        } else {
3492
            $prefix = "";
3493
        }
3494
        $result = str_replace('@@PLUGINFILE@@/', $prefix, $text);
3495
 
3496
        return $result;
3497
    }
3498
 
3499
    /**
3500
     * Render the content in editor that is often used by plugin.
3501
     *
3502
     * @param string $filearea
3503
     * @param int $submissionid
3504
     * @param string $plugintype
3505
     * @param string $editor
3506
     * @param string $component
3507
     * @param bool $shortentext Whether to shorten the text content.
3508
     * @return string
3509
     */
1441 ariadna 3510
    public function render_editor_content($filearea, $submissionid, $plugintype, $editor, $component, $shortentext = false) {
1 efrain 3511
        global $CFG;
3512
 
3513
        $result = '';
3514
 
3515
        $plugin = $this->get_submission_plugin_by_type($plugintype);
3516
 
3517
        $text = $plugin->get_editor_text($editor, $submissionid);
3518
        if ($shortentext) {
3519
            $text = shorten_text($text, 140);
3520
        }
3521
        $format = $plugin->get_editor_format($editor, $submissionid);
3522
 
1441 ariadna 3523
        $finaltext = file_rewrite_pluginfile_urls($text,
3524
                                                  'pluginfile.php',
3525
                                                  $this->get_context()->id,
3526
                                                  $component,
3527
                                                  $filearea,
3528
                                                  $submissionid);
1 efrain 3529
        $params = array('overflowdiv' => true, 'context' => $this->get_context());
3530
        $result .= format_text($finaltext, $format, $params);
3531
 
3532
        if ($CFG->enableportfolios && has_capability('mod/assign:exportownsubmission', $this->context)) {
3533
            require_once($CFG->libdir . '/portfoliolib.php');
3534
 
3535
            $button = new portfolio_add_button();
1441 ariadna 3536
            $portfolioparams = array('cmid' => $this->get_course_module()->id,
3537
                                     'sid' => $submissionid,
3538
                                     'plugin' => $plugintype,
3539
                                     'editor' => $editor,
3540
                                     'area'=>$filearea);
1 efrain 3541
            $button->set_callback_options('assign_portfolio_caller', $portfolioparams, 'mod_assign');
3542
            $fs = get_file_storage();
3543
 
1441 ariadna 3544
            if ($files = $fs->get_area_files($this->context->id,
3545
                                             $component,
3546
                                             $filearea,
3547
                                             $submissionid,
3548
                                             'timemodified',
3549
                                             false)) {
1 efrain 3550
                $button->set_formats(PORTFOLIO_FORMAT_RICHHTML);
3551
            } else {
3552
                $button->set_formats(PORTFOLIO_FORMAT_PLAINHTML);
3553
            }
3554
            $result .= $button->to_html(PORTFOLIO_ADD_TEXT_LINK);
3555
        }
3556
        return $result;
3557
    }
3558
 
3559
    /**
3560
     * Display a continue page after grading.
3561
     *
3562
     * @param string $message - The message to display.
3563
     * @return string
3564
     */
1441 ariadna 3565
    protected function view_savegrading_result($message) {
1 efrain 3566
        $o = '';
1441 ariadna 3567
        $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
3568
                                                      $this->get_context(),
3569
                                                      $this->show_intro(),
3570
                                                      $this->get_course_module()->id,
3571
                                                      get_string('savegradingresult', 'assign')));
3572
        $gradingresult = new assign_gradingmessage(get_string('savegradingresult', 'assign'),
3573
                                                   $message,
3574
                                                   $this->get_course_module()->id);
1 efrain 3575
        $o .= $this->get_renderer()->render($gradingresult);
3576
        $o .= $this->view_footer();
3577
        return $o;
3578
    }
3579
    /**
3580
     * Display a continue page after quickgrading.
3581
     *
3582
     * @param string $message - The message to display.
3583
     * @return string
3584
     */
1441 ariadna 3585
    protected function view_quickgrading_result($message) {
1 efrain 3586
        $o = '';
1441 ariadna 3587
        $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
3588
                                                      $this->get_context(),
3589
                                                      $this->show_intro(),
3590
                                                      $this->get_course_module()->id,
3591
                                                      get_string('quickgradingresult', 'assign')));
1 efrain 3592
        $gradingerror = in_array($message, $this->get_error_messages());
3593
        $lastpage = optional_param('lastpage', null, PARAM_INT);
1441 ariadna 3594
        $gradingresult = new assign_gradingmessage(get_string('quickgradingresult', 'assign'),
3595
                                                   $message,
3596
                                                   $this->get_course_module()->id,
3597
                                                   $gradingerror,
3598
                                                   $lastpage);
1 efrain 3599
        $o .= $this->get_renderer()->render($gradingresult);
3600
        $o .= $this->view_footer();
3601
        return $o;
3602
    }
3603
 
3604
    /**
3605
     * Display the page footer.
3606
     *
3607
     * @return string
3608
     */
1441 ariadna 3609
    protected function view_footer() {
1 efrain 3610
        // When viewing the footer during PHPUNIT tests a set_state error is thrown.
3611
        if (!PHPUNIT_TEST) {
3612
            return $this->get_renderer()->render_footer();
3613
        }
3614
 
3615
        return '';
3616
    }
3617
 
3618
    /**
3619
     * Throw an error if the permissions to view this users' group submission are missing.
3620
     *
3621
     * @param int $groupid Group id.
3622
     * @throws required_capability_exception
3623
     */
1441 ariadna 3624
    public function require_view_group_submission($groupid) {
1 efrain 3625
        if (!$this->can_view_group_submission($groupid)) {
3626
            throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', '');
3627
        }
3628
    }
3629
 
3630
    /**
3631
     * Throw an error if the permissions to view this users submission are missing.
3632
     *
3633
     * @throws required_capability_exception
3634
     * @return none
3635
     */
1441 ariadna 3636
    public function require_view_submission($userid) {
1 efrain 3637
        if (!$this->can_view_submission($userid)) {
3638
            throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', '');
3639
        }
3640
    }
3641
 
3642
    /**
3643
     * Throw an error if the permissions to view grades in this assignment are missing.
3644
     *
3645
     * @throws required_capability_exception
3646
     * @return none
3647
     */
1441 ariadna 3648
    public function require_view_grades() {
1 efrain 3649
        if (!$this->can_view_grades()) {
3650
            throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', '');
3651
        }
3652
    }
3653
 
3654
    /**
3655
     * Does this user have view grade or grade permission for this assignment?
3656
     *
3657
     * @param mixed $groupid int|null when is set to a value, use this group instead calculating it
3658
     * @return bool
3659
     */
1441 ariadna 3660
    public function can_view_grades($groupid = null) {
1 efrain 3661
        // Permissions check.
3662
        if (!has_any_capability(array('mod/assign:viewgrades', 'mod/assign:grade'), $this->context)) {
3663
            return false;
3664
        }
3665
        // Checks for the edge case when user belongs to no groups and groupmode is sep.
3666
        if ($this->get_course_module()->effectivegroupmode == SEPARATEGROUPS) {
3667
            if ($groupid === null) {
3668
                $groupid = groups_get_activity_allowed_groups($this->get_course_module());
3669
            }
3670
            $groupflag = has_capability('moodle/site:accessallgroups', $this->get_context());
3671
            $groupflag = $groupflag || !empty($groupid);
3672
            return (bool)$groupflag;
3673
        }
3674
        return true;
3675
    }
3676
 
3677
    /**
3678
     * Does this user have grade permission for this assignment?
3679
     *
3680
     * @param int|stdClass $user The object or id of the user who will do the editing (default to current user).
3681
     * @return bool
3682
     */
1441 ariadna 3683
    public function can_grade($user = null) {
1 efrain 3684
        // Permissions check.
3685
        if (!has_capability('mod/assign:grade', $this->context, $user)) {
3686
            return false;
3687
        }
3688
 
3689
        return true;
3690
    }
3691
 
3692
    /**
3693
     * Download a zip file of all assignment submissions.
3694
     *
1441 ariadna 3695
     * @param int[]|null $userids Array of user ids to download assignment submissions in a zip file
1 efrain 3696
     * @return string - If an error occurs, this will contain the error page.
3697
     */
1441 ariadna 3698
    protected function download_submissions($userids = null) {
1 efrain 3699
        $downloader = new downloader($this, $userids ?: null);
3700
        if ($downloader->load_filelist()) {
3701
            $downloader->download_zip();
3702
        }
3703
        // Show some notification if we have nothing to download.
3704
        $cm = $this->get_course_module();
3705
        $renderer = $this->get_renderer();
3706
        $header = new assign_header(
3707
            $this->get_instance(),
3708
            $this->get_context(),
3709
            '',
3710
            $cm->id,
3711
            get_string('downloadall', 'mod_assign')
3712
        );
3713
        $result = $renderer->render($header);
3714
        $result .= $renderer->notification(get_string('nosubmission', 'mod_assign'));
3715
        $url = new moodle_url('/mod/assign/view.php', ['id' => $cm->id, 'action' => 'grading']);
3716
        $result .= $renderer->continue_button($url);
3717
        $result .= $this->view_footer();
3718
        return $result;
3719
    }
3720
 
3721
    /**
3722
     * Lazy load the page renderer and expose the renderer to plugins.
3723
     *
3724
     * @return assign_renderer
3725
     */
1441 ariadna 3726
    public function get_renderer() {
1 efrain 3727
        global $PAGE;
3728
        if ($this->output) {
3729
            return $this->output;
3730
        }
3731
        $this->output = $PAGE->get_renderer('mod_assign', null, RENDERER_TARGET_GENERAL);
3732
        return $this->output;
3733
    }
3734
 
3735
    /**
3736
     * Load the submission object for a particular user, optionally creating it if required.
3737
     *
3738
     * For team assignments there are 2 submissions - the student submission and the team submission
3739
     * All files are associated with the team submission but the status of the students contribution is
3740
     * recorded separately.
3741
     *
3742
     * @param int $userid The id of the user whose submission we want or 0 in which case USER->id is used
3743
     * @param bool $create If set to true a new submission object will be created in the database with the status set to "new".
3744
     * @param int $attemptnumber - -1 means the latest attempt
3745
     * @return stdClass|false The submission
3746
     */
1441 ariadna 3747
    public function get_user_submission($userid, $create, $attemptnumber=-1) {
1 efrain 3748
        global $DB, $USER;
3749
 
3750
        if (!$userid) {
3751
            $userid = $USER->id;
3752
        }
3753
        // If the userid is not null then use userid.
1441 ariadna 3754
        $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid, 'groupid'=>0);
1 efrain 3755
        if ($attemptnumber >= 0) {
3756
            $params['attemptnumber'] = $attemptnumber;
3757
        }
3758
 
3759
        // Only return the row with the highest attemptnumber.
3760
        $submission = null;
3761
        $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', '*', 0, 1);
3762
        if ($submissions) {
3763
            $submission = reset($submissions);
3764
        }
3765
 
3766
        if ($submission) {
3767
            if ($create) {
3768
                $action = optional_param('action', '', PARAM_TEXT);
3769
                if ($action == 'editsubmission') {
3770
                    if (empty($submission->timestarted) && $this->get_instance()->timelimit) {
3771
                        $submission->timestarted = time();
3772
                        $DB->update_record('assign_submission', $submission);
3773
                    }
3774
                }
3775
            }
3776
            return $submission;
3777
        }
3778
        if ($create) {
3779
            $submission = new stdClass();
3780
            $submission->assignment   = $this->get_instance()->id;
3781
            $submission->userid       = $userid;
3782
            $submission->timecreated = time();
3783
            $submission->timemodified = $submission->timecreated;
3784
            $submission->status = ASSIGN_SUBMISSION_STATUS_NEW;
3785
            if ($attemptnumber >= 0) {
3786
                $submission->attemptnumber = $attemptnumber;
3787
            } else {
3788
                $submission->attemptnumber = 0;
3789
            }
3790
            // Work out if this is the latest submission.
3791
            $submission->latest = 0;
1441 ariadna 3792
            $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid, 'groupid'=>0);
1 efrain 3793
            if ($attemptnumber == -1) {
3794
                // This is a new submission so it must be the latest.
3795
                $submission->latest = 1;
3796
            } else {
3797
                // We need to work this out.
3798
                $result = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', 'attemptnumber', 0, 1);
3799
                $latestsubmission = null;
3800
                if ($result) {
3801
                    $latestsubmission = reset($result);
3802
                }
3803
                if (empty($latestsubmission) || ($attemptnumber > $latestsubmission->attemptnumber)) {
3804
                    $submission->latest = 1;
3805
                }
3806
            }
3807
            $transaction = $DB->start_delegated_transaction();
3808
            if ($submission->latest) {
3809
                // This is the case when we need to set latest to 0 for all the other attempts.
3810
                $DB->set_field('assign_submission', 'latest', 0, $params);
3811
            }
3812
            $sid = $DB->insert_record('assign_submission', $submission);
3813
            $transaction->allow_commit();
3814
            return $DB->get_record('assign_submission', array('id' => $sid));
3815
        }
3816
        return false;
3817
    }
3818
 
3819
    /**
3820
     * Load the submission object from it's id.
3821
     *
3822
     * @param int $submissionid The id of the submission we want
3823
     * @return stdClass The submission
3824
     */
1441 ariadna 3825
    protected function get_submission($submissionid) {
1 efrain 3826
        global $DB;
3827
 
1441 ariadna 3828
        $params = array('assignment'=>$this->get_instance()->id, 'id'=>$submissionid);
1 efrain 3829
        return $DB->get_record('assign_submission', $params, '*', MUST_EXIST);
3830
    }
3831
 
3832
    /**
3833
     * This will retrieve a user flags object from the db optionally creating it if required.
3834
     * The user flags was split from the user_grades table in 2.5.
3835
     *
3836
     * @param int $userid The user we are getting the flags for.
3837
     * @param bool $create If true the flags record will be created if it does not exist
3838
     * @return stdClass The flags record
3839
     */
1441 ariadna 3840
    public function get_user_flags($userid, $create) {
1 efrain 3841
        global $DB, $USER;
3842
 
3843
        // If the userid is not null then use userid.
3844
        if (!$userid) {
3845
            $userid = $USER->id;
3846
        }
3847
 
1441 ariadna 3848
        $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
1 efrain 3849
 
3850
        $flags = $DB->get_record('assign_user_flags', $params);
3851
 
3852
        if ($flags) {
3853
            return $flags;
3854
        }
3855
        if ($create) {
3856
            $flags = new stdClass();
3857
            $flags->assignment = $this->get_instance()->id;
3858
            $flags->userid = $userid;
3859
            $flags->locked = 0;
3860
            $flags->extensionduedate = 0;
3861
            $flags->workflowstate = '';
3862
            $flags->allocatedmarker = 0;
3863
 
3864
            // The mailed flag can be one of 3 values: 0 is unsent, 1 is sent and 2 is do not send yet.
3865
            // This is because students only want to be notified about certain types of update (grades and feedback).
3866
            $flags->mailed = 2;
3867
 
3868
            $fid = $DB->insert_record('assign_user_flags', $flags);
3869
            $flags->id = $fid;
3870
            return $flags;
3871
        }
3872
        return false;
3873
    }
3874
 
3875
    /**
3876
     * This will retrieve a grade object from the db, optionally creating it if required.
3877
     *
3878
     * @param int $userid The user we are grading
3879
     * @param bool $create If true the grade will be created if it does not exist
3880
     * @param int $attemptnumber The attempt number to retrieve the grade for. -1 means the latest submission.
3881
     * @return stdClass The grade record
3882
     */
1441 ariadna 3883
    public function get_user_grade($userid, $create, $attemptnumber=-1) {
1 efrain 3884
        global $DB, $USER;
3885
 
3886
        // If the userid is not null then use userid.
3887
        if (!$userid) {
3888
            $userid = $USER->id;
3889
        }
3890
        $submission = null;
3891
 
1441 ariadna 3892
        $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
1 efrain 3893
        if ($attemptnumber < 0 || $create) {
3894
            // Make sure this grade matches the latest submission attempt.
3895
            if ($this->get_instance()->teamsubmission) {
3896
                $submission = $this->get_group_submission($userid, 0, true, $attemptnumber);
3897
            } else {
3898
                $submission = $this->get_user_submission($userid, true, $attemptnumber);
3899
            }
3900
            if ($submission) {
3901
                $attemptnumber = $submission->attemptnumber;
3902
            }
3903
        }
3904
 
3905
        if ($attemptnumber >= 0) {
3906
            $params['attemptnumber'] = $attemptnumber;
3907
        }
3908
 
3909
        $grades = $DB->get_records('assign_grades', $params, 'attemptnumber DESC', '*', 0, 1);
3910
 
3911
        if ($grades) {
3912
            return reset($grades);
3913
        }
3914
        if ($create) {
3915
            $grade = new stdClass();
3916
            $grade->assignment   = $this->get_instance()->id;
3917
            $grade->userid       = $userid;
3918
            $grade->timecreated = time();
3919
            // If we are "auto-creating" a grade - and there is a submission
3920
            // the new grade should not have a more recent timemodified value
3921
            // than the submission.
3922
            if ($submission) {
3923
                $grade->timemodified = $submission->timemodified;
3924
            } else {
3925
                $grade->timemodified = $grade->timecreated;
3926
            }
3927
            $grade->grade = -1;
3928
            // Do not set the grader id here as it would be the admin users which is incorrect.
3929
            $grade->grader = -1;
3930
            if ($attemptnumber >= 0) {
3931
                $grade->attemptnumber = $attemptnumber;
3932
            }
1441 ariadna 3933
            $grade->penalty = 0.0;
1 efrain 3934
 
3935
            $gid = $DB->insert_record('assign_grades', $grade);
3936
            $grade->id = $gid;
3937
            return $grade;
3938
        }
3939
        return false;
3940
    }
3941
 
3942
    /**
3943
     * This will retrieve a grade object from the db.
3944
     *
3945
     * @param int $gradeid The id of the grade
3946
     * @return stdClass The grade record
3947
     */
1441 ariadna 3948
    protected function get_grade($gradeid) {
1 efrain 3949
        global $DB;
3950
 
1441 ariadna 3951
        $params = array('assignment'=>$this->get_instance()->id, 'id'=>$gradeid);
1 efrain 3952
        return $DB->get_record('assign_grades', $params, '*', MUST_EXIST);
3953
    }
3954
 
3955
    /**
3956
     * Print the grading page for a single user submission.
3957
     *
3958
     * @param array $args Optional args array (better than pulling args from _GET and _POST)
3959
     * @return string
3960
     */
1441 ariadna 3961
    protected function view_single_grading_panel($args) {
1 efrain 3962
        global $DB, $CFG;
3963
 
3964
        $o = '';
3965
 
3966
        require_once($CFG->dirroot . '/mod/assign/gradeform.php');
3967
 
3968
        // Need submit permission to submit an assignment.
3969
        require_capability('mod/assign:grade', $this->context);
3970
 
3971
        // If userid is passed - we are only grading a single student.
3972
        $userid = $args['userid'];
3973
        $attemptnumber = $args['attemptnumber'];
3974
        $instance = $this->get_instance($userid);
3975
 
3976
        // Apply overrides.
3977
        $this->update_effective_access($userid);
3978
 
3979
        $rownum = 0;
3980
        $useridlist = array($userid);
3981
 
3982
        $last = true;
3983
        // This variation on the url will link direct to this student, with no next/previous links.
3984
        // The benefit is the url will be the same every time for this student, so Atto autosave drafts can match up.
3985
        $returnparams = array('userid' => $userid, 'rownum' => 0, 'useridlistid' => 0);
3986
        $this->register_return_link('grade', $returnparams);
3987
 
3988
        $user = $DB->get_record('user', array('id' => $userid));
3989
        $submission = $this->get_user_submission($userid, false, $attemptnumber);
3990
        $submissiongroup = null;
3991
        $teamsubmission = null;
3992
        $notsubmitted = array();
3993
        if ($instance->teamsubmission) {
3994
            $teamsubmission = $this->get_group_submission($userid, 0, false, $attemptnumber);
3995
            $submissiongroup = $this->get_submission_group($userid);
3996
            $groupid = 0;
3997
            if ($submissiongroup) {
3998
                $groupid = $submissiongroup->id;
3999
            }
4000
            $notsubmitted = $this->get_submission_group_members_who_have_not_submitted($groupid, false);
1441 ariadna 4001
 
1 efrain 4002
        }
4003
 
4004
        // Get the requested grade.
4005
        $grade = $this->get_user_grade($userid, false, $attemptnumber);
4006
        $flags = $this->get_user_flags($userid, false);
4007
        if ($this->can_view_submission($userid)) {
4008
            $submissionlocked = ($flags && $flags->locked);
4009
            $extensionduedate = null;
4010
            if ($flags) {
4011
                $extensionduedate = $flags->extensionduedate;
4012
            }
4013
            $showedit = $this->submissions_open($userid) && ($this->is_any_submission_plugin_enabled());
4014
            $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
4015
            $usergroups = $this->get_all_groups($user->id);
4016
 
1441 ariadna 4017
            $submissionstatus = new assign_submission_status_compact($instance->allowsubmissionsfromdate,
4018
                                                                     $instance->alwaysshowdescription,
4019
                                                                     $submission,
4020
                                                                     $instance->teamsubmission,
4021
                                                                     $teamsubmission,
4022
                                                                     $submissiongroup,
4023
                                                                     $notsubmitted,
4024
                                                                     $this->is_any_submission_plugin_enabled(),
4025
                                                                     $submissionlocked,
4026
                                                                     $this->is_graded($userid),
4027
                                                                     $instance->duedate,
4028
                                                                     $instance->cutoffdate,
4029
                                                                     $this->get_submission_plugins(),
4030
                                                                     $this->get_return_action(),
4031
                                                                     $this->get_return_params(),
4032
                                                                     $this->get_course_module()->id,
4033
                                                                     $this->get_course()->id,
4034
                                                                     assign_submission_status::GRADER_VIEW,
4035
                                                                     $showedit,
4036
                                                                     false,
4037
                                                                     $viewfullnames,
4038
                                                                     $extensionduedate,
4039
                                                                     $this->get_context(),
4040
                                                                     $this->is_blind_marking(),
4041
                                                                     '',
4042
                                                                     $instance->attemptreopenmethod,
4043
                                                                     $instance->maxattempts,
4044
                                                                     $this->get_grading_status($userid),
4045
                                                                     $instance->preventsubmissionnotingroup,
4046
                                                                     $usergroups,
4047
                                                                     $instance->timelimit);
1 efrain 4048
            $o .= $this->get_renderer()->render($submissionstatus);
4049
        }
4050
 
4051
        if ($grade) {
4052
            $data = new stdClass();
4053
            if ($grade->grade !== null && $grade->grade >= 0) {
4054
                $data->grade = format_float($grade->grade, $this->get_grade_item()->get_decimals());
4055
            }
4056
        } else {
4057
            $data = new stdClass();
4058
            $data->grade = '';
4059
        }
4060
 
4061
        if (!empty($flags->workflowstate)) {
4062
            $data->workflowstate = $flags->workflowstate;
4063
        }
4064
        if (!empty($flags->allocatedmarker)) {
4065
            $data->allocatedmarker = $flags->allocatedmarker;
4066
        }
4067
 
4068
        // Warning if required.
4069
        $allsubmissions = $this->get_all_submissions($userid);
4070
 
4071
        if ($attemptnumber != -1 && ($attemptnumber + 1) != count($allsubmissions)) {
1441 ariadna 4072
            $params = array('attemptnumber' => $attemptnumber + 1,
4073
                            'totalattempts' => count($allsubmissions));
1 efrain 4074
            $message = get_string('editingpreviousfeedbackwarning', 'assign', $params);
4075
            $o .= $this->get_renderer()->notification($message);
4076
        }
4077
 
1441 ariadna 4078
        $pagination = array('rownum' => $rownum,
4079
                            'useridlistid' => 0,
4080
                            'last' => $last,
4081
                            'userid' => $userid,
4082
                            'attemptnumber' => $attemptnumber,
4083
                            'gradingpanel' => true);
1 efrain 4084
 
4085
        if (!empty($args['formdata'])) {
4086
            $data = (array) $data;
4087
            $data = (object) array_merge($data, $args['formdata']);
4088
        }
4089
        $formparams = array($this, $data, $pagination);
1441 ariadna 4090
        $mform = new mod_assign_grade_form(null,
4091
                                           $formparams,
4092
                                           'post',
4093
                                           '',
4094
                                           array('class' => 'gradeform'));
1 efrain 4095
 
4096
        if (!empty($args['formdata'])) {
4097
            // If we were passed form data - we want the form to check the data
4098
            // and show errors.
4099
            $mform->is_validated();
4100
        }
4101
 
4102
        $o .= $this->get_renderer()->render(new assign_form('gradingform', $mform));
4103
 
4104
        if (count($allsubmissions) > 1) {
4105
            $allgrades = $this->get_all_grades($userid);
1441 ariadna 4106
            $history = new assign_attempt_history_chooser($allsubmissions,
4107
                                                          $allgrades,
4108
                                                          $this->get_course_module()->id,
4109
                                                          $userid);
1 efrain 4110
 
4111
            $o .= $this->get_renderer()->render($history);
4112
        }
4113
 
4114
        \mod_assign\event\grading_form_viewed::create_from_user($this, $user)->trigger();
4115
 
4116
        return $o;
4117
    }
4118
 
4119
    /**
4120
     * Print the grading page for a single user submission.
4121
     *
4122
     * @param moodleform $mform
4123
     * @return string
4124
     */
1441 ariadna 4125
    protected function view_single_grade_page($mform) {
1 efrain 4126
        global $DB, $CFG, $SESSION;
4127
 
4128
        $o = '';
4129
        $instance = $this->get_instance();
4130
 
4131
        require_once($CFG->dirroot . '/mod/assign/gradeform.php');
4132
 
4133
        // Need submit permission to submit an assignment.
4134
        require_capability('mod/assign:grade', $this->context);
4135
 
1441 ariadna 4136
        $header = new assign_header($instance,
4137
                                    $this->get_context(),
4138
                                    false,
4139
                                    $this->get_course_module()->id,
4140
                                    get_string('grading', 'assign'));
1 efrain 4141
        $o .= $this->get_renderer()->render($header);
4142
 
4143
        // If userid is passed - we are only grading a single student.
4144
        $rownum = optional_param('rownum', 0, PARAM_INT);
4145
        $useridlistid = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
4146
        $userid = optional_param('userid', 0, PARAM_INT);
4147
        $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT);
4148
 
4149
        if (!$userid) {
4150
            $useridlist = $this->get_grading_userid_list(true, $useridlistid);
4151
        } else {
4152
            $rownum = 0;
4153
            $useridlistid = 0;
4154
            $useridlist = array($userid);
4155
        }
4156
 
4157
        if ($rownum < 0 || $rownum > count($useridlist)) {
4158
            throw new coding_exception('Row is out of bounds for the current grading table: ' . $rownum);
4159
        }
4160
 
4161
        $last = false;
4162
        $userid = $useridlist[$rownum];
4163
        if ($rownum == count($useridlist) - 1) {
4164
            $last = true;
4165
        }
4166
        // This variation on the url will link direct to this student, with no next/previous links.
4167
        // The benefit is the url will be the same every time for this student, so Atto autosave drafts can match up.
4168
        $returnparams = array('userid' => $userid, 'rownum' => 0, 'useridlistid' => 0);
4169
        $this->register_return_link('grade', $returnparams);
4170
 
4171
        $user = $DB->get_record('user', array('id' => $userid));
4172
        if ($user) {
4173
            $this->update_effective_access($userid);
4174
            $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
1441 ariadna 4175
            $usersummary = new assign_user_summary($user,
4176
                                                   $this->get_course()->id,
4177
                                                   $viewfullnames,
4178
                                                   $this->is_blind_marking(),
4179
                                                   $this->get_uniqueid_for_user($user->id),
4180
                                                   // TODO Does not support custom user profile fields (MDL-70456).
4181
                                                   \core_user\fields::get_identity_fields($this->get_context(), false),
4182
                                                   !$this->is_active_user($userid));
1 efrain 4183
            $o .= $this->get_renderer()->render($usersummary);
4184
        }
4185
        $submission = $this->get_user_submission($userid, false, $attemptnumber);
4186
        $submissiongroup = null;
4187
        $teamsubmission = null;
4188
        $notsubmitted = array();
4189
        if ($instance->teamsubmission) {
4190
            $teamsubmission = $this->get_group_submission($userid, 0, false, $attemptnumber);
4191
            $submissiongroup = $this->get_submission_group($userid);
4192
            $groupid = 0;
4193
            if ($submissiongroup) {
4194
                $groupid = $submissiongroup->id;
4195
            }
4196
            $notsubmitted = $this->get_submission_group_members_who_have_not_submitted($groupid, false);
1441 ariadna 4197
 
1 efrain 4198
        }
4199
 
4200
        // Get the requested grade.
4201
        $grade = $this->get_user_grade($userid, false, $attemptnumber);
4202
        $flags = $this->get_user_flags($userid, false);
4203
        if ($this->can_view_submission($userid)) {
4204
            $submissionlocked = ($flags && $flags->locked);
4205
            $extensionduedate = null;
4206
            if ($flags) {
4207
                $extensionduedate = $flags->extensionduedate;
4208
            }
4209
            $showedit = $this->submissions_open($userid) && ($this->is_any_submission_plugin_enabled());
4210
            $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
4211
            $usergroups = $this->get_all_groups($user->id);
1441 ariadna 4212
            $submissionstatus = new assign_submission_status($instance->allowsubmissionsfromdate,
4213
                                                             $instance->alwaysshowdescription,
4214
                                                             $submission,
4215
                                                             $instance->teamsubmission,
4216
                                                             $teamsubmission,
4217
                                                             $submissiongroup,
4218
                                                             $notsubmitted,
4219
                                                             $this->is_any_submission_plugin_enabled(),
4220
                                                             $submissionlocked,
4221
                                                             $this->is_graded($userid),
4222
                                                             $instance->duedate,
4223
                                                             $instance->cutoffdate,
4224
                                                             $this->get_submission_plugins(),
4225
                                                             $this->get_return_action(),
4226
                                                             $this->get_return_params(),
4227
                                                             $this->get_course_module()->id,
4228
                                                             $this->get_course()->id,
4229
                                                             assign_submission_status::GRADER_VIEW,
4230
                                                             $showedit,
4231
                                                             false,
4232
                                                             $viewfullnames,
4233
                                                             $extensionduedate,
4234
                                                             $this->get_context(),
4235
                                                             $this->is_blind_marking(),
4236
                                                             '',
4237
                                                             $instance->attemptreopenmethod,
4238
                                                             $instance->maxattempts,
4239
                                                             $this->get_grading_status($userid),
4240
                                                             $instance->preventsubmissionnotingroup,
4241
                                                             $usergroups,
4242
                                                             $instance->timelimit);
1 efrain 4243
            $o .= $this->get_renderer()->render($submissionstatus);
4244
        }
4245
 
4246
        if ($grade) {
4247
            $data = new stdClass();
4248
            if ($grade->grade !== null && $grade->grade >= 0) {
4249
                $data->grade = format_float($grade->grade, $this->get_grade_item()->get_decimals());
4250
            }
4251
        } else {
4252
            $data = new stdClass();
4253
            $data->grade = '';
4254
        }
4255
 
4256
        if (!empty($flags->workflowstate)) {
4257
            $data->workflowstate = $flags->workflowstate;
4258
        }
4259
        if (!empty($flags->allocatedmarker)) {
4260
            $data->allocatedmarker = $flags->allocatedmarker;
4261
        }
4262
 
4263
        // Warning if required.
4264
        $allsubmissions = $this->get_all_submissions($userid);
4265
 
4266
        if ($attemptnumber != -1 && ($attemptnumber + 1) != count($allsubmissions)) {
1441 ariadna 4267
            $params = array('attemptnumber'=>$attemptnumber + 1,
4268
                            'totalattempts'=>count($allsubmissions));
1 efrain 4269
            $message = get_string('editingpreviousfeedbackwarning', 'assign', $params);
4270
            $o .= $this->get_renderer()->notification($message);
4271
        }
4272
 
4273
        // Now show the grading form.
4274
        if (!$mform) {
1441 ariadna 4275
            $pagination = array('rownum' => $rownum,
4276
                                'useridlistid' => $useridlistid,
4277
                                'last' => $last,
4278
                                'userid' => $userid,
4279
                                'attemptnumber' => $attemptnumber);
1 efrain 4280
            $formparams = array($this, $data, $pagination);
1441 ariadna 4281
            $mform = new mod_assign_grade_form(null,
4282
                                               $formparams,
4283
                                               'post',
4284
                                               '',
4285
                                               array('class'=>'gradeform'));
1 efrain 4286
        }
4287
 
4288
        $o .= $this->get_renderer()->render(new assign_form('gradingform', $mform));
4289
 
4290
        if (count($allsubmissions) > 1 && $attemptnumber == -1) {
4291
            $allgrades = $this->get_all_grades($userid);
1441 ariadna 4292
            $history = new assign_attempt_history($allsubmissions,
4293
                                                  $allgrades,
4294
                                                  $this->get_submission_plugins(),
4295
                                                  $this->get_feedback_plugins(),
4296
                                                  $this->get_course_module()->id,
4297
                                                  $this->get_return_action(),
4298
                                                  $this->get_return_params(),
4299
                                                  true,
4300
                                                  $useridlistid,
4301
                                                  $rownum);
1 efrain 4302
 
4303
            $o .= $this->get_renderer()->render($history);
4304
        }
4305
 
4306
        \mod_assign\event\grading_form_viewed::create_from_user($this, $user)->trigger();
4307
 
4308
        $o .= $this->view_footer();
4309
        return $o;
4310
    }
4311
 
4312
    /**
4313
     * Show a confirmation page to make sure they want to remove submission data.
4314
     *
4315
     * @return string
4316
     */
1441 ariadna 4317
    protected function view_remove_submission_confirm() {
1 efrain 4318
        global $USER;
4319
 
4320
        $userid = optional_param('userid', $USER->id, PARAM_INT);
4321
 
4322
        if (!$this->can_edit_submission($userid, $USER->id)) {
4323
            throw new \moodle_exception('nopermission');
4324
        }
4325
        $user = core_user::get_user($userid, '*', MUST_EXIST);
4326
 
4327
        $o = '';
1441 ariadna 4328
        $header = new assign_header($this->get_instance(),
4329
                                    $this->get_context(),
4330
                                    false,
4331
                                    $this->get_course_module()->id);
1 efrain 4332
        $o .= $this->get_renderer()->render($header);
4333
 
1441 ariadna 4334
        $urlparams = array('id' => $this->get_course_module()->id,
4335
                           'action' => 'removesubmission',
4336
                           'userid' => $userid,
4337
                           'sesskey' => sesskey());
1 efrain 4338
        $confirmurl = new moodle_url('/mod/assign/view.php', $urlparams);
4339
 
1441 ariadna 4340
        $urlparams = array('id' => $this->get_course_module()->id,
4341
                           'action' => 'view');
1 efrain 4342
        $cancelurl = new moodle_url('/mod/assign/view.php', $urlparams);
4343
 
4344
        if ($userid == $USER->id) {
4345
            if ($this->is_time_limit_enabled($userid)) {
4346
                $confirmstr = get_string('removesubmissionconfirmwithtimelimit', 'assign');
4347
            } else {
4348
                $confirmstr = get_string('removesubmissionconfirm', 'assign');
4349
            }
4350
        } else {
4351
            if ($this->is_time_limit_enabled($userid)) {
4352
                $confirmstr = get_string('removesubmissionconfirmforstudentwithtimelimit', 'assign', $this->fullname($user));
4353
            } else {
4354
                $confirmstr = get_string('removesubmissionconfirmforstudent', 'assign', $this->fullname($user));
4355
            }
4356
        }
1441 ariadna 4357
        $o .= $this->get_renderer()->confirm($confirmstr,
4358
                                             $confirmurl,
4359
                                             $cancelurl);
1 efrain 4360
        $o .= $this->view_footer();
4361
 
4362
        \mod_assign\event\remove_submission_form_viewed::create_from_user($this, $user)->trigger();
4363
 
4364
        return $o;
4365
    }
4366
 
4367
 
4368
    /**
4369
     * Show a confirmation page to make sure they want to release student identities.
4370
     *
4371
     * @return string
4372
     */
1441 ariadna 4373
    protected function view_reveal_identities_confirm() {
1 efrain 4374
        require_capability('mod/assign:revealidentities', $this->get_context());
4375
 
4376
        $o = '';
1441 ariadna 4377
        $header = new assign_header($this->get_instance(),
4378
                                    $this->get_context(),
4379
                                    false,
4380
                                    $this->get_course_module()->id);
1 efrain 4381
        $o .= $this->get_renderer()->render($header);
4382
 
1441 ariadna 4383
        $urlparams = array('id'=>$this->get_course_module()->id,
4384
                           'action'=>'revealidentitiesconfirm',
4385
                           'sesskey'=>sesskey());
1 efrain 4386
        $confirmurl = new moodle_url('/mod/assign/view.php', $urlparams);
4387
 
1441 ariadna 4388
        $urlparams = array('id'=>$this->get_course_module()->id,
4389
                           'action'=>'grading');
1 efrain 4390
        $cancelurl = new moodle_url('/mod/assign/view.php', $urlparams);
4391
 
1441 ariadna 4392
        $o .= $this->get_renderer()->confirm(get_string('revealidentitiesconfirm', 'assign'),
4393
                                             $confirmurl,
4394
                                             $cancelurl);
1 efrain 4395
        $o .= $this->view_footer();
4396
 
4397
        \mod_assign\event\reveal_identities_confirmation_page_viewed::create_from_assign($this)->trigger();
4398
 
4399
        return $o;
4400
    }
4401
 
4402
    /**
4403
     * View a link to go back to the previous page. Uses url parameters returnaction and returnparams.
4404
     *
4405
     * @return string
4406
     */
1441 ariadna 4407
    protected function view_return_links() {
1 efrain 4408
        $returnaction = optional_param('returnaction', '', PARAM_ALPHA);
4409
        $returnparams = optional_param('returnparams', '', PARAM_TEXT);
4410
 
4411
        $params = array();
4412
        $returnparams = str_replace('&amp;', '&', $returnparams);
4413
        parse_str($returnparams, $params);
4414
        $newparams = array('id' => $this->get_course_module()->id, 'action' => $returnaction);
4415
        $params = array_merge($newparams, $params);
4416
 
4417
        $url = new moodle_url('/mod/assign/view.php', $params);
4418
        return $this->get_renderer()->single_button($url, get_string('back'), 'get');
4419
    }
4420
 
4421
    /**
4422
     * View the grading table of all submissions for this assignment.
4423
     *
4424
     * @return string
4425
     */
1441 ariadna 4426
    protected function view_grading_table() {
4427
        global $USER, $CFG, $SESSION, $PAGE, $OUTPUT;
1 efrain 4428
 
4429
        require_once($CFG->dirroot . '/mod/assign/quickgradingform.php');
4430
 
1441 ariadna 4431
        $submittedfilter = optional_param('status', null, PARAM_ALPHA);
4432
        if (isset($submittedfilter)) {
4433
            $validfilters = array_column($this->get_filters(), 'key');
4434
            $validfilters = array_diff($validfilters, [ASSIGN_FILTER_NONE]); // The 'none' filter is not a real filter.
4435
            if ($submittedfilter === '' || in_array($submittedfilter, $validfilters)) {
4436
                set_user_preference('assign_filter', $submittedfilter);
4437
            }
1 efrain 4438
        }
1441 ariadna 4439
 
4440
        $submittedquickgrading = optional_param('quickgrading', null, PARAM_BOOL);
4441
        if (isset($submittedquickgrading)) {
4442
            set_user_preference('assign_quickgrading', $submittedquickgrading);
1 efrain 4443
        }
1441 ariadna 4444
 
4445
        $submitteddownloadasfolders = optional_param('downloadasfolders', null, PARAM_BOOL);
4446
        if (isset($submitteddownloadasfolders)) {
4447
            set_user_preference('assign_downloadasfolders', $submitteddownloadasfolders);
1 efrain 4448
        }
4449
 
1441 ariadna 4450
        $submittedperpage = optional_param('perpage', null, PARAM_INT);
4451
        if (isset($submittedperpage)) {
4452
            set_user_preference('assign_perpage', $submittedperpage);
4453
        }
1 efrain 4454
 
1441 ariadna 4455
        $o = '';
4456
        $cmid = $this->get_course_module()->id;
1 efrain 4457
 
4458
        $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
4459
 
4460
        $perpage = $this->get_assign_perpage();
4461
        $filter = get_user_preferences('assign_filter', '');
1441 ariadna 4462
 
4463
        // Retrieve the 'workflowfilter' parameter, or set it to null if not provided.
4464
        $workflowfilter = optional_param('workflowfilter', null, PARAM_ALPHA);
4465
        // Check if the parameter is not null and if it exists in the list of valid workflow filters.
4466
        if ($workflowfilter !== null && array_key_exists($workflowfilter, $this->get_marking_workflow_filters())) {
4467
            // Save the valid 'workflowfilter' value as a user preference.
4468
            set_user_preference('assign_workflowfilter', $workflowfilter);
4469
        }
4470
 
4471
        // Retrieve the 'markingallocationfilter' parameter, or set it to null if not provided.
4472
        $markingallocationfilter = optional_param('markingallocationfilter', null, PARAM_ALPHANUMEXT);
4473
        // Check if the parameter is not null and if it exists in the list of valid marking allocation filters.
4474
        if ($markingallocationfilter !== null &&
4475
                array_key_exists($markingallocationfilter, $this->get_marking_allocation_filters())) {
4476
            // Save the valid 'markingallocationfilter' value as a user preference.
4477
            set_user_preference('assign_markerfilter', $markingallocationfilter);
4478
        }
4479
 
4480
        // Retrieve the 'suspendedparticipantsfilter' parameter, or set it to null if not provided.
4481
        $suspendedparticipantsfilter = optional_param('suspendedparticipantsfilter', null, PARAM_BOOL);
4482
        if ($suspendedparticipantsfilter !== null &&
4483
                has_capability('moodle/course:viewsuspendedusers', $this->get_context())) {
4484
            // Save the 'suspendedparticipantsfilter' value as a user preference.
4485
            set_user_preference('grade_report_showonlyactiveenrol', !$suspendedparticipantsfilter);
4486
        }
4487
 
1 efrain 4488
        $controller = $gradingmanager->get_active_controller();
4489
        $showquickgrading = empty($controller) && $this->can_grade();
4490
        $quickgrading = get_user_preferences('assign_quickgrading', false);
4491
 
4492
        $markingallocation = $this->get_instance()->markingworkflow &&
4493
            $this->get_instance()->markingallocation &&
4494
            has_capability('mod/assign:manageallocations', $this->context);
4495
 
4496
        $markingworkflow = $this->get_instance()->markingworkflow;
4497
 
1441 ariadna 4498
        // Load the table of submissions, to be printed further down.
4499
        $usequickgrading = $showquickgrading && $quickgrading;
4500
        $gradingtable = new assign_grading_table($this, $perpage, $filter, 0, $usequickgrading);
4501
        $gradingtable->responsive = false;
4502
        $table = $this->get_renderer()->render($gradingtable);
4503
        // This initialises the selected first/last initials for the action menu.
4504
        $gradingtable->initialbars(true);
1254 ariadna 4505
        $buttons = new \mod_assign\output\grading_actionmenu(
1441 ariadna 4506
            cmid: $this->get_course_module()->id,
4507
            assign: $this,
4508
            userinitials: [
4509
                'firstname' => $gradingtable->get_initial_first(),
4510
                'lastname' => $gradingtable->get_initial_last(),
4511
            ]
1254 ariadna 4512
        );
1 efrain 4513
        $actionformtext = $this->get_renderer()->render($buttons);
1441 ariadna 4514
        $currenturl = new moodle_url('/mod/assign/view.php', ['id' => $this->get_course_module()->id, 'action' => 'grading']);
1 efrain 4515
        $PAGE->activityheader->set_attrs(['hidecompletion' => true]);
4516
 
1441 ariadna 4517
        $PAGE->requires->js_call_amd('mod_assign/user', 'init', [$currenturl->out(false)]);
1 efrain 4518
 
1441 ariadna 4519
        // Conditionally add the group JS if we have groups enabled.
4520
        if (groups_get_activity_groupmode($this->get_course_module(), $this->get_course())) {
4521
            $PAGE->requires->js_call_amd('core_course/actionbar/group', 'init', [$currenturl->out(false), $cmid]);
4522
        }
4523
 
4524
        $header = new assign_header($this->get_instance(),
4525
                                    $this->get_context(),
4526
                                    false,
4527
                                    $this->get_course_module()->id,
4528
                                    get_string('gradeitem:submissions', 'assign'),
4529
                                    '',
4530
                                    '',
4531
                                    $currenturl);
1 efrain 4532
        $o .= $this->get_renderer()->render($header);
4533
 
4534
        $o .= $actionformtext;
4535
 
4536
        if ($this->is_blind_marking() && has_capability('mod/assign:viewblinddetails', $this->get_context())) {
1441 ariadna 4537
            if ($this->is_marking_anonymous()) {
4538
                $o .= $this->get_renderer()->notification(get_string('blindmarkingenabledwarning', 'assign'), 'notifymessage');
4539
            } else {
4540
                $o .= $this->get_renderer()->notification(get_string('blindmarkingnogradewarning', 'assign'), 'notifymessage');
4541
            }
1 efrain 4542
        }
4543
 
1441 ariadna 4544
        // Print the table of submissions.
4545
        $footerdata = [
4546
            'perpage' => $gradingtable->get_paging_selector(),
4547
            'pagingbar' => $gradingtable->get_paging_bar(),
4548
            'hassubmit' => $usequickgrading,
4549
            'sendstudentnotifications' => $this->get_instance()->sendstudentnotifications,
4550
        ];
4551
        $footer = new core\output\sticky_footer($OUTPUT->render_from_template('mod_assign/grading_sticky_footer', $footerdata));
4552
 
4553
        if ($usequickgrading) {
1267 ariadna 4554
            $page = optional_param('page', null, PARAM_INT);
1441 ariadna 4555
            $PAGE->requires->js_call_amd('mod_assign/quick_grading', 'init', []);
4556
            $quickformparams = [
1254 ariadna 4557
                'cm' => $this->get_course_module()->id,
4558
                'gradingtable' => $table,
1441 ariadna 4559
                'page' => $page,
4560
                'footer' => $this->get_renderer()->render($footer),
4561
            ];
4562
            $quickgradingform = new mod_assign_quick_grading_form(null, $quickformparams);
1 efrain 4563
 
4564
            $o .= $this->get_renderer()->render(new assign_form('quickgradingform', $quickgradingform));
4565
        } else {
1441 ariadna 4566
            $o .= $table;
4567
            $o .= $this->get_renderer()->render($footer);
1 efrain 4568
        }
4569
 
4570
        if ($this->can_grade()) {
4571
            // We need to store the order of uses in the table as the person may wish to grade them.
4572
            // This is done based on the row number of the user.
1441 ariadna 4573
            // Pagination has be added before this because calling get_column_data will reset the pagination.
1 efrain 4574
            $useridlist = $gradingtable->get_column_data('userid');
4575
            $SESSION->mod_assign_useridlist[$this->get_useridlist_key()] = $useridlist;
4576
        }
1441 ariadna 4577
 
4578
        $currentgroup = groups_get_activity_group($this->get_course_module(), true);
4579
        $users = array_keys($this->list_participants($currentgroup, true));
4580
        if (count($users) != 0 && $this->can_grade()) {
4581
            $jsparams = [];
4582
            $jsparams['message'] = !empty($CFG->messaging)
4583
                && has_all_capabilities(['moodle/site:sendmessage', 'moodle/course:bulkmessaging'], $this->context);
4584
            $jsparams['submissiondrafts'] = !empty($this->get_instance()->submissiondrafts);
4585
            $jsparams['removesubmission'] = has_capability('mod/assign:editothersubmission', $this->context);
4586
            $jsparams['extend'] = $this->get_instance()->duedate && has_capability('mod/assign:grantextension', $this->context);
4587
 
4588
            $multipleattemptsallowed = $this->get_instance()->maxattempts > 1
4589
                || $this->get_instance()->maxattempts == ASSIGN_UNLIMITED_ATTEMPTS;
4590
            $jsparams['grantattempt'] =
4591
                $multipleattemptsallowed && $this->get_instance()->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL;
4592
 
4593
            $jsparams['pluginoperations'] = [];
4594
            foreach ($this->get_feedback_plugins() as $plugin) {
4595
                if ($plugin->is_visible() && $plugin->is_enabled()) {
4596
                    foreach ($plugin->get_grading_batch_operation_details() as $operation) {
4597
                        $jsparams['pluginoperations'][] = [
4598
                            'key' => 'plugingradingbatchoperation_' . $plugin->get_type() . '_' . $operation->key,
4599
                            'label' => $operation->label,
4600
                            'icon' => $operation->icon,
4601
                            'confirmationtitle' => $operation->confirmationtitle,
4602
                            'confirmationquestion' => $operation->confirmationquestion,
4603
                        ];
4604
                    }
4605
                }
4606
            }
4607
 
4608
            $jsparams['workflowstate'] = !empty($this->get_instance()->markingworkflow);
4609
            $jsparams['markingallocation'] = !empty($this->get_instance()->markingallocation);
4610
            $jsparams['cmid'] = $this->get_course_module()->id;
4611
            $jsparams['sesskey'] = sesskey();
4612
 
4613
            $PAGE->requires->js_call_amd('mod_assign/bulkactions/grading/bulk_actions', 'init', [$jsparams]);
4614
        }
4615
 
1 efrain 4616
        return $o;
4617
    }
4618
 
4619
    /**
4620
     * View entire grader app.
4621
     *
4622
     * @return string
4623
     */
1441 ariadna 4624
    protected function view_grader() {
1 efrain 4625
        global $USER, $PAGE;
4626
 
4627
        $o = '';
4628
        // Need submit permission to submit an assignment.
4629
        $this->require_view_grades();
4630
 
4631
        $PAGE->set_pagelayout('embedded');
4632
 
4633
        $PAGE->activityheader->disable();
4634
 
4635
        $courseshortname = $this->get_context()->get_course_context()->get_context_name(false, true);
4636
        $args = [
4637
            'contextname' => $this->get_context()->get_context_name(false, true),
4638
            'subpage' => get_string('grading', 'assign')
4639
        ];
4640
        $title = get_string('subpagetitle', 'assign', $args);
4641
        $title = $courseshortname . ': ' . $title;
4642
        $PAGE->set_title($title);
4643
 
4644
        $o .= $this->get_renderer()->header();
4645
 
4646
        $userid = optional_param('userid', 0, PARAM_INT);
4647
        $blindid = optional_param('blindid', 0, PARAM_INT);
4648
 
4649
        if (!$userid && $blindid) {
4650
            $userid = $this->get_user_id_for_uniqueid($blindid);
4651
        }
4652
 
4653
        // Instantiate table object to apply table preferences.
4654
        $gradingtable = new assign_grading_table($this, 10, '', 0, false);
4655
        $gradingtable->setup();
4656
 
4657
        $currentgroup = groups_get_activity_group($this->get_course_module(), true);
4658
        $framegrader = new grading_app($userid, $currentgroup, $this);
4659
 
4660
        $this->update_effective_access($userid);
4661
 
4662
        $o .= $this->get_renderer()->render($framegrader);
4663
 
4664
        $o .= $this->view_footer();
4665
 
4666
        \mod_assign\event\grading_table_viewed::create_from_assign($this)->trigger();
4667
 
4668
        return $o;
4669
    }
4670
    /**
4671
     * View entire grading page.
4672
     *
4673
     * @return string
4674
     */
1441 ariadna 4675
    protected function view_grading_page() {
4676
        global $CFG, $PAGE;
1 efrain 4677
 
1441 ariadna 4678
        // Ensure that the 'Submissions' navigation node is highlighted as 'active' in the secondary navigation.
4679
        $PAGE->set_secondary_active_tab('mod_assign_submissions');
4680
 
1 efrain 4681
        $o = '';
4682
        // Need submit permission to submit an assignment.
4683
        $this->require_view_grades();
4684
        require_once($CFG->dirroot . '/mod/assign/gradeform.php');
4685
 
4686
        $this->add_grade_notices();
4687
 
4688
        // Only load this if it is.
4689
        $o .= $this->view_grading_table();
4690
 
4691
        $o .= $this->view_footer();
4692
 
4693
        \mod_assign\event\grading_table_viewed::create_from_assign($this)->trigger();
4694
 
4695
        return $o;
4696
    }
4697
 
4698
    /**
4699
     * Capture the output of the plagiarism plugins disclosures and return it as a string.
4700
     *
4701
     * @return string
4702
     */
1441 ariadna 4703
    protected function plagiarism_print_disclosure() {
1 efrain 4704
        global $CFG;
4705
        $o = '';
4706
 
4707
        if (!empty($CFG->enableplagiarism)) {
4708
            require_once($CFG->libdir . '/plagiarismlib.php');
4709
 
4710
            $o .= plagiarism_print_disclosure($this->get_course_module()->id);
4711
        }
4712
 
4713
        return $o;
4714
    }
4715
 
4716
    /**
4717
     * Message for students when assignment submissions have been closed.
4718
     *
4719
     * @param string $title The page title
4720
     * @param array $notices The array of notices to show.
4721
     * @return string
4722
     */
1441 ariadna 4723
    protected function view_notices($title, $notices) {
1 efrain 4724
        global $CFG;
4725
 
4726
        $o = '';
4727
 
1441 ariadna 4728
        $header = new assign_header($this->get_instance(),
4729
                                    $this->get_context(),
4730
                                    $this->show_intro(),
4731
                                    $this->get_course_module()->id,
4732
                                    $title);
1 efrain 4733
        $o .= $this->get_renderer()->render($header);
4734
 
4735
        foreach ($notices as $notice) {
4736
            $o .= $this->get_renderer()->notification($notice);
4737
        }
4738
 
1441 ariadna 4739
        $url = new moodle_url('/mod/assign/view.php', array('id'=>$this->get_course_module()->id, 'action'=>'view'));
1 efrain 4740
        $o .= $this->get_renderer()->continue_button($url);
4741
 
4742
        $o .= $this->view_footer();
4743
 
4744
        return $o;
4745
    }
4746
 
4747
    /**
4748
     * Get the name for a user - hiding their real name if blind marking is on.
4749
     *
4750
     * @param stdClass $user The user record as required by fullname()
4751
     * @return string The name.
4752
     */
1441 ariadna 4753
    public function fullname($user) {
1 efrain 4754
        if ($this->is_blind_marking()) {
4755
            $hasviewblind = has_capability('mod/assign:viewblinddetails', $this->get_context());
4756
            if (empty($user->recordid)) {
4757
                $uniqueid = $this->get_uniqueid_for_user($user->id);
4758
            } else {
4759
                $uniqueid = $user->recordid;
4760
            }
4761
            if ($hasviewblind) {
4762
                return get_string('participant', 'assign') . ' ' . $uniqueid . ' (' .
1441 ariadna 4763
                        fullname($user, has_capability('moodle/site:viewfullnames', $this->get_context())) . ')';
1 efrain 4764
            } else {
4765
                return get_string('participant', 'assign') . ' ' . $uniqueid;
4766
            }
4767
        } else {
4768
            return fullname($user, has_capability('moodle/site:viewfullnames', $this->get_context()));
4769
        }
4770
    }
4771
 
4772
    /**
4773
     * View edit submissions page.
4774
     *
4775
     * @param moodleform $mform
4776
     * @param array $notices A list of notices to display at the top of the
4777
     *                       edit submission form (e.g. from plugins).
4778
     * @return string The page output.
4779
     */
1441 ariadna 4780
    protected function view_edit_submission_page($mform, $notices) {
1 efrain 4781
        global $CFG, $USER, $DB, $PAGE;
4782
 
4783
        $o = '';
4784
        require_once($CFG->dirroot . '/mod/assign/submission_form.php');
4785
        // Need submit permission to submit an assignment.
4786
        $userid = optional_param('userid', $USER->id, PARAM_INT);
1441 ariadna 4787
        $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
1 efrain 4788
        $timelimitenabled = get_config('assign', 'enabletimelimit');
4789
 
4790
        // This variation on the url will link direct to this student.
4791
        // The benefit is the url will be the same every time for this student, so Atto autosave drafts can match up.
4792
        $returnparams = array('userid' => $userid, 'rownum' => 0, 'useridlistid' => 0);
4793
        $this->register_return_link('editsubmission', $returnparams);
4794
 
4795
        if ($userid == $USER->id) {
4796
            if (!$this->can_edit_submission($userid, $USER->id)) {
4797
                throw new \moodle_exception('nopermission');
4798
            }
4799
            // User is editing their own submission.
4800
            require_capability('mod/assign:submit', $this->context);
4801
            $title = get_string('editsubmission', 'assign');
4802
        } else {
4803
            // User is editing another user's submission.
4804
            if (!$this->can_edit_submission($userid, $USER->id)) {
4805
                throw new \moodle_exception('nopermission');
4806
            }
4807
 
4808
            $name = $this->fullname($user);
4809
            $title = get_string('editsubmissionother', 'assign', $name);
4810
        }
4811
 
4812
        if (!$this->submissions_open($userid)) {
4813
            $message = array(get_string('submissionsclosed', 'assign'));
4814
            return $this->view_notices($title, $message);
4815
        }
4816
 
4817
        $postfix = '';
4818
        if ($this->has_visible_attachments()) {
4819
            $postfix = $this->render_area_files('mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0);
4820
        }
4821
 
4822
        $data = new stdClass();
4823
        $data->userid = $userid;
4824
        if (!$mform) {
4825
            $mform = new mod_assign_submission_form(null, array($this, $data));
4826
        }
4827
 
4828
        if ($this->get_instance()->teamsubmission) {
4829
            $submission = $this->get_group_submission($userid, 0, false);
4830
        } else {
4831
            $submission = $this->get_user_submission($userid, false);
4832
        }
4833
 
4834
        if ($timelimitenabled && !empty($submission->timestarted) && $this->get_instance()->timelimit) {
4835
            $navbc = $this->get_timelimit_panel($submission);
4836
            $regions = $PAGE->blocks->get_regions();
4837
            $bc = new \block_contents();
4838
            $bc->attributes['id'] = 'mod_assign_timelimit_block';
4839
            $bc->attributes['role'] = 'navigation';
4840
            $bc->attributes['aria-labelledby'] = 'mod_assign_timelimit_block_title';
4841
            $bc->title = get_string('assigntimeleft', 'assign');
4842
            $bc->content = $navbc;
4843
            $PAGE->blocks->add_fake_block($bc, reset($regions));
4844
        }
4845
 
4846
        $o .= $this->get_renderer()->render(
1441 ariadna 4847
            new assign_header($this->get_instance(),
4848
                              $this->get_context(),
4849
                              $this->show_intro(),
4850
                              $this->get_course_module()->id,
4851
                              $title,
4852
                              '',
4853
                              $postfix,
4854
                              null,
4855
                              true
1 efrain 4856
            )
4857
        );
4858
 
4859
        // Show plagiarism disclosure for any user submitter.
4860
        $o .= $this->plagiarism_print_disclosure();
4861
 
4862
        foreach ($notices as $notice) {
4863
            $o .= $this->get_renderer()->notification($notice);
4864
        }
4865
 
4866
        $o .= $this->get_renderer()->render(new assign_form('editsubmissionform', $mform));
4867
        $o .= $this->view_footer();
4868
 
4869
        \mod_assign\event\submission_form_viewed::create_from_user($this, $user)->trigger();
4870
 
4871
        return $o;
4872
    }
4873
 
4874
    /**
4875
     * Get the time limit panel object for this submission attempt.
4876
     *
4877
     * @param stdClass $submission assign submission.
4878
     * @return string the panel output.
4879
     */
1441 ariadna 4880
    public function get_timelimit_panel(stdClass $submission): string {
1 efrain 4881
        global $USER;
4882
 
4883
        // Apply overrides.
4884
        $this->update_effective_access($USER->id);
4885
        $panel = new timelimit_panel($submission, $this->get_instance());
4886
        return $this->get_renderer()->render($panel);
4887
    }
4888
 
4889
    /**
4890
     * See if this assignment has a grade yet.
4891
     *
4892
     * @param int $userid
4893
     * @return bool
4894
     */
1441 ariadna 4895
    protected function is_graded($userid) {
1 efrain 4896
        $grade = $this->get_user_grade($userid, false);
4897
        if ($grade) {
4898
            return ($grade->grade !== null && $grade->grade >= 0);
4899
        }
4900
        return false;
4901
    }
4902
 
4903
    /**
4904
     * Perform an access check to see if the current $USER can edit this group submission.
4905
     *
4906
     * @param int $groupid
4907
     * @return bool
4908
     */
1441 ariadna 4909
    public function can_edit_group_submission($groupid) {
1 efrain 4910
        global $USER;
4911
 
4912
        $members = $this->get_submission_group_members($groupid, true);
4913
        foreach ($members as $member) {
4914
            // If we can edit any members submission, we can edit the submission for the group.
4915
            if ($this->can_edit_submission($member->id)) {
4916
                return true;
4917
            }
4918
        }
4919
        return false;
4920
    }
4921
 
4922
    /**
4923
     * Perform an access check to see if the current $USER can view this group submission.
4924
     *
4925
     * @param int $groupid
4926
     * @return bool
4927
     */
1441 ariadna 4928
    public function can_view_group_submission($groupid) {
1 efrain 4929
        global $USER;
4930
 
4931
        $members = $this->get_submission_group_members($groupid, true);
4932
        foreach ($members as $member) {
4933
            // If we can view any members submission, we can view the submission for the group.
4934
            if ($this->can_view_submission($member->id)) {
4935
                return true;
4936
            }
4937
        }
4938
        return false;
4939
    }
4940
 
4941
    /**
4942
     * Perform an access check to see if the current $USER can view this users submission.
4943
     *
4944
     * @param int $userid
4945
     * @return bool
4946
     */
1441 ariadna 4947
    public function can_view_submission($userid) {
1 efrain 4948
        global $USER;
4949
 
4950
        if (!$this->is_active_user($userid) && !has_capability('moodle/course:viewsuspendedusers', $this->context)) {
4951
            return false;
4952
        }
4953
        if (!is_enrolled($this->get_course_context(), $userid)) {
4954
            return false;
4955
        }
4956
        if (has_any_capability(array('mod/assign:viewgrades', 'mod/assign:grade'), $this->context)) {
4957
            return true;
4958
        }
4959
        if ($userid == $USER->id) {
4960
            return true;
4961
        }
4962
        return false;
4963
    }
4964
 
4965
    /**
4966
     * Allows the plugin to show a batch grading operation page.
1441 ariadna 4967
     * You should confirm sesskey before calling this function.
1 efrain 4968
     *
4969
     * @return none
4970
     */
1441 ariadna 4971
    protected function view_plugin_grading_batch_operation() {
1 efrain 4972
        require_capability('mod/assign:grade', $this->context);
4973
        $prefix = 'plugingradingbatchoperation_';
4974
 
1441 ariadna 4975
        $operation = required_param('operation', PARAM_ALPHAEXT);
1 efrain 4976
 
1441 ariadna 4977
        $tail = substr($operation, strlen($prefix));
4978
        list($plugintype, $action) = explode('_', $tail, 2);
4979
 
4980
        $plugin = $this->get_feedback_plugin_by_type($plugintype);
4981
        if ($plugin) {
4982
            $users = required_param('selectedusers', PARAM_SEQUENCE);
4983
            $userlist = explode(',', $users);
4984
            echo $plugin->grading_batch_operation($action, $userlist);
4985
            return;
1 efrain 4986
        }
1441 ariadna 4987
 
1 efrain 4988
        throw new \moodle_exception('invalidformdata', '');
4989
    }
4990
 
4991
    /**
4992
     * Ask the user to confirm they want to perform this batch operation
4993
     *
4994
     * @return string - the page to view after processing these actions
4995
     */
1441 ariadna 4996
    protected function process_grading_batch_operation() {
1 efrain 4997
        require_sesskey();
4998
 
1441 ariadna 4999
        $operation = required_param('operation', PARAM_ALPHAEXT);
5000
        $selectedusers = required_param('selectedusers', PARAM_SEQUENCE);
1 efrain 5001
 
1441 ariadna 5002
        // Get the list of users.
5003
        $userlist = explode(',', $selectedusers);
1 efrain 5004
 
1441 ariadna 5005
        $prefix = 'plugingradingbatchoperation_';
1 efrain 5006
 
1441 ariadna 5007
        if ($operation == 'grantextension') {
5008
            return 'grantextension';
5009
        } else if ($operation == 'setmarkingworkflowstate') {
5010
            return 'viewbatchsetmarkingworkflowstate';
5011
        } else if ($operation == 'setmarkingallocation') {
5012
            return 'viewbatchmarkingallocation';
5013
        } else if (strpos($operation, $prefix) === 0) {
5014
            $tail = substr($operation, strlen($prefix));
5015
            list($plugintype, $action) = explode('_', $tail, 2);
1 efrain 5016
 
1441 ariadna 5017
            $plugin = $this->get_feedback_plugin_by_type($plugintype);
5018
            if ($plugin) {
5019
                return 'plugingradingbatchoperation';
1 efrain 5020
            }
1441 ariadna 5021
        }
1 efrain 5022
 
1441 ariadna 5023
        if ($operation == 'downloadselected') {
5024
            $this->download_submissions($userlist);
5025
        } else {
5026
            foreach ($userlist as $userid) {
5027
                if ($operation == 'lock') {
5028
                    $this->process_lock_submission($userid);
5029
                } else if ($operation == 'unlock') {
5030
                    $this->process_unlock_submission($userid);
5031
                } else if ($operation == 'reverttodraft') {
5032
                    $this->process_revert_to_draft($userid);
5033
                } else if ($operation == 'removesubmission') {
5034
                    $this->process_remove_submission($userid);
5035
                } else if ($operation == 'addattempt') {
5036
                    if (!$this->get_instance()->teamsubmission) {
5037
                        $this->process_add_attempt($userid);
1 efrain 5038
                    }
5039
                }
5040
            }
5041
        }
1441 ariadna 5042
        if ($this->get_instance()->teamsubmission && $operation == 'addattempt') {
5043
            // This needs to be handled separately so that each team submission is only re-opened one time.
5044
            $this->process_add_attempt_group($userlist);
5045
        }
1 efrain 5046
 
5047
        return 'grading';
5048
    }
5049
 
5050
    /**
5051
     * Shows a form that allows the workflow state for selected submissions to be changed.
1441 ariadna 5052
     * You should confirm sesskey before calling this function.
1 efrain 5053
     *
5054
     * @return string - the page to view after processing these actions
5055
     */
1441 ariadna 5056
    protected function view_batch_set_workflow_state() {
1 efrain 5057
        global $CFG, $DB;
5058
 
5059
        require_once($CFG->dirroot . '/mod/assign/batchsetmarkingworkflowstateform.php');
5060
 
5061
        $o = '';
5062
 
1441 ariadna 5063
        $users = required_param('selectedusers', PARAM_SEQUENCE);
1 efrain 5064
        $userlist = explode(',', $users);
5065
 
1441 ariadna 5066
        $formdata = array('id' => $this->get_course_module()->id,
5067
                          'selectedusers' => $users);
1 efrain 5068
 
5069
        $usershtml = '';
5070
 
5071
        $usercount = 0;
5072
        // TODO Does not support custom user profile fields (MDL-70456).
5073
        $extrauserfields = \core_user\fields::get_identity_fields($this->get_context(), false);
5074
        $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
5075
        foreach ($userlist as $userid) {
5076
            if ($usercount >= 5) {
5077
                $usershtml .= get_string('moreusers', 'assign', count($userlist) - 5);
5078
                break;
5079
            }
1441 ariadna 5080
            $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
1 efrain 5081
 
1441 ariadna 5082
            $usershtml .= $this->get_renderer()->render(new assign_user_summary($user,
5083
                                                                $this->get_course()->id,
5084
                                                                $viewfullnames,
5085
                                                                $this->is_blind_marking(),
5086
                                                                $this->get_uniqueid_for_user($user->id),
5087
                                                                $extrauserfields,
5088
                                                                !$this->is_active_user($userid)));
1 efrain 5089
            $usercount += 1;
5090
        }
5091
 
5092
        $formparams = array(
5093
            'userscount' => count($userlist),
5094
            'usershtml' => $usershtml,
5095
            'markingworkflowstates' => $this->get_marking_workflow_states_for_current_user()
5096
        );
5097
 
5098
        $mform = new mod_assign_batch_set_marking_workflow_state_form(null, $formparams);
5099
        $mform->set_data($formdata);    // Initialises the hidden elements.
1441 ariadna 5100
        $header = new assign_header($this->get_instance(),
1 efrain 5101
            $this->get_context(),
5102
            $this->show_intro(),
5103
            $this->get_course_module()->id,
1441 ariadna 5104
            get_string('setmarkingworkflowstate', 'assign'));
1 efrain 5105
        $o .= $this->get_renderer()->render($header);
5106
        $o .= $this->get_renderer()->render(new assign_form('setworkflowstate', $mform));
5107
        $o .= $this->view_footer();
5108
 
5109
        \mod_assign\event\batch_set_workflow_state_viewed::create_from_assign($this)->trigger();
5110
 
5111
        return $o;
5112
    }
5113
 
5114
    /**
5115
     * Shows a form that allows the allocated marker for selected submissions to be changed.
1441 ariadna 5116
     * You should confirm sesskey before calling this function.
1 efrain 5117
     *
5118
     * @return string - the page to view after processing these actions
5119
     */
1441 ariadna 5120
    public function view_batch_markingallocation() {
1 efrain 5121
        global $CFG, $DB;
5122
 
5123
        require_once($CFG->dirroot . '/mod/assign/batchsetallocatedmarkerform.php');
5124
 
5125
        $o = '';
5126
 
1441 ariadna 5127
        $users = required_param('selectedusers', PARAM_SEQUENCE);
1 efrain 5128
        $userlist = explode(',', $users);
5129
 
1441 ariadna 5130
        $formdata = array('id' => $this->get_course_module()->id,
5131
                          'selectedusers' => $users);
1 efrain 5132
 
5133
        $usershtml = '';
5134
 
5135
        $usercount = 0;
5136
        // TODO Does not support custom user profile fields (MDL-70456).
5137
        $extrauserfields = \core_user\fields::get_identity_fields($this->get_context(), false);
5138
        $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
5139
        foreach ($userlist as $userid) {
5140
            if ($usercount >= 5) {
5141
                $usershtml .= get_string('moreusers', 'assign', count($userlist) - 5);
5142
                break;
5143
            }
1441 ariadna 5144
            $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
1 efrain 5145
 
1441 ariadna 5146
            $usershtml .= $this->get_renderer()->render(new assign_user_summary($user,
1 efrain 5147
                $this->get_course()->id,
5148
                $viewfullnames,
5149
                $this->is_blind_marking(),
5150
                $this->get_uniqueid_for_user($user->id),
5151
                $extrauserfields,
1441 ariadna 5152
                !$this->is_active_user($userid)));
1 efrain 5153
            $usercount += 1;
5154
        }
5155
 
5156
        $formparams = array(
5157
            'userscount' => count($userlist),
5158
            'usershtml' => $usershtml,
5159
        );
5160
 
5161
        list($sort, $params) = users_order_by_sql('u');
5162
        // Only enrolled users could be assigned as potential markers.
5163
        $markers = get_enrolled_users($this->get_context(), 'mod/assign:grade', 0, 'u.*', $sort);
5164
        $markerlist = array();
5165
        foreach ($markers as $marker) {
5166
            $markerlist[$marker->id] = fullname($marker);
5167
        }
5168
 
5169
        $formparams['markers'] = $markerlist;
5170
 
5171
        $mform = new mod_assign_batch_set_allocatedmarker_form(null, $formparams);
5172
        $mform->set_data($formdata);    // Initialises the hidden elements.
1441 ariadna 5173
        $header = new assign_header($this->get_instance(),
1 efrain 5174
            $this->get_context(),
5175
            $this->show_intro(),
5176
            $this->get_course_module()->id,
1441 ariadna 5177
            get_string('setmarkingallocation', 'assign'));
1 efrain 5178
        $o .= $this->get_renderer()->render($header);
5179
        $o .= $this->get_renderer()->render(new assign_form('setworkflowstate', $mform));
5180
        $o .= $this->view_footer();
5181
 
5182
        \mod_assign\event\batch_set_marker_allocation_viewed::create_from_assign($this)->trigger();
5183
 
5184
        return $o;
5185
    }
5186
 
5187
    /**
5188
     * Ask the user to confirm they want to submit their work for grading.
5189
     *
5190
     * @param moodleform $mform - null unless form validation has failed
5191
     * @return string
5192
     */
1441 ariadna 5193
    protected function check_submit_for_grading($mform) {
1 efrain 5194
        global $USER, $CFG;
5195
 
5196
        require_once($CFG->dirroot . '/mod/assign/submissionconfirmform.php');
5197
 
5198
        // Check that all of the submission plugins are ready for this submission.
5199
        // Also check whether there is something to be submitted as well against atleast one.
5200
        $notifications = array();
5201
        $submission = $this->get_user_submission($USER->id, false);
5202
        if ($this->get_instance()->teamsubmission) {
5203
            $submission = $this->get_group_submission($USER->id, 0, false);
5204
        }
5205
 
5206
        $plugins = $this->get_submission_plugins();
5207
        $hassubmission = false;
5208
        foreach ($plugins as $plugin) {
5209
            if ($plugin->is_enabled() && $plugin->is_visible()) {
5210
                $check = $plugin->precheck_submission($submission);
5211
                if ($check !== true) {
5212
                    $notifications[] = $check;
5213
                }
5214
 
5215
                if (is_object($submission) && !$plugin->is_empty($submission)) {
5216
                    $hassubmission = true;
5217
                }
5218
            }
5219
        }
5220
 
5221
        // If there are no submissions and no existing notifications to be displayed the stop.
5222
        if (!$hassubmission && !$notifications) {
5223
            $notifications[] = get_string('addsubmission_help', 'assign');
5224
        }
5225
 
5226
        $data = new stdClass();
5227
        $adminconfig = $this->get_admin_config();
5228
        $requiresubmissionstatement = $this->get_instance()->requiresubmissionstatement;
5229
        $submissionstatement = '';
5230
 
5231
        if ($requiresubmissionstatement) {
5232
            $submissionstatement = $this->get_submissionstatement($adminconfig, $this->get_instance(), $this->get_context());
5233
        }
5234
 
5235
        // If we get back an empty submission statement, we have to set $requiredsubmisisonstatement to false to prevent
5236
        // that the submission statement checkbox will be displayed.
5237
        if (empty($submissionstatement)) {
5238
            $requiresubmissionstatement = false;
5239
        }
5240
 
5241
        if ($mform == null) {
1441 ariadna 5242
            $mform = new mod_assign_confirm_submission_form(null, array($requiresubmissionstatement,
5243
                                                                        $submissionstatement,
5244
                                                                        $this->get_course_module()->id,
5245
                                                                        $data));
1 efrain 5246
        }
5247
        $o = '';
1441 ariadna 5248
        $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
5249
                                                              $this->get_context(),
5250
                                                              $this->show_intro(),
5251
                                                              $this->get_course_module()->id,
5252
                                                              get_string('confirmsubmissionheading', 'assign')));
5253
        $submitforgradingpage = new assign_submit_for_grading_page($notifications,
5254
                                                                   $this->get_course_module()->id,
5255
                                                                   $mform);
1 efrain 5256
        $o .= $this->get_renderer()->render($submitforgradingpage);
5257
        $o .= $this->view_footer();
5258
 
5259
        \mod_assign\event\submission_confirmation_form_viewed::create_from_assign($this)->trigger();
5260
 
5261
        return $o;
5262
    }
5263
 
5264
    /**
5265
     * Creates an assign_submission_status renderable.
5266
     *
5267
     * @param stdClass $user the user to get the report for
5268
     * @param bool $showlinks return plain text or links to the profile
5269
     * @return assign_submission_status renderable object
5270
     */
1441 ariadna 5271
    public function get_assign_submission_status_renderable($user, $showlinks) {
1 efrain 5272
        global $PAGE;
5273
 
5274
        $instance = $this->get_instance();
5275
        $flags = $this->get_user_flags($user->id, false);
5276
        $submission = $this->get_user_submission($user->id, false);
5277
 
5278
        $teamsubmission = null;
5279
        $submissiongroup = null;
5280
        $notsubmitted = array();
5281
        if ($instance->teamsubmission) {
5282
            $teamsubmission = $this->get_group_submission($user->id, 0, false);
5283
            $submissiongroup = $this->get_submission_group($user->id);
5284
            $groupid = 0;
5285
            if ($submissiongroup) {
5286
                $groupid = $submissiongroup->id;
5287
            }
5288
            $notsubmitted = $this->get_submission_group_members_who_have_not_submitted($groupid, false);
5289
        }
5290
 
5291
        $showedit = $showlinks &&
1441 ariadna 5292
                    ($this->is_any_submission_plugin_enabled()) &&
5293
                    $this->can_edit_submission($user->id);
1 efrain 5294
 
5295
        $submissionlocked = ($flags && $flags->locked);
5296
 
5297
        // Grading criteria preview.
5298
        $gradingmanager = get_grading_manager($this->context, 'mod_assign', 'submissions');
5299
        $gradingcontrollerpreview = '';
5300
        if ($gradingmethod = $gradingmanager->get_active_method()) {
5301
            $controller = $gradingmanager->get_controller($gradingmethod);
5302
            if ($controller->is_form_defined()) {
5303
                $gradingcontrollerpreview = $controller->render_preview($PAGE);
5304
            }
5305
        }
5306
 
5307
        $showsubmit = ($showlinks && $this->submissions_open($user->id));
5308
        $showsubmit = ($showsubmit && $this->show_submit_button($submission, $teamsubmission, $user->id));
5309
 
5310
        $extensionduedate = null;
5311
        if ($flags) {
5312
            $extensionduedate = $flags->extensionduedate;
5313
        }
5314
        $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
5315
 
5316
        $gradingstatus = $this->get_grading_status($user->id);
5317
        $usergroups = $this->get_all_groups($user->id);
1441 ariadna 5318
        $submissionstatus = new assign_submission_status($instance->allowsubmissionsfromdate,
5319
                                                          $instance->alwaysshowdescription,
5320
                                                          $submission,
5321
                                                          $instance->teamsubmission,
5322
                                                          $teamsubmission,
5323
                                                          $submissiongroup,
5324
                                                          $notsubmitted,
5325
                                                          $this->is_any_submission_plugin_enabled(),
5326
                                                          $submissionlocked,
5327
                                                          $this->is_graded($user->id),
5328
                                                          $instance->duedate,
5329
                                                          $instance->cutoffdate,
5330
                                                          $this->get_submission_plugins(),
5331
                                                          $this->get_return_action(),
5332
                                                          $this->get_return_params(),
5333
                                                          $this->get_course_module()->id,
5334
                                                          $this->get_course()->id,
5335
                                                          assign_submission_status::STUDENT_VIEW,
5336
                                                          $showedit,
5337
                                                          $showsubmit,
5338
                                                          $viewfullnames,
5339
                                                          $extensionduedate,
5340
                                                          $this->get_context(),
5341
                                                          $this->is_blind_marking(),
5342
                                                          $gradingcontrollerpreview,
5343
                                                          $instance->attemptreopenmethod,
5344
                                                          $instance->maxattempts,
5345
                                                          $gradingstatus,
5346
                                                          $instance->preventsubmissionnotingroup,
5347
                                                          $usergroups,
5348
                                                          $instance->timelimit);
1 efrain 5349
        return $submissionstatus;
5350
    }
5351
 
5352
 
5353
    /**
5354
     * Creates an assign_feedback_status renderable.
5355
     *
5356
     * @param stdClass $user the user to get the report for
5357
     * @return assign_feedback_status renderable object
5358
     */
1441 ariadna 5359
    public function get_assign_feedback_status_renderable($user) {
1 efrain 5360
        global $CFG, $DB, $PAGE;
5361
 
1441 ariadna 5362
        require_once($CFG->libdir.'/gradelib.php');
5363
        require_once($CFG->dirroot.'/grade/grading/lib.php');
1 efrain 5364
 
5365
        $instance = $this->get_instance();
5366
        $grade = $this->get_user_grade($user->id, false);
5367
        $gradingstatus = $this->get_grading_status($user->id);
5368
 
1441 ariadna 5369
        $gradinginfo = grade_get_grades($this->get_course()->id,
5370
                                    'mod',
5371
                                    'assign',
5372
                                    $instance->id,
5373
                                    $user->id);
1 efrain 5374
 
5375
        $gradingitem = null;
5376
        $gradebookgrade = null;
5377
        if (isset($gradinginfo->items[0])) {
5378
            $gradingitem = $gradinginfo->items[0];
5379
            $gradebookgrade = $gradingitem->grades[$user->id];
5380
        }
5381
 
5382
        // Check to see if all feedback plugins are empty.
5383
        $emptyplugins = true;
5384
        if ($grade) {
5385
            foreach ($this->get_feedback_plugins() as $plugin) {
5386
                if ($plugin->is_visible() && $plugin->is_enabled()) {
5387
                    if (!$plugin->is_empty($grade)) {
5388
                        $emptyplugins = false;
5389
                    }
5390
                }
5391
            }
5392
        }
5393
 
5394
        if ($this->get_instance()->markingworkflow && $gradingstatus != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
5395
            $emptyplugins = true; // Don't show feedback plugins until released either.
5396
        }
5397
 
5398
        $cangrade = has_capability('mod/assign:grade', $this->get_context());
5399
        $hasgrade = $this->get_instance()->grade != GRADE_TYPE_NONE &&
1441 ariadna 5400
                        !is_null($gradebookgrade) && !is_null($gradebookgrade->grade);
1 efrain 5401
        $gradevisible = $cangrade || $this->get_instance()->grade == GRADE_TYPE_NONE ||
1441 ariadna 5402
                        (!is_null($gradebookgrade) && !$gradebookgrade->hidden);
1 efrain 5403
        // If there is a visible grade, show the summary.
5404
        if (($hasgrade || !$emptyplugins) && $gradevisible) {
5405
 
5406
            $gradefordisplay = null;
5407
            $gradeddate = null;
5408
            $grader = null;
5409
            $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
5410
 
5411
            $gradingcontrollergrade = '';
5412
            if ($hasgrade) {
5413
                if ($controller = $gradingmanager->get_active_controller()) {
5414
                    $menu = make_grades_menu($this->get_instance()->grade);
5415
                    $controller->set_grade_range($menu, $this->get_instance()->grade > 0);
5416
                    $gradingcontrollergrade = $controller->render_grade(
5417
                        $PAGE,
5418
                        $grade->id,
5419
                        $gradingitem,
5420
                        '',
5421
                        $cangrade
5422
                    );
1441 ariadna 5423
                    // Display the penalty indicator next to the penalized grade, if applicable.
5424
                    $penaltyindicator = \core_grades\penalty_manager::show_penalty_indicator(
5425
                        new \grade_grade([
5426
                            'deductedmark' => $gradebookgrade->deductedmark ?? 0,
5427
                            'overridden' => $gradebookgrade->overridden ?? 0,
5428
                        ], false)
5429
                    );
5430
 
5431
                    $gradefordisplay = $penaltyindicator . $gradebookgrade->str_long_grade;
1 efrain 5432
                } else {
1441 ariadna 5433
                    // This grade info is the grade from gradebook.
5434
                    // We need user id to determine if the grade is overridden or not.
5435
                    $gradefordisplay = $this->display_grade(
5436
                        $gradebookgrade->grade,
5437
                        false,
5438
                        $user->id,
5439
                        0,
5440
                        $gradebookgrade->deductedmark
5441
                    );
1 efrain 5442
                }
5443
                $gradeddate = $gradebookgrade->dategraded;
5444
 
5445
                // Show the grader's identity if 'Hide Grader' is disabled or has the 'Show Hidden Grader' capability.
5446
                if (has_capability('mod/assign:showhiddengrader', $this->context) || !$this->is_hidden_grader()) {
5447
                    // Only display the grader if it is in the right state.
5448
                    if (in_array($gradingstatus, [ASSIGN_GRADING_STATUS_GRADED, ASSIGN_MARKING_WORKFLOW_STATE_RELEASED])) {
5449
                        if (isset($grade->grader) && $grade->grader > 0) {
5450
                            $grader = $DB->get_record('user', array('id' => $grade->grader));
1441 ariadna 5451
                        } else if (isset($gradebookgrade->usermodified)
1 efrain 5452
                            && $gradebookgrade->usermodified > 0
1441 ariadna 5453
                            && has_capability('mod/assign:grade', $this->get_context(), $gradebookgrade->usermodified)) {
1 efrain 5454
                            // Grader not provided. Check that usermodified is a user who can grade.
5455
                            // Case 1: When an assignment is reopened an empty assign_grade is created so the feedback
5456
                            // plugin can know which attempt it's referring to. In this case, usermodifed is a student.
5457
                            // Case 2: When an assignment's grade is overrided via the gradebook, usermodified is a grader.
5458
                            $grader = $DB->get_record('user', array('id' => $gradebookgrade->usermodified));
5459
                        }
5460
                    }
5461
                }
5462
            }
5463
 
5464
            $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
5465
 
5466
            if ($grade) {
5467
                \mod_assign\event\feedback_viewed::create_from_grade($this, $grade)->trigger();
5468
            }
5469
            $feedbackstatus = new assign_feedback_status(
5470
                $gradefordisplay,
5471
                $gradeddate,
5472
                $grader,
5473
                $this->get_feedback_plugins(),
5474
                $grade,
5475
                $this->get_course_module()->id,
5476
                $this->get_return_action(),
5477
                $this->get_return_params(),
5478
                $viewfullnames,
5479
                $gradingcontrollergrade
5480
            );
5481
 
5482
            return $feedbackstatus;
5483
        }
5484
        return;
5485
    }
5486
 
5487
    /**
5488
     * Creates an assign_attempt_history renderable.
5489
     *
5490
     * @param stdClass $user the user to get the report for
5491
     * @return assign_attempt_history renderable object
5492
     */
1441 ariadna 5493
    public function get_assign_attempt_history_renderable($user) {
1 efrain 5494
 
5495
        $allsubmissions = $this->get_all_submissions($user->id);
5496
        $allgrades = $this->get_all_grades($user->id);
5497
 
1441 ariadna 5498
        $history = new assign_attempt_history($allsubmissions,
5499
                                              $allgrades,
5500
                                              $this->get_submission_plugins(),
5501
                                              $this->get_feedback_plugins(),
5502
                                              $this->get_course_module()->id,
5503
                                              $this->get_return_action(),
5504
                                              $this->get_return_params(),
5505
                                              false,
5506
                                              0,
5507
                                              0);
1 efrain 5508
        return $history;
5509
    }
5510
 
5511
    /**
5512
     * Print 2 tables of information with no action links -
5513
     * the submission summary and the grading summary.
5514
     *
5515
     * @param stdClass $user the user to print the report for
5516
     * @param bool $showlinks - Return plain text or links to the profile
5517
     * @return string - the html summary
5518
     */
1441 ariadna 5519
    public function view_student_summary($user, $showlinks) {
1 efrain 5520
 
5521
        $o = '';
5522
 
5523
        if ($this->can_view_submission($user->id)) {
5524
            if (has_capability('mod/assign:viewownsubmissionsummary', $this->get_context(), $user, false)) {
5525
                // The user can view the submission summary.
5526
                $submissionstatus = $this->get_assign_submission_status_renderable($user, $showlinks);
5527
                $o .= $this->get_renderer()->render($submissionstatus);
5528
            }
5529
 
5530
            // If there is a visible grade, show the feedback.
5531
            $feedbackstatus = $this->get_assign_feedback_status_renderable($user);
5532
            if ($feedbackstatus) {
5533
                $o .= $this->get_renderer()->render($feedbackstatus);
5534
            }
5535
 
5536
            // If there is more than one submission, show the history.
5537
            $history = $this->get_assign_attempt_history_renderable($user);
5538
            if (count($history->submissions) > 1) {
5539
                $o .= $this->get_renderer()->render($history);
5540
            }
5541
        }
5542
        return $o;
5543
    }
5544
 
5545
    /**
5546
     * Returns true if the submit subsission button should be shown to the user.
5547
     *
5548
     * @param stdClass $submission The users own submission record.
5549
     * @param stdClass $teamsubmission The users team submission record if there is one
5550
     * @param int $userid The user
5551
     * @return bool
5552
     */
1441 ariadna 5553
    protected function show_submit_button($submission = null, $teamsubmission = null, $userid = null) {
1 efrain 5554
        if (!has_capability('mod/assign:submit', $this->get_context(), $userid, false)) {
5555
            // The user does not have the capability to submit.
5556
            return false;
5557
        }
5558
        if ($teamsubmission) {
5559
            if ($teamsubmission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
5560
                // The assignment submission has been completed.
5561
                return false;
5562
            } else if ($this->submission_empty($teamsubmission)) {
5563
                // There is nothing to submit yet.
5564
                return false;
5565
            } else if ($submission && $submission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
5566
                // The user has already clicked the submit button on the team submission.
5567
                return false;
5568
            } else if (
5569
                !empty($this->get_instance()->preventsubmissionnotingroup)
5570
                && $this->get_submission_group($userid) == false
5571
            ) {
5572
                return false;
5573
            }
5574
        } else if ($submission) {
5575
            if ($submission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
5576
                // The assignment submission has been completed.
5577
                return false;
5578
            } else if ($this->submission_empty($submission)) {
5579
                // There is nothing to submit.
5580
                return false;
5581
            }
5582
        } else {
5583
            // We've not got a valid submission or team submission.
5584
            return false;
5585
        }
5586
        // Last check is that this instance allows drafts.
5587
        return $this->get_instance()->submissiondrafts;
5588
    }
5589
 
5590
    /**
5591
     * Get the grades for all previous attempts.
5592
     * For each grade - the grader is a full user record,
5593
     * and gradefordisplay is added (rendered from grading manager).
5594
     *
5595
     * @param int $userid If not set, $USER->id will be used.
5596
     * @return array $grades All grade records for this user.
5597
     */
1441 ariadna 5598
    protected function get_all_grades($userid) {
1 efrain 5599
        global $DB, $USER, $PAGE;
5600
 
5601
        // If the userid is not null then use userid.
5602
        if (!$userid) {
5603
            $userid = $USER->id;
5604
        }
5605
 
1441 ariadna 5606
        $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
1 efrain 5607
 
5608
        $grades = $DB->get_records('assign_grades', $params, 'attemptnumber ASC');
5609
 
5610
        $gradercache = array();
5611
        $cangrade = has_capability('mod/assign:grade', $this->get_context());
5612
 
5613
        // Show the grader's identity if 'Hide Grader' is disabled or has the 'Show Hidden Grader' capability.
5614
        $showgradername = (
5615
            has_capability('mod/assign:showhiddengrader', $this->context, $userid) or
5616
            !$this->is_hidden_grader()
5617
        );
5618
 
5619
        // Need gradingitem and gradingmanager.
5620
        $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
5621
        $controller = $gradingmanager->get_active_controller();
5622
 
1441 ariadna 5623
        $gradinginfo = grade_get_grades($this->get_course()->id,
5624
                                        'mod',
5625
                                        'assign',
5626
                                        $this->get_instance()->id,
5627
                                        $userid);
1 efrain 5628
 
5629
        $gradingitem = null;
5630
        if (isset($gradinginfo->items[0])) {
5631
            $gradingitem = $gradinginfo->items[0];
5632
        }
5633
 
5634
        foreach ($grades as $grade) {
5635
            // First lookup the grader info.
5636
            if (!$showgradername) {
5637
                $grade->grader = null;
5638
            } else if (isset($gradercache[$grade->grader])) {
5639
                $grade->grader = $gradercache[$grade->grader];
5640
            } else if ($grade->grader > 0) {
5641
                // Not in cache - need to load the grader record.
1441 ariadna 5642
                $grade->grader = $DB->get_record('user', array('id'=>$grade->grader));
1 efrain 5643
                if ($grade->grader) {
5644
                    $gradercache[$grade->grader->id] = $grade->grader;
5645
                }
5646
            }
5647
 
1441 ariadna 5648
            // The assign grade for each attempt is not stored in the gradebook.
5649
            // We need to calculate them from assign_grade records.
5650
            [$penalisedgrade, $deductedmark] = $this->calculate_penalised_grade($grade);
5651
 
1 efrain 5652
            // Now get the gradefordisplay.
5653
            if ($controller) {
5654
                $controller->set_grade_range(make_grades_menu($this->get_instance()->grade), $this->get_instance()->grade > 0);
1441 ariadna 5655
                // Display the penalty indicator next to the penalized grade, if applicable.
5656
                $penaltyindicator = \core_grades\penalty_manager::show_penalty_indicator(
5657
                    new \grade_grade([
5658
                        'deductedmark' => $deductedmark,
5659
                        'overridden' => $userid > 0 ? $this->get_grade_item()->get_grade($userid)->overridden : 0,
5660
                    ], false)
1254 ariadna 5661
                );
1441 ariadna 5662
                $gradeoutput = $penaltyindicator . format_float($penalisedgrade, $this->get_grade_item()->get_decimals());
5663
 
5664
                $grade->gradefordisplay = $controller->render_grade($PAGE, $grade->id, $gradingitem, $gradeoutput, $cangrade);
1 efrain 5665
            } else {
1441 ariadna 5666
                // We do not need user id here as the overriden grade should not affect the previous attempts.
5667
                $grade->gradefordisplay = $this->display_grade($penalisedgrade, false, 0, 0, $deductedmark);
1 efrain 5668
            }
1441 ariadna 5669
 
1 efrain 5670
        }
5671
 
5672
        return $grades;
5673
    }
5674
 
5675
    /**
1441 ariadna 5676
     * Calculate penalised grade and deducted mark.
5677
     *
5678
     * @param stdClass $grade The grade object
5679
     * @return array [$penalisedgrade, $deductedmark] the penalised grade and the deducted mark
5680
     */
5681
    public function calculate_penalised_grade(stdClass $grade): array {
5682
        $penalisedgrade = $grade->grade;
5683
        $deductedmark = 0;
5684
 
5685
        // No calculation needed if the grade is null or negative.
5686
        if (is_null($penalisedgrade) || $penalisedgrade < 0) {
5687
            return [$penalisedgrade, $deductedmark];
5688
        }
5689
 
5690
        if ($grade->penalty > 0) {
5691
            $deductedmark = $grade->grade * $grade->penalty / 100;
5692
            $penalisedgrade = $grade->grade - $deductedmark;
5693
        }
5694
        return [$penalisedgrade, $deductedmark];
5695
    }
5696
 
5697
    /**
1 efrain 5698
     * Get the submissions for all previous attempts.
5699
     *
5700
     * @param int $userid If not set, $USER->id will be used.
5701
     * @return array $submissions All submission records for this user (or group).
5702
     */
1441 ariadna 5703
    public function get_all_submissions($userid) {
1 efrain 5704
        global $DB, $USER;
5705
 
5706
        // If the userid is not null then use userid.
5707
        if (!$userid) {
5708
            $userid = $USER->id;
5709
        }
5710
 
5711
        $params = array();
5712
 
5713
        if ($this->get_instance()->teamsubmission) {
5714
            $groupid = 0;
5715
            $group = $this->get_submission_group($userid);
5716
            if ($group) {
5717
                $groupid = $group->id;
5718
            }
5719
 
5720
            // Params to get the group submissions.
1441 ariadna 5721
            $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
1 efrain 5722
        } else {
5723
            // Params to get the user submissions.
1441 ariadna 5724
            $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
1 efrain 5725
        }
5726
 
5727
        // Return the submissions ordered by attempt.
5728
        $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber ASC');
5729
 
5730
        return $submissions;
5731
    }
5732
 
5733
    /**
5734
     * Creates an assign_grading_summary renderable.
5735
     *
5736
     * @param mixed $activitygroup int|null the group for calculating the grading summary (if null the function will determine it)
5737
     * @return assign_grading_summary renderable object
5738
     */
1441 ariadna 5739
    public function get_assign_grading_summary_renderable($activitygroup = null) {
1 efrain 5740
 
5741
        $instance = $this->get_default_instance(); // Grading summary requires the raw dates, regardless of relativedates mode.
5742
        $cm = $this->get_course_module();
5743
        $course = $this->get_course();
5744
 
5745
        $draft = ASSIGN_SUBMISSION_STATUS_DRAFT;
5746
        $submitted = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
5747
        $isvisible = $cm->visible;
5748
 
5749
        if ($activitygroup === null) {
5750
            $activitygroup = groups_get_activity_group($cm);
5751
        }
5752
 
5753
        if ($instance->teamsubmission) {
5754
            $warnofungroupedusers = assign_grading_summary::WARN_GROUPS_NO;
5755
            $defaultteammembers = $this->get_submission_group_members(0, true);
5756
            if (count($defaultteammembers) > 0) {
5757
                if ($instance->preventsubmissionnotingroup) {
5758
                    $warnofungroupedusers = assign_grading_summary::WARN_GROUPS_REQUIRED;
5759
                } else {
5760
                    $warnofungroupedusers = assign_grading_summary::WARN_GROUPS_OPTIONAL;
5761
                }
5762
            }
5763
 
5764
            $summary = new assign_grading_summary(
5765
                $this->count_teams($activitygroup),
5766
                $instance->submissiondrafts,
5767
                $this->count_submissions_with_status($draft, $activitygroup),
5768
                $this->is_any_submission_plugin_enabled(),
5769
                $this->count_submissions_with_status($submitted, $activitygroup),
5770
                $this->get_cutoffdate($activitygroup),
5771
                $this->get_duedate($activitygroup),
5772
                $this->get_timelimit($activitygroup),
5773
                $this->get_course_module()->id,
5774
                $this->count_submissions_need_grading($activitygroup),
5775
                $instance->teamsubmission,
5776
                $warnofungroupedusers,
5777
                $course->relativedatesmode,
5778
                $course->startdate,
5779
                $this->can_grade(),
5780
                $isvisible,
5781
                $this->get_course_module()
5782
            );
5783
        } else {
5784
            // The active group has already been updated in groups_print_activity_menu().
5785
            $countparticipants = $this->count_participants($activitygroup);
5786
            $summary = new assign_grading_summary(
5787
                $countparticipants,
5788
                $instance->submissiondrafts,
5789
                $this->count_submissions_with_status($draft, $activitygroup),
5790
                $this->is_any_submission_plugin_enabled(),
5791
                $this->count_submissions_with_status($submitted, $activitygroup),
5792
                $this->get_cutoffdate($activitygroup),
5793
                $this->get_duedate($activitygroup),
5794
                $this->get_timelimit($activitygroup),
5795
                $this->get_course_module()->id,
5796
                $this->count_submissions_need_grading($activitygroup),
5797
                $instance->teamsubmission,
5798
                assign_grading_summary::WARN_GROUPS_NO,
5799
                $course->relativedatesmode,
5800
                $course->startdate,
5801
                $this->can_grade(),
5802
                $isvisible,
5803
                $this->get_course_module()
5804
            );
5805
        }
5806
 
5807
        return $summary;
5808
    }
5809
 
5810
    /**
5811
     * Helper function to allow up to fetch the group overrides via one query as opposed to many calls.
5812
     *
5813
     * @param int $activitygroup The group we want to check the overrides of
5814
     * @return mixed Can return either a fetched DB object, local object or false
5815
     */
1441 ariadna 5816
    private function get_override_data(int $activitygroup) {
1 efrain 5817
        global $DB;
5818
 
5819
        $instanceid = $this->get_instance()->id;
5820
        $cachekey = "$instanceid-$activitygroup";
5821
        if (isset($this->overridedata[$cachekey])) {
5822
            return $this->overridedata[$cachekey];
5823
        }
5824
 
5825
        $params = ['groupid' => $activitygroup, 'assignid' => $instanceid];
5826
        $this->overridedata[$cachekey] = $DB->get_record('assign_overrides', $params);
5827
        return $this->overridedata[$cachekey];
5828
    }
5829
 
5830
    /**
5831
     * Return group override duedate.
5832
     *
5833
     * @param int $activitygroup Activity active group
5834
     * @return int $duedate
5835
     */
1441 ariadna 5836
    private function get_duedate($activitygroup = null) {
1 efrain 5837
        if ($activitygroup === null) {
5838
            $activitygroup = groups_get_activity_group($this->get_course_module());
5839
        }
5840
        if ($this->can_view_grades() && !empty($activitygroup)) {
5841
            $groupoverride = $this->get_override_data($activitygroup);
5842
            if (!empty($groupoverride->duedate)) {
5843
                return $groupoverride->duedate;
5844
            }
5845
        }
5846
        return $this->get_instance()->duedate;
5847
    }
5848
 
5849
    /**
5850
     * Return group override timelimit.
5851
     *
5852
     * @param null|int $activitygroup Activity active group
5853
     * @return int $timelimit
5854
     */
1441 ariadna 5855
    private function get_timelimit(?int $activitygroup = null): int {
1 efrain 5856
        if ($activitygroup === null) {
5857
            $activitygroup = groups_get_activity_group($this->get_course_module());
5858
        }
5859
        if ($this->can_view_grades() && !empty($activitygroup)) {
5860
            $groupoverride = $this->get_override_data($activitygroup);
5861
            if (!empty($groupoverride->timelimit)) {
5862
                return $groupoverride->timelimit;
5863
            }
5864
        }
5865
        return $this->get_instance()->timelimit;
5866
    }
5867
 
5868
    /**
5869
     * Return group override cutoffdate.
5870
     *
5871
     * @param null|int $activitygroup Activity active group
5872
     * @return int $cutoffdate
5873
     */
1441 ariadna 5874
    private function get_cutoffdate(?int $activitygroup = null): int {
1 efrain 5875
        if ($activitygroup === null) {
5876
            $activitygroup = groups_get_activity_group($this->get_course_module());
5877
        }
5878
        if ($this->can_view_grades() && !empty($activitygroup)) {
5879
            $groupoverride = $this->get_override_data($activitygroup);
5880
            if (!empty($groupoverride->cutoffdate)) {
5881
                return $groupoverride->cutoffdate;
5882
            }
5883
        }
5884
        return $this->get_instance()->cutoffdate;
5885
    }
5886
 
5887
    /**
5888
     * View submissions page (contains details of current submission).
5889
     *
5890
     * @return string
5891
     */
1441 ariadna 5892
    protected function view_submission_page() {
1 efrain 5893
        global $CFG, $DB, $USER, $PAGE;
5894
 
5895
        $instance = $this->get_instance();
5896
 
5897
        $this->add_grade_notices();
5898
 
5899
        $o = '';
5900
 
5901
        $postfix = '';
5902
        if ($this->has_visible_attachments() && (!$this->get_instance($USER->id)->submissionattachments)) {
5903
            $postfix = $this->render_area_files('mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0);
5904
        }
5905
 
1441 ariadna 5906
        $o .= $this->get_renderer()->render(new assign_header($instance,
5907
                                                      $this->get_context(),
5908
                                                      $this->show_intro(),
5909
                                                      $this->get_course_module()->id,
5910
                                                      '', '', $postfix));
1 efrain 5911
 
5912
        // Display plugin specific headers.
5913
        $plugins = array_merge($this->get_submission_plugins(), $this->get_feedback_plugins());
5914
        foreach ($plugins as $plugin) {
5915
            if ($plugin->is_enabled() && $plugin->is_visible()) {
5916
                $o .= $this->get_renderer()->render(new assign_plugin_header($plugin));
5917
            }
5918
        }
5919
 
5920
        if ($this->can_view_grades()) {
5921
            $actionbuttons = new \mod_assign\output\actionmenu($this->get_course_module()->id);
5922
            $o .= $this->get_renderer()->submission_actionmenu($actionbuttons);
5923
 
5924
            $summary = $this->get_assign_grading_summary_renderable();
5925
            $o .= $this->get_renderer()->render($summary);
5926
        }
5927
 
5928
        if ($this->can_view_submission($USER->id)) {
5929
            $o .= $this->view_submission_action_bar($instance, $USER);
5930
            $o .= $this->view_student_summary($USER, true);
5931
        }
5932
 
5933
        $o .= $this->view_footer();
5934
 
5935
        \mod_assign\event\submission_status_viewed::create_from_assign($this)->trigger();
5936
 
5937
        return $o;
5938
    }
5939
 
5940
    /**
5941
     * The action bar displayed in the submissions page.
5942
     *
5943
     * @param stdClass $instance The settings for the current instance of this assignment
5944
     * @param stdClass $user The user to print the action bar for
5945
     * @return string
5946
     */
1441 ariadna 5947
    public function view_submission_action_bar(stdClass $instance, stdClass $user): string {
1 efrain 5948
        $submission = $this->get_user_submission($user->id, false);
5949
        // Figure out if we are team or solitary submission.
5950
        $teamsubmission = null;
5951
        if ($instance->teamsubmission) {
5952
            $teamsubmission = $this->get_group_submission($user->id, 0, false);
5953
        }
5954
 
5955
        $showsubmit = ($this->submissions_open($user->id)
5956
            && $this->show_submit_button($submission, $teamsubmission, $user->id));
5957
        $showedit = ($this->is_any_submission_plugin_enabled()) && $this->can_edit_submission($user->id);
5958
 
5959
        // The method get_group_submission() says that it returns a stdClass, but it can return false >_>.
5960
        if ($teamsubmission === false) {
5961
            $teamsubmission = new stdClass();
5962
        }
5963
        // Same goes for get_user_submission().
5964
        if ($submission === false) {
5965
            $submission = new stdClass();
5966
        }
5967
        $actionbuttons = new \mod_assign\output\user_submission_actionmenu(
5968
            $this->get_course_module()->id,
5969
            $showsubmit,
5970
            $showedit,
5971
            $submission,
5972
            $teamsubmission,
5973
            $instance->timelimit
5974
        );
5975
 
5976
        return $this->get_renderer()->render($actionbuttons);
5977
    }
5978
 
5979
    /**
5980
     * Convert the final raw grade(s) in the grading table for the gradebook.
5981
     *
5982
     * @param stdClass $grade
5983
     * @return array
5984
     */
1441 ariadna 5985
    protected function convert_grade_for_gradebook(stdClass $grade) {
1 efrain 5986
        $gradebookgrade = array();
5987
        if ($grade->grade >= 0) {
5988
            $gradebookgrade['rawgrade'] = $grade->grade;
5989
        }
5990
        // Allow "no grade" to be chosen.
5991
        if ($grade->grade == -1) {
5992
            $gradebookgrade['rawgrade'] = NULL;
5993
        }
5994
        $gradebookgrade['userid'] = $grade->userid;
5995
        $gradebookgrade['usermodified'] = $grade->grader;
5996
        $gradebookgrade['datesubmitted'] = null;
5997
        $gradebookgrade['dategraded'] = $grade->timemodified;
5998
        if (isset($grade->feedbackformat)) {
5999
            $gradebookgrade['feedbackformat'] = $grade->feedbackformat;
6000
        }
6001
        if (isset($grade->feedbacktext)) {
6002
            $gradebookgrade['feedback'] = $grade->feedbacktext;
6003
        }
6004
        if (isset($grade->feedbackfiles)) {
6005
            $gradebookgrade['feedbackfiles'] = $grade->feedbackfiles;
6006
        }
6007
 
6008
        return $gradebookgrade;
6009
    }
6010
 
6011
    /**
6012
     * Convert submission details for the gradebook.
6013
     *
6014
     * @param stdClass $submission
6015
     * @return array
6016
     */
1441 ariadna 6017
    protected function convert_submission_for_gradebook(stdClass $submission) {
1 efrain 6018
        $gradebookgrade = array();
6019
 
6020
        $gradebookgrade['userid'] = $submission->userid;
6021
        $gradebookgrade['usermodified'] = $submission->userid;
6022
        $gradebookgrade['datesubmitted'] = $submission->timemodified;
6023
 
6024
        return $gradebookgrade;
6025
    }
6026
 
6027
    /**
6028
     * Update grades in the gradebook.
6029
     *
6030
     * @param mixed $submission stdClass|null
6031
     * @param mixed $grade stdClass|null
6032
     * @return bool
6033
     */
1441 ariadna 6034
    protected function gradebook_item_update($submission=null, $grade=null) {
1 efrain 6035
        global $CFG;
6036
 
1441 ariadna 6037
        require_once($CFG->dirroot.'/mod/assign/lib.php');
1 efrain 6038
        // Do not push grade to gradebook if blind marking is active as
6039
        // the gradebook would reveal the students.
1441 ariadna 6040
        if ($this->is_blind_marking() && !$this->is_marking_anonymous()) {
1 efrain 6041
            return false;
6042
        }
6043
 
6044
        // If marking workflow is enabled and grade is not released then remove any grade that may exist in the gradebook.
1441 ariadna 6045
        if ($this->get_instance()->markingworkflow && !empty($grade) &&
6046
                $this->get_grading_status($grade->userid) != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
1 efrain 6047
            // Remove the grade (if it exists) from the gradebook as it is not 'final'.
6048
            $grade->grade = -1;
6049
            $grade->feedbacktext = '';
6050
            $grade->feebackfiles = [];
6051
        }
6052
 
6053
        if ($submission != null) {
6054
            if ($submission->userid == 0) {
6055
                // This is a group submission update.
6056
                $team = groups_get_members($submission->groupid, 'u.id');
6057
 
6058
                foreach ($team as $member) {
6059
                    $membersubmission = clone $submission;
6060
                    $membersubmission->groupid = 0;
6061
                    $membersubmission->userid = $member->id;
6062
                    $this->gradebook_item_update($membersubmission, null);
6063
                }
6064
                return;
6065
            }
6066
 
6067
            $gradebookgrade = $this->convert_submission_for_gradebook($submission);
1441 ariadna 6068
 
1 efrain 6069
        } else {
6070
            $gradebookgrade = $this->convert_grade_for_gradebook($grade);
6071
        }
6072
        // Grading is disabled, return.
6073
        if ($this->grading_disabled($gradebookgrade['userid'])) {
6074
            return false;
6075
        }
6076
        $assign = clone $this->get_instance();
6077
        $assign->cmidnumber = $this->get_course_module()->idnumber;
6078
        // Set assign gradebook feedback plugin status (enabled and visible).
6079
        $assign->gradefeedbackenabled = $this->is_gradebook_feedback_enabled();
6080
        return assign_grade_item_update($assign, $gradebookgrade) == GRADE_UPDATE_OK;
6081
    }
6082
 
6083
    /**
6084
     * Update team submission.
6085
     *
6086
     * @param stdClass $submission
6087
     * @param int $userid
6088
     * @param bool $updatetime
6089
     * @return bool
6090
     */
1441 ariadna 6091
    protected function update_team_submission(stdClass $submission, $userid, $updatetime) {
1 efrain 6092
        global $DB;
6093
 
6094
        if ($updatetime) {
6095
            $submission->timemodified = time();
6096
        }
6097
 
6098
        // First update the submission for the current user.
6099
        $mysubmission = $this->get_user_submission($userid, true, $submission->attemptnumber);
6100
        $mysubmission->status = $submission->status;
6101
 
6102
        $this->update_submission($mysubmission, 0, $updatetime, false);
6103
 
6104
        // Now check the team settings to see if this assignment qualifies as submitted or draft.
6105
        $team = $this->get_submission_group_members($submission->groupid, true);
6106
 
6107
        $allsubmitted = true;
6108
        $anysubmitted = false;
6109
        $result = true;
6110
        if (!in_array($submission->status, [ASSIGN_SUBMISSION_STATUS_NEW, ASSIGN_SUBMISSION_STATUS_REOPENED])) {
6111
            foreach ($team as $member) {
6112
                $membersubmission = $this->get_user_submission($member->id, false, $submission->attemptnumber);
6113
 
6114
                // If no submission found for team member and member is active then everyone has not submitted.
1441 ariadna 6115
                if (!$membersubmission || $membersubmission->status != ASSIGN_SUBMISSION_STATUS_SUBMITTED
6116
                        && ($this->is_active_user($member->id))) {
1 efrain 6117
                    $allsubmitted = false;
6118
                    if ($anysubmitted) {
6119
                        break;
6120
                    }
6121
                } else {
6122
                    $anysubmitted = true;
6123
                }
6124
            }
6125
            if ($this->get_instance()->requireallteammemberssubmit) {
6126
                if ($allsubmitted) {
6127
                    $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
6128
                } else {
6129
                    $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
6130
                }
6131
                $result = $DB->update_record('assign_submission', $submission);
6132
            } else {
6133
                if ($anysubmitted) {
6134
                    $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
6135
                } else {
6136
                    $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
6137
                }
6138
                $result = $DB->update_record('assign_submission', $submission);
6139
            }
6140
        } else {
6141
            // Set the group submission to reopened.
6142
            foreach ($team as $member) {
6143
                $membersubmission = $this->get_user_submission($member->id, true, $submission->attemptnumber);
6144
                $membersubmission->status = $submission->status;
6145
                $result = $DB->update_record('assign_submission', $membersubmission) && $result;
6146
            }
6147
            $result = $DB->update_record('assign_submission', $submission) && $result;
6148
        }
6149
 
6150
        $this->gradebook_item_update($submission);
6151
        return $result;
6152
    }
6153
 
6154
    /**
6155
     * Update grades in the gradebook based on submission time.
6156
     *
6157
     * @param stdClass $submission
6158
     * @param int $userid
6159
     * @param bool $updatetime
6160
     * @param bool $teamsubmission
6161
     * @return bool
6162
     */
1441 ariadna 6163
    protected function update_submission(stdClass $submission, $userid, $updatetime, $teamsubmission) {
1 efrain 6164
        global $DB;
6165
 
6166
        if ($teamsubmission) {
6167
            return $this->update_team_submission($submission, $userid, $updatetime);
6168
        }
6169
 
6170
        if ($updatetime) {
6171
            $submission->timemodified = time();
6172
        }
1441 ariadna 6173
        $result= $DB->update_record('assign_submission', $submission);
1 efrain 6174
        if ($result) {
6175
            $this->gradebook_item_update($submission);
6176
        }
6177
        return $result;
6178
    }
6179
 
6180
    /**
6181
     * Is this assignment open for submissions?
6182
     *
6183
     * Check the due date,
6184
     * prevent late submissions,
6185
     * has this person already submitted,
6186
     * is the assignment locked?
6187
     *
6188
     * @param int $userid - Optional userid so we can see if a different user can submit
6189
     * @param bool $skipenrolled - Skip enrollment checks (because they have been done already)
6190
     * @param stdClass $submission - Pre-fetched submission record (or false to fetch it)
6191
     * @param stdClass $flags - Pre-fetched user flags record (or false to fetch it)
6192
     * @param stdClass $gradinginfo - Pre-fetched user gradinginfo record (or false to fetch it)
6193
     * @return bool
6194
     */
1441 ariadna 6195
    public function submissions_open($userid = 0,
6196
                                     $skipenrolled = false,
6197
                                     $submission = false,
6198
                                     $flags = false,
6199
                                     $gradinginfo = false) {
1 efrain 6200
        global $USER;
6201
 
6202
        if (!$userid) {
6203
            $userid = $USER->id;
6204
        }
6205
 
6206
        $time = time();
6207
        $dateopen = true;
6208
        $finaldate = false;
6209
        if ($this->get_instance()->cutoffdate) {
6210
            $finaldate = $this->get_instance()->cutoffdate;
6211
        }
6212
 
6213
        if ($flags === false) {
6214
            $flags = $this->get_user_flags($userid, false);
6215
        }
6216
        if ($flags && $flags->locked) {
6217
            return false;
6218
        }
6219
 
6220
        // User extensions.
6221
        if ($finaldate) {
6222
            if ($flags && $flags->extensionduedate) {
6223
                // Extension can be before cut off date.
6224
                if ($flags->extensionduedate > $finaldate) {
6225
                    $finaldate = $flags->extensionduedate;
6226
                }
6227
            }
6228
        }
6229
 
6230
        if ($finaldate) {
6231
            $dateopen = ($this->get_instance()->allowsubmissionsfromdate <= $time && $time <= $finaldate);
6232
        } else {
6233
            $dateopen = ($this->get_instance()->allowsubmissionsfromdate <= $time);
6234
        }
6235
 
6236
        if (!$dateopen) {
6237
            return false;
6238
        }
6239
 
6240
        // Now check if this user has already submitted etc.
6241
        if (!$skipenrolled && !is_enrolled($this->get_course_context(), $userid)) {
6242
            return false;
6243
        }
6244
        // Note you can pass null for submission and it will not be fetched.
6245
        if ($submission === false) {
6246
            if ($this->get_instance()->teamsubmission) {
6247
                $submission = $this->get_group_submission($userid, 0, false);
6248
            } else {
6249
                $submission = $this->get_user_submission($userid, false);
6250
            }
6251
        }
6252
        if ($submission) {
6253
 
6254
            if ($this->get_instance()->submissiondrafts && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
6255
                // Drafts are tracked and the student has submitted the assignment.
6256
                return false;
6257
            }
6258
        }
6259
 
6260
        // See if this user grade is locked in the gradebook.
6261
        if ($gradinginfo === false) {
1441 ariadna 6262
            $gradinginfo = grade_get_grades($this->get_course()->id,
6263
                                            'mod',
6264
                                            'assign',
6265
                                            $this->get_instance()->id,
6266
                                            array($userid));
1 efrain 6267
        }
1441 ariadna 6268
        if ($gradinginfo &&
6269
                isset($gradinginfo->items[0]->grades[$userid]) &&
6270
                $gradinginfo->items[0]->grades[$userid]->locked) {
1 efrain 6271
            return false;
6272
        }
6273
 
6274
        return true;
6275
    }
6276
 
6277
    /**
6278
     * Render the files in file area.
6279
     *
6280
     * @param string $component
6281
     * @param string $area
6282
     * @param int $submissionid
6283
     * @return string
6284
     */
1441 ariadna 6285
    public function render_area_files($component, $area, $submissionid) {
1 efrain 6286
        global $USER;
6287
 
1441 ariadna 6288
        return $this->get_renderer()->assign_files($this->context, $submissionid, $area, $component,
6289
                                                   $this->course, $this->coursemodule);
6290
 
1 efrain 6291
    }
6292
 
6293
    /**
6294
     * Capability check to make sure this grader can edit this submission.
6295
     *
6296
     * @param int $userid - The user whose submission is to be edited
6297
     * @param int $graderid (optional) - The user who will do the editing (default to $USER->id).
6298
     * @return bool
6299
     */
1441 ariadna 6300
    public function can_edit_submission($userid, $graderid = 0) {
1 efrain 6301
        global $USER;
6302
 
6303
        if (empty($graderid)) {
6304
            $graderid = $USER->id;
6305
        }
6306
 
6307
        $instance = $this->get_instance();
1441 ariadna 6308
        if ($userid == $graderid &&
1 efrain 6309
            $instance->teamsubmission &&
6310
            $instance->preventsubmissionnotingroup &&
1441 ariadna 6311
            $this->get_submission_group($userid) == false) {
1 efrain 6312
            return false;
6313
        }
6314
 
6315
        if ($userid == $graderid) {
1441 ariadna 6316
            if ($this->submissions_open($userid) &&
6317
                    has_capability('mod/assign:submit', $this->context, $graderid)) {
1 efrain 6318
                // User can edit their own submission.
6319
                return true;
6320
            } else {
6321
                // We need to return here because editothersubmission should never apply to a users own submission.
6322
                return false;
6323
            }
6324
        }
6325
 
6326
        if (!has_capability('mod/assign:editothersubmission', $this->context, $graderid)) {
6327
            return false;
6328
        }
6329
 
6330
        $cm = $this->get_course_module();
1441 ariadna 6331
        if (groups_get_activity_groupmode($cm) == SEPARATEGROUPS &&
6332
                !has_capability('moodle/site:accessallgroups', $this->context, $graderid)) {
1 efrain 6333
            $sharedgroupmembers = $this->get_shared_group_members($cm, $graderid);
6334
            return in_array($userid, $sharedgroupmembers);
6335
        }
6336
        return true;
6337
    }
6338
 
6339
    /**
6340
     * Returns IDs of the users who share group membership with the specified user.
6341
     *
6342
     * @param stdClass|cm_info $cm Course-module
6343
     * @param int $userid User ID
6344
     * @return array An array of ID of users.
6345
     */
1441 ariadna 6346
    public function get_shared_group_members($cm, $userid) {
1 efrain 6347
        if (!isset($this->sharedgroupmembers[$userid])) {
6348
            $this->sharedgroupmembers[$userid] = array();
6349
            if ($members = groups_get_activity_shared_group_members($cm, $userid)) {
6350
                $this->sharedgroupmembers[$userid] = array_keys($members);
6351
            }
6352
        }
6353
 
6354
        return $this->sharedgroupmembers[$userid];
6355
    }
6356
 
6357
    /**
6358
     * Returns a list of teachers that should be grading given submission.
6359
     *
6360
     * @param int $userid The submission to grade
6361
     * @return array
6362
     */
1441 ariadna 6363
    protected function get_graders($userid) {
1 efrain 6364
        // Potential graders should be active users only.
6365
        $potentialgraders = get_enrolled_users($this->context, "mod/assign:grade", null, 'u.*', null, null, null, true);
6366
 
6367
        $graders = array();
6368
        if (groups_get_activity_groupmode($this->get_course_module()) == SEPARATEGROUPS) {
6369
            if ($groups = groups_get_all_groups($this->get_course()->id, $userid, $this->get_course_module()->groupingid)) {
6370
                foreach ($groups as $group) {
6371
                    foreach ($potentialgraders as $grader) {
6372
                        if ($grader->id == $userid) {
6373
                            // Do not send self.
6374
                            continue;
6375
                        }
6376
                        if (groups_is_member($group->id, $grader->id)) {
6377
                            $graders[$grader->id] = $grader;
6378
                        }
6379
                    }
6380
                }
6381
            } else {
6382
                // User not in group, try to find graders without group.
6383
                foreach ($potentialgraders as $grader) {
6384
                    if ($grader->id == $userid) {
6385
                        // Do not send self.
6386
                        continue;
6387
                    }
6388
                    if (!groups_has_membership($this->get_course_module(), $grader->id)) {
6389
                        $graders[$grader->id] = $grader;
6390
                    }
6391
                }
6392
            }
6393
        } else {
6394
            foreach ($potentialgraders as $grader) {
6395
                if ($grader->id == $userid) {
6396
                    // Do not send self.
6397
                    continue;
6398
                }
6399
                // Must be enrolled.
6400
                if (is_enrolled($this->get_course_context(), $grader->id)) {
6401
                    $graders[$grader->id] = $grader;
6402
                }
6403
            }
6404
        }
6405
        return $graders;
6406
    }
6407
 
6408
    /**
6409
     * Returns a list of users that should receive notification about given submission.
6410
     *
6411
     * @param int $userid The submission to grade
6412
     * @return array
6413
     */
1441 ariadna 6414
    protected function get_notifiable_users($userid) {
1 efrain 6415
        // Potential users should be active users only.
1441 ariadna 6416
        $potentialusers = get_enrolled_users($this->context, "mod/assign:receivegradernotifications",
6417
                                             null, 'u.*', null, null, null, true);
1 efrain 6418
 
6419
        $notifiableusers = array();
6420
        if (groups_get_activity_groupmode($this->get_course_module()) == SEPARATEGROUPS) {
6421
            if ($groups = groups_get_all_groups($this->get_course()->id, $userid, $this->get_course_module()->groupingid)) {
6422
                foreach ($groups as $group) {
6423
                    foreach ($potentialusers as $potentialuser) {
6424
                        if ($potentialuser->id == $userid) {
6425
                            // Do not send self.
6426
                            continue;
6427
                        }
6428
                        if (groups_is_member($group->id, $potentialuser->id)) {
6429
                            $notifiableusers[$potentialuser->id] = $potentialuser;
6430
                        }
6431
                    }
6432
                }
6433
            } else {
6434
                // User not in group, try to find graders without group.
6435
                foreach ($potentialusers as $potentialuser) {
6436
                    if ($potentialuser->id == $userid) {
6437
                        // Do not send self.
6438
                        continue;
6439
                    }
6440
                    if (!groups_has_membership($this->get_course_module(), $potentialuser->id)) {
6441
                        $notifiableusers[$potentialuser->id] = $potentialuser;
6442
                    }
6443
                }
6444
            }
6445
        } else {
6446
            foreach ($potentialusers as $potentialuser) {
6447
                if ($potentialuser->id == $userid) {
6448
                    // Do not send self.
6449
                    continue;
6450
                }
6451
                // Must be enrolled.
6452
                if (is_enrolled($this->get_course_context(), $potentialuser->id)) {
6453
                    $notifiableusers[$potentialuser->id] = $potentialuser;
6454
                }
6455
            }
6456
        }
6457
        return $notifiableusers;
6458
    }
6459
 
6460
    /**
6461
     * Message someone about something (static so it can be called from cron).
6462
     *
6463
     * @param stdClass $userfrom
6464
     * @param stdClass $userto
6465
     * @param string $messagetype
6466
     * @param string $eventtype
6467
     * @param int $updatetime
6468
     * @param stdClass $coursemodule
1441 ariadna 6469
     * @param context $context
1 efrain 6470
     * @param stdClass $course
1441 ariadna 6471
     * @param string $modulename - no longer used.
1 efrain 6472
     * @param string $assignmentname
6473
     * @param bool $blindmarking
6474
     * @param int $uniqueidforuser
1441 ariadna 6475
     * @param array $extrainfo extra values to pass to any language strings or templates used in preparing the message.
1 efrain 6476
     * @return void
6477
     */
1441 ariadna 6478
    public static function send_assignment_notification($userfrom,
6479
                                                        $userto,
6480
                                                        $messagetype,
6481
                                                        $eventtype,
6482
                                                        $updatetime,
6483
                                                        $coursemodule,
6484
                                                        $context,
6485
                                                        $course,
6486
                                                        $modulename,
6487
                                                        $assignmentname,
6488
                                                        $blindmarking,
6489
                                                        $uniqueidforuser,
6490
                                                        $extrainfo = []) {
1 efrain 6491
        global $CFG, $PAGE;
6492
 
1441 ariadna 6493
        // We put more data into info that is used by the default language strings,
6494
        // so that there is more flexibility for organisations that want to use
6495
        // Language Customisation to customise the messages.
6496
 
6497
        // Information about the sender - normally the user who performed the action.
1 efrain 6498
        $info = new stdClass();
6499
        if ($blindmarking) {
1441 ariadna 6500
            $userfrom = clone($userfrom);
1 efrain 6501
            $info->username = get_string('participant', 'assign') . ' ' . $uniqueidforuser;
6502
            $userfrom->firstname = get_string('participant', 'assign');
6503
            $userfrom->lastname = $uniqueidforuser;
6504
            $userfrom->email = $CFG->noreplyaddress;
6505
        } else {
1441 ariadna 6506
            $info->username = core_user::get_fullname($userfrom, $context, ['override' => true]);
1 efrain 6507
        }
1441 ariadna 6508
 
6509
        // Information about the recipient (for greeting, etc.).
6510
        $info->recipentname = core_user::get_fullname($userto, $context, ['override' => true]);
6511
 
6512
        // Information about the assignment.
6513
        $info->assignment = format_string($assignmentname, true, ['context' => $context]);
1254 ariadna 6514
        $info->url = $CFG->wwwroot . '/mod/assign/view.php?id=' . $coursemodule->id;
1441 ariadna 6515
        // Note: URLs here avoid the & character to avoid escaping issues between text and HTML messages.
6516
        $info->assignmentlink  = '<a href="' . $info->url . '">' . s($info->assignment) . ' report</a>';
6517
        $info->assigncmid = $coursemodule->id;
1 efrain 6518
 
1441 ariadna 6519
        // Information about the course.
6520
        $info->courseshortname = format_string($course->shortname, true, ['context' => $context]);
6521
        $info->coursefullname = format_string($course->fullname, true, ['context' => $context]);
6522
        // Note: URLs here avoid the & character to avoid escaping issues between text and HTML messages.
6523
        $info->courseurl = $CFG->wwwroot . '/course/view.php?id=' . $course->id;
6524
        $info->courseassignsurl = $CFG->wwwroot . '/mod/assign/index.php?id=' . $course->id;
6525
 
6526
        // Time of the action.
6527
        $info->timeupdated = userdate($updatetime);
6528
 
6529
        // Other data passed in.
6530
        $info = (object) array_merge((array) $info, $extrainfo);
6531
 
6532
        // Since format_string returns text with HTML characters escaped,
6533
        // we need to un-escape before including in the plain text email and subject line.
6534
        // (Test with a course or assignment called "Escaping & unescaping" to understand).
6535
        $plaintextinfo = clone $info;
6536
        $plaintextinfo->assignment = html_entity_decode($info->assignment);
6537
        $plaintextinfo->courseshortname = html_entity_decode($info->courseshortname);
6538
        $plaintextinfo->coursefullname = html_entity_decode($info->coursefullname);
6539
 
6540
        // Prepare the message subject and bodies.
6541
        $postsubject = get_string($messagetype . 'small', 'assign', $plaintextinfo);
6542
        $smsmessage = get_string($messagetype . 'sms', 'assign', $plaintextinfo);
6543
 
6544
        $renderer = $PAGE->get_renderer('mod_assign');
6545
        $context = clone $plaintextinfo;
6546
        $context->messagetext = get_string($messagetype . 'text', 'assign', $plaintextinfo);
6547
        // Mustache strips off all training whitespace, but we want a newline at the end.
6548
        $posttext = $renderer->render_from_template(
6549
            'mod_assign/messages/notification_text', $context) . "\n";
6550
 
1 efrain 6551
        $posthtml = '';
6552
        if ($userto->mailformat == 1) {
1441 ariadna 6553
            $context = clone $info;
6554
            $context->messagehtml = get_string($messagetype . 'html', 'assign', $info);
6555
            $posthtml = $renderer->render_from_template(
6556
                'mod_assign/messages/notification_html', $context);
1 efrain 6557
        }
6558
 
1441 ariadna 6559
        // Build the message object.
1 efrain 6560
        $eventdata = new \core\message\message();
6561
        $eventdata->courseid         = $course->id;
6562
        $eventdata->modulename       = 'assign';
6563
        $eventdata->userfrom         = $userfrom;
6564
        $eventdata->userto           = $userto;
6565
        $eventdata->subject          = $postsubject;
6566
        $eventdata->fullmessage      = $posttext;
6567
        $eventdata->fullmessageformat = FORMAT_PLAIN;
6568
        $eventdata->fullmessagehtml  = $posthtml;
1441 ariadna 6569
        $eventdata->fullmessagesms   = $smsmessage;
1 efrain 6570
        $eventdata->smallmessage     = $postsubject;
6571
 
6572
        $eventdata->name            = $eventtype;
6573
        $eventdata->component       = 'mod_assign';
6574
        $eventdata->notification    = 1;
6575
        $eventdata->contexturl      = $info->url;
6576
        $eventdata->contexturlname  = $info->assignment;
6577
        $customdata = [
6578
            'cmid' => $coursemodule->id,
6579
            'instance' => $coursemodule->instance,
6580
            'messagetype' => $messagetype,
6581
            'blindmarking' => $blindmarking,
6582
            'uniqueidforuser' => $uniqueidforuser,
6583
        ];
6584
        // Check if the userfrom is real and visible.
6585
        if (!empty($userfrom->id) && core_user::is_real_user($userfrom->id)) {
6586
            $userpicture = new user_picture($userfrom);
6587
            $userpicture->size = 1; // Use f1 size.
6588
            $userpicture->includetoken = $userto->id; // Generate an out-of-session token for the user receiving the message.
6589
            $customdata['notificationiconurl'] = $userpicture->get_url($PAGE)->out(false);
6590
        }
6591
        $eventdata->customdata = $customdata;
6592
 
6593
        message_send($eventdata);
6594
    }
6595
 
6596
    /**
6597
     * Message someone about something.
6598
     *
6599
     * @param stdClass $userfrom
6600
     * @param stdClass $userto
6601
     * @param string $messagetype
6602
     * @param string $eventtype
6603
     * @param int $updatetime
1441 ariadna 6604
     * @param array $extrainfo extra values to pass to any language strings or templates used in preparing the message.
1 efrain 6605
     * @return void
6606
     */
1441 ariadna 6607
    public function send_notification($userfrom, $userto, $messagetype, $eventtype, $updatetime, $extrainfo = []) {
1 efrain 6608
        global $USER;
6609
        $userid = core_user::is_real_user($userfrom->id) ? $userfrom->id : $USER->id;
6610
        $uniqueid = $this->get_uniqueid_for_user($userid);
1441 ariadna 6611
        $oldforcelang = force_current_language($userto->lang);
6612
        self::send_assignment_notification($userfrom,
6613
                                           $userto,
6614
                                           $messagetype,
6615
                                           $eventtype,
6616
                                           $updatetime,
6617
                                           $this->get_course_module(),
6618
                                           $this->get_context(),
6619
                                           $this->get_course(),
6620
                                           $this->get_module_name(),
6621
                                           $this->get_instance()->name,
6622
                                           $this->is_blind_marking(),
6623
                                           $uniqueid,
6624
                                           $extrainfo);
6625
        force_current_language($oldforcelang);
1 efrain 6626
    }
6627
 
6628
    /**
6629
     * Notify student upon successful submission copy.
6630
     *
6631
     * @param stdClass $submission
6632
     * @return void
6633
     */
1441 ariadna 6634
    protected function notify_student_submission_copied(stdClass $submission) {
1 efrain 6635
        global $DB, $USER;
6636
 
6637
        $adminconfig = $this->get_admin_config();
6638
        // Use the same setting for this - no need for another one.
6639
        if (empty($adminconfig->submissionreceipts)) {
6640
            // No need to do anything.
6641
            return;
6642
        }
6643
        if ($submission->userid) {
1441 ariadna 6644
            $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST);
1 efrain 6645
        } else {
6646
            $user = $USER;
6647
        }
1441 ariadna 6648
        $this->send_notification($user,
6649
                                 $user,
6650
                                 'submissioncopied',
6651
                                 'assign_notification',
6652
                                 $submission->timemodified);
1 efrain 6653
    }
6654
    /**
6655
     * Notify student upon successful submission.
6656
     *
6657
     * @param stdClass $submission
6658
     * @return void
6659
     */
1441 ariadna 6660
    protected function notify_student_submission_receipt(stdClass $submission) {
1 efrain 6661
        global $DB, $USER;
6662
 
6663
        $adminconfig = $this->get_admin_config();
6664
        if (empty($adminconfig->submissionreceipts)) {
6665
            // No need to do anything.
6666
            return;
6667
        }
6668
        if ($submission->userid) {
1441 ariadna 6669
            $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST);
1 efrain 6670
        } else {
6671
            $user = $USER;
6672
        }
1441 ariadna 6673
        // Prepare extra data for submission receipt notification.
6674
        $oldforcelang = force_current_language($user->lang);
6675
        $extrainfo = $this->get_submission_summaries_for_messages($submission);
6676
        force_current_language($oldforcelang);
1 efrain 6677
        if ($submission->userid == $USER->id) {
1441 ariadna 6678
            $this->send_notification(core_user::get_noreply_user(),
6679
                                     $user,
6680
                                     'submissionreceipt',
6681
                                     'assign_notification',
6682
                                     $submission->timemodified,
6683
                                     $extrainfo);
1 efrain 6684
        } else {
1441 ariadna 6685
            $this->send_notification($USER,
6686
                                     $user,
6687
                                     'submissionreceiptother',
6688
                                     'assign_notification',
6689
                                     $submission->timemodified,
6690
                                     $extrainfo);
1 efrain 6691
        }
6692
    }
6693
 
6694
    /**
1441 ariadna 6695
     * Produce a summary of a submission that can be used in messages.
6696
     *
6697
     * This function iterates through all enabled submission plugins and calls their
6698
     * `get_submission_summary` method (if implemented). It aggregates the results
6699
     * into a formatted summary string.
6700
     *
6701
     * @param stdClass $submission the submission the message is about. Row from assign_submission table.
6702
     * @return string[] with two elements:
6703
     *      'submissionsummarytext' => a plain text summary,
6704
     *      'submissionsummaryhtml' => an HTML summary.
6705
     */
6706
    protected function get_submission_summaries_for_messages(stdClass $submission): array {
6707
        $textsummaries = [];
6708
        $htmlsummaries = [];
6709
        foreach ($this->submissionplugins as $plugin) {
6710
            if ($plugin->is_enabled() && $plugin->is_visible()) {
6711
                [$textsummary, $htmlsummary] = $plugin->submission_summary_for_messages($submission);
6712
                if ($textsummary) {
6713
                    $textsummaries[] = $textsummary;
6714
                }
6715
                if ($htmlsummary) {
6716
                    $htmlsummaries[] = $htmlsummary;
6717
                }
6718
            }
6719
        }
6720
 
6721
        $textsummary = '';
6722
        if ($textsummaries) {
6723
            $textsummary = get_string('submissioncontains', 'assign') . "\n\n" .
6724
                implode("\n", $textsummaries);
6725
        }
6726
 
6727
        $htmlsummary = '';
6728
        if ($htmlsummaries) {
6729
            $htmlsummary = html_writer::tag('h2', get_string('submissioncontains', 'assign')) .
6730
                implode('', $htmlsummaries);
6731
        }
6732
 
6733
        return [
6734
            'submissionsummarytext' => $textsummary,
6735
            'submissionsummaryhtml' => $htmlsummary,
6736
        ];
6737
    }
6738
 
6739
    /**
1 efrain 6740
     * Send notifications to graders upon student submissions.
6741
     *
6742
     * @param stdClass $submission
6743
     * @return void
6744
     */
1441 ariadna 6745
    protected function notify_graders(stdClass $submission) {
1 efrain 6746
        global $DB, $USER;
6747
 
6748
        $instance = $this->get_instance();
6749
 
6750
        $late = $instance->duedate && ($instance->duedate < time());
6751
 
6752
        if (!$instance->sendnotifications && !($late && $instance->sendlatenotifications)) {
6753
            // No need to do anything.
6754
            return;
6755
        }
6756
 
6757
        if ($submission->userid) {
1441 ariadna 6758
            $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST);
1 efrain 6759
        } else {
6760
            $user = $USER;
6761
        }
6762
 
6763
        if ($notifyusers = $this->get_notifiable_users($user->id)) {
6764
            foreach ($notifyusers as $notifyuser) {
1441 ariadna 6765
                $this->send_notification($user,
6766
                                         $notifyuser,
6767
                                         'gradersubmissionupdated',
6768
                                         'assign_notification',
6769
                                         $submission->timemodified);
1 efrain 6770
            }
6771
        }
6772
    }
6773
 
6774
    /**
6775
     * Submit a submission for grading.
6776
     *
6777
     * @param stdClass $data - The form data
6778
     * @param array $notices - List of error messages to display on an error condition.
6779
     * @return bool Return false if the submission was not submitted.
6780
     */
1441 ariadna 6781
    public function submit_for_grading($data, $notices) {
1 efrain 6782
        global $USER;
6783
 
6784
        $userid = $USER->id;
6785
        if (!empty($data->userid)) {
6786
            $userid = $data->userid;
6787
        }
6788
        // Need submit permission to submit an assignment.
6789
        if ($userid == $USER->id) {
6790
            require_capability('mod/assign:submit', $this->context);
6791
        } else {
6792
            if (!$this->can_edit_submission($userid, $USER->id)) {
6793
                throw new \moodle_exception('nopermission');
6794
            }
6795
        }
6796
 
6797
        $instance = $this->get_instance();
6798
 
6799
        if ($instance->teamsubmission) {
6800
            $submission = $this->get_group_submission($userid, 0, true);
6801
        } else {
6802
            $submission = $this->get_user_submission($userid, true);
6803
        }
6804
 
6805
        if (!$this->submissions_open($userid)) {
6806
            $notices[] = get_string('submissionsclosed', 'assign');
6807
            return false;
6808
        }
6809
 
6810
        $adminconfig = $this->get_admin_config();
6811
 
6812
        $submissionstatement = '';
6813
        if ($instance->requiresubmissionstatement) {
6814
            $submissionstatement = $this->get_submissionstatement($adminconfig, $instance, $this->context);
6815
        }
6816
 
1441 ariadna 6817
        if (!empty($submissionstatement) && $instance->requiresubmissionstatement
6818
                && empty($data->submissionstatement) && $USER->id == $userid) {
1 efrain 6819
            return false;
6820
        }
6821
 
6822
        if ($submission->status != ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
6823
            // Give each submission plugin a chance to process the submission.
6824
            $plugins = $this->get_submission_plugins();
6825
            foreach ($plugins as $plugin) {
6826
                if ($plugin->is_enabled() && $plugin->is_visible()) {
6827
                    $plugin->submit_for_grading($submission);
6828
                }
6829
            }
6830
 
6831
            $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
6832
            $this->update_submission($submission, $userid, true, $instance->teamsubmission);
6833
            $completion = new completion_info($this->get_course());
6834
            if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
1441 ariadna 6835
                $this->update_activity_completion_records($instance->teamsubmission,
6836
                                                          $instance->requireallteammemberssubmit,
6837
                                                          $submission,
6838
                                                          $userid,
6839
                                                          COMPLETION_COMPLETE,
6840
                                                          $completion);
1 efrain 6841
            }
6842
 
6843
            if (!empty($data->submissionstatement) && $USER->id == $userid) {
6844
                \mod_assign\event\statement_accepted::create_from_submission($this, $submission)->trigger();
6845
            }
6846
            $this->notify_graders($submission);
6847
            $this->notify_student_submission_receipt($submission);
6848
 
6849
            \mod_assign\event\assessable_submitted::create_from_submission($this, $submission, false)->trigger();
6850
 
6851
            return true;
6852
        }
6853
        $notices[] = get_string('submissionsclosed', 'assign');
6854
        return false;
6855
    }
6856
 
6857
    /**
6858
     * A students submission is submitted for grading by a teacher.
6859
     *
6860
     * @return bool
6861
     */
1441 ariadna 6862
    protected function process_submit_other_for_grading($mform, $notices) {
1 efrain 6863
        global $USER, $CFG;
6864
 
6865
        require_sesskey();
6866
 
6867
        $userid = optional_param('userid', $USER->id, PARAM_INT);
6868
 
6869
        if (!$this->submissions_open($userid)) {
6870
            $notices[] = get_string('submissionsclosed', 'assign');
6871
            return false;
6872
        }
6873
        $data = new stdClass();
6874
        $data->userid = $userid;
6875
        return $this->submit_for_grading($data, $notices);
6876
    }
6877
 
6878
    /**
6879
     * Assignment submission is processed before grading.
6880
     *
6881
     * @param moodleform|null $mform If validation failed when submitting this form - this is the moodleform.
6882
     *               It can be null.
6883
     * @return bool Return false if the validation fails. This affects which page is displayed next.
6884
     */
1441 ariadna 6885
    protected function process_submit_for_grading($mform, $notices) {
1 efrain 6886
        global $CFG;
6887
 
6888
        require_once($CFG->dirroot . '/mod/assign/submissionconfirmform.php');
6889
        require_sesskey();
6890
 
6891
        if (!$this->submissions_open()) {
6892
            $notices[] = get_string('submissionsclosed', 'assign');
6893
            return false;
6894
        }
6895
 
6896
        $data = new stdClass();
6897
        $adminconfig = $this->get_admin_config();
6898
        $requiresubmissionstatement = $this->get_instance()->requiresubmissionstatement;
6899
 
6900
        $submissionstatement = '';
6901
        if ($requiresubmissionstatement) {
6902
            $submissionstatement = $this->get_submissionstatement($adminconfig, $this->get_instance(), $this->get_context());
6903
        }
6904
 
6905
        // If we get back an empty submission statement, we have to set $requiredsubmisisonstatement to false to prevent
6906
        // that the submission statement checkbox will be displayed.
6907
        if (empty($submissionstatement)) {
6908
            $requiresubmissionstatement = false;
6909
        }
6910
 
6911
        if ($mform == null) {
1441 ariadna 6912
            $mform = new mod_assign_confirm_submission_form(null, array($requiresubmissionstatement,
6913
                                                                    $submissionstatement,
6914
                                                                    $this->get_course_module()->id,
6915
                                                                    $data));
1 efrain 6916
        }
6917
 
6918
        $data = $mform->get_data();
6919
        if (!$mform->is_cancelled()) {
6920
            if ($mform->get_data() == false) {
6921
                return false;
6922
            }
6923
            return $this->submit_for_grading($data, $notices);
6924
        }
6925
        return true;
6926
    }
6927
 
6928
    /**
6929
     * Save the extension date for a single user.
6930
     *
6931
     * @param int $userid The user id
6932
     * @param mixed $extensionduedate Either an integer date or null
6933
     * @return boolean
6934
     */
1441 ariadna 6935
    public function save_user_extension($userid, $extensionduedate) {
6936
        global $DB, $CFG;
6937
        require_once($CFG->dirroot.'/calendar/lib.php');
1 efrain 6938
 
6939
        // Need submit permission to submit an assignment.
6940
        require_capability('mod/assign:grantextension', $this->context);
6941
 
6942
        if (!is_enrolled($this->get_course_context(), $userid)) {
6943
            return false;
6944
        }
6945
        if (!has_capability('mod/assign:submit', $this->context, $userid)) {
6946
            return false;
6947
        }
6948
 
6949
        if ($this->get_instance()->duedate && $extensionduedate) {
6950
            if ($this->get_instance()->duedate > $extensionduedate) {
6951
                return false;
6952
            }
6953
        }
6954
        if ($this->get_instance()->allowsubmissionsfromdate && $extensionduedate) {
6955
            if ($this->get_instance()->allowsubmissionsfromdate > $extensionduedate) {
6956
                return false;
6957
            }
6958
        }
6959
 
6960
        $flags = $this->get_user_flags($userid, true);
6961
        $flags->extensionduedate = $extensionduedate;
6962
 
6963
        $result = $this->update_user_flags($flags);
6964
 
6965
        if ($result) {
6966
            \mod_assign\event\extension_granted::create_from_assign($this, $userid)->trigger();
1441 ariadna 6967
 
6968
            $cm = $this->get_course_module();
6969
            $instance = $this->get_instance();
6970
 
6971
            if ($extensionduedate) {
6972
                $event = $DB->get_record('event', [
6973
                    'userid' => $userid,
6974
                    'eventtype' => ASSIGN_EVENT_TYPE_EXTENSION,
6975
                    'modulename' => 'assign',
6976
                    'instance' => $instance->id,
6977
                ]);
6978
 
6979
                if ($event) {
6980
                    $event->timestart = $extensionduedate;
6981
                    $DB->update_record('event', $event);
6982
                } else {
6983
                    $event = new stdClass();
6984
                    $event->type          = CALENDAR_EVENT_TYPE_ACTION;
6985
                    $event->name          = get_string('calendarextension', 'assign', $instance->name);
6986
                    $event->description   = format_module_intro('assign', $instance, $cm->id);
6987
                    $event->format        = FORMAT_HTML;
6988
                    $event->courseid      = 0;
6989
                    $event->groupid       = 0;
6990
                    $event->userid        = $userid;
6991
                    $event->modulename    = 'assign';
6992
                    $event->instance      = $instance->id;
6993
                    $event->timestart     = $extensionduedate;
6994
                    $event->timeduration  = 0;
6995
                    $event->visible       = instance_is_visible('assign', $instance);
6996
                    $event->eventtype     = ASSIGN_EVENT_TYPE_EXTENSION;
6997
                    $event->priority      = null;
6998
 
6999
                    calendar_event::create($event, false);
7000
                }
7001
            } else {
7002
                $DB->delete_records('event', [
7003
                  'userid' => $userid,
7004
                  'eventtype' => ASSIGN_EVENT_TYPE_EXTENSION,
7005
                  'modulename' => 'assign',
7006
                  'instance' => $instance->id,
7007
                ]);
7008
            }
1 efrain 7009
        }
7010
        return $result;
7011
    }
7012
 
7013
    /**
7014
     * Save extension date.
7015
     *
7016
     * @param moodleform $mform The submitted form
7017
     * @return boolean
7018
     */
1441 ariadna 7019
    protected function process_save_extension(& $mform) {
1 efrain 7020
        global $DB, $CFG;
7021
 
7022
        // Include extension form.
7023
        require_once($CFG->dirroot . '/mod/assign/extensionform.php');
7024
        require_sesskey();
7025
 
7026
        $users = optional_param('userid', 0, PARAM_INT);
7027
        if (!$users) {
7028
            $users = required_param('selectedusers', PARAM_SEQUENCE);
7029
        }
7030
        $userlist = explode(',', $users);
7031
 
7032
        $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate');
7033
        $maxoverride = array('allowsubmissionsfromdate' => 0, 'duedate' => 0, 'cutoffdate' => 0);
7034
        foreach ($userlist as $userid) {
7035
            // To validate extension date with users overrides.
7036
            $override = $this->override_exists($userid);
7037
            foreach ($keys as $key) {
7038
                if ($override->{$key}) {
7039
                    if ($maxoverride[$key] < $override->{$key}) {
7040
                        $maxoverride[$key] = $override->{$key};
7041
                    }
7042
                } else if ($maxoverride[$key] < $this->get_instance()->{$key}) {
7043
                    $maxoverride[$key] = $this->get_instance()->{$key};
7044
                }
7045
            }
7046
        }
7047
        foreach ($keys as $key) {
7048
            if ($maxoverride[$key]) {
7049
                $this->get_instance()->{$key} = $maxoverride[$key];
7050
            }
7051
        }
7052
 
7053
        $formparams = array(
7054
            'instance' => $this->get_instance(),
7055
            'assign' => $this,
7056
            'userlist' => $userlist
7057
        );
7058
 
7059
        $mform = new mod_assign_extension_form(null, $formparams);
7060
 
7061
        if ($mform->is_cancelled()) {
7062
            return true;
7063
        }
7064
 
7065
        if ($formdata = $mform->get_data()) {
7066
            if (!empty($formdata->selectedusers)) {
7067
                $users = explode(',', $formdata->selectedusers);
7068
                $result = true;
7069
                foreach ($users as $userid) {
7070
                    $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
7071
                    $result = $this->save_user_extension($user->id, $formdata->extensionduedate) && $result;
7072
                }
7073
                return $result;
7074
            }
7075
            if (!empty($formdata->userid)) {
7076
                $user = $DB->get_record('user', array('id' => $formdata->userid), '*', MUST_EXIST);
7077
                return $this->save_user_extension($user->id, $formdata->extensionduedate);
7078
            }
7079
        }
7080
 
7081
        return false;
7082
    }
7083
 
7084
    /**
7085
     * Save quick grades.
7086
     *
7087
     * @return string The result of the save operation
7088
     */
1441 ariadna 7089
    protected function process_save_quick_grades() {
1 efrain 7090
        global $USER, $DB, $CFG;
7091
 
7092
        // Need grade permission.
7093
        require_capability('mod/assign:grade', $this->context);
7094
        require_sesskey();
7095
 
7096
        // Make sure advanced grading is disabled.
7097
        $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
7098
        $controller = $gradingmanager->get_active_controller();
7099
        if (!empty($controller)) {
7100
            $message = get_string('errorquickgradingvsadvancedgrading', 'assign');
7101
            $this->set_error_message($message);
7102
            return $message;
7103
        }
7104
 
7105
        $users = array();
7106
        // First check all the last modified values.
7107
        $currentgroup = groups_get_activity_group($this->get_course_module(), true);
7108
        $participants = $this->list_participants($currentgroup, true);
7109
 
7110
        // Gets a list of possible users and look for values based upon that.
7111
        foreach ($participants as $userid => $unused) {
7112
            $modified = optional_param('grademodified_' . $userid, -1, PARAM_INT);
7113
            $attemptnumber = optional_param('gradeattempt_' . $userid, -1, PARAM_INT);
7114
            // Gather the userid, updated grade and last modified value.
7115
            $record = new stdClass();
7116
            $record->userid = $userid;
7117
            if ($modified >= 0) {
7118
                $record->grade = unformat_float(optional_param('quickgrade_' . $record->userid, -1, PARAM_TEXT));
1441 ariadna 7119
                $record->workflowstate = optional_param('quickgrade_' . $record->userid.'_workflowstate', false, PARAM_ALPHA);
7120
                $record->allocatedmarker = optional_param('quickgrade_' . $record->userid.'_allocatedmarker', false, PARAM_INT);
1 efrain 7121
            } else {
7122
                // This user was not in the grading table.
7123
                continue;
7124
            }
7125
            $record->attemptnumber = $attemptnumber;
7126
            $record->lastmodified = $modified;
1441 ariadna 7127
            $record->gradinginfo = grade_get_grades($this->get_course()->id,
7128
                                                    'mod',
7129
                                                    'assign',
7130
                                                    $this->get_instance()->id,
7131
                                                    array($userid));
1 efrain 7132
            $users[$userid] = $record;
7133
        }
7134
 
7135
        if (empty($users)) {
7136
            $message = get_string('nousersselected', 'assign');
7137
            $this->set_error_message($message);
7138
            return $message;
7139
        }
7140
 
7141
        list($userids, $params) = $DB->get_in_or_equal(array_keys($users), SQL_PARAMS_NAMED);
7142
        $params['assignid1'] = $this->get_instance()->id;
7143
        $params['assignid2'] = $this->get_instance()->id;
7144
 
7145
        // Check them all for currency.
7146
        $grademaxattempt = 'SELECT s.userid, s.attemptnumber AS maxattempt
7147
                              FROM {assign_submission} s
7148
                             WHERE s.assignment = :assignid1 AND s.latest = 1';
7149
 
7150
        $sql = 'SELECT u.id AS userid, g.grade AS grade, g.timemodified AS lastmodified,
7151
                       uf.workflowstate, uf.allocatedmarker, gmx.maxattempt AS attemptnumber
7152
                  FROM {user} u
7153
             LEFT JOIN ( ' . $grademaxattempt . ' ) gmx ON u.id = gmx.userid
7154
             LEFT JOIN {assign_grades} g ON
7155
                       u.id = g.userid AND
7156
                       g.assignment = :assignid2 AND
7157
                       g.attemptnumber = gmx.maxattempt
7158
             LEFT JOIN {assign_user_flags} uf ON uf.assignment = g.assignment AND uf.userid = g.userid
7159
                 WHERE u.id ' . $userids;
7160
        $currentgrades = $DB->get_recordset_sql($sql, $params);
7161
 
7162
        $modifiedusers = array();
7163
        foreach ($currentgrades as $current) {
7164
            $modified = $users[(int)$current->userid];
7165
            $grade = $this->get_user_grade($modified->userid, false);
7166
            // Check to see if the grade column was even visible.
7167
            $gradecolpresent = optional_param('quickgrade_' . $modified->userid, false, PARAM_INT) !== false;
7168
 
7169
            // Check to see if the outcomes were modified.
7170
            if ($CFG->enableoutcomes) {
7171
                foreach ($modified->gradinginfo->outcomes as $outcomeid => $outcome) {
7172
                    $oldoutcome = $outcome->grades[$modified->userid]->grade;
7173
                    $paramname = 'outcome_' . $outcomeid . '_' . $modified->userid;
7174
                    $newoutcome = optional_param($paramname, -1, PARAM_FLOAT);
7175
                    // Check to see if the outcome column was even visible.
7176
                    $outcomecolpresent = optional_param($paramname, false, PARAM_FLOAT) !== false;
7177
                    if ($outcomecolpresent && ($oldoutcome != $newoutcome)) {
7178
                        // Can't check modified time for outcomes because it is not reported.
7179
                        $modifiedusers[$modified->userid] = $modified;
7180
                        continue;
7181
                    }
7182
                }
7183
            }
7184
 
7185
            // Let plugins participate.
7186
            foreach ($this->feedbackplugins as $plugin) {
7187
                if ($plugin->is_visible() && $plugin->is_enabled() && $plugin->supports_quickgrading()) {
7188
                    // The plugins must handle is_quickgrading_modified correctly - ie
7189
                    // handle hidden columns.
7190
                    if ($plugin->is_quickgrading_modified($modified->userid, $grade)) {
7191
                        if ((int)$current->lastmodified > (int)$modified->lastmodified) {
7192
                            $message = get_string('errorrecordmodified', 'assign');
7193
                            $this->set_error_message($message);
7194
                            return $message;
7195
                        } else {
7196
                            $modifiedusers[$modified->userid] = $modified;
7197
                            continue;
7198
                        }
7199
                    }
7200
                }
7201
            }
7202
 
7203
            if (($current->grade < 0 || $current->grade === null) &&
1441 ariadna 7204
                ($modified->grade < 0 || $modified->grade === null)) {
1 efrain 7205
                // Different ways to indicate no grade.
7206
                $modified->grade = $current->grade; // Keep existing grade.
7207
            }
7208
            // Treat 0 and null as different values.
7209
            if ($current->grade !== null) {
7210
                $current->grade = floatval($current->grade);
7211
            }
7212
            $gradechanged = $gradecolpresent && grade_floats_different($current->grade, $modified->grade);
7213
            $markingallocationchanged = $this->get_instance()->markingworkflow &&
1441 ariadna 7214
                                        $this->get_instance()->markingallocation &&
7215
                                            ($modified->allocatedmarker !== false) &&
7216
                                            ($current->allocatedmarker != $modified->allocatedmarker);
1 efrain 7217
            $workflowstatechanged = $this->get_instance()->markingworkflow &&
1441 ariadna 7218
                                            ($modified->workflowstate !== false) &&
7219
                                            ($current->workflowstate != $modified->workflowstate);
1 efrain 7220
            if ($gradechanged || $markingallocationchanged || $workflowstatechanged) {
7221
                // Grade changed.
7222
                if ($this->grading_disabled($modified->userid)) {
7223
                    continue;
7224
                }
7225
                $badmodified = (int)$current->lastmodified > (int)$modified->lastmodified;
7226
                $badattempt = (int)$current->attemptnumber != (int)$modified->attemptnumber;
7227
                if ($badmodified || $badattempt) {
7228
                    // Error - record has been modified since viewing the page.
7229
                    $message = get_string('errorrecordmodified', 'assign');
7230
                    $this->set_error_message($message);
7231
                    return $message;
7232
                } else {
7233
                    $modifiedusers[$modified->userid] = $modified;
7234
                }
7235
            }
1441 ariadna 7236
 
1 efrain 7237
        }
7238
        $currentgrades->close();
7239
 
7240
        $adminconfig = $this->get_admin_config();
7241
        $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
7242
 
7243
        // Ok - ready to process the updates.
7244
        foreach ($modifiedusers as $userid => $modified) {
7245
            $grade = $this->get_user_grade($userid, true);
7246
            $flags = $this->get_user_flags($userid, true);
1441 ariadna 7247
            $grade->grade= grade_floatval(unformat_float($modified->grade));
7248
            $grade->grader= $USER->id;
1 efrain 7249
            $gradecolpresent = optional_param('quickgrade_' . $userid, false, PARAM_INT) !== false;
7250
 
7251
            // Save plugins data.
7252
            foreach ($this->feedbackplugins as $plugin) {
7253
                if ($plugin->is_visible() && $plugin->is_enabled() && $plugin->supports_quickgrading()) {
7254
                    $plugin->save_quickgrading_changes($userid, $grade);
7255
                    if (('assignfeedback_' . $plugin->get_type()) == $gradebookplugin) {
7256
                        // This is the feedback plugin chose to push comments to the gradebook.
7257
                        $grade->feedbacktext = $plugin->text_for_gradebook($grade);
7258
                        $grade->feedbackformat = $plugin->format_for_gradebook($grade);
7259
                        $grade->feedbackfiles = $plugin->files_for_gradebook($grade);
7260
                    }
7261
                }
7262
            }
7263
 
7264
            // These will be set to false if they are not present in the quickgrading
7265
            // form (e.g. column hidden).
7266
            $workflowstatemodified = ($modified->workflowstate !== false) &&
1441 ariadna 7267
                                        ($flags->workflowstate != $modified->workflowstate);
1 efrain 7268
 
7269
            $allocatedmarkermodified = ($modified->allocatedmarker !== false) &&
1441 ariadna 7270
                                        ($flags->allocatedmarker != $modified->allocatedmarker);
1 efrain 7271
 
7272
            if ($workflowstatemodified) {
7273
                $flags->workflowstate = $modified->workflowstate;
7274
            }
7275
            if ($allocatedmarkermodified) {
7276
                $flags->allocatedmarker = $modified->allocatedmarker;
7277
            }
7278
            if ($workflowstatemodified || $allocatedmarkermodified) {
7279
                if ($this->update_user_flags($flags) && $workflowstatemodified) {
7280
                    $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
7281
                    \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $flags->workflowstate)->trigger();
7282
                }
7283
            }
7284
            $this->update_grade($grade);
7285
 
7286
            // Allow teachers to skip sending notifications.
7287
            if (optional_param('sendstudentnotifications', true, PARAM_BOOL)) {
7288
                $this->notify_grade_modified($grade, true);
7289
            }
7290
 
7291
            // Save outcomes.
7292
            if ($CFG->enableoutcomes) {
7293
                $data = array();
7294
                foreach ($modified->gradinginfo->outcomes as $outcomeid => $outcome) {
7295
                    $oldoutcome = $outcome->grades[$modified->userid]->grade;
7296
                    $paramname = 'outcome_' . $outcomeid . '_' . $modified->userid;
7297
                    // This will be false if the input was not in the quickgrading
7298
                    // form (e.g. column hidden).
7299
                    $newoutcome = optional_param($paramname, false, PARAM_INT);
7300
                    if ($newoutcome !== false && ($oldoutcome != $newoutcome)) {
7301
                        $data[$outcomeid] = $newoutcome;
7302
                    }
7303
                }
7304
                if (count($data) > 0) {
1441 ariadna 7305
                    grade_update_outcomes('mod/assign',
7306
                                          $this->course->id,
7307
                                          'mod',
7308
                                          'assign',
7309
                                          $this->get_instance()->id,
7310
                                          $userid,
7311
                                          $data);
1 efrain 7312
                }
7313
            }
7314
        }
7315
 
7316
        return get_string('quickgradingchangessaved', 'assign');
7317
    }
7318
 
7319
    /**
7320
     * Reveal student identities to markers (and the gradebook).
7321
     *
7322
     * @return void
7323
     */
1441 ariadna 7324
    public function reveal_identities() {
1 efrain 7325
        global $DB;
7326
 
7327
        require_capability('mod/assign:revealidentities', $this->context);
7328
 
7329
        if ($this->get_instance()->revealidentities || empty($this->get_instance()->blindmarking)) {
7330
            return false;
7331
        }
7332
 
7333
        // Update the assignment record.
7334
        $update = new stdClass();
7335
        $update->id = $this->get_instance()->id;
7336
        $update->revealidentities = 1;
7337
        $DB->update_record('assign', $update);
7338
 
7339
        // Refresh the instance data.
7340
        $this->instance = null;
7341
 
7342
        // Release the grades to the gradebook.
7343
        // First create the column in the gradebook.
7344
        $this->update_gradebook(false, $this->get_course_module()->id);
7345
 
7346
        // Now release all grades.
7347
 
7348
        $adminconfig = $this->get_admin_config();
7349
        $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
7350
        $gradebookplugin = str_replace('assignfeedback_', '', $gradebookplugin);
1441 ariadna 7351
        $grades = $DB->get_records('assign_grades', array('assignment'=>$this->get_instance()->id));
1 efrain 7352
 
7353
        $plugin = $this->get_feedback_plugin_by_type($gradebookplugin);
7354
 
7355
        foreach ($grades as $grade) {
7356
            // Fetch any comments for this student.
7357
            if ($plugin && $plugin->is_enabled() && $plugin->is_visible()) {
7358
                $grade->feedbacktext = $plugin->text_for_gradebook($grade);
7359
                $grade->feedbackformat = $plugin->format_for_gradebook($grade);
7360
                $grade->feedbackfiles = $plugin->files_for_gradebook($grade);
7361
            }
7362
            $this->gradebook_item_update(null, $grade);
7363
        }
7364
 
7365
        \mod_assign\event\identities_revealed::create_from_assign($this)->trigger();
7366
    }
7367
 
7368
    /**
7369
     * Reveal student identities to markers (and the gradebook).
7370
     *
7371
     * @return void
7372
     */
1441 ariadna 7373
    protected function process_reveal_identities() {
1 efrain 7374
 
7375
        if (!confirm_sesskey()) {
7376
            return false;
7377
        }
7378
 
7379
        return $this->reveal_identities();
7380
    }
7381
 
7382
 
7383
    /**
7384
     * Save grading options.
7385
     *
1441 ariadna 7386
     * @deprecated since Moodle 4.5
7387
     * @todo Final deprecation in Moodle 6.0. See MDL-82876.
1 efrain 7388
     * @return void
7389
     */
1441 ariadna 7390
    #[\core\attribute\deprecated(
7391
        'null',
7392
        since: '4.5',
7393
        reason: 'It is no longer used.',
7394
        mdl: 'MDL-82681',
7395
    )]
7396
    protected function process_save_grading_options() {
7397
        \core\deprecation::emit_deprecation([self::class, __FUNCTION__]);
1 efrain 7398
    }
7399
 
7400
    /**
7401
     * Require a valid sess key and then call copy_previous_attempt.
7402
     *
7403
     * @param  array $notices Any error messages that should be shown
7404
     *                        to the user at the top of the edit submission form.
7405
     * @return bool
7406
     */
1441 ariadna 7407
    protected function process_copy_previous_attempt(&$notices) {
1 efrain 7408
        require_sesskey();
7409
 
7410
        return $this->copy_previous_attempt($notices);
7411
    }
7412
 
7413
    /**
7414
     * Copy the current assignment submission from the last submitted attempt.
7415
     *
7416
     * @param  array $notices Any error messages that should be shown
7417
     *                        to the user at the top of the edit submission form.
7418
     * @return bool
7419
     */
1441 ariadna 7420
    public function copy_previous_attempt(&$notices) {
1 efrain 7421
        global $USER, $CFG;
7422
 
7423
        require_capability('mod/assign:submit', $this->context);
7424
 
7425
        $instance = $this->get_instance();
7426
        if ($instance->teamsubmission) {
7427
            $submission = $this->get_group_submission($USER->id, 0, true);
7428
        } else {
7429
            $submission = $this->get_user_submission($USER->id, true);
7430
        }
7431
        if (!$submission || $submission->status != ASSIGN_SUBMISSION_STATUS_REOPENED) {
7432
            $notices[] = get_string('submissionnotcopiedinvalidstatus', 'assign');
7433
            return false;
7434
        }
7435
        $flags = $this->get_user_flags($USER->id, false);
7436
 
7437
        // Get the flags to check if it is locked.
7438
        if ($flags && $flags->locked) {
7439
            $notices[] = get_string('submissionslocked', 'assign');
7440
            return false;
7441
        }
7442
        if ($instance->submissiondrafts) {
7443
            $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
7444
        } else {
7445
            $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
7446
        }
7447
        $this->update_submission($submission, $USER->id, true, $instance->teamsubmission);
7448
 
7449
        // Find the previous submission.
7450
        if ($instance->teamsubmission) {
7451
            $previoussubmission = $this->get_group_submission($USER->id, 0, true, $submission->attemptnumber - 1);
7452
        } else {
7453
            $previoussubmission = $this->get_user_submission($USER->id, true, $submission->attemptnumber - 1);
7454
        }
7455
 
7456
        if (!$previoussubmission) {
7457
            // There was no previous submission so there is nothing else to do.
7458
            return true;
7459
        }
7460
 
7461
        $pluginerror = false;
7462
        foreach ($this->get_submission_plugins() as $plugin) {
7463
            if ($plugin->is_visible() && $plugin->is_enabled()) {
7464
                if (!$plugin->copy_submission($previoussubmission, $submission)) {
7465
                    $notices[] = $plugin->get_error();
7466
                    $pluginerror = true;
7467
                }
7468
            }
7469
        }
7470
        if ($pluginerror) {
7471
            return false;
7472
        }
7473
 
7474
        \mod_assign\event\submission_duplicated::create_from_submission($this, $submission)->trigger();
7475
 
7476
        $complete = COMPLETION_INCOMPLETE;
7477
        if ($submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
7478
            $complete = COMPLETION_COMPLETE;
7479
        }
7480
        $completion = new completion_info($this->get_course());
7481
        if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
1441 ariadna 7482
            $this->update_activity_completion_records($instance->teamsubmission,
7483
                                                      $instance->requireallteammemberssubmit,
7484
                                                      $submission,
7485
                                                      $USER->id,
7486
                                                      $complete,
7487
                                                      $completion);
1 efrain 7488
        }
7489
 
7490
        if (!$instance->submissiondrafts) {
7491
            // There is a case for not notifying the student about the submission copy,
7492
            // but it provides a record of the event and if they then cancel editing it
7493
            // is clear that the submission was copied.
7494
            $this->notify_student_submission_copied($submission);
7495
            $this->notify_graders($submission);
7496
 
7497
            // The same logic applies here - we could not notify teachers,
7498
            // but then they would wonder why there are submitted assignments
7499
            // and they haven't been notified.
7500
            \mod_assign\event\assessable_submitted::create_from_submission($this, $submission, true)->trigger();
7501
        }
7502
        return true;
7503
    }
7504
 
7505
    /**
7506
     * Determine if the current submission is empty or not.
7507
     *
7508
     * @param submission $submission the students submission record to check.
7509
     * @return bool
7510
     */
1441 ariadna 7511
    public function submission_empty($submission) {
1 efrain 7512
        $allempty = true;
7513
 
7514
        foreach ($this->submissionplugins as $plugin) {
7515
            if ($plugin->is_enabled() && $plugin->is_visible()) {
7516
                if (!$allempty || !$plugin->is_empty($submission)) {
7517
                    $allempty = false;
7518
                }
7519
            }
7520
        }
7521
        return $allempty;
7522
    }
7523
 
7524
    /**
7525
     * Determine if a new submission is empty or not
7526
     *
7527
     * @param stdClass $data Submission data
7528
     * @return bool
7529
     */
1441 ariadna 7530
    public function new_submission_empty($data) {
1 efrain 7531
        foreach ($this->submissionplugins as $plugin) {
1441 ariadna 7532
            if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->allow_submissions() &&
7533
                    !$plugin->submission_is_empty($data)) {
1 efrain 7534
                return false;
7535
            }
7536
        }
7537
        return true;
7538
    }
7539
 
7540
    /**
7541
     * Save assignment submission for the current user.
7542
     *
7543
     * @param  stdClass $data
7544
     * @param  array $notices Any error messages that should be shown
7545
     *                        to the user.
7546
     * @return bool
7547
     */
1441 ariadna 7548
    public function save_submission(stdClass $data, & $notices) {
1 efrain 7549
        global $CFG, $USER, $DB;
7550
 
7551
        $userid = $USER->id;
7552
        if (!empty($data->userid)) {
7553
            $userid = $data->userid;
7554
        }
7555
 
1441 ariadna 7556
        $user = clone($USER);
1 efrain 7557
        if ($userid == $USER->id) {
7558
            require_capability('mod/assign:submit', $this->context);
7559
        } else {
1441 ariadna 7560
            $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
1 efrain 7561
            if (!$this->can_edit_submission($userid, $USER->id)) {
7562
                throw new \moodle_exception('nopermission');
7563
            }
7564
        }
7565
        $instance = $this->get_instance();
7566
 
7567
        if ($instance->teamsubmission) {
7568
            $submission = $this->get_group_submission($userid, 0, true);
7569
        } else {
7570
            $submission = $this->get_user_submission($userid, true);
7571
        }
7572
 
7573
        if ($this->new_submission_empty($data)) {
7574
            $notices[] = get_string('submissionempty', 'mod_assign');
7575
            return false;
7576
        }
7577
 
7578
        // Check that no one has modified the submission since we started looking at it.
7579
        if (isset($data->lastmodified) && ($submission->timemodified > $data->lastmodified)) {
7580
            // Another user has submitted something. Notify the current user.
7581
            if ($submission->status !== ASSIGN_SUBMISSION_STATUS_NEW) {
7582
                $notices[] = $instance->teamsubmission ? get_string('submissionmodifiedgroup', 'mod_assign')
1441 ariadna 7583
                                                       : get_string('submissionmodified', 'mod_assign');
1 efrain 7584
                return false;
7585
            }
7586
        }
7587
 
7588
        if ($instance->submissiondrafts) {
7589
            $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
7590
        } else {
7591
            $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
7592
        }
7593
 
7594
        $flags = $this->get_user_flags($userid, false);
7595
 
7596
        // Get the flags to check if it is locked.
7597
        if ($flags && $flags->locked) {
7598
            throw new \moodle_exception('submissionslocked', 'assign');
7599
            return true;
7600
        }
7601
 
7602
        $pluginerror = false;
7603
        foreach ($this->submissionplugins as $plugin) {
7604
            if ($plugin->is_enabled() && $plugin->is_visible()) {
7605
                if (!$plugin->save($submission, $data)) {
7606
                    $notices[] = $plugin->get_error();
7607
                    $pluginerror = true;
7608
                }
7609
            }
7610
        }
7611
 
7612
        $allempty = $this->submission_empty($submission);
7613
        if ($pluginerror || $allempty) {
7614
            if ($allempty) {
7615
                $notices[] = get_string('submissionempty', 'mod_assign');
7616
            }
7617
            return false;
7618
        }
7619
 
7620
        $this->update_submission($submission, $userid, true, $instance->teamsubmission);
7621
        $users = [$userid];
7622
 
7623
        if ($instance->teamsubmission && !$instance->requireallteammemberssubmit) {
7624
            $team = $this->get_submission_group_members($submission->groupid, true);
7625
 
7626
            foreach ($team as $member) {
7627
                if ($member->id != $userid) {
1441 ariadna 7628
                    $membersubmission = clone($submission);
1 efrain 7629
                    $this->update_submission($membersubmission, $member->id, true, $instance->teamsubmission);
7630
                    $users[] = $member->id;
7631
                }
7632
            }
7633
        }
7634
 
7635
        $complete = COMPLETION_INCOMPLETE;
7636
        if ($submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
7637
            $complete = COMPLETION_COMPLETE;
7638
        }
7639
 
7640
        $completion = new completion_info($this->get_course());
7641
        if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
7642
            foreach ($users as $id) {
7643
                $completion->update_state($this->get_course_module(), $complete, $id);
7644
            }
7645
        }
7646
 
7647
        // Logging.
7648
        if (isset($data->submissionstatement) && ($userid == $USER->id)) {
7649
            \mod_assign\event\statement_accepted::create_from_submission($this, $submission)->trigger();
7650
        }
7651
 
7652
        if (!$instance->submissiondrafts) {
7653
            $this->notify_student_submission_receipt($submission);
7654
            $this->notify_graders($submission);
7655
            \mod_assign\event\assessable_submitted::create_from_submission($this, $submission, true)->trigger();
7656
        }
7657
        return true;
7658
    }
7659
 
7660
    /**
7661
     * Save assignment submission.
7662
     *
7663
     * @param  moodleform $mform
7664
     * @param  array $notices Any error messages that should be shown
7665
     *                        to the user at the top of the edit submission form.
7666
     * @return bool
7667
     */
1441 ariadna 7668
    protected function process_save_submission(&$mform, &$notices) {
1 efrain 7669
        global $CFG, $USER;
7670
 
7671
        // Include submission form.
7672
        require_once($CFG->dirroot . '/mod/assign/submission_form.php');
7673
 
7674
        $userid = optional_param('userid', $USER->id, PARAM_INT);
7675
        // Need submit permission to submit an assignment.
7676
        require_sesskey();
7677
        if (!$this->submissions_open($userid)) {
7678
            $notices[] = get_string('duedatereached', 'assign');
7679
            return false;
7680
        }
7681
        $instance = $this->get_instance();
7682
 
7683
        $data = new stdClass();
7684
        $data->userid = $userid;
7685
        $mform = new mod_assign_submission_form(null, array($this, $data));
7686
        if ($mform->is_cancelled()) {
7687
            return true;
7688
        }
7689
        if ($data = $mform->get_data()) {
7690
            return $this->save_submission($data, $notices);
7691
        }
7692
        return false;
7693
    }
7694
 
7695
 
7696
    /**
7697
     * Determine if this users grade can be edited.
7698
     *
7699
     * @param int $userid - The student userid
7700
     * @param bool $checkworkflow - whether to include a check for the workflow state.
7701
     * @param stdClass $gradinginfo - optional, allow gradinginfo to be passed for performance.
7702
     * @return bool $gradingdisabled
7703
     */
1441 ariadna 7704
    public function grading_disabled($userid, $checkworkflow = true, $gradinginfo = null) {
1 efrain 7705
        if ($checkworkflow && $this->get_instance()->markingworkflow) {
7706
            $grade = $this->get_user_grade($userid, false);
7707
            $validstates = $this->get_marking_workflow_states_for_current_user();
7708
            if (!empty($grade) && !empty($grade->workflowstate) && !array_key_exists($grade->workflowstate, $validstates)) {
7709
                return true;
7710
            }
7711
        }
7712
 
7713
        if (is_null($gradinginfo)) {
1441 ariadna 7714
            $gradinginfo = grade_get_grades($this->get_course()->id,
1 efrain 7715
                'mod',
7716
                'assign',
7717
                $this->get_instance()->id,
1441 ariadna 7718
                array($userid));
1 efrain 7719
        }
7720
 
7721
        if (!$gradinginfo) {
7722
            return false;
7723
        }
7724
 
7725
        if (!isset($gradinginfo->items[0]->grades[$userid])) {
7726
            return false;
7727
        }
7728
        $gradingdisabled = $gradinginfo->items[0]->grades[$userid]->locked ||
1441 ariadna 7729
                           $gradinginfo->items[0]->grades[$userid]->overridden;
1 efrain 7730
        return $gradingdisabled;
7731
    }
7732
 
7733
 
7734
    /**
7735
     * Get an instance of a grading form if advanced grading is enabled.
7736
     * This is specific to the assignment, marker and student.
7737
     *
7738
     * @param int $userid - The student userid
7739
     * @param stdClass|false $grade - The grade record
7740
     * @param bool $gradingdisabled
7741
     * @return mixed gradingform_instance|null $gradinginstance
7742
     */
1441 ariadna 7743
    protected function get_grading_instance($userid, $grade, $gradingdisabled) {
1 efrain 7744
        global $CFG, $USER;
7745
 
7746
        $grademenu = make_grades_menu($this->get_instance()->grade);
7747
        $allowgradedecimals = $this->get_instance()->grade > 0;
7748
 
7749
        $advancedgradingwarning = false;
7750
        $gradingmanager = get_grading_manager($this->context, 'mod_assign', 'submissions');
7751
        $gradinginstance = null;
7752
        if ($gradingmethod = $gradingmanager->get_active_method()) {
7753
            $controller = $gradingmanager->get_controller($gradingmethod);
7754
            if ($controller->is_form_available()) {
7755
                $itemid = null;
7756
                if ($grade) {
7757
                    $itemid = $grade->id;
7758
                }
7759
                if ($gradingdisabled && $itemid) {
7760
                    $gradinginstance = $controller->get_current_instance($USER->id, $itemid);
7761
                } else if (!$gradingdisabled) {
7762
                    $instanceid = optional_param('advancedgradinginstanceid', 0, PARAM_INT);
1441 ariadna 7763
                    $gradinginstance = $controller->get_or_create_instance($instanceid,
7764
                                                                           $USER->id,
7765
                                                                           $itemid);
1 efrain 7766
                }
7767
            } else {
7768
                $advancedgradingwarning = $controller->form_unavailable_notification();
7769
            }
7770
        }
7771
        if ($gradinginstance) {
7772
            $gradinginstance->get_controller()->set_grade_range($grademenu, $allowgradedecimals);
7773
        }
7774
        return $gradinginstance;
7775
    }
7776
 
7777
    /**
7778
     * Add elements to grade form.
7779
     *
7780
     * @param MoodleQuickForm $mform
7781
     * @param stdClass $data
7782
     * @param array $params
7783
     * @return void
7784
     */
1441 ariadna 7785
    public function add_grade_form_elements(MoodleQuickForm $mform, stdClass $data, $params) {
7786
        global $USER, $CFG, $SESSION, $PAGE;
1 efrain 7787
        $settings = $this->get_instance();
7788
 
7789
        $rownum = isset($params['rownum']) ? $params['rownum'] : 0;
7790
        $last = isset($params['last']) ? $params['last'] : true;
7791
        $useridlistid = isset($params['useridlistid']) ? $params['useridlistid'] : 0;
7792
        $userid = isset($params['userid']) ? $params['userid'] : 0;
7793
        $attemptnumber = isset($params['attemptnumber']) ? $params['attemptnumber'] : 0;
7794
        $gradingpanel = !empty($params['gradingpanel']);
7795
        $bothids = ($userid && $useridlistid);
7796
 
7797
        if (!$userid || $bothids) {
7798
            $useridlist = $this->get_grading_userid_list(true, $useridlistid);
7799
        } else {
7800
            $useridlist = array($userid);
7801
            $rownum = 0;
7802
            $useridlistid = '';
7803
        }
7804
 
7805
        $userid = $useridlist[$rownum];
7806
        // We need to create a grade record matching this attempt number
7807
        // or the feedback plugin will have no way to know what is the correct attempt.
7808
        $grade = $this->get_user_grade($userid, true, $attemptnumber);
7809
 
7810
        $submission = null;
7811
        if ($this->get_instance()->teamsubmission) {
7812
            $submission = $this->get_group_submission($userid, 0, false, $attemptnumber);
7813
        } else {
7814
            $submission = $this->get_user_submission($userid, false, $attemptnumber);
7815
        }
7816
 
7817
        // Add advanced grading.
7818
        $gradingdisabled = $this->grading_disabled($userid);
7819
        $gradinginstance = $this->get_grading_instance($userid, $grade, $gradingdisabled);
7820
 
7821
        $mform->addElement('header', 'gradeheader', get_string('gradenoun'));
7822
        if ($gradinginstance) {
1441 ariadna 7823
            $gradingelement = $mform->addElement('grading',
7824
                                                 'advancedgrading',
7825
                                                 get_string('gradenoun') . ':',
7826
                                                 array('gradinginstance' => $gradinginstance));
1 efrain 7827
            if ($gradingdisabled) {
7828
                $gradingelement->freeze();
7829
            } else {
7830
                $mform->addElement('hidden', 'advancedgradinginstanceid', $gradinginstance->get_id());
7831
                $mform->setType('advancedgradinginstanceid', PARAM_INT);
7832
            }
7833
        } else {
7834
            // Use simple direct grading.
7835
            if ($this->get_instance()->grade > 0) {
7836
                $name = get_string('gradeoutof', 'assign', $this->get_instance()->grade);
7837
                if (!$gradingdisabled) {
7838
                    $gradingelement = $mform->addElement('text', 'grade', $name);
7839
                    $mform->addHelpButton('grade', 'gradeoutofhelp', 'assign');
7840
                    $mform->setType('grade', PARAM_RAW);
7841
                } else {
7842
                    $strgradelocked = get_string('gradelocked', 'assign');
7843
                    $mform->addElement('static', 'gradedisabled', $name, $strgradelocked);
7844
                    $mform->addHelpButton('gradedisabled', 'gradeoutofhelp', 'assign');
7845
                }
7846
            } else {
7847
                $grademenu = array(-1 => get_string("nograde")) + make_grades_menu($this->get_instance()->grade);
7848
                if (count($grademenu) > 1) {
7849
                    $gradingelement = $mform->addElement('select', 'grade', get_string('gradenoun') . ':', $grademenu);
7850
 
7851
                    // The grade is already formatted with format_float so it needs to be converted back to an integer.
7852
                    if (!empty($data->grade)) {
7853
                        $data->grade = (int)unformat_float($data->grade);
7854
                    }
7855
                    $mform->setType('grade', PARAM_INT);
7856
                    if ($gradingdisabled) {
7857
                        $gradingelement->freeze();
7858
                    }
7859
                }
7860
            }
7861
        }
7862
 
1441 ariadna 7863
        $gradinginfo = grade_get_grades($this->get_course()->id,
7864
                                        'mod',
7865
                                        'assign',
7866
                                        $this->get_instance()->id,
7867
                                        $userid);
1 efrain 7868
        if (!empty($CFG->enableoutcomes)) {
7869
            foreach ($gradinginfo->outcomes as $index => $outcome) {
7870
                $options = make_grades_menu(-$outcome->scaleid);
7871
                $options[0] = get_string('nooutcome', 'grades');
7872
                if ($outcome->grades[$userid]->locked) {
1441 ariadna 7873
                    $mform->addElement('static',
7874
                                       'outcome_' . $index . '[' . $userid . ']',
7875
                                       $outcome->name . ':',
7876
                                       $options[$outcome->grades[$userid]->grade]);
1 efrain 7877
                } else {
1441 ariadna 7878
                    $attributes = array('id' => 'menuoutcome_' . $index );
7879
                    $mform->addElement('select',
7880
                                       'outcome_' . $index . '[' . $userid . ']',
7881
                                       $outcome->name.':',
7882
                                       $options,
7883
                                       $attributes);
1 efrain 7884
                    $mform->setType('outcome_' . $index . '[' . $userid . ']', PARAM_INT);
1441 ariadna 7885
                    $mform->setDefault('outcome_' . $index . '[' . $userid . ']',
7886
                                       $outcome->grades[$userid]->grade);
1 efrain 7887
                }
7888
            }
7889
        }
7890
 
7891
        $capabilitylist = array('gradereport/grader:view', 'moodle/grade:viewall');
7892
        $usergrade = get_string('notgraded', 'assign');
7893
        if (has_all_capabilities($capabilitylist, $this->get_course_context())) {
1441 ariadna 7894
            $urlparams = array('id'=>$this->get_course()->id);
1 efrain 7895
            $url = new moodle_url('/grade/report/grader/index.php', $urlparams);
7896
            if (isset($gradinginfo->items[0]->grades[$userid]->grade)) {
7897
                $usergrade = $gradinginfo->items[0]->grades[$userid]->str_grade;
7898
            }
7899
            $gradestring = $this->get_renderer()->action_link($url, $usergrade);
7900
        } else {
1441 ariadna 7901
            if (isset($gradinginfo->items[0]->grades[$userid]) &&
7902
                    !$gradinginfo->items[0]->grades[$userid]->hidden) {
1 efrain 7903
                $usergrade = $gradinginfo->items[0]->grades[$userid]->str_grade;
7904
            }
7905
            $gradestring = $usergrade;
7906
        }
7907
 
1441 ariadna 7908
        // Penalty indicator.
7909
        $userassigngrade = $gradinginfo->items[0]->grades[$userid];
7910
        if (isset($userassigngrade->grade)) {
7911
            $gradegrade = new \grade_grade();
7912
            $gradegrade->deductedmark = $userassigngrade->deductedmark;
7913
            $gradegrade->overridden = $userassigngrade->overridden;
7914
            $penaltyindicator = \core_grades\penalty_manager::show_penalty_indicator($gradegrade);
7915
            $gradestring = $penaltyindicator . $gradestring;
7916
        }
7917
 
1 efrain 7918
        if ($this->get_instance()->markingworkflow) {
7919
            $states = $this->get_marking_workflow_states_for_current_user();
7920
            $options = array('' => get_string('markingworkflowstatenotmarked', 'assign')) + $states;
7921
            $select = $mform->addElement('select', 'workflowstate', get_string('markingworkflowstate', 'assign'), $options);
7922
            $mform->addHelpButton('workflowstate', 'markingworkflowstate', 'assign');
7923
            if (!empty($data->workflowstate) && !array_key_exists($data->workflowstate, $states)) {
7924
                // In a workflow state that user should not be able to change, so freeze workflow selector.
7925
                // Have to add the state so it shows in the frozen selector.
7926
                $allworkflowstates = $this->get_all_marking_workflow_states();
7927
                $select->addOption($allworkflowstates[$data->workflowstate], $data->workflowstate);
7928
                $mform->freeze('workflowstate');
7929
            }
7930
            $gradingstatus = $this->get_grading_status($userid);
7931
            if ($gradingstatus != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
7932
                if ($grade->grade && $grade->grade != -1) {
7933
                    if ($settings->grade > 0) {
7934
                        $assigngradestring = format_float($grade->grade, $this->get_grade_item()->get_decimals());
7935
                    } else {
7936
                        $assigngradestring = make_grades_menu($settings->grade)[grade_floatval($grade->grade)];
7937
                    }
7938
                    $assigngradestring = html_writer::span($assigngradestring, 'currentgrade');
7939
                    $label = get_string('currentassigngrade', 'assign');
7940
                    $mform->addElement('static', 'currentassigngrade', $label, $assigngradestring);
7941
                }
7942
            }
7943
        }
7944
 
1441 ariadna 7945
        if ($this->get_instance()->markingworkflow &&
1 efrain 7946
            $this->get_instance()->markingallocation &&
1441 ariadna 7947
            has_capability('mod/assign:manageallocations', $this->context)) {
1 efrain 7948
 
7949
            list($sort, $params) = users_order_by_sql('u');
7950
            // Only enrolled users could be assigned as potential markers.
7951
            $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
7952
            $markerlist = array('' =>  get_string('choosemarker', 'assign'));
7953
            $viewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
7954
            foreach ($markers as $marker) {
7955
                $markerlist[$marker->id] = fullname($marker, $viewfullnames);
7956
            }
7957
            $mform->addElement('select', 'allocatedmarker', get_string('allocatedmarker', 'assign'), $markerlist);
7958
            $mform->addHelpButton('allocatedmarker', 'allocatedmarker', 'assign');
7959
            $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW);
7960
            $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW);
7961
            $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE);
7962
            $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_RELEASED);
7963
        }
7964
 
7965
        $gradestring = '<span class="currentgrade">' . $gradestring . '</span>';
7966
        $mform->addElement('static', 'currentgrade', get_string('currentgrade', 'assign'), $gradestring);
7967
 
7968
        if (count($useridlist) > 1) {
1441 ariadna 7969
            $strparams = array('current'=>$rownum+1, 'total'=>count($useridlist));
1 efrain 7970
            $name = get_string('outof', 'assign', $strparams);
7971
            $mform->addElement('static', 'gradingstudent', get_string('gradingstudent', 'assign'), $name);
7972
        }
7973
 
7974
        // Let feedback plugins add elements to the grading form.
7975
        $this->add_plugin_grade_elements($grade, $mform, $data, $userid);
7976
 
7977
        // Hidden params.
7978
        $mform->addElement('hidden', 'id', $this->get_course_module()->id);
7979
        $mform->setType('id', PARAM_INT);
7980
        $mform->addElement('hidden', 'rownum', $rownum);
7981
        $mform->setType('rownum', PARAM_INT);
7982
        $mform->setConstant('rownum', $rownum);
7983
        $mform->addElement('hidden', 'useridlistid', $useridlistid);
7984
        $mform->setType('useridlistid', PARAM_ALPHANUM);
7985
        $mform->addElement('hidden', 'attemptnumber', $attemptnumber);
7986
        $mform->setType('attemptnumber', PARAM_INT);
7987
        $mform->addElement('hidden', 'ajax', optional_param('ajax', 0, PARAM_INT));
7988
        $mform->setType('ajax', PARAM_INT);
7989
        $mform->addElement('hidden', 'userid', optional_param('userid', 0, PARAM_INT));
7990
        $mform->setType('userid', PARAM_INT);
7991
 
7992
        if ($this->get_instance()->teamsubmission) {
7993
            $mform->addElement('header', 'groupsubmissionsettings', get_string('groupsubmissionsettings', 'assign'));
7994
            $mform->addElement('selectyesno', 'applytoall', get_string('applytoteam', 'assign'));
7995
            $mform->setDefault('applytoall', 1);
7996
        }
7997
 
7998
        // Do not show if we are editing a previous attempt.
1441 ariadna 7999
        if (($attemptnumber == -1 || ($attemptnumber + 1) == count($this->get_all_submissions($userid))) &&
8000
                ($this->get_instance()->maxattempts > 1 || $this->get_instance()->maxattempts == ASSIGN_UNLIMITED_ATTEMPTS)) {
1 efrain 8001
            $mform->addElement('header', 'attemptsettings', get_string('attemptsettings', 'assign'));
8002
            $attemptreopenmethod = get_string('attemptreopenmethod_' . $this->get_instance()->attemptreopenmethod, 'assign');
8003
            $mform->addElement('static', 'attemptreopenmethod', get_string('attemptreopenmethod', 'assign'), $attemptreopenmethod);
8004
 
8005
            $attemptnumber = 0;
8006
            if ($submission) {
8007
                $attemptnumber = $submission->attemptnumber;
8008
            }
8009
            $maxattempts = $this->get_instance()->maxattempts;
8010
            if ($maxattempts == ASSIGN_UNLIMITED_ATTEMPTS) {
8011
                $maxattempts = get_string('unlimitedattempts', 'assign');
8012
            }
8013
            $mform->addelement('static', 'maxattemptslabel', get_string('maxattempts', 'assign'), $maxattempts);
8014
            $mform->addelement('static', 'attemptnumberlabel', get_string('attemptnumber', 'assign'), $attemptnumber + 1);
8015
 
8016
            $ismanual = $this->get_instance()->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL;
8017
            $issubmission = !empty($submission);
8018
            $isunlimited = $this->get_instance()->maxattempts == ASSIGN_UNLIMITED_ATTEMPTS;
1441 ariadna 8019
            $islessthanmaxattempts = $issubmission && ($submission->attemptnumber < ($this->get_instance()->maxattempts-1));
1 efrain 8020
 
8021
            if ($ismanual && (!$issubmission || $isunlimited || $islessthanmaxattempts)) {
8022
                $mform->addElement('selectyesno', 'addattempt', get_string('addattempt', 'assign'));
8023
                $mform->setDefault('addattempt', 0);
8024
            }
8025
        }
8026
        if (!$gradingpanel) {
8027
            $mform->addElement('selectyesno', 'sendstudentnotifications', get_string('sendstudentnotifications', 'assign'));
8028
        } else {
8029
            $mform->addElement('hidden', 'sendstudentnotifications', get_string('sendstudentnotifications', 'assign'));
8030
            $mform->setType('sendstudentnotifications', PARAM_BOOL);
8031
        }
8032
        // Get assignment visibility information for student.
8033
        $modinfo = get_fast_modinfo($settings->course, $userid);
8034
        $cm = $modinfo->get_cm($this->get_course_module()->id);
8035
 
8036
        // Don't allow notification to be sent if the student can't access the assignment,
8037
        // or until in "Released" state if using marking workflow.
8038
        if (!$cm->uservisible) {
8039
            $mform->setDefault('sendstudentnotifications', 0);
8040
            $mform->freeze('sendstudentnotifications');
8041
        } else if ($this->get_instance()->markingworkflow) {
8042
            $mform->setDefault('sendstudentnotifications', 0);
8043
            if (!$gradingpanel) {
8044
                $mform->disabledIf('sendstudentnotifications', 'workflowstate', 'neq', ASSIGN_MARKING_WORKFLOW_STATE_RELEASED);
8045
            }
8046
        } else {
8047
            $mform->setDefault('sendstudentnotifications', $this->get_instance()->sendstudentnotifications);
8048
        }
8049
 
8050
        $mform->addElement('hidden', 'action', 'submitgrade');
8051
        $mform->setType('action', PARAM_ALPHA);
8052
 
8053
        if (!$gradingpanel) {
8054
 
8055
            $buttonarray = array();
8056
            $name = get_string('savechanges', 'assign');
8057
            $buttonarray[] = $mform->createElement('submit', 'savegrade', $name);
8058
            if (!$last) {
8059
                $name = get_string('savenext', 'assign');
8060
                $buttonarray[] = $mform->createElement('submit', 'saveandshownext', $name);
8061
            }
8062
            $buttonarray[] = $mform->createElement('cancel', 'cancelbutton', get_string('cancel'));
8063
            $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false);
8064
            $mform->closeHeaderBefore('buttonar');
8065
            $buttonarray = array();
8066
 
8067
            if ($rownum > 0) {
8068
                $name = get_string('previous', 'assign');
8069
                $buttonarray[] = $mform->createElement('submit', 'nosaveandprevious', $name);
8070
            }
8071
 
8072
            if (!$last) {
8073
                $name = get_string('nosavebutnext', 'assign');
8074
                $buttonarray[] = $mform->createElement('submit', 'nosaveandnext', $name);
8075
            }
8076
            if (!empty($buttonarray)) {
8077
                $mform->addGroup($buttonarray, 'navar', '', array(' '), false);
8078
            }
8079
        }
8080
        // The grading form does not work well with shortforms.
8081
        $mform->setDisableShortforms();
8082
    }
8083
 
8084
    /**
8085
     * Add elements in submission plugin form.
8086
     *
8087
     * @param mixed $submission stdClass|null
8088
     * @param MoodleQuickForm $mform
8089
     * @param stdClass $data
8090
     * @param int $userid The current userid (same as $USER->id)
8091
     * @return void
8092
     */
1441 ariadna 8093
    protected function add_plugin_submission_elements($submission,
8094
                                                    MoodleQuickForm $mform,
8095
                                                    stdClass $data,
8096
                                                    $userid) {
1 efrain 8097
        foreach ($this->submissionplugins as $plugin) {
8098
            if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->allow_submissions()) {
8099
                $plugin->get_form_elements_for_user($submission, $mform, $data, $userid);
8100
            }
8101
        }
8102
    }
8103
 
8104
    /**
8105
     * Check if feedback plugins installed are enabled.
8106
     *
8107
     * @return bool
8108
     */
1441 ariadna 8109
    public function is_any_feedback_plugin_enabled() {
1 efrain 8110
        if (!isset($this->cache['any_feedback_plugin_enabled'])) {
8111
            $this->cache['any_feedback_plugin_enabled'] = false;
8112
            foreach ($this->feedbackplugins as $plugin) {
8113
                if ($plugin->is_enabled() && $plugin->is_visible()) {
8114
                    $this->cache['any_feedback_plugin_enabled'] = true;
8115
                    break;
8116
                }
8117
            }
8118
        }
8119
 
8120
        return $this->cache['any_feedback_plugin_enabled'];
1441 ariadna 8121
 
1 efrain 8122
    }
8123
 
8124
    /**
8125
     * Check if submission plugins installed are enabled.
8126
     *
8127
     * @return bool
8128
     */
1441 ariadna 8129
    public function is_any_submission_plugin_enabled() {
1 efrain 8130
        if (!isset($this->cache['any_submission_plugin_enabled'])) {
8131
            $this->cache['any_submission_plugin_enabled'] = false;
8132
            foreach ($this->submissionplugins as $plugin) {
8133
                if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->allow_submissions()) {
8134
                    $this->cache['any_submission_plugin_enabled'] = true;
8135
                    break;
8136
                }
8137
            }
8138
        }
8139
 
8140
        return $this->cache['any_submission_plugin_enabled'];
1441 ariadna 8141
 
1 efrain 8142
    }
8143
 
8144
    /**
8145
     * Add elements to submission form.
8146
     * @param MoodleQuickForm $mform
8147
     * @param stdClass $data
8148
     * @return void
8149
     */
1441 ariadna 8150
    public function add_submission_form_elements(MoodleQuickForm $mform, stdClass $data) {
1 efrain 8151
        global $USER;
8152
 
8153
        $userid = $data->userid;
8154
        // Team submissions.
8155
        if ($this->get_instance()->teamsubmission) {
8156
            $submission = $this->get_group_submission($userid, 0, false);
8157
        } else {
8158
            $submission = $this->get_user_submission($userid, false);
8159
        }
8160
 
8161
        // Submission statement.
8162
        $adminconfig = $this->get_admin_config();
8163
        $requiresubmissionstatement = $this->get_instance()->requiresubmissionstatement;
8164
 
8165
        $draftsenabled = $this->get_instance()->submissiondrafts;
8166
        $submissionstatement = '';
8167
 
8168
        if ($requiresubmissionstatement) {
8169
            $submissionstatement = $this->get_submissionstatement($adminconfig, $this->get_instance(), $this->get_context());
8170
        }
8171
 
8172
        // If we get back an empty submission statement, we have to set $requiredsubmisisonstatement to false to prevent
8173
        // that the submission statement checkbox will be displayed.
8174
        if (empty($submissionstatement)) {
8175
            $requiresubmissionstatement = false;
8176
        }
8177
 
8178
        $mform->addElement('header', 'submission header', get_string('addsubmission', 'mod_assign'));
8179
 
8180
        // Only show submission statement if we are editing our own submission.
8181
        if ($requiresubmissionstatement && !$draftsenabled && $userid == $USER->id) {
8182
            $mform->addElement('checkbox', 'submissionstatement', '', $submissionstatement);
1441 ariadna 8183
            $mform->addRule('submissionstatement', get_string('submissionstatementrequired', 'mod_assign'),
8184
                'required', null, 'client');
1 efrain 8185
        }
8186
 
8187
        $this->add_plugin_submission_elements($submission, $mform, $data, $userid);
8188
 
8189
        // Hidden params.
8190
        $mform->addElement('hidden', 'id', $this->get_course_module()->id);
8191
        $mform->setType('id', PARAM_INT);
8192
 
8193
        $mform->addElement('hidden', 'userid', $userid);
8194
        $mform->setType('userid', PARAM_INT);
8195
 
8196
        $mform->addElement('hidden', 'action', 'savesubmission');
8197
        $mform->setType('action', PARAM_ALPHA);
8198
    }
8199
 
8200
    /**
8201
     * Remove any data from the current submission.
8202
     *
8203
     * @param int $userid
8204
     * @return boolean
8205
     * @throws coding_exception
8206
     */
1441 ariadna 8207
    public function remove_submission($userid) {
1 efrain 8208
        global $USER;
8209
 
8210
        if (!$this->can_edit_submission($userid, $USER->id)) {
8211
            $user = core_user::get_user($userid);
8212
            $message = get_string('usersubmissioncannotberemoved', 'assign', fullname($user));
8213
            $this->set_error_message($message);
8214
            return false;
8215
        }
8216
 
8217
        if ($this->get_instance()->teamsubmission) {
8218
            $submission = $this->get_group_submission($userid, 0, false);
8219
        } else {
8220
            $submission = $this->get_user_submission($userid, false);
8221
        }
8222
 
8223
        if (!$submission) {
8224
            return false;
8225
        }
8226
        $submission->status = $submission->attemptnumber ? ASSIGN_SUBMISSION_STATUS_REOPENED : ASSIGN_SUBMISSION_STATUS_NEW;
8227
        $this->update_submission($submission, $userid, false, $this->get_instance()->teamsubmission);
8228
 
8229
        // Tell each submission plugin we were saved with no data.
8230
        $plugins = $this->get_submission_plugins();
8231
        foreach ($plugins as $plugin) {
8232
            if ($plugin->is_enabled() && $plugin->is_visible()) {
8233
                $plugin->remove($submission);
8234
            }
8235
        }
8236
 
8237
        $completion = new completion_info($this->get_course());
1441 ariadna 8238
        if ($completion->is_enabled($this->get_course_module()) &&
8239
                $this->get_instance()->completionsubmit) {
1 efrain 8240
            $completion->update_state($this->get_course_module(), COMPLETION_INCOMPLETE, $userid);
8241
        }
8242
 
8243
        submission_removed::create_from_submission($this, $submission)->trigger();
8244
        submission_status_updated::create_from_submission($this, $submission)->trigger();
8245
        return true;
8246
    }
8247
 
8248
    /**
8249
     * Revert to draft.
8250
     *
8251
     * @param int $userid
8252
     * @return boolean
8253
     */
1441 ariadna 8254
    public function revert_to_draft($userid) {
1 efrain 8255
        global $DB, $USER;
8256
 
8257
        // Need grade permission.
8258
        require_capability('mod/assign:grade', $this->context);
8259
 
8260
        if ($this->get_instance()->teamsubmission) {
8261
            $submission = $this->get_group_submission($userid, 0, false);
8262
        } else {
8263
            $submission = $this->get_user_submission($userid, false);
8264
        }
8265
 
8266
        if (!$submission) {
8267
            return false;
8268
        }
8269
        $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
8270
        $this->update_submission($submission, $userid, false, $this->get_instance()->teamsubmission);
8271
 
8272
        // Give each submission plugin a chance to process the reverting to draft.
8273
        $plugins = $this->get_submission_plugins();
8274
        foreach ($plugins as $plugin) {
8275
            if ($plugin->is_enabled() && $plugin->is_visible()) {
8276
                $plugin->revert_to_draft($submission);
8277
            }
8278
        }
8279
        // Update the modified time on the grade (grader modified).
8280
        $grade = $this->get_user_grade($userid, true);
8281
        $grade->grader = $USER->id;
8282
        $this->update_grade($grade);
8283
 
8284
        $completion = new completion_info($this->get_course());
1441 ariadna 8285
        if ($completion->is_enabled($this->get_course_module()) &&
8286
                $this->get_instance()->completionsubmit) {
1 efrain 8287
            $completion->update_state($this->get_course_module(), COMPLETION_INCOMPLETE, $userid);
8288
        }
8289
        \mod_assign\event\submission_status_updated::create_from_submission($this, $submission)->trigger();
8290
        return true;
8291
    }
8292
 
8293
    /**
8294
     * Remove the current submission.
8295
     *
8296
     * @param int $userid
8297
     * @return boolean
8298
     */
1441 ariadna 8299
    protected function process_remove_submission($userid = 0) {
1 efrain 8300
        require_sesskey();
8301
 
8302
        if (!$userid) {
8303
            $userid = required_param('userid', PARAM_INT);
8304
        }
8305
 
8306
        return $this->remove_submission($userid);
8307
    }
8308
 
8309
    /**
8310
     * Revert to draft.
8311
     * Uses url parameter userid if userid not supplied as a parameter.
8312
     *
8313
     * @param int $userid
8314
     * @return boolean
8315
     */
1441 ariadna 8316
    protected function process_revert_to_draft($userid = 0) {
1 efrain 8317
        require_sesskey();
8318
 
8319
        if (!$userid) {
8320
            $userid = required_param('userid', PARAM_INT);
8321
        }
8322
 
8323
        return $this->revert_to_draft($userid);
8324
    }
8325
 
8326
    /**
8327
     * Prevent student updates to this submission
8328
     *
8329
     * @param int $userid
8330
     * @return bool
8331
     */
1441 ariadna 8332
    public function lock_submission($userid) {
1 efrain 8333
        global $USER, $DB;
8334
        // Need grade permission.
8335
        require_capability('mod/assign:grade', $this->context);
8336
 
8337
        // Give each submission plugin a chance to process the locking.
8338
        $plugins = $this->get_submission_plugins();
8339
        $submission = $this->get_user_submission($userid, false);
8340
 
8341
        $flags = $this->get_user_flags($userid, true);
8342
        $flags->locked = 1;
8343
        $this->update_user_flags($flags);
8344
 
8345
        foreach ($plugins as $plugin) {
8346
            if ($plugin->is_enabled() && $plugin->is_visible()) {
8347
                $plugin->lock($submission, $flags);
8348
            }
8349
        }
8350
 
8351
        $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
8352
        \mod_assign\event\submission_locked::create_from_user($this, $user)->trigger();
8353
        return true;
8354
    }
8355
 
8356
 
8357
    /**
8358
     * Set the workflow state for multiple users
8359
     *
8360
     * @return void
8361
     */
1441 ariadna 8362
    protected function process_set_batch_marking_workflow_state() {
1 efrain 8363
        global $CFG, $DB;
8364
 
8365
        // Include batch marking workflow form.
8366
        require_once($CFG->dirroot . '/mod/assign/batchsetmarkingworkflowstateform.php');
8367
 
8368
        $formparams = array(
8369
            'userscount' => 0,  // This form is never re-displayed, so we don't need to
8370
            'usershtml' => '',  // initialise these parameters with real information.
8371
            'markingworkflowstates' => $this->get_marking_workflow_states_for_current_user()
8372
        );
8373
 
8374
        $mform = new mod_assign_batch_set_marking_workflow_state_form(null, $formparams);
8375
 
8376
        if ($mform->is_cancelled()) {
8377
            return true;
8378
        }
8379
 
8380
        if ($formdata = $mform->get_data()) {
8381
            $useridlist = explode(',', $formdata->selectedusers);
8382
            $state = $formdata->markingworkflowstate;
8383
 
8384
            foreach ($useridlist as $userid) {
8385
                $flags = $this->get_user_flags($userid, true);
8386
 
8387
                $flags->workflowstate = $state;
8388
 
8389
                // Clear the mailed flag if notification is requested, the student hasn't been
8390
                // notified previously, the student can access the assignment, and the state
8391
                // is "Released".
8392
                $modinfo = get_fast_modinfo($this->course, $userid);
8393
                $cm = $modinfo->get_cm($this->get_course_module()->id);
1441 ariadna 8394
                if ($formdata->sendstudentnotifications && $cm->uservisible &&
8395
                        $state == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
1 efrain 8396
                    $flags->mailed = 0;
8397
                }
8398
 
8399
                $gradingdisabled = $this->grading_disabled($userid);
8400
 
8401
                // Will not apply update if user does not have permission to assign this workflow state.
8402
                if (!$gradingdisabled && $this->update_user_flags($flags)) {
8403
                    // Update Gradebook.
8404
                    $grade = $this->get_user_grade($userid, true);
8405
                    // Fetch any feedback for this student.
8406
                    $gradebookplugin = $this->get_admin_config()->feedback_plugin_for_gradebook;
8407
                    $gradebookplugin = str_replace('assignfeedback_', '', $gradebookplugin);
8408
                    $plugin = $this->get_feedback_plugin_by_type($gradebookplugin);
8409
                    if ($plugin && $plugin->is_enabled() && $plugin->is_visible()) {
8410
                        $grade->feedbacktext = $plugin->text_for_gradebook($grade);
8411
                        $grade->feedbackformat = $plugin->format_for_gradebook($grade);
8412
                        $grade->feedbackfiles = $plugin->files_for_gradebook($grade);
8413
                    }
8414
                    $this->update_grade($grade);
8415
                    $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
8416
                    \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $state)->trigger();
8417
                }
8418
            }
8419
        }
8420
    }
8421
 
8422
    /**
8423
     * Set the marking allocation for multiple users
8424
     *
8425
     * @return void
8426
     */
1441 ariadna 8427
    protected function process_set_batch_marking_allocation() {
1 efrain 8428
        global $CFG, $DB;
8429
 
8430
        // Include batch marking allocation form.
8431
        require_once($CFG->dirroot . '/mod/assign/batchsetallocatedmarkerform.php');
8432
 
8433
        $formparams = array(
8434
            'userscount' => 0,  // This form is never re-displayed, so we don't need to
8435
            'usershtml' => ''   // initialise these parameters with real information.
8436
        );
8437
 
8438
        list($sort, $params) = users_order_by_sql('u');
8439
        // Only enrolled users could be assigned as potential markers.
8440
        $markers = get_enrolled_users($this->get_context(), 'mod/assign:grade', 0, 'u.*', $sort);
8441
        $markerlist = array();
8442
        foreach ($markers as $marker) {
8443
            $markerlist[$marker->id] = fullname($marker);
8444
        }
8445
 
8446
        $formparams['markers'] = $markerlist;
8447
 
8448
        $mform = new mod_assign_batch_set_allocatedmarker_form(null, $formparams);
8449
 
8450
        if ($mform->is_cancelled()) {
8451
            return true;
8452
        }
8453
 
8454
        if ($formdata = $mform->get_data()) {
8455
            $useridlist = explode(',', $formdata->selectedusers);
8456
            $marker = $DB->get_record('user', array('id' => $formdata->allocatedmarker), '*', MUST_EXIST);
8457
 
8458
            foreach ($useridlist as $userid) {
8459
                $flags = $this->get_user_flags($userid, true);
1441 ariadna 8460
                if ($flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW ||
1 efrain 8461
                    $flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW ||
8462
                    $flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE ||
1441 ariadna 8463
                    $flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
1 efrain 8464
 
8465
                    continue; // Allocated marker can only be changed in certain workflow states.
8466
                }
8467
 
8468
                $flags->allocatedmarker = $marker->id;
8469
 
8470
                if ($this->update_user_flags($flags)) {
8471
                    $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
8472
                    \mod_assign\event\marker_updated::create_from_marker($this, $user, $marker)->trigger();
8473
                }
8474
            }
8475
        }
8476
    }
8477
 
8478
 
8479
    /**
8480
     * Prevent student updates to this submission.
8481
     * Uses url parameter userid.
8482
     *
8483
     * @param int $userid
8484
     * @return void
8485
     */
1441 ariadna 8486
    protected function process_lock_submission($userid = 0) {
1 efrain 8487
 
8488
        require_sesskey();
8489
 
8490
        if (!$userid) {
8491
            $userid = required_param('userid', PARAM_INT);
8492
        }
8493
 
8494
        return $this->lock_submission($userid);
8495
    }
8496
 
8497
    /**
8498
     * Unlock the student submission.
8499
     *
8500
     * @param int $userid
8501
     * @return bool
8502
     */
1441 ariadna 8503
    public function unlock_submission($userid) {
1 efrain 8504
        global $USER, $DB;
8505
 
8506
        // Need grade permission.
8507
        require_capability('mod/assign:grade', $this->context);
8508
 
8509
        // Give each submission plugin a chance to process the unlocking.
8510
        $plugins = $this->get_submission_plugins();
8511
        $submission = $this->get_user_submission($userid, false);
8512
 
8513
        $flags = $this->get_user_flags($userid, true);
8514
        $flags->locked = 0;
8515
        $this->update_user_flags($flags);
8516
 
8517
        foreach ($plugins as $plugin) {
8518
            if ($plugin->is_enabled() && $plugin->is_visible()) {
8519
                $plugin->unlock($submission, $flags);
8520
            }
8521
        }
8522
 
8523
        $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
8524
        \mod_assign\event\submission_unlocked::create_from_user($this, $user)->trigger();
8525
        return true;
8526
    }
8527
 
8528
    /**
8529
     * Unlock the student submission.
8530
     * Uses url parameter userid.
8531
     *
8532
     * @param int $userid
8533
     * @return bool
8534
     */
1441 ariadna 8535
    protected function process_unlock_submission($userid = 0) {
1 efrain 8536
 
8537
        require_sesskey();
8538
 
8539
        if (!$userid) {
8540
            $userid = required_param('userid', PARAM_INT);
8541
        }
8542
 
8543
        return $this->unlock_submission($userid);
8544
    }
8545
 
8546
    /**
8547
     * Apply a grade from a grading form to a user (may be called multiple times for a group submission).
8548
     *
8549
     * @param stdClass $formdata - the data from the form
8550
     * @param int $userid - the user to apply the grade to
8551
     * @param int $attemptnumber - The attempt number to apply the grade to.
8552
     * @return void
8553
     */
1441 ariadna 8554
    protected function apply_grade_to_user($formdata, $userid, $attemptnumber) {
1 efrain 8555
        global $USER, $CFG, $DB;
8556
 
8557
        $grade = $this->get_user_grade($userid, true, $attemptnumber);
8558
        $originalgrade = $grade->grade;
8559
        $gradingdisabled = $this->grading_disabled($userid);
8560
        $gradinginstance = $this->get_grading_instance($userid, $grade, $gradingdisabled);
8561
        if (!$gradingdisabled) {
8562
            if ($gradinginstance) {
1441 ariadna 8563
                $grade->grade = $gradinginstance->submit_and_get_grade($formdata->advancedgrading,
8564
                                                                       $grade->id);
1 efrain 8565
            } else {
8566
                // Handle the case when grade is set to No Grade.
8567
                if (isset($formdata->grade)) {
8568
                    $grade->grade = grade_floatval(unformat_float($formdata->grade));
8569
                }
8570
            }
8571
            if (isset($formdata->workflowstate) || isset($formdata->allocatedmarker)) {
8572
                $flags = $this->get_user_flags($userid, true);
8573
                $oldworkflowstate = $flags->workflowstate;
8574
                $flags->workflowstate = isset($formdata->workflowstate) ? $formdata->workflowstate : $flags->workflowstate;
8575
                $flags->allocatedmarker = isset($formdata->allocatedmarker) ? $formdata->allocatedmarker : $flags->allocatedmarker;
1441 ariadna 8576
                if ($this->update_user_flags($flags) &&
8577
                        isset($formdata->workflowstate) &&
8578
                        $formdata->workflowstate !== $oldworkflowstate) {
1 efrain 8579
                    $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
8580
                    \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $formdata->workflowstate)->trigger();
8581
                }
8582
            }
8583
        }
1441 ariadna 8584
        $grade->grader= $USER->id;
1 efrain 8585
 
8586
        $adminconfig = $this->get_admin_config();
8587
        $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
8588
 
8589
        $feedbackmodified = false;
8590
 
8591
        // Call save in plugins.
8592
        foreach ($this->feedbackplugins as $plugin) {
8593
            if ($plugin->is_enabled() && $plugin->is_visible()) {
8594
                $gradingmodified = $plugin->is_feedback_modified($grade, $formdata);
8595
                if ($gradingmodified) {
8596
                    if (!$plugin->save($grade, $formdata)) {
8597
                        $result = false;
8598
                        throw new \moodle_exception($plugin->get_error());
8599
                    }
8600
                    // If $feedbackmodified is true, keep it true.
8601
                    $feedbackmodified = $feedbackmodified || $gradingmodified;
8602
                }
8603
                if (('assignfeedback_' . $plugin->get_type()) == $gradebookplugin) {
8604
                    // This is the feedback plugin chose to push comments to the gradebook.
8605
                    $grade->feedbacktext = $plugin->text_for_gradebook($grade);
8606
                    $grade->feedbackformat = $plugin->format_for_gradebook($grade);
8607
                    $grade->feedbackfiles = $plugin->files_for_gradebook($grade);
8608
                }
8609
            }
8610
        }
8611
 
8612
        // We do not want to update the timemodified if no grade was added.
1441 ariadna 8613
        if (!empty($formdata->addattempt) ||
8614
                ($originalgrade !== null && $originalgrade != -1) ||
8615
                ($grade->grade !== null && $grade->grade != -1) ||
8616
                $feedbackmodified) {
1 efrain 8617
            $this->update_grade($grade, !empty($formdata->addattempt));
8618
        }
8619
 
8620
        // We never send notifications if we have marking workflow and the grade is not released.
1441 ariadna 8621
        if ($this->get_instance()->markingworkflow &&
8622
                isset($formdata->workflowstate) &&
8623
                $formdata->workflowstate != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
1 efrain 8624
            $formdata->sendstudentnotifications = false;
8625
        }
8626
 
8627
        // Note the default if not provided for this option is true (e.g. webservices).
8628
        // This is for backwards compatibility.
8629
        if (!isset($formdata->sendstudentnotifications) || $formdata->sendstudentnotifications) {
8630
            $this->notify_grade_modified($grade, true);
8631
        }
8632
    }
8633
 
8634
 
8635
    /**
8636
     * Save outcomes submitted from grading form.
8637
     *
8638
     * @param int $userid
8639
     * @param stdClass $formdata
8640
     * @param int $sourceuserid The user ID under which the outcome data is accessible. This is relevant
8641
     *                          for an outcome set to a user but applied to an entire group.
8642
     */
1441 ariadna 8643
    protected function process_outcomes($userid, $formdata, $sourceuserid = null) {
1 efrain 8644
        global $CFG, $USER;
8645
 
8646
        if (empty($CFG->enableoutcomes)) {
8647
            return;
8648
        }
8649
        if ($this->grading_disabled($userid)) {
8650
            return;
8651
        }
8652
 
1441 ariadna 8653
        require_once($CFG->libdir.'/gradelib.php');
1 efrain 8654
 
8655
        $data = array();
1441 ariadna 8656
        $gradinginfo = grade_get_grades($this->get_course()->id,
8657
                                        'mod',
8658
                                        'assign',
8659
                                        $this->get_instance()->id,
8660
                                        $userid);
1 efrain 8661
 
8662
        if (!empty($gradinginfo->outcomes)) {
8663
            foreach ($gradinginfo->outcomes as $index => $oldoutcome) {
1441 ariadna 8664
                $name = 'outcome_'.$index;
1 efrain 8665
                $sourceuserid = $sourceuserid !== null ? $sourceuserid : $userid;
1441 ariadna 8666
                if (isset($formdata->{$name}[$sourceuserid]) &&
8667
                        $oldoutcome->grades[$userid]->grade != $formdata->{$name}[$sourceuserid]) {
1 efrain 8668
                    $data[$index] = $formdata->{$name}[$sourceuserid];
8669
                }
8670
            }
8671
        }
8672
        if (count($data) > 0) {
1441 ariadna 8673
            grade_update_outcomes('mod/assign',
8674
                                  $this->course->id,
8675
                                  'mod',
8676
                                  'assign',
8677
                                  $this->get_instance()->id,
8678
                                  $userid,
8679
                                  $data);
1 efrain 8680
        }
8681
    }
8682
 
8683
    /**
8684
     * If the requirements are met - reopen the submission for another attempt.
8685
     * Only call this function when grading the latest attempt.
8686
     *
8687
     * @param int $userid The userid.
8688
     * @param stdClass $submission The submission (may be a group submission).
8689
     * @param bool $addattempt - True if the "allow another attempt" checkbox was checked.
8690
     * @return bool - true if another attempt was added.
8691
     */
1441 ariadna 8692
    protected function reopen_submission_if_required($userid, $submission, $addattempt) {
1 efrain 8693
        $instance = $this->get_instance();
8694
        $maxattemptsreached = !empty($submission) &&
1441 ariadna 8695
                              $submission->attemptnumber >= ($instance->maxattempts - 1) &&
8696
                              $instance->maxattempts != ASSIGN_UNLIMITED_ATTEMPTS;
1 efrain 8697
        $shouldreopen = false;
1441 ariadna 8698
        switch ($instance->attemptreopenmethod) {
8699
            case ASSIGN_ATTEMPT_REOPEN_METHOD_AUTOMATIC:
8700
                $shouldreopen = true;
8701
                break;
8702
            case ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS:
8703
                // Check the gradetopass from the gradebook.
8704
                $gradeitem = $this->get_grade_item();
8705
                if ($gradeitem) {
8706
                    $gradegrade = grade_grade::fetch(['userid' => $userid, 'itemid' => $gradeitem->id]);
1 efrain 8707
 
1441 ariadna 8708
                    // Do not reopen if is_passed returns null, e.g. if there is no pass criterion set.
8709
                    if ($gradegrade && ($gradegrade->is_passed() === false)) {
8710
                        $shouldreopen = true;
8711
                    }
8712
                }
8713
                break;
8714
            case ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL:
8715
                if (!empty($addattempt)) {
1 efrain 8716
                    $shouldreopen = true;
8717
                }
1441 ariadna 8718
                break;
1 efrain 8719
        }
8720
        if ($shouldreopen && !$maxattemptsreached) {
8721
            $this->add_attempt($userid);
8722
            return true;
8723
        }
8724
        return false;
8725
    }
8726
 
8727
    /**
8728
     * Save grade update.
8729
     *
8730
     * @param int $userid
8731
     * @param  stdClass $data
8732
     * @return bool - was the grade saved
8733
     */
1441 ariadna 8734
    public function save_grade($userid, $data) {
1 efrain 8735
 
8736
        // Need grade permission.
8737
        require_capability('mod/assign:grade', $this->context);
8738
 
8739
        $instance = $this->get_instance();
8740
        $submission = null;
8741
        if ($instance->teamsubmission) {
8742
            // We need to know what the most recent group submission is.
8743
            // Specifically when determining if we are adding another attempt (we only want to add one attempt per team),
8744
            // and when deciding if we need to update the gradebook with an edited grade.
8745
            $mostrecentsubmission = $this->get_group_submission($userid, 0, false, -1);
8746
            $this->set_most_recent_team_submission($mostrecentsubmission);
8747
            // Get the submission that we are saving grades for. The data attempt number determines which submission attempt.
8748
            $submission = $this->get_group_submission($userid, 0, false, $data->attemptnumber);
8749
        } else {
8750
            $submission = $this->get_user_submission($userid, false, $data->attemptnumber);
8751
        }
8752
        if ($instance->teamsubmission && !empty($data->applytoall)) {
8753
            $groupid = 0;
8754
            if ($this->get_submission_group($userid)) {
8755
                $group = $this->get_submission_group($userid);
8756
                if ($group) {
8757
                    $groupid = $group->id;
8758
                }
8759
            }
8760
            $members = $this->get_submission_group_members($groupid, true, $this->show_only_active_users());
8761
            foreach ($members as $member) {
8762
                // We only want to update the grade for this group submission attempt. The data attempt number could be
8763
                // -1 which may end up in additional attempts being created for each group member instead of just one
8764
                // additional attempt for the group.
8765
                $this->apply_grade_to_user($data, $member->id, $submission->attemptnumber);
8766
                $this->process_outcomes($member->id, $data, $userid);
8767
            }
8768
        } else {
8769
            $this->apply_grade_to_user($data, $userid, $data->attemptnumber);
8770
 
8771
            $this->process_outcomes($userid, $data);
8772
        }
8773
 
8774
        return true;
8775
    }
8776
 
8777
    /**
8778
     * Save grade.
8779
     *
8780
     * @param  moodleform $mform
8781
     * @return bool - was the grade saved
8782
     */
1441 ariadna 8783
    protected function process_save_grade(&$mform) {
1 efrain 8784
        global $CFG, $SESSION;
8785
        // Include grade form.
8786
        require_once($CFG->dirroot . '/mod/assign/gradeform.php');
8787
 
8788
        require_sesskey();
8789
 
8790
        $instance = $this->get_instance();
8791
        $rownum = required_param('rownum', PARAM_INT);
8792
        $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT);
8793
        $useridlistid = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
8794
        $userid = optional_param('userid', 0, PARAM_INT);
8795
        if (!$userid) {
8796
            if (empty($SESSION->mod_assign_useridlist[$this->get_useridlist_key($useridlistid)])) {
8797
                // If the userid list is not stored we must not save, as it is possible that the user in a
8798
                // given row position may not be the same now as when the grading page was generated.
8799
                $url = new moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id));
8800
                throw new moodle_exception('useridlistnotcached', 'mod_assign', $url);
8801
            }
8802
            $useridlist = $SESSION->mod_assign_useridlist[$this->get_useridlist_key($useridlistid)];
8803
        } else {
8804
            $useridlist = array($userid);
8805
            $rownum = 0;
8806
        }
8807
 
8808
        $last = false;
8809
        $userid = $useridlist[$rownum];
8810
        if ($rownum == count($useridlist) - 1) {
8811
            $last = true;
8812
        }
8813
 
8814
        $data = new stdClass();
8815
 
1441 ariadna 8816
        $gradeformparams = array('rownum' => $rownum,
8817
                                 'useridlistid' => $useridlistid,
8818
                                 'last' => $last,
8819
                                 'attemptnumber' => $attemptnumber,
8820
                                 'userid' => $userid);
8821
        $mform = new mod_assign_grade_form(null,
8822
                                           array($this, $data, $gradeformparams),
8823
                                           'post',
8824
                                           '',
8825
                                           array('class'=>'gradeform'));
1 efrain 8826
 
8827
        if ($formdata = $mform->get_data()) {
8828
            return $this->save_grade($userid, $formdata);
8829
        } else {
8830
            return false;
8831
        }
8832
    }
8833
 
8834
    /**
8835
     * This function is a static wrapper around can_upgrade.
8836
     *
8837
     * @param string $type The plugin type
8838
     * @param int $version The plugin version
8839
     * @return bool
8840
     */
1441 ariadna 8841
    public static function can_upgrade_assignment($type, $version) {
1 efrain 8842
        $assignment = new assign(null, null, null);
8843
        return $assignment->can_upgrade($type, $version);
8844
    }
8845
 
8846
    /**
8847
     * This function returns true if it can upgrade an assignment from the 2.2 module.
8848
     *
8849
     * @param string $type The plugin type
8850
     * @param int $version The plugin version
8851
     * @return bool
8852
     */
1441 ariadna 8853
    public function can_upgrade($type, $version) {
1 efrain 8854
        if ($type == 'offline' && $version >= 2011112900) {
8855
            return true;
8856
        }
8857
        foreach ($this->submissionplugins as $plugin) {
8858
            if ($plugin->can_upgrade($type, $version)) {
8859
                return true;
8860
            }
8861
        }
8862
        foreach ($this->feedbackplugins as $plugin) {
8863
            if ($plugin->can_upgrade($type, $version)) {
8864
                return true;
8865
            }
8866
        }
8867
        return false;
8868
    }
8869
 
8870
    /**
8871
     * Copy all the files from the old assignment files area to the new one.
8872
     * This is used by the plugin upgrade code.
8873
     *
8874
     * @param int $oldcontextid The old assignment context id
8875
     * @param int $oldcomponent The old assignment component ('assignment')
8876
     * @param int $oldfilearea The old assignment filearea ('submissions')
8877
     * @param int $olditemid The old submissionid (can be null e.g. intro)
8878
     * @param int $newcontextid The new assignment context id
8879
     * @param int $newcomponent The new assignment component ('assignment')
8880
     * @param int $newfilearea The new assignment filearea ('submissions')
8881
     * @param int $newitemid The new submissionid (can be null e.g. intro)
8882
     * @return int The number of files copied
8883
     */
1441 ariadna 8884
    public function copy_area_files_for_upgrade($oldcontextid,
8885
                                                $oldcomponent,
8886
                                                $oldfilearea,
8887
                                                $olditemid,
8888
                                                $newcontextid,
8889
                                                $newcomponent,
8890
                                                $newfilearea,
8891
                                                $newitemid) {
1 efrain 8892
        // Note, this code is based on some code in filestorage - but that code
8893
        // deleted the old files (which we don't want).
8894
        $count = 0;
8895
 
8896
        $fs = get_file_storage();
8897
 
1441 ariadna 8898
        $oldfiles = $fs->get_area_files($oldcontextid,
8899
                                        $oldcomponent,
8900
                                        $oldfilearea,
8901
                                        $olditemid,
8902
                                        'id',
8903
                                        false);
1 efrain 8904
        foreach ($oldfiles as $oldfile) {
8905
            $filerecord = new stdClass();
8906
            $filerecord->contextid = $newcontextid;
8907
            $filerecord->component = $newcomponent;
8908
            $filerecord->filearea = $newfilearea;
8909
            $filerecord->itemid = $newitemid;
8910
            $fs->create_file_from_storedfile($filerecord, $oldfile);
8911
            $count += 1;
8912
        }
8913
 
8914
        return $count;
8915
    }
8916
 
8917
    /**
8918
     * Add a new attempt for each user in the list - but reopen each group assignment
8919
     * at most 1 time.
8920
     *
8921
     * @param array $useridlist Array of userids to reopen.
8922
     * @return bool
8923
     */
1441 ariadna 8924
    protected function process_add_attempt_group($useridlist) {
1 efrain 8925
        $groupsprocessed = array();
8926
        $result = true;
8927
 
8928
        foreach ($useridlist as $userid) {
8929
            $groupid = 0;
8930
            $group = $this->get_submission_group($userid);
8931
            if ($group) {
8932
                $groupid = $group->id;
8933
            }
8934
 
8935
            if (empty($groupsprocessed[$groupid])) {
8936
                // We need to know what the most recent group submission is.
8937
                // Specifically when determining if we are adding another attempt (we only want to add one attempt per team),
8938
                // and when deciding if we need to update the gradebook with an edited grade.
8939
                $currentsubmission = $this->get_group_submission($userid, 0, false, -1);
8940
                $this->set_most_recent_team_submission($currentsubmission);
8941
                $result = $this->process_add_attempt($userid) && $result;
8942
                $groupsprocessed[$groupid] = true;
8943
            }
8944
        }
8945
        return $result;
8946
    }
8947
 
8948
    /**
8949
     * Check for a sess key and then call add_attempt.
8950
     *
8951
     * @param int $userid int The user to add the attempt for
8952
     * @return bool - true if successful.
8953
     */
1441 ariadna 8954
    protected function process_add_attempt($userid) {
1 efrain 8955
        require_sesskey();
8956
 
8957
        return $this->add_attempt($userid);
8958
    }
8959
 
8960
    /**
8961
     * Add a new attempt for a user.
8962
     *
8963
     * @param int $userid int The user to add the attempt for
8964
     * @return bool - true if successful.
8965
     */
1441 ariadna 8966
    protected function add_attempt($userid) {
1 efrain 8967
        require_capability('mod/assign:grade', $this->context);
8968
 
1441 ariadna 8969
        // If additional attempts are disallowed.
8970
        if ($this->get_instance()->maxattempts == 1) {
1 efrain 8971
            return false;
8972
        }
8973
 
8974
        if ($this->get_instance()->teamsubmission) {
8975
            $oldsubmission = $this->get_group_submission($userid, 0, false);
8976
        } else {
8977
            $oldsubmission = $this->get_user_submission($userid, false);
8978
        }
8979
 
8980
        if (!$oldsubmission) {
8981
            return false;
8982
        }
8983
 
8984
        // No more than max attempts allowed.
1441 ariadna 8985
        if ($this->get_instance()->maxattempts != ASSIGN_UNLIMITED_ATTEMPTS &&
8986
            $oldsubmission->attemptnumber >= ($this->get_instance()->maxattempts - 1)) {
1 efrain 8987
            return false;
8988
        }
8989
 
8990
        // Create the new submission record for the group/user.
8991
        if ($this->get_instance()->teamsubmission) {
8992
            if (isset($this->mostrecentteamsubmission)) {
8993
                // Team submissions can end up in this function for each user (via save_grade). We don't want to create
8994
                // more than one attempt for the whole team.
8995
                if ($this->mostrecentteamsubmission->attemptnumber == $oldsubmission->attemptnumber) {
8996
                    $newsubmission = $this->get_group_submission($userid, 0, true, $oldsubmission->attemptnumber + 1);
8997
                } else {
8998
                    $newsubmission = $this->get_group_submission($userid, 0, false, $oldsubmission->attemptnumber);
8999
                }
9000
            } else {
9001
                debugging('Please use set_most_recent_team_submission() before calling add_attempt', DEBUG_DEVELOPER);
9002
                $newsubmission = $this->get_group_submission($userid, 0, true, $oldsubmission->attemptnumber + 1);
9003
            }
9004
        } else {
9005
            $newsubmission = $this->get_user_submission($userid, true, $oldsubmission->attemptnumber + 1);
9006
        }
9007
 
9008
        // Set the status of the new attempt to reopened.
9009
        $newsubmission->status = ASSIGN_SUBMISSION_STATUS_REOPENED;
9010
 
9011
        // Give each submission plugin a chance to process the add_attempt.
9012
        $plugins = $this->get_submission_plugins();
9013
        foreach ($plugins as $plugin) {
9014
            if ($plugin->is_enabled() && $plugin->is_visible()) {
9015
                $plugin->add_attempt($oldsubmission, $newsubmission);
9016
            }
9017
        }
9018
 
9019
        $this->update_submission($newsubmission, $userid, false, $this->get_instance()->teamsubmission);
9020
        $flags = $this->get_user_flags($userid, false);
9021
        if (isset($flags->locked) && $flags->locked) { // May not exist.
9022
            $this->process_unlock_submission($userid);
9023
        }
9024
        return true;
9025
    }
9026
 
9027
    /**
9028
     * Get an upto date list of user grades and feedback for the gradebook.
9029
     *
9030
     * @param int $userid int or 0 for all users
9031
     * @return array of grade data formated for the gradebook api
9032
     *         The data required by the gradebook api is userid,
9033
     *                                                   rawgrade,
9034
     *                                                   feedback,
9035
     *                                                   feedbackformat,
9036
     *                                                   usermodified,
9037
     *                                                   dategraded,
9038
     *                                                   datesubmitted
9039
     */
1441 ariadna 9040
    public function get_user_grades_for_gradebook($userid) {
1 efrain 9041
        global $DB, $CFG;
9042
        $grades = array();
9043
        $assignmentid = $this->get_instance()->id;
9044
 
9045
        $adminconfig = $this->get_admin_config();
9046
        $gradebookpluginname = $adminconfig->feedback_plugin_for_gradebook;
9047
        $gradebookplugin = null;
9048
 
9049
        // Find the gradebook plugin.
9050
        foreach ($this->feedbackplugins as $plugin) {
9051
            if ($plugin->is_enabled() && $plugin->is_visible()) {
9052
                if (('assignfeedback_' . $plugin->get_type()) == $gradebookpluginname) {
9053
                    $gradebookplugin = $plugin;
9054
                }
9055
            }
9056
        }
9057
        if ($userid) {
9058
            $where = ' WHERE u.id = :userid ';
9059
        } else {
9060
            $where = ' WHERE u.id != :userid ';
9061
        }
9062
 
9063
        // When the gradebook asks us for grades - only return the last attempt for each user.
1441 ariadna 9064
        $params = array('assignid1'=>$assignmentid,
9065
                        'assignid2'=>$assignmentid,
9066
                        'userid'=>$userid);
1 efrain 9067
        $graderesults = $DB->get_recordset_sql('SELECT
9068
                                                    u.id as userid,
9069
                                                    s.timemodified as datesubmitted,
9070
                                                    g.grade as rawgrade,
9071
                                                    g.timemodified as dategraded,
9072
                                                    g.grader as usermodified
9073
                                                FROM {user} u
9074
                                                LEFT JOIN {assign_submission} s
9075
                                                    ON u.id = s.userid and s.assignment = :assignid1 AND
9076
                                                    s.latest = 1
9077
                                                JOIN {assign_grades} g
9078
                                                    ON u.id = g.userid and g.assignment = :assignid2 AND
9079
                                                    g.attemptnumber = s.attemptnumber' .
1441 ariadna 9080
                                                $where, $params);
1 efrain 9081
 
9082
        foreach ($graderesults as $result) {
9083
            $gradingstatus = $this->get_grading_status($result->userid);
9084
            if (!$this->get_instance()->markingworkflow || $gradingstatus == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
9085
                $gradebookgrade = clone $result;
9086
                // Now get the feedback.
9087
                if ($gradebookplugin) {
9088
                    $grade = $this->get_user_grade($result->userid, false);
9089
                    if ($grade) {
9090
                        $feedbacktext = $gradebookplugin->text_for_gradebook($grade);
9091
                        if (!empty($feedbacktext)) {
9092
                            $gradebookgrade->feedback = $feedbacktext;
9093
                        }
9094
                        $gradebookgrade->feedbackformat = $gradebookplugin->format_for_gradebook($grade);
9095
                        $gradebookgrade->feedbackfiles = $gradebookplugin->files_for_gradebook($grade);
9096
                    }
9097
                }
9098
                $grades[$gradebookgrade->userid] = $gradebookgrade;
9099
            }
9100
        }
9101
 
9102
        $graderesults->close();
9103
        return $grades;
9104
    }
9105
 
9106
    /**
9107
     * Call the static version of this function
9108
     *
9109
     * @param int $userid The userid to lookup
9110
     * @return int The unique id
9111
     */
1441 ariadna 9112
    public function get_uniqueid_for_user($userid) {
1 efrain 9113
        return self::get_uniqueid_for_user_static($this->get_instance()->id, $userid);
9114
    }
9115
 
9116
    /**
9117
     * Foreach participant in the course - assign them a random id.
9118
     *
9119
     * @param int $assignid The assignid to lookup
9120
     */
1441 ariadna 9121
    public static function allocate_unique_ids($assignid) {
1 efrain 9122
        global $DB;
9123
 
9124
        $cm = get_coursemodule_from_instance('assign', $assignid, 0, false, MUST_EXIST);
9125
        $context = context_module::instance($cm->id);
9126
 
9127
        $currentgroup = groups_get_activity_group($cm, true);
9128
        $users = get_enrolled_users($context, "mod/assign:submit", $currentgroup, 'u.id');
9129
 
9130
        // Shuffle the users.
9131
        shuffle($users);
9132
 
9133
        foreach ($users as $user) {
1441 ariadna 9134
            $record = $DB->get_record('assign_user_mapping',
9135
                                      array('assignment'=>$assignid, 'userid'=>$user->id),
9136
                                     'id');
1 efrain 9137
            if (!$record) {
9138
                $record = new stdClass();
9139
                $record->assignment = $assignid;
9140
                $record->userid = $user->id;
9141
                $DB->insert_record('assign_user_mapping', $record);
9142
            }
9143
        }
9144
    }
9145
 
9146
    /**
9147
     * Lookup this user id and return the unique id for this assignment.
9148
     *
9149
     * @param int $assignid The assignment id
9150
     * @param int $userid The userid to lookup
9151
     * @return int The unique id
9152
     */
1441 ariadna 9153
    public static function get_uniqueid_for_user_static($assignid, $userid) {
1 efrain 9154
        global $DB;
9155
 
9156
        // Search for a record.
1441 ariadna 9157
        $params = array('assignment'=>$assignid, 'userid'=>$userid);
1 efrain 9158
        if ($record = $DB->get_record('assign_user_mapping', $params, 'id')) {
9159
            return $record->id;
9160
        }
9161
 
9162
        // Be a little smart about this - there is no record for the current user.
9163
        // We should ensure any unallocated ids for the current participant
9164
        // list are distrubited randomly.
9165
        self::allocate_unique_ids($assignid);
9166
 
9167
        // Retry the search for a record.
9168
        if ($record = $DB->get_record('assign_user_mapping', $params, 'id')) {
9169
            return $record->id;
9170
        }
9171
 
9172
        // The requested user must not be a participant. Add a record anyway.
9173
        $record = new stdClass();
9174
        $record->assignment = $assignid;
9175
        $record->userid = $userid;
9176
 
9177
        return $DB->insert_record('assign_user_mapping', $record);
9178
    }
9179
 
9180
    /**
9181
     * Call the static version of this function.
9182
     *
9183
     * @param int $uniqueid The uniqueid to lookup
9184
     * @return int The user id or false if they don't exist
9185
     */
1441 ariadna 9186
    public function get_user_id_for_uniqueid($uniqueid) {
1 efrain 9187
        return self::get_user_id_for_uniqueid_static($this->get_instance()->id, $uniqueid);
9188
    }
9189
 
9190
    /**
9191
     * Lookup this unique id and return the user id for this assignment.
9192
     *
9193
     * @param int $assignid The id of the assignment this user mapping is in
9194
     * @param int $uniqueid The uniqueid to lookup
9195
     * @return int The user id or false if they don't exist
9196
     */
1441 ariadna 9197
    public static function get_user_id_for_uniqueid_static($assignid, $uniqueid) {
1 efrain 9198
        global $DB;
9199
 
9200
        // Search for a record.
1441 ariadna 9201
        if ($record = $DB->get_record('assign_user_mapping',
9202
                                      array('assignment'=>$assignid, 'id'=>$uniqueid),
9203
                                      'userid',
9204
                                      IGNORE_MISSING)) {
1 efrain 9205
            return $record->userid;
9206
        }
9207
 
9208
        return false;
9209
    }
9210
 
9211
    /**
9212
     * Get the list of marking_workflow states the current user has permission to transition a grade to.
9213
     *
9214
     * @return array of state => description
9215
     */
1441 ariadna 9216
    public function get_marking_workflow_states_for_current_user() {
1 efrain 9217
        if (!empty($this->markingworkflowstates)) {
9218
            return $this->markingworkflowstates;
9219
        }
9220
        $states = array();
9221
        if (has_capability('mod/assign:grade', $this->context)) {
9222
            $states[ASSIGN_MARKING_WORKFLOW_STATE_INMARKING] = get_string('markingworkflowstateinmarking', 'assign');
9223
            $states[ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW] = get_string('markingworkflowstatereadyforreview', 'assign');
9224
        }
1441 ariadna 9225
        if (has_any_capability(array('mod/assign:reviewgrades',
9226
                                     'mod/assign:managegrades'), $this->context)) {
1 efrain 9227
            $states[ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW] = get_string('markingworkflowstateinreview', 'assign');
9228
            $states[ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE] = get_string('markingworkflowstatereadyforrelease', 'assign');
9229
        }
1441 ariadna 9230
        if (has_any_capability(array('mod/assign:releasegrades',
9231
                                     'mod/assign:managegrades'), $this->context)) {
1 efrain 9232
            $states[ASSIGN_MARKING_WORKFLOW_STATE_RELEASED] = get_string('markingworkflowstatereleased', 'assign');
9233
        }
9234
        $this->markingworkflowstates = $states;
9235
        return $this->markingworkflowstates;
9236
    }
9237
 
9238
    /**
9239
     * Get the list of marking_workflow states.
9240
     *
9241
     * @return array Array of multiple state => description.
9242
     */
1441 ariadna 9243
    public function get_all_marking_workflow_states(): array {
1 efrain 9244
        if (!empty($this->allmarkingworkflowstates)) {
9245
            return $this->allmarkingworkflowstates;
9246
        }
9247
 
9248
        $this->allmarkingworkflowstates = [
9249
            ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED => get_string('markingworkflowstatenotmarked', 'assign'),
9250
            ASSIGN_MARKING_WORKFLOW_STATE_INMARKING => get_string('markingworkflowstateinmarking', 'assign'),
9251
            ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW => get_string('markingworkflowstatereadyforreview', 'assign'),
9252
            ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW => get_string('markingworkflowstateinreview', 'assign'),
9253
            ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE => get_string('markingworkflowstatereadyforrelease', 'assign'),
9254
            ASSIGN_MARKING_WORKFLOW_STATE_RELEASED => get_string('markingworkflowstatereleased', 'assign'),
9255
        ];
9256
 
9257
        return $this->allmarkingworkflowstates;
9258
    }
9259
 
9260
    /**
9261
     * Check is only active users in course should be shown.
9262
     *
9263
     * @return bool true if only active users should be shown.
9264
     */
1441 ariadna 9265
    public function show_only_active_users() {
1 efrain 9266
        global $CFG;
9267
 
9268
        if (is_null($this->showonlyactiveenrol)) {
9269
            $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol);
9270
            $this->showonlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol);
9271
 
9272
            if (!is_null($this->context)) {
9273
                $this->showonlyactiveenrol = $this->showonlyactiveenrol ||
1441 ariadna 9274
                            !has_capability('moodle/course:viewsuspendedusers', $this->context);
1 efrain 9275
            }
9276
        }
9277
        return $this->showonlyactiveenrol;
9278
    }
9279
 
9280
    /**
9281
     * Return true is user is active user in course else false
9282
     *
9283
     * @param int $userid
9284
     * @return bool true is user is active in course.
9285
     */
1441 ariadna 9286
    public function is_active_user($userid) {
1 efrain 9287
        return !in_array($userid, get_suspended_userids($this->context, true));
9288
    }
9289
 
9290
    /**
9291
     * Returns true if gradebook feedback plugin is enabled
9292
     *
9293
     * @return bool true if gradebook feedback plugin is enabled and visible else false.
9294
     */
1441 ariadna 9295
    public function is_gradebook_feedback_enabled() {
1 efrain 9296
        // Get default grade book feedback plugin.
9297
        $adminconfig = $this->get_admin_config();
9298
        $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
9299
        $gradebookplugin = str_replace('assignfeedback_', '', $gradebookplugin);
9300
 
9301
        // Check if default gradebook feedback is visible and enabled.
9302
        $gradebookfeedbackplugin = $this->get_feedback_plugin_by_type($gradebookplugin);
9303
 
9304
        if (empty($gradebookfeedbackplugin)) {
9305
            return false;
9306
        }
9307
 
9308
        if ($gradebookfeedbackplugin->is_visible() && $gradebookfeedbackplugin->is_enabled()) {
9309
            return true;
9310
        }
9311
 
9312
        // Gradebook feedback plugin is either not visible/enabled.
9313
        return false;
9314
    }
9315
 
9316
    /**
9317
     * Returns the grading status.
9318
     *
9319
     * @param int $userid the user id
9320
     * @return string returns the grading status
9321
     */
1441 ariadna 9322
    public function get_grading_status($userid) {
1 efrain 9323
        if ($this->get_instance()->markingworkflow) {
9324
            $flags = $this->get_user_flags($userid, false);
9325
            if (!empty($flags->workflowstate)) {
9326
                return $flags->workflowstate;
9327
            }
9328
            return ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED;
9329
        } else {
9330
            $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT);
9331
            $grade = $this->get_user_grade($userid, false, $attemptnumber);
9332
 
9333
            if (!empty($grade) && $grade->grade !== null && $grade->grade >= 0) {
9334
                return ASSIGN_GRADING_STATUS_GRADED;
9335
            } else {
9336
                return ASSIGN_GRADING_STATUS_NOT_GRADED;
9337
            }
9338
        }
9339
    }
9340
 
9341
    /**
9342
     * The id used to uniquily identify the cache for this instance of the assign object.
9343
     *
9344
     * @return string
9345
     */
1441 ariadna 9346
    public function get_useridlist_key_id() {
1 efrain 9347
        return $this->useridlistid;
9348
    }
9349
 
9350
    /**
9351
     * Generates the key that should be used for an entry in the useridlist cache.
9352
     *
9353
     * @param string $id Generate a key for this instance (optional)
9354
     * @return string The key for the id, or new entry if no $id is passed.
9355
     */
1441 ariadna 9356
    public function get_useridlist_key($id = null) {
1 efrain 9357
        global $SESSION;
9358
 
9359
        // Ensure the user id list cache is initialised.
9360
        if (!isset($SESSION->mod_assign_useridlist)) {
9361
            $SESSION->mod_assign_useridlist = [];
9362
        }
9363
 
9364
        if ($id === null) {
9365
            $id = $this->get_useridlist_key_id();
9366
        }
9367
        return $this->get_course_module()->id . '_' . $id;
9368
    }
9369
 
9370
    /**
9371
     * Updates and creates the completion records in mdl_course_modules_completion.
9372
     *
9373
     * @param int $teamsubmission value of 0 or 1 to indicate whether this is a group activity
9374
     * @param int $requireallteammemberssubmit value of 0 or 1 to indicate whether all group members must click Submit
9375
     * @param obj $submission the submission
9376
     * @param int $userid the user id
9377
     * @param int $complete
9378
     * @param obj $completion
9379
     *
9380
     * @return null
9381
     */
1441 ariadna 9382
    protected function update_activity_completion_records($teamsubmission,
9383
                                                          $requireallteammemberssubmit,
9384
                                                          $submission,
9385
                                                          $userid,
9386
                                                          $complete,
9387
                                                          $completion) {
1 efrain 9388
 
9389
        if (($teamsubmission && $submission->groupid > 0 && !$requireallteammemberssubmit) ||
9390
            ($teamsubmission && $submission->groupid > 0 && $requireallteammemberssubmit &&
1441 ariadna 9391
             $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED)) {
1 efrain 9392
 
9393
            $members = groups_get_members($submission->groupid);
9394
 
9395
            foreach ($members as $member) {
9396
                $completion->update_state($this->get_course_module(), $complete, $member->id);
9397
            }
9398
        } else {
9399
            $completion->update_state($this->get_course_module(), $complete, $userid);
9400
        }
9401
 
9402
        return;
9403
    }
9404
 
9405
    /**
9406
     * Update the module completion status (set it viewed) and trigger module viewed event.
9407
     *
9408
     * @since Moodle 3.2
9409
     */
1441 ariadna 9410
    public function set_module_viewed() {
1 efrain 9411
        $completion = new completion_info($this->get_course());
9412
        $completion->set_module_viewed($this->get_course_module());
9413
 
9414
        // Trigger the course module viewed event.
9415
        $assigninstance = $this->get_instance();
9416
        $params = [
9417
            'objectid' => $assigninstance->id,
9418
            'context' => $this->get_context()
9419
        ];
9420
        if ($this->is_blind_marking()) {
9421
            $params['anonymous'] = 1;
9422
        }
9423
 
9424
        $event = \mod_assign\event\course_module_viewed::create($params);
9425
 
9426
        $event->add_record_snapshot('assign', $assigninstance);
9427
        $event->trigger();
9428
    }
9429
 
9430
    /**
9431
     * Checks for any grade notices, and adds notifications. Will display on assignment main page and grading table.
9432
     *
9433
     * @return void The notifications API will render the notifications at the appropriate part of the page.
9434
     */
1441 ariadna 9435
    protected function add_grade_notices() {
1 efrain 9436
        if (has_capability('mod/assign:grade', $this->get_context()) && get_config('assign', 'has_rescaled_null_grades_' . $this->get_instance()->id)) {
9437
            $link = new \moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id, 'action' => 'fixrescalednullgrades'));
9438
            \core\notification::warning(get_string('fixrescalednullgrades', 'mod_assign', ['link' => $link->out()]));
9439
        }
9440
    }
9441
 
9442
    /**
9443
     * View fix rescaled null grades.
9444
     *
9445
     * @return bool True if null all grades are now fixed.
9446
     */
1441 ariadna 9447
    protected function fix_null_grades() {
1 efrain 9448
        global $DB;
9449
        $result = $DB->set_field_select(
9450
            'assign_grades',
9451
            'grade',
9452
            ASSIGN_GRADE_NOT_SET,
9453
            'grade <> ? AND grade < 0',
9454
            [ASSIGN_GRADE_NOT_SET]
9455
        );
9456
        $assign = clone $this->get_instance();
9457
        $assign->cmidnumber = $this->get_course_module()->idnumber;
9458
        assign_update_grades($assign);
9459
        return $result;
9460
    }
9461
 
9462
    /**
9463
     * View fix rescaled null grades.
9464
     *
9465
     * @return void The notifications API will render the notifications at the appropriate part of the page.
9466
     */
1441 ariadna 9467
    protected function view_fix_rescaled_null_grades() {
1 efrain 9468
        global $OUTPUT;
9469
 
9470
        $o = '';
9471
 
9472
        require_capability('mod/assign:grade', $this->get_context());
9473
 
9474
        $instance = $this->get_instance();
9475
 
9476
        $o .= $this->get_renderer()->render(
9477
            new assign_header(
9478
                $instance,
9479
                $this->get_context(),
9480
                $this->show_intro(),
9481
                $this->get_course_module()->id
9482
            )
9483
        );
9484
 
9485
        $confirm = optional_param('confirm', 0, PARAM_BOOL);
9486
 
9487
        if ($confirm) {
9488
            if (confirm_sesskey()) {
9489
                // Fix the grades.
9490
                $this->fix_null_grades();
9491
                unset_config('has_rescaled_null_grades_' . $instance->id, 'assign');
9492
                // Display the success notice.
9493
                $o .= $this->get_renderer()->notification(get_string('fixrescalednullgradesdone', 'assign'), 'notifysuccess');
9494
            } else {
9495
                // If the sesskey is not valid, then display the error notice.
9496
                $o .= $this->get_renderer()->notification(get_string('invalidsesskey', 'error'), 'notifyerror');
9497
            }
9498
            $url = new moodle_url(
9499
                url: '/mod/assign/view.php',
9500
                params: [
9501
                    'id' => $this->get_course_module()->id,
9502
                    'action' => 'grading',
9503
                ],
9504
            );
9505
            $o .= $this->get_renderer()->continue_button($url);
9506
        } else {
9507
            // Ask for confirmation.
9508
            $continue = new \moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id, 'action' => 'fixrescalednullgrades', 'confirm' => true, 'sesskey' => sesskey()));
9509
            $cancel = new \moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id));
9510
            $o .= $OUTPUT->confirm(get_string('fixrescalednullgradesconfirm', 'mod_assign'), $continue, $cancel);
9511
        }
9512
 
9513
        $o .= $this->view_footer();
9514
 
9515
        return $o;
9516
    }
9517
 
9518
    /**
9519
     * Set the most recent submission for the team.
9520
     * The most recent team submission is used to determine if another attempt should be created when allowing another
9521
     * attempt on a group assignment, and whether the gradebook should be updated.
9522
     *
9523
     * @since Moodle 3.4
9524
     * @param stdClass $submission The most recent submission of the group.
9525
     */
1441 ariadna 9526
    public function set_most_recent_team_submission($submission) {
1 efrain 9527
        $this->mostrecentteamsubmission = $submission;
9528
    }
9529
 
9530
    /**
9531
     * Return array of valid grading allocation filters for the grading interface.
9532
     *
9533
     * @param boolean $export Export the list of filters for a template.
9534
     * @return array
9535
     */
1441 ariadna 9536
    public function get_marking_allocation_filters($export = false) {
1 efrain 9537
        $markingallocation = $this->get_instance()->markingworkflow &&
9538
            $this->get_instance()->markingallocation &&
9539
            has_capability('mod/assign:manageallocations', $this->context);
9540
        // Get markers to use in drop lists.
9541
        $markingallocationoptions = array();
9542
        if ($markingallocation) {
9543
            list($sort, $params) = users_order_by_sql('u');
9544
            // Only enrolled users could be assigned as potential markers.
9545
            $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
9546
            $markingallocationoptions[''] = get_string('filternone', 'assign');
9547
            $markingallocationoptions[ASSIGN_MARKER_FILTER_NO_MARKER] = get_string('markerfilternomarker', 'assign');
9548
            $viewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
9549
            foreach ($markers as $marker) {
9550
                $markingallocationoptions[$marker->id] = fullname($marker, $viewfullnames);
9551
            }
9552
        }
9553
        if ($export) {
9554
            $allocationfilter = get_user_preferences('assign_markerfilter', '');
9555
            $result = [];
9556
            foreach ($markingallocationoptions as $option => $label) {
9557
                array_push($result, [
9558
                    'key' => $option,
9559
                    'name' => $label,
9560
                    'active' => ($allocationfilter == $option),
9561
                ]);
9562
            }
9563
            return $result;
9564
        }
1441 ariadna 9565
        return $markingallocationoptions;
1 efrain 9566
    }
9567
 
9568
    /**
9569
     * Return array of valid grading workflow filters for the grading interface.
9570
     *
9571
     * @param boolean $export Export the list of filters for a template.
9572
     * @return array
9573
     */
1441 ariadna 9574
    public function get_marking_workflow_filters($export = false) {
1 efrain 9575
        $markingworkflow = $this->get_instance()->markingworkflow;
9576
        // Get marking states to show in form.
9577
        $markingworkflowoptions = array();
9578
        if ($markingworkflow) {
9579
            $notmarked = get_string('markingworkflowstatenotmarked', 'assign');
9580
            $markingworkflowoptions[''] = get_string('filternone', 'assign');
9581
            $markingworkflowoptions[ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED] = $notmarked;
9582
            $markingworkflowoptions = array_merge($markingworkflowoptions, $this->get_marking_workflow_states_for_current_user());
9583
        }
9584
        if ($export) {
9585
            $workflowfilter = get_user_preferences('assign_workflowfilter', '');
9586
            $result = [];
9587
            foreach ($markingworkflowoptions as $option => $label) {
9588
                array_push($result, [
9589
                    'key' => $option,
9590
                    'name' => $label,
9591
                    'active' => ($workflowfilter == $option),
9592
                ]);
9593
            }
9594
            return $result;
9595
        }
9596
        return $markingworkflowoptions;
9597
    }
9598
 
9599
    /**
9600
     * Return array of valid search filters for the grading interface.
9601
     *
1441 ariadna 9602
     * @param bool $grouped Whether to return the filters grouped or not.
1 efrain 9603
     * @return array
9604
     */
1441 ariadna 9605
    public function get_filters(bool $grouped = false): array {
9606
        $groupedfilterkeys = [
9607
            [
9608
                ASSIGN_FILTER_NOT_SUBMITTED,
9609
                ASSIGN_FILTER_DRAFT,
9610
                ASSIGN_FILTER_SUBMITTED,
9611
                ASSIGN_FILTER_REQUIRE_GRADING,
9612
                ASSIGN_FILTER_GRADED,
9613
            ],
9614
            [
9615
                ASSIGN_FILTER_GRANTED_EXTENSION,
9616
            ],
1 efrain 9617
        ];
9618
 
9619
        $current = get_user_preferences('assign_filter', '');
9620
 
9621
        $filters = [];
9622
        // First is always "no filter" option.
1441 ariadna 9623
        $filters[0] = [
9624
            [
9625
                'key' => ASSIGN_FILTER_NONE,
9626
                'name' => get_string('filterall', 'assign'),
9627
                'active' => ($current == ''),
9628
            ],
9629
        ];
1 efrain 9630
 
1441 ariadna 9631
        foreach ($groupedfilterkeys as $group => $filterkeys) {
9632
            foreach ($filterkeys as $key) {
9633
                $filters[$group] = $filters[$group] ?? [];
9634
                $filters[$group][] = [
9635
                    'key' => $key,
9636
                    'name' => get_string('filter' . $key, 'assign'),
9637
                    'active' => ($current == $key),
9638
                ];
9639
            }
1 efrain 9640
        }
1441 ariadna 9641
 
9642
        return $grouped ? $filters : array_merge(...$filters);
1 efrain 9643
    }
9644
 
9645
    /**
9646
     * Get the correct submission statement depending on single submisison, team submission or team submission
9647
     * where all team memebers must submit.
9648
     *
9649
     * @param stdClass $adminconfig
9650
     * @param stdClass $instance
9651
     * @param context $context
9652
     *
9653
     * @return string
9654
     */
1441 ariadna 9655
    protected function get_submissionstatement($adminconfig, $instance, $context) {
1 efrain 9656
        $submissionstatement = '';
9657
 
9658
        if (!($context instanceof context)) {
9659
            return $submissionstatement;
9660
        }
9661
 
9662
        // Single submission.
9663
        if (!$instance->teamsubmission) {
9664
            // Single submission statement is not empty.
9665
            if (!empty($adminconfig->submissionstatement)) {
9666
                // Format the submission statement before its sent. We turn off para because this is going within
9667
                // a form element.
9668
                $options = array(
9669
                    'context' => $context,
9670
                    'para'    => false
9671
                );
9672
                $submissionstatement = format_text($adminconfig->submissionstatement, FORMAT_MOODLE, $options);
9673
            }
9674
        } else { // Team submission.
9675
            // One user can submit for the whole team.
9676
            if (!empty($adminconfig->submissionstatementteamsubmission) && !$instance->requireallteammemberssubmit) {
9677
                // Format the submission statement before its sent. We turn off para because this is going within
9678
                // a form element.
9679
                $options = array(
9680
                    'context' => $context,
9681
                    'para'    => false
9682
                );
1441 ariadna 9683
                $submissionstatement = format_text($adminconfig->submissionstatementteamsubmission,
9684
                    FORMAT_MOODLE, $options);
9685
            } else if (!empty($adminconfig->submissionstatementteamsubmissionallsubmit) &&
9686
                $instance->requireallteammemberssubmit) {
1 efrain 9687
                // All team members must submit.
9688
                // Format the submission statement before its sent. We turn off para because this is going within
9689
                // a form element.
9690
                $options = array(
9691
                    'context' => $context,
9692
                    'para'    => false
9693
                );
1441 ariadna 9694
                $submissionstatement = format_text($adminconfig->submissionstatementteamsubmissionallsubmit,
9695
                    FORMAT_MOODLE, $options);
1 efrain 9696
            }
9697
        }
9698
 
9699
        return $submissionstatement;
9700
    }
9701
 
9702
    /**
9703
     * Check if time limit for assignment enabled and set up.
9704
     *
9705
     * @param int|null $userid User ID. If null, use global user.
9706
     * @return bool
9707
     */
1441 ariadna 9708
    public function is_time_limit_enabled(?int $userid = null): bool {
1 efrain 9709
        $instance = $this->get_instance($userid);
9710
        return get_config('assign', 'enabletimelimit') && !empty($instance->timelimit);
9711
    }
9712
 
9713
    /**
9714
     * Check if an assignment submission is already started and not yet submitted.
9715
     *
9716
     * @param int|null $userid User ID. If null, use global user.
9717
     * @param int $groupid Group ID. If 0, use user id to determine group.
9718
     * @param int $attemptnumber Attempt number. If -1, check latest submission.
9719
     * @return bool
9720
     */
1441 ariadna 9721
    public function is_attempt_in_progress(?int $userid = null, int $groupid = 0, int $attemptnumber = -1): bool {
1 efrain 9722
        if ($this->get_instance($userid)->teamsubmission) {
9723
            $submission = $this->get_group_submission($userid, $groupid, false, $attemptnumber);
9724
        } else {
9725
            $submission = $this->get_user_submission($userid, false, $attemptnumber);
9726
        }
9727
 
9728
        // If time limit is enabled, we only assume it is in progress if there is a start time for submission.
9729
        $timedattemptstarted = true;
9730
        if ($this->is_time_limit_enabled($userid)) {
9731
            $timedattemptstarted = !empty($submission) && !empty($submission->timestarted);
9732
        }
9733
 
9734
        return !empty($submission) && $submission->status !== ASSIGN_SUBMISSION_STATUS_SUBMITTED && $timedattemptstarted;
9735
    }
1441 ariadna 9736
 
9737
    /**
9738
     * Is "Allow partial release of grades while marking anonymously" enabled?
9739
     *
9740
     * @return bool
9741
     */
9742
    public function is_marking_anonymous(): bool {
9743
        return isset($this->get_instance()->markinganonymous) && $this->get_instance()->markinganonymous;
9744
    }
1 efrain 9745
}
9746
 
9747
/**
9748
 * Portfolio caller class for mod_assign.
9749
 *
9750
 * @package   mod_assign
9751
 * @copyright 2012 NetSpot {@link http://www.netspot.com.au}
9752
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
9753
 */
1441 ariadna 9754
class assign_portfolio_caller extends portfolio_module_caller_base {
1 efrain 9755
 
9756
    /** @var int callback arg - the id of submission we export */
9757
    protected $sid;
9758
 
9759
    /** @var string component of the submission files we export*/
9760
    protected $component;
9761
 
9762
    /** @var string callback arg - the area of submission files we export */
9763
    protected $area;
9764
 
9765
    /** @var int callback arg - the id of file we export */
9766
    protected $fileid;
9767
 
9768
    /** @var int callback arg - the cmid of the assignment we export */
9769
    protected $cmid;
9770
 
9771
    /** @var string callback arg - the plugintype of the editor we export */
9772
    protected $plugin;
9773
 
9774
    /** @var string callback arg - the name of the editor field we export */
9775
    protected $editor;
9776
 
9777
    /**
9778
     * Callback arg for a single file export.
9779
     */
1441 ariadna 9780
    public static function expected_callbackargs() {
1 efrain 9781
        return array(
9782
            'cmid' => true,
9783
            'sid' => false,
9784
            'area' => false,
9785
            'component' => false,
9786
            'fileid' => false,
9787
            'plugin' => false,
9788
            'editor' => false,
9789
        );
9790
    }
9791
 
9792
    /**
9793
     * The constructor.
9794
     *
9795
     * @param array $callbackargs
9796
     */
1441 ariadna 9797
    public function __construct($callbackargs) {
1 efrain 9798
        parent::__construct($callbackargs);
9799
        $this->cm = get_coursemodule_from_id('assign', $this->cmid, 0, false, MUST_EXIST);
9800
    }
9801
 
9802
    /**
9803
     * Load data needed for the portfolio export.
9804
     *
9805
     * If the assignment type implements portfolio_load_data(), the processing is delegated
9806
     * to it. Otherwise, the caller must provide either fileid (to export single file) or
9807
     * submissionid and filearea (to export all data attached to the given submission file area)
9808
     * via callback arguments.
9809
     *
9810
     * @throws     portfolio_caller_exception
9811
     */
1441 ariadna 9812
    public function load_data() {
1 efrain 9813
        global $DB;
9814
 
9815
        $context = context_module::instance($this->cmid);
9816
 
9817
        if (empty($this->fileid)) {
9818
            if (empty($this->sid) || empty($this->area)) {
9819
                throw new portfolio_caller_exception('invalidfileandsubmissionid', 'mod_assign');
9820
            }
9821
 
9822
            $submission = $DB->get_record('assign_submission', array('id' => $this->sid));
9823
        } else {
9824
            $submissionid = $DB->get_field('files', 'itemid', array('id' => $this->fileid, 'contextid' => $context->id));
9825
            if ($submissionid) {
9826
                $submission = $DB->get_record('assign_submission', array('id' => $submissionid));
9827
            }
9828
        }
9829
 
9830
        if (empty($submission)) {
9831
            throw new portfolio_caller_exception('filenotfound');
9832
        } else if ($submission->userid == 0) {
9833
            // This must be a group submission.
9834
            if (!groups_is_member($submission->groupid, $this->user->id)) {
9835
                throw new portfolio_caller_exception('filenotfound');
9836
            }
9837
        } else if ($this->user->id != $submission->userid) {
9838
            throw new portfolio_caller_exception('filenotfound');
9839
        }
9840
 
9841
        // Export either an area of files or a single file (see function for more detail).
9842
        // The first arg is an id or null. If it is an id, the rest of the args are ignored.
9843
        // If it is null, the rest of the args are used to load a list of files from get_areafiles.
1441 ariadna 9844
        $this->set_file_and_format_data($this->fileid,
9845
                                        $context->id,
9846
                                        $this->component,
9847
                                        $this->area,
9848
                                        $this->sid,
9849
                                        'timemodified',
9850
                                        false);
9851
 
1 efrain 9852
    }
9853
 
9854
    /**
9855
     * Prepares the package up before control is passed to the portfolio plugin.
9856
     *
9857
     * @throws portfolio_caller_exception
9858
     * @return mixed
9859
     */
1441 ariadna 9860
    public function prepare_package() {
1 efrain 9861
 
9862
        if ($this->plugin && $this->editor) {
9863
            $options = portfolio_format_text_options();
9864
            $context = context_module::instance($this->cmid);
9865
            $options->context = $context;
9866
 
9867
            $plugin = $this->get_submission_plugin();
9868
 
9869
            $text = $plugin->get_editor_text($this->editor, $this->sid);
9870
            $format = $plugin->get_editor_format($this->editor, $this->sid);
9871
 
9872
            $html = format_text($text, $format, $options);
1441 ariadna 9873
            $html = portfolio_rewrite_pluginfile_urls($html,
9874
                                                      $context->id,
9875
                                                      'mod_assign',
9876
                                                      $this->area,
9877
                                                      $this->sid,
9878
                                                      $this->exporter->get('format'));
1 efrain 9879
 
9880
            $exporterclass = $this->exporter->get('formatclass');
9881
            if (in_array($exporterclass, array(PORTFOLIO_FORMAT_PLAINHTML, PORTFOLIO_FORMAT_RICHHTML))) {
9882
                if ($files = $this->exporter->get('caller')->get('multifiles')) {
9883
                    foreach ($files as $file) {
9884
                        $this->exporter->copy_existing_file($file);
9885
                    }
9886
                }
9887
                return $this->exporter->write_new_file($html, 'assignment.html', !empty($files));
9888
            } else if ($this->exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) {
9889
                $leapwriter = $this->exporter->get('format')->leap2a_writer();
1441 ariadna 9890
                $entry = new portfolio_format_leap2a_entry($this->area . $this->cmid,
9891
                                                           $context->get_context_name(),
9892
                                                           'resource',
9893
                                                           $html);
1 efrain 9894
 
9895
                $entry->add_category('web', 'resource_type');
9896
                $entry->author = $this->user;
9897
                $leapwriter->add_entry($entry);
9898
                if ($files = $this->exporter->get('caller')->get('multifiles')) {
9899
                    $leapwriter->link_files($entry, $files, $this->area . $this->cmid . 'file');
9900
                    foreach ($files as $file) {
9901
                        $this->exporter->copy_existing_file($file);
9902
                    }
9903
                }
1441 ariadna 9904
                return $this->exporter->write_new_file($leapwriter->to_xml(),
9905
                                                       $this->exporter->get('format')->manifest_name(),
9906
                                                       true);
1 efrain 9907
            } else {
9908
                debugging('invalid format class: ' . $this->exporter->get('formatclass'));
9909
            }
1441 ariadna 9910
 
1 efrain 9911
        }
9912
 
9913
        if ($this->exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) {
9914
            $leapwriter = $this->exporter->get('format')->leap2a_writer();
9915
            $files = array();
9916
            if ($this->singlefile) {
9917
                $files[] = $this->singlefile;
9918
            } else if ($this->multifiles) {
9919
                $files = $this->multifiles;
9920
            } else {
1441 ariadna 9921
                throw new portfolio_caller_exception('invalidpreparepackagefile',
9922
                                                     'portfolio',
9923
                                                     $this->get_return_url());
1 efrain 9924
            }
9925
 
9926
            $entryids = array();
9927
            foreach ($files as $file) {
9928
                $entry = new portfolio_format_leap2a_file($file->get_filename(), $file);
9929
                $entry->author = $this->user;
9930
                $leapwriter->add_entry($entry);
9931
                $this->exporter->copy_existing_file($file);
9932
                $entryids[] = $entry->id;
9933
            }
9934
            if (count($files) > 1) {
9935
                $baseid = 'assign' . $this->cmid . $this->area;
9936
                $context = context_module::instance($this->cmid);
9937
 
9938
                // If we have multiple files, they should be grouped together into a folder.
1441 ariadna 9939
                $entry = new portfolio_format_leap2a_entry($baseid . 'group',
9940
                                                           $context->get_context_name(),
9941
                                                           'selection');
1 efrain 9942
                $leapwriter->add_entry($entry);
9943
                $leapwriter->make_selection($entry, $entryids, 'Folder');
9944
            }
1441 ariadna 9945
            return $this->exporter->write_new_file($leapwriter->to_xml(),
9946
                                                   $this->exporter->get('format')->manifest_name(),
9947
                                                   true);
1 efrain 9948
        }
9949
        return $this->prepare_package_file();
9950
    }
9951
 
9952
    /**
9953
     * Fetch the plugin by its type.
9954
     *
9955
     * @return assign_submission_plugin
9956
     */
1441 ariadna 9957
    protected function get_submission_plugin() {
1 efrain 9958
        global $CFG;
9959
        if (!$this->plugin || !$this->cmid) {
9960
            return null;
9961
        }
9962
 
9963
        require_once($CFG->dirroot . '/mod/assign/locallib.php');
9964
 
9965
        $context = context_module::instance($this->cmid);
9966
 
9967
        $assignment = new assign($context, null, null);
9968
        return $assignment->get_submission_plugin_by_type($this->plugin);
9969
    }
9970
 
9971
    /**
9972
     * Calculate a sha1 has of either a single file or a list
9973
     * of files based on the data set by load_data.
9974
     *
9975
     * @return string
9976
     */
1441 ariadna 9977
    public function get_sha1() {
1 efrain 9978
 
9979
        if ($this->plugin && $this->editor) {
9980
            $plugin = $this->get_submission_plugin();
9981
            $options = portfolio_format_text_options();
9982
            $options->context = context_module::instance($this->cmid);
9983
 
1441 ariadna 9984
            $text = format_text($plugin->get_editor_text($this->editor, $this->sid),
9985
                                $plugin->get_editor_format($this->editor, $this->sid),
9986
                                $options);
1 efrain 9987
            $textsha1 = sha1($text);
9988
            $filesha1 = '';
9989
            try {
9990
                $filesha1 = $this->get_sha1_file();
9991
            } catch (portfolio_caller_exception $e) {
9992
                // No files.
9993
            }
9994
            return sha1($textsha1 . $filesha1);
9995
        }
9996
        return $this->get_sha1_file();
9997
    }
9998
 
9999
    /**
10000
     * Calculate the time to transfer either a single file or a list
10001
     * of files based on the data set by load_data.
10002
     *
10003
     * @return int
10004
     */
1441 ariadna 10005
    public function expected_time() {
1 efrain 10006
        return $this->expected_time_file();
10007
    }
10008
 
10009
    /**
10010
     * Checking the permissions.
10011
     *
10012
     * @return bool
10013
     */
1441 ariadna 10014
    public function check_permissions() {
1 efrain 10015
        $context = context_module::instance($this->cmid);
10016
        return has_capability('mod/assign:exportownsubmission', $context);
10017
    }
10018
 
10019
    /**
10020
     * Display a module name.
10021
     *
10022
     * @return string
10023
     */
1441 ariadna 10024
    public static function display_name() {
1 efrain 10025
        return get_string('modulename', 'assign');
10026
    }
10027
 
10028
    /**
10029
     * Return array of formats supported by this portfolio call back.
10030
     *
10031
     * @return array
10032
     */
1441 ariadna 10033
    public static function base_supported_formats() {
1 efrain 10034
        return array(PORTFOLIO_FORMAT_FILE, PORTFOLIO_FORMAT_LEAP2A);
10035
    }
10036
}
10037
 
10038
/**
10039
 * Logic to happen when a/some group(s) has/have been deleted in a course.
10040
 *
10041
 * @param int $courseid The course ID.
10042
 * @param int $groupid The group id if it is known
10043
 * @return void
10044
 */
1441 ariadna 10045
function assign_process_group_deleted_in_course($courseid, $groupid = null) {
1 efrain 10046
    global $DB;
10047
 
10048
    $params = array('courseid' => $courseid);
10049
    if ($groupid) {
10050
        $params['groupid'] = $groupid;
10051
        // We just update the group that was deleted.
10052
        $sql = "SELECT o.id, o.assignid, o.groupid
10053
                  FROM {assign_overrides} o
10054
                  JOIN {assign} assign ON assign.id = o.assignid
10055
                 WHERE assign.course = :courseid
10056
                   AND o.groupid = :groupid";
10057
    } else {
10058
        // No groupid, we update all orphaned group overrides for all assign in course.
10059
        $sql = "SELECT o.id, o.assignid, o.groupid
10060
                  FROM {assign_overrides} o
10061
                  JOIN {assign} assign ON assign.id = o.assignid
10062
             LEFT JOIN {groups} grp ON grp.id = o.groupid
10063
                 WHERE assign.course = :courseid
10064
                   AND o.groupid IS NOT NULL
10065
                   AND grp.id IS NULL";
10066
    }
10067
    $records = $DB->get_records_sql($sql, $params);
10068
    if (!$records) {
10069
        return; // Nothing to do.
10070
    }
10071
    $DB->delete_records_list('assign_overrides', 'id', array_keys($records));
10072
    $cache = cache::make('mod_assign', 'overrides');
10073
    foreach ($records as $record) {
10074
        $cache->delete("{$record->assignid}_g_{$record->groupid}");
10075
    }
10076
}
10077
 
10078
/**
10079
 * Change the sort order of an override
10080
 *
10081
 * @param int $id of the override
10082
 * @param string $move direction of move
10083
 * @param int $assignid of the assignment
10084
 * @return bool success of operation
10085
 */
1441 ariadna 10086
function move_group_override($id, $move, $assignid) {
1 efrain 10087
    global $DB;
10088
 
10089
    // Get the override object.
10090
    if (!$override = $DB->get_record('assign_overrides', ['id' => $id, 'assignid' => $assignid], 'id, sortorder, groupid')) {
10091
        return false;
10092
    }
10093
    // Count the number of group overrides.
10094
    $overridecountgroup = $DB->count_records('assign_overrides', array('userid' => null, 'assignid' => $assignid));
10095
 
10096
    // Calculate the new sortorder.
1441 ariadna 10097
    if ( ($move == 'up') and ($override->sortorder > 1)) {
1 efrain 10098
        $neworder = $override->sortorder - 1;
10099
    } else if (($move == 'down') and ($override->sortorder < $overridecountgroup)) {
10100
        $neworder = $override->sortorder + 1;
10101
    } else {
10102
        return false;
10103
    }
10104
 
10105
    // Retrieve the override object that is currently residing in the new position.
10106
    $params = ['sortorder' => $neworder, 'assignid' => $assignid];
10107
    if ($swapoverride = $DB->get_record('assign_overrides', $params, 'id, sortorder, groupid')) {
10108
 
10109
        // Swap the sortorders.
10110
        $swapoverride->sortorder = $override->sortorder;
10111
        $override->sortorder     = $neworder;
10112
 
10113
        // Update the override records.
10114
        $DB->update_record('assign_overrides', $override);
10115
        $DB->update_record('assign_overrides', $swapoverride);
10116
 
10117
        // Delete cache for the 2 records we updated above.
10118
        $cache = cache::make('mod_assign', 'overrides');
10119
        $cache->delete("{$assignid}_g_{$override->groupid}");
10120
        $cache->delete("{$assignid}_g_{$swapoverride->groupid}");
10121
    }
10122
 
10123
    reorder_group_overrides($assignid);
10124
    return true;
10125
}
10126
 
10127
/**
10128
 * Reorder the overrides starting at the override at the given startorder.
10129
 *
10130
 * @param int $assignid of the assigment
10131
 */
1441 ariadna 10132
function reorder_group_overrides($assignid) {
1 efrain 10133
    global $DB;
10134
 
10135
    $i = 1;
10136
    if ($overrides = $DB->get_records('assign_overrides', array('userid' => null, 'assignid' => $assignid), 'sortorder ASC')) {
10137
        $cache = cache::make('mod_assign', 'overrides');
10138
        foreach ($overrides as $override) {
10139
            $f = new stdClass();
10140
            $f->id = $override->id;
10141
            $f->sortorder = $i++;
10142
            $DB->update_record('assign_overrides', $f);
10143
            $cache->delete("{$assignid}_g_{$override->groupid}");
10144
 
10145
            // Update priorities of group overrides.
10146
            $params = [
10147
                'modulename' => 'assign',
10148
                'instance' => $override->assignid,
10149
                'groupid' => $override->groupid
10150
            ];
10151
            $DB->set_field('event', 'priority', $f->sortorder, $params);
10152
        }
10153
    }
10154
}
10155
 
10156
/**
10157
 * Get the information about the standard assign JavaScript module.
10158
 * @return array a standard jsmodule structure.
10159
 */
1441 ariadna 10160
function assign_get_js_module() {
1 efrain 10161
    return array(
10162
        'name' => 'mod_assign',
10163
        'fullpath' => '/mod/assign/module.js',
10164
    );
10165
}