Proyectos de Subversion Moodle

Rev

Rev 1 | Rev 1255 | Ir a la última revisión | | 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.
1254 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);
1 efrain 4870
        if ($showquickgrading && $quickgrading) {
4871
            $table = $this->get_renderer()->render($gradingtable);
4872
            $page = optional_param('page', null, PARAM_INT);
1254 ariadna 4873
            $quickformparams = array(
4874
                'cm' => $this->get_course_module()->id,
4875
                'gradingtable' => $table,
4876
                'sendstudentnotifications' => $this->get_instance()->sendstudentnotifications,
4877
                'page' => $page
4878
            );
4879
            $gradingtable = new assign_grading_table($this, $perpage, $filter, 0, true);
1 efrain 4880
            $quickgradingform = new mod_assign_quick_grading_form(null, $quickformparams);
4881
 
4882
            $o .= $this->get_renderer()->render(new assign_form('quickgradingform', $quickgradingform));
4883
        } else {
4884
            $gradingtable = new assign_grading_table($this, $perpage, $filter, 0, false);
4885
            $o .= $this->get_renderer()->render($gradingtable);
4886
        }
4887
 
4888
        if ($this->can_grade()) {
4889
            // We need to store the order of uses in the table as the person may wish to grade them.
4890
            // This is done based on the row number of the user.
4891
            $useridlist = $gradingtable->get_column_data('userid');
4892
            $SESSION->mod_assign_useridlist[$this->get_useridlist_key()] = $useridlist;
4893
        }
4894
 
4895
        $currentgroup = groups_get_activity_group($this->get_course_module(), true);
4896
        $users = array_keys($this->list_participants($currentgroup, true));
4897
        if (count($users) != 0 && $this->can_grade()) {
4898
            // If no enrolled user in a course then don't display the batch operations feature.
4899
            $assignform = new assign_form('gradingbatchoperationsform', $gradingbatchoperationsform);
4900
            $o .= $this->get_renderer()->render($assignform);
4901
        }
4902
        return $o;
4903
    }
4904
 
4905
    /**
4906
     * View entire grader app.
4907
     *
4908
     * @return string
4909
     */
1254 ariadna 4910
    protected function view_grader()
4911
    {
1 efrain 4912
        global $USER, $PAGE;
4913
 
4914
        $o = '';
4915
        // Need submit permission to submit an assignment.
4916
        $this->require_view_grades();
4917
 
4918
        $PAGE->set_pagelayout('embedded');
4919
 
4920
        $PAGE->activityheader->disable();
4921
 
4922
        $courseshortname = $this->get_context()->get_course_context()->get_context_name(false, true);
4923
        $args = [
4924
            'contextname' => $this->get_context()->get_context_name(false, true),
4925
            'subpage' => get_string('grading', 'assign')
4926
        ];
4927
        $title = get_string('subpagetitle', 'assign', $args);
4928
        $title = $courseshortname . ': ' . $title;
4929
        $PAGE->set_title($title);
4930
 
4931
        $o .= $this->get_renderer()->header();
4932
 
4933
        $userid = optional_param('userid', 0, PARAM_INT);
4934
        $blindid = optional_param('blindid', 0, PARAM_INT);
4935
 
4936
        if (!$userid && $blindid) {
4937
            $userid = $this->get_user_id_for_uniqueid($blindid);
4938
        }
4939
 
4940
        // Instantiate table object to apply table preferences.
4941
        $gradingtable = new assign_grading_table($this, 10, '', 0, false);
4942
        $gradingtable->setup();
4943
 
4944
        $currentgroup = groups_get_activity_group($this->get_course_module(), true);
4945
        $framegrader = new grading_app($userid, $currentgroup, $this);
4946
 
4947
        $this->update_effective_access($userid);
4948
 
4949
        $o .= $this->get_renderer()->render($framegrader);
4950
 
4951
        $o .= $this->view_footer();
4952
 
4953
        \mod_assign\event\grading_table_viewed::create_from_assign($this)->trigger();
4954
 
4955
        return $o;
4956
    }
4957
    /**
4958
     * View entire grading page.
4959
     *
4960
     * @return string
4961
     */
1254 ariadna 4962
    protected function view_grading_page()
4963
    {
1 efrain 4964
        global $CFG;
4965
 
4966
        $o = '';
4967
        // Need submit permission to submit an assignment.
4968
        $this->require_view_grades();
4969
        require_once($CFG->dirroot . '/mod/assign/gradeform.php');
4970
 
4971
        $this->add_grade_notices();
4972
 
4973
        // Only load this if it is.
4974
        $o .= $this->view_grading_table();
4975
 
4976
        $o .= $this->view_footer();
4977
 
4978
        \mod_assign\event\grading_table_viewed::create_from_assign($this)->trigger();
4979
 
4980
        return $o;
4981
    }
4982
 
4983
    /**
4984
     * Capture the output of the plagiarism plugins disclosures and return it as a string.
4985
     *
4986
     * @return string
4987
     */
1254 ariadna 4988
    protected function plagiarism_print_disclosure()
4989
    {
1 efrain 4990
        global $CFG;
4991
        $o = '';
4992
 
4993
        if (!empty($CFG->enableplagiarism)) {
4994
            require_once($CFG->libdir . '/plagiarismlib.php');
4995
 
4996
            $o .= plagiarism_print_disclosure($this->get_course_module()->id);
4997
        }
4998
 
4999
        return $o;
5000
    }
5001
 
5002
    /**
5003
     * Message for students when assignment submissions have been closed.
5004
     *
5005
     * @param string $title The page title
5006
     * @param array $notices The array of notices to show.
5007
     * @return string
5008
     */
1254 ariadna 5009
    protected function view_notices($title, $notices)
5010
    {
1 efrain 5011
        global $CFG;
5012
 
5013
        $o = '';
5014
 
1254 ariadna 5015
        $header = new assign_header(
5016
            $this->get_instance(),
5017
            $this->get_context(),
5018
            $this->show_intro(),
5019
            $this->get_course_module()->id,
5020
            $title
5021
        );
1 efrain 5022
        $o .= $this->get_renderer()->render($header);
5023
 
5024
        foreach ($notices as $notice) {
5025
            $o .= $this->get_renderer()->notification($notice);
5026
        }
5027
 
1254 ariadna 5028
        $url = new moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id, 'action' => 'view'));
1 efrain 5029
        $o .= $this->get_renderer()->continue_button($url);
5030
 
5031
        $o .= $this->view_footer();
5032
 
5033
        return $o;
5034
    }
5035
 
5036
    /**
5037
     * Get the name for a user - hiding their real name if blind marking is on.
5038
     *
5039
     * @param stdClass $user The user record as required by fullname()
5040
     * @return string The name.
5041
     */
1254 ariadna 5042
    public function fullname($user)
5043
    {
1 efrain 5044
        if ($this->is_blind_marking()) {
5045
            $hasviewblind = has_capability('mod/assign:viewblinddetails', $this->get_context());
5046
            if (empty($user->recordid)) {
5047
                $uniqueid = $this->get_uniqueid_for_user($user->id);
5048
            } else {
5049
                $uniqueid = $user->recordid;
5050
            }
5051
            if ($hasviewblind) {
5052
                return get_string('participant', 'assign') . ' ' . $uniqueid . ' (' .
1254 ariadna 5053
                    fullname($user, has_capability('moodle/site:viewfullnames', $this->get_context())) . ')';
1 efrain 5054
            } else {
5055
                return get_string('participant', 'assign') . ' ' . $uniqueid;
5056
            }
5057
        } else {
5058
            return fullname($user, has_capability('moodle/site:viewfullnames', $this->get_context()));
5059
        }
5060
    }
5061
 
5062
    /**
5063
     * View edit submissions page.
5064
     *
5065
     * @param moodleform $mform
5066
     * @param array $notices A list of notices to display at the top of the
5067
     *                       edit submission form (e.g. from plugins).
5068
     * @return string The page output.
5069
     */
1254 ariadna 5070
    protected function view_edit_submission_page($mform, $notices)
