Proyectos de Subversion Moodle

Rev

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