MDL-53077 assign: Set specific pagetype for pages with actions.
[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_SUBMITTED', 'submitted');
37 define('ASSIGN_FILTER_NOT_SUBMITTED', 'notsubmitted');
38 define('ASSIGN_FILTER_SINGLE_USER', 'singleuser');
39 define('ASSIGN_FILTER_REQUIRE_GRADING', 'require_grading');
41 // Marker filter for grading page.
42 define('ASSIGN_MARKER_FILTER_NO_MARKER', -1);
44 // Reopen attempt methods.
45 define('ASSIGN_ATTEMPT_REOPEN_METHOD_NONE', 'none');
46 define('ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL', 'manual');
47 define('ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS', 'untilpass');
49 // Special value means allow unlimited attempts.
50 define('ASSIGN_UNLIMITED_ATTEMPTS', -1);
52 // Grading states.
53 define('ASSIGN_GRADING_STATUS_GRADED', 'graded');
54 define('ASSIGN_GRADING_STATUS_NOT_GRADED', 'notgraded');
56 // Marking workflow states.
57 define('ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED', 'notmarked');
58 define('ASSIGN_MARKING_WORKFLOW_STATE_INMARKING', 'inmarking');
59 define('ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW', 'readyforreview');
60 define('ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW', 'inreview');
61 define('ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE', 'readyforrelease');
62 define('ASSIGN_MARKING_WORKFLOW_STATE_RELEASED', 'released');
64 // Name of file area for intro attachments.
65 define('ASSIGN_INTROATTACHMENT_FILEAREA', 'introattachment');
67 require_once($CFG->libdir . '/accesslib.php');
68 require_once($CFG->libdir . '/formslib.php');
69 require_once($CFG->dirroot . '/repository/lib.php');
70 require_once($CFG->dirroot . '/mod/assign/mod_form.php');
71 require_once($CFG->libdir . '/gradelib.php');
72 require_once($CFG->dirroot . '/grade/grading/lib.php');
73 require_once($CFG->dirroot . '/mod/assign/feedbackplugin.php');
74 require_once($CFG->dirroot . '/mod/assign/submissionplugin.php');
75 require_once($CFG->dirroot . '/mod/assign/renderable.php');
76 require_once($CFG->dirroot . '/mod/assign/gradingtable.php');
77 require_once($CFG->libdir . '/eventslib.php');
78 require_once($CFG->libdir . '/portfolio/caller.php');
80 /**
81  * Standard base class for mod_assign (assignment types).
82  *
83  * @package   mod_assign
84  * @copyright 2012 NetSpot {@link http://www.netspot.com.au}
85  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
86  */
87 class assign {
89     /** @var stdClass the assignment record that contains the global settings for this assign instance */
90     private $instance;
92     /** @var stdClass the grade_item record for this assign instance's primary grade item. */
93     private $gradeitem;
95     /** @var context the context of the course module for this assign instance
96      *               (or just the course if we are creating a new one)
97      */
98     private $context;
100     /** @var stdClass the course this assign instance belongs to */
101     private $course;
103     /** @var stdClass the admin config for all assign instances  */
104     private $adminconfig;
106     /** @var assign_renderer the custom renderer for this module */
107     private $output;
109     /** @var cm_info the course module for this assign instance */
110     private $coursemodule;
112     /** @var array cache for things like the coursemodule name or the scale menu -
113      *             only lives for a single request.
114      */
115     private $cache;
117     /** @var array list of the installed submission plugins */
118     private $submissionplugins;
120     /** @var array list of the installed feedback plugins */
121     private $feedbackplugins;
123     /** @var string action to be used to return to this page
124      *              (without repeating any form submissions etc).
125      */
126     private $returnaction = 'view';
128     /** @var array params to be used to return to this page */
129     private $returnparams = array();
131     /** @var string modulename prevents excessive calls to get_string */
132     private static $modulename = null;
134     /** @var string modulenameplural prevents excessive calls to get_string */
135     private static $modulenameplural = null;
137     /** @var array of marking workflow states for the current user */
138     private $markingworkflowstates = null;
140     /** @var bool whether to exclude users with inactive enrolment */
141     private $showonlyactiveenrol = null;
143     /** @var string A key used to identify userlists created by this object. */
144     private $useridlistid = null;
146     /** @var array cached list of participants for this assignment. The cache key will be group, showactive and the context id */
147     private $participants = array();
149     /** @var array cached list of user groups when team submissions are enabled. The cache key will be the user. */
150     private $usersubmissiongroups = array();
152     /** @var array cached list of user groups. The cache key will be the user. */
153     private $usergroups = array();
155     /** @var array cached list of IDs of users who share group membership with the user. The cache key will be the user. */
156     private $sharedgroupmembers = array();
158     /**
159      * Constructor for the base assign class.
160      *
161      * Note: For $coursemodule you can supply a stdclass if you like, but it
162      * will be more efficient to supply a cm_info object.
163      *
164      * @param mixed $coursemodulecontext context|null the course module context
165      *                                   (or the course context if the coursemodule has not been
166      *                                   created yet).
167      * @param mixed $coursemodule the current course module if it was already loaded,
168      *                            otherwise this class will load one from the context as required.
169      * @param mixed $course the current course  if it was already loaded,
170      *                      otherwise this class will load one from the context as required.
171      */
172     public function __construct($coursemodulecontext, $coursemodule, $course) {
173         global $SESSION;
175         $this->context = $coursemodulecontext;
176         $this->course = $course;
178         // Ensure that $this->coursemodule is a cm_info object (or null).
179         $this->coursemodule = cm_info::create($coursemodule);
181         // Temporary cache only lives for a single request - used to reduce db lookups.
182         $this->cache = array();
184         $this->submissionplugins = $this->load_plugins('assignsubmission');
185         $this->feedbackplugins = $this->load_plugins('assignfeedback');
187         // Extra entropy is required for uniqid() to work on cygwin.
188         $this->useridlistid = clean_param(uniqid('', true), PARAM_ALPHANUM);
190         if (!isset($SESSION->mod_assign_useridlist)) {
191             $SESSION->mod_assign_useridlist = [];
192         }
193     }
195     /**
196      * Set the action and parameters that can be used to return to the current page.
197      *
198      * @param string $action The action for the current page
199      * @param array $params An array of name value pairs which form the parameters
200      *                      to return to the current page.
201      * @return void
202      */
203     public function register_return_link($action, $params) {
204         global $PAGE;
205         $params['action'] = $action;
206         $currenturl = $PAGE->url;
208         $currenturl->params($params);
209         $PAGE->set_url($currenturl);
210     }
212     /**
213      * Return an action that can be used to get back to the current page.
214      *
215      * @return string action
216      */
217     public function get_return_action() {
218         global $PAGE;
220         $params = $PAGE->url->params();
222         if (!empty($params['action'])) {
223             return $params['action'];
224         }
225         return '';
226     }
228     /**
229      * Based on the current assignment settings should we display the intro.
230      *
231      * @return bool showintro
232      */
233     public function show_intro() {
234         if ($this->get_instance()->alwaysshowdescription ||
235                 time() > $this->get_instance()->allowsubmissionsfromdate) {
236             return true;
237         }
238         return false;
239     }
241     /**
242      * Return a list of parameters that can be used to get back to the current page.
243      *
244      * @return array params
245      */
246     public function get_return_params() {
247         global $PAGE;
249         $params = $PAGE->url->params();
250         unset($params['id']);
251         unset($params['action']);
252         return $params;
253     }
255     /**
256      * Set the submitted form data.
257      *
258      * @param stdClass $data The form data (instance)
259      */
260     public function set_instance(stdClass $data) {
261         $this->instance = $data;
262     }
264     /**
265      * Set the context.
266      *
267      * @param context $context The new context
268      */
269     public function set_context(context $context) {
270         $this->context = $context;
271     }
273     /**
274      * Set the course data.
275      *
276      * @param stdClass $course The course data
277      */
278     public function set_course(stdClass $course) {
279         $this->course = $course;
280     }
282     /**
283      * Get list of feedback plugins installed.
284      *
285      * @return array
286      */
287     public function get_feedback_plugins() {
288         return $this->feedbackplugins;
289     }
291     /**
292      * Get list of submission plugins installed.
293      *
294      * @return array
295      */
296     public function get_submission_plugins() {
297         return $this->submissionplugins;
298     }
300     /**
301      * Is blind marking enabled and reveal identities not set yet?
302      *
303      * @return bool
304      */
305     public function is_blind_marking() {
306         return $this->get_instance()->blindmarking && !$this->get_instance()->revealidentities;
307     }
309     /**
310      * Does an assignment have submission(s) or grade(s) already?
311      *
312      * @return bool
313      */
314     public function has_submissions_or_grades() {
315         $allgrades = $this->count_grades();
316         $allsubmissions = $this->count_submissions();
317         if (($allgrades == 0) && ($allsubmissions == 0)) {
318             return false;
319         }
320         return true;
321     }
323     /**
324      * Get a specific submission plugin by its type.
325      *
326      * @param string $subtype assignsubmission | assignfeedback
327      * @param string $type
328      * @return mixed assign_plugin|null
329      */
330     public function get_plugin_by_type($subtype, $type) {
331         $shortsubtype = substr($subtype, strlen('assign'));
332         $name = $shortsubtype . 'plugins';
333         if ($name != 'feedbackplugins' && $name != 'submissionplugins') {
334             return null;
335         }
336         $pluginlist = $this->$name;
337         foreach ($pluginlist as $plugin) {
338             if ($plugin->get_type() == $type) {
339                 return $plugin;
340             }
341         }
342         return null;
343     }
345     /**
346      * Get a feedback plugin by type.
347      *
348      * @param string $type - The type of plugin e.g comments
349      * @return mixed assign_feedback_plugin|null
350      */
351     public function get_feedback_plugin_by_type($type) {
352         return $this->get_plugin_by_type('assignfeedback', $type);
353     }
355     /**
356      * Get a submission plugin by type.
357      *
358      * @param string $type - The type of plugin e.g comments
359      * @return mixed assign_submission_plugin|null
360      */
361     public function get_submission_plugin_by_type($type) {
362         return $this->get_plugin_by_type('assignsubmission', $type);
363     }
365     /**
366      * Load the plugins from the sub folders under subtype.
367      *
368      * @param string $subtype - either submission or feedback
369      * @return array - The sorted list of plugins
370      */
371     protected function load_plugins($subtype) {
372         global $CFG;
373         $result = array();
375         $names = core_component::get_plugin_list($subtype);
377         foreach ($names as $name => $path) {
378             if (file_exists($path . '/locallib.php')) {
379                 require_once($path . '/locallib.php');
381                 $shortsubtype = substr($subtype, strlen('assign'));
382                 $pluginclass = 'assign_' . $shortsubtype . '_' . $name;
384                 $plugin = new $pluginclass($this, $name);
386                 if ($plugin instanceof assign_plugin) {
387                     $idx = $plugin->get_sort_order();
388                     while (array_key_exists($idx, $result)) {
389                         $idx +=1;
390                     }
391                     $result[$idx] = $plugin;
392                 }
393             }
394         }
395         ksort($result);
396         return $result;
397     }
399     /**
400      * Display the assignment, used by view.php
401      *
402      * The assignment is displayed differently depending on your role,
403      * the settings for the assignment and the status of the assignment.
404      *
405      * @param string $action The current action if any.
406      * @return string - The page output.
407      */
408     public function view($action='') {
409         global $PAGE;
411         $o = '';
412         $mform = null;
413         $notices = array();
414         $nextpageparams = array();
416         if (!empty($this->get_course_module()->id)) {
417             $nextpageparams['id'] = $this->get_course_module()->id;
418         }
420         // Handle form submissions first.
421         if ($action == 'savesubmission') {
422             $action = 'editsubmission';
423             if ($this->process_save_submission($mform, $notices)) {
424                 $action = 'redirect';
425                 $nextpageparams['action'] = 'view';
426             }
427         } else if ($action == 'editprevioussubmission') {
428             $action = 'editsubmission';
429             if ($this->process_copy_previous_attempt($notices)) {
430                 $action = 'redirect';
431                 $nextpageparams['action'] = 'editsubmission';
432             }
433         } else if ($action == 'lock') {
434             $this->process_lock_submission();
435             $action = 'redirect';
436             $nextpageparams['action'] = 'grading';
437         } else if ($action == 'addattempt') {
438             $this->process_add_attempt(required_param('userid', PARAM_INT));
439             $action = 'redirect';
440             $nextpageparams['action'] = 'grading';
441         } else if ($action == 'reverttodraft') {
442             $this->process_revert_to_draft();
443             $action = 'redirect';
444             $nextpageparams['action'] = 'grading';
445         } else if ($action == 'unlock') {
446             $this->process_unlock_submission();
447             $action = 'redirect';
448             $nextpageparams['action'] = 'grading';
449         } else if ($action == 'setbatchmarkingworkflowstate') {
450             $this->process_set_batch_marking_workflow_state();
451             $action = 'redirect';
452             $nextpageparams['action'] = 'grading';
453         } else if ($action == 'setbatchmarkingallocation') {
454             $this->process_set_batch_marking_allocation();
455             $action = 'redirect';
456             $nextpageparams['action'] = 'grading';
457         } else if ($action == 'confirmsubmit') {
458             $action = 'submit';
459             if ($this->process_submit_for_grading($mform, $notices)) {
460                 $action = 'redirect';
461                 $nextpageparams['action'] = 'view';
462             } else if ($notices) {
463                 $action = 'viewsubmitforgradingerror';
464             }
465         } else if ($action == 'submitotherforgrading') {
466             if ($this->process_submit_other_for_grading($mform, $notices)) {
467                 $action = 'redirect';
468                 $nextpageparams['action'] = 'grading';
469             } else {
470                 $action = 'viewsubmitforgradingerror';
471             }
472         } else if ($action == 'gradingbatchoperation') {
473             $action = $this->process_grading_batch_operation($mform);
474             if ($action == 'grading') {
475                 $action = 'redirect';
476                 $nextpageparams['action'] = 'grading';
477             }
478         } else if ($action == 'submitgrade') {
479             if (optional_param('saveandshownext', null, PARAM_RAW)) {
480                 // Save and show next.
481                 $action = 'grade';
482                 if ($this->process_save_grade($mform)) {
483                     $action = 'redirect';
484                     $nextpageparams['action'] = 'grade';
485                     $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) + 1;
486                     $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
487                 }
488             } else if (optional_param('nosaveandprevious', null, PARAM_RAW)) {
489                 $action = 'redirect';
490                 $nextpageparams['action'] = 'grade';
491                 $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) - 1;
492                 $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
493             } else if (optional_param('nosaveandnext', null, PARAM_RAW)) {
494                 $action = 'redirect';
495                 $nextpageparams['action'] = 'grade';
496                 $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) + 1;
497                 $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
498             } else if (optional_param('savegrade', null, PARAM_RAW)) {
499                 // Save changes button.
500                 $action = 'grade';
501                 if ($this->process_save_grade($mform)) {
502                     $action = 'redirect';
503                     $nextpageparams['action'] = 'savegradingresult';
504                 }
505             } else {
506                 // Cancel button.
507                 $action = 'redirect';
508                 $nextpageparams['action'] = 'grading';
509             }
510         } else if ($action == 'quickgrade') {
511             $message = $this->process_save_quick_grades();
512             $action = 'quickgradingresult';
513         } else if ($action == 'saveoptions') {
514             $this->process_save_grading_options();
515             $action = 'redirect';
516             $nextpageparams['action'] = 'grading';
517         } else if ($action == 'saveextension') {
518             $action = 'grantextension';
519             if ($this->process_save_extension($mform)) {
520                 $action = 'redirect';
521                 $nextpageparams['action'] = 'grading';
522             }
523         } else if ($action == 'revealidentitiesconfirm') {
524             $this->process_reveal_identities();
525             $action = 'redirect';
526             $nextpageparams['action'] = 'grading';
527         }
529         $returnparams = array('rownum'=>optional_param('rownum', 0, PARAM_INT),
530                               'useridlistid' => optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM));
531         $this->register_return_link($action, $returnparams);
533         // Include any page action as part of the body tag CSS id.
534         if (!empty($action)) {
535             $PAGE->set_pagetype('mod-assign-' . $action);
536         }
537         // Now show the right view page.
538         if ($action == 'redirect') {
539             $nextpageurl = new moodle_url('/mod/assign/view.php', $nextpageparams);
540             redirect($nextpageurl);
541             return;
542         } else if ($action == 'savegradingresult') {
543             $message = get_string('gradingchangessaved', 'assign');
544             $o .= $this->view_savegrading_result($message);
545         } else if ($action == 'quickgradingresult') {
546             $mform = null;
547             $o .= $this->view_quickgrading_result($message);
548         } else if ($action == 'grade') {
549             $o .= $this->view_single_grade_page($mform);
550         } else if ($action == 'viewpluginassignfeedback') {
551             $o .= $this->view_plugin_content('assignfeedback');
552         } else if ($action == 'viewpluginassignsubmission') {
553             $o .= $this->view_plugin_content('assignsubmission');
554         } else if ($action == 'editsubmission') {
555             $o .= $this->view_edit_submission_page($mform, $notices);
556         } else if ($action == 'grading') {
557             $o .= $this->view_grading_page();
558         } else if ($action == 'downloadall') {
559             $o .= $this->download_submissions();
560         } else if ($action == 'submit') {
561             $o .= $this->check_submit_for_grading($mform);
562         } else if ($action == 'grantextension') {
563             $o .= $this->view_grant_extension($mform);
564         } else if ($action == 'revealidentities') {
565             $o .= $this->view_reveal_identities_confirm($mform);
566         } else if ($action == 'plugingradingbatchoperation') {
567             $o .= $this->view_plugin_grading_batch_operation($mform);
568         } else if ($action == 'viewpluginpage') {
569              $o .= $this->view_plugin_page();
570         } else if ($action == 'viewcourseindex') {
571              $o .= $this->view_course_index();
572         } else if ($action == 'viewbatchsetmarkingworkflowstate') {
573              $o .= $this->view_batch_set_workflow_state($mform);
574         } else if ($action == 'viewbatchmarkingallocation') {
575             $o .= $this->view_batch_markingallocation($mform);
576         } else if ($action == 'viewsubmitforgradingerror') {
577             $o .= $this->view_error_page(get_string('submitforgrading', 'assign'), $notices);
578         } else {
579             $o .= $this->view_submission_page();
580         }
582         return $o;
583     }
585     /**
586      * Add this instance to the database.
587      *
588      * @param stdClass $formdata The data submitted from the form
589      * @param bool $callplugins This is used to skip the plugin code
590      *             when upgrading an old assignment to a new one (the plugins get called manually)
591      * @return mixed false if an error occurs or the int id of the new instance
592      */
593     public function add_instance(stdClass $formdata, $callplugins) {
594         global $DB;
595         $adminconfig = $this->get_admin_config();
597         $err = '';
599         // Add the database record.
600         $update = new stdClass();
601         $update->name = $formdata->name;
602         $update->timemodified = time();
603         $update->timecreated = time();
604         $update->course = $formdata->course;
605         $update->courseid = $formdata->course;
606         $update->intro = $formdata->intro;
607         $update->introformat = $formdata->introformat;
608         $update->alwaysshowdescription = !empty($formdata->alwaysshowdescription);
609         $update->submissiondrafts = $formdata->submissiondrafts;
610         $update->requiresubmissionstatement = $formdata->requiresubmissionstatement;
611         $update->sendnotifications = $formdata->sendnotifications;
612         $update->sendlatenotifications = $formdata->sendlatenotifications;
613         $update->sendstudentnotifications = $adminconfig->sendstudentnotifications;
614         if (isset($formdata->sendstudentnotifications)) {
615             $update->sendstudentnotifications = $formdata->sendstudentnotifications;
616         }
617         $update->duedate = $formdata->duedate;
618         $update->cutoffdate = $formdata->cutoffdate;
619         $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate;
620         $update->grade = $formdata->grade;
621         $update->completionsubmit = !empty($formdata->completionsubmit);
622         $update->teamsubmission = $formdata->teamsubmission;
623         $update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit;
624         if (isset($formdata->teamsubmissiongroupingid)) {
625             $update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid;
626         }
627         $update->blindmarking = $formdata->blindmarking;
628         $update->attemptreopenmethod = ASSIGN_ATTEMPT_REOPEN_METHOD_NONE;
629         if (!empty($formdata->attemptreopenmethod)) {
630             $update->attemptreopenmethod = $formdata->attemptreopenmethod;
631         }
632         if (!empty($formdata->maxattempts)) {
633             $update->maxattempts = $formdata->maxattempts;
634         }
635         if (isset($formdata->preventsubmissionnotingroup)) {
636             $update->preventsubmissionnotingroup = $formdata->preventsubmissionnotingroup;
637         }
638         $update->markingworkflow = $formdata->markingworkflow;
639         $update->markingallocation = $formdata->markingallocation;
640         if (empty($update->markingworkflow)) { // If marking workflow is disabled, make sure allocation is disabled.
641             $update->markingallocation = 0;
642         }
644         $returnid = $DB->insert_record('assign', $update);
645         $this->instance = $DB->get_record('assign', array('id'=>$returnid), '*', MUST_EXIST);
646         // Cache the course record.
647         $this->course = $DB->get_record('course', array('id'=>$formdata->course), '*', MUST_EXIST);
649         $this->save_intro_draft_files($formdata);
651         if ($callplugins) {
652             // Call save_settings hook for submission plugins.
653             foreach ($this->submissionplugins as $plugin) {
654                 if (!$this->update_plugin_instance($plugin, $formdata)) {
655                     print_error($plugin->get_error());
656                     return false;
657                 }
658             }
659             foreach ($this->feedbackplugins as $plugin) {
660                 if (!$this->update_plugin_instance($plugin, $formdata)) {
661                     print_error($plugin->get_error());
662                     return false;
663                 }
664             }
666             // In the case of upgrades the coursemodule has not been set,
667             // so we need to wait before calling these two.
668             $this->update_calendar($formdata->coursemodule);
669             $this->update_gradebook(false, $formdata->coursemodule);
671         }
673         $update = new stdClass();
674         $update->id = $this->get_instance()->id;
675         $update->nosubmissions = (!$this->is_any_submission_plugin_enabled()) ? 1: 0;
676         $DB->update_record('assign', $update);
678         return $returnid;
679     }
681     /**
682      * Delete all grades from the gradebook for this assignment.
683      *
684      * @return bool
685      */
686     protected function delete_grades() {
687         global $CFG;
689         $result = grade_update('mod/assign',
690                                $this->get_course()->id,
691                                'mod',
692                                'assign',
693                                $this->get_instance()->id,
694                                0,
695                                null,
696                                array('deleted'=>1));
697         return $result == GRADE_UPDATE_OK;
698     }
700     /**
701      * Delete this instance from the database.
702      *
703      * @return bool false if an error occurs
704      */
705     public function delete_instance() {
706         global $DB;
707         $result = true;
709         foreach ($this->submissionplugins as $plugin) {
710             if (!$plugin->delete_instance()) {
711                 print_error($plugin->get_error());
712                 $result = false;
713             }
714         }
715         foreach ($this->feedbackplugins as $plugin) {
716             if (!$plugin->delete_instance()) {
717                 print_error($plugin->get_error());
718                 $result = false;
719             }
720         }
722         // Delete files associated with this assignment.
723         $fs = get_file_storage();
724         if (! $fs->delete_area_files($this->context->id) ) {
725             $result = false;
726         }
728         // Delete_records will throw an exception if it fails - so no need for error checking here.
729         $DB->delete_records('assign_submission', array('assignment' => $this->get_instance()->id));
730         $DB->delete_records('assign_grades', array('assignment' => $this->get_instance()->id));
731         $DB->delete_records('assign_plugin_config', array('assignment' => $this->get_instance()->id));
732         $DB->delete_records('assign_user_flags', array('assignment' => $this->get_instance()->id));
733         $DB->delete_records('assign_user_mapping', array('assignment' => $this->get_instance()->id));
735         // Delete items from the gradebook.
736         if (! $this->delete_grades()) {
737             $result = false;
738         }
740         // Delete the instance.
741         $DB->delete_records('assign', array('id'=>$this->get_instance()->id));
743         return $result;
744     }
746     /**
747      * Actual implementation of the reset course functionality, delete all the
748      * assignment submissions for course $data->courseid.
749      *
750      * @param stdClass $data the data submitted from the reset course.
751      * @return array status array
752      */
753     public function reset_userdata($data) {
754         global $CFG, $DB;
756         $componentstr = get_string('modulenameplural', 'assign');
757         $status = array();
759         $fs = get_file_storage();
760         if (!empty($data->reset_assign_submissions)) {
761             // Delete files associated with this assignment.
762             foreach ($this->submissionplugins as $plugin) {
763                 $fileareas = array();
764                 $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type();
765                 $fileareas = $plugin->get_file_areas();
766                 foreach ($fileareas as $filearea => $notused) {
767                     $fs->delete_area_files($this->context->id, $plugincomponent, $filearea);
768                 }
770                 if (!$plugin->delete_instance()) {
771                     $status[] = array('component'=>$componentstr,
772                                       'item'=>get_string('deleteallsubmissions', 'assign'),
773                                       'error'=>$plugin->get_error());
774                 }
775             }
777             foreach ($this->feedbackplugins as $plugin) {
778                 $fileareas = array();
779                 $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type();
780                 $fileareas = $plugin->get_file_areas();
781                 foreach ($fileareas as $filearea => $notused) {
782                     $fs->delete_area_files($this->context->id, $plugincomponent, $filearea);
783                 }
785                 if (!$plugin->delete_instance()) {
786                     $status[] = array('component'=>$componentstr,
787                                       'item'=>get_string('deleteallsubmissions', 'assign'),
788                                       'error'=>$plugin->get_error());
789                 }
790             }
792             $assignids = $DB->get_records('assign', array('course' => $data->courseid), '', 'id');
793             list($sql, $params) = $DB->get_in_or_equal(array_keys($assignids));
795             $DB->delete_records_select('assign_submission', "assignment $sql", $params);
796             $DB->delete_records_select('assign_user_flags', "assignment $sql", $params);
798             $status[] = array('component'=>$componentstr,
799                               'item'=>get_string('deleteallsubmissions', 'assign'),
800                               'error'=>false);
802             if (!empty($data->reset_gradebook_grades)) {
803                 $DB->delete_records_select('assign_grades', "assignment $sql", $params);
804                 // Remove all grades from gradebook.
805                 require_once($CFG->dirroot.'/mod/assign/lib.php');
806                 assign_reset_gradebook($data->courseid);
808                 // Reset revealidentities if both submissions and grades have been reset.
809                 if ($this->get_instance()->blindmarking && $this->get_instance()->revealidentities) {
810                     $DB->set_field('assign', 'revealidentities', 0, array('id' => $this->get_instance()->id));
811                 }
812             }
813         }
814         // Updating dates - shift may be negative too.
815         if ($data->timeshift) {
816             shift_course_mod_dates('assign',
817                                     array('duedate', 'allowsubmissionsfromdate', 'cutoffdate'),
818                                     $data->timeshift,
819                                     $data->courseid, $this->get_instance()->id);
820             $status[] = array('component'=>$componentstr,
821                               'item'=>get_string('datechanged'),
822                               'error'=>false);
823         }
825         return $status;
826     }
828     /**
829      * Update the settings for a single plugin.
830      *
831      * @param assign_plugin $plugin The plugin to update
832      * @param stdClass $formdata The form data
833      * @return bool false if an error occurs
834      */
835     protected function update_plugin_instance(assign_plugin $plugin, stdClass $formdata) {
836         if ($plugin->is_visible()) {
837             $enabledname = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
838             if (!empty($formdata->$enabledname)) {
839                 $plugin->enable();
840                 if (!$plugin->save_settings($formdata)) {
841                     print_error($plugin->get_error());
842                     return false;
843                 }
844             } else {
845                 $plugin->disable();
846             }
847         }
848         return true;
849     }
851     /**
852      * Update the gradebook information for this assignment.
853      *
854      * @param bool $reset If true, will reset all grades in the gradbook for this assignment
855      * @param int $coursemoduleid This is required because it might not exist in the database yet
856      * @return bool
857      */
858     public function update_gradebook($reset, $coursemoduleid) {
859         global $CFG;
861         require_once($CFG->dirroot.'/mod/assign/lib.php');
862         $assign = clone $this->get_instance();
863         $assign->cmidnumber = $coursemoduleid;
865         // Set assign gradebook feedback plugin status (enabled and visible).
866         $assign->gradefeedbackenabled = $this->is_gradebook_feedback_enabled();
868         $param = null;
869         if ($reset) {
870             $param = 'reset';
871         }
873         return assign_grade_item_update($assign, $param);
874     }
876     /**
877      * Load and cache the admin config for this module.
878      *
879      * @return stdClass the plugin config
880      */
881     public function get_admin_config() {
882         if ($this->adminconfig) {
883             return $this->adminconfig;
884         }
885         $this->adminconfig = get_config('assign');
886         return $this->adminconfig;
887     }
889     /**
890      * Update the calendar entries for this assignment.
891      *
892      * @param int $coursemoduleid - Required to pass this in because it might
893      *                              not exist in the database yet.
894      * @return bool
895      */
896     public function update_calendar($coursemoduleid) {
897         global $DB, $CFG;
898         require_once($CFG->dirroot.'/calendar/lib.php');
900         // Special case for add_instance as the coursemodule has not been set yet.
901         $instance = $this->get_instance();
903         $eventtype = 'due';
905         if ($instance->duedate) {
906             $event = new stdClass();
908             $params = array('modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => $eventtype);
909             $event->id = $DB->get_field('event', 'id', $params);
910             $event->name = $instance->name;
911             $event->timestart = $instance->duedate;
913             // Convert the links to pluginfile. It is a bit hacky but at this stage the files
914             // might not have been saved in the module area yet.
915             $intro = $instance->intro;
916             if ($draftid = file_get_submitted_draft_itemid('introeditor')) {
917                 $intro = file_rewrite_urls_to_pluginfile($intro, $draftid);
918             }
920             // We need to remove the links to files as the calendar is not ready
921             // to support module events with file areas.
922             $intro = strip_pluginfile_content($intro);
923             if ($this->show_intro()) {
924                 $event->description = array(
925                     'text' => $intro,
926                     'format' => $instance->introformat
927                 );
928             } else {
929                 $event->description = array(
930                     'text' => '',
931                     'format' => $instance->introformat
932                 );
933             }
935             if ($event->id) {
936                 $calendarevent = calendar_event::load($event->id);
937                 $calendarevent->update($event);
938             } else {
939                 unset($event->id);
940                 $event->courseid    = $instance->course;
941                 $event->groupid     = 0;
942                 $event->userid      = 0;
943                 $event->modulename  = 'assign';
944                 $event->instance    = $instance->id;
945                 $event->eventtype   = $eventtype;
946                 $event->timeduration = 0;
947                 calendar_event::create($event);
948             }
949         } else {
950             $DB->delete_records('event', array('modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => $eventtype));
951         }
952     }
955     /**
956      * Update this instance in the database.
957      *
958      * @param stdClass $formdata - the data submitted from the form
959      * @return bool false if an error occurs
960      */
961     public function update_instance($formdata) {
962         global $DB;
963         $adminconfig = $this->get_admin_config();
965         $update = new stdClass();
966         $update->id = $formdata->instance;
967         $update->name = $formdata->name;
968         $update->timemodified = time();
969         $update->course = $formdata->course;
970         $update->intro = $formdata->intro;
971         $update->introformat = $formdata->introformat;
972         $update->alwaysshowdescription = !empty($formdata->alwaysshowdescription);
973         $update->submissiondrafts = $formdata->submissiondrafts;
974         $update->requiresubmissionstatement = $formdata->requiresubmissionstatement;
975         $update->sendnotifications = $formdata->sendnotifications;
976         $update->sendlatenotifications = $formdata->sendlatenotifications;
977         $update->sendstudentnotifications = $adminconfig->sendstudentnotifications;
978         if (isset($formdata->sendstudentnotifications)) {
979             $update->sendstudentnotifications = $formdata->sendstudentnotifications;
980         }
981         $update->duedate = $formdata->duedate;
982         $update->cutoffdate = $formdata->cutoffdate;
983         $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate;
984         $update->grade = $formdata->grade;
985         if (!empty($formdata->completionunlocked)) {
986             $update->completionsubmit = !empty($formdata->completionsubmit);
987         }
988         $update->teamsubmission = $formdata->teamsubmission;
989         $update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit;
990         if (isset($formdata->teamsubmissiongroupingid)) {
991             $update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid;
992         }
993         $update->blindmarking = $formdata->blindmarking;
994         $update->attemptreopenmethod = ASSIGN_ATTEMPT_REOPEN_METHOD_NONE;
995         if (!empty($formdata->attemptreopenmethod)) {
996             $update->attemptreopenmethod = $formdata->attemptreopenmethod;
997         }
998         if (!empty($formdata->maxattempts)) {
999             $update->maxattempts = $formdata->maxattempts;
1000         }
1001         if (isset($formdata->preventsubmissionnotingroup)) {
1002             $update->preventsubmissionnotingroup = $formdata->preventsubmissionnotingroup;
1003         }
1004         $update->markingworkflow = $formdata->markingworkflow;
1005         $update->markingallocation = $formdata->markingallocation;
1006         if (empty($update->markingworkflow)) { // If marking workflow is disabled, make sure allocation is disabled.
1007             $update->markingallocation = 0;
1008         }
1010         $result = $DB->update_record('assign', $update);
1011         $this->instance = $DB->get_record('assign', array('id'=>$update->id), '*', MUST_EXIST);
1013         $this->save_intro_draft_files($formdata);
1015         // Load the assignment so the plugins have access to it.
1017         // Call save_settings hook for submission plugins.
1018         foreach ($this->submissionplugins as $plugin) {
1019             if (!$this->update_plugin_instance($plugin, $formdata)) {
1020                 print_error($plugin->get_error());
1021                 return false;
1022             }
1023         }
1024         foreach ($this->feedbackplugins as $plugin) {
1025             if (!$this->update_plugin_instance($plugin, $formdata)) {
1026                 print_error($plugin->get_error());
1027                 return false;
1028             }
1029         }
1031         $this->update_calendar($this->get_course_module()->id);
1032         $this->update_gradebook(false, $this->get_course_module()->id);
1034         $update = new stdClass();
1035         $update->id = $this->get_instance()->id;
1036         $update->nosubmissions = (!$this->is_any_submission_plugin_enabled()) ? 1: 0;
1037         $DB->update_record('assign', $update);
1039         return $result;
1040     }
1042     /**
1043      * Save the attachments in the draft areas.
1044      *
1045      * @param stdClass $formdata
1046      */
1047     protected function save_intro_draft_files($formdata) {
1048         if (isset($formdata->introattachments)) {
1049             file_save_draft_area_files($formdata->introattachments, $this->get_context()->id,
1050                                        'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0);
1051         }
1052     }
1054     /**
1055      * Add elements in grading plugin form.
1056      *
1057      * @param mixed $grade stdClass|null
1058      * @param MoodleQuickForm $mform
1059      * @param stdClass $data
1060      * @param int $userid - The userid we are grading
1061      * @return void
1062      */
1063     protected function add_plugin_grade_elements($grade, MoodleQuickForm $mform, stdClass $data, $userid) {
1064         foreach ($this->feedbackplugins as $plugin) {
1065             if ($plugin->is_enabled() && $plugin->is_visible()) {
1066                 $plugin->get_form_elements_for_user($grade, $mform, $data, $userid);
1067             }
1068         }
1069     }
1073     /**
1074      * Add one plugins settings to edit plugin form.
1075      *
1076      * @param assign_plugin $plugin The plugin to add the settings from
1077      * @param MoodleQuickForm $mform The form to add the configuration settings to.
1078      *                               This form is modified directly (not returned).
1079      * @param array $pluginsenabled A list of form elements to be added to a group.
1080      *                              The new element is added to this array by this function.
1081      * @return void
1082      */
1083     protected function add_plugin_settings(assign_plugin $plugin, MoodleQuickForm $mform, & $pluginsenabled) {
1084         global $CFG;
1085         if ($plugin->is_visible() && !$plugin->is_configurable() && $plugin->is_enabled()) {
1086             $name = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
1087             $pluginsenabled[] = $mform->createElement('hidden', $name, 1);
1088             $mform->setType($name, PARAM_BOOL);
1089             $plugin->get_settings($mform);
1090         } else if ($plugin->is_visible() && $plugin->is_configurable()) {
1091             $name = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
1092             $label = $plugin->get_name();
1093             $label .= ' ' . $this->get_renderer()->help_icon('enabled', $plugin->get_subtype() . '_' . $plugin->get_type());
1094             $pluginsenabled[] = $mform->createElement('checkbox', $name, '', $label);
1096             $default = get_config($plugin->get_subtype() . '_' . $plugin->get_type(), 'default');
1097             if ($plugin->get_config('enabled') !== false) {
1098                 $default = $plugin->is_enabled();
1099             }
1100             $mform->setDefault($plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled', $default);
1102             $plugin->get_settings($mform);
1104         }
1105     }
1107     /**
1108      * Add settings to edit plugin form.
1109      *
1110      * @param MoodleQuickForm $mform The form to add the configuration settings to.
1111      *                               This form is modified directly (not returned).
1112      * @return void
1113      */
1114     public function add_all_plugin_settings(MoodleQuickForm $mform) {
1115         $mform->addElement('header', 'submissiontypes', get_string('submissiontypes', 'assign'));
1117         $submissionpluginsenabled = array();
1118         $group = $mform->addGroup(array(), 'submissionplugins', get_string('submissiontypes', 'assign'), array(' '), false);
1119         foreach ($this->submissionplugins as $plugin) {
1120             $this->add_plugin_settings($plugin, $mform, $submissionpluginsenabled);
1121         }
1122         $group->setElements($submissionpluginsenabled);
1124         $mform->addElement('header', 'feedbacktypes', get_string('feedbacktypes', 'assign'));
1125         $feedbackpluginsenabled = array();
1126         $group = $mform->addGroup(array(), 'feedbackplugins', get_string('feedbacktypes', 'assign'), array(' '), false);
1127         foreach ($this->feedbackplugins as $plugin) {
1128             $this->add_plugin_settings($plugin, $mform, $feedbackpluginsenabled);
1129         }
1130         $group->setElements($feedbackpluginsenabled);
1131         $mform->setExpanded('submissiontypes');
1132     }
1134     /**
1135      * Allow each plugin an opportunity to update the defaultvalues
1136      * passed in to the settings form (needed to set up draft areas for
1137      * editor and filemanager elements)
1138      *
1139      * @param array $defaultvalues
1140      */
1141     public function plugin_data_preprocessing(&$defaultvalues) {
1142         foreach ($this->submissionplugins as $plugin) {
1143             if ($plugin->is_visible()) {
1144                 $plugin->data_preprocessing($defaultvalues);
1145             }
1146         }
1147         foreach ($this->feedbackplugins as $plugin) {
1148             if ($plugin->is_visible()) {
1149                 $plugin->data_preprocessing($defaultvalues);
1150             }
1151         }
1152     }
1154     /**
1155      * Get the name of the current module.
1156      *
1157      * @return string the module name (Assignment)
1158      */
1159     protected function get_module_name() {
1160         if (isset(self::$modulename)) {
1161             return self::$modulename;
1162         }
1163         self::$modulename = get_string('modulename', 'assign');
1164         return self::$modulename;
1165     }
1167     /**
1168      * Get the plural name of the current module.
1169      *
1170      * @return string the module name plural (Assignments)
1171      */
1172     protected function get_module_name_plural() {
1173         if (isset(self::$modulenameplural)) {
1174             return self::$modulenameplural;
1175         }
1176         self::$modulenameplural = get_string('modulenameplural', 'assign');
1177         return self::$modulenameplural;
1178     }
1180     /**
1181      * Has this assignment been constructed from an instance?
1182      *
1183      * @return bool
1184      */
1185     public function has_instance() {
1186         return $this->instance || $this->get_course_module();
1187     }
1189     /**
1190      * Get the settings for the current instance of this assignment
1191      *
1192      * @return stdClass The settings
1193      */
1194     public function get_instance() {
1195         global $DB;
1196         if ($this->instance) {
1197             return $this->instance;
1198         }
1199         if ($this->get_course_module()) {
1200             $params = array('id' => $this->get_course_module()->instance);
1201             $this->instance = $DB->get_record('assign', $params, '*', MUST_EXIST);
1202         }
1203         if (!$this->instance) {
1204             throw new coding_exception('Improper use of the assignment class. ' .
1205                                        'Cannot load the assignment record.');
1206         }
1207         return $this->instance;
1208     }
1210     /**
1211      * Get the primary grade item for this assign instance.
1212      *
1213      * @return stdClass The grade_item record
1214      */
1215     public function get_grade_item() {
1216         if ($this->gradeitem) {
1217             return $this->gradeitem;
1218         }
1219         $instance = $this->get_instance();
1220         $params = array('itemtype' => 'mod',
1221                         'itemmodule' => 'assign',
1222                         'iteminstance' => $instance->id,
1223                         'courseid' => $instance->course,
1224                         'itemnumber' => 0);
1225         $this->gradeitem = grade_item::fetch($params);
1226         if (!$this->gradeitem) {
1227             throw new coding_exception('Improper use of the assignment class. ' .
1228                                        'Cannot load the grade item.');
1229         }
1230         return $this->gradeitem;
1231     }
1233     /**
1234      * Get the context of the current course.
1235      *
1236      * @return mixed context|null The course context
1237      */
1238     public function get_course_context() {
1239         if (!$this->context && !$this->course) {
1240             throw new coding_exception('Improper use of the assignment class. ' .
1241                                        'Cannot load the course context.');
1242         }
1243         if ($this->context) {
1244             return $this->context->get_course_context();
1245         } else {
1246             return context_course::instance($this->course->id);
1247         }
1248     }
1251     /**
1252      * Get the current course module.
1253      *
1254      * @return cm_info|null The course module or null if not known
1255      */
1256     public function get_course_module() {
1257         if ($this->coursemodule) {
1258             return $this->coursemodule;
1259         }
1260         if (!$this->context) {
1261             return null;
1262         }
1264         if ($this->context->contextlevel == CONTEXT_MODULE) {
1265             $modinfo = get_fast_modinfo($this->get_course());
1266             $this->coursemodule = $modinfo->get_cm($this->context->instanceid);
1267             return $this->coursemodule;
1268         }
1269         return null;
1270     }
1272     /**
1273      * Get context module.
1274      *
1275      * @return context
1276      */
1277     public function get_context() {
1278         return $this->context;
1279     }
1281     /**
1282      * Get the current course.
1283      *
1284      * @return mixed stdClass|null The course
1285      */
1286     public function get_course() {
1287         global $DB;
1289         if ($this->course) {
1290             return $this->course;
1291         }
1293         if (!$this->context) {
1294             return null;
1295         }
1296         $params = array('id' => $this->get_course_context()->instanceid);
1297         $this->course = $DB->get_record('course', $params, '*', MUST_EXIST);
1299         return $this->course;
1300     }
1302     /**
1303      * Count the number of intro attachments.
1304      *
1305      * @return int
1306      */
1307     protected function count_attachments() {
1309         $fs = get_file_storage();
1310         $files = $fs->get_area_files($this->get_context()->id, 'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA,
1311                         0, 'id', false);
1313         return count($files);
1314     }
1316     /**
1317      * Are there any intro attachments to display?
1318      *
1319      * @return boolean
1320      */
1321     protected function has_visible_attachments() {
1322         return ($this->count_attachments() > 0);
1323     }
1325     /**
1326      * Return a grade in user-friendly form, whether it's a scale or not.
1327      *
1328      * @param mixed $grade int|null
1329      * @param boolean $editing Are we allowing changes to this grade?
1330      * @param int $userid The user id the grade belongs to
1331      * @param int $modified Timestamp from when the grade was last modified
1332      * @return string User-friendly representation of grade
1333      */
1334     public function display_grade($grade, $editing, $userid=0, $modified=0) {
1335         global $DB;
1337         static $scalegrades = array();
1339         $o = '';
1341         if ($this->get_instance()->grade >= 0) {
1342             // Normal number.
1343             if ($editing && $this->get_instance()->grade > 0) {
1344                 if ($grade < 0) {
1345                     $displaygrade = '';
1346                 } else {
1347                     $displaygrade = format_float($grade, 2);
1348                 }
1349                 $o .= '<label class="accesshide" for="quickgrade_' . $userid . '">' .
1350                        get_string('usergrade', 'assign') .
1351                        '</label>';
1352                 $o .= '<input type="text"
1353                               id="quickgrade_' . $userid . '"
1354                               name="quickgrade_' . $userid . '"
1355                               value="' .  $displaygrade . '"
1356                               size="6"
1357                               maxlength="10"
1358                               class="quickgrade"/>';
1359                 $o .= '&nbsp;/&nbsp;' . format_float($this->get_instance()->grade, 2);
1360                 return $o;
1361             } else {
1362                 if ($grade == -1 || $grade === null) {
1363                     $o .= '-';
1364                 } else {
1365                     $item = $this->get_grade_item();
1366                     $o .= grade_format_gradevalue($grade, $item);
1367                     if ($item->get_displaytype() == GRADE_DISPLAY_TYPE_REAL) {
1368                         // If displaying the raw grade, also display the total value.
1369                         $o .= '&nbsp;/&nbsp;' . format_float($this->get_instance()->grade, 2);
1370                     }
1371                 }
1372                 return $o;
1373             }
1375         } else {
1376             // Scale.
1377             if (empty($this->cache['scale'])) {
1378                 if ($scale = $DB->get_record('scale', array('id'=>-($this->get_instance()->grade)))) {
1379                     $this->cache['scale'] = make_menu_from_list($scale->scale);
1380                 } else {
1381                     $o .= '-';
1382                     return $o;
1383                 }
1384             }
1385             if ($editing) {
1386                 $o .= '<label class="accesshide"
1387                               for="quickgrade_' . $userid . '">' .
1388                       get_string('usergrade', 'assign') .
1389                       '</label>';
1390                 $o .= '<select name="quickgrade_' . $userid . '" class="quickgrade">';
1391                 $o .= '<option value="-1">' . get_string('nograde') . '</option>';
1392                 foreach ($this->cache['scale'] as $optionid => $option) {
1393                     $selected = '';
1394                     if ($grade == $optionid) {
1395                         $selected = 'selected="selected"';
1396                     }
1397                     $o .= '<option value="' . $optionid . '" ' . $selected . '>' . $option . '</option>';
1398                 }
1399                 $o .= '</select>';
1400                 return $o;
1401             } else {
1402                 $scaleid = (int)$grade;
1403                 if (isset($this->cache['scale'][$scaleid])) {
1404                     $o .= $this->cache['scale'][$scaleid];
1405                     return $o;
1406                 }
1407                 $o .= '-';
1408                 return $o;
1409             }
1410         }
1411     }
1413     /**
1414      * Load a list of users enrolled in the current course with the specified permission and group.
1415      * 0 for no group.
1416      *
1417      * @param int $currentgroup
1418      * @param bool $idsonly
1419      * @return array List of user records
1420      */
1421     public function list_participants($currentgroup, $idsonly) {
1423         if (empty($currentgroup)) {
1424             $currentgroup = 0;
1425         }
1427         $key = $this->context->id . '-' . $currentgroup . '-' . $this->show_only_active_users();
1428         if (!isset($this->participants[$key])) {
1429             $users = get_enrolled_users($this->context, 'mod/assign:submit', $currentgroup, 'u.*', null, null, null,
1430                     $this->show_only_active_users());
1432             $cm = $this->get_course_module();
1433             $info = new \core_availability\info_module($cm);
1434             $users = $info->filter_user_list($users);
1436             $this->participants[$key] = $users;
1437         }
1439         if ($idsonly) {
1440             $idslist = array();
1441             foreach ($this->participants[$key] as $id => $user) {
1442                 $idslist[$id] = new stdClass();
1443                 $idslist[$id]->id = $id;
1444             }
1445             return $idslist;
1446         }
1447         return $this->participants[$key];
1448     }
1450     /**
1451      * Load a count of valid teams for this assignment.
1452      *
1453      * @param int $activitygroup Activity active group
1454      * @return int number of valid teams
1455      */
1456     public function count_teams($activitygroup = 0) {
1458         $count = 0;
1460         $participants = $this->list_participants($activitygroup, true);
1462         // If a team submission grouping id is provided all good as all returned groups
1463         // are the submission teams, but if no team submission grouping was specified
1464         // $groups will contain all participants groups.
1465         if ($this->get_instance()->teamsubmissiongroupingid) {
1467             // We restrict the users to the selected group ones.
1468             $groups = groups_get_all_groups($this->get_course()->id,
1469                                             array_keys($participants),
1470                                             $this->get_instance()->teamsubmissiongroupingid,
1471                                             'DISTINCT g.id, g.name');
1473             $count = count($groups);
1475             // When a specific group is selected we don't count the default group users.
1476             if ($activitygroup == 0) {
1477                 if (empty($this->get_instance()->preventsubmissionnotingroup)) {
1478                     // See if there are any users in the default group.
1479                     $defaultusers = $this->get_submission_group_members(0, true);
1480                     if (count($defaultusers) > 0) {
1481                         $count += 1;
1482                     }
1483                 }
1484             }
1485         } else {
1486             // It is faster to loop around participants if no grouping was specified.
1487             $groups = array();
1488             foreach ($participants as $participant) {
1489                 if ($group = $this->get_submission_group($participant->id)) {
1490                     $groups[$group->id] = true;
1491                 } else if (empty($this->get_instance()->preventsubmissionnotingroup)) {
1492                     $groups[0] = true;
1493                 }
1494             }
1496             $count = count($groups);
1497         }
1499         return $count;
1500     }
1502     /**
1503      * Load a count of active users enrolled in the current course with the specified permission and group.
1504      * 0 for no group.
1505      *
1506      * @param int $currentgroup
1507      * @return int number of matching users
1508      */
1509     public function count_participants($currentgroup) {
1510         return count($this->list_participants($currentgroup, true));
1511     }
1513     /**
1514      * Load a count of active users submissions in the current module that require grading
1515      * This means the submission modification time is more recent than the
1516      * grading modification time and the status is SUBMITTED.
1517      *
1518      * @return int number of matching submissions
1519      */
1520     public function count_submissions_need_grading() {
1521         global $DB;
1523         if ($this->get_instance()->teamsubmission) {
1524             // This does not make sense for group assignment because the submission is shared.
1525             return 0;
1526         }
1528         $currentgroup = groups_get_activity_group($this->get_course_module(), true);
1529         list($esql, $params) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
1531         $params['assignid'] = $this->get_instance()->id;
1532         $params['submitted'] = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
1534         $sql = 'SELECT COUNT(s.userid)
1535                    FROM {assign_submission} s
1536                    LEFT JOIN {assign_grades} g ON
1537                         s.assignment = g.assignment AND
1538                         s.userid = g.userid AND
1539                         g.attemptnumber = s.attemptnumber
1540                    JOIN(' . $esql . ') e ON e.id = s.userid
1541                    WHERE
1542                         s.latest = 1 AND
1543                         s.assignment = :assignid AND
1544                         s.timemodified IS NOT NULL AND
1545                         s.status = :submitted AND
1546                         (s.timemodified >= g.timemodified OR g.timemodified IS NULL OR g.grade IS NULL)';
1548         return $DB->count_records_sql($sql, $params);
1549     }
1551     /**
1552      * Load a count of grades.
1553      *
1554      * @return int number of grades
1555      */
1556     public function count_grades() {
1557         global $DB;
1559         if (!$this->has_instance()) {
1560             return 0;
1561         }
1563         $currentgroup = groups_get_activity_group($this->get_course_module(), true);
1564         list($esql, $params) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
1566         $params['assignid'] = $this->get_instance()->id;
1568         $sql = 'SELECT COUNT(g.userid)
1569                    FROM {assign_grades} g
1570                    JOIN(' . $esql . ') e ON e.id = g.userid
1571                    WHERE g.assignment = :assignid';
1573         return $DB->count_records_sql($sql, $params);
1574     }
1576     /**
1577      * Load a count of submissions.
1578      *
1579      * @param bool $includenew When true, also counts the submissions with status 'new'.
1580      * @return int number of submissions
1581      */
1582     public function count_submissions($includenew = false) {
1583         global $DB;
1585         if (!$this->has_instance()) {
1586             return 0;
1587         }
1589         $params = array();
1590         $sqlnew = '';
1592         if (!$includenew) {
1593             $sqlnew = ' AND s.status <> :status ';
1594             $params['status'] = ASSIGN_SUBMISSION_STATUS_NEW;
1595         }
1597         if ($this->get_instance()->teamsubmission) {
1598             // We cannot join on the enrolment tables for group submissions (no userid).
1599             $sql = 'SELECT COUNT(DISTINCT s.groupid)
1600                         FROM {assign_submission} s
1601                         WHERE
1602                             s.assignment = :assignid AND
1603                             s.timemodified IS NOT NULL AND
1604                             s.userid = :groupuserid' .
1605                             $sqlnew;
1607             $params['assignid'] = $this->get_instance()->id;
1608             $params['groupuserid'] = 0;
1609         } else {
1610             $currentgroup = groups_get_activity_group($this->get_course_module(), true);
1611             list($esql, $enrolparams) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
1613             $params = array_merge($params, $enrolparams);
1614             $params['assignid'] = $this->get_instance()->id;
1616             $sql = 'SELECT COUNT(DISTINCT s.userid)
1617                        FROM {assign_submission} s
1618                        JOIN(' . $esql . ') e ON e.id = s.userid
1619                        WHERE
1620                             s.assignment = :assignid AND
1621                             s.timemodified IS NOT NULL ' .
1622                             $sqlnew;
1624         }
1626         return $DB->count_records_sql($sql, $params);
1627     }
1629     /**
1630      * Load a count of submissions with a specified status.
1631      *
1632      * @param string $status The submission status - should match one of the constants
1633      * @return int number of matching submissions
1634      */
1635     public function count_submissions_with_status($status) {
1636         global $DB;
1638         $currentgroup = groups_get_activity_group($this->get_course_module(), true);
1639         list($esql, $params) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
1641         $params['assignid'] = $this->get_instance()->id;
1642         $params['assignid2'] = $this->get_instance()->id;
1643         $params['submissionstatus'] = $status;
1645         if ($this->get_instance()->teamsubmission) {
1647             $groupsstr = '';
1648             if ($currentgroup != 0) {
1649                 // If there is an active group we should only display the current group users groups.
1650                 $participants = $this->list_participants($currentgroup, true);
1651                 $groups = groups_get_all_groups($this->get_course()->id,
1652                                                 array_keys($participants),
1653                                                 $this->get_instance()->teamsubmissiongroupingid,
1654                                                 'DISTINCT g.id, g.name');
1655                 list($groupssql, $groupsparams) = $DB->get_in_or_equal(array_keys($groups), SQL_PARAMS_NAMED);
1656                 $groupsstr = 's.groupid ' . $groupssql . ' AND';
1657                 $params = $params + $groupsparams;
1658             }
1659             $sql = 'SELECT COUNT(s.groupid)
1660                         FROM {assign_submission} s
1661                         WHERE
1662                             s.latest = 1 AND
1663                             s.assignment = :assignid AND
1664                             s.timemodified IS NOT NULL AND
1665                             s.userid = :groupuserid AND '
1666                             . $groupsstr . '
1667                             s.status = :submissionstatus';
1668             $params['groupuserid'] = 0;
1669         } else {
1670             $sql = 'SELECT COUNT(s.userid)
1671                         FROM {assign_submission} s
1672                         JOIN(' . $esql . ') e ON e.id = s.userid
1673                         WHERE
1674                             s.latest = 1 AND
1675                             s.assignment = :assignid AND
1676                             s.timemodified IS NOT NULL AND
1677                             s.status = :submissionstatus';
1679         }
1681         return $DB->count_records_sql($sql, $params);
1682     }
1684     /**
1685      * Utility function to get the userid for every row in the grading table
1686      * so the order can be frozen while we iterate it.
1687      *
1688      * @return array An array of userids
1689      */
1690     protected function get_grading_userid_list() {
1691         $filter = get_user_preferences('assign_filter', '');
1692         $table = new assign_grading_table($this, 0, $filter, 0, false);
1694         $useridlist = $table->get_column_data('userid');
1696         return $useridlist;
1697     }
1699     /**
1700      * Generate zip file from array of given files.
1701      *
1702      * @param array $filesforzipping - array of files to pass into archive_to_pathname.
1703      *                                 This array is indexed by the final file name and each
1704      *                                 element in the array is an instance of a stored_file object.
1705      * @return path of temp file - note this returned file does
1706      *         not have a .zip extension - it is a temp file.
1707      */
1708     protected function pack_files($filesforzipping) {
1709         global $CFG;
1710         // Create path for new zip file.
1711         $tempzip = tempnam($CFG->tempdir . '/', 'assignment_');
1712         // Zip files.
1713         $zipper = new zip_packer();
1714         if ($zipper->archive_to_pathname($filesforzipping, $tempzip)) {
1715             return $tempzip;
1716         }
1717         return false;
1718     }
1720     /**
1721      * Finds all assignment notifications that have yet to be mailed out, and mails them.
1722      *
1723      * Cron function to be run periodically according to the moodle cron.
1724      *
1725      * @return bool
1726      */
1727     public static function cron() {
1728         global $DB;
1730         // Only ever send a max of one days worth of updates.
1731         $yesterday = time() - (24 * 3600);
1732         $timenow   = time();
1733         $lastcron = $DB->get_field('modules', 'lastcron', array('name' => 'assign'));
1735         // Collect all submissions that require mailing.
1736         // Submissions are included if all are true:
1737         //   - The assignment is visible in the gradebook.
1738         //   - No previous notification has been sent.
1739         //   - If marking workflow is not enabled, the grade was updated in the past 24 hours, or
1740         //     if marking workflow is enabled, the workflow state is at 'released'.
1741         $sql = "SELECT g.id as gradeid, a.course, a.name, a.blindmarking, a.revealidentities,
1742                        g.*, g.timemodified as lastmodified, cm.id as cmid
1743                  FROM {assign} a
1744                  JOIN {assign_grades} g ON g.assignment = a.id
1745             LEFT JOIN {assign_user_flags} uf ON uf.assignment = a.id AND uf.userid = g.userid
1746                  JOIN {course_modules} cm ON cm.course = a.course AND cm.instance = a.id
1747                  JOIN {modules} md ON md.id = cm.module AND md.name = 'assign'
1748                  JOIN {grade_items} gri ON gri.iteminstance = a.id AND gri.courseid = a.course AND gri.itemmodule = md.name
1749                  WHERE ((a.markingworkflow = 0 AND g.timemodified >= :yesterday AND g.timemodified <= :today) OR
1750                         (a.markingworkflow = 1 AND uf.workflowstate = :wfreleased)) AND
1751                        uf.mailed = 0 AND gri.hidden = 0
1752               ORDER BY a.course, cm.id";
1754         $params = array(
1755             'yesterday' => $yesterday,
1756             'today' => $timenow,
1757             'wfreleased' => ASSIGN_MARKING_WORKFLOW_STATE_RELEASED,
1758         );
1759         $submissions = $DB->get_records_sql($sql, $params);
1761         if (!empty($submissions)) {
1763             mtrace('Processing ' . count($submissions) . ' assignment submissions ...');
1765             // Preload courses we are going to need those.
1766             $courseids = array();
1767             foreach ($submissions as $submission) {
1768                 $courseids[] = $submission->course;
1769             }
1771             // Filter out duplicates.
1772             $courseids = array_unique($courseids);
1773             $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
1774             list($courseidsql, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
1775             $sql = 'SELECT c.*, ' . $ctxselect .
1776                       ' FROM {course} c
1777                  LEFT JOIN {context} ctx ON ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel
1778                      WHERE c.id ' . $courseidsql;
1780             $params['contextlevel'] = CONTEXT_COURSE;
1781             $courses = $DB->get_records_sql($sql, $params);
1783             // Clean up... this could go on for a while.
1784             unset($courseids);
1785             unset($ctxselect);
1786             unset($courseidsql);
1787             unset($params);
1789             // Message students about new feedback.
1790             foreach ($submissions as $submission) {
1792                 mtrace("Processing assignment submission $submission->id ...");
1794                 // Do not cache user lookups - could be too many.
1795                 if (!$user = $DB->get_record('user', array('id'=>$submission->userid))) {
1796                     mtrace('Could not find user ' . $submission->userid);
1797                     continue;
1798                 }
1800                 // Use a cache to prevent the same DB queries happening over and over.
1801                 if (!array_key_exists($submission->course, $courses)) {
1802                     mtrace('Could not find course ' . $submission->course);
1803                     continue;
1804                 }
1805                 $course = $courses[$submission->course];
1806                 if (isset($course->ctxid)) {
1807                     // Context has not yet been preloaded. Do so now.
1808                     context_helper::preload_from_record($course);
1809                 }
1811                 // Override the language and timezone of the "current" user, so that
1812                 // mail is customised for the receiver.
1813                 cron_setup_user($user, $course);
1815                 // Context lookups are already cached.
1816                 $coursecontext = context_course::instance($course->id);
1817                 if (!is_enrolled($coursecontext, $user->id)) {
1818                     $courseshortname = format_string($course->shortname,
1819                                                      true,
1820                                                      array('context' => $coursecontext));
1821                     mtrace(fullname($user) . ' not an active participant in ' . $courseshortname);
1822                     continue;
1823                 }
1825                 if (!$grader = $DB->get_record('user', array('id'=>$submission->grader))) {
1826                     mtrace('Could not find grader ' . $submission->grader);
1827                     continue;
1828                 }
1830                 $modinfo = get_fast_modinfo($course, $user->id);
1831                 $cm = $modinfo->get_cm($submission->cmid);
1832                 // Context lookups are already cached.
1833                 $contextmodule = context_module::instance($cm->id);
1835                 if (!$cm->uservisible) {
1836                     // Hold mail notification for assignments the user cannot access until later.
1837                     continue;
1838                 }
1840                 // Need to send this to the student.
1841                 $messagetype = 'feedbackavailable';
1842                 $eventtype = 'assign_notification';
1843                 $updatetime = $submission->lastmodified;
1844                 $modulename = get_string('modulename', 'assign');
1846                 $uniqueid = 0;
1847                 if ($submission->blindmarking && !$submission->revealidentities) {
1848                     $uniqueid = self::get_uniqueid_for_user_static($submission->assignment, $user->id);
1849                 }
1850                 $showusers = $submission->blindmarking && !$submission->revealidentities;
1851                 self::send_assignment_notification($grader,
1852                                                    $user,
1853                                                    $messagetype,
1854                                                    $eventtype,
1855                                                    $updatetime,
1856                                                    $cm,
1857                                                    $contextmodule,
1858                                                    $course,
1859                                                    $modulename,
1860                                                    $submission->name,
1861                                                    $showusers,
1862                                                    $uniqueid);
1864                 $flags = $DB->get_record('assign_user_flags', array('userid'=>$user->id, 'assignment'=>$submission->assignment));
1865                 if ($flags) {
1866                     $flags->mailed = 1;
1867                     $DB->update_record('assign_user_flags', $flags);
1868                 } else {
1869                     $flags = new stdClass();
1870                     $flags->userid = $user->id;
1871                     $flags->assignment = $submission->assignment;
1872                     $flags->mailed = 1;
1873                     $DB->insert_record('assign_user_flags', $flags);
1874                 }
1876                 mtrace('Done');
1877             }
1878             mtrace('Done processing ' . count($submissions) . ' assignment submissions');
1880             cron_setup_user();
1882             // Free up memory just to be sure.
1883             unset($courses);
1884         }
1886         // Update calendar events to provide a description.
1887         $sql = 'SELECT id
1888                     FROM {assign}
1889                     WHERE
1890                         allowsubmissionsfromdate >= :lastcron AND
1891                         allowsubmissionsfromdate <= :timenow AND
1892                         alwaysshowdescription = 0';
1893         $params = array('lastcron' => $lastcron, 'timenow' => $timenow);
1894         $newlyavailable = $DB->get_records_sql($sql, $params);
1895         foreach ($newlyavailable as $record) {
1896             $cm = get_coursemodule_from_instance('assign', $record->id, 0, false, MUST_EXIST);
1897             $context = context_module::instance($cm->id);
1899             $assignment = new assign($context, null, null);
1900             $assignment->update_calendar($cm->id);
1901         }
1903         return true;
1904     }
1906     /**
1907      * Mark in the database that this grade record should have an update notification sent by cron.
1908      *
1909      * @param stdClass $grade a grade record keyed on id
1910      * @param bool $mailedoverride when true, flag notification to be sent again.
1911      * @return bool true for success
1912      */
1913     public function notify_grade_modified($grade, $mailedoverride = false) {
1914         global $DB;
1916         $flags = $this->get_user_flags($grade->userid, true);
1917         if ($flags->mailed != 1 || $mailedoverride) {
1918             $flags->mailed = 0;
1919         }
1921         return $this->update_user_flags($flags);
1922     }
1924     /**
1925      * Update user flags for this user in this assignment.
1926      *
1927      * @param stdClass $flags a flags record keyed on id
1928      * @return bool true for success
1929      */
1930     public function update_user_flags($flags) {
1931         global $DB;
1932         if ($flags->userid <= 0 || $flags->assignment <= 0 || $flags->id <= 0) {
1933             return false;
1934         }
1936         $result = $DB->update_record('assign_user_flags', $flags);
1937         return $result;
1938     }
1940     /**
1941      * Update a grade in the grade table for the assignment and in the gradebook.
1942      *
1943      * @param stdClass $grade a grade record keyed on id
1944      * @param bool $reopenattempt If the attempt reopen method is manual, allow another attempt at this assignment.
1945      * @return bool true for success
1946      */
1947     public function update_grade($grade, $reopenattempt = false) {
1948         global $DB;
1950         $grade->timemodified = time();
1952         if (!empty($grade->workflowstate)) {
1953             $validstates = $this->get_marking_workflow_states_for_current_user();
1954             if (!array_key_exists($grade->workflowstate, $validstates)) {
1955                 return false;
1956             }
1957         }
1959         if ($grade->grade && $grade->grade != -1) {
1960             if ($this->get_instance()->grade > 0) {
1961                 if (!is_numeric($grade->grade)) {
1962                     return false;
1963                 } else if ($grade->grade > $this->get_instance()->grade) {
1964                     return false;
1965                 } else if ($grade->grade < 0) {
1966                     return false;
1967                 }
1968             } else {
1969                 // This is a scale.
1970                 if ($scale = $DB->get_record('scale', array('id' => -($this->get_instance()->grade)))) {
1971                     $scaleoptions = make_menu_from_list($scale->scale);
1972                     if (!array_key_exists((int) $grade->grade, $scaleoptions)) {
1973                         return false;
1974                     }
1975                 }
1976             }
1977         }
1979         if (empty($grade->attemptnumber)) {
1980             // Set it to the default.
1981             $grade->attemptnumber = 0;
1982         }
1983         $DB->update_record('assign_grades', $grade);
1985         $submission = null;
1986         if ($this->get_instance()->teamsubmission) {
1987             $submission = $this->get_group_submission($grade->userid, 0, false);
1988         } else {
1989             $submission = $this->get_user_submission($grade->userid, false);
1990         }
1992         // Only push to gradebook if the update is for the latest attempt.
1993         // Not the latest attempt.
1994         if ($submission && $submission->attemptnumber != $grade->attemptnumber) {
1995             return true;
1996         }
1998         if ($this->gradebook_item_update(null, $grade)) {
1999             \mod_assign\event\submission_graded::create_from_grade($this, $grade)->trigger();
2000         }
2002         // If the conditions are met, allow another attempt.
2003         if ($submission) {
2004             $this->reopen_submission_if_required($grade->userid,
2005                     $submission,
2006                     $reopenattempt);
2007         }
2009         return true;
2010     }
2012     /**
2013      * View the grant extension date page.
2014      *
2015      * Uses url parameters 'userid'
2016      * or from parameter 'selectedusers'
2017      *
2018      * @param moodleform $mform - Used for validation of the submitted data
2019      * @return string
2020      */
2021     protected function view_grant_extension($mform) {
2022         global $DB, $CFG;
2023         require_once($CFG->dirroot . '/mod/assign/extensionform.php');
2025         $o = '';
2027         $data = new stdClass();
2028         $data->id = $this->get_course_module()->id;
2030         $formparams = array(
2031             'instance' => $this->get_instance()
2032         );
2034         $extrauserfields = get_extra_user_fields($this->get_context());
2036         if ($mform) {
2037             $submitteddata = $mform->get_data();
2038             $users = $submitteddata->selectedusers;
2039             $userlist = explode(',', $users);
2041             $data->selectedusers = $users;
2042             $data->userid = 0;
2044             $usershtml = '';
2045             $usercount = 0;
2046             foreach ($userlist as $userid) {
2047                 if ($usercount >= 5) {
2048                     $usershtml .= get_string('moreusers', 'assign', count($userlist) - 5);
2049                     break;
2050                 }
2051                 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
2053                 $usershtml .= $this->get_renderer()->render(new assign_user_summary($user,
2054                                                                     $this->get_course()->id,
2055                                                                     has_capability('moodle/site:viewfullnames',
2056                                                                     $this->get_course_context()),
2057                                                                     $this->is_blind_marking(),
2058                                                                     $this->get_uniqueid_for_user($user->id),
2059                                                                     $extrauserfields,
2060                                                                     !$this->is_active_user($userid)));
2061                 $usercount += 1;
2062             }
2064             $formparams['userscount'] = count($userlist);
2065             $formparams['usershtml'] = $usershtml;
2067         } else {
2068             $userid = required_param('userid', PARAM_INT);
2069             $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
2070             $flags = $this->get_user_flags($userid, false);
2072             $data->userid = $user->id;
2073             if ($flags) {
2074                 $data->extensionduedate = $flags->extensionduedate;
2075             }
2077             $usershtml = $this->get_renderer()->render(new assign_user_summary($user,
2078                                                                 $this->get_course()->id,
2079                                                                 has_capability('moodle/site:viewfullnames',
2080                                                                 $this->get_course_context()),
2081                                                                 $this->is_blind_marking(),
2082                                                                 $this->get_uniqueid_for_user($user->id),
2083                                                                 $extrauserfields,
2084                                                                 !$this->is_active_user($userid)));
2085             $formparams['usershtml'] = $usershtml;
2086         }
2088         $mform = new mod_assign_extension_form(null, $formparams);
2089         $mform->set_data($data);
2090         $header = new assign_header($this->get_instance(),
2091                                     $this->get_context(),
2092                                     $this->show_intro(),
2093                                     $this->get_course_module()->id,
2094                                     get_string('grantextension', 'assign'));
2095         $o .= $this->get_renderer()->render($header);
2096         $o .= $this->get_renderer()->render(new assign_form('extensionform', $mform));
2097         $o .= $this->view_footer();
2098         return $o;
2099     }
2101     /**
2102      * Get a list of the users in the same group as this user.
2103      *
2104      * @param int $groupid The id of the group whose members we want or 0 for the default group
2105      * @param bool $onlyids Whether to retrieve only the user id's
2106      * @param bool $excludesuspended Whether to exclude suspended users
2107      * @return array The users (possibly id's only)
2108      */
2109     public function get_submission_group_members($groupid, $onlyids, $excludesuspended = false) {
2110         $members = array();
2111         if ($groupid != 0) {
2112             if ($onlyids) {
2113                 $allusers = groups_get_members($groupid, 'u.id');
2114             } else {
2115                 $allusers = groups_get_members($groupid);
2116             }
2117             foreach ($allusers as $user) {
2118                 if ($this->get_submission_group($user->id)) {
2119                     $members[] = $user;
2120                 }
2121             }
2122         } else {
2123             $allusers = $this->list_participants(null, $onlyids);
2124             foreach ($allusers as $user) {
2125                 if ($this->get_submission_group($user->id) == null) {
2126                     $members[] = $user;
2127                 }
2128             }
2129         }
2130         // Exclude suspended users, if user can't see them.
2131         if ($excludesuspended || !has_capability('moodle/course:viewsuspendedusers', $this->context)) {
2132             foreach ($members as $key => $member) {
2133                 if (!$this->is_active_user($member->id)) {
2134                     unset($members[$key]);
2135                 }
2136             }
2137         }
2139         return $members;
2140     }
2142     /**
2143      * Get a list of the users in the same group as this user that have not submitted the assignment.
2144      *
2145      * @param int $groupid The id of the group whose members we want or 0 for the default group
2146      * @param bool $onlyids Whether to retrieve only the user id's
2147      * @return array The users (possibly id's only)
2148      */
2149     public function get_submission_group_members_who_have_not_submitted($groupid, $onlyids) {
2150         $instance = $this->get_instance();
2151         if (!$instance->teamsubmission || !$instance->requireallteammemberssubmit) {
2152             return array();
2153         }
2154         $members = $this->get_submission_group_members($groupid, $onlyids);
2156         foreach ($members as $id => $member) {
2157             $submission = $this->get_user_submission($member->id, false);
2158             if ($submission && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
2159                 unset($members[$id]);
2160             } else {
2161                 if ($this->is_blind_marking()) {
2162                     $members[$id]->alias = get_string('hiddenuser', 'assign') .
2163                                            $this->get_uniqueid_for_user($id);
2164                 }
2165             }
2166         }
2167         return $members;
2168     }
2170     /**
2171      * Load the group submission object for a particular user, optionally creating it if required.
2172      *
2173      * @param int $userid The id of the user whose submission we want
2174      * @param int $groupid The id of the group for this user - may be 0 in which
2175      *                     case it is determined from the userid.
2176      * @param bool $create If set to true a new submission object will be created in the database
2177      *                     with the status set to "new".
2178      * @param int $attemptnumber - -1 means the latest attempt
2179      * @return stdClass The submission
2180      */
2181     public function get_group_submission($userid, $groupid, $create, $attemptnumber=-1) {
2182         global $DB;
2184         if ($groupid == 0) {
2185             $group = $this->get_submission_group($userid);
2186             if ($group) {
2187                 $groupid = $group->id;
2188             }
2189         }
2191         // Now get the group submission.
2192         $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
2193         if ($attemptnumber >= 0) {
2194             $params['attemptnumber'] = $attemptnumber;
2195         }
2197         // Only return the row with the highest attemptnumber.
2198         $submission = null;
2199         $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', '*', 0, 1);
2200         if ($submissions) {
2201             $submission = reset($submissions);
2202         }
2204         if ($submission) {
2205             return $submission;
2206         }
2207         if ($create) {
2208             $submission = new stdClass();
2209             $submission->assignment = $this->get_instance()->id;
2210             $submission->userid = 0;
2211             $submission->groupid = $groupid;
2212             $submission->timecreated = time();
2213             $submission->timemodified = $submission->timecreated;
2214             if ($attemptnumber >= 0) {
2215                 $submission->attemptnumber = $attemptnumber;
2216             } else {
2217                 $submission->attemptnumber = 0;
2218             }
2219             // Work out if this is the latest submission.
2220             $submission->latest = 0;
2221             $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
2222             if ($attemptnumber == -1) {
2223                 // This is a new submission so it must be the latest.
2224                 $submission->latest = 1;
2225             } else {
2226                 // We need to work this out.
2227                 $result = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', 'attemptnumber', 0, 1);
2228                 if ($result) {
2229                     $latestsubmission = reset($result);
2230                 }
2231                 if (!$latestsubmission || ($attemptnumber == $latestsubmission->attemptnumber)) {
2232                     $submission->latest = 1;
2233                 }
2234             }
2235             if ($submission->latest) {
2236                 // This is the case when we need to set latest to 0 for all the other attempts.
2237                 $DB->set_field('assign_submission', 'latest', 0, $params);
2238             }
2239             $submission->status = ASSIGN_SUBMISSION_STATUS_NEW;
2240             $sid = $DB->insert_record('assign_submission', $submission);
2241             return $DB->get_record('assign_submission', array('id' => $sid));
2242         }
2243         return false;
2244     }
2246     /**
2247      * View a summary listing of all assignments in the current course.
2248      *
2249      * @return string
2250      */
2251     private function view_course_index() {
2252         global $USER;
2254         $o = '';
2256         $course = $this->get_course();
2257         $strplural = get_string('modulenameplural', 'assign');
2259         if (!$cms = get_coursemodules_in_course('assign', $course->id, 'm.duedate')) {
2260             $o .= $this->get_renderer()->notification(get_string('thereareno', 'moodle', $strplural));
2261             $o .= $this->get_renderer()->continue_button(new moodle_url('/course/view.php', array('id' => $course->id)));
2262             return $o;
2263         }
2265         $strsectionname = '';
2266         $usesections = course_format_uses_sections($course->format);
2267         $modinfo = get_fast_modinfo($course);
2269         if ($usesections) {
2270             $strsectionname = get_string('sectionname', 'format_'.$course->format);
2271             $sections = $modinfo->get_section_info_all();
2272         }
2273         $courseindexsummary = new assign_course_index_summary($usesections, $strsectionname);
2275         $timenow = time();
2277         $currentsection = '';
2278         foreach ($modinfo->instances['assign'] as $cm) {
2279             if (!$cm->uservisible) {
2280                 continue;
2281             }
2283             $timedue = $cms[$cm->id]->duedate;
2285             $sectionname = '';
2286             if ($usesections && $cm->sectionnum) {
2287                 $sectionname = get_section_name($course, $sections[$cm->sectionnum]);
2288             }
2290             $submitted = '';
2291             $context = context_module::instance($cm->id);
2293             $assignment = new assign($context, $cm, $course);
2295             if (has_capability('mod/assign:grade', $context)) {
2296                 $submitted = $assignment->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED);
2298             } else if (has_capability('mod/assign:submit', $context)) {
2299                 $usersubmission = $assignment->get_user_submission($USER->id, false);
2301                 if (!empty($usersubmission->status)) {
2302                     $submitted = get_string('submissionstatus_' . $usersubmission->status, 'assign');
2303                 } else {
2304                     $submitted = get_string('submissionstatus_', 'assign');
2305                 }
2306             }
2307             $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $cm->instance, $USER->id);
2308             if (isset($gradinginfo->items[0]->grades[$USER->id]) &&
2309                     !$gradinginfo->items[0]->grades[$USER->id]->hidden ) {
2310                 $grade = $gradinginfo->items[0]->grades[$USER->id]->str_grade;
2311             } else {
2312                 $grade = '-';
2313             }
2315             $courseindexsummary->add_assign_info($cm->id, $cm->name, $sectionname, $timedue, $submitted, $grade);
2317         }
2319         $o .= $this->get_renderer()->render($courseindexsummary);
2320         $o .= $this->view_footer();
2322         return $o;
2323     }
2325     /**
2326      * View a page rendered by a plugin.
2327      *
2328      * Uses url parameters 'pluginaction', 'pluginsubtype', 'plugin', and 'id'.
2329      *
2330      * @return string
2331      */
2332     protected function view_plugin_page() {
2333         global $USER;
2335         $o = '';
2337         $pluginsubtype = required_param('pluginsubtype', PARAM_ALPHA);
2338         $plugintype = required_param('plugin', PARAM_TEXT);
2339         $pluginaction = required_param('pluginaction', PARAM_ALPHA);
2341         $plugin = $this->get_plugin_by_type($pluginsubtype, $plugintype);
2342         if (!$plugin) {
2343             print_error('invalidformdata', '');
2344             return;
2345         }
2347         $o .= $plugin->view_page($pluginaction);
2349         return $o;
2350     }
2353     /**
2354      * This is used for team assignments to get the group for the specified user.
2355      * If the user is a member of multiple or no groups this will return false
2356      *
2357      * @param int $userid The id of the user whose submission we want
2358      * @return mixed The group or false
2359      */
2360     public function get_submission_group($userid) {
2362         if (isset($this->usersubmissiongroups[$userid])) {
2363             return $this->usersubmissiongroups[$userid];
2364         }
2366         $groups = $this->get_all_groups($userid);
2367         if (count($groups) != 1) {
2368             $return = false;
2369         } else {
2370             $return = array_pop($groups);
2371         }
2373         // Cache the user submission group.
2374         $this->usersubmissiongroups[$userid] = $return;
2376         return $return;
2377     }
2379     /**
2380      * Gets all groups the user is a member of.
2381      *
2382      * @param int $userid Teh id of the user who's groups we are checking
2383      * @return array The group objects
2384      */
2385     public function get_all_groups($userid) {
2386         if (isset($this->usergroups[$userid])) {
2387             return $this->usergroups[$userid];
2388         }
2390         $grouping = $this->get_instance()->teamsubmissiongroupingid;
2391         $return = groups_get_all_groups($this->get_course()->id, $userid, $grouping);
2393         $this->usergroups[$userid] = $return;
2395         return $return;
2396     }
2399     /**
2400      * Display the submission that is used by a plugin.
2401      *
2402      * Uses url parameters 'sid', 'gid' and 'plugin'.
2403      *
2404      * @param string $pluginsubtype
2405      * @return string
2406      */
2407     protected function view_plugin_content($pluginsubtype) {
2408         $o = '';
2410         $submissionid = optional_param('sid', 0, PARAM_INT);
2411         $gradeid = optional_param('gid', 0, PARAM_INT);
2412         $plugintype = required_param('plugin', PARAM_TEXT);
2413         $item = null;
2414         if ($pluginsubtype == 'assignsubmission') {
2415             $plugin = $this->get_submission_plugin_by_type($plugintype);
2416             if ($submissionid <= 0) {
2417                 throw new coding_exception('Submission id should not be 0');
2418             }
2419             $item = $this->get_submission($submissionid);
2421             // Check permissions.
2422             $this->require_view_submission($item->userid);
2423             $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
2424                                                               $this->get_context(),
2425                                                               $this->show_intro(),
2426                                                               $this->get_course_module()->id,
2427                                                               $plugin->get_name()));
2428             $o .= $this->get_renderer()->render(new assign_submission_plugin_submission($plugin,
2429                                                               $item,
2430                                                               assign_submission_plugin_submission::FULL,
2431                                                               $this->get_course_module()->id,
2432                                                               $this->get_return_action(),
2433                                                               $this->get_return_params()));
2435             // Trigger event for viewing a submission.
2436             \mod_assign\event\submission_viewed::create_from_submission($this, $item)->trigger();
2438         } else {
2439             $plugin = $this->get_feedback_plugin_by_type($plugintype);
2440             if ($gradeid <= 0) {
2441                 throw new coding_exception('Grade id should not be 0');
2442             }
2443             $item = $this->get_grade($gradeid);
2444             // Check permissions.
2445             $this->require_view_submission($item->userid);
2446             $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
2447                                                               $this->get_context(),
2448                                                               $this->show_intro(),
2449                                                               $this->get_course_module()->id,
2450                                                               $plugin->get_name()));
2451             $o .= $this->get_renderer()->render(new assign_feedback_plugin_feedback($plugin,
2452                                                               $item,
2453                                                               assign_feedback_plugin_feedback::FULL,
2454                                                               $this->get_course_module()->id,
2455                                                               $this->get_return_action(),
2456                                                               $this->get_return_params()));
2458             // Trigger event for viewing feedback.
2459             \mod_assign\event\feedback_viewed::create_from_grade($this, $item)->trigger();
2460         }
2462         $o .= $this->view_return_links();
2464         $o .= $this->view_footer();
2466         return $o;
2467     }
2469     /**
2470      * Rewrite plugin file urls so they resolve correctly in an exported zip.
2471      *
2472      * @param string $text - The replacement text
2473      * @param stdClass $user - The user record
2474      * @param assign_plugin $plugin - The assignment plugin
2475      */
2476     public function download_rewrite_pluginfile_urls($text, $user, $plugin) {
2477         $groupmode = groups_get_activity_groupmode($this->get_course_module());
2478         $groupname = '';
2479         if ($groupmode) {
2480             $groupid = groups_get_activity_group($this->get_course_module(), true);
2481             $groupname = groups_get_group_name($groupid).'-';
2482         }
2484         if ($this->is_blind_marking()) {
2485             $prefix = $groupname . get_string('participant', 'assign');
2486             $prefix = str_replace('_', ' ', $prefix);
2487             $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($user->id) . '_');
2488         } else {
2489             $prefix = $groupname . fullname($user);
2490             $prefix = str_replace('_', ' ', $prefix);
2491             $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($user->id) . '_');
2492         }
2494         $subtype = $plugin->get_subtype();
2495         $type = $plugin->get_type();
2496         $prefix = $prefix . $subtype . '_' . $type . '_';
2498         $result = str_replace('@@PLUGINFILE@@/', $prefix, $text);
2500         return $result;
2501     }
2503     /**
2504      * Render the content in editor that is often used by plugin.
2505      *
2506      * @param string $filearea
2507      * @param int  $submissionid
2508      * @param string $plugintype
2509      * @param string $editor
2510      * @param string $component
2511      * @return string
2512      */
2513     public function render_editor_content($filearea, $submissionid, $plugintype, $editor, $component) {
2514         global $CFG;
2516         $result = '';
2518         $plugin = $this->get_submission_plugin_by_type($plugintype);
2520         $text = $plugin->get_editor_text($editor, $submissionid);
2521         $format = $plugin->get_editor_format($editor, $submissionid);
2523         $finaltext = file_rewrite_pluginfile_urls($text,
2524                                                   'pluginfile.php',
2525                                                   $this->get_context()->id,
2526                                                   $component,
2527                                                   $filearea,
2528                                                   $submissionid);
2529         $params = array('overflowdiv' => true, 'context' => $this->get_context());
2530         $result .= format_text($finaltext, $format, $params);
2532         if ($CFG->enableportfolios && has_capability('mod/assign:exportownsubmission', $this->context)) {
2533             require_once($CFG->libdir . '/portfoliolib.php');
2535             $button = new portfolio_add_button();
2536             $portfolioparams = array('cmid' => $this->get_course_module()->id,
2537                                      'sid' => $submissionid,
2538                                      'plugin' => $plugintype,
2539                                      'editor' => $editor,
2540                                      'area'=>$filearea);
2541             $button->set_callback_options('assign_portfolio_caller', $portfolioparams, 'mod_assign');
2542             $fs = get_file_storage();
2544             if ($files = $fs->get_area_files($this->context->id,
2545                                              $component,
2546                                              $filearea,
2547                                              $submissionid,
2548                                              'timemodified',
2549                                              false)) {
2550                 $button->set_formats(PORTFOLIO_FORMAT_RICHHTML);
2551             } else {
2552                 $button->set_formats(PORTFOLIO_FORMAT_PLAINHTML);
2553             }
2554             $result .= $button->to_html();
2555         }
2556         return $result;
2557     }
2559     /**
2560      * Display a continue page after grading.
2561      *
2562      * @param string $message - The message to display.
2563      * @return string
2564      */
2565     protected function view_savegrading_result($message) {
2566         $o = '';
2567         $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
2568                                                       $this->get_context(),
2569                                                       $this->show_intro(),
2570                                                       $this->get_course_module()->id,
2571                                                       get_string('savegradingresult', 'assign')));
2572         $gradingresult = new assign_gradingmessage(get_string('savegradingresult', 'assign'),
2573                                                    $message,
2574                                                    $this->get_course_module()->id);
2575         $o .= $this->get_renderer()->render($gradingresult);
2576         $o .= $this->view_footer();
2577         return $o;
2578     }
2579     /**
2580      * Display a continue page after quickgrading.
2581      *
2582      * @param string $message - The message to display.
2583      * @return string
2584      */
2585     protected function view_quickgrading_result($message) {
2586         $o = '';
2587         $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
2588                                                       $this->get_context(),
2589                                                       $this->show_intro(),
2590                                                       $this->get_course_module()->id,
2591                                                       get_string('quickgradingresult', 'assign')));
2592         $lastpage = optional_param('lastpage', null, PARAM_INT);
2593         $gradingresult = new assign_gradingmessage(get_string('quickgradingresult', 'assign'),
2594                                                    $message,
2595                                                    $this->get_course_module()->id,
2596                                                    false,
2597                                                    $lastpage);
2598         $o .= $this->get_renderer()->render($gradingresult);
2599         $o .= $this->view_footer();
2600         return $o;
2601     }
2603     /**
2604      * Display the page footer.
2605      *
2606      * @return string
2607      */
2608     protected function view_footer() {
2609         // When viewing the footer during PHPUNIT tests a set_state error is thrown.
2610         if (!PHPUNIT_TEST) {
2611             return $this->get_renderer()->render_footer();
2612         }
2614         return '';
2615     }
2617     /**
2618      * Throw an error if the permissions to view this users submission are missing.
2619      *
2620      * @throws required_capability_exception
2621      * @return none
2622      */
2623     public function require_view_submission($userid) {
2624         if (!$this->can_view_submission($userid)) {
2625             throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', '');
2626         }
2627     }
2629     /**
2630      * Throw an error if the permissions to view grades in this assignment are missing.
2631      *
2632      * @throws required_capability_exception
2633      * @return none
2634      */
2635     public function require_view_grades() {
2636         if (!$this->can_view_grades()) {
2637             throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', '');
2638         }
2639     }
2641     /**
2642      * Does this user have view grade or grade permission for this assignment?
2643      *
2644      * @return bool
2645      */
2646     public function can_view_grades() {
2647         // Permissions check.
2648         if (!has_any_capability(array('mod/assign:viewgrades', 'mod/assign:grade'), $this->context)) {
2649             return false;
2650         }
2652         return true;
2653     }
2655     /**
2656      * Does this user have grade permission for this assignment?
2657      *
2658      * @return bool
2659      */
2660     public function can_grade() {
2661         // Permissions check.
2662         if (!has_capability('mod/assign:grade', $this->context)) {
2663             return false;
2664         }
2666         return true;
2667     }
2669     /**
2670      * Download a zip file of all assignment submissions.
2671      *
2672      * @return string - If an error occurs, this will contain the error page.
2673      */
2674     protected function download_submissions() {
2675         global $CFG, $DB;
2677         // More efficient to load this here.
2678         require_once($CFG->libdir.'/filelib.php');
2680         // Increase the server timeout to handle the creation and sending of large zip files.
2681         core_php_time_limit::raise();
2683         $this->require_view_grades();
2685         // Load all users with submit.
2686         $students = get_enrolled_users($this->context, "mod/assign:submit", null, 'u.*', null, null, null,
2687                         $this->show_only_active_users());
2689         // Build a list of files to zip.
2690         $filesforzipping = array();
2691         $fs = get_file_storage();
2693         $groupmode = groups_get_activity_groupmode($this->get_course_module());
2694         // All users.
2695         $groupid = 0;
2696         $groupname = '';
2697         if ($groupmode) {
2698             $groupid = groups_get_activity_group($this->get_course_module(), true);
2699             $groupname = groups_get_group_name($groupid).'-';
2700         }
2702         // Construct the zip file name.
2703         $filename = clean_filename($this->get_course()->shortname . '-' .
2704                                    $this->get_instance()->name . '-' .
2705                                    $groupname.$this->get_course_module()->id . '.zip');
2707         // Get all the files for each student.
2708         foreach ($students as $student) {
2709             $userid = $student->id;
2711             if ((groups_is_member($groupid, $userid) or !$groupmode or !$groupid)) {
2712                 // Get the plugins to add their own files to the zip.
2714                 $submissiongroup = false;
2715                 $groupname = '';
2716                 if ($this->get_instance()->teamsubmission) {
2717                     $submission = $this->get_group_submission($userid, 0, false);
2718                     $submissiongroup = $this->get_submission_group($userid);
2719                     if ($submissiongroup) {
2720                         $groupname = $submissiongroup->name . '-';
2721                     } else {
2722                         $groupname = get_string('defaultteam', 'assign') . '-';
2723                     }
2724                 } else {
2725                     $submission = $this->get_user_submission($userid, false);
2726                 }
2728                 if ($this->is_blind_marking()) {
2729                     $prefix = str_replace('_', ' ', $groupname . get_string('participant', 'assign'));
2730                     $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($userid) . '_');
2731                 } else {
2732                     $prefix = str_replace('_', ' ', $groupname . fullname($student));
2733                     $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($userid) . '_');
2734                 }
2736                 if ($submission) {
2737                     foreach ($this->submissionplugins as $plugin) {
2738                         if ($plugin->is_enabled() && $plugin->is_visible()) {
2739                             $pluginfiles = $plugin->get_files($submission, $student);
2740                             foreach ($pluginfiles as $zipfilename => $file) {
2741                                 $subtype = $plugin->get_subtype();
2742                                 $type = $plugin->get_type();
2743                                 $prefixedfilename = clean_filename($prefix .
2744                                                                    $subtype .
2745                                                                    '_' .
2746                                                                    $type .
2747                                                                    '_' .
2748                                                                    $zipfilename);
2749                                 $filesforzipping[$prefixedfilename] = $file;
2750                             }
2751                         }
2752                     }
2753                 }
2754             }
2755         }
2756         $result = '';
2757         if (count($filesforzipping) == 0) {
2758             $header = new assign_header($this->get_instance(),
2759                                         $this->get_context(),
2760                                         '',
2761                                         $this->get_course_module()->id,
2762                                         get_string('downloadall', 'assign'));
2763             $result .= $this->get_renderer()->render($header);
2764             $result .= $this->get_renderer()->notification(get_string('nosubmission', 'assign'));
2765             $url = new moodle_url('/mod/assign/view.php', array('id'=>$this->get_course_module()->id,
2766                                                                     'action'=>'grading'));
2767             $result .= $this->get_renderer()->continue_button($url);
2768             $result .= $this->view_footer();
2769         } else if ($zipfile = $this->pack_files($filesforzipping)) {
2770             \mod_assign\event\all_submissions_downloaded::create_from_assign($this)->trigger();
2771             // Send file and delete after sending.
2772             send_temp_file($zipfile, $filename);
2773             // We will not get here - send_temp_file calls exit.
2774         }
2775         return $result;
2776     }
2778     /**
2779      * Util function to add a message to the log.
2780      *
2781      * @deprecated since 2.7 - Use new events system instead.
2782      *             (see http://docs.moodle.org/dev/Migrating_logging_calls_in_plugins).
2783      *
2784      * @param string $action The current action
2785      * @param string $info A detailed description of the change. But no more than 255 characters.
2786      * @param string $url The url to the assign module instance.
2787      * @param bool $return If true, returns the arguments, else adds to log. The purpose of this is to
2788      *                     retrieve the arguments to use them with the new event system (Event 2).
2789      * @return void|array
2790      */
2791     public function add_to_log($action = '', $info = '', $url='', $return = false) {
2792         global $USER;
2794         $fullurl = 'view.php?id=' . $this->get_course_module()->id;
2795         if ($url != '') {
2796             $fullurl .= '&' . $url;
2797         }
2799         $args = array(
2800             $this->get_course()->id,
2801             'assign',
2802             $action,
2803             $fullurl,
2804             $info,
2805             $this->get_course_module()->id
2806         );
2808         if ($return) {
2809             // We only need to call debugging when returning a value. This is because the call to
2810             // call_user_func_array('add_to_log', $args) will trigger a debugging message of it's own.
2811             debugging('The mod_assign add_to_log() function is now deprecated.', DEBUG_DEVELOPER);
2812             return $args;
2813         }
2814         call_user_func_array('add_to_log', $args);
2815     }
2817     /**
2818      * Lazy load the page renderer and expose the renderer to plugins.
2819      *
2820      * @return assign_renderer
2821      */
2822     public function get_renderer() {
2823         global $PAGE;
2824         if ($this->output) {
2825             return $this->output;
2826         }
2827         $this->output = $PAGE->get_renderer('mod_assign');
2828         return $this->output;
2829     }
2831     /**
2832      * Load the submission object for a particular user, optionally creating it if required.
2833      *
2834      * For team assignments there are 2 submissions - the student submission and the team submission
2835      * All files are associated with the team submission but the status of the students contribution is
2836      * recorded separately.
2837      *
2838      * @param int $userid The id of the user whose submission we want or 0 in which case USER->id is used
2839      * @param bool $create If set to true a new submission object will be created in the database with the status set to "new".
2840      * @param int $attemptnumber - -1 means the latest attempt
2841      * @return stdClass The submission
2842      */
2843     public function get_user_submission($userid, $create, $attemptnumber=-1) {
2844         global $DB, $USER;
2846         if (!$userid) {
2847             $userid = $USER->id;
2848         }
2849         // If the userid is not null then use userid.
2850         $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid, 'groupid'=>0);
2851         if ($attemptnumber >= 0) {
2852             $params['attemptnumber'] = $attemptnumber;
2853         }
2855         // Only return the row with the highest attemptnumber.
2856         $submission = null;
2857         $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', '*', 0, 1);
2858         if ($submissions) {
2859             $submission = reset($submissions);
2860         }
2862         if ($submission) {
2863             return $submission;
2864         }
2865         if ($create) {
2866             $submission = new stdClass();
2867             $submission->assignment   = $this->get_instance()->id;
2868             $submission->userid       = $userid;
2869             $submission->timecreated = time();
2870             $submission->timemodified = $submission->timecreated;
2871             $submission->status = ASSIGN_SUBMISSION_STATUS_NEW;
2872             if ($attemptnumber >= 0) {
2873                 $submission->attemptnumber = $attemptnumber;
2874             } else {
2875                 $submission->attemptnumber = 0;
2876             }
2877             // Work out if this is the latest submission.
2878             $submission->latest = 0;
2879             $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid, 'groupid'=>0);
2880             if ($attemptnumber == -1) {
2881                 // This is a new submission so it must be the latest.
2882                 $submission->latest = 1;
2883             } else {
2884                 // We need to work this out.
2885                 $result = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', 'attemptnumber', 0, 1);
2886                 $latestsubmission = null;
2887                 if ($result) {
2888                     $latestsubmission = reset($result);
2889                 }
2890                 if (empty($latestsubmission) || ($attemptnumber > $latestsubmission->attemptnumber)) {
2891                     $submission->latest = 1;
2892                 }
2893             }
2894             if ($submission->latest) {
2895                 // This is the case when we need to set latest to 0 for all the other attempts.
2896                 $DB->set_field('assign_submission', 'latest', 0, $params);
2897             }
2898             $sid = $DB->insert_record('assign_submission', $submission);
2899             return $DB->get_record('assign_submission', array('id' => $sid));
2900         }
2901         return false;
2902     }
2904     /**
2905      * Load the submission object from it's id.
2906      *
2907      * @param int $submissionid The id of the submission we want
2908      * @return stdClass The submission
2909      */
2910     protected function get_submission($submissionid) {
2911         global $DB;
2913         $params = array('assignment'=>$this->get_instance()->id, 'id'=>$submissionid);
2914         return $DB->get_record('assign_submission', $params, '*', MUST_EXIST);
2915     }
2917     /**
2918      * This will retrieve a user flags object from the db optionally creating it if required.
2919      * The user flags was split from the user_grades table in 2.5.
2920      *
2921      * @param int $userid The user we are getting the flags for.
2922      * @param bool $create If true the flags record will be created if it does not exist
2923      * @return stdClass The flags record
2924      */
2925     public function get_user_flags($userid, $create) {
2926         global $DB, $USER;
2928         // If the userid is not null then use userid.
2929         if (!$userid) {
2930             $userid = $USER->id;
2931         }
2933         $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
2935         $flags = $DB->get_record('assign_user_flags', $params);
2937         if ($flags) {
2938             return $flags;
2939         }
2940         if ($create) {
2941             $flags = new stdClass();
2942             $flags->assignment = $this->get_instance()->id;
2943             $flags->userid = $userid;
2944             $flags->locked = 0;
2945             $flags->extensionduedate = 0;
2946             $flags->workflowstate = '';
2947             $flags->allocatedmarker = 0;
2949             // The mailed flag can be one of 3 values: 0 is unsent, 1 is sent and 2 is do not send yet.
2950             // This is because students only want to be notified about certain types of update (grades and feedback).
2951             $flags->mailed = 2;
2953             $fid = $DB->insert_record('assign_user_flags', $flags);
2954             $flags->id = $fid;
2955             return $flags;
2956         }
2957         return false;
2958     }
2960     /**
2961      * This will retrieve a grade object from the db, optionally creating it if required.
2962      *
2963      * @param int $userid The user we are grading
2964      * @param bool $create If true the grade will be created if it does not exist
2965      * @param int $attemptnumber The attempt number to retrieve the grade for. -1 means the latest submission.
2966      * @return stdClass The grade record
2967      */
2968     public function get_user_grade($userid, $create, $attemptnumber=-1) {
2969         global $DB, $USER;
2971         // If the userid is not null then use userid.
2972         if (!$userid) {
2973             $userid = $USER->id;
2974         }
2975         $submission = null;
2977         $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
2978         if ($attemptnumber < 0 || $create) {
2979             // Make sure this grade matches the latest submission attempt.
2980             if ($this->get_instance()->teamsubmission) {
2981                 $submission = $this->get_group_submission($userid, 0, true);
2982             } else {
2983                 $submission = $this->get_user_submission($userid, true);
2984             }
2985             if ($submission) {
2986                 $attemptnumber = $submission->attemptnumber;
2987             }
2988         }
2990         if ($attemptnumber >= 0) {
2991             $params['attemptnumber'] = $attemptnumber;
2992         }
2994         $grades = $DB->get_records('assign_grades', $params, 'attemptnumber DESC', '*', 0, 1);
2996         if ($grades) {
2997             return reset($grades);
2998         }
2999         if ($create) {
3000             $grade = new stdClass();
3001             $grade->assignment   = $this->get_instance()->id;
3002             $grade->userid       = $userid;
3003             $grade->timecreated = time();
3004             // If we are "auto-creating" a grade - and there is a submission
3005             // the new grade should not have a more recent timemodified value
3006             // than the submission.
3007             if ($submission) {
3008                 $grade->timemodified = $submission->timemodified;
3009             } else {
3010                 $grade->timemodified = $grade->timecreated;
3011             }
3012             $grade->grade = -1;
3013             $grade->grader = $USER->id;
3014             if ($attemptnumber >= 0) {
3015                 $grade->attemptnumber = $attemptnumber;
3016             }
3018             $gid = $DB->insert_record('assign_grades', $grade);
3019             $grade->id = $gid;
3020             return $grade;
3021         }
3022         return false;
3023     }
3025     /**
3026      * This will retrieve a grade object from the db.
3027      *
3028      * @param int $gradeid The id of the grade
3029      * @return stdClass The grade record
3030      */
3031     protected function get_grade($gradeid) {
3032         global $DB;
3034         $params = array('assignment'=>$this->get_instance()->id, 'id'=>$gradeid);
3035         return $DB->get_record('assign_grades', $params, '*', MUST_EXIST);
3036     }
3038     /**
3039      * Print the grading page for a single user submission.
3040      *
3041      * @param moodleform $mform
3042      * @return string
3043      */
3044     protected function view_single_grade_page($mform) {
3045         global $DB, $CFG, $SESSION;
3047         $o = '';
3048         $instance = $this->get_instance();
3050         require_once($CFG->dirroot . '/mod/assign/gradeform.php');
3052         // Need submit permission to submit an assignment.
3053         require_capability('mod/assign:grade', $this->context);
3055         $header = new assign_header($instance,
3056                                     $this->get_context(),
3057                                     false,
3058                                     $this->get_course_module()->id,
3059                                     get_string('grading', 'assign'));
3060         $o .= $this->get_renderer()->render($header);
3062         // If userid is passed - we are only grading a single student.
3063         $rownum = required_param('rownum', PARAM_INT);
3064         $useridlistid = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
3065         $userid = optional_param('userid', 0, PARAM_INT);
3066         $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT);
3068         if (!$userid) {
3069             $useridlistkey = $this->get_useridlist_key($useridlistid);
3070             if (empty($SESSION->mod_assign_useridlist[$useridlistkey])) {
3071                 $SESSION->mod_assign_useridlist[$useridlistkey] = $this->get_grading_userid_list();
3072             }
3073             $useridlist = $SESSION->mod_assign_useridlist[$useridlistkey];
3074         } else {
3075             $rownum = 0;
3076             $useridlist = array($userid);
3077         }
3079         if ($rownum < 0 || $rownum > count($useridlist)) {
3080             throw new coding_exception('Row is out of bounds for the current grading table: ' . $rownum);
3081         }
3083         $last = false;
3084         $userid = $useridlist[$rownum];
3085         if ($rownum == count($useridlist) - 1) {
3086             $last = true;
3087         }
3088         // This variation on the url will link direct to this student, with no next/previous links.
3089         // The benefit is the url will be the same every time for this student, so Atto autosave drafts can match up.
3090         $returnparams = array('userid' => $userid, 'rownum' => 0, 'useridlistid' => 0);
3091         $this->register_return_link('grade', $returnparams);
3093         $user = $DB->get_record('user', array('id' => $userid));
3094         if ($user) {
3095             $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_course_context());
3096             $usersummary = new assign_user_summary($user,
3097                                                    $this->get_course()->id,
3098                                                    $viewfullnames,
3099                                                    $this->is_blind_marking(),
3100                                                    $this->get_uniqueid_for_user($user->id),
3101                                                    get_extra_user_fields($this->get_context()),
3102                                                    !$this->is_active_user($userid));
3103             $o .= $this->get_renderer()->render($usersummary);
3104         }
3105         $submission = $this->get_user_submission($userid, false, $attemptnumber);
3106         $submissiongroup = null;
3107         $teamsubmission = null;
3108         $notsubmitted = array();
3109         if ($instance->teamsubmission) {
3110             $teamsubmission = $this->get_group_submission($userid, 0, false, $attemptnumber);
3111             $submissiongroup = $this->get_submission_group($userid);
3112             $groupid = 0;
3113             if ($submissiongroup) {
3114                 $groupid = $submissiongroup->id;
3115             }
3116             $notsubmitted = $this->get_submission_group_members_who_have_not_submitted($groupid, false);
3118         }
3120         // Get the requested grade.
3121         $grade = $this->get_user_grade($userid, false, $attemptnumber);
3122         $flags = $this->get_user_flags($userid, false);
3123         if ($this->can_view_submission($userid)) {
3124             $gradelocked = ($flags && $flags->locked) || $this->grading_disabled($userid);
3125             $extensionduedate = null;
3126             if ($flags) {
3127                 $extensionduedate = $flags->extensionduedate;
3128             }
3129             $showedit = $this->submissions_open($userid) && ($this->is_any_submission_plugin_enabled());
3130             $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_course_context());
3131             $usergroups = $this->get_all_groups($user->id);
3133             $submissionstatus = new assign_submission_status($instance->allowsubmissionsfromdate,
3134                                                              $instance->alwaysshowdescription,
3135                                                              $submission,
3136                                                              $instance->teamsubmission,
3137                                                              $teamsubmission,
3138                                                              $submissiongroup,
3139                                                              $notsubmitted,
3140                                                              $this->is_any_submission_plugin_enabled(),
3141                                                              $gradelocked,
3142                                                              $this->is_graded($userid),
3143                                                              $instance->duedate,
3144                                                              $instance->cutoffdate,
3145                                                              $this->get_submission_plugins(),
3146                                                              $this->get_return_action(),
3147                                                              $this->get_return_params(),
3148                                                              $this->get_course_module()->id,
3149                                                              $this->get_course()->id,
3150                                                              assign_submission_status::GRADER_VIEW,
3151                                                              $showedit,
3152                                                              false,
3153                                                              $viewfullnames,
3154                                                              $extensionduedate,
3155                                                              $this->get_context(),
3156                                                              $this->is_blind_marking(),
3157                                                              '',
3158                                                              $instance->attemptreopenmethod,
3159                                                              $instance->maxattempts,
3160                                                              $this->get_grading_status($userid),
3161                                                              $instance->preventsubmissionnotingroup,
3162                                                              $usergroups);
3163             $o .= $this->get_renderer()->render($submissionstatus);
3164         }
3166         if ($grade) {
3167             $data = new stdClass();
3168             if ($grade->grade !== null && $grade->grade >= 0) {
3169                 $data->grade = format_float($grade->grade, 2);
3170             }
3171         } else {
3172             $data = new stdClass();
3173             $data->grade = '';
3174         }
3176         if (!empty($flags->workflowstate)) {
3177             $data->workflowstate = $flags->workflowstate;
3178         }
3179         if (!empty($flags->allocatedmarker)) {
3180             $data->allocatedmarker = $flags->allocatedmarker;
3181         }
3183         // Warning if required.
3184         $allsubmissions = $this->get_all_submissions($userid);
3186         if ($attemptnumber != -1) {
3187             $params = array('attemptnumber'=>$attemptnumber + 1,
3188                             'totalattempts'=>count($allsubmissions));
3189             $message = get_string('editingpreviousfeedbackwarning', 'assign', $params);
3190             $o .= $this->get_renderer()->notification($message);
3191         }
3193         // Now show the grading form.
3194         if (!$mform) {
3195             $pagination = array('rownum'=>$rownum,
3196                                 'useridlistid'=>$useridlistid,
3197                                 'last'=>$last,
3198                                 'userid'=>optional_param('userid', 0, PARAM_INT),
3199                                 'attemptnumber'=>$attemptnumber);
3200             $formparams = array($this, $data, $pagination);
3201             $mform = new mod_assign_grade_form(null,
3202                                                $formparams,
3203                                                'post',
3204                                                '',
3205                                                array('class'=>'gradeform'));
3206         }
3207         $o .= $this->get_renderer()->heading(get_string('grade'), 3);
3208         $o .= $this->get_renderer()->render(new assign_form('gradingform', $mform));
3210         if (count($allsubmissions) > 1 && $attemptnumber == -1) {
3211             $allgrades = $this->get_all_grades($userid);
3212             $history = new assign_attempt_history($allsubmissions,
3213                                                   $allgrades,
3214                                                   $this->get_submission_plugins(),
3215                                                   $this->get_feedback_plugins(),
3216                                                   $this->get_course_module()->id,
3217                                                   $this->get_return_action(),
3218                                                   $this->get_return_params(),
3219                                                   true,
3220                                                   $useridlistid,
3221                                                   $rownum);
3223             $o .= $this->get_renderer()->render($history);
3224         }
3226         \mod_assign\event\grading_form_viewed::create_from_user($this, $user)->trigger();
3228         $o .= $this->view_footer();
3229         return $o;
3230     }
3232     /**
3233      * Show a confirmation page to make sure they want to release student identities.
3234      *
3235      * @return string
3236      */
3237     protected function view_reveal_identities_confirm() {
3238         require_capability('mod/assign:revealidentities', $this->get_context());
3240         $o = '';
3241         $header = new assign_header($this->get_instance(),
3242                                     $this->get_context(),
3243                                     false,
3244                                     $this->get_course_module()->id);
3245         $o .= $this->get_renderer()->render($header);
3247         $urlparams = array('id'=>$this->get_course_module()->id,
3248                            'action'=>'revealidentitiesconfirm',
3249                            'sesskey'=>sesskey());
3250         $confirmurl = new moodle_url('/mod/assign/view.php', $urlparams);
3252         $urlparams = array('id'=>$this->get_course_module()->id,
3253                            'action'=>'grading');
3254         $cancelurl = new moodle_url('/mod/assign/view.php', $urlparams);
3256         $o .= $this->get_renderer()->confirm(get_string('revealidentitiesconfirm', 'assign'),
3257