5071
    {
1 efrain 5072
        global $CFG, $USER, $DB, $PAGE;
5073
 
5074
        $o = '';
5075
        require_once($CFG->dirroot . '/mod/assign/submission_form.php');
5076
        // Need submit permission to submit an assignment.
5077
        $userid = optional_param('userid', $USER->id, PARAM_INT);
1254 ariadna 5078
        $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
1 efrain 5079
        $timelimitenabled = get_config('assign', 'enabletimelimit');
5080
 
5081
        // This variation on the url will link direct to this student.
5082
        // The benefit is the url will be the same every time for this student, so Atto autosave drafts can match up.
5083
        $returnparams = array('userid' => $userid, 'rownum' => 0, 'useridlistid' => 0);
5084
        $this->register_return_link('editsubmission', $returnparams);
5085
 
5086
        if ($userid == $USER->id) {
5087
            if (!$this->can_edit_submission($userid, $USER->id)) {
5088
                throw new \moodle_exception('nopermission');
5089
            }
5090
            // User is editing their own submission.
5091
            require_capability('mod/assign:submit', $this->context);
5092
            $title = get_string('editsubmission', 'assign');
5093
        } else {
5094
            // User is editing another user's submission.
5095
            if (!$this->can_edit_submission($userid, $USER->id)) {
5096
                throw new \moodle_exception('nopermission');
5097
            }
5098
 
5099
            $name = $this->fullname($user);
5100
            $title = get_string('editsubmissionother', 'assign', $name);
5101
        }
5102
 
5103
        if (!$this->submissions_open($userid)) {
5104
            $message = array(get_string('submissionsclosed', 'assign'));
5105
            return $this->view_notices($title, $message);
5106
        }
5107
 
5108
        $postfix = '';
5109
        if ($this->has_visible_attachments()) {
5110
            $postfix = $this->render_area_files('mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0);
5111
        }
5112
 
5113
        $data = new stdClass();
5114
        $data->userid = $userid;
5115
        if (!$mform) {
5116
            $mform = new mod_assign_submission_form(null, array($this, $data));
5117
        }
5118
 
5119
        if ($this->get_instance()->teamsubmission) {
5120
            $submission = $this->get_group_submission($userid, 0, false);
5121
        } else {
5122
            $submission = $this->get_user_submission($userid, false);
5123
        }
5124
 
5125
        if ($timelimitenabled && !empty($submission->timestarted) && $this->get_instance()->timelimit) {
5126
            $navbc = $this->get_timelimit_panel($submission);
5127
            $regions = $PAGE->blocks->get_regions();
5128
            $bc = new \block_contents();
5129
            $bc->attributes['id'] = 'mod_assign_timelimit_block';
5130
            $bc->attributes['role'] = 'navigation';
5131
            $bc->attributes['aria-labelledby'] = 'mod_assign_timelimit_block_title';
5132
            $bc->title = get_string('assigntimeleft', 'assign');
5133
            $bc->content = $navbc;
5134
            $PAGE->blocks->add_fake_block($bc, reset($regions));
5135
        }
5136
 
5137
        $o .= $this->get_renderer()->render(
1254 ariadna 5138
            new assign_header(
5139
                $this->get_instance(),
5140
                $this->get_context(),
5141
                $this->show_intro(),
5142
                $this->get_course_module()->id,
5143
                $title,
5144
                '',
5145
                $postfix,
5146
                null,
5147
                true
1 efrain 5148
            )
5149
        );
5150
 
5151
        // Show plagiarism disclosure for any user submitter.
5152
        $o .= $this->plagiarism_print_disclosure();
5153
 
5154
        foreach ($notices as $notice) {
5155
            $o .= $this->get_renderer()->notification($notice);
5156
        }
5157
 
5158
        $o .= $this->get_renderer()->render(new assign_form('editsubmissionform', $mform));
5159
        $o .= $this->view_footer();
5160
 
5161
        \mod_assign\event\submission_form_viewed::create_from_user($this, $user)->trigger();
5162
 
5163
        return $o;
5164
    }
5165
 
5166
    /**
5167
     * Get the time limit panel object for this submission attempt.
5168
     *
5169
     * @param stdClass $submission assign submission.
5170
     * @return string the panel output.
5171
     */
1254 ariadna 5172
    public function get_timelimit_panel(stdClass $submission): string
5173
    {
1 efrain 5174
        global $USER;
5175
 
5176
        // Apply overrides.
5177
        $this->update_effective_access($USER->id);
5178
        $panel = new timelimit_panel($submission, $this->get_instance());
5179
        return $this->get_renderer()->render($panel);
5180
    }
5181
 
5182
    /**
5183
     * See if this assignment has a grade yet.
5184
     *
5185
     * @param int $userid
5186
     * @return bool
5187
     */
1254 ariadna 5188
    protected function is_graded($userid)
5189
    {
1 efrain 5190
        $grade = $this->get_user_grade($userid, false);
5191
        if ($grade) {
5192
            return ($grade->grade !== null && $grade->grade >= 0);
5193
        }
5194
        return false;
5195
    }
5196
 
5197
    /**
5198
     * Perform an access check to see if the current $USER can edit this group submission.
5199
     *
5200
     * @param int $groupid
5201
     * @return bool
5202
     */
1254 ariadna 5203
    public function can_edit_group_submission($groupid)
5204
    {
1 efrain 5205
        global $USER;
5206
 
5207
        $members = $this->get_submission_group_members($groupid, true);
5208
        foreach ($members as $member) {
5209
            // If we can edit any members submission, we can edit the submission for the group.
5210
            if ($this->can_edit_submission($member->id)) {
5211
                return true;
5212
            }
5213
        }
5214
        return false;
5215
    }
5216
 
5217
    /**
5218
     * Perform an access check to see if the current $USER can view this group submission.
5219
     *
5220
     * @param int $groupid
5221
     * @return bool
5222
     */
1254 ariadna 5223
    public function can_view_group_submission($groupid)
5224
    {
1 efrain 5225
        global $USER;
5226
 
5227
        $members = $this->get_submission_group_members($groupid, true);
5228
        foreach ($members as $member) {
5229
            // If we can view any members submission, we can view the submission for the group.
5230
            if ($this->can_view_submission($member->id)) {
5231
                return true;
5232
            }
5233
        }
5234
        return false;
5235
    }
5236
 
5237
    /**
5238
     * Perform an access check to see if the current $USER can view this users submission.
5239
     *
5240
     * @param int $userid
5241
     * @return bool
5242
     */
1254 ariadna 5243
    public function can_view_submission($userid)
5244
    {
1 efrain 5245
        global $USER;
5246
 
5247
        if (!$this->is_active_user($userid) && !has_capability('moodle/course:viewsuspendedusers', $this->context)) {
5248
            return false;
5249
        }
5250
        if (!is_enrolled($this->get_course_context(), $userid)) {
5251
            return false;
5252
        }
5253
        if (has_any_capability(array('mod/assign:viewgrades', 'mod/assign:grade'), $this->context)) {
5254
            return true;
5255
        }
5256
        if ($userid == $USER->id) {
5257
            return true;
5258
        }
5259
        return false;
5260
    }
5261
 
5262
    /**
5263
     * Allows the plugin to show a batch grading operation page.
5264
     *
5265
     * @param moodleform $mform
5266
     * @return none
5267
     */
1254 ariadna 5268
    protected function view_plugin_grading_batch_operation($mform)
5269
    {
1 efrain 5270
        require_capability('mod/assign:grade', $this->context);
5271
        $prefix = 'plugingradingbatchoperation_';
5272
 
5273
        if ($data = $mform->get_data()) {
5274
            $tail = substr($data->operation, strlen($prefix));
5275
            list($plugintype, $action) = explode('_', $tail, 2);
5276
 
5277
            $plugin = $this->get_feedback_plugin_by_type($plugintype);
5278
            if ($plugin) {
5279
                $users = $data->selectedusers;
5280
                $userlist = explode(',', $users);
5281
                echo $plugin->grading_batch_operation($action, $userlist);
5282
                return;
5283
            }
5284
        }
5285
        throw new \moodle_exception('invalidformdata', '');
5286
    }
5287
 
5288
    /**
5289
     * Ask the user to confirm they want to perform this batch operation
5290
     *
5291
     * @param moodleform $mform Set to a grading batch operations form
5292
     * @return string - the page to view after processing these actions
5293
     */
1254 ariadna 5294
    protected function process_grading_batch_operation(&$mform)
5295
    {
1 efrain 5296
        global $CFG;
5297
        require_once($CFG->dirroot . '/mod/assign/gradingbatchoperationsform.php');
5298
        require_sesskey();
5299
 
5300
        $markingallocation = $this->get_instance()->markingworkflow &&
5301
            $this->get_instance()->markingallocation &&
5302
            has_capability('mod/assign:manageallocations', $this->context);
5303
 
1254 ariadna 5304
        $batchformparams = array(
5305
            'cm' => $this->get_course_module()->id,
5306
            'submissiondrafts' => $this->get_instance()->submissiondrafts,
5307
            'duedate' => $this->get_instance()->duedate,
5308
            'attemptreopenmethod' => $this->get_instance()->attemptreopenmethod,
5309
            'feedbackplugins' => $this->get_feedback_plugins(),
5310
            'context' => $this->get_context(),
5311
            'markingworkflow' => $this->get_instance()->markingworkflow,
5312
            'markingallocation' => $markingallocation
5313
        );
1 efrain 5314
        $formclasses = [
5315
            'class' => 'gradingbatchoperationsform',
5316
            'data-double-submit-protection' => 'off'
5317
        ];
5318
 
1254 ariadna 5319
        $mform = new mod_assign_grading_batch_operations_form(
5320
            null,
5321
            $batchformparams,
5322
            'post',
5323
            '',
5324
            $formclasses
5325
        );
1 efrain 5326
 
5327
        if ($data = $mform->get_data()) {
5328
            // Get the list of users.
5329
            $users = $data->selectedusers;
5330
            $userlist = explode(',', $users);
5331
 
5332
            $prefix = 'plugingradingbatchoperation_';
5333
 
5334
            if ($data->operation == 'grantextension') {
5335
                // Reset the form so the grant extension page will create the extension form.
5336
                $mform = null;
5337
                return 'grantextension';
5338
            } else if ($data->operation == 'setmarkingworkflowstate') {
5339
                return 'viewbatchsetmarkingworkflowstate';
5340
            } else if ($data->operation == 'setmarkingallocation') {
5341
                return 'viewbatchmarkingallocation';
5342
            } else if (strpos($data->operation, $prefix) === 0) {
5343
                $tail = substr($data->operation, strlen($prefix));
5344
                list($plugintype, $action) = explode('_', $tail, 2);
5345
 
5346
                $plugin = $this->get_feedback_plugin_by_type($plugintype);
5347
                if ($plugin) {
5348
                    return 'plugingradingbatchoperation';
5349
                }
5350
            }
5351
 
5352
            if ($data->operation == 'downloadselected') {
5353
                $this->download_submissions($userlist);
5354
            } else {
5355
                foreach ($userlist as $userid) {
5356
                    if ($data->operation == 'lock') {
5357
                        $this->process_lock_submission($userid);
5358
                    } else if ($data->operation == 'unlock') {
5359
                        $this->process_unlock_submission($userid);
5360
                    } else if ($data->operation == 'reverttodraft') {
5361
                        $this->process_revert_to_draft($userid);
5362
                    } else if ($data->operation == 'removesubmission') {
5363
                        $this->process_remove_submission($userid);
5364
                    } else if ($data->operation == 'addattempt') {
5365
                        if (!$this->get_instance()->teamsubmission) {
5366
                            $this->process_add_attempt($userid);
5367
                        }
5368
                    }
5369
                }
5370
            }
5371
            if ($this->get_instance()->teamsubmission && $data->operation == 'addattempt') {
5372
                // This needs to be handled separately so that each team submission is only re-opened one time.
5373
                $this->process_add_attempt_group($userlist);
5374
            }
5375
        }
5376
 
5377
        return 'grading';
5378
    }
5379
 
5380
    /**
5381
     * Shows a form that allows the workflow state for selected submissions to be changed.
5382
     *
5383
     * @param moodleform $mform Set to a grading batch operations form
5384
     * @return string - the page to view after processing these actions
5385
     */
1254 ariadna 5386
    protected function view_batch_set_workflow_state($mform)
5387
    {
1 efrain 5388
        global $CFG, $DB;
5389
 
5390
        require_once($CFG->dirroot . '/mod/assign/batchsetmarkingworkflowstateform.php');
5391
 
5392
        $o = '';
5393
 
5394
        $submitteddata = $mform->get_data();
5395
        $users = $submitteddata->selectedusers;
5396
        $userlist = explode(',', $users);
5397
 
1254 ariadna 5398
        $formdata = array(
5399
            'id' => $this->get_course_module()->id,
5400
            'selectedusers' => $users
5401
        );
1 efrain 5402
 
5403
        $usershtml = '';
5404
 
5405
        $usercount = 0;
5406
        // TODO Does not support custom user profile fields (MDL-70456).
5407
        $extrauserfields = \core_user\fields::get_identity_fields($this->get_context(), false);
5408
        $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
5409
        foreach ($userlist as $userid) {
5410
            if ($usercount >= 5) {
5411
                $usershtml .= get_string('moreusers', 'assign', count($userlist) - 5);
5412
                break;
5413
            }
1254 ariadna 5414
            $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
1 efrain 5415
 
1254 ariadna 5416
            $usershtml .= $this->get_renderer()->render(new assign_user_summary(
5417
                $user,
5418
                $this->get_course()->id,
5419
                $viewfullnames,
5420
                $this->is_blind_marking(),
5421
                $this->get_uniqueid_for_user($user->id),
5422
                $extrauserfields,
5423
                !$this->is_active_user($userid)
5424
            ));
1 efrain 5425
            $usercount += 1;
5426
        }
5427
 
5428
        $formparams = array(
5429
            'userscount' => count($userlist),
5430
            'usershtml' => $usershtml,
5431
            'markingworkflowstates' => $this->get_marking_workflow_states_for_current_user()
5432
        );
5433
 
5434
        $mform = new mod_assign_batch_set_marking_workflow_state_form(null, $formparams);
5435
        $mform->set_data($formdata);    // Initialises the hidden elements.
1254 ariadna 5436
        $header = new assign_header(
5437
            $this->get_instance(),
1 efrain 5438
            $this->get_context(),
5439
            $this->show_intro(),
5440
            $this->get_course_module()->id,
1254 ariadna 5441
            get_string('setmarkingworkflowstate', 'assign')
5442
        );
1 efrain 5443
        $o .= $this->get_renderer()->render($header);
5444
        $o .= $this->get_renderer()->render(new assign_form('setworkflowstate', $mform));
5445
        $o .= $this->view_footer();
5446
 
5447
        \mod_assign\event\batch_set_workflow_state_viewed::create_from_assign($this)->trigger();
5448
 
5449
        return $o;
5450
    }
5451
 
5452
    /**
5453
     * Shows a form that allows the allocated marker for selected submissions to be changed.
5454
     *
5455
     * @param moodleform $mform Set to a grading batch operations form
5456
     * @return string - the page to view after processing these actions
5457
     */
1254 ariadna 5458
    public function view_batch_markingallocation($mform)
5459
    {
1 efrain 5460
        global $CFG, $DB;
5461
 
5462
        require_once($CFG->dirroot . '/mod/assign/batchsetallocatedmarkerform.php');
5463
 
5464
        $o = '';
5465
 
5466
        $submitteddata = $mform->get_data();
5467
        $users = $submitteddata->selectedusers;
5468
        $userlist = explode(',', $users);
5469
 
1254 ariadna 5470
        $formdata = array(
5471
            'id' => $this->get_course_module()->id,
5472
            'selectedusers' => $users
5473
        );
1 efrain 5474
 
5475
        $usershtml = '';
5476
 
5477
        $usercount = 0;
5478
        // TODO Does not support custom user profile fields (MDL-70456).
5479
        $extrauserfields = \core_user\fields::get_identity_fields($this->get_context(), false);
5480
        $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
5481
        foreach ($userlist as $userid) {
5482
            if ($usercount >= 5) {
5483
                $usershtml .= get_string('moreusers', 'assign', count($userlist) - 5);
5484
                break;
5485
            }
1254 ariadna 5486
            $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
1 efrain 5487
 
1254 ariadna 5488
            $usershtml .= $this->get_renderer()->render(new assign_user_summary(
5489
                $user,
1 efrain 5490
                $this->get_course()->id,
5491
                $viewfullnames,
5492
                $this->is_blind_marking(),
5493
                $this->get_uniqueid_for_user($user->id),
5494
                $extrauserfields,
1254 ariadna 5495
                !$this->is_active_user($userid)
5496
            ));
1 efrain 5497
            $usercount += 1;
5498
        }
5499
 
5500
        $formparams = array(
5501
            'userscount' => count($userlist),
5502
            'usershtml' => $usershtml,
5503
        );
5504
 
5505
        list($sort, $params) = users_order_by_sql('u');
5506
        // Only enrolled users could be assigned as potential markers.
5507
        $markers = get_enrolled_users($this->get_context(), 'mod/assign:grade', 0, 'u.*', $sort);
5508
        $markerlist = array();
5509
        foreach ($markers as $marker) {
5510
            $markerlist[$marker->id] = fullname($marker);
5511
        }
5512
 
5513
        $formparams['markers'] = $markerlist;
5514
 
5515
        $mform = new mod_assign_batch_set_allocatedmarker_form(null, $formparams);
5516
        $mform->set_data($formdata);    // Initialises the hidden elements.
1254 ariadna 5517
        $header = new assign_header(
5518
            $this->get_instance(),
1 efrain 5519
            $this->get_context(),
5520
            $this->show_intro(),
5521
            $this->get_course_module()->id,
1254 ariadna 5522
            get_string('setmarkingallocation', 'assign')
5523
        );
1 efrain 5524
        $o .= $this->get_renderer()->render($header);
5525
        $o .= $this->get_renderer()->render(new assign_form('setworkflowstate', $mform));
5526
        $o .= $this->view_footer();
5527
 
5528
        \mod_assign\event\batch_set_marker_allocation_viewed::create_from_assign($this)->trigger();
5529
 
5530
        return $o;
5531
    }
5532
 
5533
    /**
5534
     * Ask the user to confirm they want to submit their work for grading.
5535
     *
5536
     * @param moodleform $mform - null unless form validation has failed
5537
     * @return string
5538
     */
1254 ariadna 5539
    protected function check_submit_for_grading($mform)
5540
    {
1 efrain 5541
        global $USER, $CFG;
5542
 
5543
        require_once($CFG->dirroot . '/mod/assign/submissionconfirmform.php');
5544
 
5545
        // Check that all of the submission plugins are ready for this submission.
5546
        // Also check whether there is something to be submitted as well against atleast one.
5547
        $notifications = array();
5548
        $submission = $this->get_user_submission($USER->id, false);
5549
        if ($this->get_instance()->teamsubmission) {
5550
            $submission = $this->get_group_submission($USER->id, 0, false);
5551
        }
5552
 
5553
        $plugins = $this->get_submission_plugins();
5554
        $hassubmission = false;
5555
        foreach ($plugins as $plugin) {
5556
            if ($plugin->is_enabled() && $plugin->is_visible()) {
5557
                $check = $plugin->precheck_submission($submission);
5558
                if ($check !== true) {
5559
                    $notifications[] = $check;
5560
                }
5561
 
5562
                if (is_object($submission) && !$plugin->is_empty($submission)) {
5563
                    $hassubmission = true;
5564
                }
5565
            }
5566
        }
5567
 
5568
        // If there are no submissions and no existing notifications to be displayed the stop.
5569
        if (!$hassubmission && !$notifications) {
5570
            $notifications[] = get_string('addsubmission_help', 'assign');
5571
        }
5572
 
5573
        $data = new stdClass();
5574
        $adminconfig = $this->get_admin_config();
5575
        $requiresubmissionstatement = $this->get_instance()->requiresubmissionstatement;
5576
        $submissionstatement = '';
5577
 
5578
        if ($requiresubmissionstatement) {
5579
            $submissionstatement = $this->get_submissionstatement($adminconfig, $this->get_instance(), $this->get_context());
5580
        }
5581
 
5582
        // If we get back an empty submission statement, we have to set $requiredsubmisisonstatement to false to prevent
5583
        // that the submission statement checkbox will be displayed.
5584
        if (empty($submissionstatement)) {
5585
            $requiresubmissionstatement = false;
5586
        }
5587
 
5588
        if ($mform == null) {
1254 ariadna 5589
            $mform = new mod_assign_confirm_submission_form(null, array(
5590
                $requiresubmissionstatement,
5591
                $submissionstatement,
5592
                $this->get_course_module()->id,
5593
                $data
5594
            ));
1 efrain 5595
        }
5596
        $o = '';
1254 ariadna 5597
        $o .= $this->get_renderer()->render(new assign_header(
5598
            $this->get_instance(),
5599
            $this->get_context(),
5600
            $this->show_intro(),
5601
            $this->get_course_module()->id,
5602
            get_string('confirmsubmissionheading', 'assign')
5603
        ));
5604
        $submitforgradingpage = new assign_submit_for_grading_page(
5605
            $notifications,
5606
            $this->get_course_module()->id,
5607
            $mform
5608
        );
1 efrain 5609
        $o .= $this->get_renderer()->render($submitforgradingpage);
5610
        $o .= $this->view_footer();
5611
 
5612
        \mod_assign\event\submission_confirmation_form_viewed::create_from_assign($this)->trigger();
5613
 
5614
        return $o;
5615
    }
5616
 
5617
    /**
5618
     * Creates an assign_submission_status renderable.
5619
     *
5620
     * @param stdClass $user the user to get the report for
5621
     * @param bool $showlinks return plain text or links to the profile
5622
     * @return assign_submission_status renderable object
5623
     */
1254 ariadna 5624
    public function get_assign_submission_status_renderable($user, $showlinks)
5625
    {
1 efrain 5626
        global $PAGE;
5627
 
5628
        $instance = $this->get_instance();
5629
        $flags = $this->get_user_flags($user->id, false);
5630
        $submission = $this->get_user_submission($user->id, false);
5631
 
5632
        $teamsubmission = null;
5633
        $submissiongroup = null;
5634
        $notsubmitted = array();
5635
        if ($instance->teamsubmission) {
5636
            $teamsubmission = $this->get_group_submission($user->id, 0, false);
5637
            $submissiongroup = $this->get_submission_group($user->id);
5638
            $groupid = 0;
5639
            if ($submissiongroup) {
5640
                $groupid = $submissiongroup->id;
5641
            }
5642
            $notsubmitted = $this->get_submission_group_members_who_have_not_submitted($groupid, false);
5643
        }
5644
 
5645
        $showedit = $showlinks &&
1254 ariadna 5646
            ($this->is_any_submission_plugin_enabled()) &&
5647
            $this->can_edit_submission($user->id);
1 efrain 5648
 
5649
        $submissionlocked = ($flags && $flags->locked);
5650
 
5651
        // Grading criteria preview.
5652
        $gradingmanager = get_grading_manager($this->context, 'mod_assign', 'submissions');
5653
        $gradingcontrollerpreview = '';
5654
        if ($gradingmethod = $gradingmanager->get_active_method()) {
5655
            $controller = $gradingmanager->get_controller($gradingmethod);
5656
            if ($controller->is_form_defined()) {
5657
                $gradingcontrollerpreview = $controller->render_preview($PAGE);
5658
            }
5659
        }
5660
 
5661
        $showsubmit = ($showlinks && $this->submissions_open($user->id));
5662
        $showsubmit = ($showsubmit && $this->show_submit_button($submission, $teamsubmission, $user->id));
5663
 
5664
        $extensionduedate = null;
5665
        if ($flags) {
5666
            $extensionduedate = $flags->extensionduedate;
5667
        }
5668
        $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
5669
 
5670
        $gradingstatus = $this->get_grading_status($user->id);
5671
        $usergroups = $this->get_all_groups($user->id);
1254 ariadna 5672
        $submissionstatus = new assign_submission_status(
5673
            $instance->allowsubmissionsfromdate,
5674
            $instance->alwaysshowdescription,
5675
            $submission,
5676
            $instance->teamsubmission,
5677
            $teamsubmission,
5678
            $submissiongroup,
5679
            $notsubmitted,
5680
            $this->is_any_submission_plugin_enabled(),
5681
            $submissionlocked,
5682
            $this->is_graded($user->id),
5683
            $instance->duedate,
5684
            $instance->cutoffdate,
5685
            $this->get_submission_plugins(),
5686
            $this->get_return_action(),
5687
            $this->get_return_params(),
5688
            $this->get_course_module()->id,
5689
            $this->get_course()->id,
5690
            assign_submission_status::STUDENT_VIEW,
5691
            $showedit,
5692
            $showsubmit,
5693
            $viewfullnames,
5694
            $extensionduedate,
5695
            $this->get_context(),
5696
            $this->is_blind_marking(),
5697
            $gradingcontrollerpreview,
5698
            $instance->attemptreopenmethod,
5699
            $instance->maxattempts,
5700
            $gradingstatus,
5701
            $instance->preventsubmissionnotingroup,
5702
            $usergroups,
5703
            $instance->timelimit
5704
        );
1 efrain 5705
        return $submissionstatus;
5706
    }
5707
 
5708
 
5709
    /**
5710
     * Creates an assign_feedback_status renderable.
5711
     *
5712
     * @param stdClass $user the user to get the report for
5713
     * @return assign_feedback_status renderable object
5714
     */
1254 ariadna 5715
    public function get_assign_feedback_status_renderable($user)
5716
    {
1 efrain 5717
        global $CFG, $DB, $PAGE;
5718
 
1254 ariadna 5719
        require_once($CFG->libdir . '/gradelib.php');
5720
        require_once($CFG->dirroot . '/grade/grading/lib.php');
1 efrain 5721
 
5722
        $instance = $this->get_instance();
5723
        $grade = $this->get_user_grade($user->id, false);
5724
        $gradingstatus = $this->get_grading_status($user->id);
5725
 
1254 ariadna 5726
        $gradinginfo = grade_get_grades(
5727
            $this->get_course()->id,
5728
            'mod',
5729
            'assign',
5730
            $instance->id,
5731
            $user->id
5732
        );
1 efrain 5733
 
5734
        $gradingitem = null;
5735
        $gradebookgrade = null;
5736
        if (isset($gradinginfo->items[0])) {
5737
            $gradingitem = $gradinginfo->items[0];
5738
            $gradebookgrade = $gradingitem->grades[$user->id];
5739
        }
5740
 
5741
        // Check to see if all feedback plugins are empty.
5742
        $emptyplugins = true;
5743
        if ($grade) {
5744
            foreach ($this->get_feedback_plugins() as $plugin) {
5745
                if ($plugin->is_visible() && $plugin->is_enabled()) {
5746
                    if (!$plugin->is_empty($grade)) {
5747
                        $emptyplugins = false;
5748
                    }
5749
                }
5750
            }
5751
        }
5752
 
5753
        if ($this->get_instance()->markingworkflow && $gradingstatus != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
5754
            $emptyplugins = true; // Don't show feedback plugins until released either.
5755
        }
5756
 
5757
        $cangrade = has_capability('mod/assign:grade', $this->get_context());
5758
        $hasgrade = $this->get_instance()->grade != GRADE_TYPE_NONE &&
1254 ariadna 5759
            !is_null($gradebookgrade) && !is_null($gradebookgrade->grade);
1 efrain 5760
        $gradevisible = $cangrade || $this->get_instance()->grade == GRADE_TYPE_NONE ||
1254 ariadna 5761
            (!is_null($gradebookgrade) && !$gradebookgrade->hidden);
1 efrain 5762
        // If there is a visible grade, show the summary.
5763
        if (($hasgrade || !$emptyplugins) && $gradevisible) {
5764
 
5765
            $gradefordisplay = null;
5766
            $gradeddate = null;
5767
            $grader = null;
5768
            $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
5769
 
5770
            $gradingcontrollergrade = '';
5771
            if ($hasgrade) {
5772
                if ($controller = $gradingmanager->get_active_controller()) {
5773
                    $menu = make_grades_menu($this->get_instance()->grade);
5774
                    $controller->set_grade_range($menu, $this->get_instance()->grade > 0);
5775
                    $gradingcontrollergrade = $controller->render_grade(
5776
                        $PAGE,
5777
                        $grade->id,
5778
                        $gradingitem,
5779
                        '',
5780
                        $cangrade
5781
                    );
5782
                    $gradefordisplay = $gradebookgrade->str_long_grade;
5783
                } else {
5784
                    $gradefordisplay = $this->display_grade($gradebookgrade->grade, false);
5785
                }
5786
                $gradeddate = $gradebookgrade->dategraded;
5787
 
5788
                // Show the grader's identity if 'Hide Grader' is disabled or has the 'Show Hidden Grader' capability.
5789
                if (has_capability('mod/assign:showhiddengrader', $this->context) || !$this->is_hidden_grader()) {
5790
                    // Only display the grader if it is in the right state.
5791
                    if (in_array($gradingstatus, [ASSIGN_GRADING_STATUS_GRADED, ASSIGN_MARKING_WORKFLOW_STATE_RELEASED])) {
5792
                        if (isset($grade->grader) && $grade->grader > 0) {
5793
                            $grader = $DB->get_record('user', array('id' => $grade->grader));
1254 ariadna 5794
                        } else if (
5795
                            isset($gradebookgrade->usermodified)
1 efrain 5796
                            && $gradebookgrade->usermodified > 0
1254 ariadna 5797
                            && has_capability('mod/assign:grade', $this->get_context(), $gradebookgrade->usermodified)
5798
                        ) {
1 efrain 5799
                            // Grader not provided. Check that usermodified is a user who can grade.
5800
                            // Case 1: When an assignment is reopened an empty assign_grade is created so the feedback
5801
                            // plugin can know which attempt it's referring to. In this case, usermodifed is a student.
5802
                            // Case 2: When an assignment's grade is overrided via the gradebook, usermodified is a grader.
5803
                            $grader = $DB->get_record('user', array('id' => $gradebookgrade->usermodified));
5804
                        }
5805
                    }
5806
                }
5807
            }
5808
 
5809
            $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
5810
 
5811
            if ($grade) {
5812
                \mod_assign\event\feedback_viewed::create_from_grade($this, $grade)->trigger();
5813
            }
5814
            $feedbackstatus = new assign_feedback_status(
5815
                $gradefordisplay,
5816
                $gradeddate,
5817
                $grader,
5818
                $this->get_feedback_plugins(),
5819
                $grade,
5820
                $this->get_course_module()->id,
5821
                $this->get_return_action(),
5822
                $this->get_return_params(),
5823
                $viewfullnames,
5824
                $gradingcontrollergrade
5825
            );
5826
 
5827
            return $feedbackstatus;
5828
        }
5829
        return;
5830
    }
5831
 
5832
    /**
5833
     * Creates an assign_attempt_history renderable.
5834
     *
5835
     * @param stdClass $user the user to get the report for
5836
     * @return assign_attempt_history renderable object
5837
     */
1254 ariadna 5838
    public function get_assign_attempt_history_renderable($user)
5839
    {
1 efrain 5840
 
5841
        $allsubmissions = $this->get_all_submissions($user->id);
5842
        $allgrades = $this->get_all_grades($user->id);
5843
 
1254 ariadna 5844
        $history = new assign_attempt_history(
5845
            $allsubmissions,
5846
            $allgrades,
5847
            $this->get_submission_plugins(),
5848
            $this->get_feedback_plugins(),
5849
            $this->get_course_module()->id,
5850
            $this->get_return_action(),
5851
            $this->get_return_params(),
5852
            false,
5853
            0,
5854
 
5855
        );
1 efrain 5856
        return $history;
5857
    }
5858
 
5859
    /**
5860
     * Print 2 tables of information with no action links -
5861
     * the submission summary and the grading summary.
5862
     *
5863
     * @param stdClass $user the user to print the report for
5864
     * @param bool $showlinks - Return plain text or links to the profile
5865
     * @return string - the html summary
5866
     */
1254 ariadna 5867
    public function view_student_summary($user, $showlinks)
5868
    {
1 efrain 5869
 
5870
        $o = '';
5871
 
5872
        if ($this->can_view_submission($user->id)) {
5873
            if (has_capability('mod/assign:viewownsubmissionsummary', $this->get_context(), $user, false)) {
5874
                // The user can view the submission summary.
5875
                $submissionstatus = $this->get_assign_submission_status_renderable($user, $showlinks);
5876
                $o .= $this->get_renderer()->render($submissionstatus);
5877
            }
5878
 
5879
            // If there is a visible grade, show the feedback.
5880
            $feedbackstatus = $this->get_assign_feedback_status_renderable($user);
5881
            if ($feedbackstatus) {
5882
                $o .= $this->get_renderer()->render($feedbackstatus);
5883
            }
5884
 
5885
            // If there is more than one submission, show the history.
5886
            $history = $this->get_assign_attempt_history_renderable($user);
5887
            if (count($history->submissions) > 1) {
5888
                $o .= $this->get_renderer()->render($history);
5889
            }
5890
        }
5891
        return $o;
5892
    }
5893
 
5894
    /**
5895
     * Returns true if the submit subsission button should be shown to the user.
5896
     *
5897
     * @param stdClass $submission The users own submission record.
5898
     * @param stdClass $teamsubmission The users team submission record if there is one
5899
     * @param int $userid The user
5900
     * @return bool
5901
     */
1254 ariadna 5902
    protected function show_submit_button($submission = null, $teamsubmission = null, $userid = null)
5903
    {
1 efrain 5904
        if (!has_capability('mod/assign:submit', $this->get_context(), $userid, false)) {
5905
            // The user does not have the capability to submit.
5906
            return false;
5907
        }
5908
        if ($teamsubmission) {
5909
            if ($teamsubmission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
5910
                // The assignment submission has been completed.
5911
                return false;
5912
            } else if ($this->submission_empty($teamsubmission)) {
5913
                // There is nothing to submit yet.
5914
                return false;
5915
            } else if ($submission && $submission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
5916
                // The user has already clicked the submit button on the team submission.
5917
                return false;
5918
            } else if (
5919
                !empty($this->get_instance()->preventsubmissionnotingroup)
5920
                && $this->get_submission_group($userid) == false
5921
            ) {
5922
                return false;
5923
            }
5924
        } else if ($submission) {
5925
            if ($submission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
5926
                // The assignment submission has been completed.
5927
                return false;
5928
            } else if ($this->submission_empty($submission)) {
5929
                // There is nothing to submit.
5930
                return false;
5931
            }
5932
        } else {
5933
            // We've not got a valid submission or team submission.
5934
            return false;
5935
        }
5936
        // Last check is that this instance allows drafts.
5937
        return $this->get_instance()->submissiondrafts;
5938
    }
5939
 
5940
    /**
5941
     * Get the grades for all previous attempts.
5942
     * For each grade - the grader is a full user record,
5943
     * and gradefordisplay is added (rendered from grading manager).
5944
     *
5945
     * @param int $userid If not set, $USER->id will be used.
5946
     * @return array $grades All grade records for this user.
5947
     */
1254 ariadna 5948
    protected function get_all_grades($userid)
5949
    {
1 efrain 5950
        global $DB, $USER, $PAGE;
5951
 
5952
        // If the userid is not null then use userid.
5953
        if (!$userid) {
5954
            $userid = $USER->id;
5955
        }
5956
 
1254 ariadna 5957
        $params = array('assignment' => $this->get_instance()->id, 'userid' => $userid);
1 efrain 5958
 
5959
        $grades = $DB->get_records('assign_grades', $params, 'attemptnumber ASC');
5960
 
5961
        $gradercache = array();
5962
        $cangrade = has_capability('mod/assign:grade', $this->get_context());
5963
 
5964
        // Show the grader's identity if 'Hide Grader' is disabled or has the 'Show Hidden Grader' capability.
5965
        $showgradername = (
5966
            has_capability('mod/assign:showhiddengrader', $this->context, $userid) or
5967
            !$this->is_hidden_grader()
5968
        );
5969
 
5970
        // Need gradingitem and gradingmanager.
5971
        $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
5972
        $controller = $gradingmanager->get_active_controller();
5973
 
1254 ariadna 5974
        $gradinginfo = grade_get_grades(
5975
            $this->get_course()->id,
5976
            'mod',
5977
            'assign',
5978
            $this->get_instance()->id,
5979
            $userid
5980
        );
1 efrain 5981
 
5982
        $gradingitem = null;
5983
        if (isset($gradinginfo->items[0])) {
5984
            $gradingitem = $gradinginfo->items[0];
5985
        }
5986
 
5987
        foreach ($grades as $grade) {
5988
            // First lookup the grader info.
5989
            if (!$showgradername) {
5990
                $grade->grader = null;
5991
            } else if (isset($gradercache[$grade->grader])) {
5992
                $grade->grader = $gradercache[$grade->grader];
5993
            } else if ($grade->grader > 0) {
5994
                // Not in cache - need to load the grader record.
1254 ariadna 5995
                $grade->grader = $DB->get_record('user', array('id' => $grade->grader));
1 efrain 5996
                if ($grade->grader) {
5997
                    $gradercache[$grade->grader->id] = $grade->grader;
5998
                }
5999
            }
6000
 
6001
            // Now get the gradefordisplay.
6002
            if ($controller) {
6003
                $controller->set_grade_range(make_grades_menu($this->get_instance()->grade), $this->get_instance()->grade > 0);
1254 ariadna 6004
                $grade->gradefordisplay = $controller->render_grade(
6005
                    $PAGE,
6006
                    $grade->id,
6007
                    $gradingitem,
6008
                    $grade->grade,
6009
                    $cangrade
6010
                );
1 efrain 6011
            } else {
6012
                $grade->gradefordisplay = $this->display_grade($grade->grade, false);
6013
            }
6014
        }
6015
 
6016
        return $grades;
6017
    }
6018
 
6019
    /**
6020
     * Get the submissions for all previous attempts.
6021
     *
6022
     * @param int $userid If not set, $USER->id will be used.
6023
     * @return array $submissions All submission records for this user (or group).
6024
     */
1254 ariadna 6025
    public function get_all_submissions($userid)
6026
    {
1 efrain 6027
        global $DB, $USER;
6028
 
6029
        // If the userid is not null then use userid.
6030
        if (!$userid) {
6031
            $userid = $USER->id;
6032
        }
6033
 
6034
        $params = array();
6035
 
6036
        if ($this->get_instance()->teamsubmission) {
6037
            $groupid = 0;
6038
            $group = $this->get_submission_group($userid);
6039
            if ($group) {
6040
                $groupid = $group->id;
6041
            }
6042
 
6043
            // Params to get the group submissions.
1254 ariadna 6044
            $params = array('assignment' => $this->get_instance()->id, 'groupid' => $groupid, 'userid' => 0);
1 efrain 6045
        } else {
6046
            // Params to get the user submissions.
1254 ariadna 6047
            $params = array('assignment' => $this->get_instance()->id, 'userid' => $userid);
1 efrain 6048
        }
6049
 
6050
        // Return the submissions ordered by attempt.
6051
        $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber ASC');
6052
 
6053
        return $submissions;
6054
    }
6055
 
6056
    /**
6057
     * Creates an assign_grading_summary renderable.
6058
     *
6059
     * @param mixed $activitygroup int|null the group for calculating the grading summary (if null the function will determine it)
6060
     * @return assign_grading_summary renderable object
6061
     */
1254 ariadna 6062
    public function get_assign_grading_summary_renderable($activitygroup = null)
6063
    {
1 efrain 6064
 
6065
        $instance = $this->get_default_instance(); // Grading summary requires the raw dates, regardless of relativedates mode.
6066
        $cm = $this->get_course_module();
6067
        $course = $this->get_course();
6068
 
6069
        $draft = ASSIGN_SUBMISSION_STATUS_DRAFT;
6070
        $submitted = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
6071
        $isvisible = $cm->visible;
6072
 
6073
        if ($activitygroup === null) {
6074
            $activitygroup = groups_get_activity_group($cm);
6075
        }
6076
 
6077
        if ($instance->teamsubmission) {
6078
            $warnofungroupedusers = assign_grading_summary::WARN_GROUPS_NO;
6079
            $defaultteammembers = $this->get_submission_group_members(0, true);
6080
            if (count($defaultteammembers) > 0) {
6081
                if ($instance->preventsubmissionnotingroup) {
6082
                    $warnofungroupedusers = assign_grading_summary::WARN_GROUPS_REQUIRED;
6083
                } else {
6084
                    $warnofungroupedusers = assign_grading_summary::WARN_GROUPS_OPTIONAL;
6085
                }
6086
            }
6087
 
6088
            $summary = new assign_grading_summary(
6089
                $this->count_teams($activitygroup),
6090
                $instance->submissiondrafts,
6091
                $this->count_submissions_with_status($draft, $activitygroup),
6092
                $this->is_any_submission_plugin_enabled(),
6093
                $this->count_submissions_with_status($submitted, $activitygroup),
6094
                $this->get_cutoffdate($activitygroup),
6095
                $this->get_duedate($activitygroup),
6096
                $this->get_timelimit($activitygroup),
6097
                $this->get_course_module()->id,
6098
                $this->count_submissions_need_grading($activitygroup),
6099
                $instance->teamsubmission,
6100
                $warnofungroupedusers,
6101
                $course->relativedatesmode,
6102
                $course->startdate,
6103
                $this->can_grade(),
6104
                $isvisible,
6105
                $this->get_course_module()
6106
            );
6107
        } else {
6108
            // The active group has already been updated in groups_print_activity_menu().
6109
            $countparticipants = $this->count_participants($activitygroup);
6110
            $summary = new assign_grading_summary(
6111
                $countparticipants,
6112
                $instance->submissiondrafts,
6113
                $this->count_submissions_with_status($draft, $activitygroup),
6114
                $this->is_any_submission_plugin_enabled(),
6115
                $this->count_submissions_with_status($submitted, $activitygroup),
6116
                $this->get_cutoffdate($activitygroup),
6117
                $this->get_duedate($activitygroup),
6118
                $this->get_timelimit($activitygroup),
6119
                $this->get_course_module()->id,
6120
                $this->count_submissions_need_grading($activitygroup),
6121
                $instance->teamsubmission,
6122
                assign_grading_summary::WARN_GROUPS_NO,
6123
                $course->relativedatesmode,
6124
                $course->startdate,
6125
                $this->can_grade(),
6126
                $isvisible,
6127
                $this->get_course_module()
6128
            );
6129
        }
6130
 
6131
        return $summary;
6132
    }
6133
 
6134
    /**
6135
     * Helper function to allow up to fetch the group overrides via one query as opposed to many calls.
6136
     *
6137
     * @param int $activitygroup The group we want to check the overrides of
6138
     * @return mixed Can return either a fetched DB object, local object or false
6139
     */
1254 ariadna 6140
    private function get_override_data(int $activitygroup)
6141
    {
1 efrain 6142
        global $DB;
6143
 
6144
        $instanceid = $this->get_instance()->id;
6145
        $cachekey = "$instanceid-$activitygroup";
6146
        if (isset($this->overridedata[$cachekey])) {
6147
            return $this->overridedata[$cachekey];
6148
        }
6149
 
6150
        $params = ['groupid' => $activitygroup, 'assignid' => $instanceid];
6151
        $this->overridedata[$cachekey] = $DB->get_record('assign_overrides', $params);
6152
        return $this->overridedata[$cachekey];
6153
    }
6154
 
6155
    /**
6156
     * Return group override duedate.
6157
     *
6158
     * @param int $activitygroup Activity active group
6159
     * @return int $duedate
6160
     */
1254 ariadna 6161
    private function get_duedate($activitygroup = null)
6162
    {
1 efrain 6163
        if ($activitygroup === null) {
6164
            $activitygroup = groups_get_activity_group($this->get_course_module());
6165
        }
6166
        if ($this->can_view_grades() && !empty($activitygroup)) {
6167
            $groupoverride = $this->get_override_data($activitygroup);
6168
            if (!empty($groupoverride->duedate)) {
6169
                return $groupoverride->duedate;
6170
            }
6171
        }
6172
        return $this->get_instance()->duedate;
6173
    }
6174
 
6175
    /**
6176
     * Return group override timelimit.
6177
     *
6178
     * @param null|int $activitygroup Activity active group
6179
     * @return int $timelimit
6180
     */
1254 ariadna 6181
    private function get_timelimit(?int $activitygroup = null): int
6182
    {
1 efrain 6183
        if ($activitygroup === null) {
6184
            $activitygroup = groups_get_activity_group($this->get_course_module());
6185
        }
6186
        if ($this->can_view_grades() && !empty($activitygroup)) {
6187
            $groupoverride = $this->get_override_data($activitygroup);
6188
            if (!empty($groupoverride->timelimit)) {
6189
                return $groupoverride->timelimit;
6190
            }
6191
        }
6192
        return $this->get_instance()->timelimit;
6193
    }
6194
 
6195
    /**
6196
     * Return group override cutoffdate.
6197
     *
6198
     * @param null|int $activitygroup Activity active group
6199
     * @return int $cutoffdate
6200
     */
1254 ariadna 6201
    private function get_cutoffdate(?int $activitygroup = null): int
6202
    {
1 efrain 6203
        if ($activitygroup === null) {
6204
            $activitygroup = groups_get_activity_group($this->get_course_module());
6205
        }
6206
        if ($this->can_view_grades() && !empty($activitygroup)) {
6207
            $groupoverride = $this->get_override_data($activitygroup);
6208
            if (!empty($groupoverride->cutoffdate)) {
6209
                return $groupoverride->cutoffdate;
6210
            }
6211
        }
6212
        return $this->get_instance()->cutoffdate;
6213
    }
6214
 
6215
    /**
6216
     * View submissions page (contains details of current submission).
6217
     *
6218
     * @return string
6219
     */
1254 ariadna 6220
    protected function view_submission_page()
6221
    {
1 efrain 6222
        global $CFG, $DB, $USER, $PAGE;
6223
 
6224
        $instance = $this->get_instance();
6225
 
6226
        $this->add_grade_notices();
6227
 
6228
        $o = '';
6229
 
6230
        $postfix = '';
6231
        if ($this->has_visible_attachments() && (!$this->get_instance($USER->id)->submissionattachments)) {
6232
            $postfix = $this->render_area_files('mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0);
6233
        }
6234
 
1254 ariadna 6235
        $o .= $this->get_renderer()->render(new assign_header(
6236
            $instance,
6237
            $this->get_context(),
6238
            $this->show_intro(),
6239
            $this->get_course_module()->id,
6240
            '',
6241
            '',
6242
            $postfix
6243
        ));
1 efrain 6244
 
6245
        // Display plugin specific headers.
6246
        $plugins = array_merge($this->get_submission_plugins(), $this->get_feedback_plugins());
6247
        foreach ($plugins as $plugin) {
6248
            if ($plugin->is_enabled() && $plugin->is_visible()) {
6249
                $o .= $this->get_renderer()->render(new assign_plugin_header($plugin));
6250
            }
6251
        }
6252
 
6253
        if ($this->can_view_grades()) {
6254
            $actionbuttons = new \mod_assign\output\actionmenu($this->get_course_module()->id);
6255
            $o .= $this->get_renderer()->submission_actionmenu($actionbuttons);
6256
 
6257
            $summary = $this->get_assign_grading_summary_renderable();
6258
            $o .= $this->get_renderer()->render($summary);
6259
        }
6260
 
6261
        if ($this->can_view_submission($USER->id)) {
6262
            $o .= $this->view_submission_action_bar($instance, $USER);
6263
            $o .= $this->view_student_summary($USER, true);
6264
        }
6265
 
6266
        $o .= $this->view_footer();
6267
 
6268
        \mod_assign\event\submission_status_viewed::create_from_assign($this)->trigger();
6269
 
6270
        return $o;
6271
    }
6272
 
6273
    /**
6274
     * The action bar displayed in the submissions page.
6275
     *
6276
     * @param stdClass $instance The settings for the current instance of this assignment
6277
     * @param stdClass $user The user to print the action bar for
6278
     * @return string
6279
     */
1254 ariadna 6280
    public function view_submission_action_bar(stdClass $instance, stdClass $user): string
6281
    {
1 efrain 6282
        $submission = $this->get_user_submission($user->id, false);
6283
        // Figure out if we are team or solitary submission.
6284
        $teamsubmission = null;
6285
        if ($instance->teamsubmission) {
6286
            $teamsubmission = $this->get_group_submission($user->id, 0, false);
6287
        }
6288
 
6289
        $showsubmit = ($this->submissions_open($user->id)
6290
            && $this->show_submit_button($submission, $teamsubmission, $user->id));
6291
        $showedit = ($this->is_any_submission_plugin_enabled()) && $this->can_edit_submission($user->id);
6292
 
6293
        // The method get_group_submission() says that it returns a stdClass, but it can return false >_>.
6294
        if ($teamsubmission === false) {
6295
            $teamsubmission = new stdClass();
6296
        }
6297
        // Same goes for get_user_submission().
6298
        if ($submission === false) {
6299
            $submission = new stdClass();
6300
        }
6301
        $actionbuttons = new \mod_assign\output\user_submission_actionmenu(
6302
            $this->get_course_module()->id,
6303
            $showsubmit,
6304
            $showedit,
6305
            $submission,
6306
            $teamsubmission,
6307
            $instance->timelimit
6308
        );
6309
 
6310
        return $this->get_renderer()->render($actionbuttons);
6311
    }
6312
 
6313
    /**
6314
     * Convert the final raw grade(s) in the grading table for the gradebook.
6315
     *
6316
     * @param stdClass $grade
6317
     * @return array
6318
     */
1254 ariadna 6319
    protected function convert_grade_for_gradebook(stdClass $grade)
6320
    {
1 efrain 6321
        $gradebookgrade = array();
6322
        if ($grade->grade >= 0) {
6323
            $gradebookgrade['rawgrade'] = $grade->grade;
6324
        }
6325
        // Allow "no grade" to be chosen.
6326
        if ($grade->grade == -1) {
6327
            $gradebookgrade['rawgrade'] = NULL;
6328
        }
6329
        $gradebookgrade['userid'] = $grade->userid;
6330
        $gradebookgrade['usermodified'] = $grade->grader;
6331
        $gradebookgrade['datesubmitted'] = null;
6332
        $gradebookgrade['dategraded'] = $grade->timemodified;
6333
        if (isset($grade->feedbackformat)) {
6334
            $gradebookgrade['feedbackformat'] = $grade->feedbackformat;
6335
        }
6336
        if (isset($grade->feedbacktext)) {
6337
            $gradebookgrade['feedback'] = $grade->feedbacktext;
6338
        }
6339
        if (isset($grade->feedbackfiles)) {
6340
            $gradebookgrade['feedbackfiles'] = $grade->feedbackfiles;
6341
        }
6342
 
6343
        return $gradebookgrade;
6344
    }
6345
 
6346
    /**
6347
     * Convert submission details for the gradebook.
6348
     *
6349
     * @param stdClass $submission
6350
     * @return array
6351
     */
1254 ariadna 6352
    protected function convert_submission_for_gradebook(stdClass $submission)
6353
    {
1 efrain 6354
        $gradebookgrade = array();
6355
 
6356
        $gradebookgrade['userid'] = $submission->userid;
6357
        $gradebookgrade['usermodified'] = $submission->userid;
6358
        $gradebookgrade['datesubmitted'] = $submission->timemodified;
6359
 
6360
        return $gradebookgrade;
6361
    }
6362
 
6363
    /**
6364
     * Update grades in the gradebook.
6365
     *
6366
     * @param mixed $submission stdClass|null
6367
     * @param mixed $grade stdClass|null
6368
     * @return bool
6369
     */
1254 ariadna 6370
    protected function gradebook_item_update($submission = null, $grade = null)
6371
    {
1 efrain 6372
        global $CFG;
6373
 
1254 ariadna 6374
        require_once($CFG->dirroot . '/mod/assign/lib.php');
1 efrain 6375
        // Do not push grade to gradebook if blind marking is active as
6376
        // the gradebook would reveal the students.
6377
        if ($this->is_blind_marking()) {
6378
            return false;
6379
        }
6380
 
6381
        // If marking workflow is enabled and grade is not released then remove any grade that may exist in the gradebook.
1254 ariadna 6382
        if (
6383
            $this->get_instance()->markingworkflow && !empty($grade) &&
6384
            $this->get_grading_status($grade->userid) != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED
6385
        ) {
1 efrain 6386
            // Remove the grade (if it exists) from the gradebook as it is not 'final'.
6387
            $grade->grade = -1;
6388
            $grade->feedbacktext = '';
6389
            $grade->feebackfiles = [];
6390
        }
6391
 
6392
        if ($submission != null) {
6393
            if ($submission->userid == 0) {
6394
                // This is a group submission update.
6395
                $team = groups_get_members($submission->groupid, 'u.id');
6396
 
6397
                foreach ($team as $member) {
6398
                    $membersubmission = clone $submission;
6399
                    $membersubmission->groupid = 0;
6400
                    $membersubmission->userid = $member->id;
6401
                    $this->gradebook_item_update($membersubmission, null);
6402
                }
6403
                return;
6404
            }
6405
 
6406
            $gradebookgrade = $this->convert_submission_for_gradebook($submission);
6407
        } else {
6408
            $gradebookgrade = $this->convert_grade_for_gradebook($grade);
6409
        }
6410
        // Grading is disabled, return.
6411
        if ($this->grading_disabled($gradebookgrade['userid'])) {
6412
            return false;
6413
        }
6414
        $assign = clone $this->get_instance();
6415
        $assign->cmidnumber = $this->get_course_module()->idnumber;
6416
        // Set assign gradebook feedback plugin status (enabled and visible).
6417
        $assign->gradefeedbackenabled = $this->is_gradebook_feedback_enabled();
6418
        return assign_grade_item_update($assign, $gradebookgrade) == GRADE_UPDATE_OK;
6419
    }
6420
 
6421
    /**
6422
     * Update team submission.
6423
     *
6424
     * @param stdClass $submission
6425
     * @param int $userid
6426
     * @param bool $updatetime
6427
     * @return bool
6428
     */
1254 ariadna 6429
    protected function update_team_submission(stdClass $submission, $userid, $updatetime)
6430
    {
1 efrain 6431
        global $DB;
6432
 
6433
        if ($updatetime) {
6434
            $submission->timemodified = time();
6435
        }
6436
 
6437
        // First update the submission for the current user.
6438
        $mysubmission = $this->get_user_submission($userid, true, $submission->attemptnumber);
6439
        $mysubmission->status = $submission->status;
6440
 
6441
        $this->update_submission($mysubmission, 0, $updatetime, false);
6442
 
6443
        // Now check the team settings to see if this assignment qualifies as submitted or draft.
6444
        $team = $this->get_submission_group_members($submission->groupid, true);
6445
 
6446
        $allsubmitted = true;
6447
        $anysubmitted = false;
6448
        $result = true;
6449
        if (!in_array($submission->status, [ASSIGN_SUBMISSION_STATUS_NEW, ASSIGN_SUBMISSION_STATUS_REOPENED])) {
6450
            foreach ($team as $member) {
6451
                $membersubmission = $this->get_user_submission($member->id, false, $submission->attemptnumber);
6452
 
6453
                // If no submission found for team member and member is active then everyone has not submitted.
1254 ariadna 6454
                if (
6455
                    !$membersubmission || $membersubmission->status != ASSIGN_SUBMISSION_STATUS_SUBMITTED
6456
                    && ($this->is_active_user($member->id))
6457
                ) {
1 efrain 6458
                    $allsubmitted = false;
6459
                    if ($anysubmitted) {
6460
                        break;
6461
                    }
6462
                } else {
6463
                    $anysubmitted = true;
6464
                }
6465
            }
6466
            if ($this->get_instance()->requireallteammemberssubmit) {
6467
                if ($allsubmitted) {
6468
                    $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
6469
                } else {
6470
                    $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
6471
                }
6472
                $result = $DB->update_record('assign_submission', $submission);
6473
            } else {
6474
                if ($anysubmitted) {
6475
                    $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
6476
                } else {
6477
                    $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
6478
                }
6479
                $result = $DB->update_record('assign_submission', $submission);
6480
            }
6481
        } else {
6482
            // Set the group submission to reopened.
6483
            foreach ($team as $member) {
6484
                $membersubmission = $this->get_user_submission($member->id, true, $submission->attemptnumber);
6485
                $membersubmission->status = $submission->status;
6486
                $result = $DB->update_record('assign_submission', $membersubmission) && $result;
6487
            }
6488
            $result = $DB->update_record('assign_submission', $submission) && $result;
6489
        }
6490
 
6491
        $this->gradebook_item_update($submission);
6492
        return $result;
6493
    }
6494
 
6495
    /**
6496
     * Update grades in the gradebook based on submission time.
6497
     *
6498
     * @param stdClass $submission
6499
     * @param int $userid
6500
     * @param bool $updatetime
6501
     * @param bool $teamsubmission
6502
     * @return bool
6503
     */
1254 ariadna 6504
    protected function update_submission(stdClass $submission, $userid, $updatetime, $teamsubmission)
6505
    {
1 efrain 6506
        global $DB;
6507
 
6508
        if ($teamsubmission) {
6509
            return $this->update_team_submission($submission, $userid, $updatetime);
6510
        }
6511
 
6512
        if ($updatetime) {
6513
            $submission->timemodified = time();
6514
        }
1254 ariadna 6515
        $result = $DB->update_record('assign_submission', $submission);
1 efrain 6516
        if ($result) {
6517
            $this->gradebook_item_update($submission);
6518
        }
6519
        return $result;
6520
    }
6521
 
6522
    /**
6523
     * Is this assignment open for submissions?
6524
     *
6525
     * Check the due date,
6526
     * prevent late submissions,
6527
     * has this person already submitted,
6528
     * is the assignment locked?
6529
     *
6530
     * @param int $userid - Optional userid so we can see if a different user can submit
6531
     * @param bool $skipenrolled - Skip enrollment checks (because they have been done already)
6532
     * @param stdClass $submission - Pre-fetched submission record (or false to fetch it)
6533
     * @param stdClass $flags - Pre-fetched user flags record (or false to fetch it)
6534
     * @param stdClass $gradinginfo - Pre-fetched user gradinginfo record (or false to fetch it)
6535
     * @return bool
6536
     */
1254 ariadna 6537
    public function submissions_open(
6538
        $userid = 0,
6539
        $skipenrolled = false,
6540
        $submission = false,
6541
        $flags = false,
6542
        $gradinginfo = false
6543
    ) {
1 efrain 6544
        global $USER;
6545
 
6546
        if (!$userid) {
6547
            $userid = $USER->id;
6548
        }
6549
 
6550
        $time = time();
6551
        $dateopen = true;
6552
        $finaldate = false;
6553
        if ($this->get_instance()->cutoffdate) {
6554
            $finaldate = $this->get_instance()->cutoffdate;
6555
        }
6556
 
6557
        if ($flags === false) {
6558
            $flags = $this->get_user_flags($userid, false);
6559
        }
6560
        if ($flags && $flags->locked) {
6561
            return false;
6562
        }
6563
 
6564
        // User extensions.
6565
        if ($finaldate) {
6566
            if ($flags && $flags->extensionduedate) {
6567
                // Extension can be before cut off date.
6568
                if ($flags->extensionduedate > $finaldate) {
6569
                    $finaldate = $flags->extensionduedate;
6570
                }
6571
            }
6572
        }
6573
 
6574
        if ($finaldate) {
6575
            $dateopen = ($this->get_instance()->allowsubmissionsfromdate <= $time && $time <= $finaldate);
6576
        } else {
6577
            $dateopen = ($this->get_instance()->allowsubmissionsfromdate <= $time);
6578
        }
6579
 
6580
        if (!$dateopen) {
6581
            return false;
6582
        }
6583
 
6584
        // Now check if this user has already submitted etc.
6585
        if (!$skipenrolled && !is_enrolled($this->get_course_context(), $userid)) {
6586
            return false;
6587
        }
6588
        // Note you can pass null for submission and it will not be fetched.
6589
        if ($submission === false) {
6590
            if ($this->get_instance()->teamsubmission) {
6591
                $submission = $this->get_group_submission($userid, 0, false);
6592
            } else {
6593
                $submission = $this->get_user_submission($userid, false);
6594
            }
6595
        }
6596
        if ($submission) {
6597
 
6598
            if ($this->get_instance()->submissiondrafts && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
6599
                // Drafts are tracked and the student has submitted the assignment.
6600
                return false;
6601
            }
6602
        }
6603
 
6604
        // See if this user grade is locked in the gradebook.
6605
        if ($gradinginfo === false) {
1254 ariadna 6606
            $gradinginfo = grade_get_grades(
6607
                $this->get_course()->id,
6608
                'mod',
6609
                'assign',
6610
                $this->get_instance()->id,
6611
                array($userid)
6612
            );
1 efrain 6613
        }
1254 ariadna 6614
        if (
6615
            $gradinginfo &&
6616
            isset($gradinginfo->items[0]->grades[$userid]) &&
6617
            $gradinginfo->items[0]->grades[$userid]->locked
6618
        ) {
1 efrain 6619
            return false;
6620
        }
6621
 
6622
        return true;
6623
    }
6624
 
6625
    /**
6626
     * Render the files in file area.
6627
     *
6628
     * @param string $component
6629
     * @param string $area
6630
     * @param int $submissionid
6631
     * @return string
6632
     */
1254 ariadna 6633
    public function render_area_files($component, $area, $submissionid)
6634
    {
1 efrain 6635
        global $USER;
6636
 
1254 ariadna 6637
        return $this->get_renderer()->assign_files(
6638
            $this->context,
6639
            $submissionid,
6640
            $area,
6641
            $component,
6642
            $this->course,
6643
            $this->coursemodule
6644
        );
1 efrain 6645
    }
6646
 
6647
    /**
6648
     * Capability check to make sure this grader can edit this submission.
6649
     *
6650
     * @param int $userid - The user whose submission is to be edited
6651
     * @param int $graderid (optional) - The user who will do the editing (default to $USER->id).
6652
     * @return bool
6653
     */
1254 ariadna 6654
    public function can_edit_submission($userid, $graderid = 0)
6655
    {
1 efrain 6656
        global $USER;
6657
 
6658
        if (empty($graderid)) {
6659
            $graderid = $USER->id;
6660
        }
6661
 
6662
        $instance = $this->get_instance();
1254 ariadna 6663
        if (
6664
            $userid == $graderid &&
1 efrain 6665
            $instance->teamsubmission &&
6666
            $instance->preventsubmissionnotingroup &&
1254 ariadna 6667
            $this->get_submission_group($userid) == false
6668
        ) {
1 efrain 6669
            return false;
6670
        }
6671
 
6672
        if ($userid == $graderid) {
1254 ariadna 6673
            if (
6674
                $this->submissions_open($userid) &&
6675
                has_capability('mod/assign:submit', $this->context, $graderid)
6676
            ) {
1 efrain 6677
                // User can edit their own submission.
6678
                return true;
6679
            } else {
6680
                // We need to return here because editothersubmission should never apply to a users own submission.
6681
                return false;
6682
            }
6683
        }
6684
 
6685
        if (!has_capability('mod/assign:editothersubmission', $this->context, $graderid)) {
6686
            return false;
6687
        }
6688
 
6689
        $cm = $this->get_course_module();
6690
        if (groups_get_activity_groupmode($cm) == SEPARATEGROUPS) {
6691
            $sharedgroupmembers = $this->get_shared_group_members($cm, $graderid);
6692
            return in_array($userid, $sharedgroupmembers);
6693
        }
6694
        return true;
6695
    }
6696
 
6697
    /**
6698
     * Returns IDs of the users who share group membership with the specified user.
6699
     *
6700
     * @param stdClass|cm_info $cm Course-module
6701
     * @param int $userid User ID
6702
     * @return array An array of ID of users.
6703
     */
1254 ariadna 6704
    public function get_shared_group_members($cm, $userid)
6705
    {
1 efrain 6706
        if (!isset($this->sharedgroupmembers[$userid])) {
6707
            $this->sharedgroupmembers[$userid] = array();
6708
            if ($members = groups_get_activity_shared_group_members($cm, $userid)) {
6709
                $this->sharedgroupmembers[$userid] = array_keys($members);
6710
            }
6711
        }
6712
 
6713
        return $this->sharedgroupmembers[$userid];
6714
    }
6715
 
6716
    /**
6717
     * Returns a list of teachers that should be grading given submission.
6718
     *
6719
     * @param int $userid The submission to grade
6720
     * @return array
6721
     */
1254 ariadna 6722
    protected function get_graders($userid)
6723
    {
1 efrain 6724
        // Potential graders should be active users only.
6725
        $potentialgraders = get_enrolled_users($this->context, "mod/assign:grade", null, 'u.*', null, null, null, true);
6726
 
6727
        $graders = array();
6728
        if (groups_get_activity_groupmode($this->get_course_module()) == SEPARATEGROUPS) {
6729
            if ($groups = groups_get_all_groups($this->get_course()->id, $userid, $this->get_course_module()->groupingid)) {
6730
                foreach ($groups as $group) {
6731
                    foreach ($potentialgraders as $grader) {
6732
                        if ($grader->id == $userid) {
6733
                            // Do not send self.
6734
                            continue;
6735
                        }
6736
                        if (groups_is_member($group->id, $grader->id)) {
6737
                            $graders[$grader->id] = $grader;
6738
                        }
6739
                    }
6740
                }
6741
            } else {
6742
                // User not in group, try to find graders without group.
6743
                foreach ($potentialgraders as $grader) {
6744
                    if ($grader->id == $userid) {
6745
                        // Do not send self.
6746
                        continue;
6747
                    }
6748
                    if (!groups_has_membership($this->get_course_module(), $grader->id)) {
6749
                        $graders[$grader->id] = $grader;
6750
                    }
6751
                }
6752
            }
6753
        } else {
6754
            foreach ($potentialgraders as $grader) {
6755
                if ($grader->id == $userid) {
6756
                    // Do not send self.
6757
                    continue;
6758
                }
6759
                // Must be enrolled.
6760
                if (is_enrolled($this->get_course_context(), $grader->id)) {
6761
                    $graders[$grader->id] = $grader;
6762
                }
6763
            }
6764
        }
6765
        return $graders;
6766
    }
6767
 
6768
    /**
6769
     * Returns a list of users that should receive notification about given submission.
6770
     *
6771
     * @param int $userid The submission to grade
6772
     * @return array
6773
     */
1254 ariadna 6774
    protected function get_notifiable_users($userid)
6775
    {
1 efrain 6776
        // Potential users should be active users only.
1254 ariadna 6777
        $potentialusers = get_enrolled_users(
6778
            $this->context,
6779
            "mod/assign:receivegradernotifications",
6780
            null,
6781
            'u.*',
6782
            null,
6783
            null,
6784
            null,
6785
            true
6786
        );
1 efrain 6787
 
6788
        $notifiableusers = array();
6789
        if (groups_get_activity_groupmode($this->get_course_module()) == SEPARATEGROUPS) {
6790
            if ($groups = groups_get_all_groups($this->get_course()->id, $userid, $this->get_course_module()->groupingid)) {
6791
                foreach ($groups as $group) {
6792
                    foreach ($potentialusers as $potentialuser) {
6793
                        if ($potentialuser->id == $userid) {
6794
                            // Do not send self.
6795
                            continue;
6796
                        }
6797
                        if (groups_is_member($group->id, $potentialuser->id)) {
6798
                            $notifiableusers[$potentialuser->id] = $potentialuser;
6799
                        }
6800
                    }
6801
                }
6802
            } else {
6803
                // User not in group, try to find graders without group.
6804
                foreach ($potentialusers as $potentialuser) {
6805
                    if ($potentialuser->id == $userid) {
6806
                        // Do not send self.
6807
                        continue;
6808
                    }
6809
                    if (!groups_has_membership($this->get_course_module(), $potentialuser->id)) {
6810
                        $notifiableusers[$potentialuser->id] = $potentialuser;
6811
                    }
6812
                }
6813
            }
6814
        } else {
6815
            foreach ($potentialusers as $potentialuser) {
6816
                if ($potentialuser->id == $userid) {
6817
                    // Do not send self.
6818
                    continue;
6819
                }
6820
                // Must be enrolled.
6821
                if (is_enrolled($this->get_course_context(), $potentialuser->id)) {
6822
                    $notifiableusers[$potentialuser->id] = $potentialuser;
6823
                }
6824
            }
6825
        }
6826
        return $notifiableusers;
6827
    }
6828
 
6829
    /**
6830
     * Format a notification for plain text.
6831
     *
6832
     * @param string $messagetype
6833
     * @param stdClass $info
6834
     * @param stdClass $course
6835
     * @param stdClass $context
6836
     * @param string $modulename
6837
     * @param string $assignmentname
6838
     */
1254 ariadna 6839
    protected static function format_notification_message_text(
6840
        $messagetype,
6841
        $info,
6842
        $course,
6843
        $context,
6844
        $modulename,
6845
        $assignmentname
6846
    ) {
1 efrain 6847
        $formatparams = array('context' => $context->get_course_context());
6848
        $posttext  = format_string($course->shortname, true, $formatparams) .
1254 ariadna 6849
            ' -> ' .
6850
            $modulename .
6851
            ' -> ' .
6852
            format_string($assignmentname, true, $formatparams) . "\n";
1 efrain 6853
        $posttext .= '---------------------------------------------------------------------' . "\n";
1254 ariadna 6854
        $posttext .= get_string($messagetype . 'text', 'assign', $info) . "\n";
1 efrain 6855
        $posttext .= "\n---------------------------------------------------------------------\n";
6856
        return $posttext;
6857
    }
6858
 
6859
    /**
6860
     * Format a notification for HTML.
6861
     *
6862
     * @param string $messagetype
6863
     * @param stdClass $info
6864
     * @param stdClass $course
6865
     * @param stdClass $context
6866
     * @param string $modulename
6867
     * @param stdClass $coursemodule
6868
     * @param string $assignmentname
6869
     */
1254 ariadna 6870
    protected static function format_notification_message_html(
6871
        $messagetype,
6872
        $info,
6873
        $course,
6874
        $context,
6875
        $modulename,
6876
        $coursemodule,
6877
        $assignmentname
6878
    ) {
1 efrain 6879
        global $CFG;
6880
        $formatparams = array('context' => $context->get_course_context());
6881
        $posthtml  = '<p><font face="sans-serif">' .
1254 ariadna 6882
            '<a href="' . $CFG->wwwroot . '/course/view.php?id=' . $course->id . '">' .
6883
            format_string($course->shortname, true, $formatparams) .
6884
            '</a> ->' .
6885
            '<a href="' . $CFG->wwwroot . '/mod/assign/index.php?id=' . $course->id . '">' .
6886
            $modulename .
6887
            '</a> ->' .
6888
            '<a href="' . $CFG->wwwroot . '/mod/assign/view.php?id=' . $coursemodule->id . '">' .
6889
            format_string($assignmentname, true, $formatparams) .
6890
            '</a></font></p>';
1 efrain 6891
        $posthtml .= '<hr /><font face="sans-serif">';
6892
        $posthtml .= '<p>' . get_string($messagetype . 'html', 'assign', $info) . '</p>';
6893
        $posthtml .= '</font><hr />';
6894
        return $posthtml;
6895
    }
6896
 
6897
    /**
6898
     * Message someone about something (static so it can be called from cron).
6899
     *
6900
     * @param stdClass $userfrom
6901
     * @param stdClass $userto
6902
     * @param string $messagetype
6903
     * @param string $eventtype
6904
     * @param int $updatetime
6905
     * @param stdClass $coursemodule
6906
     * @param stdClass $context
6907
     * @param stdClass $course
6908
     * @param string $modulename
6909
     * @param string $assignmentname
6910
     * @param bool $blindmarking
6911
     * @param int $uniqueidforuser
6912
     * @return void
6913
     */
1254 ariadna 6914
    public static function send_assignment_notification(
6915
        $userfrom,
6916
        $userto,
6917
        $messagetype,
6918
        $eventtype,
6919
        $updatetime,
6920
        $coursemodule,
6921
        $context,
6922
        $course,
6923
        $modulename,
6924
        $assignmentname,
6925
        $blindmarking,
6926
        $uniqueidforuser
6927
    ) {
1 efrain 6928
        global $CFG, $PAGE;
6929
 
6930
        $info = new stdClass();
6931
        if ($blindmarking) {
1254 ariadna 6932
            $userfrom = clone ($userfrom);
1 efrain 6933
            $info->username = get_string('participant', 'assign') . ' ' . $uniqueidforuser;
6934
            $userfrom->firstname = get_string('participant', 'assign');
6935
            $userfrom->lastname = $uniqueidforuser;
6936
            $userfrom->email = $CFG->noreplyaddress;
6937
        } else {
6938
            $info->username = fullname($userfrom, true);
6939
        }
1254 ariadna 6940
        $info->assignment = format_string($assignmentname, true, array('context' => $context));
6941
        $info->url = $CFG->wwwroot . '/mod/assign/view.php?id=' . $coursemodule->id;
1 efrain 6942
        $info->timeupdated = userdate($updatetime, get_string('strftimerecentfull'));
6943
 
6944
        $postsubject = get_string($messagetype . 'small', 'assign', $info);
1254 ariadna 6945
        $posttext = self::format_notification_message_text(
6946
            $messagetype,
6947
            $info,
6948
            $course,
6949
            $context,
6950
            $modulename,
6951
            $assignmentname
6952
        );
1 efrain 6953
        $posthtml = '';
6954
        if ($userto->mailformat == 1) {
1254 ariadna 6955
            $posthtml = self::format_notification_message_html(
6956
                $messagetype,
6957
                $info,
6958
                $course,
6959
                $context,
6960
                $modulename,
6961
                $coursemodule,
6962
                $assignmentname
6963
            );
1 efrain 6964
        }
6965
 
6966
        $eventdata = new \core\message\message();
6967
        $eventdata->courseid         = $course->id;
6968
        $eventdata->modulename       = 'assign';
6969
        $eventdata->userfrom         = $userfrom;
6970
        $eventdata->userto           = $userto;
6971
        $eventdata->subject          = $postsubject;
6972
        $eventdata->fullmessage      = $posttext;
6973
        $eventdata->fullmessageformat = FORMAT_PLAIN;
6974
        $eventdata->fullmessagehtml  = $posthtml;
6975
        $eventdata->smallmessage     = $postsubject;
6976
 
6977
        $eventdata->name            = $eventtype;
6978
        $eventdata->component       = 'mod_assign';
6979
        $eventdata->notification    = 1;
6980
        $eventdata->contexturl      = $info->url;
6981
        $eventdata->contexturlname  = $info->assignment;
6982
        $customdata = [
6983
            'cmid' => $coursemodule->id,
6984
            'instance' => $coursemodule->instance,
6985
            'messagetype' => $messagetype,
6986
            'blindmarking' => $blindmarking,
6987
            'uniqueidforuser' => $uniqueidforuser,
6988
        ];
6989
        // Check if the userfrom is real and visible.
6990
        if (!empty($userfrom->id) && core_user::is_real_user($userfrom->id)) {
6991
            $userpicture = new user_picture($userfrom);
6992
            $userpicture->size = 1; // Use f1 size.
6993
            $userpicture->includetoken = $userto->id; // Generate an out-of-session token for the user receiving the message.
6994
            $customdata['notificationiconurl'] = $userpicture->get_url($PAGE)->out(false);
6995
        }
6996
        $eventdata->customdata = $customdata;
6997
 
6998
        message_send($eventdata);
6999
    }
7000
 
7001
    /**
7002
     * Message someone about something.
7003
     *
7004
     * @param stdClass $userfrom
7005
     * @param stdClass $userto
7006
     * @param string $messagetype
7007
     * @param string $eventtype
7008
     * @param int $updatetime
7009
     * @return void
7010
     */
1254 ariadna 7011
    public function send_notification($userfrom, $userto, $messagetype, $eventtype, $updatetime)
7012
    {
1 efrain 7013
        global $USER;
7014
        $userid = core_user::is_real_user($userfrom->id) ? $userfrom->id : $USER->id;
7015
        $uniqueid = $this->get_uniqueid_for_user($userid);
1254 ariadna 7016
        self::send_assignment_notification(
7017
            $userfrom,
7018
            $userto,
7019
            $messagetype,
7020
            $eventtype,
7021
            $updatetime,
7022
            $this->get_course_module(),
7023
            $this->get_context(),
7024
            $this->get_course(),
7025
            $this->get_module_name(),
7026
            $this->get_instance()->name,
7027
            $this->is_blind_marking(),
7028
            $uniqueid
7029
        );
1 efrain 7030
    }
7031
 
7032
    /**
7033
     * Notify student upon successful submission copy.
7034
     *
7035
     * @param stdClass $submission
7036
     * @return void
7037
     */
1254 ariadna 7038
    protected function notify_student_submission_copied(stdClass $submission)
7039
    {
1 efrain 7040
        global $DB, $USER;
7041
 
7042
        $adminconfig = $this->get_admin_config();
7043
        // Use the same setting for this - no need for another one.
7044
        if (empty($adminconfig->submissionreceipts)) {
7045
            // No need to do anything.
7046
            return;
7047
        }
7048
        if ($submission->userid) {
1254 ariadna 7049
            $user = $DB->get_record('user', array('id' => $submission->userid), '*', MUST_EXIST);
1 efrain 7050
        } else {
7051
            $user = $USER;
7052
        }
1254 ariadna 7053
        $this->send_notification(
7054
            $user,
7055
            $user,
7056
            'submissioncopied',
7057
            'assign_notification',
7058
            $submission->timemodified
7059
        );
1 efrain 7060
    }
7061
    /**
7062
     * Notify student upon successful submission.
7063
     *
7064
     * @param stdClass $submission
7065
     * @return void
7066
     */
1254 ariadna 7067
    protected function notify_student_submission_receipt(stdClass $submission)
7068
    {
1 efrain 7069
        global $DB, $USER;
7070
 
7071
        $adminconfig = $this->get_admin_config();
7072
        if (empty($adminconfig->submissionreceipts)) {
7073
            // No need to do anything.
7074
            return;
7075
        }
7076
        if ($submission->userid) {
1254 ariadna 7077
            $user = $DB->get_record('user', array('id' => $submission->userid), '*', MUST_EXIST);
1 efrain 7078
        } else {
7079
            $user = $USER;
7080
        }
7081
        if ($submission->userid == $USER->id) {
1254 ariadna 7082
            $this->send_notification(
7083
                core_user::get_noreply_user(),
7084
                $user,
7085
                'submissionreceipt',
7086
                'assign_notification',
7087
                $submission->timemodified
7088
            );
1 efrain 7089
        } else {
1254 ariadna 7090
            $this->send_notification(
7091
                $USER,
7092
                $user,
7093
                'submissionreceiptother',
7094
                'assign_notification',
7095
                $submission->timemodified
7096
            );
1 efrain 7097
        }
7098
    }
7099
 
7100
    /**
7101
     * Send notifications to graders upon student submissions.
7102
     *
7103
     * @param stdClass $submission
7104
     * @return void
7105
     */
1254 ariadna 7106
    protected function notify_graders(stdClass $submission)
7107
    {
1 efrain 7108
        global $DB, $USER;
7109
 
7110
        $instance = $this->get_instance();
7111
 
7112
        $late = $instance->duedate && ($instance->duedate < time());
7113
 
7114
        if (!$instance->sendnotifications && !($late && $instance->sendlatenotifications)) {
7115
            // No need to do anything.
7116
            return;
7117
        }
7118
 
7119
        if ($submission->userid) {
1254 ariadna 7120
            $user = $DB->get_record('user', array('id' => $submission->userid), '*', MUST_EXIST);
1 efrain 7121
        } else {
7122
            $user = $USER;
7123
        }
7124
 
7125
        if ($notifyusers = $this->get_notifiable_users($user->id)) {
7126
            foreach ($notifyusers as $notifyuser) {
1254 ariadna 7127
                $this->send_notification(
7128
                    $user,
7129
                    $notifyuser,
7130
                    'gradersubmissionupdated',
7131
                    'assign_notification',
7132
                    $submission->timemodified
7133
                );
1 efrain 7134
            }
7135
        }
7136
    }
7137
 
7138
    /**
7139
     * Submit a submission for grading.
7140
     *
7141
     * @param stdClass $data - The form data
7142
     * @param array $notices - List of error messages to display on an error condition.
7143
     * @return bool Return false if the submission was not submitted.
7144
     */
1254 ariadna 7145
    public function submit_for_grading($data, $notices)
7146
    {
1 efrain 7147
        global $USER;
7148
 
7149
        $userid = $USER->id;
7150
        if (!empty($data->userid)) {
7151
            $userid = $data->userid;
7152
        }
7153
        // Need submit permission to submit an assignment.
7154
        if ($userid == $USER->id) {
7155
            require_capability('mod/assign:submit', $this->context);
7156
        } else {
7157
            if (!$this->can_edit_submission($userid, $USER->id)) {
7158
                throw new \moodle_exception('nopermission');
7159
            }
7160
        }
7161
 
7162
        $instance = $this->get_instance();
7163
 
7164
        if ($instance->teamsubmission) {
7165
            $submission = $this->get_group_submission($userid, 0, true);
7166
        } else {
7167
            $submission = $this->get_user_submission($userid, true);
7168
        }
7169
 
7170
        if (!$this->submissions_open($userid)) {
7171
            $notices[] = get_string('submissionsclosed', 'assign');
7172
            return false;
7173
        }
7174
 
7175
        $adminconfig = $this->get_admin_config();
7176
 
7177
        $submissionstatement = '';
7178
        if ($instance->requiresubmissionstatement) {
7179
            $submissionstatement = $this->get_submissionstatement($adminconfig, $instance, $this->context);
7180
        }
7181
 
1254 ariadna 7182
        if (
7183
            !empty($submissionstatement) && $instance->requiresubmissionstatement
7184
            && empty($data->submissionstatement) && $USER->id == $userid
7185
        ) {
1 efrain 7186
            return false;
7187
        }
7188
 
7189
        if ($submission->status != ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
7190
            // Give each submission plugin a chance to process the submission.
7191
            $plugins = $this->get_submission_plugins();
7192
            foreach ($plugins as $plugin) {
7193
                if ($plugin->is_enabled() && $plugin->is_visible()) {
7194
                    $plugin->submit_for_grading($submission);
7195
                }
7196
            }
7197
 
7198
            $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
7199
            $this->update_submission($submission, $userid, true, $instance->teamsubmission);
7200
            $completion = new completion_info($this->get_course());
7201
            if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
1254 ariadna 7202
                $this->update_activity_completion_records(
7203
                    $instance->teamsubmission,
7204
                    $instance->requireallteammemberssubmit,
7205
                    $submission,
7206
                    $userid,
7207
                    COMPLETION_COMPLETE,
7208
                    $completion
7209
                );
1 efrain 7210
            }
7211
 
7212
            if (!empty($data->submissionstatement) && $USER->id == $userid) {
7213
                \mod_assign\event\statement_accepted::create_from_submission($this, $submission)->trigger();
7214
            }
7215
            $this->notify_graders($submission);
7216
            $this->notify_student_submission_receipt($submission);
7217
 
7218
            \mod_assign\event\assessable_submitted::create_from_submission($this, $submission, false)->trigger();
7219
 
7220
            return true;
7221
        }
7222
        $notices[] = get_string('submissionsclosed', 'assign');
7223
        return false;
7224
    }
7225
 
7226
    /**
7227
     * A students submission is submitted for grading by a teacher.
7228
     *
7229
     * @return bool
7230
     */
1254 ariadna 7231
    protected function process_submit_other_for_grading($mform, $notices)
7232
    {
1 efrain 7233
        global $USER, $CFG;
7234
 
7235
        require_sesskey();
7236
 
7237
        $userid = optional_param('userid', $USER->id, PARAM_INT);
7238
 
7239
        if (!$this->submissions_open($userid)) {
7240
            $notices[] = get_string('submissionsclosed', 'assign');
7241
            return false;
7242
        }
7243
        $data = new stdClass();
7244
        $data->userid = $userid;
7245
        return $this->submit_for_grading($data, $notices);
7246
    }
7247
 
7248
    /**
7249
     * Assignment submission is processed before grading.
7250
     *
7251
     * @param moodleform|null $mform If validation failed when submitting this form - this is the moodleform.
7252
     *               It can be null.
7253
     * @return bool Return false if the validation fails. This affects which page is displayed next.
7254
     */
1254 ariadna 7255
    protected function process_submit_for_grading($mform, $notices)
7256
    {
1 efrain 7257
        global $CFG;
7258
 
7259
        require_once($CFG->dirroot . '/mod/assign/submissionconfirmform.php');
7260
        require_sesskey();
7261
 
7262
        if (!$this->submissions_open()) {
7263
            $notices[] = get_string('submissionsclosed', 'assign');
7264
            return false;
7265
        }
7266
 
7267
        $data = new stdClass();
7268
        $adminconfig = $this->get_admin_config();
7269
        $requiresubmissionstatement = $this->get_instance()->requiresubmissionstatement;
7270
 
7271
        $submissionstatement = '';
7272
        if ($requiresubmissionstatement) {
7273
            $submissionstatement = $this->get_submissionstatement($adminconfig, $this->get_instance(), $this->get_context());
7274
        }
7275
 
7276
        // If we get back an empty submission statement, we have to set $requiredsubmisisonstatement to false to prevent
7277
        // that the submission statement checkbox will be displayed.
7278
        if (empty($submissionstatement)) {
7279
            $requiresubmissionstatement = false;
7280
        }
7281
 
7282
        if ($mform == null) {
1254 ariadna 7283
            $mform = new mod_assign_confirm_submission_form(null, array(
7284
                $requiresubmissionstatement,
7285
                $submissionstatement,
7286
                $this->get_course_module()->id,
7287
                $data
7288
            ));
1 efrain 7289
        }
7290
 
7291
        $data = $mform->get_data();
7292
        if (!$mform->is_cancelled()) {
7293
            if ($mform->get_data() == false) {
7294
                return false;
7295
            }
7296
            return $this->submit_for_grading($data, $notices);
7297
        }
7298
        return true;
7299
    }
7300
 
7301
    /**
7302
     * Save the extension date for a single user.
7303
     *
7304
     * @param int $userid The user id
7305
     * @param mixed $extensionduedate Either an integer date or null
7306
     * @return boolean
7307
     */
1254 ariadna 7308
    public function save_user_extension($userid, $extensionduedate)
7309
    {
1 efrain 7310
        global $DB;
7311
 
7312
        // Need submit permission to submit an assignment.
7313
        require_capability('mod/assign:grantextension', $this->context);
7314
 
7315
        if (!is_enrolled($this->get_course_context(), $userid)) {
7316
            return false;
7317
        }
7318
        if (!has_capability('mod/assign:submit', $this->context, $userid)) {
7319
            return false;
7320
        }
7321
 
7322
        if ($this->get_instance()->duedate && $extensionduedate) {
7323
            if ($this->get_instance()->duedate > $extensionduedate) {
7324
                return false;
7325
            }
7326
        }
7327
        if ($this->get_instance()->allowsubmissionsfromdate && $extensionduedate) {
7328
            if ($this->get_instance()->allowsubmissionsfromdate > $extensionduedate) {
7329
                return false;
7330
            }
7331
        }
7332
 
7333
        $flags = $this->get_user_flags($userid, true);
7334
        $flags->extensionduedate = $extensionduedate;
7335
 
7336
        $result = $this->update_user_flags($flags);
7337
 
7338
        if ($result) {
7339
            \mod_assign\event\extension_granted::create_from_assign($this, $userid)->trigger();
7340
        }
7341
        return $result;
7342
    }
7343
 
7344
    /**
7345
     * Save extension date.
7346
     *
7347
     * @param moodleform $mform The submitted form
7348
     * @return boolean
7349
     */
1254 ariadna 7350
    protected function process_save_extension(&$mform)
7351
    {
1 efrain 7352
        global $DB, $CFG;
7353
 
7354
        // Include extension form.
7355
        require_once($CFG->dirroot . '/mod/assign/extensionform.php');
7356
        require_sesskey();
7357
 
7358
        $users = optional_param('userid', 0, PARAM_INT);
7359
        if (!$users) {
7360
            $users = required_param('selectedusers', PARAM_SEQUENCE);
7361
        }
7362
        $userlist = explode(',', $users);
7363
 
7364
        $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate');
7365
        $maxoverride = array('allowsubmissionsfromdate' => 0, 'duedate' => 0, 'cutoffdate' => 0);
7366
        foreach ($userlist as $userid) {
7367
            // To validate extension date with users overrides.
7368
            $override = $this->override_exists($userid);
7369
            foreach ($keys as $key) {
7370
                if ($override->{$key}) {
7371
                    if ($maxoverride[$key] < $override->{$key}) {
7372
                        $maxoverride[$key] = $override->{$key};
7373
                    }
7374
                } else if ($maxoverride[$key] < $this->get_instance()->{$key}) {
7375
                    $maxoverride[$key] = $this->get_instance()->{$key};
7376
                }
7377
            }
7378
        }
7379
        foreach ($keys as $key) {
7380
            if ($maxoverride[$key]) {
7381
                $this->get_instance()->{$key} = $maxoverride[$key];
7382
            }
7383
        }
7384
 
7385
        $formparams = array(
7386
            'instance' => $this->get_instance(),
7387
            'assign' => $this,
7388
            'userlist' => $userlist
7389
        );
7390
 
7391
        $mform = new mod_assign_extension_form(null, $formparams);
7392
 
7393
        if ($mform->is_cancelled()) {
7394
            return true;
7395
        }
7396
 
7397
        if ($formdata = $mform->get_data()) {
7398
            if (!empty($formdata->selectedusers)) {
7399
                $users = explode(',', $formdata->selectedusers);
7400
                $result = true;
7401
                foreach ($users as $userid) {
7402
                    $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
7403
                    $result = $this->save_user_extension($user->id, $formdata->extensionduedate) && $result;
7404
                }
7405
                return $result;
7406
            }
7407
            if (!empty($formdata->userid)) {
7408
                $user = $DB->get_record('user', array('id' => $formdata->userid), '*', MUST_EXIST);
7409
                return $this->save_user_extension($user->id, $formdata->extensionduedate);
7410
            }
7411
        }
7412
 
7413
        return false;
7414
    }
7415
 
7416
    /**
7417
     * Save quick grades.
7418
     *
7419
     * @return string The result of the save operation
7420
     */
1254 ariadna 7421
    protected function process_save_quick_grades()
7422
    {
1 efrain 7423
        global $USER, $DB, $CFG;
7424
 
7425
        // Need grade permission.
7426
        require_capability('mod/assign:grade', $this->context);
7427
        require_sesskey();
7428
 
7429
        // Make sure advanced grading is disabled.
7430
        $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
7431
        $controller = $gradingmanager->get_active_controller();
7432
        if (!empty($controller)) {
7433
            $message = get_string('errorquickgradingvsadvancedgrading', 'assign');
7434
            $this->set_error_message($message);
7435
            return $message;
7436
        }
7437
 
7438
        $users = array();
7439
        // First check all the last modified values.
7440
        $currentgroup = groups_get_activity_group($this->get_course_module(), true);
7441
        $participants = $this->list_participants($currentgroup, true);
7442
 
7443
        // Gets a list of possible users and look for values based upon that.
7444
        foreach ($participants as $userid => $unused) {
7445
            $modified = optional_param('grademodified_' . $userid, -1, PARAM_INT);
7446
            $attemptnumber = optional_param('gradeattempt_' . $userid, -1, PARAM_INT);
7447
            // Gather the userid, updated grade and last modified value.
7448
            $record = new stdClass();
7449
            $record->userid = $userid;
7450
            if ($modified >= 0) {
7451
                $record->grade = unformat_float(optional_param('quickgrade_' . $record->userid, -1, PARAM_TEXT));
1254 ariadna 7452
                $record->workflowstate = optional_param('quickgrade_' . $record->userid . '_workflowstate', false, PARAM_ALPHA);
7453
                $record->allocatedmarker = optional_param('quickgrade_' . $record->userid . '_allocatedmarker', false, PARAM_INT);
1 efrain 7454
            } else {
7455
                // This user was not in the grading table.
7456
                continue;
7457
            }
7458
            $record->attemptnumber = $attemptnumber;
7459
            $record->lastmodified = $modified;
1254 ariadna 7460
            $record->gradinginfo = grade_get_grades(
7461
                $this->get_course()->id,
7462
                'mod',
7463
                'assign',
7464
                $this->get_instance()->id,
7465
                array($userid)
7466
            );
1 efrain 7467
            $users[$userid] = $record;
7468
        }
7469
 
7470
        if (empty($users)) {
7471
            $message = get_string('nousersselected', 'assign');
7472
            $this->set_error_message($message);
7473
            return $message;
7474
        }
7475
 
7476
        list($userids, $params) = $DB->get_in_or_equal(array_keys($users), SQL_PARAMS_NAMED);
7477
        $params['assignid1'] = $this->get_instance()->id;
7478
        $params['assignid2'] = $this->get_instance()->id;
7479
 
7480
        // Check them all for currency.
7481
        $grademaxattempt = 'SELECT s.userid, s.attemptnumber AS maxattempt
7482
                              FROM {assign_submission} s
7483
                             WHERE s.assignment = :assignid1 AND s.latest = 1';
7484
 
7485
        $sql = 'SELECT u.id AS userid, g.grade AS grade, g.timemodified AS lastmodified,
7486
                       uf.workflowstate, uf.allocatedmarker, gmx.maxattempt AS attemptnumber
7487
                  FROM {user} u
7488
             LEFT JOIN ( ' . $grademaxattempt . ' ) gmx ON u.id = gmx.userid
7489
             LEFT JOIN {assign_grades} g ON
7490
                       u.id = g.userid AND
7491
                       g.assignment = :assignid2 AND
7492
                       g.attemptnumber = gmx.maxattempt
7493
             LEFT JOIN {assign_user_flags} uf ON uf.assignment = g.assignment AND uf.userid = g.userid
7494
                 WHERE u.id ' . $userids;
7495
        $currentgrades = $DB->get_recordset_sql($sql, $params);
7496
 
7497
        $modifiedusers = array();
7498
        foreach ($currentgrades as $current) {
7499
            $modified = $users[(int)$current->userid];
7500
            $grade = $this->get_user_grade($modified->userid, false);
7501
            // Check to see if the grade column was even visible.
7502
            $gradecolpresent = optional_param('quickgrade_' . $modified->userid, false, PARAM_INT) !== false;
7503
 
7504
            // Check to see if the outcomes were modified.
7505
            if ($CFG->enableoutcomes) {
7506
                foreach ($modified->gradinginfo->outcomes as $outcomeid => $outcome) {
7507
                    $oldoutcome = $outcome->grades[$modified->userid]->grade;
7508
                    $paramname = 'outcome_' . $outcomeid . '_' . $modified->userid;
7509
                    $newoutcome = optional_param($paramname, -1, PARAM_FLOAT);
7510
                    // Check to see if the outcome column was even visible.
7511
                    $outcomecolpresent = optional_param($paramname, false, PARAM_FLOAT) !== false;
7512
                    if ($outcomecolpresent && ($oldoutcome != $newoutcome)) {
7513
                        // Can't check modified time for outcomes because it is not reported.
7514
                        $modifiedusers[$modified->userid] = $modified;
7515
                        continue;
7516
                    }
7517
                }
7518
            }
7519
 
7520
            // Let plugins participate.
7521
            foreach ($this->feedbackplugins as $plugin) {
7522
                if ($plugin->is_visible() && $plugin->is_enabled() && $plugin->supports_quickgrading()) {
7523
                    // The plugins must handle is_quickgrading_modified correctly - ie
7524
                    // handle hidden columns.
7525
                    if ($plugin->is_quickgrading_modified($modified->userid, $grade)) {
7526
                        if ((int)$current->lastmodified > (int)$modified->lastmodified) {
7527
                            $message = get_string('errorrecordmodified', 'assign');
7528
                            $this->set_error_message($message);
7529
                            return $message;
7530
                        } else {
7531
                            $modifiedusers[$modified->userid] = $modified;
7532
                            continue;
7533
                        }
7534
                    }
7535
                }
7536
            }
7537
 
7538
            if (($current->grade < 0 || $current->grade === null) &&
1254 ariadna 7539
                ($modified->grade < 0 || $modified->grade === null)
7540
            ) {
1 efrain 7541
                // Different ways to indicate no grade.
7542
                $modified->grade = $current->grade; // Keep existing grade.
7543
            }
7544
            // Treat 0 and null as different values.
7545
            if ($current->grade !== null) {
7546
                $current->grade = floatval($current->grade);
7547
            }
7548
            $gradechanged = $gradecolpresent && grade_floats_different($current->grade, $modified->grade);
7549
            $markingallocationchanged = $this->get_instance()->markingworkflow &&
1254 ariadna 7550
                $this->get_instance()->markingallocation &&
7551
                ($modified->allocatedmarker !== false) &&
7552
                ($current->allocatedmarker != $modified->allocatedmarker);
1 efrain 7553
            $workflowstatechanged = $this->get_instance()->markingworkflow &&
1254 ariadna 7554
                ($modified->workflowstate !== false) &&
7555
                ($current->workflowstate != $modified->workflowstate);
1 efrain 7556
            if ($gradechanged || $markingallocationchanged || $workflowstatechanged) {
7557
                // Grade changed.
7558
                if ($this->grading_disabled($modified->userid)) {
7559
                    continue;
7560
                }
7561
                $badmodified = (int)$current->lastmodified > (int)$modified->lastmodified;
7562
                $badattempt = (int)$current->attemptnumber != (int)$modified->attemptnumber;
7563
                if ($badmodified || $badattempt) {
7564
                    // Error - record has been modified since viewing the page.
7565
                    $message = get_string('errorrecordmodified', 'assign');
7566
                    $this->set_error_message($message);
7567
                    return $message;
7568
                } else {
7569
                    $modifiedusers[$modified->userid] = $modified;
7570
                }
7571
            }
7572
        }
7573
        $currentgrades->close();
7574
 
7575
        $adminconfig = $this->get_admin_config();
7576
        $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
7577
 
7578
        // Ok - ready to process the updates.
7579
        foreach ($modifiedusers as $userid => $modified) {
7580
            $grade = $this->get_user_grade($userid, true);
7581
            $flags = $this->get_user_flags($userid, true);
1254 ariadna 7582
            $grade->grade = grade_floatval(unformat_float($modified->grade));
7583
            $grade->grader = $USER->id;
1 efrain 7584
            $gradecolpresent = optional_param('quickgrade_' . $userid, false, PARAM_INT) !== false;
7585
 
7586
            // Save plugins data.
7587
            foreach ($this->feedbackplugins as $plugin) {
7588
                if ($plugin->is_visible() && $plugin->is_enabled() && $plugin->supports_quickgrading()) {
7589
                    $plugin->save_quickgrading_changes($userid, $grade);
7590
                    if (('assignfeedback_' . $plugin->get_type()) == $gradebookplugin) {
7591
                        // This is the feedback plugin chose to push comments to the gradebook.
7592
                        $grade->feedbacktext = $plugin->text_for_gradebook($grade);
7593
                        $grade->feedbackformat = $plugin->format_for_gradebook($grade);
7594
                        $grade->feedbackfiles = $plugin->files_for_gradebook($grade);
7595
                    }
7596
                }
7597
            }
7598
 
7599
            // These will be set to false if they are not present in the quickgrading
7600
            // form (e.g. column hidden).
7601
            $workflowstatemodified = ($modified->workflowstate !== false) &&
1254 ariadna 7602
                ($flags->workflowstate != $modified->workflowstate);
1 efrain 7603
 
7604
            $allocatedmarkermodified = ($modified->allocatedmarker !== false) &&
1254 ariadna 7605
                ($flags->allocatedmarker != $modified->allocatedmarker);
1 efrain 7606
 
7607
            if ($workflowstatemodified) {
7608
                $flags->workflowstate = $modified->workflowstate;
7609
            }
7610
            if ($allocatedmarkermodified) {
7611
                $flags->allocatedmarker = $modified->allocatedmarker;
7612
            }
7613
            if ($workflowstatemodified || $allocatedmarkermodified) {
7614
                if ($this->update_user_flags($flags) && $workflowstatemodified) {
7615
                    $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
7616
                    \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $flags->workflowstate)->trigger();
7617
                }
7618
            }
7619
            $this->update_grade($grade);
7620
 
7621
            // Allow teachers to skip sending notifications.
7622
            if (optional_param('sendstudentnotifications', true, PARAM_BOOL)) {
7623
                $this->notify_grade_modified($grade, true);
7624
            }
7625
 
7626
            // Save outcomes.
7627
            if ($CFG->enableoutcomes) {
7628
                $data = array();
7629
                foreach ($modified->gradinginfo->outcomes as $outcomeid => $outcome) {
7630
                    $oldoutcome = $outcome->grades[$modified->userid]->grade;
7631
                    $paramname = 'outcome_' . $outcomeid . '_' . $modified->userid;
7632
                    // This will be false if the input was not in the quickgrading
7633
                    // form (e.g. column hidden).
7634
                    $newoutcome = optional_param($paramname, false, PARAM_INT);
7635
                    if ($newoutcome !== false && ($oldoutcome != $newoutcome)) {
7636
                        $data[$outcomeid] = $newoutcome;
7637
                    }
7638
                }
7639
                if (count($data) > 0) {
1254 ariadna 7640
                    grade_update_outcomes(
7641
                        'mod/assign',
7642
                        $this->course->id,
7643
                        'mod',
7644
                        'assign',
7645
                        $this->get_instance()->id,
7646
                        $userid,
7647
                        $data
7648
                    );
1 efrain 7649
                }
7650
            }
7651
        }
7652
 
7653
        return get_string('quickgradingchangessaved', 'assign');
7654
    }
