Merge branch 'MDL-64761-master' of git://github.com/dpalou/moodle
[moodle.git] / mod / assign / locallib.php
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/>.
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  */
27 defined('MOODLE_INTERNAL') || die();
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');
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');
43 // Marker filter for grading page.
44 define('ASSIGN_MARKER_FILTER_NO_MARKER', -1);
46 // Reopen attempt methods.
47 define('ASSIGN_ATTEMPT_REOPEN_METHOD_NONE', 'none');
48 define('ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL', 'manual');
49 define('ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS', 'untilpass');
51 // Special value means allow unlimited attempts.
52 define('ASSIGN_UNLIMITED_ATTEMPTS', -1);
54 // Special value means no grade has been set.
55 define('ASSIGN_GRADE_NOT_SET', -1);
57 // Grading states.
58 define('ASSIGN_GRADING_STATUS_GRADED', 'graded');
59 define('ASSIGN_GRADING_STATUS_NOT_GRADED', 'notgraded');
61 // Marking workflow states.
62 define('ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED', 'notmarked');
63 define('ASSIGN_MARKING_WORKFLOW_STATE_INMARKING', 'inmarking');
64 define('ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW', 'readyforreview');
65 define('ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW', 'inreview');
66 define('ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE', 'readyforrelease');
67 define('ASSIGN_MARKING_WORKFLOW_STATE_RELEASED', 'released');
69 /** ASSIGN_MAX_EVENT_LENGTH = 432000 ; 5 days maximum */
70 define("ASSIGN_MAX_EVENT_LENGTH", "432000");
72 // Name of file area for intro attachments.
73 define('ASSIGN_INTROATTACHMENT_FILEAREA', 'introattachment');
75 // Event types.
76 define('ASSIGN_EVENT_TYPE_DUE', 'due');
77 define('ASSIGN_EVENT_TYPE_GRADINGDUE', 'gradingdue');
78 define('ASSIGN_EVENT_TYPE_OPEN', 'open');
79 define('ASSIGN_EVENT_TYPE_CLOSE', 'close');
81 require_once($CFG->libdir . '/accesslib.php');
82 require_once($CFG->libdir . '/formslib.php');
83 require_once($CFG->dirroot . '/repository/lib.php');
84 require_once($CFG->dirroot . '/mod/assign/mod_form.php');
85 require_once($CFG->libdir . '/gradelib.php');
86 require_once($CFG->dirroot . '/grade/grading/lib.php');
87 require_once($CFG->dirroot . '/mod/assign/feedbackplugin.php');
88 require_once($CFG->dirroot . '/mod/assign/submissionplugin.php');
89 require_once($CFG->dirroot . '/mod/assign/renderable.php');
90 require_once($CFG->dirroot . '/mod/assign/gradingtable.php');
91 require_once($CFG->libdir . '/portfolio/caller.php');
93 use \mod_assign\output\grading_app;
95 /**
96  * Standard base class for mod_assign (assignment types).
97  *
98  * @package   mod_assign
99  * @copyright 2012 NetSpot {@link http://www.netspot.com.au}
100  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
101  */
102 class assign {
104     /** @var stdClass the assignment record that contains the global settings for this assign instance */
105     private $instance;
107     /** @var grade_item the grade_item record for this assign instance's primary grade item. */
108     private $gradeitem;
110     /** @var context the context of the course module for this assign instance
111      *               (or just the course if we are creating a new one)
112      */
113     private $context;
115     /** @var stdClass the course this assign instance belongs to */
116     private $course;
118     /** @var stdClass the admin config for all assign instances  */
119     private $adminconfig;
121     /** @var assign_renderer the custom renderer for this module */
122     private $output;
124     /** @var cm_info the course module for this assign instance */
125     private $coursemodule;
127     /** @var array cache for things like the coursemodule name or the scale menu -
128      *             only lives for a single request.
129      */
130     private $cache;
132     /** @var array list of the installed submission plugins */
133     private $submissionplugins;
135     /** @var array list of the installed feedback plugins */
136     private $feedbackplugins;
138     /** @var string action to be used to return to this page
139      *              (without repeating any form submissions etc).
140      */
141     private $returnaction = 'view';
143     /** @var array params to be used to return to this page */
144     private $returnparams = array();
146     /** @var string modulename prevents excessive calls to get_string */
147     private static $modulename = null;
149     /** @var string modulenameplural prevents excessive calls to get_string */
150     private static $modulenameplural = null;
152     /** @var array of marking workflow states for the current user */
153     private $markingworkflowstates = null;
155     /** @var bool whether to exclude users with inactive enrolment */
156     private $showonlyactiveenrol = null;
158     /** @var string A key used to identify userlists created by this object. */
159     private $useridlistid = null;
161     /** @var array cached list of participants for this assignment. The cache key will be group, showactive and the context id */
162     private $participants = array();
164     /** @var array cached list of user groups when team submissions are enabled. The cache key will be the user. */
165     private $usersubmissiongroups = array();
167     /** @var array cached list of user groups. The cache key will be the user. */
168     private $usergroups = array();
170     /** @var array cached list of IDs of users who share group membership with the user. The cache key will be the user. */
171     private $sharedgroupmembers = array();
173     /**
174      * @var stdClass The most recent team submission. Used to determine additional attempt numbers and whether
175      * to update the gradebook.
176      */
177     private $mostrecentteamsubmission = null;
179     /**
180      * Constructor for the base assign class.
181      *
182      * Note: For $coursemodule you can supply a stdclass if you like, but it
183      * will be more efficient to supply a cm_info object.
184      *
185      * @param mixed $coursemodulecontext context|null the course module context
186      *                                   (or the course context if the coursemodule has not been
187      *                                   created yet).
188      * @param mixed $coursemodule the current course module if it was already loaded,
189      *                            otherwise this class will load one from the context as required.
190      * @param mixed $course the current course  if it was already loaded,
191      *                      otherwise this class will load one from the context as required.
192      */
193     public function __construct($coursemodulecontext, $coursemodule, $course) {
194         global $SESSION;
196         $this->context = $coursemodulecontext;
197         $this->course = $course;
199         // Ensure that $this->coursemodule is a cm_info object (or null).
200         $this->coursemodule = cm_info::create($coursemodule);
202         // Temporary cache only lives for a single request - used to reduce db lookups.
203         $this->cache = array();
205         $this->submissionplugins = $this->load_plugins('assignsubmission');
206         $this->feedbackplugins = $this->load_plugins('assignfeedback');
208         // Extra entropy is required for uniqid() to work on cygwin.
209         $this->useridlistid = clean_param(uniqid('', true), PARAM_ALPHANUM);
211         if (!isset($SESSION->mod_assign_useridlist)) {
212             $SESSION->mod_assign_useridlist = [];
213         }
214     }
216     /**
217      * Set the action and parameters that can be used to return to the current page.
218      *
219      * @param string $action The action for the current page
220      * @param array $params An array of name value pairs which form the parameters
221      *                      to return to the current page.
222      * @return void
223      */
224     public function register_return_link($action, $params) {
225         global $PAGE;
226         $params['action'] = $action;
227         $cm = $this->get_course_module();
228         if ($cm) {
229             $currenturl = new moodle_url('/mod/assign/view.php', array('id' => $cm->id));
230         } else {
231             $currenturl = new moodle_url('/mod/assign/index.php', array('id' => $this->get_course()->id));
232         }
234         $currenturl->params($params);
235         $PAGE->set_url($currenturl);
236     }
238     /**
239      * Return an action that can be used to get back to the current page.
240      *
241      * @return string action
242      */
243     public function get_return_action() {
244         global $PAGE;
246         // Web services don't set a URL, we should avoid debugging when ussing the url object.
247         if (!WS_SERVER) {
248             $params = $PAGE->url->params();
249         }
251         if (!empty($params['action'])) {
252             return $params['action'];
253         }
254         return '';
255     }
257     /**
258      * Based on the current assignment settings should we display the intro.
259      *
260      * @return bool showintro
261      */
262     public function show_intro() {
263         if ($this->get_instance()->alwaysshowdescription ||
264                 time() > $this->get_instance()->allowsubmissionsfromdate) {
265             return true;
266         }
267         return false;
268     }
270     /**
271      * Return a list of parameters that can be used to get back to the current page.
272      *
273      * @return array params
274      */
275     public function get_return_params() {
276         global $PAGE;
278         $params = array();
279         if (!WS_SERVER) {
280             $params = $PAGE->url->params();
281         }
282         unset($params['id']);
283         unset($params['action']);
284         return $params;
285     }
287     /**
288      * Set the submitted form data.
289      *
290      * @param stdClass $data The form data (instance)
291      */
292     public function set_instance(stdClass $data) {
293         $this->instance = $data;
294     }
296     /**
297      * Set the context.
298      *
299      * @param context $context The new context
300      */
301     public function set_context(context $context) {
302         $this->context = $context;
303     }
305     /**
306      * Set the course data.
307      *
308      * @param stdClass $course The course data
309      */
310     public function set_course(stdClass $course) {
311         $this->course = $course;
312     }
314     /**
315      * Get list of feedback plugins installed.
316      *
317      * @return array
318      */
319     public function get_feedback_plugins() {
320         return $this->feedbackplugins;
321     }
323     /**
324      * Get list of submission plugins installed.
325      *
326      * @return array
327      */
328     public function get_submission_plugins() {
329         return $this->submissionplugins;
330     }
332     /**
333      * Is blind marking enabled and reveal identities not set yet?
334      *
335      * @return bool
336      */
337     public function is_blind_marking() {
338         return $this->get_instance()->blindmarking && !$this->get_instance()->revealidentities;
339     }
341     /**
342      * Is hidden grading enabled?
343      *
344      * This just checks the assignment settings. Remember to check
345      * the user has the 'showhiddengrader' capability too
346      *
347      * @return bool
348      */
349     public function is_hidden_grader() {
350         return $this->get_instance()->hidegrader;
351     }
353     /**
354      * Does an assignment have submission(s) or grade(s) already?
355      *
356      * @return bool
357      */
358     public function has_submissions_or_grades() {
359         $allgrades = $this->count_grades();
360         $allsubmissions = $this->count_submissions();
361         if (($allgrades == 0) && ($allsubmissions == 0)) {
362             return false;
363         }
364         return true;
365     }
367     /**
368      * Get a specific submission plugin by its type.
369      *
370      * @param string $subtype assignsubmission | assignfeedback
371      * @param string $type
372      * @return mixed assign_plugin|null
373      */
374     public function get_plugin_by_type($subtype, $type) {
375         $shortsubtype = substr($subtype, strlen('assign'));
376         $name = $shortsubtype . 'plugins';
377         if ($name != 'feedbackplugins' && $name != 'submissionplugins') {
378             return null;
379         }
380         $pluginlist = $this->$name;
381         foreach ($pluginlist as $plugin) {
382             if ($plugin->get_type() == $type) {
383                 return $plugin;
384             }
385         }
386         return null;
387     }
389     /**
390      * Get a feedback plugin by type.
391      *
392      * @param string $type - The type of plugin e.g comments
393      * @return mixed assign_feedback_plugin|null
394      */
395     public function get_feedback_plugin_by_type($type) {
396         return $this->get_plugin_by_type('assignfeedback', $type);
397     }
399     /**
400      * Get a submission plugin by type.
401      *
402      * @param string $type - The type of plugin e.g comments
403      * @return mixed assign_submission_plugin|null
404      */
405     public function get_submission_plugin_by_type($type) {
406         return $this->get_plugin_by_type('assignsubmission', $type);
407     }
409     /**
410      * Load the plugins from the sub folders under subtype.
411      *
412      * @param string $subtype - either submission or feedback
413      * @return array - The sorted list of plugins
414      */
415     public function load_plugins($subtype) {
416         global $CFG;
417         $result = array();
419         $names = core_component::get_plugin_list($subtype);
421         foreach ($names as $name => $path) {
422             if (file_exists($path . '/locallib.php')) {
423                 require_once($path . '/locallib.php');
425                 $shortsubtype = substr($subtype, strlen('assign'));
426                 $pluginclass = 'assign_' . $shortsubtype . '_' . $name;
428                 $plugin = new $pluginclass($this, $name);
430                 if ($plugin instanceof assign_plugin) {
431                     $idx = $plugin->get_sort_order();
432                     while (array_key_exists($idx, $result)) {
433                         $idx +=1;
434                     }
435                     $result[$idx] = $plugin;
436                 }
437             }
438         }
439         ksort($result);
440         return $result;
441     }
443     /**
444      * Display the assignment, used by view.php
445      *
446      * The assignment is displayed differently depending on your role,
447      * the settings for the assignment and the status of the assignment.
448      *
449      * @param string $action The current action if any.
450      * @param array $args Optional arguments to pass to the view (instead of getting them from GET and POST).
451      * @return string - The page output.
452      */
453     public function view($action='', $args = array()) {
454         global $PAGE;
456         $o = '';
457         $mform = null;
458         $notices = array();
459         $nextpageparams = array();
461         if (!empty($this->get_course_module()->id)) {
462             $nextpageparams['id'] = $this->get_course_module()->id;
463         }
465         // Handle form submissions first.
466         if ($action == 'savesubmission') {
467             $action = 'editsubmission';
468             if ($this->process_save_submission($mform, $notices)) {
469                 $action = 'redirect';
470                 if ($this->can_grade()) {
471                     $nextpageparams['action'] = 'grading';
472                 } else {
473                     $nextpageparams['action'] = 'view';
474                 }
475             }
476         } else if ($action == 'editprevioussubmission') {
477             $action = 'editsubmission';
478             if ($this->process_copy_previous_attempt($notices)) {
479                 $action = 'redirect';
480                 $nextpageparams['action'] = 'editsubmission';
481             }
482         } else if ($action == 'lock') {
483             $this->process_lock_submission();
484             $action = 'redirect';
485             $nextpageparams['action'] = 'grading';
486         } else if ($action == 'removesubmission') {
487             $this->process_remove_submission();
488             $action = 'redirect';
489             if ($this->can_grade()) {
490                 $nextpageparams['action'] = 'grading';
491             } else {
492                 $nextpageparams['action'] = 'view';
493             }
494         } else if ($action == 'addattempt') {
495             $this->process_add_attempt(required_param('userid', PARAM_INT));
496             $action = 'redirect';
497             $nextpageparams['action'] = 'grading';
498         } else if ($action == 'reverttodraft') {
499             $this->process_revert_to_draft();
500             $action = 'redirect';
501             $nextpageparams['action'] = 'grading';
502         } else if ($action == 'unlock') {
503             $this->process_unlock_submission();
504             $action = 'redirect';
505             $nextpageparams['action'] = 'grading';
506         } else if ($action == 'setbatchmarkingworkflowstate') {
507             $this->process_set_batch_marking_workflow_state();
508             $action = 'redirect';
509             $nextpageparams['action'] = 'grading';
510         } else if ($action == 'setbatchmarkingallocation') {
511             $this->process_set_batch_marking_allocation();
512             $action = 'redirect';
513             $nextpageparams['action'] = 'grading';
514         } else if ($action == 'confirmsubmit') {
515             $action = 'submit';
516             if ($this->process_submit_for_grading($mform, $notices)) {
517                 $action = 'redirect';
518                 $nextpageparams['action'] = 'view';
519             } else if ($notices) {
520                 $action = 'viewsubmitforgradingerror';
521             }
522         } else if ($action == 'submitotherforgrading') {
523             if ($this->process_submit_other_for_grading($mform, $notices)) {
524                 $action = 'redirect';
525                 $nextpageparams['action'] = 'grading';
526             } else {
527                 $action = 'viewsubmitforgradingerror';
528             }
529         } else if ($action == 'gradingbatchoperation') {
530             $action = $this->process_grading_batch_operation($mform);
531             if ($action == 'grading') {
532                 $action = 'redirect';
533                 $nextpageparams['action'] = 'grading';
534             }
535         } else if ($action == 'submitgrade') {
536             if (optional_param('saveandshownext', null, PARAM_RAW)) {
537                 // Save and show next.
538                 $action = 'grade';
539                 if ($this->process_save_grade($mform)) {
540                     $action = 'redirect';
541                     $nextpageparams['action'] = 'grade';
542                     $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) + 1;
543                     $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
544                 }
545             } else if (optional_param('nosaveandprevious', null, PARAM_RAW)) {
546                 $action = 'redirect';
547                 $nextpageparams['action'] = 'grade';
548                 $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) - 1;
549                 $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
550             } else if (optional_param('nosaveandnext', null, PARAM_RAW)) {
551                 $action = 'redirect';
552                 $nextpageparams['action'] = 'grade';
553                 $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) + 1;
554                 $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
555             } else if (optional_param('savegrade', null, PARAM_RAW)) {
556                 // Save changes button.
557                 $action = 'grade';
558                 if ($this->process_save_grade($mform)) {
559                     $action = 'redirect';
560                     $nextpageparams['action'] = 'savegradingresult';
561                 }
562             } else {
563                 // Cancel button.
564                 $action = 'redirect';
565                 $nextpageparams['action'] = 'grading';
566             }
567         } else if ($action == 'quickgrade') {
568             $message = $this->process_save_quick_grades();
569             $action = 'quickgradingresult';
570         } else if ($action == 'saveoptions') {
571             $this->process_save_grading_options();
572             $action = 'redirect';
573             $nextpageparams['action'] = 'grading';
574         } else if ($action == 'saveextension') {
575             $action = 'grantextension';
576             if ($this->process_save_extension($mform)) {
577                 $action = 'redirect';
578                 $nextpageparams['action'] = 'grading';
579             }
580         } else if ($action == 'revealidentitiesconfirm') {
581             $this->process_reveal_identities();
582             $action = 'redirect';
583             $nextpageparams['action'] = 'grading';
584         }
586         $returnparams = array('rownum'=>optional_param('rownum', 0, PARAM_INT),
587                               'useridlistid' => optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM));
588         $this->register_return_link($action, $returnparams);
590         // Include any page action as part of the body tag CSS id.
591         if (!empty($action)) {
592             $PAGE->set_pagetype('mod-assign-' . $action);
593         }
594         // Now show the right view page.
595         if ($action == 'redirect') {
596             $nextpageurl = new moodle_url('/mod/assign/view.php', $nextpageparams);
597             redirect($nextpageurl);
598             return;
599         } else if ($action == 'savegradingresult') {
600             $message = get_string('gradingchangessaved', 'assign');
601             $o .= $this->view_savegrading_result($message);
602         } else if ($action == 'quickgradingresult') {
603             $mform = null;
604             $o .= $this->view_quickgrading_result($message);
605         } else if ($action == 'gradingpanel') {
606             $o .= $this->view_single_grading_panel($args);
607         } else if ($action == 'grade') {
608             $o .= $this->view_single_grade_page($mform);
609         } else if ($action == 'viewpluginassignfeedback') {
610             $o .= $this->view_plugin_content('assignfeedback');
611         } else if ($action == 'viewpluginassignsubmission') {
612             $o .= $this->view_plugin_content('assignsubmission');
613         } else if ($action == 'editsubmission') {
614             $o .= $this->view_edit_submission_page($mform, $notices);
615         } else if ($action == 'grader') {
616             $o .= $this->view_grader();
617         } else if ($action == 'grading') {
618             $o .= $this->view_grading_page();
619         } else if ($action == 'downloadall') {
620             $o .= $this->download_submissions();
621         } else if ($action == 'submit') {
622             $o .= $this->check_submit_for_grading($mform);
623         } else if ($action == 'grantextension') {
624             $o .= $this->view_grant_extension($mform);
625         } else if ($action == 'revealidentities') {
626             $o .= $this->view_reveal_identities_confirm($mform);
627         } else if ($action == 'removesubmissionconfirm') {
628             $o .= $this->view_remove_submission_confirm();
629         } else if ($action == 'plugingradingbatchoperation') {
630             $o .= $this->view_plugin_grading_batch_operation($mform);
631         } else if ($action == 'viewpluginpage') {
632              $o .= $this->view_plugin_page();
633         } else if ($action == 'viewcourseindex') {
634              $o .= $this->view_course_index();
635         } else if ($action == 'viewbatchsetmarkingworkflowstate') {
636              $o .= $this->view_batch_set_workflow_state($mform);
637         } else if ($action == 'viewbatchmarkingallocation') {
638             $o .= $this->view_batch_markingallocation($mform);
639         } else if ($action == 'viewsubmitforgradingerror') {
640             $o .= $this->view_error_page(get_string('submitforgrading', 'assign'), $notices);
641         } else if ($action == 'fixrescalednullgrades') {
642             $o .= $this->view_fix_rescaled_null_grades();
643         } else {
644             $o .= $this->view_submission_page();
645         }
647         return $o;
648     }
650     /**
651      * Add this instance to the database.
652      *
653      * @param stdClass $formdata The data submitted from the form
654      * @param bool $callplugins This is used to skip the plugin code
655      *             when upgrading an old assignment to a new one (the plugins get called manually)
656      * @return mixed false if an error occurs or the int id of the new instance
657      */
658     public function add_instance(stdClass $formdata, $callplugins) {
659         global $DB;
660         $adminconfig = $this->get_admin_config();
662         $err = '';
664         // Add the database record.
665         $update = new stdClass();
666         $update->name = $formdata->name;
667         $update->timemodified = time();
668         $update->timecreated = time();
669         $update->course = $formdata->course;
670         $update->courseid = $formdata->course;
671         $update->intro = $formdata->intro;
672         $update->introformat = $formdata->introformat;
673         $update->alwaysshowdescription = !empty($formdata->alwaysshowdescription);
674         $update->submissiondrafts = $formdata->submissiondrafts;
675         $update->requiresubmissionstatement = $formdata->requiresubmissionstatement;
676         $update->sendnotifications = $formdata->sendnotifications;
677         $update->sendlatenotifications = $formdata->sendlatenotifications;
678         $update->sendstudentnotifications = $adminconfig->sendstudentnotifications;
679         if (isset($formdata->sendstudentnotifications)) {
680             $update->sendstudentnotifications = $formdata->sendstudentnotifications;
681         }
682         $update->duedate = $formdata->duedate;
683         $update->cutoffdate = $formdata->cutoffdate;
684         $update->gradingduedate = $formdata->gradingduedate;
685         $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate;
686         $update->grade = $formdata->grade;
687         $update->completionsubmit = !empty($formdata->completionsubmit);
688         $update->teamsubmission = $formdata->teamsubmission;
689         $update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit;
690         if (isset($formdata->teamsubmissiongroupingid)) {
691             $update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid;
692         }
693         $update->blindmarking = $formdata->blindmarking;
694         if (isset($formdata->hidegrader)) {
695             $update->hidegrader = $formdata->hidegrader;
696         }
697         $update->attemptreopenmethod = ASSIGN_ATTEMPT_REOPEN_METHOD_NONE;
698         if (!empty($formdata->attemptreopenmethod)) {
699             $update->attemptreopenmethod = $formdata->attemptreopenmethod;
700         }
701         if (!empty($formdata->maxattempts)) {
702             $update->maxattempts = $formdata->maxattempts;
703         }
704         if (isset($formdata->preventsubmissionnotingroup)) {
705             $update->preventsubmissionnotingroup = $formdata->preventsubmissionnotingroup;
706         }
707         $update->markingworkflow = $formdata->markingworkflow;
708         $update->markingallocation = $formdata->markingallocation;
709         if (empty($update->markingworkflow)) { // If marking workflow is disabled, make sure allocation is disabled.
710             $update->markingallocation = 0;
711         }
713         $returnid = $DB->insert_record('assign', $update);
714         $this->instance = $DB->get_record('assign', array('id'=>$returnid), '*', MUST_EXIST);
715         // Cache the course record.
716         $this->course = $DB->get_record('course', array('id'=>$formdata->course), '*', MUST_EXIST);
718         $this->save_intro_draft_files($formdata);
720         if ($callplugins) {
721             // Call save_settings hook for submission plugins.
722             foreach ($this->submissionplugins as $plugin) {
723                 if (!$this->update_plugin_instance($plugin, $formdata)) {
724                     print_error($plugin->get_error());
725                     return false;
726                 }
727             }
728             foreach ($this->feedbackplugins as $plugin) {
729                 if (!$this->update_plugin_instance($plugin, $formdata)) {
730                     print_error($plugin->get_error());
731                     return false;
732                 }
733             }
735             // In the case of upgrades the coursemodule has not been set,
736             // so we need to wait before calling these two.
737             $this->update_calendar($formdata->coursemodule);
738             if (!empty($formdata->completionexpected)) {
739                 \core_completion\api::update_completion_date_event($formdata->coursemodule, 'assign', $this->instance,
740                         $formdata->completionexpected);
741             }
742             $this->update_gradebook(false, $formdata->coursemodule);
744         }
746         $update = new stdClass();
747         $update->id = $this->get_instance()->id;
748         $update->nosubmissions = (!$this->is_any_submission_plugin_enabled()) ? 1: 0;
749         $DB->update_record('assign', $update);
751         return $returnid;
752     }
754     /**
755      * Delete all grades from the gradebook for this assignment.
756      *
757      * @return bool
758      */
759     protected function delete_grades() {
760         global $CFG;
762         $result = grade_update('mod/assign',
763                                $this->get_course()->id,
764                                'mod',
765                                'assign',
766                                $this->get_instance()->id,
767                                0,
768                                null,
769                                array('deleted'=>1));
770         return $result == GRADE_UPDATE_OK;
771     }
773     /**
774      * Delete this instance from the database.
775      *
776      * @return bool false if an error occurs
777      */
778     public function delete_instance() {
779         global $DB;
780         $result = true;
782         foreach ($this->submissionplugins as $plugin) {
783             if (!$plugin->delete_instance()) {
784                 print_error($plugin->get_error());
785                 $result = false;
786             }
787         }
788         foreach ($this->feedbackplugins as $plugin) {
789             if (!$plugin->delete_instance()) {
790                 print_error($plugin->get_error());
791                 $result = false;
792             }
793         }
795         // Delete files associated with this assignment.
796         $fs = get_file_storage();
797         if (! $fs->delete_area_files($this->context->id) ) {
798             $result = false;
799         }
801         $this->delete_all_overrides();
803         // Delete_records will throw an exception if it fails - so no need for error checking here.
804         $DB->delete_records('assign_submission', array('assignment' => $this->get_instance()->id));
805         $DB->delete_records('assign_grades', array('assignment' => $this->get_instance()->id));
806         $DB->delete_records('assign_plugin_config', array('assignment' => $this->get_instance()->id));
807         $DB->delete_records('assign_user_flags', array('assignment' => $this->get_instance()->id));
808         $DB->delete_records('assign_user_mapping', array('assignment' => $this->get_instance()->id));
810         // Delete items from the gradebook.
811         if (! $this->delete_grades()) {
812             $result = false;
813         }
815         // Delete the instance.
816         $DB->delete_records('assign', array('id'=>$this->get_instance()->id));
818         return $result;
819     }
821     /**
822      * Deletes a assign override from the database and clears any corresponding calendar events
823      *
824      * @param int $overrideid The id of the override being deleted
825      * @return bool true on success
826      */
827     public function delete_override($overrideid) {
828         global $CFG, $DB;
830         require_once($CFG->dirroot . '/calendar/lib.php');
832         $cm = $this->get_course_module();
833         if (empty($cm)) {
834             $instance = $this->get_instance();
835             $cm = get_coursemodule_from_instance('assign', $instance->id, $instance->course);
836         }
838         $override = $DB->get_record('assign_overrides', array('id' => $overrideid), '*', MUST_EXIST);
840         // Delete the events.
841         $conds = array('modulename' => 'assign', 'instance' => $this->get_instance()->id);
842         if (isset($override->userid)) {
843             $conds['userid'] = $override->userid;
844         } else {
845             $conds['groupid'] = $override->groupid;
846         }
847         $events = $DB->get_records('event', $conds);
848         foreach ($events as $event) {
849             $eventold = calendar_event::load($event);
850             $eventold->delete();
851         }
853         $DB->delete_records('assign_overrides', array('id' => $overrideid));
855         // Set the common parameters for one of the events we will be triggering.
856         $params = array(
857             'objectid' => $override->id,
858             'context' => context_module::instance($cm->id),
859             'other' => array(
860                 'assignid' => $override->assignid
861             )
862         );
863         // Determine which override deleted event to fire.
864         if (!empty($override->userid)) {
865             $params['relateduserid'] = $override->userid;
866             $event = \mod_assign\event\user_override_deleted::create($params);
867         } else {
868             $params['other']['groupid'] = $override->groupid;
869             $event = \mod_assign\event\group_override_deleted::create($params);
870         }
872         // Trigger the override deleted event.
873         $event->add_record_snapshot('assign_overrides', $override);
874         $event->trigger();
876         return true;
877     }
879     /**
880      * Deletes all assign overrides from the database and clears any corresponding calendar events
881      */
882     public function delete_all_overrides() {
883         global $DB;
885         $overrides = $DB->get_records('assign_overrides', array('assignid' => $this->get_instance()->id), 'id');
886         foreach ($overrides as $override) {
887             $this->delete_override($override->id);
888         }
889     }
891     /**
892      * Updates the assign properties with override information for a user.
893      *
894      * Algorithm:  For each assign setting, if there is a matching user-specific override,
895      *   then use that otherwise, if there are group-specific overrides, return the most
896      *   lenient combination of them.  If neither applies, leave the assign setting unchanged.
897      *
898      * @param int $userid The userid.
899      */
900     public function update_effective_access($userid) {
902         $override = $this->override_exists($userid);
904         // Merge with assign defaults.
905         $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate');
906         foreach ($keys as $key) {
907             if (isset($override->{$key})) {
908                 $this->get_instance()->{$key} = $override->{$key};
909             }
910         }
912     }
914     /**
915      * Returns whether an assign has any overrides.
916      *
917      * @return true if any, false if not
918      */
919     public function has_overrides() {
920         global $DB;
922         $override = $DB->record_exists('assign_overrides', array('assignid' => $this->get_instance()->id));
924         if ($override) {
925             return true;
926         }
928         return false;
929     }
931     /**
932      * Returns user override
933      *
934      * Algorithm:  For each assign setting, if there is a matching user-specific override,
935      *   then use that otherwise, if there are group-specific overrides, use the one with the
936      *   lowest sort order. If neither applies, leave the assign setting unchanged.
937      *
938      * @param int $userid The userid.
939      * @return stdClass The override
940      */
941     public function override_exists($userid) {
942         global $DB;
944         // Gets an assoc array containing the keys for defined user overrides only.
945         $getuseroverride = function($userid) use ($DB) {
946             $useroverride = $DB->get_record('assign_overrides', ['assignid' => $this->get_instance()->id, 'userid' => $userid]);
947             return $useroverride ? get_object_vars($useroverride) : [];
948         };
950         // Gets an assoc array containing the keys for defined group overrides only.
951         $getgroupoverride = function($userid) use ($DB) {
952             $groupings = groups_get_user_groups($this->get_instance()->course, $userid);
954             if (empty($groupings[0])) {
955                 return [];
956             }
958             // Select all overrides that apply to the User's groups.
959             list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0]));
960             $sql = "SELECT * FROM {assign_overrides}
961                     WHERE groupid $extra AND assignid = ? ORDER BY sortorder ASC";
962             $params[] = $this->get_instance()->id;
963             $groupoverride = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE);
965             return $groupoverride ? get_object_vars($groupoverride) : [];
966         };
968         // Later arguments clobber earlier ones with array_merge. The two helper functions
969         // return arrays containing keys for only the defined overrides. So we get the
970         // desired behaviour as per the algorithm.
971         return (object)array_merge(
972             ['duedate' => null, 'cutoffdate' => null, 'allowsubmissionsfromdate' => null],
973             $getgroupoverride($userid),
974             $getuseroverride($userid)
975         );
976     }
978     /**
979      * Check if the given calendar_event is either a user or group override
980      * event.
981      *
982      * @return bool
983      */
984     public function is_override_calendar_event(\calendar_event $event) {
985         global $DB;
987         if (!isset($event->modulename)) {
988             return false;
989         }
991         if ($event->modulename != 'assign') {
992             return false;
993         }
995         if (!isset($event->instance)) {
996             return false;
997         }
999         if (!isset($event->userid) && !isset($event->groupid)) {
1000             return false;
1001         }
1003         $overrideparams = [
1004             'assignid' => $event->instance
1005         ];
1007         if (isset($event->groupid)) {
1008             $overrideparams['groupid'] = $event->groupid;
1009         } else if (isset($event->userid)) {
1010             $overrideparams['userid'] = $event->userid;
1011         }
1013         if ($DB->get_record('assign_overrides', $overrideparams)) {
1014             return true;
1015         } else {
1016             return false;
1017         }
1018     }
1020     /**
1021      * This function calculates the minimum and maximum cutoff values for the timestart of
1022      * the given event.
1023      *
1024      * It will return an array with two values, the first being the minimum cutoff value and
1025      * the second being the maximum cutoff value. Either or both values can be null, which
1026      * indicates there is no minimum or maximum, respectively.
1027      *
1028      * If a cutoff is required then the function must return an array containing the cutoff
1029      * timestamp and error string to display to the user if the cutoff value is violated.
1030      *
1031      * A minimum and maximum cutoff return value will look like:
1032      * [
1033      *     [1505704373, 'The due date must be after the sbumission start date'],
1034      *     [1506741172, 'The due date must be before the cutoff date']
1035      * ]
1036      *
1037      * If the event does not have a valid timestart range then [false, false] will
1038      * be returned.
1039      *
1040      * @param calendar_event $event The calendar event to get the time range for
1041      * @return array
1042      */
1043     function get_valid_calendar_event_timestart_range(\calendar_event $event) {
1044         $instance = $this->get_instance();
1045         $submissionsfromdate = $instance->allowsubmissionsfromdate;
1046         $cutoffdate = $instance->cutoffdate;
1047         $duedate = $instance->duedate;
1048         $gradingduedate = $instance->gradingduedate;
1049         $mindate = null;
1050         $maxdate = null;
1052         if ($event->eventtype == ASSIGN_EVENT_TYPE_DUE) {
1053             // This check is in here because due date events are currently
1054             // the only events that can be overridden, so we can save a DB
1055             // query if we don't bother checking other events.
1056             if ($this->is_override_calendar_event($event)) {
1057                 // This is an override event so there is no valid timestart
1058                 // range to set it to.
1059                 return [false, false];
1060             }
1062             if ($submissionsfromdate) {
1063                 $mindate = [
1064                     $submissionsfromdate,
1065                     get_string('duedatevalidation', 'assign'),
1066                 ];
1067             }
1069             if ($cutoffdate) {
1070                 $maxdate = [
1071                     $cutoffdate,
1072                     get_string('cutoffdatevalidation', 'assign'),
1073                 ];
1074             }
1076             if ($gradingduedate) {
1077                 // If we don't have a cutoff date or we've got a grading due date
1078                 // that is earlier than the cutoff then we should use that as the
1079                 // upper limit for the due date.
1080                 if (!$cutoffdate || $gradingduedate < $cutoffdate) {
1081                     $maxdate = [
1082                         $gradingduedate,
1083                         get_string('gradingdueduedatevalidation', 'assign'),
1084                     ];
1085                 }
1086             }
1087         } else if ($event->eventtype == ASSIGN_EVENT_TYPE_GRADINGDUE) {
1088             if ($duedate) {
1089                 $mindate = [
1090                     $duedate,
1091                     get_string('gradingdueduedatevalidation', 'assign'),
1092                 ];
1093             } else if ($submissionsfromdate) {
1094                 $mindate = [
1095                     $submissionsfromdate,
1096                     get_string('gradingduefromdatevalidation', 'assign'),
1097                 ];
1098             }
1099         }
1101         return [$mindate, $maxdate];
1102     }
1104     /**
1105      * Actual implementation of the reset course functionality, delete all the
1106      * assignment submissions for course $data->courseid.
1107      *
1108      * @param stdClass $data the data submitted from the reset course.
1109      * @return array status array
1110      */
1111     public function reset_userdata($data) {
1112         global $CFG, $DB;
1114         $componentstr = get_string('modulenameplural', 'assign');
1115         $status = array();
1117         $fs = get_file_storage();
1118         if (!empty($data->reset_assign_submissions)) {
1119             // Delete files associated with this assignment.
1120             foreach ($this->submissionplugins as $plugin) {
1121                 $fileareas = array();
1122                 $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type();
1123                 $fileareas = $plugin->get_file_areas();
1124                 foreach ($fileareas as $filearea => $notused) {
1125                     $fs->delete_area_files($this->context->id, $plugincomponent, $filearea);
1126                 }
1128                 if (!$plugin->delete_instance()) {
1129                     $status[] = array('component'=>$componentstr,
1130                                       'item'=>get_string('deleteallsubmissions', 'assign'),
1131                                       'error'=>$plugin->get_error());
1132                 }
1133             }
1135             foreach ($this->feedbackplugins as $plugin) {
1136                 $fileareas = array();
1137                 $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type();
1138                 $fileareas = $plugin->get_file_areas();
1139                 foreach ($fileareas as $filearea => $notused) {
1140                     $fs->delete_area_files($this->context->id, $plugincomponent, $filearea);
1141                 }
1143                 if (!$plugin->delete_instance()) {
1144                     $status[] = array('component'=>$componentstr,
1145                                       'item'=>get_string('deleteallsubmissions', 'assign'),
1146                                       'error'=>$plugin->get_error());
1147                 }
1148             }
1150             $assignids = $DB->get_records('assign', array('course' => $data->courseid), '', 'id');
1151             list($sql, $params) = $DB->get_in_or_equal(array_keys($assignids));
1153             $DB->delete_records_select('assign_submission', "assignment $sql", $params);
1154             $DB->delete_records_select('assign_user_flags', "assignment $sql", $params);
1156             $status[] = array('component'=>$componentstr,
1157                               'item'=>get_string('deleteallsubmissions', 'assign'),
1158                               'error'=>false);
1160             if (!empty($data->reset_gradebook_grades)) {
1161                 $DB->delete_records_select('assign_grades', "assignment $sql", $params);
1162                 // Remove all grades from gradebook.
1163                 require_once($CFG->dirroot.'/mod/assign/lib.php');
1164                 assign_reset_gradebook($data->courseid);
1165             }
1167             // Reset revealidentities for assign if blindmarking is enabled.
1168             if ($this->get_instance()->blindmarking) {
1169                 $DB->set_field('assign', 'revealidentities', 0, array('id' => $this->get_instance()->id));
1170             }
1171         }
1173         // Remove user overrides.
1174         if (!empty($data->reset_assign_user_overrides)) {
1175             $DB->delete_records_select('assign_overrides',
1176                 'assignid IN (SELECT id FROM {assign} WHERE course = ?) AND userid IS NOT NULL', array($data->courseid));
1177             $status[] = array(
1178                 'component' => $componentstr,
1179                 'item' => get_string('useroverridesdeleted', 'assign'),
1180                 'error' => false);
1181         }
1182         // Remove group overrides.
1183         if (!empty($data->reset_assign_group_overrides)) {
1184             $DB->delete_records_select('assign_overrides',
1185                 'assignid IN (SELECT id FROM {assign} WHERE course = ?) AND groupid IS NOT NULL', array($data->courseid));
1186             $status[] = array(
1187                 'component' => $componentstr,
1188                 'item' => get_string('groupoverridesdeleted', 'assign'),
1189                 'error' => false);
1190         }
1192         // Updating dates - shift may be negative too.
1193         if ($data->timeshift) {
1194             $DB->execute("UPDATE {assign_overrides}
1195                          SET allowsubmissionsfromdate = allowsubmissionsfromdate + ?
1196                        WHERE assignid = ? AND allowsubmissionsfromdate <> 0",
1197                 array($data->timeshift, $this->get_instance()->id));
1198             $DB->execute("UPDATE {assign_overrides}
1199                          SET duedate = duedate + ?
1200                        WHERE assignid = ? AND duedate <> 0",
1201                 array($data->timeshift, $this->get_instance()->id));
1202             $DB->execute("UPDATE {assign_overrides}
1203                          SET cutoffdate = cutoffdate + ?
1204                        WHERE assignid =? AND cutoffdate <> 0",
1205                 array($data->timeshift, $this->get_instance()->id));
1207             // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
1208             // See MDL-9367.
1209             shift_course_mod_dates('assign',
1210                                     array('duedate', 'allowsubmissionsfromdate', 'cutoffdate'),
1211                                     $data->timeshift,
1212                                     $data->courseid, $this->get_instance()->id);
1213             $status[] = array('component'=>$componentstr,
1214                               'item'=>get_string('datechanged'),
1215                               'error'=>false);
1216         }
1218         return $status;
1219     }
1221     /**
1222      * Update the settings for a single plugin.
1223      *
1224      * @param assign_plugin $plugin The plugin to update
1225      * @param stdClass $formdata The form data
1226      * @return bool false if an error occurs
1227      */
1228     protected function update_plugin_instance(assign_plugin $plugin, stdClass $formdata) {
1229         if ($plugin->is_visible()) {
1230             $enabledname = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
1231             if (!empty($formdata->$enabledname)) {
1232                 $plugin->enable();
1233                 if (!$plugin->save_settings($formdata)) {
1234                     print_error($plugin->get_error());
1235                     return false;
1236                 }
1237             } else {
1238                 $plugin->disable();
1239             }
1240         }
1241         return true;
1242     }
1244     /**
1245      * Update the gradebook information for this assignment.
1246      *
1247      * @param bool $reset If true, will reset all grades in the gradbook for this assignment
1248      * @param int $coursemoduleid This is required because it might not exist in the database yet
1249      * @return bool
1250      */
1251     public function update_gradebook($reset, $coursemoduleid) {
1252         global $CFG;
1254         require_once($CFG->dirroot.'/mod/assign/lib.php');
1255         $assign = clone $this->get_instance();
1256         $assign->cmidnumber = $coursemoduleid;
1258         // Set assign gradebook feedback plugin status (enabled and visible).
1259         $assign->gradefeedbackenabled = $this->is_gradebook_feedback_enabled();
1261         $param = null;
1262         if ($reset) {
1263             $param = 'reset';
1264         }
1266         return assign_grade_item_update($assign, $param);
1267     }
1269     /**
1270      * Get the marking table page size
1271      *
1272      * @return integer
1273      */
1274     public function get_assign_perpage() {
1275         $perpage = (int) get_user_preferences('assign_perpage', 10);
1276         $adminconfig = $this->get_admin_config();
1277         $maxperpage = -1;
1278         if (isset($adminconfig->maxperpage)) {
1279             $maxperpage = $adminconfig->maxperpage;
1280         }
1281         if (isset($maxperpage) &&
1282             $maxperpage != -1 &&
1283             ($perpage == -1 || $perpage > $maxperpage)) {
1284             $perpage = $maxperpage;
1285         }
1286         return $perpage;
1287     }
1289     /**
1290      * Load and cache the admin config for this module.
1291      *
1292      * @return stdClass the plugin config
1293      */
1294     public function get_admin_config() {
1295         if ($this->adminconfig) {
1296             return $this->adminconfig;
1297         }
1298         $this->adminconfig = get_config('assign');
1299         return $this->adminconfig;
1300     }
1302     /**
1303      * Update the calendar entries for this assignment.
1304      *
1305      * @param int $coursemoduleid - Required to pass this in because it might
1306      *                              not exist in the database yet.
1307      * @return bool
1308      */
1309     public function update_calendar($coursemoduleid) {
1310         global $DB, $CFG;
1311         require_once($CFG->dirroot.'/calendar/lib.php');
1313         // Special case for add_instance as the coursemodule has not been set yet.
1314         $instance = $this->get_instance();
1316         // Start with creating the event.
1317         $event = new stdClass();
1318         $event->modulename  = 'assign';
1319         $event->courseid = $instance->course;
1320         $event->groupid = 0;
1321         $event->userid  = 0;
1322         $event->instance  = $instance->id;
1323         $event->type = CALENDAR_EVENT_TYPE_ACTION;
1325         // Convert the links to pluginfile. It is a bit hacky but at this stage the files
1326         // might not have been saved in the module area yet.
1327         $intro = $instance->intro;
1328         if ($draftid = file_get_submitted_draft_itemid('introeditor')) {
1329             $intro = file_rewrite_urls_to_pluginfile($intro, $draftid);
1330         }
1332         // We need to remove the links to files as the calendar is not ready
1333         // to support module events with file areas.
1334         $intro = strip_pluginfile_content($intro);
1335         if ($this->show_intro()) {
1336             $event->description = array(
1337                 'text' => $intro,
1338                 'format' => $instance->introformat
1339             );
1340         } else {
1341             $event->description = array(
1342                 'text' => '',
1343                 'format' => $instance->introformat
1344             );
1345         }
1347         $eventtype = ASSIGN_EVENT_TYPE_DUE;
1348         if ($instance->duedate) {
1349             $event->name = get_string('calendardue', 'assign', $instance->name);
1350             $event->eventtype = $eventtype;
1351             $event->timestart = $instance->duedate;
1352             $event->timesort = $instance->duedate;
1353             $select = "modulename = :modulename
1354                        AND instance = :instance
1355                        AND eventtype = :eventtype
1356                        AND groupid = 0
1357                        AND courseid <> 0";
1358             $params = array('modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => $eventtype);
1359             $event->id = $DB->get_field_select('event', 'id', $select, $params);
1361             // Now process the event.
1362             if ($event->id) {
1363                 $calendarevent = calendar_event::load($event->id);
1364                 $calendarevent->update($event, false);
1365             } else {
1366                 calendar_event::create($event, false);
1367             }
1368         } else {
1369             $DB->delete_records('event', array('modulename' => 'assign', 'instance' => $instance->id,
1370                 'eventtype' => $eventtype));
1371         }
1373         $eventtype = ASSIGN_EVENT_TYPE_GRADINGDUE;
1374         if ($instance->gradingduedate) {
1375             $event->name = get_string('calendargradingdue', 'assign', $instance->name);
1376             $event->eventtype = $eventtype;
1377             $event->timestart = $instance->gradingduedate;
1378             $event->timesort = $instance->gradingduedate;
1379             $event->id = $DB->get_field('event', 'id', array('modulename' => 'assign',
1380                 'instance' => $instance->id, 'eventtype' => $event->eventtype));
1382             // Now process the event.
1383             if ($event->id) {
1384                 $calendarevent = calendar_event::load($event->id);
1385                 $calendarevent->update($event, false);
1386             } else {
1387                 calendar_event::create($event, false);
1388             }
1389         } else {
1390             $DB->delete_records('event', array('modulename' => 'assign', 'instance' => $instance->id,
1391                 'eventtype' => $eventtype));
1392         }
1394         return true;
1395     }
1397     /**
1398      * Update this instance in the database.
1399      *
1400      * @param stdClass $formdata - the data submitted from the form
1401      * @return bool false if an error occurs
1402      */
1403     public function update_instance($formdata) {
1404         global $DB;
1405         $adminconfig = $this->get_admin_config();
1407         $update = new stdClass();
1408         $update->id = $formdata->instance;
1409         $update->name = $formdata->name;
1410         $update->timemodified = time();
1411         $update->course = $formdata->course;
1412         $update->intro = $formdata->intro;
1413         $update->introformat = $formdata->introformat;
1414         $update->alwaysshowdescription = !empty($formdata->alwaysshowdescription);
1415         $update->submissiondrafts = $formdata->submissiondrafts;
1416         $update->requiresubmissionstatement = $formdata->requiresubmissionstatement;
1417         $update->sendnotifications = $formdata->sendnotifications;
1418         $update->sendlatenotifications = $formdata->sendlatenotifications;
1419         $update->sendstudentnotifications = $adminconfig->sendstudentnotifications;
1420         if (isset($formdata->sendstudentnotifications)) {
1421             $update->sendstudentnotifications = $formdata->sendstudentnotifications;
1422         }
1423         $update->duedate = $formdata->duedate;
1424         $update->cutoffdate = $formdata->cutoffdate;
1425         $update->gradingduedate = $formdata->gradingduedate;
1426         $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate;
1427         $update->grade = $formdata->grade;
1428         if (!empty($formdata->completionunlocked)) {
1429             $update->completionsubmit = !empty($formdata->completionsubmit);
1430         }
1431         $update->teamsubmission = $formdata->teamsubmission;
1432         $update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit;
1433         if (isset($formdata->teamsubmissiongroupingid)) {
1434             $update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid;
1435         }
1436         if (isset($formdata->hidegrader)) {
1437             $update->hidegrader = $formdata->hidegrader;
1438         }
1439         $update->blindmarking = $formdata->blindmarking;
1440         $update->attemptreopenmethod = ASSIGN_ATTEMPT_REOPEN_METHOD_NONE;
1441         if (!empty($formdata->attemptreopenmethod)) {
1442             $update->attemptreopenmethod = $formdata->attemptreopenmethod;
1443         }
1444         if (!empty($formdata->maxattempts)) {
1445             $update->maxattempts = $formdata->maxattempts;
1446         }
1447         if (isset($formdata->preventsubmissionnotingroup)) {
1448             $update->preventsubmissionnotingroup = $formdata->preventsubmissionnotingroup;
1449         }
1450         $update->markingworkflow = $formdata->markingworkflow;
1451         $update->markingallocation = $formdata->markingallocation;
1452         if (empty($update->markingworkflow)) { // If marking workflow is disabled, make sure allocation is disabled.
1453             $update->markingallocation = 0;
1454         }
1456         $result = $DB->update_record('assign', $update);
1457         $this->instance = $DB->get_record('assign', array('id'=>$update->id), '*', MUST_EXIST);
1459         $this->save_intro_draft_files($formdata);
1461         // Load the assignment so the plugins have access to it.
1463         // Call save_settings hook for submission plugins.
1464         foreach ($this->submissionplugins as $plugin) {
1465             if (!$this->update_plugin_instance($plugin, $formdata)) {
1466                 print_error($plugin->get_error());
1467                 return false;
1468             }
1469         }
1470         foreach ($this->feedbackplugins as $plugin) {
1471             if (!$this->update_plugin_instance($plugin, $formdata)) {
1472                 print_error($plugin->get_error());
1473                 return false;
1474             }
1475         }
1477         $this->update_calendar($this->get_course_module()->id);
1478         $completionexpected = (!empty($formdata->completionexpected)) ? $formdata->completionexpected : null;
1479         \core_completion\api::update_completion_date_event($this->get_course_module()->id, 'assign', $this->instance,
1480                 $completionexpected);
1481         $this->update_gradebook(false, $this->get_course_module()->id);
1483         $update = new stdClass();
1484         $update->id = $this->get_instance()->id;
1485         $update->nosubmissions = (!$this->is_any_submission_plugin_enabled()) ? 1: 0;
1486         $DB->update_record('assign', $update);
1488         return $result;
1489     }
1491     /**
1492      * Save the attachments in the draft areas.
1493      *
1494      * @param stdClass $formdata
1495      */
1496     protected function save_intro_draft_files($formdata) {
1497         if (isset($formdata->introattachments)) {
1498             file_save_draft_area_files($formdata->introattachments, $this->get_context()->id,
1499                                        'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0);
1500         }
1501     }
1503     /**
1504      * Add elements in grading plugin form.
1505      *
1506      * @param mixed $grade stdClass|null
1507      * @param MoodleQuickForm $mform
1508      * @param stdClass $data
1509      * @param int $userid - The userid we are grading
1510      * @return void
1511      */
1512     protected function add_plugin_grade_elements($grade, MoodleQuickForm $mform, stdClass $data, $userid) {
1513         foreach ($this->feedbackplugins as $plugin) {
1514             if ($plugin->is_enabled() && $plugin->is_visible()) {
1515                 $plugin->get_form_elements_for_user($grade, $mform, $data, $userid);
1516             }
1517         }
1518     }
1522     /**
1523      * Add one plugins settings to edit plugin form.
1524      *
1525      * @param assign_plugin $plugin The plugin to add the settings from
1526      * @param MoodleQuickForm $mform The form to add the configuration settings to.
1527      *                               This form is modified directly (not returned).
1528      * @param array $pluginsenabled A list of form elements to be added to a group.
1529      *                              The new element is added to this array by this function.
1530      * @return void
1531      */
1532     protected function add_plugin_settings(assign_plugin $plugin, MoodleQuickForm $mform, & $pluginsenabled) {
1533         global $CFG;
1534         if ($plugin->is_visible() && !$plugin->is_configurable() && $plugin->is_enabled()) {
1535             $name = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
1536             $pluginsenabled[] = $mform->createElement('hidden', $name, 1);
1537             $mform->setType($name, PARAM_BOOL);
1538             $plugin->get_settings($mform);
1539         } else if ($plugin->is_visible() && $plugin->is_configurable()) {
1540             $name = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
1541             $label = $plugin->get_name();
1542             $pluginsenabled[] = $mform->createElement('checkbox', $name, '', $label);
1543             $helpicon = $this->get_renderer()->help_icon('enabled', $plugin->get_subtype() . '_' . $plugin->get_type());
1544             $pluginsenabled[] = $mform->createElement('static', '', '', $helpicon);
1546             $default = get_config($plugin->get_subtype() . '_' . $plugin->get_type(), 'default');
1547             if ($plugin->get_config('enabled') !== false) {
1548                 $default = $plugin->is_enabled();
1549             }
1550             $mform->setDefault($plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled', $default);
1552             $plugin->get_settings($mform);
1554         }
1555     }
1557     /**
1558      * Add settings to edit plugin form.
1559      *
1560      * @param MoodleQuickForm $mform The form to add the configuration settings to.
1561      *                               This form is modified directly (not returned).
1562      * @return void
1563      */
1564     public function add_all_plugin_settings(MoodleQuickForm $mform) {
1565         $mform->addElement('header', 'submissiontypes', get_string('submissiontypes', 'assign'));
1567         $submissionpluginsenabled = array();
1568         $group = $mform->addGroup(array(), 'submissionplugins', get_string('submissiontypes', 'assign'), array(' '), false);
1569         foreach ($this->submissionplugins as $plugin) {
1570             $this->add_plugin_settings($plugin, $mform, $submissionpluginsenabled);
1571         }
1572         $group->setElements($submissionpluginsenabled);
1574         $mform->addElement('header', 'feedbacktypes', get_string('feedbacktypes', 'assign'));
1575         $feedbackpluginsenabled = array();
1576         $group = $mform->addGroup(array(), 'feedbackplugins', get_string('feedbacktypes', 'assign'), array(' '), false);
1577         foreach ($this->feedbackplugins as $plugin) {
1578             $this->add_plugin_settings($plugin, $mform, $feedbackpluginsenabled);
1579         }
1580         $group->setElements($feedbackpluginsenabled);
1581         $mform->setExpanded('submissiontypes');
1582     }
1584     /**
1585      * Allow each plugin an opportunity to update the defaultvalues
1586      * passed in to the settings form (needed to set up draft areas for
1587      * editor and filemanager elements)
1588      *
1589      * @param array $defaultvalues
1590      */
1591     public function plugin_data_preprocessing(&$defaultvalues) {
1592         foreach ($this->submissionplugins as $plugin) {
1593             if ($plugin->is_visible()) {
1594                 $plugin->data_preprocessing($defaultvalues);
1595             }
1596         }
1597         foreach ($this->feedbackplugins as $plugin) {
1598             if ($plugin->is_visible()) {
1599                 $plugin->data_preprocessing($defaultvalues);
1600             }
1601         }
1602     }
1604     /**
1605      * Get the name of the current module.
1606      *
1607      * @return string the module name (Assignment)
1608      */
1609     protected function get_module_name() {
1610         if (isset(self::$modulename)) {
1611             return self::$modulename;
1612         }
1613         self::$modulename = get_string('modulename', 'assign');
1614         return self::$modulename;
1615     }
1617     /**
1618      * Get the plural name of the current module.
1619      *
1620      * @return string the module name plural (Assignments)
1621      */
1622     protected function get_module_name_plural() {
1623         if (isset(self::$modulenameplural)) {
1624             return self::$modulenameplural;
1625         }
1626         self::$modulenameplural = get_string('modulenameplural', 'assign');
1627         return self::$modulenameplural;
1628     }
1630     /**
1631      * Has this assignment been constructed from an instance?
1632      *
1633      * @return bool
1634      */
1635     public function has_instance() {
1636         return $this->instance || $this->get_course_module();
1637     }
1639     /**
1640      * Get the settings for the current instance of this assignment
1641      *
1642      * @return stdClass The settings
1643      */
1644     public function get_instance() {
1645         global $DB;
1646         if ($this->instance) {
1647             return $this->instance;
1648         }
1649         if ($this->get_course_module()) {
1650             $params = array('id' => $this->get_course_module()->instance);
1651             $this->instance = $DB->get_record('assign', $params, '*', MUST_EXIST);
1652         }
1653         if (!$this->instance) {
1654             throw new coding_exception('Improper use of the assignment class. ' .
1655                                        'Cannot load the assignment record.');
1656         }
1657         return $this->instance;
1658     }
1660     /**
1661      * Get the primary grade item for this assign instance.
1662      *
1663      * @return grade_item The grade_item record
1664      */
1665     public function get_grade_item() {
1666         if ($this->gradeitem) {
1667             return $this->gradeitem;
1668         }
1669         $instance = $this->get_instance();
1670         $params = array('itemtype' => 'mod',
1671                         'itemmodule' => 'assign',
1672                         'iteminstance' => $instance->id,
1673                         'courseid' => $instance->course,
1674                         'itemnumber' => 0);
1675         $this->gradeitem = grade_item::fetch($params);
1676         if (!$this->gradeitem) {
1677             throw new coding_exception('Improper use of the assignment class. ' .
1678                                        'Cannot load the grade item.');
1679         }
1680         return $this->gradeitem;
1681     }
1683     /**
1684      * Get the context of the current course.
1685      *
1686      * @return mixed context|null The course context
1687      */
1688     public function get_course_context() {
1689         if (!$this->context && !$this->course) {
1690             throw new coding_exception('Improper use of the assignment class. ' .
1691                                        'Cannot load the course context.');
1692         }
1693         if ($this->context) {
1694             return $this->context->get_course_context();
1695         } else {
1696             return context_course::instance($this->course->id);
1697         }
1698     }
1701     /**
1702      * Get the current course module.
1703      *
1704      * @return cm_info|null The course module or null if not known
1705      */
1706     public function get_course_module() {
1707         if ($this->coursemodule) {
1708             return $this->coursemodule;
1709         }
1710         if (!$this->context) {
1711             return null;
1712         }
1714         if ($this->context->contextlevel == CONTEXT_MODULE) {
1715             $modinfo = get_fast_modinfo($this->get_course());
1716             $this->coursemodule = $modinfo->get_cm($this->context->instanceid);
1717             return $this->coursemodule;
1718         }
1719         return null;
1720     }
1722     /**
1723      * Get context module.
1724      *
1725      * @return context
1726      */
1727     public function get_context() {
1728         return $this->context;
1729     }
1731     /**
1732      * Get the current course.
1733      *
1734      * @return mixed stdClass|null The course
1735      */
1736     public function get_course() {
1737         global $DB;
1739         if ($this->course) {
1740             return $this->course;
1741         }
1743         if (!$this->context) {
1744             return null;
1745         }
1746         $params = array('id' => $this->get_course_context()->instanceid);
1747         $this->course = $DB->get_record('course', $params, '*', MUST_EXIST);
1749         return $this->course;
1750     }
1752     /**
1753      * Count the number of intro attachments.
1754      *
1755      * @return int
1756      */
1757     protected function count_attachments() {
1759         $fs = get_file_storage();
1760         $files = $fs->get_area_files($this->get_context()->id, 'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA,
1761                         0, 'id', false);
1763         return count($files);
1764     }
1766     /**
1767      * Are there any intro attachments to display?
1768      *
1769      * @return boolean
1770      */
1771     protected function has_visible_attachments() {
1772         return ($this->count_attachments() > 0);
1773     }
1775     /**
1776      * Return a grade in user-friendly form, whether it's a scale or not.
1777      *
1778      * @param mixed $grade int|null
1779      * @param boolean $editing Are we allowing changes to this grade?
1780      * @param int $userid The user id the grade belongs to
1781      * @param int $modified Timestamp from when the grade was last modified
1782      * @return string User-friendly representation of grade
1783      */
1784     public function display_grade($grade, $editing, $userid=0, $modified=0) {
1785         global $DB;
1787         static $scalegrades = array();
1789         $o = '';
1791         if ($this->get_instance()->grade >= 0) {
1792             // Normal number.
1793             if ($editing && $this->get_instance()->grade > 0) {
1794                 if ($grade < 0) {
1795                     $displaygrade = '';
1796                 } else {
1797                     $displaygrade = format_float($grade, $this->get_grade_item()->get_decimals());
1798                 }
1799                 $o .= '<label class="accesshide" for="quickgrade_' . $userid . '">' .
1800                        get_string('usergrade', 'assign') .
1801                        '</label>';
1802                 $o .= '<input type="text"
1803                               id="quickgrade_' . $userid . '"
1804                               name="quickgrade_' . $userid . '"
1805                               value="' .  $displaygrade . '"
1806                               size="6"
1807                               maxlength="10"
1808                               class="quickgrade"/>';
1809                 $o .= '&nbsp;/&nbsp;' . format_float($this->get_instance()->grade, $this->get_grade_item()->get_decimals());
1810                 return $o;
1811             } else {
1812                 if ($grade == -1 || $grade === null) {
1813                     $o .= '-';
1814                 } else {
1815                     $item = $this->get_grade_item();
1816                     $o .= grade_format_gradevalue($grade, $item);
1817                     if ($item->get_displaytype() == GRADE_DISPLAY_TYPE_REAL) {
1818                         // If displaying the raw grade, also display the total value.
1819                         $o .= '&nbsp;/&nbsp;' . format_float($this->get_instance()->grade, $item->get_decimals());
1820                     }
1821                 }
1822                 return $o;
1823             }
1825         } else {
1826             // Scale.
1827             if (empty($this->cache['scale'])) {
1828                 if ($scale = $DB->get_record('scale', array('id'=>-($this->get_instance()->grade)))) {
1829                     $this->cache['scale'] = make_menu_from_list($scale->scale);
1830                 } else {
1831                     $o .= '-';
1832                     return $o;
1833                 }
1834             }
1835             if ($editing) {
1836                 $o .= '<label class="accesshide"
1837                               for="quickgrade_' . $userid . '">' .
1838                       get_string('usergrade', 'assign') .
1839                       '</label>';
1840                 $o .= '<select name="quickgrade_' . $userid . '" class="quickgrade">';
1841                 $o .= '<option value="-1">' . get_string('nograde') . '</option>';
1842                 foreach ($this->cache['scale'] as $optionid => $option) {
1843                     $selected = '';
1844                     if ($grade == $optionid) {
1845                         $selected = 'selected="selected"';
1846                     }
1847                     $o .= '<option value="' . $optionid . '" ' . $selected . '>' . $option . '</option>';
1848                 }
1849                 $o .= '</select>';
1850                 return $o;
1851             } else {
1852                 $scaleid = (int)$grade;
1853                 if (isset($this->cache['scale'][$scaleid])) {
1854                     $o .= $this->cache['scale'][$scaleid];
1855                     return $o;
1856                 }
1857                 $o .= '-';
1858                 return $o;
1859             }
1860         }
1861     }
1863     /**
1864      * Get the submission status/grading status for all submissions in this assignment for the
1865      * given paticipants.
1866      *
1867      * These statuses match the available filters (requiregrading, submitted, notsubmitted, grantedextension).
1868      * If this is a group assignment, group info is also returned.
1869      *
1870      * @param array $participants an associative array where the key is the participant id and
1871      *                            the value is the participant record.
1872      * @return array an associative array where the key is the participant id and the value is
1873      *               the participant record.
1874      */
1875     private function get_submission_info_for_participants($participants) {
1876         global $DB;
1878         if (empty($participants)) {
1879             return $participants;
1880         }
1882         list($insql, $params) = $DB->get_in_or_equal(array_keys($participants), SQL_PARAMS_NAMED);
1884         $assignid = $this->get_instance()->id;
1885         $params['assignmentid1'] = $assignid;
1886         $params['assignmentid2'] = $assignid;
1887         $params['assignmentid3'] = $assignid;
1889         $fields = 'SELECT u.id, s.status, s.timemodified AS stime, g.timemodified AS gtime, g.grade, uf.extensionduedate';
1890         $from = ' FROM {user} u
1891                          LEFT JOIN {assign_submission} s
1892                                 ON u.id = s.userid
1893                                AND s.assignment = :assignmentid1
1894                                AND s.latest = 1
1895                          LEFT JOIN {assign_grades} g
1896                                 ON u.id = g.userid
1897                                AND g.assignment = :assignmentid2
1898                                AND g.attemptnumber = s.attemptnumber
1899                          LEFT JOIN {assign_user_flags} uf
1900                                 ON u.id = uf.userid
1901                                AND uf.assignment = :assignmentid3
1902             ';
1903         $where = ' WHERE u.id ' . $insql;
1905         if (!empty($this->get_instance()->blindmarking)) {
1906             $from .= 'LEFT JOIN {assign_user_mapping} um
1907                              ON u.id = um.userid
1908                             AND um.assignment = :assignmentid4 ';
1909             $params['assignmentid4'] = $assignid;
1910             $fields .= ', um.id as recordid ';
1911         }
1913         $sql = "$fields $from $where";
1915         $records = $DB->get_records_sql($sql, $params);
1917         if ($this->get_instance()->teamsubmission) {
1918             // Get all groups.
1919             $allgroups = groups_get_all_groups($this->get_course()->id,
1920                                                array_keys($participants),
1921                                                $this->get_instance()->teamsubmissiongroupingid,
1922                                                'DISTINCT g.id, g.name');
1924         }
1925         foreach ($participants as $userid => $participant) {
1926             $participants[$userid]->fullname = $this->fullname($participant);
1927             $participants[$userid]->submitted = false;
1928             $participants[$userid]->requiregrading = false;
1929             $participants[$userid]->grantedextension = false;
1930         }
1932         foreach ($records as $userid => $submissioninfo) {
1933             // These filters are 100% the same as the ones in the grading table SQL.
1934             $submitted = false;
1935             $requiregrading = false;
1936             $grantedextension = false;
1938             if (!empty($submissioninfo->stime) && $submissioninfo->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
1939                 $submitted = true;
1940             }
1942             if ($submitted && ($submissioninfo->stime >= $submissioninfo->gtime ||
1943                     empty($submissioninfo->gtime) ||
1944                     $submissioninfo->grade === null)) {
1945                 $requiregrading = true;
1946             }
1948             if (!empty($submissioninfo->extensionduedate)) {
1949                 $grantedextension = true;
1950             }
1952             $participants[$userid]->submitted = $submitted;
1953             $participants[$userid]->requiregrading = $requiregrading;
1954             $participants[$userid]->grantedextension = $grantedextension;
1955             if ($this->get_instance()->teamsubmission) {
1956                 $group = $this->get_submission_group($userid);
1957                 if ($group) {
1958                     $participants[$userid]->groupid = $group->id;
1959                     $participants[$userid]->groupname = $group->name;
1960                 }
1961             }
1962         }
1963         return $participants;
1964     }
1966     /**
1967      * Get the submission status/grading status for all submissions in this assignment.
1968      * These statuses match the available filters (requiregrading, submitted, notsubmitted, grantedextension).
1969      * If this is a group assignment, group info is also returned.
1970      *
1971      * @param int $currentgroup
1972      * @param boolean $tablesort Apply current user table sorting preferences.
1973      * @return array List of user records with extra fields 'submitted', 'notsubmitted', 'requiregrading', 'grantedextension',
1974      *               'groupid', 'groupname'
1975      */
1976     public function list_participants_with_filter_status_and_group($currentgroup, $tablesort = false) {
1977         $participants = $this->list_participants($currentgroup, false, $tablesort);
1979         if (empty($participants)) {
1980             return $participants;
1981         } else {
1982             return $this->get_submission_info_for_participants($participants);
1983         }
1984     }
1986     /**
1987      * Return a valid order by segment for list_participants that matches
1988      * the sorting of the current grading table. Not every field is supported,
1989      * we are only concerned with a list of users so we can't search on anything
1990      * that is not part of the user information (like grading statud or last modified stuff).
1991      *
1992      * @return string Order by clause for list_participants
1993      */
1994     private function get_grading_sort_sql() {
1995         $usersort = flexible_table::get_sort_for_table('mod_assign_grading');
1996         $extrauserfields = get_extra_user_fields($this->get_context());
1998         $userfields = explode(',', user_picture::fields('', $extrauserfields));
1999         $orderfields = explode(',', $usersort);
2000         $validlist = [];
2002         foreach ($orderfields as $orderfield) {
2003             $orderfield = trim($orderfield);
2004             foreach ($userfields as $field) {
2005                 $parts = explode(' ', $orderfield);
2006                 if ($parts[0] == $field) {
2007                     // Prepend the user table prefix and count this as a valid order field.
2008                     array_push($validlist, 'u.' . $orderfield);
2009                 }
2010             }
2011         }
2012         // Produce a final list.
2013         $result = implode(',', $validlist);
2014         if (empty($result)) {
2015             // Fall back ordering when none has been set.
2016             $result = 'u.lastname, u.firstname, u.id';
2017         }
2019         return $result;
2020     }
2022     /**
2023      * Load a list of users enrolled in the current course with the specified permission and group.
2024      * 0 for no group.
2025      * Apply any current sort filters from the grading table.
2026      *
2027      * @param int $currentgroup
2028      * @param bool $idsonly
2029      * @return array List of user records
2030      */
2031     public function list_participants($currentgroup, $idsonly, $tablesort = false) {
2032         global $DB, $USER;
2034         // Get the last known sort order for the grading table.
2036         if (empty($currentgroup)) {
2037             $currentgroup = 0;
2038         }
2040         $key = $this->context->id . '-' . $currentgroup . '-' . $this->show_only_active_users();
2041         if (!isset($this->participants[$key])) {
2042             list($esql, $params) = get_enrolled_sql($this->context, 'mod/assign:submit', $currentgroup,
2043                     $this->show_only_active_users());
2045             $fields = 'u.*';
2046             $orderby = 'u.lastname, u.firstname, u.id';
2048             $additionaljoins = '';
2049             $additionalfilters = '';
2050             $instance = $this->get_instance();
2051             if (!empty($instance->blindmarking)) {
2052                 $additionaljoins .= " LEFT JOIN {assign_user_mapping} um
2053                                   ON u.id = um.userid
2054                                  AND um.assignment = :assignmentid1
2055                            LEFT JOIN {assign_submission} s
2056                                   ON u.id = s.userid
2057                                  AND s.assignment = :assignmentid2
2058                                  AND s.latest = 1
2059                         ";
2060                 $params['assignmentid1'] = (int) $instance->id;
2061                 $params['assignmentid2'] = (int) $instance->id;
2062                 $fields .= ', um.id as recordid ';
2064                 // Sort by submission time first, then by um.id to sort reliably by the blind marking id.
2065                 // Note, different DBs have different ordering of NULL values.
2066                 // Therefore we coalesce the current time into the timecreated field, and the max possible integer into
2067                 // the ID field.
2068                 if (empty($tablesort)) {
2069                     $orderby = "COALESCE(s.timecreated, " . time() . ") ASC, COALESCE(s.id, " . PHP_INT_MAX . ") ASC, um.id ASC";
2070                 }
2071             }
2073             if ($instance->markingworkflow &&
2074                     $instance->markingallocation &&
2075                     !has_capability('mod/assign:manageallocations', $this->get_context()) &&
2076                     has_capability('mod/assign:grade', $this->get_context())) {
2078                 $additionaljoins .= ' LEFT JOIN {assign_user_flags} uf
2079                                      ON u.id = uf.userid
2080                                      AND uf.assignment = :assignmentid3';
2082                 $params['assignmentid3'] = (int) $instance->id;
2084                 $additionalfilters .= ' AND uf.allocatedmarker = :markerid';
2085                 $params['markerid'] = $USER->id;
2086             }
2088             $sql = "SELECT $fields
2089                       FROM {user} u
2090                       JOIN ($esql) je ON je.id = u.id
2091                            $additionaljoins
2092                      WHERE u.deleted = 0
2093                            $additionalfilters
2094                   ORDER BY $orderby";
2096             $users = $DB->get_records_sql($sql, $params);
2098             $cm = $this->get_course_module();
2099             $info = new \core_availability\info_module($cm);
2100             $users = $info->filter_user_list($users);
2102             $this->participants[$key] = $users;
2103         }
2105         if ($tablesort) {
2106             // Resort the user list according to the grading table sort and filter settings.
2107             $sortedfiltereduserids = $this->get_grading_userid_list(true, '');
2108             $sortedfilteredusers = [];
2109             foreach ($sortedfiltereduserids as $nextid) {
2110                 $nextid = intval($nextid);
2111                 if (isset($this->participants[$key][$nextid])) {
2112                     $sortedfilteredusers[$nextid] = $this->participants[$key][$nextid];
2113                 }
2114             }
2115             $this->participants[$key] = $sortedfilteredusers;
2116         }
2118         if ($idsonly) {
2119             $idslist = array();
2120             foreach ($this->participants[$key] as $id => $user) {
2121                 $idslist[$id] = new stdClass();
2122                 $idslist[$id]->id = $id;
2123             }
2124             return $idslist;
2125         }
2126         return $this->participants[$key];
2127     }
2129     /**
2130      * Load a user if they are enrolled in the current course. Populated with submission
2131      * status for this assignment.
2132      *
2133      * @param int $userid
2134      * @return null|stdClass user record
2135      */
2136     public function get_participant($userid) {
2137         global $DB, $USER;
2139         if ($userid == $USER->id) {
2140             $participant = clone ($USER);
2141         } else {
2142             $participant = $DB->get_record('user', array('id' => $userid));
2143         }
2144         if (!$participant) {
2145             return null;
2146         }
2148         if (!is_enrolled($this->context, $participant, 'mod/assign:submit', $this->show_only_active_users())) {
2149             return null;
2150         }
2152         $result = $this->get_submission_info_for_participants(array($participant->id => $participant));
2153         return $result[$participant->id];
2154     }
2156     /**
2157      * Load a count of valid teams for this assignment.
2158      *
2159      * @param int $activitygroup Activity active group
2160      * @return int number of valid teams
2161      */
2162     public function count_teams($activitygroup = 0) {
2164         $count = 0;
2166         $participants = $this->list_participants($activitygroup, true);
2168         // If a team submission grouping id is provided all good as all returned groups
2169         // are the submission teams, but if no team submission grouping was specified
2170         // $groups will contain all participants groups.
2171         if ($this->get_instance()->teamsubmissiongroupingid) {
2173             // We restrict the users to the selected group ones.
2174             $groups = groups_get_all_groups($this->get_course()->id,
2175                                             array_keys($participants),
2176                                             $this->get_instance()->teamsubmissiongroupingid,
2177                                             'DISTINCT g.id, g.name');
2179             $count = count($groups);
2181             // When a specific group is selected we don't count the default group users.
2182             if ($activitygroup == 0) {
2183                 if (empty($this->get_instance()->preventsubmissionnotingroup)) {
2184                     // See if there are any users in the default group.
2185                     $defaultusers = $this->get_submission_group_members(0, true);
2186                     if (count($defaultusers) > 0) {
2187                         $count += 1;
2188                     }
2189                 }
2190             } else if ($activitygroup != 0 && empty($groups)) {
2191                 // Set count to 1 if $groups returns empty.
2192                 // It means the group is not part of $this->get_instance()->teamsubmissiongroupingid.
2193                 $count = 1;
2194             }
2195         } else {
2196             // It is faster to loop around participants if no grouping was specified.
2197             $groups = array();
2198             foreach ($participants as $participant) {
2199                 if ($group = $this->get_submission_group($participant->id)) {
2200                     $groups[$group->id] = true;
2201                 } else if (empty($this->get_instance()->preventsubmissionnotingroup)) {
2202                     $groups[0] = true;
2203                 }
2204             }
2206             $count = count($groups);
2207         }
2209         return $count;
2210     }
2212     /**
2213      * Load a count of active users enrolled in the current course with the specified permission and group.
2214      * 0 for no group.
2215      *
2216      * @param int $currentgroup
2217      * @return int number of matching users
2218      */
2219     public function count_participants($currentgroup) {
2220         return count($this->list_participants($currentgroup, true));
2221     }
2223     /**
2224      * Load a count of active users submissions in the current module that require grading
2225      * This means the submission modification time is more recent than the
2226      * grading modification time and the status is SUBMITTED.
2227      *
2228      * @param mixed $currentgroup int|null the group for counting (if null the function will determine it)
2229      * @return int number of matching submissions
2230      */
2231     public function count_submissions_need_grading($currentgroup = null) {
2232         global $DB;
2234         if ($this->get_instance()->teamsubmission) {
2235             // This does not make sense for group assignment because the submission is shared.
2236             return 0;
2237         }
2239         if ($currentgroup === null) {
2240             $currentgroup = groups_get_activity_group($this->get_course_module(), true);
2241         }
2242         list($esql, $params) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
2244         $params['assignid'] = $this->get_instance()->id;
2245         $params['submitted'] = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
2246         $sqlscalegrade = $this->get_instance()->grade < 0 ? ' OR g.grade = -1' : '';
2248         $sql = 'SELECT COUNT(s.userid)
2249                    FROM {assign_submission} s
2250                    LEFT JOIN {assign_grades} g ON
2251                         s.assignment = g.assignment AND
2252                         s.userid = g.userid AND
2253                         g.attemptnumber = s.attemptnumber
2254                    JOIN(' . $esql . ') e ON e.id = s.userid
2255                    WHERE
2256                         s.latest = 1 AND
2257                         s.assignment = :assignid AND
2258                         s.timemodified IS NOT NULL AND
2259                         s.status = :submitted AND
2260                         (s.timemodified >= g.timemodified OR g.timemodified IS NULL OR g.grade IS NULL '
2261                             . $sqlscalegrade . ')';
2263         return $DB->count_records_sql($sql, $params);
2264     }
2266     /**
2267      * Load a count of grades.
2268      *
2269      * @return int number of grades
2270      */
2271     public function count_grades() {
2272         global $DB;
2274         if (!$this->has_instance()) {
2275             return 0;
2276         }
2278         $currentgroup = groups_get_activity_group($this->get_course_module(), true);
2279         list($esql, $params) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
2281         $params['assignid'] = $this->get_instance()->id;
2283         $sql = 'SELECT COUNT(g.userid)
2284                    FROM {assign_grades} g
2285                    JOIN(' . $esql . ') e ON e.id = g.userid
2286                    WHERE g.assignment = :assignid';
2288         return $DB->count_records_sql($sql, $params);
2289     }
2291     /**
2292      * Load a count of submissions.
2293      *
2294      * @param bool $includenew When true, also counts the submissions with status 'new'.
2295      * @return int number of submissions
2296      */
2297     public function count_submissions($includenew = false) {
2298         global $DB;
2300         if (!$this->has_instance()) {
2301             return 0;
2302         }
2304         $params = array();
2305         $sqlnew = '';
2307         if (!$includenew) {
2308             $sqlnew = ' AND s.status <> :status ';
2309             $params['status'] = ASSIGN_SUBMISSION_STATUS_NEW;
2310         }
2312         if ($this->get_instance()->teamsubmission) {
2313             // We cannot join on the enrolment tables for group submissions (no userid).
2314             $sql = 'SELECT COUNT(DISTINCT s.groupid)
2315                         FROM {assign_submission} s
2316                         WHERE
2317                             s.assignment = :assignid AND
2318                             s.timemodified IS NOT NULL AND
2319                             s.userid = :groupuserid' .
2320                             $sqlnew;
2322             $params['assignid'] = $this->get_instance()->id;
2323             $params['groupuserid'] = 0;
2324         } else {
2325             $currentgroup = groups_get_activity_group($this->get_course_module(), true);
2326             list($esql, $enrolparams) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
2328             $params = array_merge($params, $enrolparams);
2329             $params['assignid'] = $this->get_instance()->id;
2331             $sql = 'SELECT COUNT(DISTINCT s.userid)
2332                        FROM {assign_submission} s
2333                        JOIN(' . $esql . ') e ON e.id = s.userid
2334                        WHERE
2335                             s.assignment = :assignid AND
2336                             s.timemodified IS NOT NULL ' .
2337                             $sqlnew;
2339         }
2341         return $DB->count_records_sql($sql, $params);
2342     }
2344     /**
2345      * Load a count of submissions with a specified status.
2346      *
2347      * @param string $status The submission status - should match one of the constants
2348      * @param mixed $currentgroup int|null the group for counting (if null the function will determine it)
2349      * @return int number of matching submissions
2350      */
2351     public function count_submissions_with_status($status, $currentgroup = null) {
2352         global $DB;
2354         if ($currentgroup === null) {
2355             $currentgroup = groups_get_activity_group($this->get_course_module(), true);
2356         }
2357         list($esql, $params) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
2359         $params['assignid'] = $this->get_instance()->id;
2360         $params['assignid2'] = $this->get_instance()->id;
2361         $params['submissionstatus'] = $status;
2363         if ($this->get_instance()->teamsubmission) {
2365             $groupsstr = '';
2366             if ($currentgroup != 0) {
2367                 // If there is an active group we should only display the current group users groups.
2368                 $participants = $this->list_participants($currentgroup, true);
2369                 $groups = groups_get_all_groups($this->get_course()->id,
2370                                                 array_keys($participants),
2371                                                 $this->get_instance()->teamsubmissiongroupingid,
2372                                                 'DISTINCT g.id, g.name');
2373                 if (empty($groups)) {
2374                     // If $groups is empty it means it is not part of $this->get_instance()->teamsubmissiongroupingid.
2375                     // All submissions from students that do not belong to any of teamsubmissiongroupingid groups
2376                     // count towards groupid = 0. Setting to true as only '0' key matters.
2377                     $groups = [true];
2378                 }
2379                 list($groupssql, $groupsparams) = $DB->get_in_or_equal(array_keys($groups), SQL_PARAMS_NAMED);
2380                 $groupsstr = 's.groupid ' . $groupssql . ' AND';
2381                 $params = $params + $groupsparams;
2382             }
2383             $sql = 'SELECT COUNT(s.groupid)
2384                         FROM {assign_submission} s
2385                         WHERE
2386                             s.latest = 1 AND
2387                             s.assignment = :assignid AND
2388                             s.timemodified IS NOT NULL AND
2389                             s.userid = :groupuserid AND '
2390                             . $groupsstr . '
2391                             s.status = :submissionstatus';
2392             $params['groupuserid'] = 0;
2393         } else {
2394             $sql = 'SELECT COUNT(s.userid)
2395                         FROM {assign_submission} s
2396                         JOIN(' . $esql . ') e ON e.id = s.userid
2397                         WHERE
2398                             s.latest = 1 AND
2399                             s.assignment = :assignid AND
2400                             s.timemodified IS NOT NULL AND
2401                             s.status = :submissionstatus';
2403         }
2405         return $DB->count_records_sql($sql, $params);
2406     }
2408     /**
2409      * Utility function to get the userid for every row in the grading table
2410      * so the order can be frozen while we iterate it.
2411      *
2412      * @param boolean $cached If true, the cached list from the session could be returned.
2413      * @param string $useridlistid String value used for caching the participant list.
2414      * @return array An array of userids
2415      */
2416     protected function get_grading_userid_list($cached = false, $useridlistid = '') {
2417         if ($cached) {
2418             if (empty($useridlistid)) {
2419                 $useridlistid = $this->get_useridlist_key_id();
2420             }
2421             $useridlistkey = $this->get_useridlist_key($useridlistid);
2422             if (empty($SESSION->mod_assign_useridlist[$useridlistkey])) {
2423                 $SESSION->mod_assign_useridlist[$useridlistkey] = $this->get_grading_userid_list(false, '');
2424             }
2425             return $SESSION->mod_assign_useridlist[$useridlistkey];
2426         }
2427         $filter = get_user_preferences('assign_filter', '');
2428         $table = new assign_grading_table($this, 0, $filter, 0, false);
2430         $useridlist = $table->get_column_data('userid');
2432         return $useridlist;
2433     }
2435     /**
2436      * Generate zip file from array of given files.
2437      *
2438      * @param array $filesforzipping - array of files to pass into archive_to_pathname.
2439      *                                 This array is indexed by the final file name and each
2440      *                                 element in the array is an instance of a stored_file object.
2441      * @return path of temp file - note this returned file does
2442      *         not have a .zip extension - it is a temp file.
2443      */
2444     protected function pack_files($filesforzipping) {
2445         global $CFG;
2446         // Create path for new zip file.
2447         $tempzip = tempnam($CFG->tempdir . '/', 'assignment_');
2448         // Zip files.
2449         $zipper = new zip_packer();
2450         if ($zipper->archive_to_pathname($filesforzipping, $tempzip)) {
2451             return $tempzip;
2452         }
2453         return false;
2454     }
2456     /**
2457      * Finds all assignment notifications that have yet to be mailed out, and mails them.
2458      *
2459      * Cron function to be run periodically according to the moodle cron.
2460      *
2461      * @return bool
2462      */
2463     public static function cron() {
2464         global $DB;
2466         // Only ever send a max of one days worth of updates.
2467         $yesterday = time() - (24 * 3600);
2468         $timenow   = time();
2469         $lastcron = $DB->get_field('modules', 'lastcron', array('name' => 'assign'));
2471         // Collect all submissions that require mailing.
2472         // Submissions are included if all are true:
2473         //   - The assignment is visible in the gradebook.
2474         //   - No previous notification has been sent.
2475         //   - The grader was a real user, not an automated process.
2476         //   - If marking workflow is not enabled, the grade was updated in the past 24 hours, or
2477         //     if marking workflow is enabled, the workflow state is at 'released'.
2478         $sql = "SELECT g.id as gradeid, a.course, a.name, a.blindmarking, a.revealidentities, a.hidegrader,
2479                        g.*, g.timemodified as lastmodified, cm.id as cmid, um.id as recordid
2480                  FROM {assign} a
2481                  JOIN {assign_grades} g ON g.assignment = a.id
2482             LEFT JOIN {assign_user_flags} uf ON uf.assignment = a.id AND uf.userid = g.userid
2483                  JOIN {course_modules} cm ON cm.course = a.course AND cm.instance = a.id
2484                  JOIN {modules} md ON md.id = cm.module AND md.name = 'assign'
2485                  JOIN {grade_items} gri ON gri.iteminstance = a.id AND gri.courseid = a.course AND gri.itemmodule = md.name
2486             LEFT JOIN {assign_user_mapping} um ON g.id = um.userid AND um.assignment = a.id
2487                  WHERE ((a.markingworkflow = 0 AND g.timemodified >= :yesterday AND g.timemodified <= :today) OR
2488                         (a.markingworkflow = 1 AND uf.workflowstate = :wfreleased)) AND
2489                        g.grader > 0 AND uf.mailed = 0 AND gri.hidden = 0
2490               ORDER BY a.course, cm.id";
2492         $params = array(
2493             'yesterday' => $yesterday,
2494             'today' => $timenow,
2495             'wfreleased' => ASSIGN_MARKING_WORKFLOW_STATE_RELEASED,
2496         );
2497         $submissions = $DB->get_records_sql($sql, $params);
2499         if (!empty($submissions)) {
2501             mtrace('Processing ' . count($submissions) . ' assignment submissions ...');
2503             // Preload courses we are going to need those.
2504             $courseids = array();
2505             foreach ($submissions as $submission) {
2506                 $courseids[] = $submission->course;
2507             }
2509             // Filter out duplicates.
2510             $courseids = array_unique($courseids);
2511             $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
2512             list($courseidsql, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
2513             $sql = 'SELECT c.*, ' . $ctxselect .
2514                       ' FROM {course} c
2515                  LEFT JOIN {context} ctx ON ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel
2516                      WHERE c.id ' . $courseidsql;
2518             $params['contextlevel'] = CONTEXT_COURSE;
2519             $courses = $DB->get_records_sql($sql, $params);
2521             // Clean up... this could go on for a while.
2522             unset($courseids);
2523             unset($ctxselect);
2524             unset($courseidsql);
2525             unset($params);
2527             // Message students about new feedback.
2528             foreach ($submissions as $submission) {
2530                 mtrace("Processing assignment submission $submission->id ...");
2532                 // Do not cache user lookups - could be too many.
2533                 if (!$user = $DB->get_record('user', array('id'=>$submission->userid))) {
2534                     mtrace('Could not find user ' . $submission->userid);
2535                     continue;
2536                 }
2538                 // Use a cache to prevent the same DB queries happening over and over.
2539                 if (!array_key_exists($submission->course, $courses)) {
2540                     mtrace('Could not find course ' . $submission->course);
2541                     continue;
2542                 }
2543                 $course = $courses[$submission->course];
2544                 if (isset($course->ctxid)) {
2545                     // Context has not yet been preloaded. Do so now.
2546                     context_helper::preload_from_record($course);
2547                 }
2549                 // Override the language and timezone of the "current" user, so that
2550                 // mail is customised for the receiver.
2551                 cron_setup_user($user, $course);
2553                 // Context lookups are already cached.
2554                 $coursecontext = context_course::instance($course->id);
2555                 if (!is_enrolled($coursecontext, $user->id)) {
2556                     $courseshortname = format_string($course->shortname,
2557                                                      true,
2558                                                      array('context' => $coursecontext));
2559                     mtrace(fullname($user) . ' not an active participant in ' . $courseshortname);
2560                     continue;
2561                 }
2563                 if (!$grader = $DB->get_record('user', array('id'=>$submission->grader))) {
2564                     mtrace('Could not find grader ' . $submission->grader);
2565                     continue;
2566                 }
2568                 $modinfo = get_fast_modinfo($course, $user->id);
2569                 $cm = $modinfo->get_cm($submission->cmid);
2570                 // Context lookups are already cached.
2571                 $contextmodule = context_module::instance($cm->id);
2573                 if (!$cm->uservisible) {
2574                     // Hold mail notification for assignments the user cannot access until later.
2575                     continue;
2576                 }
2578                 // Notify the student. Default to the non-anon version.
2579                 $messagetype = 'feedbackavailable';
2580                 // Message type needs 'anon' if "hidden grading" is enabled and the student
2581                 // doesn't have permission to see the grader.
2582                 if ($submission->hidegrader && !has_capability('mod/assign:showhiddengrader', $contextmodule, $user)) {
2583                     $messagetype = 'feedbackavailableanon';
2584                     // There's no point in having an "anonymous grader" if the notification email
2585                     // comes from them. Send the email from the noreply user instead.
2586                     $grader = core_user::get_noreply_user();
2587                 }
2589                 $eventtype = 'assign_notification';
2590                 $updatetime = $submission->lastmodified;
2591                 $modulename = get_string('modulename', 'assign');
2593                 $uniqueid = 0;
2594                 if ($submission->blindmarking && !$submission->revealidentities) {
2595                     if (empty($submission->recordid)) {
2596                         $uniqueid = self::get_uniqueid_for_user_static($submission->assignment, $grader->id);
2597                     } else {
2598                         $uniqueid = $submission->recordid;
2599                     }
2600                 }
2601                 $showusers = $submission->blindmarking && !$submission->revealidentities;
2602                 self::send_assignment_notification($grader,
2603                                                    $user,
2604                                                    $messagetype,
2605                                                    $eventtype,
2606                                                    $updatetime,
2607                                                    $cm,
2608                                                    $contextmodule,
2609                                                    $course,
2610                                                    $modulename,
2611                                                    $submission->name,
2612                                                    $showusers,
2613                                                    $uniqueid);
2615                 $flags = $DB->get_record('assign_user_flags', array('userid'=>$user->id, 'assignment'=>$submission->assignment));
2616                 if ($flags) {
2617                     $flags->mailed = 1;
2618                     $DB->update_record('assign_user_flags', $flags);
2619                 } else {
2620                     $flags = new stdClass();
2621                     $flags->userid = $user->id;
2622                     $flags->assignment = $submission->assignment;
2623                     $flags->mailed = 1;
2624                     $DB->insert_record('assign_user_flags', $flags);
2625                 }
2627                 mtrace('Done');
2628             }
2629             mtrace('Done processing ' . count($submissions) . ' assignment submissions');
2631             cron_setup_user();
2633             // Free up memory just to be sure.
2634             unset($courses);
2635         }
2637         // Update calendar events to provide a description.
2638         $sql = 'SELECT id
2639                     FROM {assign}
2640                     WHERE
2641                         allowsubmissionsfromdate >= :lastcron AND
2642                         allowsubmissionsfromdate <= :timenow AND
2643                         alwaysshowdescription = 0';
2644         $params = array('lastcron' => $lastcron, 'timenow' => $timenow);
2645         $newlyavailable = $DB->get_records_sql($sql, $params);
2646         foreach ($newlyavailable as $record) {
2647             $cm = get_coursemodule_from_instance('assign', $record->id, 0, false, MUST_EXIST);
2648             $context = context_module::instance($cm->id);
2650             $assignment = new assign($context, null, null);
2651             $assignment->update_calendar($cm->id);
2652         }
2654         return true;
2655     }
2657     /**
2658      * Mark in the database that this grade record should have an update notification sent by cron.
2659      *
2660      * @param stdClass $grade a grade record keyed on id
2661      * @param bool $mailedoverride when true, flag notification to be sent again.
2662      * @return bool true for success
2663      */
2664     public function notify_grade_modified($grade, $mailedoverride = false) {
2665         global $DB;
2667         $flags = $this->get_user_flags($grade->userid, true);
2668         if ($flags->mailed != 1 || $mailedoverride) {
2669             $flags->mailed = 0;
2670         }
2672         return $this->update_user_flags($flags);
2673     }
2675     /**
2676      * Update user flags for this user in this assignment.
2677      *
2678      * @param stdClass $flags a flags record keyed on id
2679      * @return bool true for success
2680      */
2681     public function update_user_flags($flags) {
2682         global $DB;
2683         if ($flags->userid <= 0 || $flags->assignment <= 0 || $flags->id <= 0) {
2684             return false;
2685         }
2687         $result = $DB->update_record('assign_user_flags', $flags);
2688         return $result;
2689     }
2691     /**
2692      * Update a grade in the grade table for the assignment and in the gradebook.
2693      *
2694      * @param stdClass $grade a grade record keyed on id
2695      * @param bool $reopenattempt If the attempt reopen method is manual, allow another attempt at this assignment.
2696      * @return bool true for success
2697      */
2698     public function update_grade($grade, $reopenattempt = false) {
2699         global $DB;
2701         $grade->timemodified = time();
2703         if (!empty($grade->workflowstate)) {
2704             $validstates = $this->get_marking_workflow_states_for_current_user();
2705             if (!array_key_exists($grade->workflowstate, $validstates)) {
2706                 return false;
2707             }
2708         }
2710         if ($grade->grade && $grade->grade != -1) {
2711             if ($this->get_instance()->grade > 0) {
2712                 if (!is_numeric($grade->grade)) {
2713                     return false;
2714                 } else if ($grade->grade > $this->get_instance()->grade) {
2715                     return false;
2716                 } else if ($grade->grade < 0) {
2717                     return false;
2718                 }
2719             } else {
2720                 // This is a scale.
2721                 if ($scale = $DB->get_record('scale', array('id' => -($this->get_instance()->grade)))) {
2722                     $scaleoptions = make_menu_from_list($scale->scale);
2723                     if (!array_key_exists((int) $grade->grade, $scaleoptions)) {
2724                         return false;
2725                     }
2726                 }
2727             }
2728         }
2730         if (empty($grade->attemptnumber)) {
2731             // Set it to the default.
2732             $grade->attemptnumber = 0;
2733         }
2734         $DB->update_record('assign_grades', $grade);
2736         $submission = null;
2737         if ($this->get_instance()->teamsubmission) {
2738             if (isset($this->mostrecentteamsubmission)) {
2739                 $submission = $this->mostrecentteamsubmission;
2740             } else {
2741                 $submission = $this->get_group_submission($grade->userid, 0, false);
2742             }
2743         } else {
2744             $submission = $this->get_user_submission($grade->userid, false);
2745         }
2747         // Only push to gradebook if the update is for the most recent attempt.
2748         if ($submission && $submission->attemptnumber != $grade->attemptnumber) {
2749             return true;
2750         }
2752         if ($this->gradebook_item_update(null, $grade)) {
2753             \mod_assign\event\submission_graded::create_from_grade($this, $grade)->trigger();
2754         }
2756         // If the conditions are met, allow another attempt.
2757         if ($submission) {
2758             $this->reopen_submission_if_required($grade->userid,
2759                     $submission,
2760                     $reopenattempt);
2761         }
2763         return true;
2764     }
2766     /**
2767      * View the grant extension date page.
2768      *
2769      * Uses url parameters 'userid'
2770      * or from parameter 'selectedusers'
2771      *
2772      * @param moodleform $mform - Used for validation of the submitted data
2773      * @return string
2774      */
2775     protected function view_grant_extension($mform) {
2776         global $CFG;
2777         require_once($CFG->dirroot . '/mod/assign/extensionform.php');
2779         $o = '';
2781         $data = new stdClass();
2782         $data->id = $this->get_course_module()->id;
2784         $formparams = array(
2785             'instance' => $this->get_instance(),
2786             'assign' => $this
2787         );
2789         $users = optional_param('userid', 0, PARAM_INT);
2790         if (!$users) {
2791             $users = required_param('selectedusers', PARAM_SEQUENCE);
2792         }
2793         $userlist = explode(',', $users);
2795         $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate');
2796         $maxoverride = array('allowsubmissionsfromdate' => 0, 'duedate' => 0, 'cutoffdate' => 0);
2797         foreach ($userlist as $userid) {
2798             // To validate extension date with users overrides.
2799             $override = $this->override_exists($userid);
2800             foreach ($keys as $key) {
2801                 if ($override->{$key}) {
2802                     if ($maxoverride[$key] < $override->{$key}) {
2803                         $maxoverride[$key] = $override->{$key};
2804                     }
2805                 } else if ($maxoverride[$key] < $this->get_instance()->{$key}) {
2806                     $maxoverride[$key] = $this->get_instance()->{$key};
2807                 }
2808             }
2809         }
2810         foreach ($keys as $key) {
2811             if ($maxoverride[$key]) {
2812                 $this->get_instance()->{$key} = $maxoverride[$key];
2813             }
2814         }
2816         $formparams['userlist'] = $userlist;
2818         $data->selectedusers = $users;
2819         $data->userid = 0;
2821         if (empty($mform)) {
2822             $mform = new mod_assign_extension_form(null, $formparams);
2823         }
2824         $mform->set_data($data);
2825         $header = new assign_header($this->get_instance(),
2826                                     $this->get_context(),
2827                                     $this->show_intro(),
2828                                     $this->get_course_module()->id,
2829                                     get_string('grantextension', 'assign'));
2830         $o .= $this->get_renderer()->render($header);
2831         $o .= $this->get_renderer()->render(new assign_form('extensionform', $mform));
2832         $o .= $this->view_footer();
2833         return $o;
2834     }
2836     /**
2837      * Get a list of the users in the same group as this user.
2838      *
2839      * @param int $groupid The id of the group whose members we want or 0 for the default group
2840      * @param bool $onlyids Whether to retrieve only the user id's
2841      * @param bool $excludesuspended Whether to exclude suspended users
2842      * @return array The users (possibly id's only)
2843      */
2844     public function get_submission_group_members($groupid, $onlyids, $excludesuspended = false) {
2845         $members = array();
2846         if ($groupid != 0) {
2847             $allusers = $this->list_participants($groupid, $onlyids);
2848             foreach ($allusers as $user) {
2849                 if ($this->get_submission_group($user->id)) {
2850                     $members[] = $user;
2851                 }
2852             }
2853         } else {
2854             $allusers = $this->list_participants(null, $onlyids);
2855             foreach ($allusers as $user) {
2856                 if ($this->get_submission_group($user->id) == null) {
2857                     $members[] = $user;
2858                 }
2859             }
2860         }
2861         // Exclude suspended users, if user can't see them.
2862         if ($excludesuspended || !has_capability('moodle/course:viewsuspendedusers', $this->context)) {
2863             foreach ($members as $key => $member) {
2864                 if (!$this->is_active_user($member->id)) {
2865                     unset($members[$key]);
2866                 }
2867             }
2868         }
2870         return $members;
2871     }
2873     /**
2874      * Get a list of the users in the same group as this user that have not submitted the assignment.
2875      *
2876      * @param int $groupid The id of the group whose members we want or 0 for the default group
2877      * @param bool $onlyids Whether to retrieve only the user id's
2878      * @return array The users (possibly id's only)
2879      */
2880     public function get_submission_group_members_who_have_not_submitted($groupid, $onlyids) {
2881         $instance = $this->get_instance();
2882         if (!$instance->teamsubmission || !$instance->requireallteammemberssubmit) {
2883             return array();
2884         }
2885         $members = $this->get_submission_group_members($groupid, $onlyids);
2887         foreach ($members as $id => $member) {
2888             $submission = $this->get_user_submission($member->id, false);
2889             if ($submission && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
2890                 unset($members[$id]);
2891             } else {
2892                 if ($this->is_blind_marking()) {
2893                     $members[$id]->alias = get_string('hiddenuser', 'assign') .
2894                                            $this->get_uniqueid_for_user($id);
2895                 }
2896             }
2897         }
2898         return $members;
2899     }
2901     /**
2902      * Load the group submission object for a particular user, optionally creating it if required.
2903      *
2904      * @param int $userid The id of the user whose submission we want
2905      * @param int $groupid The id of the group for this user - may be 0 in which
2906      *                     case it is determined from the userid.
2907      * @param bool $create If set to true a new submission object will be created in the database
2908      *                     with the status set to "new".
2909      * @param int $attemptnumber - -1 means the latest attempt
2910      * @return stdClass The submission
2911      */
2912     public function get_group_submission($userid, $groupid, $create, $attemptnumber=-1) {
2913         global $DB;
2915         if ($groupid == 0) {
2916             $group = $this->get_submission_group($userid);
2917             if ($group) {
2918                 $groupid = $group->id;
2919             }
2920         }
2922         // Now get the group submission.
2923         $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
2924         if ($attemptnumber >= 0) {
2925             $params['attemptnumber'] = $attemptnumber;
2926         }
2928         // Only return the row with the highest attemptnumber.
2929         $submission = null;
2930         $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', '*', 0, 1);
2931         if ($submissions) {
2932             $submission = reset($submissions);
2933         }
2935         if ($submission) {
2936             return $submission;
2937         }
2938         if ($create) {
2939             $submission = new stdClass();
2940             $submission->assignment = $this->get_instance()->id;
2941             $submission->userid = 0;
2942             $submission->groupid = $groupid;
2943             $submission->timecreated = time();
2944             $submission->timemodified = $submission->timecreated;
2945             if ($attemptnumber >= 0) {
2946                 $submission->attemptnumber = $attemptnumber;
2947             } else {
2948                 $submission->attemptnumber = 0;
2949             }
2950             // Work out if this is the latest submission.
2951             $submission->latest = 0;
2952             $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
2953             if ($attemptnumber == -1) {
2954                 // This is a new submission so it must be the latest.
2955                 $submission->latest = 1;
2956             } else {
2957                 // We need to work this out.
2958                 $result = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', 'attemptnumber', 0, 1);
2959                 if ($result) {
2960                     $latestsubmission = reset($result);
2961                 }
2962                 if (!$latestsubmission || ($attemptnumber == $latestsubmission->attemptnumber)) {
2963                     $submission->latest = 1;
2964                 }
2965             }
2966             if ($submission->latest) {
2967                 // This is the case when we need to set latest to 0 for all the other attempts.
2968                 $DB->set_field('assign_submission', 'latest', 0, $params);
2969             }
2970             $submission->status = ASSIGN_SUBMISSION_STATUS_NEW;
2971             $sid = $DB->insert_record('assign_submission', $submission);
2972             return $DB->get_record('assign_submission', array('id' => $sid));
2973         }
2974         return false;
2975     }
2977     /**
2978      * View a summary listing of all assignments in the current course.
2979      *
2980      * @return string
2981      */
2982     private function view_course_index() {
2983         global $USER;
2985         $o = '';
2987         $course = $this->get_course();
2988         $strplural = get_string('modulenameplural', 'assign');
2990         if (!$cms = get_coursemodules_in_course('assign', $course->id, 'm.duedate')) {
2991             $o .= $this->get_renderer()->notification(get_string('thereareno', 'moodle', $strplural));
2992             $o .= $this->get_renderer()->continue_button(new moodle_url('/course/view.php', array('id' => $course->id)));
2993             return $o;
2994         }
2996         $strsectionname = '';
2997         $usesections = course_format_uses_sections($course->format);
2998         $modinfo = get_fast_modinfo($course);
3000         if ($usesections) {
3001             $strsectionname = get_string('sectionname', 'format_'.$course->format);
3002             $sections = $modinfo->get_section_info_all();
3003         }
3004         $courseindexsummary = new assign_course_index_summary($usesections, $strsectionname);
3006         $timenow = time();
3008         $currentsection = '';
3009         foreach ($modinfo->instances['assign'] as $cm) {
3010             if (!$cm->uservisible) {
3011                 continue;
3012             }
3014             $timedue = $cms[$cm->id]->duedate;
3016             $sectionname = '';
3017             if ($usesections && $cm->sectionnum) {
3018                 $sectionname = get_section_name($course, $sections[$cm->sectionnum]);
3019             }
3021             $submitted = '';
3022             $context = context_module::instance($cm->id);
3024             $assignment = new assign($context, $cm, $course);
3026             // Apply overrides.
3027             $assignment->update_effective_access($USER->id);
3028             $timedue = $assignment->get_instance()->duedate;
3030             if (has_capability('mod/assign:grade', $context)) {
3031                 $submitted = $assignment->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED);
3033             } else if (has_capability('mod/assign:submit', $context)) {
3034                 if ($assignment->get_instance()->teamsubmission) {
3035                     $usersubmission = $assignment->get_group_submission($USER->id, 0, false);
3036                 } else {
3037                     $usersubmission = $assignment->get_user_submission($USER->id, false);
3038                 }
3040                 if (!empty($usersubmission->status)) {
3041                     $submitted = get_string('submissionstatus_' . $usersubmission->status, 'assign');
3042                 } else {
3043                     $submitted = get_string('submissionstatus_', 'assign');
3044                 }
3045             }
3046             $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $cm->instance, $USER->id);
3047             if (isset($gradinginfo->items[0]->grades[$USER->id]) &&
3048                     !$gradinginfo->items[0]->grades[$USER->id]->hidden ) {
3049                 $grade = $gradinginfo->items[0]->grades[$USER->id]->str_grade;
3050             } else {
3051                 $grade = '-';
3052             }
3054             $courseindexsummary->add_assign_info($cm->id, $cm->get_formatted_name(), $sectionname, $timedue, $submitted, $grade);
3056         }
3058         $o .= $this->get_renderer()->render($courseindexsummary);
3059         $o .= $this->view_footer();
3061         return $o;
3062     }
3064     /**
3065      * View a page rendered by a plugin.
3066      *
3067      * Uses url parameters 'pluginaction', 'pluginsubtype', 'plugin', and 'id'.
3068      *
3069      * @return string
3070      */
3071     protected function view_plugin_page() {
3072         global $USER;
3074         $o = '';
3076         $pluginsubtype = required_param('pluginsubtype', PARAM_ALPHA);
3077         $plugintype = required_param('plugin', PARAM_PLUGIN);
3078         $pluginaction = required_param('pluginaction', PARAM_ALPHA);
3080         $plugin = $this->get_plugin_by_type($pluginsubtype, $plugintype);
3081         if (!$plugin) {
3082             print_error('invalidformdata', '');
3083             return;
3084         }
3086         $o .= $plugin->view_page($pluginaction);
3088         return $o;
3089     }
3092     /**
3093      * This is used for team assignments to get the group for the specified user.
3094      * If the user is a member of multiple or no groups this will return false
3095      *
3096      * @param int $userid The id of the user whose submission we want
3097      * @return mixed The group or false
3098      */
3099     public function get_submission_group($userid) {
3101         if (isset($this->usersubmissiongroups[$userid])) {
3102             return $this->usersubmissiongroups[$userid];
3103         }
3105         $groups = $this->get_all_groups($userid);
3106         if (count($groups) != 1) {
3107             $return = false;
3108         } else {
3109             $return = array_pop($groups);
3110         }
3112         // Cache the user submission group.
3113         $this->usersubmissiongroups[$userid] = $return;
3115         return $return;
3116     }
3118     /**
3119      * Gets all groups the user is a member of.
3120      *
3121      * @param int $userid Teh id of the user who's groups we are checking
3122      * @return array The group objects
3123      */
3124     public function get_all_groups($userid) {
3125         if (isset($this->usergroups[$userid])) {
3126             return $this->usergroups[$userid];
3127         }
3129         $grouping = $this->get_instance()->teamsubmissiongroupingid;
3130         $return = groups_get_all_groups($this->get_course()->id, $userid, $grouping);
3132         $this->usergroups[$userid] = $return;
3134         return $return;
3135     }
3138     /**
3139      * Display the submission that is used by a plugin.
3140      *
3141      * Uses url parameters 'sid', 'gid' and 'plugin'.
3142      *
3143      * @param string $pluginsubtype
3144      * @return string
3145      */
3146     protected function view_plugin_content($pluginsubtype) {
3147         $o = '';
3149         $submissionid = optional_param('sid', 0, PARAM_INT);
3150         $gradeid = optional_param('gid', 0, PARAM_INT);
3151         $plugintype = required_param('plugin', PARAM_PLUGIN);
3152         $item = null;
3153         if ($pluginsubtype == 'assignsubmission') {
3154             $plugin = $this->get_submission_plugin_by_type($plugintype);
3155             if ($submissionid <= 0) {
3156                 throw new coding_exception('Submission id should not be 0');
3157             }
3158             $item = $this->get_submission($submissionid);
3160             // Check permissions.
3161             if (empty($item->userid)) {
3162                 // Group submission.
3163                 $this->require_view_group_submission($item->groupid);
3164             } else {
3165                 $this->require_view_submission($item->userid);
3166             }
3167             $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
3168                                                               $this->get_context(),
3169                                                               $this->show_intro(),
3170                                                               $this->get_course_module()->id,
3171                                                               $plugin->get_name()));
3172             $o .= $this->get_renderer()->render(new assign_submission_plugin_submission($plugin,
3173                                                               $item,
3174                                                               assign_submission_plugin_submission::FULL,
3175                                                               $this->get_course_module()->id,
3176                                                               $this->get_return_action(),
3177                                                               $this->get_return_params()));
3179             // Trigger event for viewing a submission.
3180             \mod_assign\event\submission_viewed::create_from_submission($this, $item)->trigger();
3182         } else {
3183             $plugin = $this->get_feedback_plugin_by_type($plugintype);
3184             if ($gradeid <= 0) {
3185                 throw new coding_exception('Grade id should not be 0');
3186             }
3187             $item = $this->get_grade($gradeid);
3188             // Check permissions.
3189             $this->require_view_submission($item->userid);
3190             $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
3191                                                               $this->get_context(),
3192                                                               $this->show_intro(),
3193                                                               $this->get_course_module()->id,
3194                                                               $plugin->get_name()));
3195             $o .= $this->get_renderer()->render(new assign_feedback_plugin_feedback($plugin,
3196                                                               $item,
3197                                                               assign_feedback_plugin_feedback::FULL,
3198                                                               $this->get_course_module()->id,
3199                                                               $this->get_return_action(),
3200                                                               $this->get_return_params()));
3202             // Trigger event for viewing feedback.
3203             \mod_assign\event\feedback_viewed::create_from_grade($this, $item)->trigger();
3204         }
3206         $o .= $this->view_return_links();
3208         $o .= $this->view_footer();
3210         return $o;
3211     }
3213     /**
3214      * Rewrite plugin file urls so they resolve correctly in an exported zip.
3215      *
3216      * @param string $text - The replacement text
3217      * @param stdClass $user - The user record
3218      * @param assign_plugin $plugin - The assignment plugin
3219      */
3220     public function download_rewrite_pluginfile_urls($text, $user, $plugin) {
3221         // The groupname prefix for the urls doesn't depend on the group mode of the assignment instance.
3222         // Rather, it should be determined by checking the group submission settings of the instance,
3223         // which is what download_submission() does when generating the file name prefixes.
3224         $groupname = '';
3225         if ($this->get_instance()->teamsubmission) {
3226             $submissiongroup = $this->get_submission_group($user->id);
3227             if ($submissiongroup) {
3228                 $groupname = $submissiongroup->name . '-';
3229             } else {
3230                 $groupname = get_string('defaultteam', 'assign') . '-';
3231             }
3232         }
3234         if ($this->is_blind_marking()) {
3235             $prefix = $groupname . get_string('participant', 'assign');
3236             $prefix = str_replace('_', ' ', $prefix);
3237             $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($user->id) . '_');
3238         } else {
3239             $prefix = $groupname . fullname($user);
3240             $prefix = str_replace('_', ' ', $prefix);
3241             $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($user->id) . '_');
3242         }
3244         // Only prefix files if downloadasfolders user preference is NOT set.
3245         if (!get_user_preferences('assign_downloadasfolders', 1)) {
3246             $subtype = $plugin->get_subtype();
3247             $type = $plugin->get_type();
3248             $prefix = $prefix . $subtype . '_' . $type . '_';
3249         } else {
3250             $prefix = "";
3251         }
3252         $result = str_replace('@@PLUGINFILE@@/', $prefix, $text);
3254         return $result;
3255     }
3257     /**
3258      * Render the content in editor that is often used by plugin.
3259      *
3260      * @param string $filearea
3261      * @param int $submissionid
3262      * @param string $plugintype
3263      * @param string $editor
3264      * @param string $component
3265      * @param bool $shortentext Whether to shorten the text content.
3266      * @return string
3267      */
3268     public function render_editor_content($filearea, $submissionid, $plugintype, $editor, $component, $shortentext = false) {
3269         global $CFG;
3271         $result = '';
3273         $plugin = $this->get_submission_plugin_by_type($plugintype);
3275         $text = $plugin->get_editor_text($editor, $submissionid);
3276         if ($shortentext) {
3277             $text = shorten_text($text, 140);
3278         }
3279         $format = $plugin->get_editor_format($editor, $submissionid);
3281         $finaltext = file_rewrite_pluginfile_urls($text,
3282                                                   'pluginfile.php',
3283                                                   $this->get_context()->id,
3284                                                   $component,
3285                                                   $filearea,
3286                                                   $submissionid);
3287         $params = array('overflowdiv' => true, 'context' => $this->get_context());
3288         $result .= format_text($finaltext, $format, $params);
3290         if ($CFG->enableportfolios && has_capability('mod/assign:exportownsubmission', $this->context)) {
3291             require_once($CFG->libdir . '/portfoliolib.php');
3293             $button = new portfolio_add_button();
3294             $portfolioparams = array('cmid' => $this->get_course_module()->id,
3295                                      'sid' => $submissionid,
3296                                      'plugin' => $plugintype,
3297                                      'editor' => $editor,
3298                                      'area'=>$filearea);
3299             $button->set_callback_options('assign_portfolio_caller', $portfolioparams, 'mod_assign');
3300             $fs = get_file_storage();
3302             if ($files = $fs->get_area_files($this->context->id,
3303                                              $component,
3304                                              $filearea,
3305                                              $submissionid,
3306                                              'timemodified',
3307                                              false)) {
3308                 $button->set_formats(PORTFOLIO_FORMAT_RICHHTML);
3309             } else {
3310                 $button->set_formats(PORTFOLIO_FORMAT_PLAINHTML);