7655
 
7656
    /**
7657
     * Reveal student identities to markers (and the gradebook).
7658
     *
7659
     * @return void
7660
     */
1254 ariadna 7661
    public function reveal_identities()
7662
    {
1 efrain 7663
        global $DB;
7664
 
7665
        require_capability('mod/assign:revealidentities', $this->context);
7666
 
7667
        if ($this->get_instance()->revealidentities || empty($this->get_instance()->blindmarking)) {
7668
            return false;
7669
        }
7670
 
7671
        // Update the assignment record.
7672
        $update = new stdClass();
7673
        $update->id = $this->get_instance()->id;
7674
        $update->revealidentities = 1;
7675
        $DB->update_record('assign', $update);
7676
 
7677
        // Refresh the instance data.
7678
        $this->instance = null;
7679
 
7680
        // Release the grades to the gradebook.
7681
        // First create the column in the gradebook.
7682
        $this->update_gradebook(false, $this->get_course_module()->id);
7683
 
7684
        // Now release all grades.
7685
 
7686
        $adminconfig = $this->get_admin_config();
7687
        $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
7688
        $gradebookplugin = str_replace('assignfeedback_', '', $gradebookplugin);
1254 ariadna 7689
        $grades = $DB->get_records('assign_grades', array('assignment' => $this->get_instance()->id));
1 efrain 7690
 
7691
        $plugin = $this->get_feedback_plugin_by_type($gradebookplugin);
7692
 
7693
        foreach ($grades as $grade) {
7694
            // Fetch any comments for this student.
7695
            if ($plugin && $plugin->is_enabled() && $plugin->is_visible()) {
7696
                $grade->feedbacktext = $plugin->text_for_gradebook($grade);
7697
                $grade->feedbackformat = $plugin->format_for_gradebook($grade);
7698
                $grade->feedbackfiles = $plugin->files_for_gradebook($grade);
7699
            }
7700
            $this->gradebook_item_update(null, $grade);
7701
        }
7702
 
7703
        \mod_assign\event\identities_revealed::create_from_assign($this)->trigger();
7704
    }
7705
 
7706
    /**
7707
     * Reveal student identities to markers (and the gradebook).
7708
     *
7709
     * @return void
7710
     */
1254 ariadna 7711
    protected function process_reveal_identities()
7712
    {
1 efrain 7713
 
7714
        if (!confirm_sesskey()) {
7715
            return false;
7716
        }
7717
 
7718
        return $this->reveal_identities();
7719
    }
7720
 
7721
 
7722
    /**
7723
     * Save grading options.
7724
     *
7725
     * @return void
7726
     */
1254 ariadna 7727
    protected function process_save_grading_options()
7728
    {
1 efrain 7729
        global $USER, $CFG;
7730
 
7731
        // Include grading options form.
7732
        require_once($CFG->dirroot . '/mod/assign/gradingoptionsform.php');
7733
 
7734
        // Need submit permission to submit an assignment.
7735
        $this->require_view_grades();
7736
        require_sesskey();
7737
 
7738
        // Is advanced grading enabled?
7739
        $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
7740
        $controller = $gradingmanager->get_active_controller();
7741
        $showquickgrading = empty($controller);
7742
        if (!is_null($this->context)) {
7743
            $showonlyactiveenrolopt = has_capability('moodle/course:viewsuspendedusers', $this->context);
7744
        } else {
7745
            $showonlyactiveenrolopt = false;
7746
        }
7747
 
7748
        $markingallocation = $this->get_instance()->markingworkflow &&
7749
            $this->get_instance()->markingallocation &&
7750
            has_capability('mod/assign:manageallocations', $this->context);
7751
        // Get markers to use in drop lists.
7752
        $markingallocationoptions = array();
7753
        if ($markingallocation) {
7754
            $markingallocationoptions[''] = get_string('filternone', 'assign');
7755
            $markingallocationoptions[ASSIGN_MARKER_FILTER_NO_MARKER] = get_string('markerfilternomarker', 'assign');
7756
            list($sort, $params) = users_order_by_sql('u');
7757
            // Only enrolled users could be assigned as potential markers.
7758
            $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
7759
            foreach ($markers as $marker) {
7760
                $markingallocationoptions[$marker->id] = fullname($marker);
7761
            }
7762
        }
7763
 
7764
        // Get marking states to show in form.
7765
        $markingworkflowoptions = $this->get_marking_workflow_filters();
7766
 
1254 ariadna 7767
        $gradingoptionsparams = array(
7768
            'cm' => $this->get_course_module()->id,
7769
            'contextid' => $this->context->id,
7770
            'userid' => $USER->id,
7771
            'submissionsenabled' => $this->is_any_submission_plugin_enabled(),
7772
            'showquickgrading' => $showquickgrading,
7773
            'quickgrading' => false,
7774
            'markingworkflowopt' => $markingworkflowoptions,
7775
            'markingallocationopt' => $markingallocationoptions,
7776
            'showonlyactiveenrolopt' => $showonlyactiveenrolopt,
7777
            'showonlyactiveenrol' => $this->show_only_active_users(),
7778
            'downloadasfolders' => get_user_preferences('assign_downloadasfolders', 1)
7779
        );
1 efrain 7780
        $mform = new mod_assign_grading_options_form(null, $gradingoptionsparams);
7781
        if ($formdata = $mform->get_data()) {
7782
            set_user_preference('assign_perpage', $formdata->perpage);
7783
            if (isset($formdata->filter)) {
7784
                set_user_preference('assign_filter', $formdata->filter);
7785
            }
7786
            if (isset($formdata->markerfilter)) {
7787
                set_user_preference('assign_markerfilter', $formdata->markerfilter);
7788
            }
7789
            if (isset($formdata->workflowfilter)) {
7790
                set_user_preference('assign_workflowfilter', $formdata->workflowfilter);
7791
            }
7792
            if ($showquickgrading) {
7793
                set_user_preference('assign_quickgrading', isset($formdata->quickgrading));
7794
            }
7795
            if (isset($formdata->downloadasfolders)) {
7796
                set_user_preference('assign_downloadasfolders', 1); // Enabled.
7797
            } else {
7798
                set_user_preference('assign_downloadasfolders', 0); // Disabled.
7799
            }
7800
            if (!empty($showonlyactiveenrolopt)) {
7801
                $showonlyactiveenrol = isset($formdata->showonlyactiveenrol);
7802
                set_user_preference('grade_report_showonlyactiveenrol', $showonlyactiveenrol);
7803
                $this->showonlyactiveenrol = $showonlyactiveenrol;
7804
            }
7805
        }
7806
    }
7807
 
7808
    /**
7809
     * @deprecated since 2.7
7810
     */
1254 ariadna 7811
    public function format_grade_for_log()
7812
    {
1 efrain 7813
        throw new coding_exception(__FUNCTION__ . ' has been deprecated, please do not use it any more');
7814
    }
7815
 
7816
    /**
7817
     * @deprecated since 2.7
7818
     */
1254 ariadna 7819
    public function format_submission_for_log()
7820
    {
1 efrain 7821
        throw new coding_exception(__FUNCTION__ . ' has been deprecated, please do not use it any more');
7822
    }
7823
 
7824
    /**
7825
     * Require a valid sess key and then call copy_previous_attempt.
7826
     *
7827
     * @param  array $notices Any error messages that should be shown
7828
     *                        to the user at the top of the edit submission form.
7829
     * @return bool
7830
     */
1254 ariadna 7831
    protected function process_copy_previous_attempt(&$notices)
7832
    {
1 efrain 7833
        require_sesskey();
7834
 
7835
        return $this->copy_previous_attempt($notices);
7836
    }
7837
 
7838
    /**
7839
     * Copy the current assignment submission from the last submitted attempt.
7840
     *
7841
     * @param  array $notices Any error messages that should be shown
7842
     *                        to the user at the top of the edit submission form.
7843
     * @return bool
7844
     */
1254 ariadna 7845
    public function copy_previous_attempt(&$notices)
7846
    {
1 efrain 7847
        global $USER, $CFG;
7848
 
7849
        require_capability('mod/assign:submit', $this->context);
7850
 
7851
        $instance = $this->get_instance();
7852
        if ($instance->teamsubmission) {
7853
            $submission = $this->get_group_submission($USER->id, 0, true);
7854
        } else {
7855
            $submission = $this->get_user_submission($USER->id, true);
7856
        }
7857
        if (!$submission || $submission->status != ASSIGN_SUBMISSION_STATUS_REOPENED) {
7858
            $notices[] = get_string('submissionnotcopiedinvalidstatus', 'assign');
7859
            return false;
7860
        }
7861
        $flags = $this->get_user_flags($USER->id, false);
7862
 
7863
        // Get the flags to check if it is locked.
7864
        if ($flags && $flags->locked) {
7865
            $notices[] = get_string('submissionslocked', 'assign');
7866
            return false;
7867
        }
7868
        if ($instance->submissiondrafts) {
7869
            $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
7870
        } else {
7871
            $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
7872
        }
7873
        $this->update_submission($submission, $USER->id, true, $instance->teamsubmission);
7874
 
7875
        // Find the previous submission.
7876
        if ($instance->teamsubmission) {
7877
            $previoussubmission = $this->get_group_submission($USER->id, 0, true, $submission->attemptnumber - 1);
7878
        } else {
7879
            $previoussubmission = $this->get_user_submission($USER->id, true, $submission->attemptnumber - 1);
7880
        }
7881
 
7882
        if (!$previoussubmission) {
7883
            // There was no previous submission so there is nothing else to do.
7884
            return true;
7885
        }
7886
 
7887
        $pluginerror = false;
7888
        foreach ($this->get_submission_plugins() as $plugin) {
7889
            if ($plugin->is_visible() && $plugin->is_enabled()) {
7890
                if (!$plugin->copy_submission($previoussubmission, $submission)) {
7891
                    $notices[] = $plugin->get_error();
7892
                    $pluginerror = true;
7893
                }
7894
            }
7895
        }
7896
        if ($pluginerror) {
7897
            return false;
7898
        }
7899
 
7900
        \mod_assign\event\submission_duplicated::create_from_submission($this, $submission)->trigger();
7901
 
7902
        $complete = COMPLETION_INCOMPLETE;
7903
        if ($submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
7904
            $complete = COMPLETION_COMPLETE;
7905
        }
7906
        $completion = new completion_info($this->get_course());
7907
        if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
1254 ariadna 7908
            $this->update_activity_completion_records(
7909
                $instance->teamsubmission,
7910
                $instance->requireallteammemberssubmit,
7911
                $submission,
7912
                $USER->id,
7913
                $complete,
7914
                $completion
7915
            );
1 efrain 7916
        }
7917
 
7918
        if (!$instance->submissiondrafts) {
7919
            // There is a case for not notifying the student about the submission copy,
7920
            // but it provides a record of the event and if they then cancel editing it
7921
            // is clear that the submission was copied.
7922
            $this->notify_student_submission_copied($submission);
7923
            $this->notify_graders($submission);
7924
 
7925
            // The same logic applies here - we could not notify teachers,
7926
            // but then they would wonder why there are submitted assignments
7927
            // and they haven't been notified.
7928
            \mod_assign\event\assessable_submitted::create_from_submission($this, $submission, true)->trigger();
7929
        }
7930
        return true;
7931
    }
7932
 
7933
    /**
7934
     * Determine if the current submission is empty or not.
7935
     *
7936
     * @param submission $submission the students submission record to check.
7937
     * @return bool
7938
     */
1254 ariadna 7939
    public function submission_empty($submission)
7940
    {
1 efrain 7941
        $allempty = true;
7942
 
7943
        foreach ($this->submissionplugins as $plugin) {
7944
            if ($plugin->is_enabled() && $plugin->is_visible()) {
7945
                if (!$allempty || !$plugin->is_empty($submission)) {
7946
                    $allempty = false;
7947
                }
7948
            }
7949
        }
7950
        return $allempty;
7951
    }
7952
 
7953
    /**
7954
     * Determine if a new submission is empty or not
7955
     *
7956
     * @param stdClass $data Submission data
7957
     * @return bool
7958
     */
1254 ariadna 7959
    public function new_submission_empty($data)
7960
    {
1 efrain 7961
        foreach ($this->submissionplugins as $plugin) {
1254 ariadna 7962
            if (
7963
                $plugin->is_enabled() && $plugin->is_visible() && $plugin->allow_submissions() &&
7964
                !$plugin->submission_is_empty($data)
7965
            ) {
1 efrain 7966
                return false;
7967
            }
7968
        }
7969
        return true;
7970
    }
7971
 
7972
    /**
7973
     * Save assignment submission for the current user.
7974
     *
7975
     * @param  stdClass $data
7976
     * @param  array $notices Any error messages that should be shown
7977
     *                        to the user.
7978
     * @return bool
7979
     */
1254 ariadna 7980
    public function save_submission(stdClass $data, &$notices)
7981
    {
1 efrain 7982
        global $CFG, $USER, $DB;
7983
 
7984
        $userid = $USER->id;
7985
        if (!empty($data->userid)) {
7986
            $userid = $data->userid;
7987
        }
7988
 
1254 ariadna 7989
        $user = clone ($USER);
1 efrain 7990
        if ($userid == $USER->id) {
7991
            require_capability('mod/assign:submit', $this->context);
7992
        } else {
1254 ariadna 7993
            $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
1 efrain 7994
            if (!$this->can_edit_submission($userid, $USER->id)) {
7995
                throw new \moodle_exception('nopermission');
7996
            }
7997
        }
7998
        $instance = $this->get_instance();
7999
 
8000
        if ($instance->teamsubmission) {
8001
            $submission = $this->get_group_submission($userid, 0, true);
8002
        } else {
8003
            $submission = $this->get_user_submission($userid, true);
8004
        }
8005
 
8006
        if ($this->new_submission_empty($data)) {
8007
            $notices[] = get_string('submissionempty', 'mod_assign');
8008
            return false;
8009
        }
8010
 
8011
        // Check that no one has modified the submission since we started looking at it.
8012
        if (isset($data->lastmodified) && ($submission->timemodified > $data->lastmodified)) {
8013
            // Another user has submitted something. Notify the current user.
8014
            if ($submission->status !== ASSIGN_SUBMISSION_STATUS_NEW) {
8015
                $notices[] = $instance->teamsubmission ? get_string('submissionmodifiedgroup', 'mod_assign')
1254 ariadna 8016
                    : get_string('submissionmodified', 'mod_assign');
1 efrain 8017
                return false;
8018
            }
8019
        }
8020
 
8021
        if ($instance->submissiondrafts) {
8022
            $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
8023
        } else {
8024
            $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
8025
        }
8026
 
8027
        $flags = $this->get_user_flags($userid, false);
8028
 
8029
        // Get the flags to check if it is locked.
8030
        if ($flags && $flags->locked) {
8031
            throw new \moodle_exception('submissionslocked', 'assign');
8032
            return true;
8033
        }
8034
 
8035
        $pluginerror = false;
8036
        foreach ($this->submissionplugins as $plugin) {
8037
            if ($plugin->is_enabled() && $plugin->is_visible()) {
8038
                if (!$plugin->save($submission, $data)) {
8039
                    $notices[] = $plugin->get_error();
8040
                    $pluginerror = true;
8041
                }
8042
            }
8043
        }
8044
 
8045
        $allempty = $this->submission_empty($submission);
8046
        if ($pluginerror || $allempty) {
8047
            if ($allempty) {
8048
                $notices[] = get_string('submissionempty', 'mod_assign');
8049
            }
8050
            return false;
8051
        }
8052
 
8053
        $this->update_submission($submission, $userid, true, $instance->teamsubmission);
8054
        $users = [$userid];
8055
 
8056
        if ($instance->teamsubmission && !$instance->requireallteammemberssubmit) {
8057
            $team = $this->get_submission_group_members($submission->groupid, true);
8058
 
8059
            foreach ($team as $member) {
8060
                if ($member->id != $userid) {
1254 ariadna 8061
                    $membersubmission = clone ($submission);
1 efrain 8062
                    $this->update_submission($membersubmission, $member->id, true, $instance->teamsubmission);
8063
                    $users[] = $member->id;
8064
                }
8065
            }
8066
        }
8067
 
8068
        $complete = COMPLETION_INCOMPLETE;
8069
        if ($submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
8070
            $complete = COMPLETION_COMPLETE;
8071
        }
8072
 
8073
        $completion = new completion_info($this->get_course());
8074
        if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
8075
            foreach ($users as $id) {
8076
                $completion->update_state($this->get_course_module(), $complete, $id);
8077
            }
8078
        }
8079
 
8080
        // Logging.
8081
        if (isset($data->submissionstatement) && ($userid == $USER->id)) {
8082
            \mod_assign\event\statement_accepted::create_from_submission($this, $submission)->trigger();
8083
        }
8084
 
8085
        if (!$instance->submissiondrafts) {
8086
            $this->notify_student_submission_receipt($submission);
8087
            $this->notify_graders($submission);
8088
            \mod_assign\event\assessable_submitted::create_from_submission($this, $submission, true)->trigger();
8089
        }
8090
        return true;
8091
    }
8092
 
8093
    /**
8094
     * Save assignment submission.
8095
     *
8096
     * @param  moodleform $mform
8097
     * @param  array $notices Any error messages that should be shown
8098
     *                        to the user at the top of the edit submission form.
8099
     * @return bool
8100
     */
1254 ariadna 8101
    protected function process_save_submission(&$mform, &$notices)
8102
    {
1 efrain 8103
        global $CFG, $USER;
8104
 
8105
        // Include submission form.
8106
        require_once($CFG->dirroot . '/mod/assign/submission_form.php');
8107
 
8108
        $userid = optional_param('userid', $USER->id, PARAM_INT);
8109
        // Need submit permission to submit an assignment.
8110
        require_sesskey();
8111
        if (!$this->submissions_open($userid)) {
8112
            $notices[] = get_string('duedatereached', 'assign');
8113
            return false;
8114
        }
8115
        $instance = $this->get_instance();
8116
 
8117
        $data = new stdClass();
8118
        $data->userid = $userid;
8119
        $mform = new mod_assign_submission_form(null, array($this, $data));
8120
        if ($mform->is_cancelled()) {
8121
            return true;
8122
        }
8123
        if ($data = $mform->get_data()) {
8124
            return $this->save_submission($data, $notices);
8125
        }
8126
        return false;
8127
    }
8128
 
8129
 
8130
    /**
8131
     * Determine if this users grade can be edited.
8132
     *
8133
     * @param int $userid - The student userid
8134
     * @param bool $checkworkflow - whether to include a check for the workflow state.
8135
     * @param stdClass $gradinginfo - optional, allow gradinginfo to be passed for performance.
8136
     * @return bool $gradingdisabled
8137
     */
1254 ariadna 8138
    public function grading_disabled($userid, $checkworkflow = true, $gradinginfo = null)
8139
    {
1 efrain 8140
        if ($checkworkflow && $this->get_instance()->markingworkflow) {
8141
            $grade = $this->get_user_grade($userid, false);
8142
            $validstates = $this->get_marking_workflow_states_for_current_user();
8143
            if (!empty($grade) && !empty($grade->workflowstate) && !array_key_exists($grade->workflowstate, $validstates)) {
8144
                return true;
8145
            }
8146
        }
8147
 
8148
        if (is_null($gradinginfo)) {
1254 ariadna 8149
            $gradinginfo = grade_get_grades(
8150
                $this->get_course()->id,
1 efrain 8151
                'mod',
8152
                'assign',
8153
                $this->get_instance()->id,
1254 ariadna 8154
                array($userid)
8155
            );
1 efrain 8156
        }
8157
 
8158
        if (!$gradinginfo) {
8159
            return false;
8160
        }
8161
 
8162
        if (!isset($gradinginfo->items[0]->grades[$userid])) {
8163
            return false;
8164
        }
8165
        $gradingdisabled = $gradinginfo->items[0]->grades[$userid]->locked ||
1254 ariadna 8166
            $gradinginfo->items[0]->grades[$userid]->overridden;
1 efrain 8167
        return $gradingdisabled;
8168
    }
8169
 
8170
 
8171
    /**
8172
     * Get an instance of a grading form if advanced grading is enabled.
8173
     * This is specific to the assignment, marker and student.
8174
     *
8175
     * @param int $userid - The student userid
8176
     * @param stdClass|false $grade - The grade record
8177
     * @param bool $gradingdisabled
8178
     * @return mixed gradingform_instance|null $gradinginstance
8179
     */
1254 ariadna 8180
    protected function get_grading_instance($userid, $grade, $gradingdisabled)
8181
    {
1 efrain 8182
        global $CFG, $USER;
8183
 
8184
        $grademenu = make_grades_menu($this->get_instance()->grade);
8185
        $allowgradedecimals = $this->get_instance()->grade > 0;
8186
 
8187
        $advancedgradingwarning = false;
8188
        $gradingmanager = get_grading_manager($this->context, 'mod_assign', 'submissions');
8189
        $gradinginstance = null;
8190
        if ($gradingmethod = $gradingmanager->get_active_method()) {
8191
            $controller = $gradingmanager->get_controller($gradingmethod);
8192
            if ($controller->is_form_available()) {
8193
                $itemid = null;
8194
                if ($grade) {
8195
                    $itemid = $grade->id;
8196
                }
8197
                if ($gradingdisabled && $itemid) {
8198
                    $gradinginstance = $controller->get_current_instance($USER->id, $itemid);
8199
                } else if (!$gradingdisabled) {
8200
                    $instanceid = optional_param('advancedgradinginstanceid', 0, PARAM_INT);
1254 ariadna 8201
                    $gradinginstance = $controller->get_or_create_instance(
8202
                        $instanceid,
8203
                        $USER->id,
8204
                        $itemid
8205
                    );
1 efrain 8206
                }
8207
            } else {
8208
                $advancedgradingwarning = $controller->form_unavailable_notification();
8209
            }
8210
        }
8211
        if ($gradinginstance) {
8212
            $gradinginstance->get_controller()->set_grade_range($grademenu, $allowgradedecimals);
8213
        }
8214
        return $gradinginstance;
8215
    }
8216
 
8217
    /**
8218
     * Add elements to grade form.
8219
     *
8220
     * @param MoodleQuickForm $mform
8221
     * @param stdClass $data
8222
     * @param array $params
8223
     * @return void
8224
     */
1254 ariadna 8225
    public function add_grade_form_elements(MoodleQuickForm $mform, stdClass $data, $params)
8226
    {
1 efrain 8227
        global $USER, $CFG, $SESSION;
8228
        $settings = $this->get_instance();
8229
 
8230
        $rownum = isset($params['rownum']) ? $params['rownum'] : 0;
8231
        $last = isset($params['last']) ? $params['last'] : true;
8232
        $useridlistid = isset($params['useridlistid']) ? $params['useridlistid'] : 0;
8233
        $userid = isset($params['userid']) ? $params['userid'] : 0;
8234
        $attemptnumber = isset($params['attemptnumber']) ? $params['attemptnumber'] : 0;
8235
        $gradingpanel = !empty($params['gradingpanel']);
8236
        $bothids = ($userid && $useridlistid);
8237
 
8238
        if (!$userid || $bothids) {
8239
            $useridlist = $this->get_grading_userid_list(true, $useridlistid);
8240
        } else {
8241
            $useridlist = array($userid);
8242
            $rownum = 0;
8243
            $useridlistid = '';
8244
        }
8245
 
8246
        $userid = $useridlist[$rownum];
8247
        // We need to create a grade record matching this attempt number
8248
        // or the feedback plugin will have no way to know what is the correct attempt.
8249
        $grade = $this->get_user_grade($userid, true, $attemptnumber);
8250
 
8251
        $submission = null;
8252
        if ($this->get_instance()->teamsubmission) {
8253
            $submission = $this->get_group_submission($userid, 0, false, $attemptnumber);
8254
        } else {
8255
            $submission = $this->get_user_submission($userid, false, $attemptnumber);
8256
        }
8257
 
8258
        // Add advanced grading.
8259
        $gradingdisabled = $this->grading_disabled($userid);
8260
        $gradinginstance = $this->get_grading_instance($userid, $grade, $gradingdisabled);
8261
 
8262
        $mform->addElement('header', 'gradeheader', get_string('gradenoun'));
8263
        if ($gradinginstance) {
1254 ariadna 8264
            $gradingelement = $mform->addElement(
8265
                'grading',
8266
                'advancedgrading',
8267
                get_string('gradenoun') . ':',
8268
                array('gradinginstance' => $gradinginstance)
8269
            );
1 efrain 8270
            if ($gradingdisabled) {
8271
                $gradingelement->freeze();
8272
            } else {
8273
                $mform->addElement('hidden', 'advancedgradinginstanceid', $gradinginstance->get_id());
8274
                $mform->setType('advancedgradinginstanceid', PARAM_INT);
8275
            }
8276
        } else {
8277
            // Use simple direct grading.
8278
            if ($this->get_instance()->grade > 0) {
8279
                $name = get_string('gradeoutof', 'assign', $this->get_instance()->grade);
8280
                if (!$gradingdisabled) {
8281
                    $gradingelement = $mform->addElement('text', 'grade', $name);
8282
                    $mform->addHelpButton('grade', 'gradeoutofhelp', 'assign');
8283
                    $mform->setType('grade', PARAM_RAW);
8284
                } else {
8285
                    $strgradelocked = get_string('gradelocked', 'assign');
8286
                    $mform->addElement('static', 'gradedisabled', $name, $strgradelocked);
8287
                    $mform->addHelpButton('gradedisabled', 'gradeoutofhelp', 'assign');
8288
                }
8289
            } else {
8290
                $grademenu = array(-1 => get_string("nograde")) + make_grades_menu($this->get_instance()->grade);
8291
                if (count($grademenu) > 1) {
8292
                    $gradingelement = $mform->addElement('select', 'grade', get_string('gradenoun') . ':', $grademenu);
8293
 
8294
                    // The grade is already formatted with format_float so it needs to be converted back to an integer.
8295
                    if (!empty($data->grade)) {
8296
                        $data->grade = (int)unformat_float($data->grade);
8297
                    }
8298
                    $mform->setType('grade', PARAM_INT);
8299
                    if ($gradingdisabled) {
8300
                        $gradingelement->freeze();
8301
                    }
8302
                }
8303
            }
8304
        }
8305
 
1254 ariadna 8306
        $gradinginfo = grade_get_grades(
8307
            $this->get_course()->id,
8308
            'mod',
8309
            'assign',
8310
            $this->get_instance()->id,
8311
            $userid
8312
        );
1 efrain 8313
        if (!empty($CFG->enableoutcomes)) {
8314
            foreach ($gradinginfo->outcomes as $index => $outcome) {
8315
                $options = make_grades_menu(-$outcome->scaleid);
8316
                $options[0] = get_string('nooutcome', 'grades');
8317
                if ($outcome->grades[$userid]->locked) {
1254 ariadna 8318
                    $mform->addElement(
8319
                        'static',
8320
                        'outcome_' . $index . '[' . $userid . ']',
8321
                        $outcome->name . ':',
8322
                        $options[$outcome->grades[$userid]->grade]
8323
                    );
1 efrain 8324
                } else {
1254 ariadna 8325
                    $attributes = array('id' => 'menuoutcome_' . $index);
8326
                    $mform->addElement(
8327
                        'select',
8328
                        'outcome_' . $index . '[' . $userid . ']',
8329
                        $outcome->name . ':',
8330
                        $options,
8331
                        $attributes
8332
                    );
1 efrain 8333
                    $mform->setType('outcome_' . $index . '[' . $userid . ']', PARAM_INT);
1254 ariadna 8334
                    $mform->setDefault(
8335
                        'outcome_' . $index . '[' . $userid . ']',
8336
                        $outcome->grades[$userid]->grade
8337
                    );
1 efrain 8338
                }
8339
            }
8340
        }
8341
 
8342
        $capabilitylist = array('gradereport/grader:view', 'moodle/grade:viewall');
8343
        $usergrade = get_string('notgraded', 'assign');
8344
        if (has_all_capabilities($capabilitylist, $this->get_course_context())) {
1254 ariadna 8345
            $urlparams = array('id' => $this->get_course()->id);
1 efrain 8346
            $url = new moodle_url('/grade/report/grader/index.php', $urlparams);
8347
            if (isset($gradinginfo->items[0]->grades[$userid]->grade)) {
8348
                $usergrade = $gradinginfo->items[0]->grades[$userid]->str_grade;
8349
            }
8350
            $gradestring = $this->get_renderer()->action_link($url, $usergrade);
8351
        } else {
1254 ariadna 8352
            if (
8353
                isset($gradinginfo->items[0]->grades[$userid]) &&
8354
                !$gradinginfo->items[0]->grades[$userid]->hidden
8355
            ) {
1 efrain 8356
                $usergrade = $gradinginfo->items[0]->grades[$userid]->str_grade;
8357
            }
8358
            $gradestring = $usergrade;
8359
        }
8360
 
8361
        if ($this->get_instance()->markingworkflow) {
8362
            $states = $this->get_marking_workflow_states_for_current_user();
8363
            $options = array('' => get_string('markingworkflowstatenotmarked', 'assign')) + $states;
8364
            $select = $mform->addElement('select', 'workflowstate', get_string('markingworkflowstate', 'assign'), $options);
8365
            $mform->addHelpButton('workflowstate', 'markingworkflowstate', 'assign');
8366
            if (!empty($data->workflowstate) && !array_key_exists($data->workflowstate, $states)) {
8367
                // In a workflow state that user should not be able to change, so freeze workflow selector.
8368
                // Have to add the state so it shows in the frozen selector.
8369
                $allworkflowstates = $this->get_all_marking_workflow_states();
8370
                $select->addOption($allworkflowstates[$data->workflowstate], $data->workflowstate);
8371
                $mform->freeze('workflowstate');
8372
            }
8373
            $gradingstatus = $this->get_grading_status($userid);
8374
            if ($gradingstatus != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
8375
                if ($grade->grade && $grade->grade != -1) {
8376
                    if ($settings->grade > 0) {
8377
                        $assigngradestring = format_float($grade->grade, $this->get_grade_item()->get_decimals());
8378
                    } else {
8379
                        $assigngradestring = make_grades_menu($settings->grade)[grade_floatval($grade->grade)];
8380
                    }
8381
                    $assigngradestring = html_writer::span($assigngradestring, 'currentgrade');
8382
                    $label = get_string('currentassigngrade', 'assign');
8383
                    $mform->addElement('static', 'currentassigngrade', $label, $assigngradestring);
8384
                }
8385
            }
8386
        }
8387
 
1254 ariadna 8388
        if (
8389
            $this->get_instance()->markingworkflow &&
1 efrain 8390
            $this->get_instance()->markingallocation &&
1254 ariadna 8391
            has_capability('mod/assign:manageallocations', $this->context)
8392
        ) {
1 efrain 8393
 
8394
            list($sort, $params) = users_order_by_sql('u');
8395
            // Only enrolled users could be assigned as potential markers.
8396
            $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
8397
            $markerlist = array('' =>  get_string('choosemarker', 'assign'));
8398
            $viewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
8399
            foreach ($markers as $marker) {
8400
                $markerlist[$marker->id] = fullname($marker, $viewfullnames);
8401
            }
8402
            $mform->addElement('select', 'allocatedmarker', get_string('allocatedmarker', 'assign'), $markerlist);
8403
            $mform->addHelpButton('allocatedmarker', 'allocatedmarker', 'assign');
8404
            $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW);
8405
            $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW);
8406
            $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE);
8407
            $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_RELEASED);
8408
        }
8409
 
8410
        $gradestring = '<span class="currentgrade">' . $gradestring . '</span>';
8411
        $mform->addElement('static', 'currentgrade', get_string('currentgrade', 'assign'), $gradestring);
8412
 
8413
        if (count($useridlist) > 1) {
1254 ariadna 8414
            $strparams = array('current' => $rownum + 1, 'total' => count($useridlist));
1 efrain 8415
            $name = get_string('outof', 'assign', $strparams);
8416
            $mform->addElement('static', 'gradingstudent', get_string('gradingstudent', 'assign'), $name);
8417
        }
8418
 
8419
        // Let feedback plugins add elements to the grading form.
8420
        $this->add_plugin_grade_elements($grade, $mform, $data, $userid);
8421
 
8422
        // Hidden params.
8423
        $mform->addElement('hidden', 'id', $this->get_course_module()->id);
8424
        $mform->setType('id', PARAM_INT);
8425
        $mform->addElement('hidden', 'rownum', $rownum);
8426
        $mform->setType('rownum', PARAM_INT);
8427
        $mform->setConstant('rownum', $rownum);
8428
        $mform->addElement('hidden', 'useridlistid', $useridlistid);
8429
        $mform->setType('useridlistid', PARAM_ALPHANUM);
8430
        $mform->addElement('hidden', 'attemptnumber', $attemptnumber);
8431
        $mform->setType('attemptnumber', PARAM_INT);
8432
        $mform->addElement('hidden', 'ajax', optional_param('ajax', 0, PARAM_INT));
8433
        $mform->setType('ajax', PARAM_INT);
8434
        $mform->addElement('hidden', 'userid', optional_param('userid', 0, PARAM_INT));
8435
        $mform->setType('userid', PARAM_INT);
8436
 
8437
        if ($this->get_instance()->teamsubmission) {
8438
            $mform->addElement('header', 'groupsubmissionsettings', get_string('groupsubmissionsettings', 'assign'));
8439
            $mform->addElement('selectyesno', 'applytoall', get_string('applytoteam', 'assign'));
8440
            $mform->setDefault('applytoall', 1);
8441
        }
8442
 
8443
        // Do not show if we are editing a previous attempt.
8444
        if (($attemptnumber == -1 ||
1254 ariadna 8445
                ($attemptnumber + 1) == count($this->get_all_submissions($userid))) &&
8446
            $this->get_instance()->attemptreopenmethod != ASSIGN_ATTEMPT_REOPEN_METHOD_NONE
8447
        ) {
1 efrain 8448
            $mform->addElement('header', 'attemptsettings', get_string('attemptsettings', 'assign'));
8449
            $attemptreopenmethod = get_string('attemptreopenmethod_' . $this->get_instance()->attemptreopenmethod, 'assign');
8450
            $mform->addElement('static', 'attemptreopenmethod', get_string('attemptreopenmethod', 'assign'), $attemptreopenmethod);
8451
 
8452
            $attemptnumber = 0;
8453
            if ($submission) {
8454
                $attemptnumber = $submission->attemptnumber;
8455
            }
8456
            $maxattempts = $this->get_instance()->maxattempts;
8457
            if ($maxattempts == ASSIGN_UNLIMITED_ATTEMPTS) {
8458
                $maxattempts = get_string('unlimitedattempts', 'assign');
8459
            }
8460
            $mform->addelement('static', 'maxattemptslabel', get_string('maxattempts', 'assign'), $maxattempts);
8461
            $mform->addelement('static', 'attemptnumberlabel', get_string('attemptnumber', 'assign'), $attemptnumber + 1);
8462
 
8463
            $ismanual = $this->get_instance()->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL;
8464
            $issubmission = !empty($submission);
8465
            $isunlimited = $this->get_instance()->maxattempts == ASSIGN_UNLIMITED_ATTEMPTS;
1254 ariadna 8466
            $islessthanmaxattempts = $issubmission && ($submission->attemptnumber < ($this->get_instance()->maxattempts - 1));
1 efrain 8467
 
8468
            if ($ismanual && (!$issubmission || $isunlimited || $islessthanmaxattempts)) {
8469
                $mform->addElement('selectyesno', 'addattempt', get_string('addattempt', 'assign'));
8470
                $mform->setDefault('addattempt', 0);
8471
            }
8472
        }
8473
        if (!$gradingpanel) {
8474
            $mform->addElement('selectyesno', 'sendstudentnotifications', get_string('sendstudentnotifications', 'assign'));
8475
        } else {
8476
            $mform->addElement('hidden', 'sendstudentnotifications', get_string('sendstudentnotifications', 'assign'));
8477
            $mform->setType('sendstudentnotifications', PARAM_BOOL);
8478
        }
8479
        // Get assignment visibility information for student.
8480
        $modinfo = get_fast_modinfo($settings->course, $userid);
8481
        $cm = $modinfo->get_cm($this->get_course_module()->id);
8482
 
8483
        // Don't allow notification to be sent if the student can't access the assignment,
8484
        // or until in "Released" state if using marking workflow.
8485
        if (!$cm->uservisible) {
8486
            $mform->setDefault('sendstudentnotifications', 0);
8487
            $mform->freeze('sendstudentnotifications');
8488
        } else if ($this->get_instance()->markingworkflow) {
8489
            $mform->setDefault('sendstudentnotifications', 0);
8490
            if (!$gradingpanel) {
8491
                $mform->disabledIf('sendstudentnotifications', 'workflowstate', 'neq', ASSIGN_MARKING_WORKFLOW_STATE_RELEASED);
8492
            }
8493
        } else {
8494
            $mform->setDefault('sendstudentnotifications', $this->get_instance()->sendstudentnotifications);
8495
        }
8496
 
8497
        $mform->addElement('hidden', 'action', 'submitgrade');
8498
        $mform->setType('action', PARAM_ALPHA);
8499
 
8500
        if (!$gradingpanel) {
8501
 
8502
            $buttonarray = array();
8503
            $name = get_string('savechanges', 'assign');
8504
            $buttonarray[] = $mform->createElement('submit', 'savegrade', $name);
8505
            if (!$last) {
8506
                $name = get_string('savenext', 'assign');
8507
                $buttonarray[] = $mform->createElement('submit', 'saveandshownext', $name);
8508
            }
8509
            $buttonarray[] = $mform->createElement('cancel', 'cancelbutton', get_string('cancel'));
8510
            $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false);
8511
            $mform->closeHeaderBefore('buttonar');
8512
            $buttonarray = array();
8513
 
8514
            if ($rownum > 0) {
8515
                $name = get_string('previous', 'assign');
8516
                $buttonarray[] = $mform->createElement('submit', 'nosaveandprevious', $name);
8517
            }
8518
 
8519
            if (!$last) {
8520
                $name = get_string('nosavebutnext', 'assign');
8521
                $buttonarray[] = $mform->createElement('submit', 'nosaveandnext', $name);
8522
            }
8523
            if (!empty($buttonarray)) {
8524
                $mform->addGroup($buttonarray, 'navar', '', array(' '), false);
8525
            }
8526
        }
8527
        // The grading form does not work well with shortforms.
8528
        $mform->setDisableShortforms();
8529
    }
8530
 
8531
    /**
8532
     * Add elements in submission plugin form.
8533
     *
8534
     * @param mixed $submission stdClass|null
8535
     * @param MoodleQuickForm $mform
8536
     * @param stdClass $data
8537
     * @param int $userid The current userid (same as $USER->id)
8538
     * @return void
8539
     */
1254 ariadna 8540
    protected function add_plugin_submission_elements(
8541
        $submission,
8542
        MoodleQuickForm $mform,
8543
        stdClass $data,
8544
        $userid
8545
    ) {
1 efrain 8546
        foreach ($this->submissionplugins as $plugin) {
8547
            if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->allow_submissions()) {
8548
                $plugin->get_form_elements_for_user($submission, $mform, $data, $userid);
8549
            }
8550
        }
8551
    }
8552
 
8553
    /**
8554
     * Check if feedback plugins installed are enabled.
8555
     *
8556
     * @return bool
8557
     */
1254 ariadna 8558
    public function is_any_feedback_plugin_enabled()
8559
    {
1 efrain 8560
        if (!isset($this->cache['any_feedback_plugin_enabled'])) {
8561
            $this->cache['any_feedback_plugin_enabled'] = false;
8562
            foreach ($this->feedbackplugins as $plugin) {
8563
                if ($plugin->is_enabled() && $plugin->is_visible()) {
8564
                    $this->cache['any_feedback_plugin_enabled'] = true;
8565
                    break;
8566
                }
8567
            }
8568
        }
8569
 
8570
        return $this->cache['any_feedback_plugin_enabled'];
8571
    }
8572
 
8573
    /**
8574
     * Check if submission plugins installed are enabled.
8575
     *
8576
     * @return bool
8577
     */
1254 ariadna 8578
    public function is_any_submission_plugin_enabled()
8579
    {
1 efrain 8580
        if (!isset($this->cache['any_submission_plugin_enabled'])) {
8581
            $this->cache['any_submission_plugin_enabled'] = false;
8582
            foreach ($this->submissionplugins as $plugin) {
8583
                if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->allow_submissions()) {
8584
                    $this->cache['any_submission_plugin_enabled'] = true;
8585
                    break;
8586
                }
8587
            }
8588
        }
8589
 
8590
        return $this->cache['any_submission_plugin_enabled'];
8591
    }
8592
 
8593
    /**
8594
     * Add elements to submission form.
8595
     * @param MoodleQuickForm $mform
8596
     * @param stdClass $data
8597
     * @return void
8598
     */
1254 ariadna 8599
    public function add_submission_form_elements(MoodleQuickForm $mform, stdClass $data)
8600
    {
1 efrain 8601
        global $USER;
8602
 
8603
        $userid = $data->userid;
8604
        // Team submissions.
8605
        if ($this->get_instance()->teamsubmission) {
8606
            $submission = $this->get_group_submission($userid, 0, false);
8607
        } else {
8608
            $submission = $this->get_user_submission($userid, false);
8609
        }
8610
 
8611
        // Submission statement.
8612
        $adminconfig = $this->get_admin_config();
8613
        $requiresubmissionstatement = $this->get_instance()->requiresubmissionstatement;
8614
 
8615
        $draftsenabled = $this->get_instance()->submissiondrafts;
8616
        $submissionstatement = '';
8617
 
8618
        if ($requiresubmissionstatement) {
8619
            $submissionstatement = $this->get_submissionstatement($adminconfig, $this->get_instance(), $this->get_context());
8620
        }
8621
 
8622
        // If we get back an empty submission statement, we have to set $requiredsubmisisonstatement to false to prevent
8623
        // that the submission statement checkbox will be displayed.
8624
        if (empty($submissionstatement)) {
8625
            $requiresubmissionstatement = false;
8626
        }
8627
 
8628
        $mform->addElement('header', 'submission header', get_string('addsubmission', 'mod_assign'));
8629
 
8630
        // Only show submission statement if we are editing our own submission.
8631
        if ($requiresubmissionstatement && !$draftsenabled && $userid == $USER->id) {
8632
            $mform->addElement('checkbox', 'submissionstatement', '', $submissionstatement);
1254 ariadna 8633
            $mform->addRule(
8634
                'submissionstatement',
8635
                get_string('submissionstatementrequired', 'mod_assign'),
8636
                'required',
8637
                null,
8638
                'client'
8639
            );
1 efrain 8640
        }
8641
 
8642
        $this->add_plugin_submission_elements($submission, $mform, $data, $userid);
8643
 
8644
        // Hidden params.
8645
        $mform->addElement('hidden', 'id', $this->get_course_module()->id);
8646
        $mform->setType('id', PARAM_INT);
8647
 
8648
        $mform->addElement('hidden', 'userid', $userid);
8649
        $mform->setType('userid', PARAM_INT);
8650
 
8651
        $mform->addElement('hidden', 'action', 'savesubmission');
8652
        $mform->setType('action', PARAM_ALPHA);
8653
    }
8654
 
8655
    /**
8656
     * Remove any data from the current submission.
8657
     *
8658
     * @param int $userid
8659
     * @return boolean
8660
     * @throws coding_exception
8661
     */
1254 ariadna 8662
    public function remove_submission($userid)
8663
    {
1 efrain 8664
        global $USER;
8665
 
8666
        if (!$this->can_edit_submission($userid, $USER->id)) {
8667
            $user = core_user::get_user($userid);
8668
            $message = get_string('usersubmissioncannotberemoved', 'assign', fullname($user));
8669
            $this->set_error_message($message);
8670
            return false;
8671
        }
8672
 
8673
        if ($this->get_instance()->teamsubmission) {
8674
            $submission = $this->get_group_submission($userid, 0, false);
8675
        } else {
8676
            $submission = $this->get_user_submission($userid, false);
8677
        }
8678
 
8679
        if (!$submission) {
8680
            return false;
8681
        }
8682
        $submission->status = $submission->attemptnumber ? ASSIGN_SUBMISSION_STATUS_REOPENED : ASSIGN_SUBMISSION_STATUS_NEW;
8683
        $this->update_submission($submission, $userid, false, $this->get_instance()->teamsubmission);
8684
 
8685
        // Tell each submission plugin we were saved with no data.
8686
        $plugins = $this->get_submission_plugins();
8687
        foreach ($plugins as $plugin) {
8688
            if ($plugin->is_enabled() && $plugin->is_visible()) {
8689
                $plugin->remove($submission);
8690
            }
8691
        }
8692
 
8693
        $completion = new completion_info($this->get_course());
1254 ariadna 8694
        if (
8695
            $completion->is_enabled($this->get_course_module()) &&
8696
            $this->get_instance()->completionsubmit
8697
        ) {
1 efrain 8698
            $completion->update_state($this->get_course_module(), COMPLETION_INCOMPLETE, $userid);
8699
        }
8700
 
8701
        submission_removed::create_from_submission($this, $submission)->trigger();
8702
        submission_status_updated::create_from_submission($this, $submission)->trigger();
8703
        return true;
8704
    }
8705
 
8706
    /**
8707
     * Revert to draft.
8708
     *
8709
     * @param int $userid
8710
     * @return boolean
8711
     */
1254 ariadna 8712
    public function revert_to_draft($userid)
8713
    {
1 efrain 8714
        global $DB, $USER;
8715
 
8716
        // Need grade permission.
8717
        require_capability('mod/assign:grade', $this->context);
8718
 
8719
        if ($this->get_instance()->teamsubmission) {
8720
            $submission = $this->get_group_submission($userid, 0, false);
8721
        } else {
8722
            $submission = $this->get_user_submission($userid, false);
8723
        }
8724
 
8725
        if (!$submission) {
8726
            return false;
8727
        }
8728
        $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
8729
        $this->update_submission($submission, $userid, false, $this->get_instance()->teamsubmission);
8730
 
8731
        // Give each submission plugin a chance to process the reverting to draft.
8732
        $plugins = $this->get_submission_plugins();
8733
        foreach ($plugins as $plugin) {
8734
            if ($plugin->is_enabled() && $plugin->is_visible()) {
8735
                $plugin->revert_to_draft($submission);
8736
            }
8737
        }
8738
        // Update the modified time on the grade (grader modified).
8739
        $grade = $this->get_user_grade($userid, true);
8740
        $grade->grader = $USER->id;
8741
        $this->update_grade($grade);
8742
 
8743
        $completion = new completion_info($this->get_course());
1254 ariadna 8744
        if (
8745
            $completion->is_enabled($this->get_course_module()) &&
8746
            $this->get_instance()->completionsubmit
8747
        ) {
1 efrain 8748
            $completion->update_state($this->get_course_module(), COMPLETION_INCOMPLETE, $userid);
8749
        }
8750
        \mod_assign\event\submission_status_updated::create_from_submission($this, $submission)->trigger();
8751
        return true;
8752
    }
8753
 
8754
    /**
8755
     * Remove the current submission.
8756
     *
8757
     * @param int $userid
8758
     * @return boolean
8759
     */
1254 ariadna 8760
    protected function process_remove_submission($userid = 0)
8761
    {
1 efrain 8762
        require_sesskey();
8763
 
8764
        if (!$userid) {
8765
            $userid = required_param('userid', PARAM_INT);
8766
        }
8767
 
8768
        return $this->remove_submission($userid);
8769
    }
8770
 
8771
    /**
8772
     * Revert to draft.
8773
     * Uses url parameter userid if userid not supplied as a parameter.
8774
     *
8775
     * @param int $userid
8776
     * @return boolean
8777
     */
1254 ariadna 8778
    protected function process_revert_to_draft($userid = 0)
8779
    {
1 efrain 8780
        require_sesskey();
8781
 
8782
        if (!$userid) {
8783
            $userid = required_param('userid', PARAM_INT);
8784
        }
8785
 
8786
        return $this->revert_to_draft($userid);
8787
    }
8788
 
8789
    /**
8790
     * Prevent student updates to this submission
8791
     *
8792
     * @param int $userid
8793
     * @return bool
8794
     */
1254 ariadna 8795
    public function lock_submission($userid)
8796
    {
1 efrain 8797
        global $USER, $DB;
8798
        // Need grade permission.
8799
        require_capability('mod/assign:grade', $this->context);
8800
 
8801
        // Give each submission plugin a chance to process the locking.
8802
        $plugins = $this->get_submission_plugins();
8803
        $submission = $this->get_user_submission($userid, false);
8804
 
8805
        $flags = $this->get_user_flags($userid, true);
8806
        $flags->locked = 1;
8807
        $this->update_user_flags($flags);
8808
 
8809
        foreach ($plugins as $plugin) {
8810
            if ($plugin->is_enabled() && $plugin->is_visible()) {
8811
                $plugin->lock($submission, $flags);
8812
            }
8813
        }
8814
 
8815
        $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
8816
        \mod_assign\event\submission_locked::create_from_user($this, $user)->trigger();
8817
        return true;
8818
    }
8819
 
8820
 
8821
    /**
8822
     * Set the workflow state for multiple users
8823
     *
8824
     * @return void
8825
     */
1254 ariadna 8826
    protected function process_set_batch_marking_workflow_state()
8827
    {
1 efrain 8828
        global $CFG, $DB;
8829
 
8830
        // Include batch marking workflow form.
8831
        require_once($CFG->dirroot . '/mod/assign/batchsetmarkingworkflowstateform.php');
8832
 
8833
        $formparams = array(
8834
            'userscount' => 0,  // This form is never re-displayed, so we don't need to
8835
            'usershtml' => '',  // initialise these parameters with real information.
8836
            'markingworkflowstates' => $this->get_marking_workflow_states_for_current_user()
8837
        );
8838
 
8839
        $mform = new mod_assign_batch_set_marking_workflow_state_form(null, $formparams);
8840
 
8841
        if ($mform->is_cancelled()) {
8842
            return true;
8843
        }
8844
 
8845
        if ($formdata = $mform->get_data()) {
8846
            $useridlist = explode(',', $formdata->selectedusers);
8847
            $state = $formdata->markingworkflowstate;
8848
 
8849
            foreach ($useridlist as $userid) {
8850
                $flags = $this->get_user_flags($userid, true);
8851
 
8852
                $flags->workflowstate = $state;
8853
 
8854
                // Clear the mailed flag if notification is requested, the student hasn't been
8855
                // notified previously, the student can access the assignment, and the state
8856
                // is "Released".
8857
                $modinfo = get_fast_modinfo($this->course, $userid);
8858
                $cm = $modinfo->get_cm($this->get_course_module()->id);
1254 ariadna 8859
                if (
8860
                    $formdata->sendstudentnotifications && $cm->uservisible &&
8861
                    $state == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED
8862
                ) {
1 efrain 8863
                    $flags->mailed = 0;
8864
                }
8865
 
8866
                $gradingdisabled = $this->grading_disabled($userid);
8867
 
8868
                // Will not apply update if user does not have permission to assign this workflow state.
8869
                if (!$gradingdisabled && $this->update_user_flags($flags)) {
8870
                    // Update Gradebook.
8871
                    $grade = $this->get_user_grade($userid, true);
8872
                    // Fetch any feedback for this student.
8873
                    $gradebookplugin = $this->get_admin_config()->feedback_plugin_for_gradebook;
8874
                    $gradebookplugin = str_replace('assignfeedback_', '', $gradebookplugin);
8875
                    $plugin = $this->get_feedback_plugin_by_type($gradebookplugin);
8876
                    if ($plugin && $plugin->is_enabled() && $plugin->is_visible()) {
8877
                        $grade->feedbacktext = $plugin->text_for_gradebook($grade);
8878
                        $grade->feedbackformat = $plugin->format_for_gradebook($grade);
8879
                        $grade->feedbackfiles = $plugin->files_for_gradebook($grade);
8880
                    }
8881
                    $this->update_grade($grade);
8882
                    $assign = clone $this->get_instance();
8883
                    $assign->cmidnumber = $this->get_course_module()->idnumber;
8884
                    // Set assign gradebook feedback plugin status.
8885
                    $assign->gradefeedbackenabled = $this->is_gradebook_feedback_enabled();
8886
 
8887
                    // If markinganonymous is enabled then allow to release grades anonymously.
8888
                    if (isset($assign->markinganonymous) && $assign->markinganonymous == 1) {
8889
                        assign_update_grades($assign, $userid);
8890
                    }
8891
                    $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
8892
                    \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $state)->trigger();
8893
                }
8894
            }
8895
        }
8896
    }
8897
 
8898
    /**
8899
     * Set the marking allocation for multiple users
8900
     *
8901
     * @return void
8902
     */
1254 ariadna 8903
    protected function process_set_batch_marking_allocation()
8904
    {
1 efrain 8905
        global $CFG, $DB;
8906
 
8907
        // Include batch marking allocation form.
8908
        require_once($CFG->dirroot . '/mod/assign/batchsetallocatedmarkerform.php');
8909
 
8910
        $formparams = array(
8911
            'userscount' => 0,  // This form is never re-displayed, so we don't need to
8912
            'usershtml' => ''   // initialise these parameters with real information.
8913
        );
8914
 
8915
        list($sort, $params) = users_order_by_sql('u');
8916
        // Only enrolled users could be assigned as potential markers.
8917
        $markers = get_enrolled_users($this->get_context(), 'mod/assign:grade', 0, 'u.*', $sort);
8918
        $markerlist = array();
8919
        foreach ($markers as $marker) {
8920
            $markerlist[$marker->id] = fullname($marker);
8921
        }
8922
 
8923
        $formparams['markers'] = $markerlist;
8924
 
8925
        $mform = new mod_assign_batch_set_allocatedmarker_form(null, $formparams);
8926
 
8927
        if ($mform->is_cancelled()) {
8928
            return true;
8929
        }
8930
 
8931
        if ($formdata = $mform->get_data()) {
8932
            $useridlist = explode(',', $formdata->selectedusers);
8933
            $marker = $DB->get_record('user', array('id' => $formdata->allocatedmarker), '*', MUST_EXIST);
8934
 
8935
            foreach ($useridlist as $userid) {
8936
                $flags = $this->get_user_flags($userid, true);
1254 ariadna 8937
                if (
8938
                    $flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW ||
1 efrain 8939
                    $flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW ||
8940
                    $flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE ||
1254 ariadna 8941
                    $flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED
8942
                ) {
1 efrain 8943
 
8944
                    continue; // Allocated marker can only be changed in certain workflow states.
8945
                }
8946
 
8947
                $flags->allocatedmarker = $marker->id;
8948
 
8949
                if ($this->update_user_flags($flags)) {
8950
                    $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
8951
                    \mod_assign\event\marker_updated::create_from_marker($this, $user, $marker)->trigger();
8952
                }
8953
            }
8954
        }
8955
    }
8956
 
8957
 
8958
    /**
8959
     * Prevent student updates to this submission.
8960
     * Uses url parameter userid.
8961
     *
8962
     * @param int $userid
8963
     * @return void
8964
     */
1254 ariadna 8965
    protected function process_lock_submission($userid = 0)
8966
    {
1 efrain 8967
 
8968
        require_sesskey();
8969
 
8970
        if (!$userid) {
8971
            $userid = required_param('userid', PARAM_INT);
8972
        }
8973
 
8974
        return $this->lock_submission($userid);
8975
    }
8976
 
8977
    /**
8978
     * Unlock the student submission.
8979
     *
8980
     * @param int $userid
8981
     * @return bool
8982
     */
1254 ariadna 8983
    public function unlock_submission($userid)
8984
    {
1 efrain 8985
        global $USER, $DB;
8986
 
8987
        // Need grade permission.
8988
        require_capability('mod/assign:grade', $this->context);
8989
 
8990
        // Give each submission plugin a chance to process the unlocking.
8991
        $plugins = $this->get_submission_plugins();
8992
        $submission = $this->get_user_submission($userid, false);
8993
 
8994
        $flags = $this->get_user_flags($userid, true);
8995
        $flags->locked = 0;
8996
        $this->update_user_flags($flags);
8997
 
8998
        foreach ($plugins as $plugin) {
8999
            if ($plugin->is_enabled() && $plugin->is_visible()) {
9000
                $plugin->unlock($submission, $flags);
9001
            }
9002
        }
9003
 
9004
        $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
9005
        \mod_assign\event\submission_unlocked::create_from_user($this, $user)->trigger();
9006
        return true;
9007
    }
9008
 
9009
    /**
9010
     * Unlock the student submission.
9011
     * Uses url parameter userid.
9012
     *
9013
     * @param int $userid
9014
     * @return bool
9015
     */
1254 ariadna 9016
    protected function process_unlock_submission($userid = 0)
9017
    {
1 efrain 9018
 
9019
        require_sesskey();
9020
 
9021
        if (!$userid) {
9022
            $userid = required_param('userid', PARAM_INT);
9023
        }
9024
 
9025
        return $this->unlock_submission($userid);
9026
    }
9027
 
9028
    /**
9029
     * Apply a grade from a grading form to a user (may be called multiple times for a group submission).
9030
     *
9031
     * @param stdClass $formdata - the data from the form
9032
     * @param int $userid - the user to apply the grade to
9033
     * @param int $attemptnumber - The attempt number to apply the grade to.
9034
     * @return void
9035
     */
1254 ariadna 9036
    protected function apply_grade_to_user($formdata, $userid, $attemptnumber)
9037
    {
1 efrain 9038
        global $USER, $CFG, $DB;
9039
 
9040
        $grade = $this->get_user_grade($userid, true, $attemptnumber);
9041
        $originalgrade = $grade->grade;
9042
        $gradingdisabled = $this->grading_disabled($userid);
9043
        $gradinginstance = $this->get_grading_instance($userid, $grade, $gradingdisabled);
9044
        if (!$gradingdisabled) {
9045
            if ($gradinginstance) {
1254 ariadna 9046
                $grade->grade = $gradinginstance->submit_and_get_grade(
9047
                    $formdata->advancedgrading,
9048
                    $grade->id
9049
                );
1 efrain 9050
            } else {
9051
                // Handle the case when grade is set to No Grade.
9052
                if (isset($formdata->grade)) {
9053
                    $grade->grade = grade_floatval(unformat_float($formdata->grade));
9054
                }
9055
            }
9056
            if (isset($formdata->workflowstate) || isset($formdata->allocatedmarker)) {
9057
                $flags = $this->get_user_flags($userid, true);
9058
                $oldworkflowstate = $flags->workflowstate;
9059
                $flags->workflowstate = isset($formdata->workflowstate) ? $formdata->workflowstate : $flags->workflowstate;
9060
                $flags->allocatedmarker = isset($formdata->allocatedmarker) ? $formdata->allocatedmarker : $flags->allocatedmarker;
1254 ariadna 9061
                if (
9062
                    $this->update_user_flags($flags) &&
9063
                    isset($formdata->workflowstate) &&
9064
                    $formdata->workflowstate !== $oldworkflowstate
9065
                ) {
1 efrain 9066
                    $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
9067
                    \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $formdata->workflowstate)->trigger();
9068
                }
9069
            }
9070
        }
1254 ariadna 9071
        $grade->grader = $USER->id;
1 efrain 9072
 
9073
        $adminconfig = $this->get_admin_config();
9074
        $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
9075
 
9076
        $feedbackmodified = false;
9077
 
9078
        // Call save in plugins.
9079
        foreach ($this->feedbackplugins as $plugin) {
9080
            if ($plugin->is_enabled() && $plugin->is_visible()) {
9081
                $gradingmodified = $plugin->is_feedback_modified($grade, $formdata);
9082
                if ($gradingmodified) {
9083
                    if (!$plugin->save($grade, $formdata)) {
9084
                        $result = false;
9085
                        throw new \moodle_exception($plugin->get_error());
9086
                    }
9087
                    // If $feedbackmodified is true, keep it true.
9088
                    $feedbackmodified = $feedbackmodified || $gradingmodified;
9089
                }
9090
                if (('assignfeedback_' . $plugin->get_type()) == $gradebookplugin) {
9091
                    // This is the feedback plugin chose to push comments to the gradebook.
9092
                    $grade->feedbacktext = $plugin->text_for_gradebook($grade);
9093
                    $grade->feedbackformat = $plugin->format_for_gradebook($grade);
9094
                    $grade->feedbackfiles = $plugin->files_for_gradebook($grade);
9095
                }
9096
            }
9097
        }
9098
 
9099
        // We do not want to update the timemodified if no grade was added.
1254 ariadna 9100
        if (
9101
            !empty($formdata->addattempt) ||
9102
            ($originalgrade !== null && $originalgrade != -1) ||
9103
            ($grade->grade !== null && $grade->grade != -1) ||
9104
            $feedbackmodified
9105
        ) {
1 efrain 9106
            $this->update_grade($grade, !empty($formdata->addattempt));
9107
        }
9108
 
9109
        // We never send notifications if we have marking workflow and the grade is not released.
1254 ariadna 9110
        if (
9111
            $this->get_instance()->markingworkflow &&
9112
            isset($formdata->workflowstate) &&
9113
            $formdata->workflowstate != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED
9114
        ) {
1 efrain 9115
            $formdata->sendstudentnotifications = false;
9116
        }
9117
 
9118
        // Note the default if not provided for this option is true (e.g. webservices).
9119
        // This is for backwards compatibility.
9120
        if (!isset($formdata->sendstudentnotifications) || $formdata->sendstudentnotifications) {
9121
            $this->notify_grade_modified($grade, true);
9122
        }
9123
    }
9124
 
9125
 
9126
    /**
9127
     * Save outcomes submitted from grading form.
9128
     *
9129
     * @param int $userid
9130
     * @param stdClass $formdata
9131
     * @param int $sourceuserid The user ID under which the outcome data is accessible. This is relevant
9132
     *                          for an outcome set to a user but applied to an entire group.
9133
     */
1254 ariadna 9134
    protected function process_outcomes($userid, $formdata, $sourceuserid = null)
9135
    {
1 efrain 9136
        global $CFG, $USER;
9137
 
9138
        if (empty($CFG->enableoutcomes)) {
9139
            return;
9140
        }
9141
        if ($this->grading_disabled($userid)) {
9142
            return;
9143
        }
9144
 
1254 ariadna 9145
        require_once($CFG->libdir . '/gradelib.php');
1 efrain 9146
 
9147
        $data = array();
1254 ariadna 9148
        $gradinginfo = grade_get_grades(
9149
            $this->get_course()->id,
9150
            'mod',
9151
            'assign',
9152
            $this->get_instance()->id,
9153
            $userid
9154
        );
1 efrain 9155
 
9156
        if (!empty($gradinginfo->outcomes)) {
9157
            foreach ($gradinginfo->outcomes as $index => $oldoutcome) {
1254 ariadna 9158
                $name = 'outcome_' . $index;
1 efrain 9159
                $sourceuserid = $sourceuserid !== null ? $sourceuserid : $userid;
1254 ariadna 9160
                if (
9161
                    isset($formdata->{$name}[$sourceuserid]) &&
9162
                    $oldoutcome->grades[$userid]->grade != $formdata->{$name}[$sourceuserid]
9163
                ) {
1 efrain 9164
                    $data[$index] = $formdata->{$name}[$sourceuserid];
9165
                }
9166
            }
9167
        }
9168
        if (count($data) > 0) {
1254 ariadna 9169
            grade_update_outcomes(
9170
                'mod/assign',
9171
                $this->course->id,
9172
                'mod',
9173
                'assign',
9174
                $this->get_instance()->id,
9175
                $userid,
9176
                $data
9177
            );
1 efrain 9178
        }
9179
    }
9180
 
9181
    /**
9182
     * If the requirements are met - reopen the submission for another attempt.
9183
     * Only call this function when grading the latest attempt.
9184
     *
9185
     * @param int $userid The userid.
9186
     * @param stdClass $submission The submission (may be a group submission).
9187
     * @param bool $addattempt - True if the "allow another attempt" checkbox was checked.
9188
     * @return bool - true if another attempt was added.
9189
     */
1254 ariadna 9190
    protected function reopen_submission_if_required($userid, $submission, $addattempt)
9191
    {
1 efrain 9192
        $instance = $this->get_instance();
9193
        $maxattemptsreached = !empty($submission) &&
1254 ariadna 9194
            $submission->attemptnumber >= ($instance->maxattempts - 1) &&
9195
            $instance->maxattempts != ASSIGN_UNLIMITED_ATTEMPTS;
1 efrain 9196
        $shouldreopen = false;
9197
        if ($instance->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS) {
9198
            // Check the gradetopass from the gradebook.
9199
            $gradeitem = $this->get_grade_item();
9200
            if ($gradeitem) {
9201
                $gradegrade = grade_grade::fetch(array('userid' => $userid, 'itemid' => $gradeitem->id));
9202
 
9203
                // Do not reopen if is_passed returns null, e.g. if there is no pass criterion set.
9204
                if ($gradegrade && ($gradegrade->is_passed() === false)) {
9205
                    $shouldreopen = true;
9206
                }
9207
            }
9208
        }
1254 ariadna 9209
        if (
9210
            $instance->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL &&
9211
            !empty($addattempt)
9212
        ) {
1 efrain 9213
            $shouldreopen = true;
9214
        }
9215
        if ($shouldreopen && !$maxattemptsreached) {
9216
            $this->add_attempt($userid);
9217
            return true;
9218
        }
9219
        return false;
9220
    }
9221
 
9222
    /**
9223
     * Save grade update.
9224
     *
9225
     * @param int $userid
9226
     * @param  stdClass $data
9227
     * @return bool - was the grade saved
9228
     */
1254 ariadna 9229
    public function save_grade($userid, $data)
9230
    {
1 efrain 9231
 
9232
        // Need grade permission.
9233
        require_capability('mod/assign:grade', $this->context);
9234
 
9235
        $instance = $this->get_instance();
9236
        $submission = null;
9237
        if ($instance->teamsubmission) {
9238
            // We need to know what the most recent group submission is.
9239
            // Specifically when determining if we are adding another attempt (we only want to add one attempt per team),
9240
            // and when deciding if we need to update the gradebook with an edited grade.
9241
            $mostrecentsubmission = $this->get_group_submission($userid, 0, false, -1);
9242
            $this->set_most_recent_team_submission($mostrecentsubmission);
9243
            // Get the submission that we are saving grades for. The data attempt number determines which submission attempt.
9244
            $submission = $this->get_group_submission($userid, 0, false, $data->attemptnumber);
9245
        } else {
9246
            $submission = $this->get_user_submission($userid, false, $data->attemptnumber);
9247
        }
9248
        if ($instance->teamsubmission && !empty($data->applytoall)) {
9249
            $groupid = 0;
9250
            if ($this->get_submission_group($userid)) {
9251
                $group = $this->get_submission_group($userid);
9252
                if ($group) {
9253
                    $groupid = $group->id;
9254
                }
9255
            }
9256
            $members = $this->get_submission_group_members($groupid, true, $this->show_only_active_users());
9257
            foreach ($members as $member) {
9258
                // We only want to update the grade for this group submission attempt. The data attempt number could be
9259
                // -1 which may end up in additional attempts being created for each group member instead of just one
9260
                // additional attempt for the group.
9261
                $this->apply_grade_to_user($data, $member->id, $submission->attemptnumber);
9262
                $this->process_outcomes($member->id, $data, $userid);
9263
            }
9264
        } else {
9265
            $this->apply_grade_to_user($data, $userid, $data->attemptnumber);
9266
 
9267
            $this->process_outcomes($userid, $data);
9268
        }
9269
 
9270
        return true;
9271
    }
9272
 
9273
    /**
9274
     * Save grade.
9275
     *
9276
     * @param  moodleform $mform
9277
     * @return bool - was the grade saved
9278
     */
1254 ariadna 9279
    protected function process_save_grade(&$mform)
9280
    {
1 efrain 9281
        global $CFG, $SESSION;
9282
        // Include grade form.
9283
        require_once($CFG->dirroot . '/mod/assign/gradeform.php');
9284
 
9285
        require_sesskey();
9286
 
9287
        $instance = $this->get_instance();
9288
        $rownum = required_param('rownum', PARAM_INT);
9289
        $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT);
9290
        $useridlistid = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
9291
        $userid = optional_param('userid', 0, PARAM_INT);
9292
        if (!$userid) {
9293
            if (empty($SESSION->mod_assign_useridlist[$this->get_useridlist_key($useridlistid)])) {
9294
                // If the userid list is not stored we must not save, as it is possible that the user in a
9295
                // given row position may not be the same now as when the grading page was generated.
9296
                $url = new moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id));
9297
                throw new moodle_exception('useridlistnotcached', 'mod_assign', $url);
9298
            }
9299
            $useridlist = $SESSION->mod_assign_useridlist[$this->get_useridlist_key($useridlistid)];
9300
        } else {
9301
            $useridlist = array($userid);
9302
            $rownum = 0;
9303
        }
9304
 
9305
        $last = false;
9306
        $userid = $useridlist[$rownum];
9307
        if ($rownum == count($useridlist) - 1) {
9308
            $last = true;
9309
        }
9310
 
9311
        $data = new stdClass();
9312
 
1254 ariadna 9313
        $gradeformparams = array(
9314
            'rownum' => $rownum,
9315
            'useridlistid' => $useridlistid,
9316
            'last' => $last,
9317
            'attemptnumber' => $attemptnumber,
9318
            'userid' => $userid
9319
        );
9320
        $mform = new mod_assign_grade_form(
9321
            null,
9322
            array($this, $data, $gradeformparams),
9323
            'post',
9324
            '',
9325
            array('class' => 'gradeform')
9326
        );
1 efrain 9327
 
9328
        if ($formdata = $mform->get_data()) {
9329
            return $this->save_grade($userid, $formdata);
9330
        } else {
9331
            return false;
9332
        }
9333
    }
9334
 
9335
    /**
9336
     * This function is a static wrapper around can_upgrade.
9337
     *
9338
     * @param string $type The plugin type
9339
     * @param int $version The plugin version
9340
     * @return bool
9341
     */
1254 ariadna 9342
    public static function can_upgrade_assignment($type, $version)
9343
    {
1 efrain 9344
        $assignment = new assign(null, null, null);
9345
        return $assignment->can_upgrade($type, $version);
9346
    }
9347
 
9348
    /**
9349
     * This function returns true if it can upgrade an assignment from the 2.2 module.
9350
     *
9351
     * @param string $type The plugin type
9352
     * @param int $version The plugin version
9353
     * @return bool
9354
     */
1254 ariadna 9355
    public function can_upgrade($type, $version)
9356
    {
1 efrain 9357
        if ($type == 'offline' && $version >= 2011112900) {
9358
            return true;
9359
        }
9360
        foreach ($this->submissionplugins as $plugin) {
9361
            if ($plugin->can_upgrade($type, $version)) {
9362
                return true;
9363
            }
9364
        }
9365
        foreach ($this->feedbackplugins as $plugin) {
9366
            if ($plugin->can_upgrade($type, $version)) {
9367
                return true;
9368
            }
9369
        }
9370
        return false;
9371
    }
9372
 
9373
    /**
9374
     * Copy all the files from the old assignment files area to the new one.
9375
     * This is used by the plugin upgrade code.
9376
     *
9377
     * @param int $oldcontextid The old assignment context id
9378
     * @param int $oldcomponent The old assignment component ('assignment')
9379
     * @param int $oldfilearea The old assignment filearea ('submissions')
9380
     * @param int $olditemid The old submissionid (can be null e.g. intro)
9381
     * @param int $newcontextid The new assignment context id
9382
     * @param int $newcomponent The new assignment component ('assignment')
9383
     * @param int $newfilearea The new assignment filearea ('submissions')
9384
     * @param int $newitemid The new submissionid (can be null e.g. intro)
9385
     * @return int The number of files copied
9386
     */
1254 ariadna 9387
    public function copy_area_files_for_upgrade(
9388
        $oldcontextid,
9389
        $oldcomponent,
9390
        $oldfilearea,
9391
        $olditemid,
9392
        $newcontextid,
9393
        $newcomponent,
9394
        $newfilearea,
9395
        $newitemid
9396
    ) {
1 efrain 9397
        // Note, this code is based on some code in filestorage - but that code
9398
        // deleted the old files (which we don't want).
9399
        $count = 0;
9400
 
9401
        $fs = get_file_storage();
9402
 
1254 ariadna 9403
        $oldfiles = $fs->get_area_files(
9404
            $oldcontextid,
9405
            $oldcomponent,
9406
            $oldfilearea,
9407
            $olditemid,
9408
            'id',
9409
            false
9410
        );
1 efrain 9411
        foreach ($oldfiles as $oldfile) {
9412
            $filerecord = new stdClass();
9413
            $filerecord->contextid = $newcontextid;
9414
            $filerecord->component = $newcomponent;
9415
            $filerecord->filearea = $newfilearea;
9416
            $filerecord->itemid = $newitemid;
9417
            $fs->create_file_from_storedfile($filerecord, $oldfile);
9418
            $count += 1;
9419
        }
9420
 
9421
        return $count;
9422
    }
9423
 
9424
    /**
9425
     * Add a new attempt for each user in the list - but reopen each group assignment
9426
     * at most 1 time.
9427
     *
9428
     * @param array $useridlist Array of userids to reopen.
9429
     * @return bool
9430
     */
1254 ariadna 9431
    protected function process_add_attempt_group($useridlist)
9432
    {
1 efrain 9433
        $groupsprocessed = array();
9434
        $result = true;
9435
 
9436
        foreach ($useridlist as $userid) {
9437
            $groupid = 0;
9438
            $group = $this->get_submission_group($userid);
9439
            if ($group) {
9440
                $groupid = $group->id;
9441
            }
9442
 
9443
            if (empty($groupsprocessed[$groupid])) {
9444
                // We need to know what the most recent group submission is.
9445
                // Specifically when determining if we are adding another attempt (we only want to add one attempt per team),
9446
                // and when deciding if we need to update the gradebook with an edited grade.
9447
                $currentsubmission = $this->get_group_submission($userid, 0, false, -1);
9448
                $this->set_most_recent_team_submission($currentsubmission);
9449
                $result = $this->process_add_attempt($userid) && $result;
9450
                $groupsprocessed[$groupid] = true;
9451
            }
9452
        }
9453
        return $result;
9454
    }
9455
 
9456
    /**
9457
     * Check for a sess key and then call add_attempt.
9458
     *
9459
     * @param int $userid int The user to add the attempt for
9460
     * @return bool - true if successful.
9461
     */
1254 ariadna 9462
    protected function process_add_attempt($userid)
9463
    {
1 efrain 9464
        require_sesskey();
9465
 
9466
        return $this->add_attempt($userid);
9467
    }
9468
 
9469
    /**
9470
     * Add a new attempt for a user.
9471
     *
9472
     * @param int $userid int The user to add the attempt for
9473
     * @return bool - true if successful.
9474
     */
1254 ariadna 9475
    protected function add_attempt($userid)
9476
    {
1 efrain 9477
        require_capability('mod/assign:grade', $this->context);
9478
 
9479
        if ($this->get_instance()->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_NONE) {
9480
            return false;
9481
        }
9482
 
9483
        if ($this->get_instance()->teamsubmission) {
9484
            $oldsubmission = $this->get_group_submission($userid, 0, false);
9485
        } else {
9486
            $oldsubmission = $this->get_user_submission($userid, false);
9487
        }
9488
 
9489
        if (!$oldsubmission) {
9490
            return false;
9491
        }
9492
 
9493
        // No more than max attempts allowed.
1254 ariadna 9494
        if (
9495
            $this->get_instance()->maxattempts != ASSIGN_UNLIMITED_ATTEMPTS &&
9496
            $oldsubmission->attemptnumber >= ($this->get_instance()->maxattempts - 1)
9497
        ) {
1 efrain 9498
            return false;
9499
        }
9500
 
9501
        // Create the new submission record for the group/user.
9502
        if ($this->get_instance()->teamsubmission) {
9503
            if (isset($this->mostrecentteamsubmission)) {
9504
                // Team submissions can end up in this function for each user (via save_grade). We don't want to create
9505
                // more than one attempt for the whole team.
9506
                if ($this->mostrecentteamsubmission->attemptnumber == $oldsubmission->attemptnumber) {
9507
                    $newsubmission = $this->get_group_submission($userid, 0, true, $oldsubmission->attemptnumber + 1);
9508
                } else {
9509
                    $newsubmission = $this->get_group_submission($userid, 0, false, $oldsubmission->attemptnumber);
9510
                }
9511
            } else {
9512
                debugging('Please use set_most_recent_team_submission() before calling add_attempt', DEBUG_DEVELOPER);
9513
                $newsubmission = $this->get_group_submission($userid, 0, true, $oldsubmission->attemptnumber + 1);
9514
            }
9515
        } else {
9516
            $newsubmission = $this->get_user_submission($userid, true, $oldsubmission->attemptnumber + 1);
9517
        }
9518
 
9519
        // Set the status of the new attempt to reopened.
9520
        $newsubmission->status = ASSIGN_SUBMISSION_STATUS_REOPENED;
9521
 
9522
        // Give each submission plugin a chance to process the add_attempt.
9523
        $plugins = $this->get_submission_plugins();
9524
        foreach ($plugins as $plugin) {
9525
            if ($plugin->is_enabled() && $plugin->is_visible()) {
9526
                $plugin->add_attempt($oldsubmission, $newsubmission);
9527
            }
9528
        }
9529
 
9530
        $this->update_submission($newsubmission, $userid, false, $this->get_instance()->teamsubmission);
9531
        $flags = $this->get_user_flags($userid, false);
9532
        if (isset($flags->locked) && $flags->locked) { // May not exist.
9533
            $this->process_unlock_submission($userid);
9534
        }
9535
        return true;
9536
    }
9537
 
9538
    /**
9539
     * Get an upto date list of user grades and feedback for the gradebook.
9540
     *
9541
     * @param int $userid int or 0 for all users
9542
     * @return array of grade data formated for the gradebook api
9543
     *         The data required by the gradebook api is userid,
9544
     *                                                   rawgrade,
9545
     *                                                   feedback,
9546
     *                                                   feedbackformat,
9547
     *                                                   usermodified,
9548
     *                                                   dategraded,
9549
     *                                                   datesubmitted
9550
     */
1254 ariadna 9551
    public function get_user_grades_for_gradebook($userid)
9552
    {
1 efrain 9553
        global $DB, $CFG;
9554
        $grades = array();
9555
        $assignmentid = $this->get_instance()->id;
9556
 
9557
        $adminconfig = $this->get_admin_config();
9558
        $gradebookpluginname = $adminconfig->feedback_plugin_for_gradebook;
9559
        $gradebookplugin = null;
9560
 
9561
        // Find the gradebook plugin.
9562
        foreach ($this->feedbackplugins as $plugin) {
9563
            if ($plugin->is_enabled() && $plugin->is_visible()) {
9564
                if (('assignfeedback_' . $plugin->get_type()) == $gradebookpluginname) {
9565
                    $gradebookplugin = $plugin;
9566
                }
9567
            }
9568
        }
9569
        if ($userid) {
9570
            $where = ' WHERE u.id = :userid ';
9571
        } else {
9572
            $where = ' WHERE u.id != :userid ';
9573
        }
9574
 
9575
        // When the gradebook asks us for grades - only return the last attempt for each user.
1254 ariadna 9576
        $params = array(
9577
            'assignid1' => $assignmentid,
9578
            'assignid2' => $assignmentid,
9579
            'userid' => $userid
9580
        );
1 efrain 9581
        $graderesults = $DB->get_recordset_sql('SELECT
9582
                                                    u.id as userid,
9583
                                                    s.timemodified as datesubmitted,
9584
                                                    g.grade as rawgrade,
9585
                                                    g.timemodified as dategraded,
9586
                                                    g.grader as usermodified
9587
                                                FROM {user} u
9588
                                                LEFT JOIN {assign_submission} s
9589
                                                    ON u.id = s.userid and s.assignment = :assignid1 AND
9590
                                                    s.latest = 1
9591
                                                JOIN {assign_grades} g
9592
                                                    ON u.id = g.userid and g.assignment = :assignid2 AND
9593
                                                    g.attemptnumber = s.attemptnumber' .
1254 ariadna 9594
            $where, $params);
1 efrain 9595
 
9596
        foreach ($graderesults as $result) {
9597
            $gradingstatus = $this->get_grading_status($result->userid);
9598
            if (!$this->get_instance()->markingworkflow || $gradingstatus == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
9599
                $gradebookgrade = clone $result;
9600
                // Now get the feedback.
9601
                if ($gradebookplugin) {
9602
                    $grade = $this->get_user_grade($result->userid, false);
9603
                    if ($grade) {
9604
                        $feedbacktext = $gradebookplugin->text_for_gradebook($grade);
9605
                        if (!empty($feedbacktext)) {
9606
                            $gradebookgrade->feedback = $feedbacktext;
9607
                        }
9608
                        $gradebookgrade->feedbackformat = $gradebookplugin->format_for_gradebook($grade);
9609
                        $gradebookgrade->feedbackfiles = $gradebookplugin->files_for_gradebook($grade);
9610
                    }
9611
                }
9612
                $grades[$gradebookgrade->userid] = $gradebookgrade;
9613
            }
9614
        }
9615
 
9616
        $graderesults->close();
9617
        return $grades;
9618
    }
9619
 
9620
    /**
9621
     * Call the static version of this function
9622
     *
9623
     * @param int $userid The userid to lookup
9624
     * @return int The unique id
9625
     */
1254 ariadna 9626
    public function get_uniqueid_for_user($userid)
9627
    {
1 efrain 9628
        return self::get_uniqueid_for_user_static($this->get_instance()->id, $userid);
9629
    }
9630
 
9631
    /**
9632
     * Foreach participant in the course - assign them a random id.
9633
     *
9634
     * @param int $assignid The assignid to lookup
9635
     */
1254 ariadna 9636
    public static function allocate_unique_ids($assignid)
9637
    {
1 efrain 9638
        global $DB;
9639
 
9640
        $cm = get_coursemodule_from_instance('assign', $assignid, 0, false, MUST_EXIST);
9641
        $context = context_module::instance($cm->id);
9642
 
9643
        $currentgroup = groups_get_activity_group($cm, true);
9644
        $users = get_enrolled_users($context, "mod/assign:submit", $currentgroup, 'u.id');
9645
 
9646
        // Shuffle the users.
9647
        shuffle($users);
9648
 
9649
        foreach ($users as $user) {
1254 ariadna 9650
            $record = $DB->get_record(
9651
                'assign_user_mapping',
9652
                array('assignment' => $assignid, 'userid' => $user->id),
9653
                'id'
9654
            );
1 efrain 9655
            if (!$record) {
9656
                $record = new stdClass();
9657
                $record->assignment = $assignid;
9658
                $record->userid = $user->id;
9659
                $DB->insert_record('assign_user_mapping', $record);
9660
            }
9661
        }
9662
    }
9663
 
9664
    /**
9665
     * Lookup this user id and return the unique id for this assignment.
9666
     *
9667
     * @param int $assignid The assignment id
9668
     * @param int $userid The userid to lookup
9669
     * @return int The unique id
9670
     */
1254 ariadna 9671
    public static function get_uniqueid_for_user_static($assignid, $userid)
9672
    {
1 efrain 9673
        global $DB;
9674
 
9675
        // Search for a record.
1254 ariadna 9676
        $params = array('assignment' => $assignid, 'userid' => $userid);
1 efrain 9677
        if ($record = $DB->get_record('assign_user_mapping', $params, 'id')) {
9678
            return $record->id;
9679
        }
9680
 
9681
        // Be a little smart about this - there is no record for the current user.
9682
        // We should ensure any unallocated ids for the current participant
9683
        // list are distrubited randomly.
9684
        self::allocate_unique_ids($assignid);
9685
 
9686
        // Retry the search for a record.
9687
        if ($record = $DB->get_record('assign_user_mapping', $params, 'id')) {
9688
            return $record->id;
9689
        }
9690
 
9691
        // The requested user must not be a participant. Add a record anyway.
9692
        $record = new stdClass();
9693
        $record->assignment = $assignid;
9694
        $record->userid = $userid;
9695
 
9696
        return $DB->insert_record('assign_user_mapping', $record);
9697
    }
9698
 
9699
    /**
9700
     * Call the static version of this function.
9701
     *
9702
     * @param int $uniqueid The uniqueid to lookup
9703
     * @return int The user id or false if they don't exist
9704
     */
1254 ariadna 9705
    public function get_user_id_for_uniqueid($uniqueid)
9706
    {
1 efrain 9707
        return self::get_user_id_for_uniqueid_static($this->get_instance()->id, $uniqueid);
9708
    }
9709
 
9710
    /**
9711
     * Lookup this unique id and return the user id for this assignment.
9712
     *
9713
     * @param int $assignid The id of the assignment this user mapping is in
9714
     * @param int $uniqueid The uniqueid to lookup
9715
     * @return int The user id or false if they don't exist
9716
     */
1254 ariadna 9717
    public static function get_user_id_for_uniqueid_static($assignid, $uniqueid)
9718
    {
1 efrain 9719
        global $DB;
9720
 
9721
        // Search for a record.
1254 ariadna 9722
        if ($record = $DB->get_record(
9723
            'assign_user_mapping',
9724
            array('assignment' => $assignid, 'id' => $uniqueid),
9725
            'userid',
9726
            IGNORE_MISSING
9727
        )) {
1 efrain 9728
            return $record->userid;
9729
        }
9730
 
9731
        return false;
9732
    }
9733
 
9734
    /**
9735
     * Get the list of marking_workflow states the current user has permission to transition a grade to.
9736
     *
9737
     * @return array of state => description
9738
     */
1254 ariadna 9739
    public function get_marking_workflow_states_for_current_user()
9740
    {
1 efrain 9741
        if (!empty($this->markingworkflowstates)) {
9742
            return $this->markingworkflowstates;
9743
        }
9744
        $states = array();
9745
        if (has_capability('mod/assign:grade', $this->context)) {
9746
            $states[ASSIGN_MARKING_WORKFLOW_STATE_INMARKING] = get_string('markingworkflowstateinmarking', 'assign');
9747
            $states[ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW] = get_string('markingworkflowstatereadyforreview', 'assign');
9748
        }
1254 ariadna 9749
        if (has_any_capability(array(
9750
            'mod/assign:reviewgrades',
9751
            'mod/assign:managegrades'
9752
        ), $this->context)) {
1 efrain 9753
            $states[ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW] = get_string('markingworkflowstateinreview', 'assign');
9754
            $states[ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE] = get_string('markingworkflowstatereadyforrelease', 'assign');
9755
        }
1254 ariadna 9756
        if (has_any_capability(array(
9757
            'mod/assign:releasegrades',
9758
            'mod/assign:managegrades'
9759
        ), $this->context)) {
1 efrain 9760
            $states[ASSIGN_MARKING_WORKFLOW_STATE_RELEASED] = get_string('markingworkflowstatereleased', 'assign');
9761
        }
9762
        $this->markingworkflowstates = $states;
9763
        return $this->markingworkflowstates;
9764
    }
9765
 
9766
    /**
9767
     * Get the list of marking_workflow states.
9768
     *
9769
     * @return array Array of multiple state => description.
9770
     */
1254 ariadna 9771
    public function get_all_marking_workflow_states(): array
9772
    {
1 efrain 9773
        if (!empty($this->allmarkingworkflowstates)) {
9774
            return $this->allmarkingworkflowstates;
9775
        }
9776
 
9777
        $this->allmarkingworkflowstates = [
9778
            ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED => get_string('markingworkflowstatenotmarked', 'assign'),
9779
            ASSIGN_MARKING_WORKFLOW_STATE_INMARKING => get_string('markingworkflowstateinmarking', 'assign'),
9780
            ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW => get_string('markingworkflowstatereadyforreview', 'assign'),
9781
            ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW => get_string('markingworkflowstateinreview', 'assign'),
9782
            ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE => get_string('markingworkflowstatereadyforrelease', 'assign'),
9783
            ASSIGN_MARKING_WORKFLOW_STATE_RELEASED => get_string('markingworkflowstatereleased', 'assign'),
9784
        ];
9785
 
9786
        return $this->allmarkingworkflowstates;
9787
    }
9788
 
9789
    /**
9790
     * Check is only active users in course should be shown.
9791
     *
9792
     * @return bool true if only active users should be shown.
9793
     */
1254 ariadna 9794
    public function show_only_active_users()
9795
    {
1 efrain 9796
        global $CFG;
9797
 
9798
        if (is_null($this->showonlyactiveenrol)) {
9799
            $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol);
9800
            $this->showonlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol);
9801
 
9802
            if (!is_null($this->context)) {
9803
                $this->showonlyactiveenrol = $this->showonlyactiveenrol ||
1254 ariadna 9804
                    !has_capability('moodle/course:viewsuspendedusers', $this->context);
1 efrain 9805
            }
9806
        }
9807
        return $this->showonlyactiveenrol;
9808
    }
9809
 
9810
    /**
9811
     * Return true is user is active user in course else false
9812
     *
9813
     * @param int $userid
9814
     * @return bool true is user is active in course.
9815
     */
1254 ariadna 9816
    public function is_active_user($userid)
9817
    {
1 efrain 9818
        return !in_array($userid, get_suspended_userids($this->context, true));
9819
    }
9820
 
9821
    /**
9822
     * Returns true if gradebook feedback plugin is enabled
9823
     *
9824
     * @return bool true if gradebook feedback plugin is enabled and visible else false.
9825
     */
1254 ariadna 9826
    public function is_gradebook_feedback_enabled()
9827
    {
1 efrain 9828
        // Get default grade book feedback plugin.
9829
        $adminconfig = $this->get_admin_config();
9830
        $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
9831
        $gradebookplugin = str_replace('assignfeedback_', '', $gradebookplugin);
9832
 
9833
        // Check if default gradebook feedback is visible and enabled.
9834
        $gradebookfeedbackplugin = $this->get_feedback_plugin_by_type($gradebookplugin);
9835
 
9836
        if (empty($gradebookfeedbackplugin)) {
9837
            return false;
9838
        }
9839
 
9840
        if ($gradebookfeedbackplugin->is_visible() && $gradebookfeedbackplugin->is_enabled()) {
9841
            return true;
9842
        }
9843
 
9844
        // Gradebook feedback plugin is either not visible/enabled.
9845
        return false;
9846
    }
9847
 
9848
    /**
9849
     * Returns the grading status.
9850
     *
9851
     * @param int $userid the user id
9852
     * @return string returns the grading status
9853
     */
1254 ariadna 9854
    public function get_grading_status($userid)
9855
    {
1 efrain 9856
        if ($this->get_instance()->markingworkflow) {
9857
            $flags = $this->get_user_flags($userid, false);
9858
            if (!empty($flags->workflowstate)) {
9859
                return $flags->workflowstate;
9860
            }
9861
            return ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED;
9862
        } else {
9863
            $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT);
9864
            $grade = $this->get_user_grade($userid, false, $attemptnumber);
9865
 
9866
            if (!empty($grade) && $grade->grade !== null && $grade->grade >= 0) {
9867
                return ASSIGN_GRADING_STATUS_GRADED;
9868
            } else {
9869
                return ASSIGN_GRADING_STATUS_NOT_GRADED;
9870
            }
9871
        }
9872
    }
9873
 
9874
    /**
9875
     * The id used to uniquily identify the cache for this instance of the assign object.
9876
     *
9877
     * @return string
9878
     */
1254 ariadna 9879
    public function get_useridlist_key_id()
9880
    {
1 efrain 9881
        return $this->useridlistid;
9882
    }
9883
 
9884
    /**
9885
     * Generates the key that should be used for an entry in the useridlist cache.
9886
     *
9887
     * @param string $id Generate a key for this instance (optional)
9888
     * @return string The key for the id, or new entry if no $id is passed.
9889
     */
1254 ariadna 9890
    public function get_useridlist_key($id = null)
9891
    {
1 efrain 9892
        global $SESSION;
9893
 
9894
        // Ensure the user id list cache is initialised.
9895
        if (!isset($SESSION->mod_assign_useridlist)) {
9896
            $SESSION->mod_assign_useridlist = [];
9897
        }
9898
 
9899
        if ($id === null) {
9900
            $id = $this->get_useridlist_key_id();
9901
        }
9902
        return $this->get_course_module()->id . '_' . $id;
9903
    }
9904
 
9905
    /**
9906
     * Updates and creates the completion records in mdl_course_modules_completion.
9907
     *
9908
     * @param int $teamsubmission value of 0 or 1 to indicate whether this is a group activity
9909
     * @param int $requireallteammemberssubmit value of 0 or 1 to indicate whether all group members must click Submit
9910
     * @param obj $submission the submission
9911
     * @param int $userid the user id
9912
     * @param int $complete
9913
     * @param obj $completion
9914
     *
9915
     * @return null
9916
     */
1254 ariadna 9917
    protected function update_activity_completion_records(
9918
        $teamsubmission,
9919
        $requireallteammemberssubmit,
9920
        $submission,
9921
        $userid,
9922
        $complete,
9923
        $completion
9924
    ) {
1 efrain 9925
 
9926
        if (($teamsubmission && $submission->groupid > 0 && !$requireallteammemberssubmit) ||
9927
            ($teamsubmission && $submission->groupid > 0 && $requireallteammemberssubmit &&
1254 ariadna 9928
                $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED)
9929
        ) {
1 efrain 9930
 
9931
            $members = groups_get_members($submission->groupid);
9932
 
9933
            foreach ($members as $member) {
9934
                $completion->update_state($this->get_course_module(), $complete, $member->id);
9935
            }
9936
        } else {
9937
            $completion->update_state($this->get_course_module(), $complete, $userid);
9938
        }
9939
 
9940
        return;
9941
    }
9942
 
9943
    /**
9944
     * Update the module completion status (set it viewed) and trigger module viewed event.
9945
     *
9946
     * @since Moodle 3.2
9947
     */
1254 ariadna 9948
    public function set_module_viewed()
9949
    {
1 efrain 9950
        $completion = new completion_info($this->get_course());
9951
        $completion->set_module_viewed($this->get_course_module());
9952
 
9953
        // Trigger the course module viewed event.
9954
        $assigninstance = $this->get_instance();
9955
        $params = [
9956
            'objectid' => $assigninstance->id,
9957
            'context' => $this->get_context()
9958
        ];
9959
        if ($this->is_blind_marking()) {
9960
            $params['anonymous'] = 1;
9961
        }
9962
 
9963
        $event = \mod_assign\event\course_module_viewed::create($params);
9964
 
9965
        $event->add_record_snapshot('assign', $assigninstance);
9966
        $event->trigger();
9967
    }
9968
 
9969
    /**
9970
     * Checks for any grade notices, and adds notifications. Will display on assignment main page and grading table.
9971
     *
9972
     * @return void The notifications API will render the notifications at the appropriate part of the page.
9973
     */
1254 ariadna 9974
    protected function add_grade_notices()
9975
    {
1 efrain 9976
        if (has_capability('mod/assign:grade', $this->get_context()) && get_config('assign', 'has_rescaled_null_grades_' . $this->get_instance()->id)) {
9977
            $link = new \moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id, 'action' => 'fixrescalednullgrades'));
9978
            \core\notification::warning(get_string('fixrescalednullgrades', 'mod_assign', ['link' => $link->out()]));
9979
        }
9980
    }
9981
 
9982
    /**
9983
     * View fix rescaled null grades.
9984
     *
9985
     * @return bool True if null all grades are now fixed.
9986
     */
1254 ariadna 9987
    protected function fix_null_grades()
9988
    {
1 efrain 9989
        global $DB;
9990
        $result = $DB->set_field_select(
9991
            'assign_grades',
9992
            'grade',
9993
            ASSIGN_GRADE_NOT_SET,
9994
            'grade <> ? AND grade < 0',
9995
            [ASSIGN_GRADE_NOT_SET]
9996
        );
9997
        $assign = clone $this->get_instance();
9998
        $assign->cmidnumber = $this->get_course_module()->idnumber;
9999
        assign_update_grades($assign);
10000
        return $result;
10001
    }
10002
 
10003
    /**
10004
     * View fix rescaled null grades.
10005
     *
10006
     * @return void The notifications API will render the notifications at the appropriate part of the page.
10007
     */
1254 ariadna 10008
    protected function view_fix_rescaled_null_grades()
10009
    {
1 efrain 10010
        global $OUTPUT;
10011
 
10012
        $o = '';
10013
 
10014
        require_capability('mod/assign:grade', $this->get_context());
10015
 
10016
        $instance = $this->get_instance();
10017
 
10018
        $o .= $this->get_renderer()->render(
10019
            new assign_header(
10020
                $instance,
10021
                $this->get_context(),
10022
                $this->show_intro(),
10023
                $this->get_course_module()->id
10024
            )
10025
        );
10026
 
10027
        $confirm = optional_param('confirm', 0, PARAM_BOOL);
10028
 
10029
        if ($confirm) {
10030
            if (confirm_sesskey()) {
10031
                // Fix the grades.
10032
                $this->fix_null_grades();
10033
                unset_config('has_rescaled_null_grades_' . $instance->id, 'assign');
10034
                // Display the success notice.
10035
                $o .= $this->get_renderer()->notification(get_string('fixrescalednullgradesdone', 'assign'), 'notifysuccess');
10036
            } else {
10037
                // If the sesskey is not valid, then display the error notice.
10038
                $o .= $this->get_renderer()->notification(get_string('invalidsesskey', 'error'), 'notifyerror');
10039
            }
10040
            $url = new moodle_url(
10041
                url: '/mod/assign/view.php',
10042
                params: [
10043
                    'id' => $this->get_course_module()->id,
10044
                    'action' => 'grading',
10045
                ],
10046
            );
10047
            $o .= $this->get_renderer()->continue_button($url);
10048
        } else {
10049
            // Ask for confirmation.
10050
            $continue = new \moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id, 'action' => 'fixrescalednullgrades', 'confirm' => true, 'sesskey' => sesskey()));
10051
            $cancel = new \moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id));
10052
            $o .= $OUTPUT->confirm(get_string('fixrescalednullgradesconfirm', 'mod_assign'), $continue, $cancel);
10053
        }
10054
 
10055
        $o .= $this->view_footer();
10056
 
10057
        return $o;
10058
    }
10059
 
10060
    /**
10061
     * Set the most recent submission for the team.
10062
     * The most recent team submission is used to determine if another attempt should be created when allowing another
10063
     * attempt on a group assignment, and whether the gradebook should be updated.
10064
     *
10065
     * @since Moodle 3.4
10066
     * @param stdClass $submission The most recent submission of the group.
10067
     */
1254 ariadna 10068
    public function set_most_recent_team_submission($submission)
10069
    {
1 efrain 10070
        $this->mostrecentteamsubmission = $submission;
10071
    }
10072
 
10073
    /**
10074
     * Return array of valid grading allocation filters for the grading interface.
10075
     *
10076
     * @param boolean $export Export the list of filters for a template.
10077
     * @return array
10078
     */
1254 ariadna 10079
    public function get_marking_allocation_filters($export = false)
10080
    {
1 efrain 10081
        $markingallocation = $this->get_instance()->markingworkflow &&
10082
            $this->get_instance()->markingallocation &&
10083
            has_capability('mod/assign:manageallocations', $this->context);
10084
        // Get markers to use in drop lists.
10085
        $markingallocationoptions = array();
10086
        if ($markingallocation) {
10087
            list($sort, $params) = users_order_by_sql('u');
10088
            // Only enrolled users could be assigned as potential markers.
10089
            $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
10090
            $markingallocationoptions[''] = get_string('filternone', 'assign');
10091
            $markingallocationoptions[ASSIGN_MARKER_FILTER_NO_MARKER] = get_string('markerfilternomarker', 'assign');
10092
            $viewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
10093
            foreach ($markers as $marker) {
10094
                $markingallocationoptions[$marker->id] = fullname($marker, $viewfullnames);
10095
            }
10096
        }
10097
        if ($export) {
10098
            $allocationfilter = get_user_preferences('assign_markerfilter', '');
10099
            $result = [];
10100
            foreach ($markingallocationoptions as $option => $label) {
10101
                array_push($result, [
10102
                    'key' => $option,
10103
                    'name' => $label,
10104
                    'active' => ($allocationfilter == $option),
10105
                ]);
10106
            }
10107
            return $result;
10108
        }
10109
        return $markingworkflowoptions;
10110
    }
10111
 
10112
    /**
10113
     * Return array of valid grading workflow filters for the grading interface.
10114
     *
10115
     * @param boolean $export Export the list of filters for a template.
10116
     * @return array
10117
     */
1254 ariadna 10118
    public function get_marking_workflow_filters($export = false)
10119
    {
1 efrain 10120
        $markingworkflow = $this->get_instance()->markingworkflow;
10121
        // Get marking states to show in form.
10122
        $markingworkflowoptions = array();
10123
        if ($markingworkflow) {
10124
            $notmarked = get_string('markingworkflowstatenotmarked', 'assign');
10125
            $markingworkflowoptions[''] = get_string('filternone', 'assign');
10126
            $markingworkflowoptions[ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED] = $notmarked;
10127
            $markingworkflowoptions = array_merge($markingworkflowoptions, $this->get_marking_workflow_states_for_current_user());
10128
        }
10129
        if ($export) {
10130
            $workflowfilter = get_user_preferences('assign_workflowfilter', '');
10131
            $result = [];
10132
            foreach ($markingworkflowoptions as $option => $label) {
10133
                array_push($result, [
10134
                    'key' => $option,
10135
                    'name' => $label,
10136
                    'active' => ($workflowfilter == $option),
10137
                ]);
10138
            }
10139
            return $result;
10140
        }
10141
        return $markingworkflowoptions;
10142
    }
10143
 
10144
    /**
10145
     * Return array of valid search filters for the grading interface.
10146
     *
10147
     * @return array
10148
     */
1254 ariadna 10149
    public function get_filters()
10150
    {
1 efrain 10151
        $filterkeys = [
10152
            ASSIGN_FILTER_NOT_SUBMITTED,
10153
            ASSIGN_FILTER_DRAFT,
10154
            ASSIGN_FILTER_SUBMITTED,
10155
            ASSIGN_FILTER_REQUIRE_GRADING,
10156
            ASSIGN_FILTER_GRANTED_EXTENSION
10157
        ];
10158
 
10159
        $current = get_user_preferences('assign_filter', '');
10160
 
10161
        $filters = [];
10162
        // First is always "no filter" option.
10163
        array_push($filters, [
10164
            'key' => 'none',
10165
            'name' => get_string('filternone', 'assign'),
10166
            'active' => ($current == '')
10167
        ]);
10168
 
10169
        foreach ($filterkeys as $key) {
10170
            array_push($filters, [
10171
                'key' => $key,
10172
                'name' => get_string('filter' . $key, 'assign'),
10173
                'active' => ($current == $key)
10174
            ]);
10175
        }
10176
        return $filters;
10177
    }
10178
 
10179
    /**
10180
     * Get the correct submission statement depending on single submisison, team submission or team submission
10181
     * where all team memebers must submit.
10182
     *
10183
     * @param stdClass $adminconfig
10184
     * @param stdClass $instance
10185
     * @param context $context
10186
     *
10187
     * @return string
10188
     */
1254 ariadna 10189
    protected function get_submissionstatement($adminconfig, $instance, $context)
10190
    {
1 efrain 10191
        $submissionstatement = '';
10192
 
10193
        if (!($context instanceof context)) {
10194
            return $submissionstatement;
10195
        }
10196
 
10197
        // Single submission.
10198
        if (!$instance->teamsubmission) {
10199
            // Single submission statement is not empty.
10200
            if (!empty($adminconfig->submissionstatement)) {
10201
                // Format the submission statement before its sent. We turn off para because this is going within
10202
                // a form element.
10203
                $options = array(
10204
                    'context' => $context,
10205
                    'para'    => false
10206
                );
10207
                $submissionstatement = format_text($adminconfig->submissionstatement, FORMAT_MOODLE, $options);
10208
            }
10209
        } else { // Team submission.
10210
            // One user can submit for the whole team.
10211
            if (!empty($adminconfig->submissionstatementteamsubmission) && !$instance->requireallteammemberssubmit) {
10212
                // Format the submission statement before its sent. We turn off para because this is going within
10213
                // a form element.
10214
                $options = array(
10215
                    'context' => $context,
10216
                    'para'    => false
10217
                );
1254 ariadna 10218
                $submissionstatement = format_text(
10219
                    $adminconfig->submissionstatementteamsubmission,
10220
                    FORMAT_MOODLE,
10221
                    $options
10222
                );
10223
            } else if (
10224
                !empty($adminconfig->submissionstatementteamsubmissionallsubmit) &&
10225
                $instance->requireallteammemberssubmit
10226
            ) {
1 efrain 10227
                // All team members must submit.
10228
                // Format the submission statement before its sent. We turn off para because this is going within
10229
                // a form element.
10230
                $options = array(
10231
                    'context' => $context,
10232
                    'para'    => false
10233
                );
1254 ariadna 10234
                $submissionstatement = format_text(
10235
                    $adminconfig->submissionstatementteamsubmissionallsubmit,
10236
                    FORMAT_MOODLE,
10237
                    $options
10238
                );
1 efrain 10239
            }
10240
        }
10241
 
10242
        return $submissionstatement;
10243
    }
10244
 
10245
    /**
10246
     * Check if time limit for assignment enabled and set up.
10247
     *
10248
     * @param int|null $userid User ID. If null, use global user.
10249
     * @return bool
10250
     */
1254 ariadna 10251
    public function is_time_limit_enabled(?int $userid = null): bool
10252
    {
1 efrain 10253
        $instance = $this->get_instance($userid);
10254
        return get_config('assign', 'enabletimelimit') && !empty($instance->timelimit);
10255
    }
10256
 
10257
    /**
10258
     * Check if an assignment submission is already started and not yet submitted.
10259
     *
10260
     * @param int|null $userid User ID. If null, use global user.
10261
     * @param int $groupid Group ID. If 0, use user id to determine group.
10262
     * @param int $attemptnumber Attempt number. If -1, check latest submission.
10263
     * @return bool
10264
     */
1254 ariadna 10265
    public function is_attempt_in_progress(?int $userid = null, int $groupid = 0, int $attemptnumber = -1): bool
10266
    {
1 efrain 10267
        if ($this->get_instance($userid)->teamsubmission) {
10268
            $submission = $this->get_group_submission($userid, $groupid, false, $attemptnumber);
10269
        } else {
10270
            $submission = $this->get_user_submission($userid, false, $attemptnumber);
10271
        }
10272
 
10273
        // If time limit is enabled, we only assume it is in progress if there is a start time for submission.
10274
        $timedattemptstarted = true;
10275
        if ($this->is_time_limit_enabled($userid)) {
10276
            $timedattemptstarted = !empty($submission) && !empty($submission->timestarted);
10277
        }
10278
 
10279
        return !empty($submission) && $submission->status !== ASSIGN_SUBMISSION_STATUS_SUBMITTED && $timedattemptstarted;
10280
    }
10281
}
10282
 
10283
/**
10284
 * Portfolio caller class for mod_assign.
10285
 *
10286
 * @package   mod_assign
10287
 * @copyright 2012 NetSpot {@link http://www.netspot.com.au}
10288
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
10289
 */
1254 ariadna 10290
class assign_portfolio_caller extends portfolio_module_caller_base
10291
{
1 efrain 10292
 
10293
    /** @var int callback arg - the id of submission we export */
10294
    protected $sid;
10295
 
10296
    /** @var string component of the submission files we export*/
10297
    protected $component;
10298
 
10299
    /** @var string callback arg - the area of submission files we export */
10300
    protected $area;
10301
 
10302
    /** @var int callback arg - the id of file we export */
10303
    protected $fileid;
10304
 
10305
    /** @var int callback arg - the cmid of the assignment we export */
10306
    protected $cmid;
10307
 
10308
    /** @var string callback arg - the plugintype of the editor we export */
10309
    protected $plugin;
10310
 
10311
    /** @var string callback arg - the name of the editor field we export */
10312
    protected $editor;
10313
 
10314
    /**
10315
     * Callback arg for a single file export.
10316
     */
1254 ariadna 10317
    public static function expected_callbackargs()
10318
    {
1 efrain 10319
        return array(
10320
            'cmid' => true,
10321
            'sid' => false,
10322
            'area' => false,
10323
            'component' => false,
10324
            'fileid' => false,
10325
            'plugin' => false,
10326
            'editor' => false,
10327
        );
10328
    }
10329
 
10330
    /**
10331
     * The constructor.
10332
     *
10333
     * @param array $callbackargs
10334
     */
1254 ariadna 10335
    public function __construct($callbackargs)
10336
    {
1 efrain 10337
        parent::__construct($callbackargs);
10338
        $this->cm = get_coursemodule_from_id('assign', $this->cmid, 0, false, MUST_EXIST);
10339
    }
10340
 
10341
    /**
10342
     * Load data needed for the portfolio export.
10343
     *
10344
     * If the assignment type implements portfolio_load_data(), the processing is delegated
10345
     * to it. Otherwise, the caller must provide either fileid (to export single file) or
10346
     * submissionid and filearea (to export all data attached to the given submission file area)
10347
     * via callback arguments.
10348
     *
10349
     * @throws     portfolio_caller_exception
10350
     */
1254 ariadna 10351
    public function load_data()
10352
    {
1 efrain 10353
        global $DB;
10354
 
10355
        $context = context_module::instance($this->cmid);
10356
 
10357
        if (empty($this->fileid)) {
10358
            if (empty($this->sid) || empty($this->area)) {
10359
                throw new portfolio_caller_exception('invalidfileandsubmissionid', 'mod_assign');
10360
            }
10361
 
10362
            $submission = $DB->get_record('assign_submission', array('id' => $this->sid));
10363
        } else {
10364
            $submissionid = $DB->get_field('files', 'itemid', array('id' => $this->fileid, 'contextid' => $context->id));
10365
            if ($submissionid) {
10366
                $submission = $DB->get_record('assign_submission', array('id' => $submissionid));
10367
            }
10368
        }
10369
 
10370
        if (empty($submission)) {
10371
            throw new portfolio_caller_exception('filenotfound');
10372
        } else if ($submission->userid == 0) {
10373
            // This must be a group submission.
10374
            if (!groups_is_member($submission->groupid, $this->user->id)) {
10375
                throw new portfolio_caller_exception('filenotfound');
10376
            }
10377
        } else if ($this->user->id != $submission->userid) {
10378
            throw new portfolio_caller_exception('filenotfound');
10379
        }
10380
 
10381
        // Export either an area of files or a single file (see function for more detail).
10382
        // The first arg is an id or null. If it is an id, the rest of the args are ignored.
10383
        // If it is null, the rest of the args are used to load a list of files from get_areafiles.
1254 ariadna 10384
        $this->set_file_and_format_data(
10385
            $this->fileid,
10386
            $context->id,
10387
            $this->component,
10388
            $this->area,
10389
            $this->sid,
10390
            'timemodified',
10391
            false
10392
        );
1 efrain 10393
    }
10394
 
10395
    /**
10396
     * Prepares the package up before control is passed to the portfolio plugin.
10397
     *
10398
     * @throws portfolio_caller_exception
10399
     * @return mixed
10400
     */
1254 ariadna 10401
    public function prepare_package()
10402
    {
1 efrain 10403
 
10404
        if ($this->plugin && $this->editor) {
10405
            $options = portfolio_format_text_options();
10406
            $context = context_module::instance($this->cmid);
10407
            $options->context = $context;
10408
 
10409
            $plugin = $this->get_submission_plugin();
10410
 
10411
            $text = $plugin->get_editor_text($this->editor, $this->sid);
10412
            $format = $plugin->get_editor_format($this->editor, $this->sid);
10413
 
10414
            $html = format_text($text, $format, $options);
1254 ariadna 10415
            $html = portfolio_rewrite_pluginfile_urls(
10416
                $html,
10417
                $context->id,
10418
                'mod_assign',
10419
                $this->area,
10420
                $this->sid,
10421
                $this->exporter->get('format')
10422
            );
1 efrain 10423
 
10424
            $exporterclass = $this->exporter->get('formatclass');
10425
            if (in_array($exporterclass, array(PORTFOLIO_FORMAT_PLAINHTML, PORTFOLIO_FORMAT_RICHHTML))) {
10426
                if ($files = $this->exporter->get('caller')->get('multifiles')) {
10427
                    foreach ($files as $file) {
10428
                        $this->exporter->copy_existing_file($file);
10429
                    }
10430
                }
10431
                return $this->exporter->write_new_file($html, 'assignment.html', !empty($files));
10432
            } else if ($this->exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) {
10433
                $leapwriter = $this->exporter->get('format')->leap2a_writer();
1254 ariadna 10434
                $entry = new portfolio_format_leap2a_entry(
10435
                    $this->area . $this->cmid,
10436
                    $context->get_context_name(),
10437
                    'resource',
10438
                    $html
10439
                );
1 efrain 10440
 
10441
                $entry->add_category('web', 'resource_type');
10442
                $entry->author = $this->user;
10443
                $leapwriter->add_entry($entry);
10444
                if ($files = $this->exporter->get('caller')->get('multifiles')) {
10445
                    $leapwriter->link_files($entry, $files, $this->area . $this->cmid . 'file');
10446
                    foreach ($files as $file) {
10447
                        $this->exporter->copy_existing_file($file);
10448
                    }
10449
                }
1254 ariadna 10450
                return $this->exporter->write_new_file(
10451
                    $leapwriter->to_xml(),
10452
                    $this->exporter->get('format')->manifest_name(),
10453
                    true
10454
                );
1 efrain 10455
            } else {
10456
                debugging('invalid format class: ' . $this->exporter->get('formatclass'));
10457
            }
10458
        }
10459
 
10460
        if ($this->exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) {
10461
            $leapwriter = $this->exporter->get('format')->leap2a_writer();
10462
            $files = array();
10463
            if ($this->singlefile) {
10464
                $files[] = $this->singlefile;
10465
            } else if ($this->multifiles) {
10466
                $files = $this->multifiles;
10467
            } else {
1254 ariadna 10468
                throw new portfolio_caller_exception(
10469
                    'invalidpreparepackagefile',
10470
                    'portfolio',
10471
                    $this->get_return_url()
10472
                );
1 efrain 10473
            }
10474
 
10475
            $entryids = array();
10476
            foreach ($files as $file) {
10477
                $entry = new portfolio_format_leap2a_file($file->get_filename(), $file);
10478
                $entry->author = $this->user;
10479
                $leapwriter->add_entry($entry);
10480
                $this->exporter->copy_existing_file($file);
10481
                $entryids[] = $entry->id;
10482
            }
10483
            if (count($files) > 1) {
10484
                $baseid = 'assign' . $this->cmid . $this->area;
10485
                $context = context_module::instance($this->cmid);
10486
 
10487
                // If we have multiple files, they should be grouped together into a folder.
1254 ariadna 10488
                $entry = new portfolio_format_leap2a_entry(
10489
                    $baseid . 'group',
10490
                    $context->get_context_name(),
10491
                    'selection'
10492
                );
1 efrain 10493
                $leapwriter->add_entry($entry);
10494
                $leapwriter->make_selection($entry, $entryids, 'Folder');
10495
            }
1254 ariadna 10496
            return $this->exporter->write_new_file(
10497
                $leapwriter->to_xml(),
10498
                $this->exporter->get('format')->manifest_name(),
10499
                true
10500
            );
1 efrain 10501
        }
10502
        return $this->prepare_package_file();
10503
    }
10504
 
10505
    /**
10506
     * Fetch the plugin by its type.
10507
     *
10508
     * @return assign_submission_plugin
10509
     */
1254 ariadna 10510
    protected function get_submission_plugin()
10511
    {
1 efrain 10512
        global $CFG;
10513
        if (!$this->plugin || !$this->cmid) {
10514
            return null;
10515
        }
10516
 
10517
        require_once($CFG->dirroot . '/mod/assign/locallib.php');
10518
 
10519
        $context = context_module::instance($this->cmid);
10520
 
10521
        $assignment = new assign($context, null, null);
10522
        return $assignment->get_submission_plugin_by_type($this->plugin);
10523
    }
10524
 
10525
    /**
10526
     * Calculate a sha1 has of either a single file or a list
10527
     * of files based on the data set by load_data.
10528
     *
10529
     * @return string
10530
     */
1254 ariadna 10531
    public function get_sha1()
10532
    {
1 efrain 10533
 
10534
        if ($this->plugin && $this->editor) {
10535
            $plugin = $this->get_submission_plugin();
10536
            $options = portfolio_format_text_options();
10537
            $options->context = context_module::instance($this->cmid);
10538
 
1254 ariadna 10539
            $text = format_text(
10540
                $plugin->get_editor_text($this->editor, $this->sid),
10541
                $plugin->get_editor_format($this->editor, $this->sid),
10542
                $options
10543
            );
1 efrain 10544
            $textsha1 = sha1($text);
10545
            $filesha1 = '';
10546
            try {
10547
                $filesha1 = $this->get_sha1_file();
10548
            } catch (portfolio_caller_exception $e) {
10549
                // No files.
10550
            }
10551
            return sha1($textsha1 . $filesha1);
10552
        }
10553
        return $this->get_sha1_file();
10554
    }
10555
 
10556
    /**
10557
     * Calculate the time to transfer either a single file or a list
10558
     * of files based on the data set by load_data.
10559
     *
10560
     * @return int
10561
     */
1254 ariadna 10562
    public function expected_time()
10563
    {
1 efrain 10564
        return $this->expected_time_file();
10565
    }
10566
 
10567
    /**
10568
     * Checking the permissions.
10569
     *
10570
     * @return bool
10571
     */
1254 ariadna 10572
    public function check_permissions()
10573
    {
1 efrain 10574
        $context = context_module::instance($this->cmid);
10575
        return has_capability('mod/assign:exportownsubmission', $context);
10576
    }
10577
 
10578
    /**
10579
     * Display a module name.
10580
     *
10581
     * @return string
10582
     */
1254 ariadna 10583
    public static function display_name()
10584
    {
1 efrain 10585
        return get_string('modulename', 'assign');
10586
    }
10587
 
10588
    /**
10589
     * Return array of formats supported by this portfolio call back.
10590
     *
10591
     * @return array
10592
     */
1254 ariadna 10593
    public static function base_supported_formats()
10594
    {
1 efrain 10595
        return array(PORTFOLIO_FORMAT_FILE, PORTFOLIO_FORMAT_LEAP2A);
10596
    }
10597
}
10598
 
10599
/**
10600
 * Logic to happen when a/some group(s) has/have been deleted in a course.
10601
 *
10602
 * @param int $courseid The course ID.
10603
 * @param int $groupid The group id if it is known
10604
 * @return void
10605
 */
1254 ariadna 10606
function assign_process_group_deleted_in_course($courseid, $groupid = null)
10607
{
1 efrain 10608
    global $DB;
10609
 
10610
    $params = array('courseid' => $courseid);
10611
    if ($groupid) {
10612
        $params['groupid'] = $groupid;
10613
        // We just update the group that was deleted.
10614
        $sql = "SELECT o.id, o.assignid, o.groupid
10615
                  FROM {assign_overrides} o
10616
                  JOIN {assign} assign ON assign.id = o.assignid
10617
                 WHERE assign.course = :courseid
10618
                   AND o.groupid = :groupid";
10619
    } else {
10620
        // No groupid, we update all orphaned group overrides for all assign in course.
10621
        $sql = "SELECT o.id, o.assignid, o.groupid
10622
                  FROM {assign_overrides} o
10623
                  JOIN {assign} assign ON assign.id = o.assignid
10624
             LEFT JOIN {groups} grp ON grp.id = o.groupid
10625
                 WHERE assign.course = :courseid
10626
                   AND o.groupid IS NOT NULL
10627
                   AND grp.id IS NULL";
10628
    }
10629
    $records = $DB->get_records_sql($sql, $params);
10630
    if (!$records) {
10631
        return; // Nothing to do.
10632
    }
10633
    $DB->delete_records_list('assign_overrides', 'id', array_keys($records));
10634
    $cache = cache::make('mod_assign', 'overrides');
10635
    foreach ($records as $record) {
10636
        $cache->delete("{$record->assignid}_g_{$record->groupid}");
10637
    }
10638
}
10639
 
10640
/**
10641
 * Change the sort order of an override
10642
 *
10643
 * @param int $id of the override
10644
 * @param string $move direction of move
10645
 * @param int $assignid of the assignment
10646
 * @return bool success of operation
10647
 */
1254 ariadna 10648
function move_group_override($id, $move, $assignid)
10649
{
1 efrain 10650
    global $DB;
10651
 
10652
    // Get the override object.
10653
    if (!$override = $DB->get_record('assign_overrides', ['id' => $id, 'assignid' => $assignid], 'id, sortorder, groupid')) {
10654
        return false;
10655
    }
10656
    // Count the number of group overrides.
10657
    $overridecountgroup = $DB->count_records('assign_overrides', array('userid' => null, 'assignid' => $assignid));
10658
 
10659
    // Calculate the new sortorder.
1254 ariadna 10660
    if (($move == 'up') and ($override->sortorder > 1)) {
1 efrain 10661
        $neworder = $override->sortorder - 1;
10662
    } else if (($move == 'down') and ($override->sortorder < $overridecountgroup)) {
10663
        $neworder = $override->sortorder + 1;
10664
    } else {
10665
        return false;
10666
    }
10667
 
10668
    // Retrieve the override object that is currently residing in the new position.
10669
    $params = ['sortorder' => $neworder, 'assignid' => $assignid];
10670
    if ($swapoverride = $DB->get_record('assign_overrides', $params, 'id, sortorder, groupid')) {
10671
 
10672
        // Swap the sortorders.
10673
        $swapoverride->sortorder = $override->sortorder;
10674
        $override->sortorder     = $neworder;
10675
 
10676
        // Update the override records.
10677
        $DB->update_record('assign_overrides', $override);
10678
        $DB->update_record('assign_overrides', $swapoverride);
10679
 
10680
        // Delete cache for the 2 records we updated above.
10681
        $cache = cache::make('mod_assign', 'overrides');
10682
        $cache->delete("{$assignid}_g_{$override->groupid}");
10683
        $cache->delete("{$assignid}_g_{$swapoverride->groupid}");
10684
    }
10685
 
10686
    reorder_group_overrides($assignid);
10687
    return true;
10688
}
10689
 
10690
/**
10691
 * Reorder the overrides starting at the override at the given startorder.
10692
 *
10693
 * @param int $assignid of the assigment
10694
 */
1254 ariadna 10695
function reorder_group_overrides($assignid)
10696
{
1 efrain 10697
    global $DB;
10698
 
10699
    $i = 1;
10700
    if ($overrides = $DB->get_records('assign_overrides', array('userid' => null, 'assignid' => $assignid), 'sortorder ASC')) {
10701
        $cache = cache::make('mod_assign', 'overrides');
10702
        foreach ($overrides as $override) {
10703
            $f = new stdClass();
10704
            $f->id = $override->id;
10705
            $f->sortorder = $i++;
10706
            $DB->update_record('assign_overrides', $f);
10707
            $cache->delete("{$assignid}_g_{$override->groupid}");
10708
 
10709
            // Update priorities of group overrides.
10710
            $params = [
10711
                'modulename' => 'assign',
10712
                'instance' => $override->assignid,
10713
                'groupid' => $override->groupid
10714
            ];
10715
            $DB->set_field('event', 'priority', $f->sortorder, $params);
10716
        }
10717
    }
10718
}
10719
 
10720
/**
10721
 * Get the information about the standard assign JavaScript module.
10722
 * @return array a standard jsmodule structure.
10723
 */
1254 ariadna 10724
function assign_get_js_module()
10725
{
1 efrain 10726
    return array(
10727
        'name' => 'mod_assign',
10728
        'fullpath' => '/mod/assign/module.js',
10729
    );
10730
}