MDL-60062 calendar: prevent drag and drop of override events
[moodle.git] / mod / assign / locallib.php
blobae24d0f7db8eb006ea630e96295d6ed7c0ced5bb
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.
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
20 * This class provides all the functionality for the new assign module.
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
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');
40 define('ASSIGN_FILTER_GRANTED_EXTENSION', 'granted_extension');
42 // Marker filter for grading page.
43 define('ASSIGN_MARKER_FILTER_NO_MARKER', -1);
45 // Reopen attempt methods.
46 define('ASSIGN_ATTEMPT_REOPEN_METHOD_NONE', 'none');
47 define('ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL', 'manual');
48 define('ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS', 'untilpass');
50 // Special value means allow unlimited attempts.
51 define('ASSIGN_UNLIMITED_ATTEMPTS', -1);
53 // Special value means no grade has been set.
54 define('ASSIGN_GRADE_NOT_SET', -1);
56 // Grading states.
57 define('ASSIGN_GRADING_STATUS_GRADED', 'graded');
58 define('ASSIGN_GRADING_STATUS_NOT_GRADED', 'notgraded');
60 // Marking workflow states.
61 define('ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED', 'notmarked');
62 define('ASSIGN_MARKING_WORKFLOW_STATE_INMARKING', 'inmarking');
63 define('ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW', 'readyforreview');
64 define('ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW', 'inreview');
65 define('ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE', 'readyforrelease');
66 define('ASSIGN_MARKING_WORKFLOW_STATE_RELEASED', 'released');
68 /** ASSIGN_MAX_EVENT_LENGTH = 432000 ; 5 days maximum */
69 define("ASSIGN_MAX_EVENT_LENGTH", "432000");
71 // Name of file area for intro attachments.
72 define('ASSIGN_INTROATTACHMENT_FILEAREA', 'introattachment');
74 // Event types.
75 define('ASSIGN_EVENT_TYPE_DUE', 'due');
76 define('ASSIGN_EVENT_TYPE_GRADINGDUE', 'gradingdue');
77 define('ASSIGN_EVENT_TYPE_OPEN', 'open');
78 define('ASSIGN_EVENT_TYPE_CLOSE', 'close');
80 require_once($CFG->libdir . '/accesslib.php');
81 require_once($CFG->libdir . '/formslib.php');
82 require_once($CFG->dirroot . '/repository/lib.php');
83 require_once($CFG->dirroot . '/mod/assign/mod_form.php');
84 require_once($CFG->libdir . '/gradelib.php');
85 require_once($CFG->dirroot . '/grade/grading/lib.php');
86 require_once($CFG->dirroot . '/mod/assign/feedbackplugin.php');
87 require_once($CFG->dirroot . '/mod/assign/submissionplugin.php');
88 require_once($CFG->dirroot . '/mod/assign/renderable.php');
89 require_once($CFG->dirroot . '/mod/assign/gradingtable.php');
90 require_once($CFG->libdir . '/eventslib.php');
91 require_once($CFG->libdir . '/portfolio/caller.php');
93 use \mod_assign\output\grading_app;
95 /**
96 * Standard base class for mod_assign (assignment types).
98 * @package mod_assign
99 * @copyright 2012 NetSpot {@link http://www.netspot.com.au}
100 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
102 class assign {
104 /** @var stdClass the assignment record that contains the global settings for this assign instance */
105 private $instance;
107 /** @var grade_item the grade_item record for this assign instance's primary grade item. */
108 private $gradeitem;
110 /** @var context the context of the course module for this assign instance
111 * (or just the course if we are creating a new one)
113 private $context;
115 /** @var stdClass the course this assign instance belongs to */
116 private $course;
118 /** @var stdClass the admin config for all assign instances */
119 private $adminconfig;
121 /** @var assign_renderer the custom renderer for this module */
122 private $output;
124 /** @var cm_info the course module for this assign instance */
125 private $coursemodule;
127 /** @var array cache for things like the coursemodule name or the scale menu -
128 * only lives for a single request.
130 private $cache;
132 /** @var array list of the installed submission plugins */
133 private $submissionplugins;
135 /** @var array list of the installed feedback plugins */
136 private $feedbackplugins;
138 /** @var string action to be used to return to this page
139 * (without repeating any form submissions etc).
141 private $returnaction = 'view';
143 /** @var array params to be used to return to this page */
144 private $returnparams = array();
146 /** @var string modulename prevents excessive calls to get_string */
147 private static $modulename = null;
149 /** @var string modulenameplural prevents excessive calls to get_string */
150 private static $modulenameplural = null;
152 /** @var array of marking workflow states for the current user */
153 private $markingworkflowstates = null;
155 /** @var bool whether to exclude users with inactive enrolment */
156 private $showonlyactiveenrol = null;
158 /** @var string A key used to identify userlists created by this object. */
159 private $useridlistid = null;
161 /** @var array cached list of participants for this assignment. The cache key will be group, showactive and the context id */
162 private $participants = array();
164 /** @var array cached list of user groups when team submissions are enabled. The cache key will be the user. */
165 private $usersubmissiongroups = array();
167 /** @var array cached list of user groups. The cache key will be the user. */
168 private $usergroups = array();
170 /** @var array cached list of IDs of users who share group membership with the user. The cache key will be the user. */
171 private $sharedgroupmembers = array();
174 * @var stdClass The most recent team submission. Used to determine additional attempt numbers and whether
175 * to update the gradebook.
177 private $mostrecentteamsubmission = null;
180 * Constructor for the base assign class.
182 * Note: For $coursemodule you can supply a stdclass if you like, but it
183 * will be more efficient to supply a cm_info object.
185 * @param mixed $coursemodulecontext context|null the course module context
186 * (or the course context if the coursemodule has not been
187 * created yet).
188 * @param mixed $coursemodule the current course module if it was already loaded,
189 * otherwise this class will load one from the context as required.
190 * @param mixed $course the current course if it was already loaded,
191 * otherwise this class will load one from the context as required.
193 public function __construct($coursemodulecontext, $coursemodule, $course) {
194 global $SESSION;
196 $this->context = $coursemodulecontext;
197 $this->course = $course;
199 // Ensure that $this->coursemodule is a cm_info object (or null).
200 $this->coursemodule = cm_info::create($coursemodule);
202 // Temporary cache only lives for a single request - used to reduce db lookups.
203 $this->cache = array();
205 $this->submissionplugins = $this->load_plugins('assignsubmission');
206 $this->feedbackplugins = $this->load_plugins('assignfeedback');
208 // Extra entropy is required for uniqid() to work on cygwin.
209 $this->useridlistid = clean_param(uniqid('', true), PARAM_ALPHANUM);
211 if (!isset($SESSION->mod_assign_useridlist)) {
212 $SESSION->mod_assign_useridlist = [];
217 * Set the action and parameters that can be used to return to the current page.
219 * @param string $action The action for the current page
220 * @param array $params An array of name value pairs which form the parameters
221 * to return to the current page.
222 * @return void
224 public function register_return_link($action, $params) {
225 global $PAGE;
226 $params['action'] = $action;
227 $cm = $this->get_course_module();
228 if ($cm) {
229 $currenturl = new moodle_url('/mod/assign/view.php', array('id' => $cm->id));
230 } else {
231 $currenturl = new moodle_url('/mod/assign/index.php', array('id' => $this->get_course()->id));
234 $currenturl->params($params);
235 $PAGE->set_url($currenturl);
239 * Return an action that can be used to get back to the current page.
241 * @return string action
243 public function get_return_action() {
244 global $PAGE;
246 // Web services don't set a URL, we should avoid debugging when ussing the url object.
247 if (!WS_SERVER) {
248 $params = $PAGE->url->params();
251 if (!empty($params['action'])) {
252 return $params['action'];
254 return '';
258 * Based on the current assignment settings should we display the intro.
260 * @return bool showintro
262 public function show_intro() {
263 if ($this->get_instance()->alwaysshowdescription ||
264 time() > $this->get_instance()->allowsubmissionsfromdate) {
265 return true;
267 return false;
271 * Return a list of parameters that can be used to get back to the current page.
273 * @return array params
275 public function get_return_params() {
276 global $PAGE;
278 $params = $PAGE->url->params();
279 unset($params['id']);
280 unset($params['action']);
281 return $params;
285 * Set the submitted form data.
287 * @param stdClass $data The form data (instance)
289 public function set_instance(stdClass $data) {
290 $this->instance = $data;
294 * Set the context.
296 * @param context $context The new context
298 public function set_context(context $context) {
299 $this->context = $context;
303 * Set the course data.
305 * @param stdClass $course The course data
307 public function set_course(stdClass $course) {
308 $this->course = $course;
312 * Get list of feedback plugins installed.
314 * @return array
316 public function get_feedback_plugins() {
317 return $this->feedbackplugins;
321 * Get list of submission plugins installed.
323 * @return array
325 public function get_submission_plugins() {
326 return $this->submissionplugins;
330 * Is blind marking enabled and reveal identities not set yet?
332 * @return bool
334 public function is_blind_marking() {
335 return $this->get_instance()->blindmarking && !$this->get_instance()->revealidentities;
339 * Does an assignment have submission(s) or grade(s) already?
341 * @return bool
343 public function has_submissions_or_grades() {
344 $allgrades = $this->count_grades();
345 $allsubmissions = $this->count_submissions();
346 if (($allgrades == 0) && ($allsubmissions == 0)) {
347 return false;
349 return true;
353 * Get a specific submission plugin by its type.
355 * @param string $subtype assignsubmission | assignfeedback
356 * @param string $type
357 * @return mixed assign_plugin|null
359 public function get_plugin_by_type($subtype, $type) {
360 $shortsubtype = substr($subtype, strlen('assign'));
361 $name = $shortsubtype . 'plugins';
362 if ($name != 'feedbackplugins' && $name != 'submissionplugins') {
363 return null;
365 $pluginlist = $this->$name;
366 foreach ($pluginlist as $plugin) {
367 if ($plugin->get_type() == $type) {
368 return $plugin;
371 return null;
375 * Get a feedback plugin by type.
377 * @param string $type - The type of plugin e.g comments
378 * @return mixed assign_feedback_plugin|null
380 public function get_feedback_plugin_by_type($type) {
381 return $this->get_plugin_by_type('assignfeedback', $type);
385 * Get a submission plugin by type.
387 * @param string $type - The type of plugin e.g comments
388 * @return mixed assign_submission_plugin|null
390 public function get_submission_plugin_by_type($type) {
391 return $this->get_plugin_by_type('assignsubmission', $type);
395 * Load the plugins from the sub folders under subtype.
397 * @param string $subtype - either submission or feedback
398 * @return array - The sorted list of plugins
400 public function load_plugins($subtype) {
401 global $CFG;
402 $result = array();
404 $names = core_component::get_plugin_list($subtype);
406 foreach ($names as $name => $path) {
407 if (file_exists($path . '/locallib.php')) {
408 require_once($path . '/locallib.php');
410 $shortsubtype = substr($subtype, strlen('assign'));
411 $pluginclass = 'assign_' . $shortsubtype . '_' . $name;
413 $plugin = new $pluginclass($this, $name);
415 if ($plugin instanceof assign_plugin) {
416 $idx = $plugin->get_sort_order();
417 while (array_key_exists($idx, $result)) {
418 $idx +=1;
420 $result[$idx] = $plugin;
424 ksort($result);
425 return $result;
429 * Display the assignment, used by view.php
431 * The assignment is displayed differently depending on your role,
432 * the settings for the assignment and the status of the assignment.
434 * @param string $action The current action if any.
435 * @param array $args Optional arguments to pass to the view (instead of getting them from GET and POST).
436 * @return string - The page output.
438 public function view($action='', $args = array()) {
439 global $PAGE;
441 $o = '';
442 $mform = null;
443 $notices = array();
444 $nextpageparams = array();
446 if (!empty($this->get_course_module()->id)) {
447 $nextpageparams['id'] = $this->get_course_module()->id;
450 // Handle form submissions first.
451 if ($action == 'savesubmission') {
452 $action = 'editsubmission';
453 if ($this->process_save_submission($mform, $notices)) {
454 $action = 'redirect';
455 $nextpageparams['action'] = 'view';
457 } else if ($action == 'editprevioussubmission') {
458 $action = 'editsubmission';
459 if ($this->process_copy_previous_attempt($notices)) {
460 $action = 'redirect';
461 $nextpageparams['action'] = 'editsubmission';
463 } else if ($action == 'lock') {
464 $this->process_lock_submission();
465 $action = 'redirect';
466 $nextpageparams['action'] = 'grading';
467 } else if ($action == 'addattempt') {
468 $this->process_add_attempt(required_param('userid', PARAM_INT));
469 $action = 'redirect';
470 $nextpageparams['action'] = 'grading';
471 } else if ($action == 'reverttodraft') {
472 $this->process_revert_to_draft();
473 $action = 'redirect';
474 $nextpageparams['action'] = 'grading';
475 } else if ($action == 'unlock') {
476 $this->process_unlock_submission();
477 $action = 'redirect';
478 $nextpageparams['action'] = 'grading';
479 } else if ($action == 'setbatchmarkingworkflowstate') {
480 $this->process_set_batch_marking_workflow_state();
481 $action = 'redirect';
482 $nextpageparams['action'] = 'grading';
483 } else if ($action == 'setbatchmarkingallocation') {
484 $this->process_set_batch_marking_allocation();
485 $action = 'redirect';
486 $nextpageparams['action'] = 'grading';
487 } else if ($action == 'confirmsubmit') {
488 $action = 'submit';
489 if ($this->process_submit_for_grading($mform, $notices)) {
490 $action = 'redirect';
491 $nextpageparams['action'] = 'view';
492 } else if ($notices) {
493 $action = 'viewsubmitforgradingerror';
495 } else if ($action == 'submitotherforgrading') {
496 if ($this->process_submit_other_for_grading($mform, $notices)) {
497 $action = 'redirect';
498 $nextpageparams['action'] = 'grading';
499 } else {
500 $action = 'viewsubmitforgradingerror';
502 } else if ($action == 'gradingbatchoperation') {
503 $action = $this->process_grading_batch_operation($mform);
504 if ($action == 'grading') {
505 $action = 'redirect';
506 $nextpageparams['action'] = 'grading';
508 } else if ($action == 'submitgrade') {
509 if (optional_param('saveandshownext', null, PARAM_RAW)) {
510 // Save and show next.
511 $action = 'grade';
512 if ($this->process_save_grade($mform)) {
513 $action = 'redirect';
514 $nextpageparams['action'] = 'grade';
515 $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) + 1;
516 $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
518 } else if (optional_param('nosaveandprevious', null, PARAM_RAW)) {
519 $action = 'redirect';
520 $nextpageparams['action'] = 'grade';
521 $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) - 1;
522 $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
523 } else if (optional_param('nosaveandnext', null, PARAM_RAW)) {
524 $action = 'redirect';
525 $nextpageparams['action'] = 'grade';
526 $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) + 1;
527 $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
528 } else if (optional_param('savegrade', null, PARAM_RAW)) {
529 // Save changes button.
530 $action = 'grade';
531 if ($this->process_save_grade($mform)) {
532 $action = 'redirect';
533 $nextpageparams['action'] = 'savegradingresult';
535 } else {
536 // Cancel button.
537 $action = 'redirect';
538 $nextpageparams['action'] = 'grading';
540 } else if ($action == 'quickgrade') {
541 $message = $this->process_save_quick_grades();
542 $action = 'quickgradingresult';
543 } else if ($action == 'saveoptions') {
544 $this->process_save_grading_options();
545 $action = 'redirect';
546 $nextpageparams['action'] = 'grading';
547 } else if ($action == 'saveextension') {
548 $action = 'grantextension';
549 if ($this->process_save_extension($mform)) {
550 $action = 'redirect';
551 $nextpageparams['action'] = 'grading';
553 } else if ($action == 'revealidentitiesconfirm') {
554 $this->process_reveal_identities();
555 $action = 'redirect';
556 $nextpageparams['action'] = 'grading';
559 $returnparams = array('rownum'=>optional_param('rownum', 0, PARAM_INT),
560 'useridlistid' => optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM));
561 $this->register_return_link($action, $returnparams);
563 // Include any page action as part of the body tag CSS id.
564 if (!empty($action)) {
565 $PAGE->set_pagetype('mod-assign-' . $action);
567 // Now show the right view page.
568 if ($action == 'redirect') {
569 $nextpageurl = new moodle_url('/mod/assign/view.php', $nextpageparams);
570 redirect($nextpageurl);
571 return;
572 } else if ($action == 'savegradingresult') {
573 $message = get_string('gradingchangessaved', 'assign');
574 $o .= $this->view_savegrading_result($message);
575 } else if ($action == 'quickgradingresult') {
576 $mform = null;
577 $o .= $this->view_quickgrading_result($message);
578 } else if ($action == 'gradingpanel') {
579 $o .= $this->view_single_grading_panel($args);
580 } else if ($action == 'grade') {
581 $o .= $this->view_single_grade_page($mform);
582 } else if ($action == 'viewpluginassignfeedback') {
583 $o .= $this->view_plugin_content('assignfeedback');
584 } else if ($action == 'viewpluginassignsubmission') {
585 $o .= $this->view_plugin_content('assignsubmission');
586 } else if ($action == 'editsubmission') {
587 $o .= $this->view_edit_submission_page($mform, $notices);
588 } else if ($action == 'grader') {
589 $o .= $this->view_grader();
590 } else if ($action == 'grading') {
591 $o .= $this->view_grading_page();
592 } else if ($action == 'downloadall') {
593 $o .= $this->download_submissions();
594 } else if ($action == 'submit') {
595 $o .= $this->check_submit_for_grading($mform);
596 } else if ($action == 'grantextension') {
597 $o .= $this->view_grant_extension($mform);
598 } else if ($action == 'revealidentities') {
599 $o .= $this->view_reveal_identities_confirm($mform);
600 } else if ($action == 'plugingradingbatchoperation') {
601 $o .= $this->view_plugin_grading_batch_operation($mform);
602 } else if ($action == 'viewpluginpage') {
603 $o .= $this->view_plugin_page();
604 } else if ($action == 'viewcourseindex') {
605 $o .= $this->view_course_index();
606 } else if ($action == 'viewbatchsetmarkingworkflowstate') {
607 $o .= $this->view_batch_set_workflow_state($mform);
608 } else if ($action == 'viewbatchmarkingallocation') {
609 $o .= $this->view_batch_markingallocation($mform);
610 } else if ($action == 'viewsubmitforgradingerror') {
611 $o .= $this->view_error_page(get_string('submitforgrading', 'assign'), $notices);
612 } else if ($action == 'fixrescalednullgrades') {
613 $o .= $this->view_fix_rescaled_null_grades();
614 } else {
615 $o .= $this->view_submission_page();
618 return $o;
622 * Add this instance to the database.
624 * @param stdClass $formdata The data submitted from the form
625 * @param bool $callplugins This is used to skip the plugin code
626 * when upgrading an old assignment to a new one (the plugins get called manually)
627 * @return mixed false if an error occurs or the int id of the new instance
629 public function add_instance(stdClass $formdata, $callplugins) {
630 global $DB;
631 $adminconfig = $this->get_admin_config();
633 $err = '';
635 // Add the database record.
636 $update = new stdClass();
637 $update->name = $formdata->name;
638 $update->timemodified = time();
639 $update->timecreated = time();
640 $update->course = $formdata->course;
641 $update->courseid = $formdata->course;
642 $update->intro = $formdata->intro;
643 $update->introformat = $formdata->introformat;
644 $update->alwaysshowdescription = !empty($formdata->alwaysshowdescription);
645 $update->submissiondrafts = $formdata->submissiondrafts;
646 $update->requiresubmissionstatement = $formdata->requiresubmissionstatement;
647 $update->sendnotifications = $formdata->sendnotifications;
648 $update->sendlatenotifications = $formdata->sendlatenotifications;
649 $update->sendstudentnotifications = $adminconfig->sendstudentnotifications;
650 if (isset($formdata->sendstudentnotifications)) {
651 $update->sendstudentnotifications = $formdata->sendstudentnotifications;
653 $update->duedate = $formdata->duedate;
654 $update->cutoffdate = $formdata->cutoffdate;
655 $update->gradingduedate = $formdata->gradingduedate;
656 $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate;
657 $update->grade = $formdata->grade;
658 $update->completionsubmit = !empty($formdata->completionsubmit);
659 $update->teamsubmission = $formdata->teamsubmission;
660 $update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit;
661 if (isset($formdata->teamsubmissiongroupingid)) {
662 $update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid;
664 $update->blindmarking = $formdata->blindmarking;
665 $update->attemptreopenmethod = ASSIGN_ATTEMPT_REOPEN_METHOD_NONE;
666 if (!empty($formdata->attemptreopenmethod)) {
667 $update->attemptreopenmethod = $formdata->attemptreopenmethod;
669 if (!empty($formdata->maxattempts)) {
670 $update->maxattempts = $formdata->maxattempts;
672 if (isset($formdata->preventsubmissionnotingroup)) {
673 $update->preventsubmissionnotingroup = $formdata->preventsubmissionnotingroup;
675 $update->markingworkflow = $formdata->markingworkflow;
676 $update->markingallocation = $formdata->markingallocation;
677 if (empty($update->markingworkflow)) { // If marking workflow is disabled, make sure allocation is disabled.
678 $update->markingallocation = 0;
681 $returnid = $DB->insert_record('assign', $update);
682 $this->instance = $DB->get_record('assign', array('id'=>$returnid), '*', MUST_EXIST);
683 // Cache the course record.
684 $this->course = $DB->get_record('course', array('id'=>$formdata->course), '*', MUST_EXIST);
686 $this->save_intro_draft_files($formdata);
688 if ($callplugins) {
689 // Call save_settings hook for submission plugins.
690 foreach ($this->submissionplugins as $plugin) {
691 if (!$this->update_plugin_instance($plugin, $formdata)) {
692 print_error($plugin->get_error());
693 return false;
696 foreach ($this->feedbackplugins as $plugin) {
697 if (!$this->update_plugin_instance($plugin, $formdata)) {
698 print_error($plugin->get_error());
699 return false;
703 // In the case of upgrades the coursemodule has not been set,
704 // so we need to wait before calling these two.
705 $this->update_calendar($formdata->coursemodule);
706 if (!empty($formdata->completionexpected)) {
707 \core_completion\api::update_completion_date_event($formdata->coursemodule, 'assign', $this->instance,
708 $formdata->completionexpected);
710 $this->update_gradebook(false, $formdata->coursemodule);
714 $update = new stdClass();
715 $update->id = $this->get_instance()->id;
716 $update->nosubmissions = (!$this->is_any_submission_plugin_enabled()) ? 1: 0;
717 $DB->update_record('assign', $update);
719 return $returnid;
723 * Delete all grades from the gradebook for this assignment.
725 * @return bool
727 protected function delete_grades() {
728 global $CFG;
730 $result = grade_update('mod/assign',
731 $this->get_course()->id,
732 'mod',
733 'assign',
734 $this->get_instance()->id,
736 null,
737 array('deleted'=>1));
738 return $result == GRADE_UPDATE_OK;
742 * Delete this instance from the database.
744 * @return bool false if an error occurs
746 public function delete_instance() {
747 global $DB;
748 $result = true;
750 foreach ($this->submissionplugins as $plugin) {
751 if (!$plugin->delete_instance()) {
752 print_error($plugin->get_error());
753 $result = false;
756 foreach ($this->feedbackplugins as $plugin) {
757 if (!$plugin->delete_instance()) {
758 print_error($plugin->get_error());
759 $result = false;
763 // Delete files associated with this assignment.
764 $fs = get_file_storage();
765 if (! $fs->delete_area_files($this->context->id) ) {
766 $result = false;
769 $this->delete_all_overrides();
771 // Delete_records will throw an exception if it fails - so no need for error checking here.
772 $DB->delete_records('assign_submission', array('assignment' => $this->get_instance()->id));
773 $DB->delete_records('assign_grades', array('assignment' => $this->get_instance()->id));
774 $DB->delete_records('assign_plugin_config', array('assignment' => $this->get_instance()->id));
775 $DB->delete_records('assign_user_flags', array('assignment' => $this->get_instance()->id));
776 $DB->delete_records('assign_user_mapping', array('assignment' => $this->get_instance()->id));
778 // Delete items from the gradebook.
779 if (! $this->delete_grades()) {
780 $result = false;
783 // Delete the instance.
784 $DB->delete_records('assign', array('id'=>$this->get_instance()->id));
786 return $result;
790 * Deletes a assign override from the database and clears any corresponding calendar events
792 * @param int $overrideid The id of the override being deleted
793 * @return bool true on success
795 public function delete_override($overrideid) {
796 global $CFG, $DB;
798 require_once($CFG->dirroot . '/calendar/lib.php');
800 $cm = $this->get_course_module();
801 if (empty($cm)) {
802 $instance = $this->get_instance();
803 $cm = get_coursemodule_from_instance('assign', $instance->id, $instance->course);
806 $override = $DB->get_record('assign_overrides', array('id' => $overrideid), '*', MUST_EXIST);
808 // Delete the events.
809 $conds = array('modulename' => 'assign', 'instance' => $this->get_instance()->id);
810 if (isset($override->userid)) {
811 $conds['userid'] = $override->userid;
812 } else {
813 $conds['groupid'] = $override->groupid;
815 $events = $DB->get_records('event', $conds);
816 foreach ($events as $event) {
817 $eventold = calendar_event::load($event);
818 $eventold->delete();
821 $DB->delete_records('assign_overrides', array('id' => $overrideid));
823 // Set the common parameters for one of the events we will be triggering.
824 $params = array(
825 'objectid' => $override->id,
826 'context' => context_module::instance($cm->id),
827 'other' => array(
828 'assignid' => $override->assignid
831 // Determine which override deleted event to fire.
832 if (!empty($override->userid)) {
833 $params['relateduserid'] = $override->userid;
834 $event = \mod_assign\event\user_override_deleted::create($params);
835 } else {
836 $params['other']['groupid'] = $override->groupid;
837 $event = \mod_assign\event\group_override_deleted::create($params);
840 // Trigger the override deleted event.
841 $event->add_record_snapshot('assign_overrides', $override);
842 $event->trigger();
844 return true;
848 * Deletes all assign overrides from the database and clears any corresponding calendar events
850 public function delete_all_overrides() {
851 global $DB;
853 $overrides = $DB->get_records('assign_overrides', array('assignid' => $this->get_instance()->id), 'id');
854 foreach ($overrides as $override) {
855 $this->delete_override($override->id);
860 * Updates the assign properties with override information for a user.
862 * Algorithm: For each assign setting, if there is a matching user-specific override,
863 * then use that otherwise, if there are group-specific overrides, return the most
864 * lenient combination of them. If neither applies, leave the assign setting unchanged.
866 * @param int $userid The userid.
868 public function update_effective_access($userid) {
870 $override = $this->override_exists($userid);
872 // Merge with assign defaults.
873 $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate');
874 foreach ($keys as $key) {
875 if (isset($override->{$key})) {
876 $this->get_instance()->{$key} = $override->{$key};
883 * Returns whether an assign has any overrides.
885 * @return true if any, false if not
887 public function has_overrides() {
888 global $DB;
890 $override = $DB->record_exists('assign_overrides', array('assignid' => $this->get_instance()->id));
892 if ($override) {
893 return true;
896 return false;
900 * Returns user override
902 * Algorithm: For each assign setting, if there is a matching user-specific override,
903 * then use that otherwise, if there are group-specific overrides, use the one with the
904 * lowest sort order. If neither applies, leave the assign setting unchanged.
906 * @param int $userid The userid.
907 * @return stdClass The override
909 public function override_exists($userid) {
910 global $DB;
912 // Gets an assoc array containing the keys for defined user overrides only.
913 $getuseroverride = function($userid) use ($DB) {
914 $useroverride = $DB->get_record('assign_overrides', ['assignid' => $this->get_instance()->id, 'userid' => $userid]);
915 return $useroverride ? get_object_vars($useroverride) : [];
918 // Gets an assoc array containing the keys for defined group overrides only.
919 $getgroupoverride = function($userid) use ($DB) {
920 $groupings = groups_get_user_groups($this->get_instance()->course, $userid);
922 if (empty($groupings[0])) {
923 return [];
926 // Select all overrides that apply to the User's groups.
927 list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0]));
928 $sql = "SELECT * FROM {assign_overrides}
929 WHERE groupid $extra AND assignid = ? ORDER BY sortorder ASC";
930 $params[] = $this->get_instance()->id;
931 $groupoverride = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE);
933 return $groupoverride ? get_object_vars($groupoverride) : [];
936 // Later arguments clobber earlier ones with array_merge. The two helper functions
937 // return arrays containing keys for only the defined overrides. So we get the
938 // desired behaviour as per the algorithm.
939 return (object)array_merge(
940 ['duedate' => null, 'cutoffdate' => null, 'allowsubmissionsfromdate' => null],
941 $getgroupoverride($userid),
942 $getuseroverride($userid)
947 * Check if the given calendar_event is either a user or group override
948 * event.
950 * @return bool
952 public function is_override_calendar_event(\calendar_event $event) {
953 global $DB;
955 if (!isset($event->modulename)) {
956 return false;
959 if ($event->modulename != 'assign') {
960 return false;
963 if (!isset($event->instance)) {
964 return false;
967 if (!isset($event->userid) && !isset($event->groupid)) {
968 return false;
971 $overrideparams = [
972 'assignid' => $event->instance
975 if (isset($event->groupid)) {
976 $overrideparams['groupid'] = $event->groupid;
977 } else if (isset($event->userid)) {
978 $overrideparams['userid'] = $event->userid;
981 if ($DB->get_record('assign_overrides', $overrideparams)) {
982 return true;
983 } else {
984 return false;
989 * This function calculates the minimum and maximum cutoff values for the timestart of
990 * the given event.
992 * It will return an array with two values, the first being the minimum cutoff value and
993 * the second being the maximum cutoff value. Either or both values can be null, which
994 * indicates there is no minimum or maximum, respectively.
996 * If a cutoff is required then the function must return an array containing the cutoff
997 * timestamp and error string to display to the user if the cutoff value is violated.
999 * A minimum and maximum cutoff return value will look like:
1001 * [1505704373, 'The due date must be after the sbumission start date'],
1002 * [1506741172, 'The due date must be before the cutoff date']
1005 * If the event does not have a valid timestart range then [false, false] will
1006 * be returned.
1008 * @param calendar_event $event The calendar event to get the time range for
1009 * @return array
1011 function get_valid_calendar_event_timestart_range(\calendar_event $event) {
1012 $instance = $this->get_instance();
1013 $submissionsfromdate = $instance->allowsubmissionsfromdate;
1014 $cutoffdate = $instance->cutoffdate;
1015 $duedate = $instance->duedate;
1016 $gradingduedate = $instance->gradingduedate;
1017 $mindate = null;
1018 $maxdate = null;
1020 if ($event->eventtype == ASSIGN_EVENT_TYPE_DUE) {
1021 // This check is in here because due date events are currently
1022 // the only events that can be overridden, so we can save a DB
1023 // query if we don't bother checking other events.
1024 if ($this->is_override_calendar_event($event)) {
1025 // This is an override event so there is no valid timestart
1026 // range to set it to.
1027 return [false, false];
1030 if ($submissionsfromdate) {
1031 $mindate = [
1032 $submissionsfromdate,
1033 get_string('duedatevalidation', 'assign'),
1037 if ($cutoffdate) {
1038 $maxdate = [
1039 $cutoffdate,
1040 get_string('cutoffdatevalidation', 'assign'),
1044 if ($gradingduedate) {
1045 // If we don't have a cutoff date or we've got a grading due date
1046 // that is earlier than the cutoff then we should use that as the
1047 // upper limit for the due date.
1048 if (!$cutoffdate || $gradingduedate < $cutoffdate) {
1049 $maxdate = [
1050 $gradingduedate,
1051 get_string('gradingdueduedatevalidation', 'assign'),
1055 } else if ($event->eventtype == ASSIGN_EVENT_TYPE_GRADINGDUE) {
1056 if ($duedate) {
1057 $mindate = [
1058 $duedate,
1059 get_string('gradingdueduedatevalidation', 'assign'),
1061 } else if ($submissionsfromdate) {
1062 $mindate = [
1063 $submissionsfromdate,
1064 get_string('gradingduefromdatevalidation', 'assign'),
1069 return [$mindate, $maxdate];
1073 * Actual implementation of the reset course functionality, delete all the
1074 * assignment submissions for course $data->courseid.
1076 * @param stdClass $data the data submitted from the reset course.
1077 * @return array status array
1079 public function reset_userdata($data) {
1080 global $CFG, $DB;
1082 $componentstr = get_string('modulenameplural', 'assign');
1083 $status = array();
1085 $fs = get_file_storage();
1086 if (!empty($data->reset_assign_submissions)) {
1087 // Delete files associated with this assignment.
1088 foreach ($this->submissionplugins as $plugin) {
1089 $fileareas = array();
1090 $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type();
1091 $fileareas = $plugin->get_file_areas();
1092 foreach ($fileareas as $filearea => $notused) {
1093 $fs->delete_area_files($this->context->id, $plugincomponent, $filearea);
1096 if (!$plugin->delete_instance()) {
1097 $status[] = array('component'=>$componentstr,
1098 'item'=>get_string('deleteallsubmissions', 'assign'),
1099 'error'=>$plugin->get_error());
1103 foreach ($this->feedbackplugins as $plugin) {
1104 $fileareas = array();
1105 $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type();
1106 $fileareas = $plugin->get_file_areas();
1107 foreach ($fileareas as $filearea => $notused) {
1108 $fs->delete_area_files($this->context->id, $plugincomponent, $filearea);
1111 if (!$plugin->delete_instance()) {
1112 $status[] = array('component'=>$componentstr,
1113 'item'=>get_string('deleteallsubmissions', 'assign'),
1114 'error'=>$plugin->get_error());
1118 $assignids = $DB->get_records('assign', array('course' => $data->courseid), '', 'id');
1119 list($sql, $params) = $DB->get_in_or_equal(array_keys($assignids));
1121 $DB->delete_records_select('assign_submission', "assignment $sql", $params);
1122 $DB->delete_records_select('assign_user_flags', "assignment $sql", $params);
1124 $status[] = array('component'=>$componentstr,
1125 'item'=>get_string('deleteallsubmissions', 'assign'),
1126 'error'=>false);
1128 if (!empty($data->reset_gradebook_grades)) {
1129 $DB->delete_records_select('assign_grades', "assignment $sql", $params);
1130 // Remove all grades from gradebook.
1131 require_once($CFG->dirroot.'/mod/assign/lib.php');
1132 assign_reset_gradebook($data->courseid);
1134 // Reset revealidentities if both submissions and grades have been reset.
1135 if ($this->get_instance()->blindmarking && $this->get_instance()->revealidentities) {
1136 $DB->set_field('assign', 'revealidentities', 0, array('id' => $this->get_instance()->id));
1141 // Remove user overrides.
1142 if (!empty($data->reset_assign_user_overrides)) {
1143 $DB->delete_records_select('assign_overrides',
1144 'assignid IN (SELECT id FROM {assign} WHERE course = ?) AND userid IS NOT NULL', array($data->courseid));
1145 $status[] = array(
1146 'component' => $componentstr,
1147 'item' => get_string('useroverridesdeleted', 'assign'),
1148 'error' => false);
1150 // Remove group overrides.
1151 if (!empty($data->reset_assign_group_overrides)) {
1152 $DB->delete_records_select('assign_overrides',
1153 'assignid IN (SELECT id FROM {assign} WHERE course = ?) AND groupid IS NOT NULL', array($data->courseid));
1154 $status[] = array(
1155 'component' => $componentstr,
1156 'item' => get_string('groupoverridesdeleted', 'assign'),
1157 'error' => false);
1160 // Updating dates - shift may be negative too.
1161 if ($data->timeshift) {
1162 $DB->execute("UPDATE {assign_overrides}
1163 SET allowsubmissionsfromdate = allowsubmissionsfromdate + ?
1164 WHERE assignid = ? AND allowsubmissionsfromdate <> 0",
1165 array($data->timeshift, $this->get_instance()->id));
1166 $DB->execute("UPDATE {assign_overrides}
1167 SET duedate = duedate + ?
1168 WHERE assignid = ? AND duedate <> 0",
1169 array($data->timeshift, $this->get_instance()->id));
1170 $DB->execute("UPDATE {assign_overrides}
1171 SET cutoffdate = cutoffdate + ?
1172 WHERE assignid =? AND cutoffdate <> 0",
1173 array($data->timeshift, $this->get_instance()->id));
1175 // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
1176 // See MDL-9367.
1177 shift_course_mod_dates('assign',
1178 array('duedate', 'allowsubmissionsfromdate', 'cutoffdate'),
1179 $data->timeshift,
1180 $data->courseid, $this->get_instance()->id);
1181 $status[] = array('component'=>$componentstr,
1182 'item'=>get_string('datechanged'),
1183 'error'=>false);
1186 return $status;
1190 * Update the settings for a single plugin.
1192 * @param assign_plugin $plugin The plugin to update
1193 * @param stdClass $formdata The form data
1194 * @return bool false if an error occurs
1196 protected function update_plugin_instance(assign_plugin $plugin, stdClass $formdata) {
1197 if ($plugin->is_visible()) {
1198 $enabledname = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
1199 if (!empty($formdata->$enabledname)) {
1200 $plugin->enable();
1201 if (!$plugin->save_settings($formdata)) {
1202 print_error($plugin->get_error());
1203 return false;
1205 } else {
1206 $plugin->disable();
1209 return true;
1213 * Update the gradebook information for this assignment.
1215 * @param bool $reset If true, will reset all grades in the gradbook for this assignment
1216 * @param int $coursemoduleid This is required because it might not exist in the database yet
1217 * @return bool
1219 public function update_gradebook($reset, $coursemoduleid) {
1220 global $CFG;
1222 require_once($CFG->dirroot.'/mod/assign/lib.php');
1223 $assign = clone $this->get_instance();
1224 $assign->cmidnumber = $coursemoduleid;
1226 // Set assign gradebook feedback plugin status (enabled and visible).
1227 $assign->gradefeedbackenabled = $this->is_gradebook_feedback_enabled();
1229 $param = null;
1230 if ($reset) {
1231 $param = 'reset';
1234 return assign_grade_item_update($assign, $param);
1238 * Get the marking table page size
1240 * @return integer
1242 public function get_assign_perpage() {
1243 $perpage = (int) get_user_preferences('assign_perpage', 10);
1244 $adminconfig = $this->get_admin_config();
1245 $maxperpage = -1;
1246 if (isset($adminconfig->maxperpage)) {
1247 $maxperpage = $adminconfig->maxperpage;
1249 if (isset($maxperpage) &&
1250 $maxperpage != -1 &&
1251 ($perpage == -1 || $perpage > $maxperpage)) {
1252 $perpage = $maxperpage;
1254 return $perpage;
1258 * Load and cache the admin config for this module.
1260 * @return stdClass the plugin config
1262 public function get_admin_config() {
1263 if ($this->adminconfig) {
1264 return $this->adminconfig;
1266 $this->adminconfig = get_config('assign');
1267 return $this->adminconfig;
1271 * Update the calendar entries for this assignment.
1273 * @param int $coursemoduleid - Required to pass this in because it might
1274 * not exist in the database yet.
1275 * @return bool
1277 public function update_calendar($coursemoduleid) {
1278 global $DB, $CFG;
1279 require_once($CFG->dirroot.'/calendar/lib.php');
1281 // Special case for add_instance as the coursemodule has not been set yet.
1282 $instance = $this->get_instance();
1284 // Start with creating the event.
1285 $event = new stdClass();
1286 $event->modulename = 'assign';
1287 $event->courseid = $instance->course;
1288 $event->groupid = 0;
1289 $event->userid = 0;
1290 $event->instance = $instance->id;
1291 $event->type = CALENDAR_EVENT_TYPE_ACTION;
1293 // Convert the links to pluginfile. It is a bit hacky but at this stage the files
1294 // might not have been saved in the module area yet.
1295 $intro = $instance->intro;
1296 if ($draftid = file_get_submitted_draft_itemid('introeditor')) {
1297 $intro = file_rewrite_urls_to_pluginfile($intro, $draftid);
1300 // We need to remove the links to files as the calendar is not ready
1301 // to support module events with file areas.
1302 $intro = strip_pluginfile_content($intro);
1303 if ($this->show_intro()) {
1304 $event->description = array(
1305 'text' => $intro,
1306 'format' => $instance->introformat
1308 } else {
1309 $event->description = array(
1310 'text' => '',
1311 'format' => $instance->introformat
1315 $eventtype = ASSIGN_EVENT_TYPE_DUE;
1316 if ($instance->duedate) {
1317 $event->name = get_string('calendardue', 'assign', $instance->name);
1318 $event->eventtype = $eventtype;
1319 $event->timestart = $instance->duedate;
1320 $event->timesort = $instance->duedate;
1321 $select = "modulename = :modulename
1322 AND instance = :instance
1323 AND eventtype = :eventtype
1324 AND groupid = 0
1325 AND courseid <> 0";
1326 $params = array('modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => $eventtype);
1327 $event->id = $DB->get_field_select('event', 'id', $select, $params);
1329 // Now process the event.
1330 if ($event->id) {
1331 $calendarevent = calendar_event::load($event->id);
1332 $calendarevent->update($event);
1333 } else {
1334 calendar_event::create($event);
1336 } else {
1337 $DB->delete_records('event', array('modulename' => 'assign', 'instance' => $instance->id,
1338 'eventtype' => $eventtype));
1341 $eventtype = ASSIGN_EVENT_TYPE_GRADINGDUE;
1342 if ($instance->gradingduedate) {
1343 $event->name = get_string('calendargradingdue', 'assign', $instance->name);
1344 $event->eventtype = $eventtype;
1345 $event->timestart = $instance->gradingduedate;
1346 $event->timesort = $instance->gradingduedate;
1347 $event->id = $DB->get_field('event', 'id', array('modulename' => 'assign',
1348 'instance' => $instance->id, 'eventtype' => $event->eventtype));
1350 // Now process the event.
1351 if ($event->id) {
1352 $calendarevent = calendar_event::load($event->id);
1353 $calendarevent->update($event);
1354 } else {
1355 calendar_event::create($event);
1357 } else {
1358 $DB->delete_records('event', array('modulename' => 'assign', 'instance' => $instance->id,
1359 'eventtype' => $eventtype));
1362 return true;
1366 * Update this instance in the database.
1368 * @param stdClass $formdata - the data submitted from the form
1369 * @return bool false if an error occurs
1371 public function update_instance($formdata) {
1372 global $DB;
1373 $adminconfig = $this->get_admin_config();
1375 $update = new stdClass();
1376 $update->id = $formdata->instance;
1377 $update->name = $formdata->name;
1378 $update->timemodified = time();
1379 $update->course = $formdata->course;
1380 $update->intro = $formdata->intro;
1381 $update->introformat = $formdata->introformat;
1382 $update->alwaysshowdescription = !empty($formdata->alwaysshowdescription);
1383 $update->submissiondrafts = $formdata->submissiondrafts;
1384 $update->requiresubmissionstatement = $formdata->requiresubmissionstatement;
1385 $update->sendnotifications = $formdata->sendnotifications;
1386 $update->sendlatenotifications = $formdata->sendlatenotifications;
1387 $update->sendstudentnotifications = $adminconfig->sendstudentnotifications;
1388 if (isset($formdata->sendstudentnotifications)) {
1389 $update->sendstudentnotifications = $formdata->sendstudentnotifications;
1391 $update->duedate = $formdata->duedate;
1392 $update->cutoffdate = $formdata->cutoffdate;
1393 $update->gradingduedate = $formdata->gradingduedate;
1394 $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate;
1395 $update->grade = $formdata->grade;
1396 if (!empty($formdata->completionunlocked)) {
1397 $update->completionsubmit = !empty($formdata->completionsubmit);
1399 $update->teamsubmission = $formdata->teamsubmission;
1400 $update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit;
1401 if (isset($formdata->teamsubmissiongroupingid)) {
1402 $update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid;
1404 $update->blindmarking = $formdata->blindmarking;
1405 $update->attemptreopenmethod = ASSIGN_ATTEMPT_REOPEN_METHOD_NONE;
1406 if (!empty($formdata->attemptreopenmethod)) {
1407 $update->attemptreopenmethod = $formdata->attemptreopenmethod;
1409 if (!empty($formdata->maxattempts)) {
1410 $update->maxattempts = $formdata->maxattempts;
1412 if (isset($formdata->preventsubmissionnotingroup)) {
1413 $update->preventsubmissionnotingroup = $formdata->preventsubmissionnotingroup;
1415 $update->markingworkflow = $formdata->markingworkflow;
1416 $update->markingallocation = $formdata->markingallocation;
1417 if (empty($update->markingworkflow)) { // If marking workflow is disabled, make sure allocation is disabled.
1418 $update->markingallocation = 0;
1421 $result = $DB->update_record('assign', $update);
1422 $this->instance = $DB->get_record('assign', array('id'=>$update->id), '*', MUST_EXIST);
1424 $this->save_intro_draft_files($formdata);
1426 // Load the assignment so the plugins have access to it.
1428 // Call save_settings hook for submission plugins.
1429 foreach ($this->submissionplugins as $plugin) {
1430 if (!$this->update_plugin_instance($plugin, $formdata)) {
1431 print_error($plugin->get_error());
1432 return false;
1435 foreach ($this->feedbackplugins as $plugin) {
1436 if (!$this->update_plugin_instance($plugin, $formdata)) {
1437 print_error($plugin->get_error());
1438 return false;
1442 $this->update_calendar($this->get_course_module()->id);
1443 $completionexpected = (!empty($formdata->completionexpected)) ? $formdata->completionexpected : null;
1444 \core_completion\api::update_completion_date_event($this->get_course_module()->id, 'assign', $this->instance,
1445 $completionexpected);
1446 $this->update_gradebook(false, $this->get_course_module()->id);
1448 $update = new stdClass();
1449 $update->id = $this->get_instance()->id;
1450 $update->nosubmissions = (!$this->is_any_submission_plugin_enabled()) ? 1: 0;
1451 $DB->update_record('assign', $update);
1453 return $result;
1457 * Save the attachments in the draft areas.
1459 * @param stdClass $formdata
1461 protected function save_intro_draft_files($formdata) {
1462 if (isset($formdata->introattachments)) {
1463 file_save_draft_area_files($formdata->introattachments, $this->get_context()->id,
1464 'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0);
1469 * Add elements in grading plugin form.
1471 * @param mixed $grade stdClass|null
1472 * @param MoodleQuickForm $mform
1473 * @param stdClass $data
1474 * @param int $userid - The userid we are grading
1475 * @return void
1477 protected function add_plugin_grade_elements($grade, MoodleQuickForm $mform, stdClass $data, $userid) {
1478 foreach ($this->feedbackplugins as $plugin) {
1479 if ($plugin->is_enabled() && $plugin->is_visible()) {
1480 $plugin->get_form_elements_for_user($grade, $mform, $data, $userid);
1488 * Add one plugins settings to edit plugin form.
1490 * @param assign_plugin $plugin The plugin to add the settings from
1491 * @param MoodleQuickForm $mform The form to add the configuration settings to.
1492 * This form is modified directly (not returned).
1493 * @param array $pluginsenabled A list of form elements to be added to a group.
1494 * The new element is added to this array by this function.
1495 * @return void
1497 protected function add_plugin_settings(assign_plugin $plugin, MoodleQuickForm $mform, & $pluginsenabled) {
1498 global $CFG;
1499 if ($plugin->is_visible() && !$plugin->is_configurable() && $plugin->is_enabled()) {
1500 $name = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
1501 $pluginsenabled[] = $mform->createElement('hidden', $name, 1);
1502 $mform->setType($name, PARAM_BOOL);
1503 $plugin->get_settings($mform);
1504 } else if ($plugin->is_visible() && $plugin->is_configurable()) {
1505 $name = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
1506 $label = $plugin->get_name();
1507 $label .= ' ' . $this->get_renderer()->help_icon('enabled', $plugin->get_subtype() . '_' . $plugin->get_type());
1508 $pluginsenabled[] = $mform->createElement('checkbox', $name, '', $label);
1510 $default = get_config($plugin->get_subtype() . '_' . $plugin->get_type(), 'default');
1511 if ($plugin->get_config('enabled') !== false) {
1512 $default = $plugin->is_enabled();
1514 $mform->setDefault($plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled', $default);
1516 $plugin->get_settings($mform);
1522 * Add settings to edit plugin form.
1524 * @param MoodleQuickForm $mform The form to add the configuration settings to.
1525 * This form is modified directly (not returned).
1526 * @return void
1528 public function add_all_plugin_settings(MoodleQuickForm $mform) {
1529 $mform->addElement('header', 'submissiontypes', get_string('submissiontypes', 'assign'));
1531 $submissionpluginsenabled = array();
1532 $group = $mform->addGroup(array(), 'submissionplugins', get_string('submissiontypes', 'assign'), array(' '), false);
1533 foreach ($this->submissionplugins as $plugin) {
1534 $this->add_plugin_settings($plugin, $mform, $submissionpluginsenabled);
1536 $group->setElements($submissionpluginsenabled);
1538 $mform->addElement('header', 'feedbacktypes', get_string('feedbacktypes', 'assign'));
1539 $feedbackpluginsenabled = array();
1540 $group = $mform->addGroup(array(), 'feedbackplugins', get_string('feedbacktypes', 'assign'), array(' '), false);
1541 foreach ($this->feedbackplugins as $plugin) {
1542 $this->add_plugin_settings($plugin, $mform, $feedbackpluginsenabled);
1544 $group->setElements($feedbackpluginsenabled);
1545 $mform->setExpanded('submissiontypes');
1549 * Allow each plugin an opportunity to update the defaultvalues
1550 * passed in to the settings form (needed to set up draft areas for
1551 * editor and filemanager elements)
1553 * @param array $defaultvalues
1555 public function plugin_data_preprocessing(&$defaultvalues) {
1556 foreach ($this->submissionplugins as $plugin) {
1557 if ($plugin->is_visible()) {
1558 $plugin->data_preprocessing($defaultvalues);
1561 foreach ($this->feedbackplugins as $plugin) {
1562 if ($plugin->is_visible()) {
1563 $plugin->data_preprocessing($defaultvalues);
1569 * Get the name of the current module.
1571 * @return string the module name (Assignment)
1573 protected function get_module_name() {
1574 if (isset(self::$modulename)) {
1575 return self::$modulename;
1577 self::$modulename = get_string('modulename', 'assign');
1578 return self::$modulename;
1582 * Get the plural name of the current module.
1584 * @return string the module name plural (Assignments)
1586 protected function get_module_name_plural() {
1587 if (isset(self::$modulenameplural)) {
1588 return self::$modulenameplural;
1590 self::$modulenameplural = get_string('modulenameplural', 'assign');
1591 return self::$modulenameplural;
1595 * Has this assignment been constructed from an instance?
1597 * @return bool
1599 public function has_instance() {
1600 return $this->instance || $this->get_course_module();
1604 * Get the settings for the current instance of this assignment
1606 * @return stdClass The settings
1608 public function get_instance() {
1609 global $DB;
1610 if ($this->instance) {
1611 return $this->instance;
1613 if ($this->get_course_module()) {
1614 $params = array('id' => $this->get_course_module()->instance);
1615 $this->instance = $DB->get_record('assign', $params, '*', MUST_EXIST);
1617 if (!$this->instance) {
1618 throw new coding_exception('Improper use of the assignment class. ' .
1619 'Cannot load the assignment record.');
1621 return $this->instance;
1625 * Get the primary grade item for this assign instance.
1627 * @return grade_item The grade_item record
1629 public function get_grade_item() {
1630 if ($this->gradeitem) {
1631 return $this->gradeitem;
1633 $instance = $this->get_instance();
1634 $params = array('itemtype' => 'mod',
1635 'itemmodule' => 'assign',
1636 'iteminstance' => $instance->id,
1637 'courseid' => $instance->course,
1638 'itemnumber' => 0);
1639 $this->gradeitem = grade_item::fetch($params);
1640 if (!$this->gradeitem) {
1641 throw new coding_exception('Improper use of the assignment class. ' .
1642 'Cannot load the grade item.');
1644 return $this->gradeitem;
1648 * Get the context of the current course.
1650 * @return mixed context|null The course context
1652 public function get_course_context() {
1653 if (!$this->context && !$this->course) {
1654 throw new coding_exception('Improper use of the assignment class. ' .
1655 'Cannot load the course context.');
1657 if ($this->context) {
1658 return $this->context->get_course_context();
1659 } else {
1660 return context_course::instance($this->course->id);
1666 * Get the current course module.
1668 * @return cm_info|null The course module or null if not known
1670 public function get_course_module() {
1671 if ($this->coursemodule) {
1672 return $this->coursemodule;
1674 if (!$this->context) {
1675 return null;
1678 if ($this->context->contextlevel == CONTEXT_MODULE) {
1679 $modinfo = get_fast_modinfo($this->get_course());
1680 $this->coursemodule = $modinfo->get_cm($this->context->instanceid);
1681 return $this->coursemodule;
1683 return null;
1687 * Get context module.
1689 * @return context
1691 public function get_context() {
1692 return $this->context;
1696 * Get the current course.
1698 * @return mixed stdClass|null The course
1700 public function get_course() {
1701 global $DB;
1703 if ($this->course) {
1704 return $this->course;
1707 if (!$this->context) {
1708 return null;
1710 $params = array('id' => $this->get_course_context()->instanceid);
1711 $this->course = $DB->get_record('course', $params, '*', MUST_EXIST);
1713 return $this->course;
1717 * Count the number of intro attachments.
1719 * @return int
1721 protected function count_attachments() {
1723 $fs = get_file_storage();
1724 $files = $fs->get_area_files($this->get_context()->id, 'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA,
1725 0, 'id', false);
1727 return count($files);
1731 * Are there any intro attachments to display?
1733 * @return boolean
1735 protected function has_visible_attachments() {
1736 return ($this->count_attachments() > 0);
1740 * Return a grade in user-friendly form, whether it's a scale or not.
1742 * @param mixed $grade int|null
1743 * @param boolean $editing Are we allowing changes to this grade?
1744 * @param int $userid The user id the grade belongs to
1745 * @param int $modified Timestamp from when the grade was last modified
1746 * @return string User-friendly representation of grade
1748 public function display_grade($grade, $editing, $userid=0, $modified=0) {
1749 global $DB;
1751 static $scalegrades = array();
1753 $o = '';
1755 if ($this->get_instance()->grade >= 0) {
1756 // Normal number.
1757 if ($editing && $this->get_instance()->grade > 0) {
1758 if ($grade < 0) {
1759 $displaygrade = '';
1760 } else {
1761 $displaygrade = format_float($grade, $this->get_grade_item()->get_decimals());
1763 $o .= '<label class="accesshide" for="quickgrade_' . $userid . '">' .
1764 get_string('usergrade', 'assign') .
1765 '</label>';
1766 $o .= '<input type="text"
1767 id="quickgrade_' . $userid . '"
1768 name="quickgrade_' . $userid . '"
1769 value="' . $displaygrade . '"
1770 size="6"
1771 maxlength="10"
1772 class="quickgrade"/>';
1773 $o .= '&nbsp;/&nbsp;' . format_float($this->get_instance()->grade, $this->get_grade_item()->get_decimals());
1774 return $o;
1775 } else {
1776 if ($grade == -1 || $grade === null) {
1777 $o .= '-';
1778 } else {
1779 $item = $this->get_grade_item();
1780 $o .= grade_format_gradevalue($grade, $item);
1781 if ($item->get_displaytype() == GRADE_DISPLAY_TYPE_REAL) {
1782 // If displaying the raw grade, also display the total value.
1783 $o .= '&nbsp;/&nbsp;' . format_float($this->get_instance()->grade, $item->get_decimals());
1786 return $o;
1789 } else {
1790 // Scale.
1791 if (empty($this->cache['scale'])) {
1792 if ($scale = $DB->get_record('scale', array('id'=>-($this->get_instance()->grade)))) {
1793 $this->cache['scale'] = make_menu_from_list($scale->scale);
1794 } else {
1795 $o .= '-';
1796 return $o;
1799 if ($editing) {
1800 $o .= '<label class="accesshide"
1801 for="quickgrade_' . $userid . '">' .
1802 get_string('usergrade', 'assign') .
1803 '</label>';
1804 $o .= '<select name="quickgrade_' . $userid . '" class="quickgrade">';
1805 $o .= '<option value="-1">' . get_string('nograde') . '</option>';
1806 foreach ($this->cache['scale'] as $optionid => $option) {
1807 $selected = '';
1808 if ($grade == $optionid) {
1809 $selected = 'selected="selected"';
1811 $o .= '<option value="' . $optionid . '" ' . $selected . '>' . $option . '</option>';
1813 $o .= '</select>';
1814 return $o;
1815 } else {
1816 $scaleid = (int)$grade;
1817 if (isset($this->cache['scale'][$scaleid])) {
1818 $o .= $this->cache['scale'][$scaleid];
1819 return $o;
1821 $o .= '-';
1822 return $o;
1828 * Get the submission status/grading status for all submissions in this assignment for the
1829 * given paticipants.
1831 * These statuses match the available filters (requiregrading, submitted, notsubmitted, grantedextension).
1832 * If this is a group assignment, group info is also returned.
1834 * @param array $participants an associative array where the key is the participant id and
1835 * the value is the participant record.
1836 * @return array an associative array where the key is the participant id and the value is
1837 * the participant record.
1839 private function get_submission_info_for_participants($participants) {
1840 global $DB;
1842 if (empty($participants)) {
1843 return $participants;
1846 list($insql, $params) = $DB->get_in_or_equal(array_keys($participants), SQL_PARAMS_NAMED);
1848 $assignid = $this->get_instance()->id;
1849 $params['assignmentid1'] = $assignid;
1850 $params['assignmentid2'] = $assignid;
1851 $params['assignmentid3'] = $assignid;
1853 $fields = 'SELECT u.id, s.status, s.timemodified AS stime, g.timemodified AS gtime, g.grade, uf.extensionduedate';
1854 $from = ' FROM {user} u
1855 LEFT JOIN {assign_submission} s
1856 ON u.id = s.userid
1857 AND s.assignment = :assignmentid1
1858 AND s.latest = 1
1859 LEFT JOIN {assign_grades} g
1860 ON u.id = g.userid
1861 AND g.assignment = :assignmentid2
1862 AND g.attemptnumber = s.attemptnumber
1863 LEFT JOIN {assign_user_flags} uf
1864 ON u.id = uf.userid
1865 AND uf.assignment = :assignmentid3
1867 $where = ' WHERE u.id ' . $insql;
1869 if (!empty($this->get_instance()->blindmarking)) {
1870 $from .= 'LEFT JOIN {assign_user_mapping} um
1871 ON u.id = um.userid
1872 AND um.assignment = :assignmentid4 ';
1873 $params['assignmentid4'] = $assignid;
1874 $fields .= ', um.id as recordid ';
1877 $sql = "$fields $from $where";
1879 $records = $DB->get_records_sql($sql, $params);
1881 if ($this->get_instance()->teamsubmission) {
1882 // Get all groups.
1883 $allgroups = groups_get_all_groups($this->get_course()->id,
1884 array_keys($participants),
1885 $this->get_instance()->teamsubmissiongroupingid,
1886 'DISTINCT g.id, g.name');
1889 foreach ($participants as $userid => $participant) {
1890 $participants[$userid]->fullname = $this->fullname($participant);
1891 $participants[$userid]->submitted = false;
1892 $participants[$userid]->requiregrading = false;
1893 $participants[$userid]->grantedextension = false;
1896 foreach ($records as $userid => $submissioninfo) {
1897 // These filters are 100% the same as the ones in the grading table SQL.
1898 $submitted = false;
1899 $requiregrading = false;
1900 $grantedextension = false;
1902 if (!empty($submissioninfo->stime) && $submissioninfo->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
1903 $submitted = true;
1906 if ($submitted && ($submissioninfo->stime >= $submissioninfo->gtime ||
1907 empty($submissioninfo->gtime) ||
1908 $submissioninfo->grade === null)) {
1909 $requiregrading = true;
1912 if (!empty($submissioninfo->extensionduedate)) {
1913 $grantedextension = true;
1916 $participants[$userid]->submitted = $submitted;
1917 $participants[$userid]->requiregrading = $requiregrading;
1918 $participants[$userid]->grantedextension = $grantedextension;
1919 if ($this->get_instance()->teamsubmission) {
1920 $group = $this->get_submission_group($userid);
1921 if ($group) {
1922 $participants[$userid]->groupid = $group->id;
1923 $participants[$userid]->groupname = $group->name;
1927 return $participants;
1931 * Get the submission status/grading status for all submissions in this assignment.
1932 * These statuses match the available filters (requiregrading, submitted, notsubmitted, grantedextension).
1933 * If this is a group assignment, group info is also returned.
1935 * @param int $currentgroup
1936 * @return array List of user records with extra fields 'submitted', 'notsubmitted', 'requiregrading', 'grantedextension',
1937 * 'groupid', 'groupname'
1939 public function list_participants_with_filter_status_and_group($currentgroup) {
1940 $participants = $this->list_participants($currentgroup, false);
1942 if (empty($participants)) {
1943 return $participants;
1944 } else {
1945 return $this->get_submission_info_for_participants($participants);
1950 * Load a list of users enrolled in the current course with the specified permission and group.
1951 * 0 for no group.
1953 * @param int $currentgroup
1954 * @param bool $idsonly
1955 * @return array List of user records
1957 public function list_participants($currentgroup, $idsonly) {
1958 global $DB, $USER;
1960 if (empty($currentgroup)) {
1961 $currentgroup = 0;
1964 $key = $this->context->id . '-' . $currentgroup . '-' . $this->show_only_active_users();
1965 if (!isset($this->participants[$key])) {
1966 list($esql, $params) = get_enrolled_sql($this->context, 'mod/assign:submit', $currentgroup,
1967 $this->show_only_active_users());
1969 $fields = 'u.*';
1970 $orderby = 'u.lastname, u.firstname, u.id';
1971 $additionaljoins = '';
1972 $additionalfilters = '';
1973 $instance = $this->get_instance();
1974 if (!empty($instance->blindmarking)) {
1975 $additionaljoins .= " LEFT JOIN {assign_user_mapping} um
1976 ON u.id = um.userid
1977 AND um.assignment = :assignmentid1
1978 LEFT JOIN {assign_submission} s
1979 ON u.id = s.userid
1980 AND s.assignment = :assignmentid2
1981 AND s.latest = 1
1983 $params['assignmentid1'] = (int) $instance->id;
1984 $params['assignmentid2'] = (int) $instance->id;
1985 $fields .= ', um.id as recordid ';
1987 // Sort by submission time first, then by um.id to sort reliably by the blind marking id.
1988 // Note, different DBs have different ordering of NULL values.
1989 // Therefore we coalesce the current time into the timecreated field, and the max possible integer into
1990 // the ID field.
1991 $orderby = "COALESCE(s.timecreated, " . time() . ") ASC, COALESCE(s.id, " . PHP_INT_MAX . ") ASC, um.id ASC";
1994 if ($instance->markingworkflow &&
1995 $instance->markingallocation &&
1996 !has_capability('mod/assign:manageallocations', $this->get_context()) &&
1997 has_capability('mod/assign:grade', $this->get_context())) {
1999 $additionaljoins .= ' LEFT JOIN {assign_user_flags} uf
2000 ON u.id = uf.userid
2001 AND uf.assignment = :assignmentid3';
2003 $params['assignmentid3'] = (int) $instance->id;
2005 $additionalfilters .= ' AND uf.allocatedmarker = :markerid';
2006 $params['markerid'] = $USER->id;
2009 $sql = "SELECT $fields
2010 FROM {user} u
2011 JOIN ($esql) je ON je.id = u.id
2012 $additionaljoins
2013 WHERE u.deleted = 0
2014 $additionalfilters
2015 ORDER BY $orderby";
2017 $users = $DB->get_records_sql($sql, $params);
2019 $cm = $this->get_course_module();
2020 $info = new \core_availability\info_module($cm);
2021 $users = $info->filter_user_list($users);
2023 $this->participants[$key] = $users;
2026 if ($idsonly) {
2027 $idslist = array();
2028 foreach ($this->participants[$key] as $id => $user) {
2029 $idslist[$id] = new stdClass();
2030 $idslist[$id]->id = $id;
2032 return $idslist;
2034 return $this->participants[$key];
2038 * Load a user if they are enrolled in the current course. Populated with submission
2039 * status for this assignment.
2041 * @param int $userid
2042 * @return null|stdClass user record
2044 public function get_participant($userid) {
2045 global $DB, $USER;
2047 if ($userid == $USER->id) {
2048 $participant = clone ($USER);
2049 } else {
2050 $participant = $DB->get_record('user', array('id' => $userid));
2052 if (!$participant) {
2053 return null;
2056 if (!is_enrolled($this->context, $participant, 'mod/assign:submit', $this->show_only_active_users())) {
2057 return null;
2060 $result = $this->get_submission_info_for_participants(array($participant->id => $participant));
2061 return $result[$participant->id];
2065 * Load a count of valid teams for this assignment.
2067 * @param int $activitygroup Activity active group
2068 * @return int number of valid teams
2070 public function count_teams($activitygroup = 0) {
2072 $count = 0;
2074 $participants = $this->list_participants($activitygroup, true);
2076 // If a team submission grouping id is provided all good as all returned groups
2077 // are the submission teams, but if no team submission grouping was specified
2078 // $groups will contain all participants groups.
2079 if ($this->get_instance()->teamsubmissiongroupingid) {
2081 // We restrict the users to the selected group ones.
2082 $groups = groups_get_all_groups($this->get_course()->id,
2083 array_keys($participants),
2084 $this->get_instance()->teamsubmissiongroupingid,
2085 'DISTINCT g.id, g.name');
2087 $count = count($groups);
2089 // When a specific group is selected we don't count the default group users.
2090 if ($activitygroup == 0) {
2091 if (empty($this->get_instance()->preventsubmissionnotingroup)) {
2092 // See if there are any users in the default group.
2093 $defaultusers = $this->get_submission_group_members(0, true);
2094 if (count($defaultusers) > 0) {
2095 $count += 1;
2098 } else if ($activitygroup != 0 && empty($groups)) {
2099 // Set count to 1 if $groups returns empty.
2100 // It means the group is not part of $this->get_instance()->teamsubmissiongroupingid.
2101 $count = 1;
2103 } else {
2104 // It is faster to loop around participants if no grouping was specified.
2105 $groups = array();
2106 foreach ($participants as $participant) {
2107 if ($group = $this->get_submission_group($participant->id)) {
2108 $groups[$group->id] = true;
2109 } else if (empty($this->get_instance()->preventsubmissionnotingroup)) {
2110 $groups[0] = true;
2114 $count = count($groups);
2117 return $count;
2121 * Load a count of active users enrolled in the current course with the specified permission and group.
2122 * 0 for no group.
2124 * @param int $currentgroup
2125 * @return int number of matching users
2127 public function count_participants($currentgroup) {
2128 return count($this->list_participants($currentgroup, true));
2132 * Load a count of active users submissions in the current module that require grading
2133 * This means the submission modification time is more recent than the
2134 * grading modification time and the status is SUBMITTED.
2136 * @return int number of matching submissions
2138 public function count_submissions_need_grading() {
2139 global $DB;
2141 if ($this->get_instance()->teamsubmission) {
2142 // This does not make sense for group assignment because the submission is shared.
2143 return 0;
2146 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
2147 list($esql, $params) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
2149 $params['assignid'] = $this->get_instance()->id;
2150 $params['submitted'] = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
2151 $sqlscalegrade = $this->get_instance()->grade < 0 ? ' OR g.grade = -1' : '';
2153 $sql = 'SELECT COUNT(s.userid)
2154 FROM {assign_submission} s
2155 LEFT JOIN {assign_grades} g ON
2156 s.assignment = g.assignment AND
2157 s.userid = g.userid AND
2158 g.attemptnumber = s.attemptnumber
2159 JOIN(' . $esql . ') e ON e.id = s.userid
2160 WHERE
2161 s.latest = 1 AND
2162 s.assignment = :assignid AND
2163 s.timemodified IS NOT NULL AND
2164 s.status = :submitted AND
2165 (s.timemodified >= g.timemodified OR g.timemodified IS NULL OR g.grade IS NULL '
2166 . $sqlscalegrade . ')';
2168 return $DB->count_records_sql($sql, $params);
2172 * Load a count of grades.
2174 * @return int number of grades
2176 public function count_grades() {
2177 global $DB;
2179 if (!$this->has_instance()) {
2180 return 0;
2183 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
2184 list($esql, $params) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
2186 $params['assignid'] = $this->get_instance()->id;
2188 $sql = 'SELECT COUNT(g.userid)
2189 FROM {assign_grades} g
2190 JOIN(' . $esql . ') e ON e.id = g.userid
2191 WHERE g.assignment = :assignid';
2193 return $DB->count_records_sql($sql, $params);
2197 * Load a count of submissions.
2199 * @param bool $includenew When true, also counts the submissions with status 'new'.
2200 * @return int number of submissions
2202 public function count_submissions($includenew = false) {
2203 global $DB;
2205 if (!$this->has_instance()) {
2206 return 0;
2209 $params = array();
2210 $sqlnew = '';
2212 if (!$includenew) {
2213 $sqlnew = ' AND s.status <> :status ';
2214 $params['status'] = ASSIGN_SUBMISSION_STATUS_NEW;
2217 if ($this->get_instance()->teamsubmission) {
2218 // We cannot join on the enrolment tables for group submissions (no userid).
2219 $sql = 'SELECT COUNT(DISTINCT s.groupid)
2220 FROM {assign_submission} s
2221 WHERE
2222 s.assignment = :assignid AND
2223 s.timemodified IS NOT NULL AND
2224 s.userid = :groupuserid' .
2225 $sqlnew;
2227 $params['assignid'] = $this->get_instance()->id;
2228 $params['groupuserid'] = 0;
2229 } else {
2230 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
2231 list($esql, $enrolparams) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
2233 $params = array_merge($params, $enrolparams);
2234 $params['assignid'] = $this->get_instance()->id;
2236 $sql = 'SELECT COUNT(DISTINCT s.userid)
2237 FROM {assign_submission} s
2238 JOIN(' . $esql . ') e ON e.id = s.userid
2239 WHERE
2240 s.assignment = :assignid AND
2241 s.timemodified IS NOT NULL ' .
2242 $sqlnew;
2246 return $DB->count_records_sql($sql, $params);
2250 * Load a count of submissions with a specified status.
2252 * @param string $status The submission status - should match one of the constants
2253 * @return int number of matching submissions
2255 public function count_submissions_with_status($status) {
2256 global $DB;
2258 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
2259 list($esql, $params) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
2261 $params['assignid'] = $this->get_instance()->id;
2262 $params['assignid2'] = $this->get_instance()->id;
2263 $params['submissionstatus'] = $status;
2265 if ($this->get_instance()->teamsubmission) {
2267 $groupsstr = '';
2268 if ($currentgroup != 0) {
2269 // If there is an active group we should only display the current group users groups.
2270 $participants = $this->list_participants($currentgroup, true);
2271 $groups = groups_get_all_groups($this->get_course()->id,
2272 array_keys($participants),
2273 $this->get_instance()->teamsubmissiongroupingid,
2274 'DISTINCT g.id, g.name');
2275 if (empty($groups)) {
2276 // If $groups is empty it means it is not part of $this->get_instance()->teamsubmissiongroupingid.
2277 // All submissions from students that do not belong to any of teamsubmissiongroupingid groups
2278 // count towards groupid = 0. Setting to true as only '0' key matters.
2279 $groups = [true];
2281 list($groupssql, $groupsparams) = $DB->get_in_or_equal(array_keys($groups), SQL_PARAMS_NAMED);
2282 $groupsstr = 's.groupid ' . $groupssql . ' AND';
2283 $params = $params + $groupsparams;
2285 $sql = 'SELECT COUNT(s.groupid)
2286 FROM {assign_submission} s
2287 WHERE
2288 s.latest = 1 AND
2289 s.assignment = :assignid AND
2290 s.timemodified IS NOT NULL AND
2291 s.userid = :groupuserid AND '
2292 . $groupsstr . '
2293 s.status = :submissionstatus';
2294 $params['groupuserid'] = 0;
2295 } else {
2296 $sql = 'SELECT COUNT(s.userid)
2297 FROM {assign_submission} s
2298 JOIN(' . $esql . ') e ON e.id = s.userid
2299 WHERE
2300 s.latest = 1 AND
2301 s.assignment = :assignid AND
2302 s.timemodified IS NOT NULL AND
2303 s.status = :submissionstatus';
2307 return $DB->count_records_sql($sql, $params);
2311 * Utility function to get the userid for every row in the grading table
2312 * so the order can be frozen while we iterate it.
2314 * @return array An array of userids
2316 protected function get_grading_userid_list() {
2317 $filter = get_user_preferences('assign_filter', '');
2318 $table = new assign_grading_table($this, 0, $filter, 0, false);
2320 $useridlist = $table->get_column_data('userid');
2322 return $useridlist;
2326 * Generate zip file from array of given files.
2328 * @param array $filesforzipping - array of files to pass into archive_to_pathname.
2329 * This array is indexed by the final file name and each
2330 * element in the array is an instance of a stored_file object.
2331 * @return path of temp file - note this returned file does
2332 * not have a .zip extension - it is a temp file.
2334 protected function pack_files($filesforzipping) {
2335 global $CFG;
2336 // Create path for new zip file.
2337 $tempzip = tempnam($CFG->tempdir . '/', 'assignment_');
2338 // Zip files.
2339 $zipper = new zip_packer();
2340 if ($zipper->archive_to_pathname($filesforzipping, $tempzip)) {
2341 return $tempzip;
2343 return false;
2347 * Finds all assignment notifications that have yet to be mailed out, and mails them.
2349 * Cron function to be run periodically according to the moodle cron.
2351 * @return bool
2353 public static function cron() {
2354 global $DB;
2356 // Only ever send a max of one days worth of updates.
2357 $yesterday = time() - (24 * 3600);
2358 $timenow = time();
2359 $lastcron = $DB->get_field('modules', 'lastcron', array('name' => 'assign'));
2361 // Collect all submissions that require mailing.
2362 // Submissions are included if all are true:
2363 // - The assignment is visible in the gradebook.
2364 // - No previous notification has been sent.
2365 // - If marking workflow is not enabled, the grade was updated in the past 24 hours, or
2366 // if marking workflow is enabled, the workflow state is at 'released'.
2367 $sql = "SELECT g.id as gradeid, a.course, a.name, a.blindmarking, a.revealidentities,
2368 g.*, g.timemodified as lastmodified, cm.id as cmid, um.id as recordid
2369 FROM {assign} a
2370 JOIN {assign_grades} g ON g.assignment = a.id
2371 LEFT JOIN {assign_user_flags} uf ON uf.assignment = a.id AND uf.userid = g.userid
2372 JOIN {course_modules} cm ON cm.course = a.course AND cm.instance = a.id
2373 JOIN {modules} md ON md.id = cm.module AND md.name = 'assign'
2374 JOIN {grade_items} gri ON gri.iteminstance = a.id AND gri.courseid = a.course AND gri.itemmodule = md.name
2375 LEFT JOIN {assign_user_mapping} um ON g.id = um.userid AND um.assignment = a.id
2376 WHERE ((a.markingworkflow = 0 AND g.timemodified >= :yesterday AND g.timemodified <= :today) OR
2377 (a.markingworkflow = 1 AND uf.workflowstate = :wfreleased)) AND
2378 uf.mailed = 0 AND gri.hidden = 0
2379 ORDER BY a.course, cm.id";
2381 $params = array(
2382 'yesterday' => $yesterday,
2383 'today' => $timenow,
2384 'wfreleased' => ASSIGN_MARKING_WORKFLOW_STATE_RELEASED,
2386 $submissions = $DB->get_records_sql($sql, $params);
2388 if (!empty($submissions)) {
2390 mtrace('Processing ' . count($submissions) . ' assignment submissions ...');
2392 // Preload courses we are going to need those.
2393 $courseids = array();
2394 foreach ($submissions as $submission) {
2395 $courseids[] = $submission->course;
2398 // Filter out duplicates.
2399 $courseids = array_unique($courseids);
2400 $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
2401 list($courseidsql, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
2402 $sql = 'SELECT c.*, ' . $ctxselect .
2403 ' FROM {course} c
2404 LEFT JOIN {context} ctx ON ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel
2405 WHERE c.id ' . $courseidsql;
2407 $params['contextlevel'] = CONTEXT_COURSE;
2408 $courses = $DB->get_records_sql($sql, $params);
2410 // Clean up... this could go on for a while.
2411 unset($courseids);
2412 unset($ctxselect);
2413 unset($courseidsql);
2414 unset($params);
2416 // Message students about new feedback.
2417 foreach ($submissions as $submission) {
2419 mtrace("Processing assignment submission $submission->id ...");
2421 // Do not cache user lookups - could be too many.
2422 if (!$user = $DB->get_record('user', array('id'=>$submission->userid))) {
2423 mtrace('Could not find user ' . $submission->userid);
2424 continue;
2427 // Use a cache to prevent the same DB queries happening over and over.
2428 if (!array_key_exists($submission->course, $courses)) {
2429 mtrace('Could not find course ' . $submission->course);
2430 continue;
2432 $course = $courses[$submission->course];
2433 if (isset($course->ctxid)) {
2434 // Context has not yet been preloaded. Do so now.
2435 context_helper::preload_from_record($course);
2438 // Override the language and timezone of the "current" user, so that
2439 // mail is customised for the receiver.
2440 cron_setup_user($user, $course);
2442 // Context lookups are already cached.
2443 $coursecontext = context_course::instance($course->id);
2444 if (!is_enrolled($coursecontext, $user->id)) {
2445 $courseshortname = format_string($course->shortname,
2446 true,
2447 array('context' => $coursecontext));
2448 mtrace(fullname($user) . ' not an active participant in ' . $courseshortname);
2449 continue;
2452 if (!$grader = $DB->get_record('user', array('id'=>$submission->grader))) {
2453 mtrace('Could not find grader ' . $submission->grader);
2454 continue;
2457 $modinfo = get_fast_modinfo($course, $user->id);
2458 $cm = $modinfo->get_cm($submission->cmid);
2459 // Context lookups are already cached.
2460 $contextmodule = context_module::instance($cm->id);
2462 if (!$cm->uservisible) {
2463 // Hold mail notification for assignments the user cannot access until later.
2464 continue;
2467 // Need to send this to the student.
2468 $messagetype = 'feedbackavailable';
2469 $eventtype = 'assign_notification';
2470 $updatetime = $submission->lastmodified;
2471 $modulename = get_string('modulename', 'assign');
2473 $uniqueid = 0;
2474 if ($submission->blindmarking && !$submission->revealidentities) {
2475 if (empty($submission->recordid)) {
2476 $uniqueid = self::get_uniqueid_for_user_static($submission->assignment, $grader->id);
2477 } else {
2478 $uniqueid = $submission->recordid;
2481 $showusers = $submission->blindmarking && !$submission->revealidentities;
2482 self::send_assignment_notification($grader,
2483 $user,
2484 $messagetype,
2485 $eventtype,
2486 $updatetime,
2487 $cm,
2488 $contextmodule,
2489 $course,
2490 $modulename,
2491 $submission->name,
2492 $showusers,
2493 $uniqueid);
2495 $flags = $DB->get_record('assign_user_flags', array('userid'=>$user->id, 'assignment'=>$submission->assignment));
2496 if ($flags) {
2497 $flags->mailed = 1;
2498 $DB->update_record('assign_user_flags', $flags);
2499 } else {
2500 $flags = new stdClass();
2501 $flags->userid = $user->id;
2502 $flags->assignment = $submission->assignment;
2503 $flags->mailed = 1;
2504 $DB->insert_record('assign_user_flags', $flags);
2507 mtrace('Done');
2509 mtrace('Done processing ' . count($submissions) . ' assignment submissions');
2511 cron_setup_user();
2513 // Free up memory just to be sure.
2514 unset($courses);
2517 // Update calendar events to provide a description.
2518 $sql = 'SELECT id
2519 FROM {assign}
2520 WHERE
2521 allowsubmissionsfromdate >= :lastcron AND
2522 allowsubmissionsfromdate <= :timenow AND
2523 alwaysshowdescription = 0';
2524 $params = array('lastcron' => $lastcron, 'timenow' => $timenow);
2525 $newlyavailable = $DB->get_records_sql($sql, $params);
2526 foreach ($newlyavailable as $record) {
2527 $cm = get_coursemodule_from_instance('assign', $record->id, 0, false, MUST_EXIST);
2528 $context = context_module::instance($cm->id);
2530 $assignment = new assign($context, null, null);
2531 $assignment->update_calendar($cm->id);
2534 return true;
2538 * Mark in the database that this grade record should have an update notification sent by cron.
2540 * @param stdClass $grade a grade record keyed on id
2541 * @param bool $mailedoverride when true, flag notification to be sent again.
2542 * @return bool true for success
2544 public function notify_grade_modified($grade, $mailedoverride = false) {
2545 global $DB;
2547 $flags = $this->get_user_flags($grade->userid, true);
2548 if ($flags->mailed != 1 || $mailedoverride) {
2549 $flags->mailed = 0;
2552 return $this->update_user_flags($flags);
2556 * Update user flags for this user in this assignment.
2558 * @param stdClass $flags a flags record keyed on id
2559 * @return bool true for success
2561 public function update_user_flags($flags) {
2562 global $DB;
2563 if ($flags->userid <= 0 || $flags->assignment <= 0 || $flags->id <= 0) {
2564 return false;
2567 $result = $DB->update_record('assign_user_flags', $flags);
2568 return $result;
2572 * Update a grade in the grade table for the assignment and in the gradebook.
2574 * @param stdClass $grade a grade record keyed on id
2575 * @param bool $reopenattempt If the attempt reopen method is manual, allow another attempt at this assignment.
2576 * @return bool true for success
2578 public function update_grade($grade, $reopenattempt = false) {
2579 global $DB;
2581 $grade->timemodified = time();
2583 if (!empty($grade->workflowstate)) {
2584 $validstates = $this->get_marking_workflow_states_for_current_user();
2585 if (!array_key_exists($grade->workflowstate, $validstates)) {
2586 return false;
2590 if ($grade->grade && $grade->grade != -1) {
2591 if ($this->get_instance()->grade > 0) {
2592 if (!is_numeric($grade->grade)) {
2593 return false;
2594 } else if ($grade->grade > $this->get_instance()->grade) {
2595 return false;
2596 } else if ($grade->grade < 0) {
2597 return false;
2599 } else {
2600 // This is a scale.
2601 if ($scale = $DB->get_record('scale', array('id' => -($this->get_instance()->grade)))) {
2602 $scaleoptions = make_menu_from_list($scale->scale);
2603 if (!array_key_exists((int) $grade->grade, $scaleoptions)) {
2604 return false;
2610 if (empty($grade->attemptnumber)) {
2611 // Set it to the default.
2612 $grade->attemptnumber = 0;
2614 $DB->update_record('assign_grades', $grade);
2616 $submission = null;
2617 if ($this->get_instance()->teamsubmission) {
2618 if (isset($this->mostrecentteamsubmission)) {
2619 $submission = $this->mostrecentteamsubmission;
2620 } else {
2621 $submission = $this->get_group_submission($grade->userid, 0, false);
2623 } else {
2624 $submission = $this->get_user_submission($grade->userid, false);
2627 // Only push to gradebook if the update is for the most recent attempt.
2628 if ($submission && $submission->attemptnumber != $grade->attemptnumber) {
2629 return true;
2632 if ($this->gradebook_item_update(null, $grade)) {
2633 \mod_assign\event\submission_graded::create_from_grade($this, $grade)->trigger();
2636 // If the conditions are met, allow another attempt.
2637 if ($submission) {
2638 $this->reopen_submission_if_required($grade->userid,
2639 $submission,
2640 $reopenattempt);
2643 return true;
2647 * View the grant extension date page.
2649 * Uses url parameters 'userid'
2650 * or from parameter 'selectedusers'
2652 * @param moodleform $mform - Used for validation of the submitted data
2653 * @return string
2655 protected function view_grant_extension($mform) {
2656 global $CFG;
2657 require_once($CFG->dirroot . '/mod/assign/extensionform.php');
2659 $o = '';
2661 $data = new stdClass();
2662 $data->id = $this->get_course_module()->id;
2664 $formparams = array(
2665 'instance' => $this->get_instance(),
2666 'assign' => $this
2669 $users = optional_param('userid', 0, PARAM_INT);
2670 if (!$users) {
2671 $users = required_param('selectedusers', PARAM_SEQUENCE);
2673 $userlist = explode(',', $users);
2675 $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate');
2676 $maxoverride = array('allowsubmissionsfromdate' => 0, 'duedate' => 0, 'cutoffdate' => 0);
2677 foreach ($userlist as $userid) {
2678 // To validate extension date with users overrides.
2679 $override = $this->override_exists($userid);
2680 foreach ($keys as $key) {
2681 if ($override->{$key}) {
2682 if ($maxoverride[$key] < $override->{$key}) {
2683 $maxoverride[$key] = $override->{$key};
2685 } else if ($maxoverride[$key] < $this->get_instance()->{$key}) {
2686 $maxoverride[$key] = $this->get_instance()->{$key};
2690 foreach ($keys as $key) {
2691 if ($maxoverride[$key]) {
2692 $this->get_instance()->{$key} = $maxoverride[$key];
2696 $formparams['userlist'] = $userlist;
2698 $data->selectedusers = $users;
2699 $data->userid = 0;
2701 if (empty($mform)) {
2702 $mform = new mod_assign_extension_form(null, $formparams);
2704 $mform->set_data($data);
2705 $header = new assign_header($this->get_instance(),
2706 $this->get_context(),
2707 $this->show_intro(),
2708 $this->get_course_module()->id,
2709 get_string('grantextension', 'assign'));
2710 $o .= $this->get_renderer()->render($header);
2711 $o .= $this->get_renderer()->render(new assign_form('extensionform', $mform));
2712 $o .= $this->view_footer();
2713 return $o;
2717 * Get a list of the users in the same group as this user.
2719 * @param int $groupid The id of the group whose members we want or 0 for the default group
2720 * @param bool $onlyids Whether to retrieve only the user id's
2721 * @param bool $excludesuspended Whether to exclude suspended users
2722 * @return array The users (possibly id's only)
2724 public function get_submission_group_members($groupid, $onlyids, $excludesuspended = false) {
2725 $members = array();
2726 if ($groupid != 0) {
2727 $allusers = $this->list_participants($groupid, $onlyids);
2728 foreach ($allusers as $user) {
2729 if ($this->get_submission_group($user->id)) {
2730 $members[] = $user;
2733 } else {
2734 $allusers = $this->list_participants(null, $onlyids);
2735 foreach ($allusers as $user) {
2736 if ($this->get_submission_group($user->id) == null) {
2737 $members[] = $user;
2741 // Exclude suspended users, if user can't see them.
2742 if ($excludesuspended || !has_capability('moodle/course:viewsuspendedusers', $this->context)) {
2743 foreach ($members as $key => $member) {
2744 if (!$this->is_active_user($member->id)) {
2745 unset($members[$key]);
2750 return $members;
2754 * Get a list of the users in the same group as this user that have not submitted the assignment.
2756 * @param int $groupid The id of the group whose members we want or 0 for the default group
2757 * @param bool $onlyids Whether to retrieve only the user id's
2758 * @return array The users (possibly id's only)
2760 public function get_submission_group_members_who_have_not_submitted($groupid, $onlyids) {
2761 $instance = $this->get_instance();
2762 if (!$instance->teamsubmission || !$instance->requireallteammemberssubmit) {
2763 return array();
2765 $members = $this->get_submission_group_members($groupid, $onlyids);
2767 foreach ($members as $id => $member) {
2768 $submission = $this->get_user_submission($member->id, false);
2769 if ($submission && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
2770 unset($members[$id]);
2771 } else {
2772 if ($this->is_blind_marking()) {
2773 $members[$id]->alias = get_string('hiddenuser', 'assign') .
2774 $this->get_uniqueid_for_user($id);
2778 return $members;
2782 * Load the group submission object for a particular user, optionally creating it if required.
2784 * @param int $userid The id of the user whose submission we want
2785 * @param int $groupid The id of the group for this user - may be 0 in which
2786 * case it is determined from the userid.
2787 * @param bool $create If set to true a new submission object will be created in the database
2788 * with the status set to "new".
2789 * @param int $attemptnumber - -1 means the latest attempt
2790 * @return stdClass The submission
2792 public function get_group_submission($userid, $groupid, $create, $attemptnumber=-1) {
2793 global $DB;
2795 if ($groupid == 0) {
2796 $group = $this->get_submission_group($userid);
2797 if ($group) {
2798 $groupid = $group->id;
2802 // Now get the group submission.
2803 $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
2804 if ($attemptnumber >= 0) {
2805 $params['attemptnumber'] = $attemptnumber;
2808 // Only return the row with the highest attemptnumber.
2809 $submission = null;
2810 $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', '*', 0, 1);
2811 if ($submissions) {
2812 $submission = reset($submissions);
2815 if ($submission) {
2816 return $submission;
2818 if ($create) {
2819 $submission = new stdClass();
2820 $submission->assignment = $this->get_instance()->id;
2821 $submission->userid = 0;
2822 $submission->groupid = $groupid;
2823 $submission->timecreated = time();
2824 $submission->timemodified = $submission->timecreated;
2825 if ($attemptnumber >= 0) {
2826 $submission->attemptnumber = $attemptnumber;
2827 } else {
2828 $submission->attemptnumber = 0;
2830 // Work out if this is the latest submission.
2831 $submission->latest = 0;
2832 $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
2833 if ($attemptnumber == -1) {
2834 // This is a new submission so it must be the latest.
2835 $submission->latest = 1;
2836 } else {
2837 // We need to work this out.
2838 $result = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', 'attemptnumber', 0, 1);
2839 if ($result) {
2840 $latestsubmission = reset($result);
2842 if (!$latestsubmission || ($attemptnumber == $latestsubmission->attemptnumber)) {
2843 $submission->latest = 1;
2846 if ($submission->latest) {
2847 // This is the case when we need to set latest to 0 for all the other attempts.
2848 $DB->set_field('assign_submission', 'latest', 0, $params);
2850 $submission->status = ASSIGN_SUBMISSION_STATUS_NEW;
2851 $sid = $DB->insert_record('assign_submission', $submission);
2852 return $DB->get_record('assign_submission', array('id' => $sid));
2854 return false;
2858 * View a summary listing of all assignments in the current course.
2860 * @return string
2862 private function view_course_index() {
2863 global $USER;
2865 $o = '';
2867 $course = $this->get_course();
2868 $strplural = get_string('modulenameplural', 'assign');
2870 if (!$cms = get_coursemodules_in_course('assign', $course->id, 'm.duedate')) {
2871 $o .= $this->get_renderer()->notification(get_string('thereareno', 'moodle', $strplural));
2872 $o .= $this->get_renderer()->continue_button(new moodle_url('/course/view.php', array('id' => $course->id)));
2873 return $o;
2876 $strsectionname = '';
2877 $usesections = course_format_uses_sections($course->format);
2878 $modinfo = get_fast_modinfo($course);
2880 if ($usesections) {
2881 $strsectionname = get_string('sectionname', 'format_'.$course->format);
2882 $sections = $modinfo->get_section_info_all();
2884 $courseindexsummary = new assign_course_index_summary($usesections, $strsectionname);
2886 $timenow = time();
2888 $currentsection = '';
2889 foreach ($modinfo->instances['assign'] as $cm) {
2890 if (!$cm->uservisible) {
2891 continue;
2894 $timedue = $cms[$cm->id]->duedate;
2896 $sectionname = '';
2897 if ($usesections && $cm->sectionnum) {
2898 $sectionname = get_section_name($course, $sections[$cm->sectionnum]);
2901 $submitted = '';
2902 $context = context_module::instance($cm->id);
2904 $assignment = new assign($context, $cm, $course);
2906 // Apply overrides.
2907 $assignment->update_effective_access($USER->id);
2908 $timedue = $assignment->get_instance()->duedate;
2910 if (has_capability('mod/assign:grade', $context)) {
2911 $submitted = $assignment->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED);
2913 } else if (has_capability('mod/assign:submit', $context)) {
2914 $usersubmission = $assignment->get_user_submission($USER->id, false);
2916 if (!empty($usersubmission->status)) {
2917 $submitted = get_string('submissionstatus_' . $usersubmission->status, 'assign');
2918 } else {
2919 $submitted = get_string('submissionstatus_', 'assign');
2922 $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $cm->instance, $USER->id);
2923 if (isset($gradinginfo->items[0]->grades[$USER->id]) &&
2924 !$gradinginfo->items[0]->grades[$USER->id]->hidden ) {
2925 $grade = $gradinginfo->items[0]->grades[$USER->id]->str_grade;
2926 } else {
2927 $grade = '-';
2930 $courseindexsummary->add_assign_info($cm->id, $cm->get_formatted_name(), $sectionname, $timedue, $submitted, $grade);
2934 $o .= $this->get_renderer()->render($courseindexsummary);
2935 $o .= $this->view_footer();
2937 return $o;
2941 * View a page rendered by a plugin.
2943 * Uses url parameters 'pluginaction', 'pluginsubtype', 'plugin', and 'id'.
2945 * @return string
2947 protected function view_plugin_page() {
2948 global $USER;
2950 $o = '';
2952 $pluginsubtype = required_param('pluginsubtype', PARAM_ALPHA);
2953 $plugintype = required_param('plugin', PARAM_PLUGIN);
2954 $pluginaction = required_param('pluginaction', PARAM_ALPHA);
2956 $plugin = $this->get_plugin_by_type($pluginsubtype, $plugintype);
2957 if (!$plugin) {
2958 print_error('invalidformdata', '');
2959 return;
2962 $o .= $plugin->view_page($pluginaction);
2964 return $o;
2969 * This is used for team assignments to get the group for the specified user.
2970 * If the user is a member of multiple or no groups this will return false
2972 * @param int $userid The id of the user whose submission we want
2973 * @return mixed The group or false
2975 public function get_submission_group($userid) {
2977 if (isset($this->usersubmissiongroups[$userid])) {
2978 return $this->usersubmissiongroups[$userid];
2981 $groups = $this->get_all_groups($userid);
2982 if (count($groups) != 1) {
2983 $return = false;
2984 } else {
2985 $return = array_pop($groups);
2988 // Cache the user submission group.
2989 $this->usersubmissiongroups[$userid] = $return;
2991 return $return;
2995 * Gets all groups the user is a member of.
2997 * @param int $userid Teh id of the user who's groups we are checking
2998 * @return array The group objects
3000 public function get_all_groups($userid) {
3001 if (isset($this->usergroups[$userid])) {
3002 return $this->usergroups[$userid];
3005 $grouping = $this->get_instance()->teamsubmissiongroupingid;
3006 $return = groups_get_all_groups($this->get_course()->id, $userid, $grouping);
3008 $this->usergroups[$userid] = $return;
3010 return $return;
3015 * Display the submission that is used by a plugin.
3017 * Uses url parameters 'sid', 'gid' and 'plugin'.
3019 * @param string $pluginsubtype
3020 * @return string
3022 protected function view_plugin_content($pluginsubtype) {
3023 $o = '';
3025 $submissionid = optional_param('sid', 0, PARAM_INT);
3026 $gradeid = optional_param('gid', 0, PARAM_INT);
3027 $plugintype = required_param('plugin', PARAM_PLUGIN);
3028 $item = null;
3029 if ($pluginsubtype == 'assignsubmission') {
3030 $plugin = $this->get_submission_plugin_by_type($plugintype);
3031 if ($submissionid <= 0) {
3032 throw new coding_exception('Submission id should not be 0');
3034 $item = $this->get_submission($submissionid);
3036 // Check permissions.
3037 if (empty($item->userid)) {
3038 // Group submission.
3039 $this->require_view_group_submission($item->groupid);
3040 } else {
3041 $this->require_view_submission($item->userid);
3043 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
3044 $this->get_context(),
3045 $this->show_intro(),
3046 $this->get_course_module()->id,
3047 $plugin->get_name()));
3048 $o .= $this->get_renderer()->render(new assign_submission_plugin_submission($plugin,
3049 $item,
3050 assign_submission_plugin_submission::FULL,
3051 $this->get_course_module()->id,
3052 $this->get_return_action(),
3053 $this->get_return_params()));
3055 // Trigger event for viewing a submission.
3056 \mod_assign\event\submission_viewed::create_from_submission($this, $item)->trigger();
3058 } else {
3059 $plugin = $this->get_feedback_plugin_by_type($plugintype);
3060 if ($gradeid <= 0) {
3061 throw new coding_exception('Grade id should not be 0');
3063 $item = $this->get_grade($gradeid);
3064 // Check permissions.
3065 $this->require_view_submission($item->userid);
3066 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
3067 $this->get_context(),
3068 $this->show_intro(),
3069 $this->get_course_module()->id,
3070 $plugin->get_name()));
3071 $o .= $this->get_renderer()->render(new assign_feedback_plugin_feedback($plugin,
3072 $item,
3073 assign_feedback_plugin_feedback::FULL,
3074 $this->get_course_module()->id,
3075 $this->get_return_action(),
3076 $this->get_return_params()));
3078 // Trigger event for viewing feedback.
3079 \mod_assign\event\feedback_viewed::create_from_grade($this, $item)->trigger();
3082 $o .= $this->view_return_links();
3084 $o .= $this->view_footer();
3086 return $o;
3090 * Rewrite plugin file urls so they resolve correctly in an exported zip.
3092 * @param string $text - The replacement text
3093 * @param stdClass $user - The user record
3094 * @param assign_plugin $plugin - The assignment plugin
3096 public function download_rewrite_pluginfile_urls($text, $user, $plugin) {
3097 // The groupname prefix for the urls doesn't depend on the group mode of the assignment instance.
3098 // Rather, it should be determined by checking the group submission settings of the instance,
3099 // which is what download_submission() does when generating the file name prefixes.
3100 $groupname = '';
3101 if ($this->get_instance()->teamsubmission) {
3102 $submissiongroup = $this->get_submission_group($user->id);
3103 if ($submissiongroup) {
3104 $groupname = $submissiongroup->name . '-';
3105 } else {
3106 $groupname = get_string('defaultteam', 'assign') . '-';
3110 if ($this->is_blind_marking()) {
3111 $prefix = $groupname . get_string('participant', 'assign');
3112 $prefix = str_replace('_', ' ', $prefix);
3113 $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($user->id) . '_');
3114 } else {
3115 $prefix = $groupname . fullname($user);
3116 $prefix = str_replace('_', ' ', $prefix);
3117 $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($user->id) . '_');
3120 // Only prefix files if downloadasfolders user preference is NOT set.
3121 if (!get_user_preferences('assign_downloadasfolders', 1)) {
3122 $subtype = $plugin->get_subtype();
3123 $type = $plugin->get_type();
3124 $prefix = $prefix . $subtype . '_' . $type . '_';
3125 } else {
3126 $prefix = "";
3128 $result = str_replace('@@PLUGINFILE@@/', $prefix, $text);
3130 return $result;
3134 * Render the content in editor that is often used by plugin.
3136 * @param string $filearea
3137 * @param int $submissionid
3138 * @param string $plugintype
3139 * @param string $editor
3140 * @param string $component
3141 * @return string
3143 public function render_editor_content($filearea, $submissionid, $plugintype, $editor, $component) {
3144 global $CFG;
3146 $result = '';
3148 $plugin = $this->get_submission_plugin_by_type($plugintype);
3150 $text = $plugin->get_editor_text($editor, $submissionid);
3151 $format = $plugin->get_editor_format($editor, $submissionid);
3153 $finaltext = file_rewrite_pluginfile_urls($text,
3154 'pluginfile.php',
3155 $this->get_context()->id,
3156 $component,
3157 $filearea,
3158 $submissionid);
3159 $params = array('overflowdiv' => true, 'context' => $this->get_context());
3160 $result .= format_text($finaltext, $format, $params);
3162 if ($CFG->enableportfolios && has_capability('mod/assign:exportownsubmission', $this->context)) {
3163 require_once($CFG->libdir . '/portfoliolib.php');
3165 $button = new portfolio_add_button();
3166 $portfolioparams = array('cmid' => $this->get_course_module()->id,
3167 'sid' => $submissionid,
3168 'plugin' => $plugintype,
3169 'editor' => $editor,
3170 'area'=>$filearea);
3171 $button->set_callback_options('assign_portfolio_caller', $portfolioparams, 'mod_assign');
3172 $fs = get_file_storage();
3174 if ($files = $fs->get_area_files($this->context->id,
3175 $component,
3176 $filearea,
3177 $submissionid,
3178 'timemodified',
3179 false)) {
3180 $button->set_formats(PORTFOLIO_FORMAT_RICHHTML);
3181 } else {
3182 $button->set_formats(PORTFOLIO_FORMAT_PLAINHTML);
3184 $result .= $button->to_html();
3186 return $result;
3190 * Display a continue page after grading.
3192 * @param string $message - The message to display.
3193 * @return string
3195 protected function view_savegrading_result($message) {
3196 $o = '';
3197 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
3198 $this->get_context(),
3199 $this->show_intro(),
3200 $this->get_course_module()->id,
3201 get_string('savegradingresult', 'assign')));
3202 $gradingresult = new assign_gradingmessage(get_string('savegradingresult', 'assign'),
3203 $message,
3204 $this->get_course_module()->id);
3205 $o .= $this->get_renderer()->render($gradingresult);
3206 $o .= $this->view_footer();
3207 return $o;
3210 * Display a continue page after quickgrading.
3212 * @param string $message - The message to display.
3213 * @return string
3215 protected function view_quickgrading_result($message) {
3216 $o = '';
3217 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
3218 $this->get_context(),
3219 $this->show_intro(),
3220 $this->get_course_module()->id,
3221 get_string('quickgradingresult', 'assign')));
3222 $lastpage = optional_param('lastpage', null, PARAM_INT);
3223 $gradingresult = new assign_gradingmessage(get_string('quickgradingresult', 'assign'),
3224 $message,
3225 $this->get_course_module()->id,
3226 false,
3227 $lastpage);
3228 $o .= $this->get_renderer()->render($gradingresult);
3229 $o .= $this->view_footer();
3230 return $o;
3234 * Display the page footer.
3236 * @return string
3238 protected function view_footer() {
3239 // When viewing the footer during PHPUNIT tests a set_state error is thrown.
3240 if (!PHPUNIT_TEST) {
3241 return $this->get_renderer()->render_footer();
3244 return '';
3248 * Throw an error if the permissions to view this users' group submission are missing.
3250 * @param int $groupid Group id.
3251 * @throws required_capability_exception
3253 public function require_view_group_submission($groupid) {
3254 if (!$this->can_view_group_submission($groupid)) {
3255 throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', '');
3260 * Throw an error if the permissions to view this users submission are missing.
3262 * @throws required_capability_exception
3263 * @return none
3265 public function require_view_submission($userid) {
3266 if (!$this->can_view_submission($userid)) {
3267 throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', '');
3272 * Throw an error if the permissions to view grades in this assignment are missing.
3274 * @throws required_capability_exception
3275 * @return none
3277 public function require_view_grades() {
3278 if (!$this->can_view_grades()) {
3279 throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', '');
3284 * Does this user have view grade or grade permission for this assignment?
3286 * @return bool
3288 public function can_view_grades() {
3289 // Permissions check.
3290 if (!has_any_capability(array('mod/assign:viewgrades', 'mod/assign:grade'), $this->context)) {
3291 return false;
3293 // Checks for the edge case when user belongs to no groups and groupmode is sep.
3294 if ($this->get_course_module()->effectivegroupmode == SEPARATEGROUPS) {
3295 $groupflag = has_capability('moodle/site:accessallgroups', $this->get_context());
3296 $groupflag = $groupflag || !empty(groups_get_activity_allowed_groups($this->get_course_module()));
3297 return (bool)$groupflag;
3299 return true;
3303 * Does this user have grade permission for this assignment?
3305 * @return bool
3307 public function can_grade() {
3308 // Permissions check.
3309 if (!has_capability('mod/assign:grade', $this->context)) {
3310 return false;
3313 return true;
3317 * Download a zip file of all assignment submissions.
3319 * @param array $userids Array of user ids to download assignment submissions in a zip file
3320 * @return string - If an error occurs, this will contain the error page.
3322 protected function download_submissions($userids = false) {
3323 global $CFG, $DB;
3325 // More efficient to load this here.
3326 require_once($CFG->libdir.'/filelib.php');
3328 // Increase the server timeout to handle the creation and sending of large zip files.
3329 core_php_time_limit::raise();
3331 $this->require_view_grades();
3333 // Load all users with submit.
3334 $students = get_enrolled_users($this->context, "mod/assign:submit", null, 'u.*', null, null, null,
3335 $this->show_only_active_users());
3337 // Build a list of files to zip.
3338 $filesforzipping = array();
3339 $fs = get_file_storage();
3341 $groupmode = groups_get_activity_groupmode($this->get_course_module());
3342 // All users.
3343 $groupid = 0;
3344 $groupname = '';
3345 if ($groupmode) {
3346 $groupid = groups_get_activity_group($this->get_course_module(), true);
3347 if (!empty($groupid)) {
3348 $groupname = groups_get_group_name($groupid) . '-';
3352 // Construct the zip file name.
3353 $filename = clean_filename($this->get_course()->shortname . '-' .
3354 $this->get_instance()->name . '-' .
3355 $groupname.$this->get_course_module()->id . '.zip');
3357 // Get all the files for each student.
3358 foreach ($students as $student) {
3359 $userid = $student->id;
3360 // Download all assigments submission or only selected users.
3361 if ($userids and !in_array($userid, $userids)) {
3362 continue;
3365 if ((groups_is_member($groupid, $userid) or !$groupmode or !$groupid)) {
3366 // Get the plugins to add their own files to the zip.
3368 $submissiongroup = false;
3369 $groupname = '';
3370 if ($this->get_instance()->teamsubmission) {
3371 $submission = $this->get_group_submission($userid, 0, false);
3372 $submissiongroup = $this->get_submission_group($userid);
3373 if ($submissiongroup) {
3374 $groupname = $submissiongroup->name . '-';
3375 } else {
3376 $groupname = get_string('defaultteam', 'assign') . '-';
3378 } else {
3379 $submission = $this->get_user_submission($userid, false);
3382 if ($this->is_blind_marking()) {
3383 $prefix = str_replace('_', ' ', $groupname . get_string('participant', 'assign'));
3384 $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($userid));
3385 } else {
3386 $prefix = str_replace('_', ' ', $groupname . fullname($student));
3387 $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($userid));
3390 if ($submission) {
3391 $downloadasfolders = get_user_preferences('assign_downloadasfolders', 1);
3392 foreach ($this->submissionplugins as $plugin) {
3393 if ($plugin->is_enabled() && $plugin->is_visible()) {
3394 if ($downloadasfolders) {
3395 // Create a folder for each user for each assignment plugin.
3396 // This is the default behavior for version of Moodle >= 3.1.
3397 $submission->exportfullpath = true;
3398 $pluginfiles = $plugin->get_files($submission, $student);
3399 foreach ($pluginfiles as $zipfilepath => $file) {
3400 $subtype = $plugin->get_subtype();
3401 $type = $plugin->get_type();
3402 $zipfilename = basename($zipfilepath);
3403 $prefixedfilename = clean_filename($prefix .
3404 '_' .
3405 $subtype .
3406 '_' .
3407 $type .
3408 '_');
3409 if ($type == 'file') {
3410 $pathfilename = $prefixedfilename . $file->get_filepath() . $zipfilename;
3411 } else if ($type == 'onlinetext') {
3412 $pathfilename = $prefixedfilename . '/' . $zipfilename;
3413 } else {
3414 $pathfilename = $prefixedfilename . '/' . $zipfilename;
3416 $pathfilename = clean_param($pathfilename, PARAM_PATH);
3417 $filesforzipping[$pathfilename] = $file;
3419 } else {
3420 // Create a single folder for all users of all assignment plugins.
3421 // This was the default behavior for version of Moodle < 3.1.
3422 $submission->exportfullpath = false;
3423 $pluginfiles = $plugin->get_files($submission, $student);
3424 foreach ($pluginfiles as $zipfilename => $file) {
3425 $subtype = $plugin->get_subtype();
3426 $type = $plugin->get_type();
3427 $prefixedfilename = clean_filename($prefix .
3428 '_' .
3429 $subtype .
3430 '_' .
3431 $type .
3432 '_' .
3433 $zipfilename);
3434 $filesforzipping[$prefixedfilename] = $file;
3442 $result = '';
3443 if (count($filesforzipping) == 0) {
3444 $header = new assign_header($this->get_instance(),
3445 $this->get_context(),
3447 $this->get_course_module()->id,
3448 get_string('downloadall', 'assign'));
3449 $result .= $this->get_renderer()->render($header);
3450 $result .= $this->get_renderer()->notification(get_string('nosubmission', 'assign'));
3451 $url = new moodle_url('/mod/assign/view.php', array('id'=>$this->get_course_module()->id,
3452 'action'=>'grading'));
3453 $result .= $this->get_renderer()->continue_button($url);
3454 $result .= $this->view_footer();
3455 } else if ($zipfile = $this->pack_files($filesforzipping)) {
3456 \mod_assign\event\all_submissions_downloaded::create_from_assign($this)->trigger();
3457 // Send file and delete after sending.
3458 send_temp_file($zipfile, $filename);
3459 // We will not get here - send_temp_file calls exit.
3461 return $result;
3465 * Util function to add a message to the log.
3467 * @deprecated since 2.7 - Use new events system instead.
3468 * (see http://docs.moodle.org/dev/Migrating_logging_calls_in_plugins).
3470 * @param string $action The current action
3471 * @param string $info A detailed description of the change. But no more than 255 characters.
3472 * @param string $url The url to the assign module instance.
3473 * @param bool $return If true, returns the arguments, else adds to log. The purpose of this is to
3474 * retrieve the arguments to use them with the new event system (Event 2).
3475 * @return void|array
3477 public function add_to_log($action = '', $info = '', $url='', $return = false) {
3478 global $USER;
3480 $fullurl = 'view.php?id=' . $this->get_course_module()->id;
3481 if ($url != '') {
3482 $fullurl .= '&' . $url;
3485 $args = array(
3486 $this->get_course()->id,
3487 'assign',
3488 $action,
3489 $fullurl,
3490 $info,
3491 $this->get_course_module()->id
3494 if ($return) {
3495 // We only need to call debugging when returning a value. This is because the call to
3496 // call_user_func_array('add_to_log', $args) will trigger a debugging message of it's own.
3497 debugging('The mod_assign add_to_log() function is now deprecated.', DEBUG_DEVELOPER);
3498 return $args;
3500 call_user_func_array('add_to_log', $args);
3504 * Lazy load the page renderer and expose the renderer to plugins.
3506 * @return assign_renderer
3508 public function get_renderer() {
3509 global $PAGE;
3510 if ($this->output) {
3511 return $this->output;
3513 $this->output = $PAGE->get_renderer('mod_assign', null, RENDERER_TARGET_GENERAL);
3514 return $this->output;
3518 * Load the submission object for a particular user, optionally creating it if required.
3520 * For team assignments there are 2 submissions - the student submission and the team submission
3521 * All files are associated with the team submission but the status of the students contribution is
3522 * recorded separately.
3524 * @param int $userid The id of the user whose submission we want or 0 in which case USER->id is used
3525 * @param bool $create If set to true a new submission object will be created in the database with the status set to "new".
3526 * @param int $attemptnumber - -1 means the latest attempt
3527 * @return stdClass The submission
3529 public function get_user_submission($userid, $create, $attemptnumber=-1) {
3530 global $DB, $USER;
3532 if (!$userid) {
3533 $userid = $USER->id;
3535 // If the userid is not null then use userid.
3536 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid, 'groupid'=>0);
3537 if ($attemptnumber >= 0) {
3538 $params['attemptnumber'] = $attemptnumber;
3541 // Only return the row with the highest attemptnumber.
3542 $submission = null;
3543 $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', '*', 0, 1);
3544 if ($submissions) {
3545 $submission = reset($submissions);
3548 if ($submission) {
3549 return $submission;
3551 if ($create) {
3552 $submission = new stdClass();
3553 $submission->assignment = $this->get_instance()->id;
3554 $submission->userid = $userid;
3555 $submission->timecreated = time();
3556 $submission->timemodified = $submission->timecreated;
3557 $submission->status = ASSIGN_SUBMISSION_STATUS_NEW;
3558 if ($attemptnumber >= 0) {
3559 $submission->attemptnumber = $attemptnumber;
3560 } else {
3561 $submission->attemptnumber = 0;
3563 // Work out if this is the latest submission.
3564 $submission->latest = 0;
3565 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid, 'groupid'=>0);
3566 if ($attemptnumber == -1) {
3567 // This is a new submission so it must be the latest.
3568 $submission->latest = 1;
3569 } else {
3570 // We need to work this out.
3571 $result = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', 'attemptnumber', 0, 1);
3572 $latestsubmission = null;
3573 if ($result) {
3574 $latestsubmission = reset($result);
3576 if (empty($latestsubmission) || ($attemptnumber > $latestsubmission->attemptnumber)) {
3577 $submission->latest = 1;
3580 if ($submission->latest) {
3581 // This is the case when we need to set latest to 0 for all the other attempts.
3582 $DB->set_field('assign_submission', 'latest', 0, $params);
3584 $sid = $DB->insert_record('assign_submission', $submission);
3585 return $DB->get_record('assign_submission', array('id' => $sid));
3587 return false;
3591 * Load the submission object from it's id.
3593 * @param int $submissionid The id of the submission we want
3594 * @return stdClass The submission
3596 protected function get_submission($submissionid) {
3597 global $DB;
3599 $params = array('assignment'=>$this->get_instance()->id, 'id'=>$submissionid);
3600 return $DB->get_record('assign_submission', $params, '*', MUST_EXIST);
3604 * This will retrieve a user flags object from the db optionally creating it if required.
3605 * The user flags was split from the user_grades table in 2.5.
3607 * @param int $userid The user we are getting the flags for.
3608 * @param bool $create If true the flags record will be created if it does not exist
3609 * @return stdClass The flags record
3611 public function get_user_flags($userid, $create) {
3612 global $DB, $USER;
3614 // If the userid is not null then use userid.
3615 if (!$userid) {
3616 $userid = $USER->id;
3619 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
3621 $flags = $DB->get_record('assign_user_flags', $params);
3623 if ($flags) {
3624 return $flags;
3626 if ($create) {
3627 $flags = new stdClass();
3628 $flags->assignment = $this->get_instance()->id;
3629 $flags->userid = $userid;
3630 $flags->locked = 0;
3631 $flags->extensionduedate = 0;
3632 $flags->workflowstate = '';
3633 $flags->allocatedmarker = 0;
3635 // The mailed flag can be one of 3 values: 0 is unsent, 1 is sent and 2 is do not send yet.
3636 // This is because students only want to be notified about certain types of update (grades and feedback).
3637 $flags->mailed = 2;
3639 $fid = $DB->insert_record('assign_user_flags', $flags);
3640 $flags->id = $fid;
3641 return $flags;
3643 return false;
3647 * This will retrieve a grade object from the db, optionally creating it if required.
3649 * @param int $userid The user we are grading
3650 * @param bool $create If true the grade will be created if it does not exist
3651 * @param int $attemptnumber The attempt number to retrieve the grade for. -1 means the latest submission.
3652 * @return stdClass The grade record
3654 public function get_user_grade($userid, $create, $attemptnumber=-1) {
3655 global $DB, $USER;
3657 // If the userid is not null then use userid.
3658 if (!$userid) {
3659 $userid = $USER->id;
3661 $submission = null;
3663 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
3664 if ($attemptnumber < 0 || $create) {
3665 // Make sure this grade matches the latest submission attempt.
3666 if ($this->get_instance()->teamsubmission) {
3667 $submission = $this->get_group_submission($userid, 0, true, $attemptnumber);
3668 } else {
3669 $submission = $this->get_user_submission($userid, true, $attemptnumber);
3671 if ($submission) {
3672 $attemptnumber = $submission->attemptnumber;
3676 if ($attemptnumber >= 0) {
3677 $params['attemptnumber'] = $attemptnumber;
3680 $grades = $DB->get_records('assign_grades', $params, 'attemptnumber DESC', '*', 0, 1);
3682 if ($grades) {
3683 return reset($grades);
3685 if ($create) {
3686 $grade = new stdClass();
3687 $grade->assignment = $this->get_instance()->id;
3688 $grade->userid = $userid;
3689 $grade->timecreated = time();
3690 // If we are "auto-creating" a grade - and there is a submission
3691 // the new grade should not have a more recent timemodified value
3692 // than the submission.
3693 if ($submission) {
3694 $grade->timemodified = $submission->timemodified;
3695 } else {
3696 $grade->timemodified = $grade->timecreated;
3698 $grade->grade = -1;
3699 $grade->grader = $USER->id;
3700 if ($attemptnumber >= 0) {
3701 $grade->attemptnumber = $attemptnumber;
3704 $gid = $DB->insert_record('assign_grades', $grade);
3705 $grade->id = $gid;
3706 return $grade;
3708 return false;
3712 * This will retrieve a grade object from the db.
3714 * @param int $gradeid The id of the grade
3715 * @return stdClass The grade record
3717 protected function get_grade($gradeid) {
3718 global $DB;
3720 $params = array('assignment'=>$this->get_instance()->id, 'id'=>$gradeid);
3721 return $DB->get_record('assign_grades', $params, '*', MUST_EXIST);
3725 * Print the grading page for a single user submission.
3727 * @param array $args Optional args array (better than pulling args from _GET and _POST)
3728 * @return string
3730 protected function view_single_grading_panel($args) {
3731 global $DB, $CFG, $SESSION, $PAGE;
3733 $o = '';
3734 $instance = $this->get_instance();
3736 require_once($CFG->dirroot . '/mod/assign/gradeform.php');
3738 // Need submit permission to submit an assignment.
3739 require_capability('mod/assign:grade', $this->context);
3741 // If userid is passed - we are only grading a single student.
3742 $userid = $args['userid'];
3743 $attemptnumber = $args['attemptnumber'];
3745 // Apply overrides.
3746 $this->update_effective_access($userid);
3748 $rownum = 0;
3749 $useridlist = array($userid);
3751 $last = true;
3752 // This variation on the url will link direct to this student, with no next/previous links.
3753 // The benefit is the url will be the same every time for this student, so Atto autosave drafts can match up.
3754 $returnparams = array('userid' => $userid, 'rownum' => 0, 'useridlistid' => 0);
3755 $this->register_return_link('grade', $returnparams);
3757 $user = $DB->get_record('user', array('id' => $userid));
3758 $submission = $this->get_user_submission($userid, false, $attemptnumber);
3759 $submissiongroup = null;
3760 $teamsubmission = null;
3761 $notsubmitted = array();
3762 if ($instance->teamsubmission) {
3763 $teamsubmission = $this->get_group_submission($userid, 0, false, $attemptnumber);
3764 $submissiongroup = $this->get_submission_group($userid);
3765 $groupid = 0;
3766 if ($submissiongroup) {
3767 $groupid = $submissiongroup->id;
3769 $notsubmitted = $this->get_submission_group_members_who_have_not_submitted($groupid, false);
3773 // Get the requested grade.
3774 $grade = $this->get_user_grade($userid, false, $attemptnumber);
3775 $flags = $this->get_user_flags($userid, false);
3776 if ($this->can_view_submission($userid)) {
3777 $gradelocked = ($flags && $flags->locked) || $this->grading_disabled($userid);
3778 $extensionduedate = null;
3779 if ($flags) {
3780 $extensionduedate = $flags->extensionduedate;
3782 $showedit = $this->submissions_open($userid) && ($this->is_any_submission_plugin_enabled());
3783 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
3784 $usergroups = $this->get_all_groups($user->id);
3786 $submissionstatus = new assign_submission_status_compact($instance->allowsubmissionsfromdate,
3787 $instance->alwaysshowdescription,
3788 $submission,
3789 $instance->teamsubmission,
3790 $teamsubmission,
3791 $submissiongroup,
3792 $notsubmitted,
3793 $this->is_any_submission_plugin_enabled(),
3794 $gradelocked,
3795 $this->is_graded($userid),
3796 $instance->duedate,
3797 $instance->cutoffdate,
3798 $this->get_submission_plugins(),
3799 $this->get_return_action(),
3800 $this->get_return_params(),
3801 $this->get_course_module()->id,
3802 $this->get_course()->id,
3803 assign_submission_status::GRADER_VIEW,
3804 $showedit,
3805 false,
3806 $viewfullnames,
3807 $extensionduedate,
3808 $this->get_context(),
3809 $this->is_blind_marking(),
3811 $instance->attemptreopenmethod,
3812 $instance->maxattempts,
3813 $this->get_grading_status($userid),
3814 $instance->preventsubmissionnotingroup,
3815 $usergroups);
3816 $o .= $this->get_renderer()->render($submissionstatus);
3819 if ($grade) {
3820 $data = new stdClass();
3821 if ($grade->grade !== null && $grade->grade >= 0) {
3822 $data->grade = format_float($grade->grade, $this->get_grade_item()->get_decimals());
3824 } else {
3825 $data = new stdClass();
3826 $data->grade = '';
3829 if (!empty($flags->workflowstate)) {
3830 $data->workflowstate = $flags->workflowstate;
3832 if (!empty($flags->allocatedmarker)) {
3833 $data->allocatedmarker = $flags->allocatedmarker;
3836 // Warning if required.
3837 $allsubmissions = $this->get_all_submissions($userid);
3839 if ($attemptnumber != -1 && ($attemptnumber + 1) != count($allsubmissions)) {
3840 $params = array('attemptnumber' => $attemptnumber + 1,
3841 'totalattempts' => count($allsubmissions));
3842 $message = get_string('editingpreviousfeedbackwarning', 'assign', $params);
3843 $o .= $this->get_renderer()->notification($message);
3846 $pagination = array('rownum' => $rownum,
3847 'useridlistid' => 0,
3848 'last' => $last,
3849 'userid' => $userid,
3850 'attemptnumber' => $attemptnumber,
3851 'gradingpanel' => true);
3853 if (!empty($args['formdata'])) {
3854 $data = (array) $data;
3855 $data = (object) array_merge($data, $args['formdata']);
3857 $formparams = array($this, $data, $pagination);
3858 $mform = new mod_assign_grade_form(null,
3859 $formparams,
3860 'post',
3862 array('class' => 'gradeform'));
3864 if (!empty($args['formdata'])) {
3865 // If we were passed form data - we want the form to check the data
3866 // and show errors.
3867 $mform->is_validated();
3869 $o .= $this->get_renderer()->heading(get_string('grade'), 3);
3870 $o .= $this->get_renderer()->render(new assign_form('gradingform', $mform));
3872 if (count($allsubmissions) > 1) {
3873 $allgrades = $this->get_all_grades($userid);
3874 $history = new assign_attempt_history_chooser($allsubmissions,
3875 $allgrades,
3876 $this->get_course_module()->id,
3877 $userid);
3879 $o .= $this->get_renderer()->render($history);
3882 \mod_assign\event\grading_form_viewed::create_from_user($this, $user)->trigger();
3884 return $o;
3888 * Print the grading page for a single user submission.
3890 * @param moodleform $mform
3891 * @return string
3893 protected function view_single_grade_page($mform) {
3894 global $DB, $CFG, $SESSION;
3896 $o = '';
3897 $instance = $this->get_instance();
3899 require_once($CFG->dirroot . '/mod/assign/gradeform.php');
3901 // Need submit permission to submit an assignment.
3902 require_capability('mod/assign:grade', $this->context);
3904 $header = new assign_header($instance,
3905 $this->get_context(),
3906 false,
3907 $this->get_course_module()->id,
3908 get_string('grading', 'assign'));
3909 $o .= $this->get_renderer()->render($header);
3911 // If userid is passed - we are only grading a single student.
3912 $rownum = optional_param('rownum', 0, PARAM_INT);
3913 $useridlistid = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
3914 $userid = optional_param('userid', 0, PARAM_INT);
3915 $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT);
3917 if (!$userid) {
3918 $useridlistkey = $this->get_useridlist_key($useridlistid);
3919 if (empty($SESSION->mod_assign_useridlist[$useridlistkey])) {
3920 $SESSION->mod_assign_useridlist[$useridlistkey] = $this->get_grading_userid_list();
3922 $useridlist = $SESSION->mod_assign_useridlist[$useridlistkey];
3923 } else {
3924 $rownum = 0;
3925 $useridlistid = 0;
3926 $useridlist = array($userid);
3929 if ($rownum < 0 || $rownum > count($useridlist)) {
3930 throw new coding_exception('Row is out of bounds for the current grading table: ' . $rownum);
3933 $last = false;
3934 $userid = $useridlist[$rownum];
3935 if ($rownum == count($useridlist) - 1) {
3936 $last = true;
3938 // This variation on the url will link direct to this student, with no next/previous links.
3939 // The benefit is the url will be the same every time for this student, so Atto autosave drafts can match up.
3940 $returnparams = array('userid' => $userid, 'rownum' => 0, 'useridlistid' => 0);
3941 $this->register_return_link('grade', $returnparams);
3943 $user = $DB->get_record('user', array('id' => $userid));
3944 if ($user) {
3945 $this->update_effective_access($userid);
3946 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
3947 $usersummary = new assign_user_summary($user,
3948 $this->get_course()->id,
3949 $viewfullnames,
3950 $this->is_blind_marking(),
3951 $this->get_uniqueid_for_user($user->id),
3952 get_extra_user_fields($this->get_context()),
3953 !$this->is_active_user($userid));
3954 $o .= $this->get_renderer()->render($usersummary);
3956 $submission = $this->get_user_submission($userid, false, $attemptnumber);
3957 $submissiongroup = null;
3958 $teamsubmission = null;
3959 $notsubmitted = array();
3960 if ($instance->teamsubmission) {
3961 $teamsubmission = $this->get_group_submission($userid, 0, false, $attemptnumber);
3962 $submissiongroup = $this->get_submission_group($userid);
3963 $groupid = 0;
3964 if ($submissiongroup) {
3965 $groupid = $submissiongroup->id;
3967 $notsubmitted = $this->get_submission_group_members_who_have_not_submitted($groupid, false);
3971 // Get the requested grade.
3972 $grade = $this->get_user_grade($userid, false, $attemptnumber);
3973 $flags = $this->get_user_flags($userid, false);
3974 if ($this->can_view_submission($userid)) {
3975 $gradelocked = ($flags && $flags->locked) || $this->grading_disabled($userid);
3976 $extensionduedate = null;
3977 if ($flags) {
3978 $extensionduedate = $flags->extensionduedate;
3980 $showedit = $this->submissions_open($userid) && ($this->is_any_submission_plugin_enabled());
3981 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
3982 $usergroups = $this->get_all_groups($user->id);
3984 $submissionstatus = new assign_submission_status($instance->allowsubmissionsfromdate,
3985 $instance->alwaysshowdescription,
3986 $submission,
3987 $instance->teamsubmission,
3988 $teamsubmission,
3989 $submissiongroup,
3990 $notsubmitted,
3991 $this->is_any_submission_plugin_enabled(),
3992 $gradelocked,
3993 $this->is_graded($userid),
3994 $instance->duedate,
3995 $instance->cutoffdate,
3996 $this->get_submission_plugins(),
3997 $this->get_return_action(),
3998 $this->get_return_params(),
3999 $this->get_course_module()->id,
4000 $this->get_course()->id,
4001 assign_submission_status::GRADER_VIEW,
4002 $showedit,
4003 false,
4004 $viewfullnames,
4005 $extensionduedate,
4006 $this->get_context(),
4007 $this->is_blind_marking(),
4009 $instance->attemptreopenmethod,
4010 $instance->maxattempts,
4011 $this->get_grading_status($userid),
4012 $instance->preventsubmissionnotingroup,
4013 $usergroups);
4014 $o .= $this->get_renderer()->render($submissionstatus);
4017 if ($grade) {
4018 $data = new stdClass();
4019 if ($grade->grade !== null && $grade->grade >= 0) {
4020 $data->grade = format_float($grade->grade, $this->get_grade_item()->get_decimals());
4022 } else {
4023 $data = new stdClass();
4024 $data->grade = '';
4027 if (!empty($flags->workflowstate)) {
4028 $data->workflowstate = $flags->workflowstate;
4030 if (!empty($flags->allocatedmarker)) {
4031 $data->allocatedmarker = $flags->allocatedmarker;
4034 // Warning if required.
4035 $allsubmissions = $this->get_all_submissions($userid);
4037 if ($attemptnumber != -1 && ($attemptnumber + 1) != count($allsubmissions)) {
4038 $params = array('attemptnumber'=>$attemptnumber + 1,
4039 'totalattempts'=>count($allsubmissions));
4040 $message = get_string('editingpreviousfeedbackwarning', 'assign', $params);
4041 $o .= $this->get_renderer()->notification($message);
4044 // Now show the grading form.
4045 if (!$mform) {
4046 $pagination = array('rownum' => $rownum,
4047 'useridlistid' => $useridlistid,
4048 'last' => $last,
4049 'userid' => $userid,
4050 'attemptnumber' => $attemptnumber);
4051 $formparams = array($this, $data, $pagination);
4052 $mform = new mod_assign_grade_form(null,
4053 $formparams,
4054 'post',
4056 array('class'=>'gradeform'));
4058 $o .= $this->get_renderer()->heading(get_string('grade'), 3);
4059 $o .= $this->get_renderer()->render(new assign_form('gradingform', $mform));
4061 if (count($allsubmissions) > 1 && $attemptnumber == -1) {
4062 $allgrades = $this->get_all_grades($userid);
4063 $history = new assign_attempt_history($allsubmissions,
4064 $allgrades,
4065 $this->get_submission_plugins(),
4066 $this->get_feedback_plugins(),
4067 $this->get_course_module()->id,
4068 $this->get_return_action(),
4069 $this->get_return_params(),
4070 true,
4071 $useridlistid,
4072 $rownum);
4074 $o .= $this->get_renderer()->render($history);
4077 \mod_assign\event\grading_form_viewed::create_from_user($this, $user)->trigger();
4079 $o .= $this->view_footer();
4080 return $o;
4084 * Show a confirmation page to make sure they want to release student identities.
4086 * @return string
4088 protected function view_reveal_identities_confirm() {
4089 require_capability('mod/assign:revealidentities', $this->get_context());
4091 $o = '';
4092 $header = new assign_header($this->get_instance(),
4093 $this->get_context(),
4094 false,
4095 $this->get_course_module()->id);
4096 $o .= $this->get_renderer()->render($header);
4098 $urlparams = array('id'=>$this->get_course_module()->id,
4099 'action'=>'revealidentitiesconfirm',
4100 'sesskey'=>sesskey());
4101 $confirmurl = new moodle_url('/mod/assign/view.php', $urlparams);
4103 $urlparams = array('id'=>$this->get_course_module()->id,
4104 'action'=>'grading');
4105 $cancelurl = new moodle_url('/mod/assign/view.php', $urlparams);
4107 $o .= $this->get_renderer()->confirm(get_string('revealidentitiesconfirm', 'assign'),
4108 $confirmurl,
4109 $cancelurl);
4110 $o .= $this->view_footer();
4112 \mod_assign\event\reveal_identities_confirmation_page_viewed::create_from_assign($this)->trigger();
4114 return $o;
4118 * View a link to go back to the previous page. Uses url parameters returnaction and returnparams.
4120 * @return string
4122 protected function view_return_links() {
4123 $returnaction = optional_param('returnaction', '', PARAM_ALPHA);
4124 $returnparams = optional_param('returnparams', '', PARAM_TEXT);
4126 $params = array();
4127 $returnparams = str_replace('&amp;', '&', $returnparams);
4128 parse_str($returnparams, $params);
4129 $newparams = array('id' => $this->get_course_module()->id, 'action' => $returnaction);
4130 $params = array_merge($newparams, $params);
4132 $url = new moodle_url('/mod/assign/view.php', $params);
4133 return $this->get_renderer()->single_button($url, get_string('back'), 'get');
4137 * View the grading table of all submissions for this assignment.
4139 * @return string
4141 protected function view_grading_table() {
4142 global $USER, $CFG, $SESSION;
4144 // Include grading options form.
4145 require_once($CFG->dirroot . '/mod/assign/gradingoptionsform.php');
4146 require_once($CFG->dirroot . '/mod/assign/quickgradingform.php');
4147 require_once($CFG->dirroot . '/mod/assign/gradingbatchoperationsform.php');
4148 $o = '';
4149 $cmid = $this->get_course_module()->id;
4151 $links = array();
4152 if (has_capability('gradereport/grader:view', $this->get_course_context()) &&
4153 has_capability('moodle/grade:viewall', $this->get_course_context())) {
4154 $gradebookurl = '/grade/report/grader/index.php?id=' . $this->get_course()->id;
4155 $links[$gradebookurl] = get_string('viewgradebook', 'assign');
4157 if ($this->is_any_submission_plugin_enabled() && $this->count_submissions()) {
4158 $downloadurl = '/mod/assign/view.php?id=' . $cmid . '&action=downloadall';
4159 $links[$downloadurl] = get_string('downloadall', 'assign');
4161 if ($this->is_blind_marking() &&
4162 has_capability('mod/assign:revealidentities', $this->get_context())) {
4163 $revealidentitiesurl = '/mod/assign/view.php?id=' . $cmid . '&action=revealidentities';
4164 $links[$revealidentitiesurl] = get_string('revealidentities', 'assign');
4166 foreach ($this->get_feedback_plugins() as $plugin) {
4167 if ($plugin->is_enabled() && $plugin->is_visible()) {
4168 foreach ($plugin->get_grading_actions() as $action => $description) {
4169 $url = '/mod/assign/view.php' .
4170 '?id=' . $cmid .
4171 '&plugin=' . $plugin->get_type() .
4172 '&pluginsubtype=assignfeedback' .
4173 '&action=viewpluginpage&pluginaction=' . $action;
4174 $links[$url] = $description;
4179 // Sort links alphabetically based on the link description.
4180 core_collator::asort($links);
4182 $gradingactions = new url_select($links);
4183 $gradingactions->set_label(get_string('choosegradingaction', 'assign'));
4185 $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
4187 $perpage = $this->get_assign_perpage();
4188 $filter = get_user_preferences('assign_filter', '');
4189 $markerfilter = get_user_preferences('assign_markerfilter', '');
4190 $workflowfilter = get_user_preferences('assign_workflowfilter', '');
4191 $controller = $gradingmanager->get_active_controller();
4192 $showquickgrading = empty($controller) && $this->can_grade();
4193 $quickgrading = get_user_preferences('assign_quickgrading', false);
4194 $showonlyactiveenrolopt = has_capability('moodle/course:viewsuspendedusers', $this->context);
4195 $downloadasfolders = get_user_preferences('assign_downloadasfolders', 1);
4197 $markingallocation = $this->get_instance()->markingworkflow &&
4198 $this->get_instance()->markingallocation &&
4199 has_capability('mod/assign:manageallocations', $this->context);
4200 // Get markers to use in drop lists.
4201 $markingallocationoptions = array();
4202 if ($markingallocation) {
4203 list($sort, $params) = users_order_by_sql('u');
4204 // Only enrolled users could be assigned as potential markers.
4205 $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
4206 $markingallocationoptions[''] = get_string('filternone', 'assign');
4207 $markingallocationoptions[ASSIGN_MARKER_FILTER_NO_MARKER] = get_string('markerfilternomarker', 'assign');
4208 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
4209 foreach ($markers as $marker) {
4210 $markingallocationoptions[$marker->id] = fullname($marker, $viewfullnames);
4214 $markingworkflow = $this->get_instance()->markingworkflow;
4215 // Get marking states to show in form.
4216 $markingworkflowoptions = array();
4217 if ($markingworkflow) {
4218 $notmarked = get_string('markingworkflowstatenotmarked', 'assign');
4219 $markingworkflowoptions[''] = get_string('filternone', 'assign');
4220 $markingworkflowoptions[ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED] = $notmarked;
4221 $markingworkflowoptions = array_merge($markingworkflowoptions, $this->get_marking_workflow_states_for_current_user());
4224 // Print options for changing the filter and changing the number of results per page.
4225 $gradingoptionsformparams = array('cm'=>$cmid,
4226 'contextid'=>$this->context->id,
4227 'userid'=>$USER->id,
4228 'submissionsenabled'=>$this->is_any_submission_plugin_enabled(),
4229 'showquickgrading'=>$showquickgrading,
4230 'quickgrading'=>$quickgrading,
4231 'markingworkflowopt'=>$markingworkflowoptions,
4232 'markingallocationopt'=>$markingallocationoptions,
4233 'showonlyactiveenrolopt'=>$showonlyactiveenrolopt,
4234 'showonlyactiveenrol' => $this->show_only_active_users(),
4235 'downloadasfolders' => $downloadasfolders);
4237 $classoptions = array('class'=>'gradingoptionsform');
4238 $gradingoptionsform = new mod_assign_grading_options_form(null,
4239 $gradingoptionsformparams,
4240 'post',
4242 $classoptions);
4244 $batchformparams = array('cm'=>$cmid,
4245 'submissiondrafts'=>$this->get_instance()->submissiondrafts,
4246 'duedate'=>$this->get_instance()->duedate,
4247 'attemptreopenmethod'=>$this->get_instance()->attemptreopenmethod,
4248 'feedbackplugins'=>$this->get_feedback_plugins(),
4249 'context'=>$this->get_context(),
4250 'markingworkflow'=>$markingworkflow,
4251 'markingallocation'=>$markingallocation);
4252 $classoptions = array('class'=>'gradingbatchoperationsform');
4254 $gradingbatchoperationsform = new mod_assign_grading_batch_operations_form(null,
4255 $batchformparams,
4256 'post',
4258 $classoptions);
4260 $gradingoptionsdata = new stdClass();
4261 $gradingoptionsdata->perpage = $perpage;
4262 $gradingoptionsdata->filter = $filter;
4263 $gradingoptionsdata->markerfilter = $markerfilter;
4264 $gradingoptionsdata->workflowfilter = $workflowfilter;
4265 $gradingoptionsform->set_data($gradingoptionsdata);
4267 $actionformtext = $this->get_renderer()->render($gradingactions);
4268 $header = new assign_header($this->get_instance(),
4269 $this->get_context(),
4270 false,
4271 $this->get_course_module()->id,
4272 get_string('grading', 'assign'),
4273 $actionformtext);
4274 $o .= $this->get_renderer()->render($header);
4276 $currenturl = $CFG->wwwroot .
4277 '/mod/assign/view.php?id=' .
4278 $this->get_course_module()->id .
4279 '&action=grading';
4281 $o .= groups_print_activity_menu($this->get_course_module(), $currenturl, true);
4283 // Plagiarism update status apearring in the grading book.
4284 if (!empty($CFG->enableplagiarism)) {
4285 require_once($CFG->libdir . '/plagiarismlib.php');
4286 $o .= plagiarism_update_status($this->get_course(), $this->get_course_module());
4289 if ($this->is_blind_marking() && has_capability('mod/assign:viewblinddetails', $this->get_context())) {
4290 $o .= $this->get_renderer()->notification(get_string('blindmarkingenabledwarning', 'assign'), 'notifymessage');
4293 // Load and print the table of submissions.
4294 if ($showquickgrading && $quickgrading) {
4295 $gradingtable = new assign_grading_table($this, $perpage, $filter, 0, true);
4296 $table = $this->get_renderer()->render($gradingtable);
4297 $page = optional_param('page', null, PARAM_INT);
4298 $quickformparams = array('cm'=>$this->get_course_module()->id,
4299 'gradingtable'=>$table,
4300 'sendstudentnotifications' => $this->get_instance()->sendstudentnotifications,
4301 'page' => $page);
4302 $quickgradingform = new mod_assign_quick_grading_form(null, $quickformparams);
4304 $o .= $this->get_renderer()->render(new assign_form('quickgradingform', $quickgradingform));
4305 } else {
4306 $gradingtable = new assign_grading_table($this, $perpage, $filter, 0, false);
4307 $o .= $this->get_renderer()->render($gradingtable);
4310 if ($this->can_grade()) {
4311 // We need to store the order of uses in the table as the person may wish to grade them.
4312 // This is done based on the row number of the user.
4313 $useridlist = $gradingtable->get_column_data('userid');
4314 $SESSION->mod_assign_useridlist[$this->get_useridlist_key()] = $useridlist;
4317 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
4318 $users = array_keys($this->list_participants($currentgroup, true));
4319 if (count($users) != 0 && $this->can_grade()) {
4320 // If no enrolled user in a course then don't display the batch operations feature.
4321 $assignform = new assign_form('gradingbatchoperationsform', $gradingbatchoperationsform);
4322 $o .= $this->get_renderer()->render($assignform);
4324 $assignform = new assign_form('gradingoptionsform',
4325 $gradingoptionsform,
4326 'M.mod_assign.init_grading_options');
4327 $o .= $this->get_renderer()->render($assignform);
4328 return $o;
4332 * View entire grader app.
4334 * @return string
4336 protected function view_grader() {
4337 global $USER, $PAGE;
4339 $o = '';
4340 // Need submit permission to submit an assignment.
4341 $this->require_view_grades();
4343 $PAGE->set_pagelayout('embedded');
4345 $PAGE->set_title($this->get_context()->get_context_name());
4347 $o .= $this->get_renderer()->header();
4349 $userid = optional_param('userid', 0, PARAM_INT);
4350 $blindid = optional_param('blindid', 0, PARAM_INT);
4352 if (!$userid && $blindid) {
4353 $userid = $this->get_user_id_for_uniqueid($blindid);
4356 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
4357 $framegrader = new grading_app($userid, $currentgroup, $this);
4359 $this->update_effective_access($userid);
4361 $o .= $this->get_renderer()->render($framegrader);
4363 $o .= $this->view_footer();
4365 \mod_assign\event\grading_table_viewed::create_from_assign($this)->trigger();
4367 return $o;
4370 * View entire grading page.
4372 * @return string
4374 protected function view_grading_page() {
4375 global $CFG;
4377 $o = '';
4378 // Need submit permission to submit an assignment.
4379 $this->require_view_grades();
4380 require_once($CFG->dirroot . '/mod/assign/gradeform.php');
4382 $this->add_grade_notices();
4384 // Only load this if it is.
4385 $o .= $this->view_grading_table();
4387 $o .= $this->view_footer();
4389 \mod_assign\event\grading_table_viewed::create_from_assign($this)->trigger();
4391 return $o;
4395 * Capture the output of the plagiarism plugins disclosures and return it as a string.
4397 * @return string
4399 protected function plagiarism_print_disclosure() {
4400 global $CFG;
4401 $o = '';
4403 if (!empty($CFG->enableplagiarism)) {
4404 require_once($CFG->libdir . '/plagiarismlib.php');
4406 $o .= plagiarism_print_disclosure($this->get_course_module()->id);
4409 return $o;
4413 * Message for students when assignment submissions have been closed.
4415 * @param string $title The page title
4416 * @param array $notices The array of notices to show.
4417 * @return string
4419 protected function view_notices($title, $notices) {
4420 global $CFG;
4422 $o = '';
4424 $header = new assign_header($this->get_instance(),
4425 $this->get_context(),
4426 $this->show_intro(),
4427 $this->get_course_module()->id,
4428 $title);
4429 $o .= $this->get_renderer()->render($header);
4431 foreach ($notices as $notice) {
4432 $o .= $this->get_renderer()->notification($notice);
4435 $url = new moodle_url('/mod/assign/view.php', array('id'=>$this->get_course_module()->id, 'action'=>'view'));
4436 $o .= $this->get_renderer()->continue_button($url);
4438 $o .= $this->view_footer();
4440 return $o;
4444 * Get the name for a user - hiding their real name if blind marking is on.
4446 * @param stdClass $user The user record as required by fullname()
4447 * @return string The name.
4449 public function fullname($user) {
4450 if ($this->is_blind_marking()) {
4451 $hasviewblind = has_capability('mod/assign:viewblinddetails', $this->get_context());
4452 if (empty($user->recordid)) {
4453 $uniqueid = $this->get_uniqueid_for_user($user->id);
4454 } else {
4455 $uniqueid = $user->recordid;
4457 if ($hasviewblind) {
4458 return get_string('participant', 'assign') . ' ' . $uniqueid . ' (' .
4459 fullname($user, has_capability('moodle/site:viewfullnames', $this->get_context())) . ')';
4460 } else {
4461 return get_string('participant', 'assign') . ' ' . $uniqueid;
4463 } else {
4464 return fullname($user, has_capability('moodle/site:viewfullnames', $this->get_context()));
4469 * View edit submissions page.
4471 * @param moodleform $mform
4472 * @param array $notices A list of notices to display at the top of the
4473 * edit submission form (e.g. from plugins).
4474 * @return string The page output.
4476 protected function view_edit_submission_page($mform, $notices) {
4477 global $CFG, $USER, $DB;
4479 $o = '';
4480 require_once($CFG->dirroot . '/mod/assign/submission_form.php');
4481 // Need submit permission to submit an assignment.
4482 $userid = optional_param('userid', $USER->id, PARAM_INT);
4483 $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
4485 // This variation on the url will link direct to this student.
4486 // The benefit is the url will be the same every time for this student, so Atto autosave drafts can match up.
4487 $returnparams = array('userid' => $userid, 'rownum' => 0, 'useridlistid' => 0);
4488 $this->register_return_link('editsubmission', $returnparams);
4490 if ($userid == $USER->id) {
4491 if (!$this->can_edit_submission($userid, $USER->id)) {
4492 print_error('nopermission');
4494 // User is editing their own submission.
4495 require_capability('mod/assign:submit', $this->context);
4496 $title = get_string('editsubmission', 'assign');
4497 } else {
4498 // User is editing another user's submission.
4499 if (!$this->can_edit_submission($userid, $USER->id)) {
4500 print_error('nopermission');
4503 $name = $this->fullname($user);
4504 $title = get_string('editsubmissionother', 'assign', $name);
4507 if (!$this->submissions_open($userid)) {
4508 $message = array(get_string('submissionsclosed', 'assign'));
4509 return $this->view_notices($title, $message);
4512 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
4513 $this->get_context(),
4514 $this->show_intro(),
4515 $this->get_course_module()->id,
4516 $title));
4517 if ($userid == $USER->id) {
4518 // We only show this if it their submission.
4519 $o .= $this->plagiarism_print_disclosure();
4521 $data = new stdClass();
4522 $data->userid = $userid;
4523 if (!$mform) {
4524 $mform = new mod_assign_submission_form(null, array($this, $data));
4527 foreach ($notices as $notice) {
4528 $o .= $this->get_renderer()->notification($notice);
4531 $o .= $this->get_renderer()->render(new assign_form('editsubmissionform', $mform));
4533 $o .= $this->view_footer();
4535 \mod_assign\event\submission_form_viewed::create_from_user($this, $user)->trigger();
4537 return $o;
4541 * See if this assignment has a grade yet.
4543 * @param int $userid
4544 * @return bool
4546 protected function is_graded($userid) {
4547 $grade = $this->get_user_grade($userid, false);
4548 if ($grade) {
4549 return ($grade->grade !== null && $grade->grade >= 0);
4551 return false;
4555 * Perform an access check to see if the current $USER can edit this group submission.
4557 * @param int $groupid
4558 * @return bool
4560 public function can_edit_group_submission($groupid) {
4561 global $USER;
4563 $members = $this->get_submission_group_members($groupid, true);
4564 foreach ($members as $member) {
4565 // If we can edit any members submission, we can edit the submission for the group.
4566 if ($this->can_edit_submission($member->id)) {
4567 return true;
4570 return false;
4574 * Perform an access check to see if the current $USER can view this group submission.
4576 * @param int $groupid
4577 * @return bool
4579 public function can_view_group_submission($groupid) {
4580 global $USER;
4582 $members = $this->get_submission_group_members($groupid, true);
4583 foreach ($members as $member) {
4584 // If we can view any members submission, we can view the submission for the group.
4585 if ($this->can_view_submission($member->id)) {
4586 return true;
4589 return false;
4593 * Perform an access check to see if the current $USER can view this users submission.
4595 * @param int $userid
4596 * @return bool
4598 public function can_view_submission($userid) {
4599 global $USER;
4601 if (!$this->is_active_user($userid) && !has_capability('moodle/course:viewsuspendedusers', $this->context)) {
4602 return false;
4604 if (!is_enrolled($this->get_course_context(), $userid)) {
4605 return false;
4607 if (has_any_capability(array('mod/assign:viewgrades', 'mod/assign:grade'), $this->context)) {
4608 return true;
4610 if ($userid == $USER->id && has_capability('mod/assign:submit', $this->context)) {
4611 return true;
4613 return false;
4617 * Allows the plugin to show a batch grading operation page.
4619 * @param moodleform $mform
4620 * @return none
4622 protected function view_plugin_grading_batch_operation($mform) {
4623 require_capability('mod/assign:grade', $this->context);
4624 $prefix = 'plugingradingbatchoperation_';
4626 if ($data = $mform->get_data()) {
4627 $tail = substr($data->operation, strlen($prefix));
4628 list($plugintype, $action) = explode('_', $tail, 2);
4630 $plugin = $this->get_feedback_plugin_by_type($plugintype);
4631 if ($plugin) {
4632 $users = $data->selectedusers;
4633 $userlist = explode(',', $users);
4634 echo $plugin->grading_batch_operation($action, $userlist);
4635 return;
4638 print_error('invalidformdata', '');
4642 * Ask the user to confirm they want to perform this batch operation
4644 * @param moodleform $mform Set to a grading batch operations form
4645 * @return string - the page to view after processing these actions
4647 protected function process_grading_batch_operation(& $mform) {
4648 global $CFG;
4649 require_once($CFG->dirroot . '/mod/assign/gradingbatchoperationsform.php');
4650 require_sesskey();
4652 $markingallocation = $this->get_instance()->markingworkflow &&
4653 $this->get_instance()->markingallocation &&
4654 has_capability('mod/assign:manageallocations', $this->context);
4656 $batchformparams = array('cm'=>$this->get_course_module()->id,
4657 'submissiondrafts'=>$this->get_instance()->submissiondrafts,
4658 'duedate'=>$this->get_instance()->duedate,
4659 'attemptreopenmethod'=>$this->get_instance()->attemptreopenmethod,
4660 'feedbackplugins'=>$this->get_feedback_plugins(),
4661 'context'=>$this->get_context(),
4662 'markingworkflow'=>$this->get_instance()->markingworkflow,
4663 'markingallocation'=>$markingallocation);
4664 $formclasses = array('class'=>'gradingbatchoperationsform');
4665 $mform = new mod_assign_grading_batch_operations_form(null,
4666 $batchformparams,
4667 'post',
4669 $formclasses);
4671 if ($data = $mform->get_data()) {
4672 // Get the list of users.
4673 $users = $data->selectedusers;
4674 $userlist = explode(',', $users);
4676 $prefix = 'plugingradingbatchoperation_';
4678 if ($data->operation == 'grantextension') {
4679 // Reset the form so the grant extension page will create the extension form.
4680 $mform = null;
4681 return 'grantextension';
4682 } else if ($data->operation == 'setmarkingworkflowstate') {
4683 return 'viewbatchsetmarkingworkflowstate';
4684 } else if ($data->operation == 'setmarkingallocation') {
4685 return 'viewbatchmarkingallocation';
4686 } else if (strpos($data->operation, $prefix) === 0) {
4687 $tail = substr($data->operation, strlen($prefix));
4688 list($plugintype, $action) = explode('_', $tail, 2);
4690 $plugin = $this->get_feedback_plugin_by_type($plugintype);
4691 if ($plugin) {
4692 return 'plugingradingbatchoperation';
4696 if ($data->operation == 'downloadselected') {
4697 $this->download_submissions($userlist);
4698 } else {
4699 foreach ($userlist as $userid) {
4700 if ($data->operation == 'lock') {
4701 $this->process_lock_submission($userid);
4702 } else if ($data->operation == 'unlock') {
4703 $this->process_unlock_submission($userid);
4704 } else if ($data->operation == 'reverttodraft') {
4705 $this->process_revert_to_draft($userid);
4706 } else if ($data->operation == 'addattempt') {
4707 if (!$this->get_instance()->teamsubmission) {
4708 $this->process_add_attempt($userid);
4713 if ($this->get_instance()->teamsubmission && $data->operation == 'addattempt') {
4714 // This needs to be handled separately so that each team submission is only re-opened one time.
4715 $this->process_add_attempt_group($userlist);
4719 return 'grading';
4723 * Shows a form that allows the workflow state for selected submissions to be changed.
4725 * @param moodleform $mform Set to a grading batch operations form
4726 * @return string - the page to view after processing these actions
4728 protected function view_batch_set_workflow_state($mform) {
4729 global $CFG, $DB;
4731 require_once($CFG->dirroot . '/mod/assign/batchsetmarkingworkflowstateform.php');
4733 $o = '';
4735 $submitteddata = $mform->get_data();
4736 $users = $submitteddata->selectedusers;
4737 $userlist = explode(',', $users);
4739 $formdata = array('id' => $this->get_course_module()->id,
4740 'selectedusers' => $users);
4742 $usershtml = '';
4744 $usercount = 0;
4745 $extrauserfields = get_extra_user_fields($this->get_context());
4746 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
4747 foreach ($userlist as $userid) {
4748 if ($usercount >= 5) {
4749 $usershtml .= get_string('moreusers', 'assign', count($userlist) - 5);
4750 break;
4752 $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
4754 $usershtml .= $this->get_renderer()->render(new assign_user_summary($user,
4755 $this->get_course()->id,
4756 $viewfullnames,
4757 $this->is_blind_marking(),
4758 $this->get_uniqueid_for_user($user->id),
4759 $extrauserfields,
4760 !$this->is_active_user($userid)));
4761 $usercount += 1;
4764 $formparams = array(
4765 'userscount' => count($userlist),
4766 'usershtml' => $usershtml,
4767 'markingworkflowstates' => $this->get_marking_workflow_states_for_current_user()
4770 $mform = new mod_assign_batch_set_marking_workflow_state_form(null, $formparams);
4771 $mform->set_data($formdata); // Initialises the hidden elements.
4772 $header = new assign_header($this->get_instance(),
4773 $this->get_context(),
4774 $this->show_intro(),
4775 $this->get_course_module()->id,
4776 get_string('setmarkingworkflowstate', 'assign'));
4777 $o .= $this->get_renderer()->render($header);
4778 $o .= $this->get_renderer()->render(new assign_form('setworkflowstate', $mform));
4779 $o .= $this->view_footer();
4781 \mod_assign\event\batch_set_workflow_state_viewed::create_from_assign($this)->trigger();
4783 return $o;
4787 * Shows a form that allows the allocated marker for selected submissions to be changed.
4789 * @param moodleform $mform Set to a grading batch operations form
4790 * @return string - the page to view after processing these actions
4792 public function view_batch_markingallocation($mform) {
4793 global $CFG, $DB;
4795 require_once($CFG->dirroot . '/mod/assign/batchsetallocatedmarkerform.php');
4797 $o = '';
4799 $submitteddata = $mform->get_data();
4800 $users = $submitteddata->selectedusers;
4801 $userlist = explode(',', $users);
4803 $formdata = array('id' => $this->get_course_module()->id,
4804 'selectedusers' => $users);
4806 $usershtml = '';
4808 $usercount = 0;
4809 $extrauserfields = get_extra_user_fields($this->get_context());
4810 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
4811 foreach ($userlist as $userid) {
4812 if ($usercount >= 5) {
4813 $usershtml .= get_string('moreusers', 'assign', count($userlist) - 5);
4814 break;
4816 $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
4818 $usershtml .= $this->get_renderer()->render(new assign_user_summary($user,
4819 $this->get_course()->id,
4820 $viewfullnames,
4821 $this->is_blind_marking(),
4822 $this->get_uniqueid_for_user($user->id),
4823 $extrauserfields,
4824 !$this->is_active_user($userid)));
4825 $usercount += 1;
4828 $formparams = array(
4829 'userscount' => count($userlist),
4830 'usershtml' => $usershtml,
4833 list($sort, $params) = users_order_by_sql('u');
4834 // Only enrolled users could be assigned as potential markers.
4835 $markers = get_enrolled_users($this->get_context(), 'mod/assign:grade', 0, 'u.*', $sort);
4836 $markerlist = array();
4837 foreach ($markers as $marker) {
4838 $markerlist[$marker->id] = fullname($marker);
4841 $formparams['markers'] = $markerlist;
4843 $mform = new mod_assign_batch_set_allocatedmarker_form(null, $formparams);
4844 $mform->set_data($formdata); // Initialises the hidden elements.
4845 $header = new assign_header($this->get_instance(),
4846 $this->get_context(),
4847 $this->show_intro(),
4848 $this->get_course_module()->id,
4849 get_string('setmarkingallocation', 'assign'));
4850 $o .= $this->get_renderer()->render($header);
4851 $o .= $this->get_renderer()->render(new assign_form('setworkflowstate', $mform));
4852 $o .= $this->view_footer();
4854 \mod_assign\event\batch_set_marker_allocation_viewed::create_from_assign($this)->trigger();
4856 return $o;
4860 * Ask the user to confirm they want to submit their work for grading.
4862 * @param moodleform $mform - null unless form validation has failed
4863 * @return string
4865 protected function check_submit_for_grading($mform) {
4866 global $USER, $CFG;
4868 require_once($CFG->dirroot . '/mod/assign/submissionconfirmform.php');
4870 // Check that all of the submission plugins are ready for this submission.
4871 $notifications = array();
4872 $submission = $this->get_user_submission($USER->id, false);
4873 $plugins = $this->get_submission_plugins();
4874 foreach ($plugins as $plugin) {
4875 if ($plugin->is_enabled() && $plugin->is_visible()) {
4876 $check = $plugin->precheck_submission($submission);
4877 if ($check !== true) {
4878 $notifications[] = $check;
4883 $data = new stdClass();
4884 $adminconfig = $this->get_admin_config();
4885 $requiresubmissionstatement = $this->get_instance()->requiresubmissionstatement &&
4886 !empty($adminconfig->submissionstatement);
4888 $submissionstatement = '';
4889 if (!empty($adminconfig->submissionstatement)) {
4890 // Format the submission statement before its sent. We turn off para because this is going within
4891 // a form element.
4892 $options = array(
4893 'context' => $this->get_context(),
4894 'para' => false
4896 $submissionstatement = format_text($adminconfig->submissionstatement, FORMAT_MOODLE, $options);
4899 if ($mform == null) {
4900 $mform = new mod_assign_confirm_submission_form(null, array($requiresubmissionstatement,
4901 $submissionstatement,
4902 $this->get_course_module()->id,
4903 $data));
4905 $o = '';
4906 $o .= $this->get_renderer()->header();
4907 $submitforgradingpage = new assign_submit_for_grading_page($notifications,
4908 $this->get_course_module()->id,
4909 $mform);
4910 $o .= $this->get_renderer()->render($submitforgradingpage);
4911 $o .= $this->view_footer();
4913 \mod_assign\event\submission_confirmation_form_viewed::create_from_assign($this)->trigger();
4915 return $o;
4919 * Creates an assign_submission_status renderable.
4921 * @param stdClass $user the user to get the report for
4922 * @param bool $showlinks return plain text or links to the profile
4923 * @return assign_submission_status renderable object
4925 public function get_assign_submission_status_renderable($user, $showlinks) {
4926 global $PAGE;
4928 $instance = $this->get_instance();
4929 $flags = $this->get_user_flags($user->id, false);
4930 $submission = $this->get_user_submission($user->id, false);
4932 $teamsubmission = null;
4933 $submissiongroup = null;
4934 $notsubmitted = array();
4935 if ($instance->teamsubmission) {
4936 $teamsubmission = $this->get_group_submission($user->id, 0, false);
4937 $submissiongroup = $this->get_submission_group($user->id);
4938 $groupid = 0;
4939 if ($submissiongroup) {
4940 $groupid = $submissiongroup->id;
4942 $notsubmitted = $this->get_submission_group_members_who_have_not_submitted($groupid, false);
4945 $showedit = $showlinks &&
4946 ($this->is_any_submission_plugin_enabled()) &&
4947 $this->can_edit_submission($user->id);
4949 $gradelocked = ($flags && $flags->locked) || $this->grading_disabled($user->id, false);
4951 // Grading criteria preview.
4952 $gradingmanager = get_grading_manager($this->context, 'mod_assign', 'submissions');
4953 $gradingcontrollerpreview = '';
4954 if ($gradingmethod = $gradingmanager->get_active_method()) {
4955 $controller = $gradingmanager->get_controller($gradingmethod);
4956 if ($controller->is_form_defined()) {
4957 $gradingcontrollerpreview = $controller->render_preview($PAGE);
4961 $showsubmit = ($showlinks && $this->submissions_open($user->id));
4962 $showsubmit = ($showsubmit && $this->show_submit_button($submission, $teamsubmission, $user->id));
4964 $extensionduedate = null;
4965 if ($flags) {
4966 $extensionduedate = $flags->extensionduedate;
4968 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
4970 $gradingstatus = $this->get_grading_status($user->id);
4971 $usergroups = $this->get_all_groups($user->id);
4972 $submissionstatus = new assign_submission_status($instance->allowsubmissionsfromdate,
4973 $instance->alwaysshowdescription,
4974 $submission,
4975 $instance->teamsubmission,
4976 $teamsubmission,
4977 $submissiongroup,
4978 $notsubmitted,
4979 $this->is_any_submission_plugin_enabled(),
4980 $gradelocked,
4981 $this->is_graded($user->id),
4982 $instance->duedate,
4983 $instance->cutoffdate,
4984 $this->get_submission_plugins(),
4985 $this->get_return_action(),
4986 $this->get_return_params(),
4987 $this->get_course_module()->id,
4988 $this->get_course()->id,
4989 assign_submission_status::STUDENT_VIEW,
4990 $showedit,
4991 $showsubmit,
4992 $viewfullnames,
4993 $extensionduedate,
4994 $this->get_context(),
4995 $this->is_blind_marking(),
4996 $gradingcontrollerpreview,
4997 $instance->attemptreopenmethod,
4998 $instance->maxattempts,
4999 $gradingstatus,
5000 $instance->preventsubmissionnotingroup,
5001 $usergroups);
5002 return $submissionstatus;
5007 * Creates an assign_feedback_status renderable.
5009 * @param stdClass $user the user to get the report for
5010 * @return assign_feedback_status renderable object
5012 public function get_assign_feedback_status_renderable($user) {
5013 global $CFG, $DB, $PAGE;
5015 require_once($CFG->libdir.'/gradelib.php');
5016 require_once($CFG->dirroot.'/grade/grading/lib.php');
5018 $instance = $this->get_instance();
5019 $grade = $this->get_user_grade($user->id, false);
5020 $gradingstatus = $this->get_grading_status($user->id);
5022 $gradinginfo = grade_get_grades($this->get_course()->id,
5023 'mod',
5024 'assign',
5025 $instance->id,
5026 $user->id);
5028 $gradingitem = null;
5029 $gradebookgrade = null;
5030 if (isset($gradinginfo->items[0])) {
5031 $gradingitem = $gradinginfo->items[0];
5032 $gradebookgrade = $gradingitem->grades[$user->id];
5035 // Check to see if all feedback plugins are empty.
5036 $emptyplugins = true;
5037 if ($grade) {
5038 foreach ($this->get_feedback_plugins() as $plugin) {
5039 if ($plugin->is_visible() && $plugin->is_enabled()) {
5040 if (!$plugin->is_empty($grade)) {
5041 $emptyplugins = false;
5047 if ($this->get_instance()->markingworkflow && $gradingstatus != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
5048 $emptyplugins = true; // Don't show feedback plugins until released either.
5051 $cangrade = has_capability('mod/assign:grade', $this->get_context());
5052 $hasgrade = $this->get_instance()->grade != GRADE_TYPE_NONE &&
5053 !is_null($gradebookgrade) && !is_null($gradebookgrade->grade);
5054 $gradevisible = $cangrade || $this->get_instance()->grade == GRADE_TYPE_NONE ||
5055 (!is_null($gradebookgrade) && !$gradebookgrade->hidden);
5056 // If there is a visible grade, show the summary.
5057 if (($hasgrade || !$emptyplugins) && $gradevisible) {
5059 $gradefordisplay = null;
5060 $gradeddate = null;
5061 $grader = null;
5062 $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
5064 if ($hasgrade) {
5065 if ($controller = $gradingmanager->get_active_controller()) {
5066 $menu = make_grades_menu($this->get_instance()->grade);
5067 $controller->set_grade_range($menu, $this->get_instance()->grade > 0);
5068 $gradefordisplay = $controller->render_grade($PAGE,
5069 $grade->id,
5070 $gradingitem,
5071 $gradebookgrade->str_long_grade,
5072 $cangrade);
5073 } else {
5074 $gradefordisplay = $this->display_grade($gradebookgrade->grade, false);
5076 $gradeddate = $gradebookgrade->dategraded;
5077 if (isset($grade->grader)) {
5078 $grader = $DB->get_record('user', array('id' => $grade->grader));
5082 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
5084 $feedbackstatus = new assign_feedback_status($gradefordisplay,
5085 $gradeddate,
5086 $grader,
5087 $this->get_feedback_plugins(),
5088 $grade,
5089 $this->get_course_module()->id,
5090 $this->get_return_action(),
5091 $this->get_return_params(),
5092 $viewfullnames);
5093 return $feedbackstatus;
5095 return;
5099 * Creates an assign_attempt_history renderable.
5101 * @param stdClass $user the user to get the report for
5102 * @return assign_attempt_history renderable object
5104 public function get_assign_attempt_history_renderable($user) {
5106 $allsubmissions = $this->get_all_submissions($user->id);
5107 $allgrades = $this->get_all_grades($user->id);
5109 $history = new assign_attempt_history($allsubmissions,
5110 $allgrades,
5111 $this->get_submission_plugins(),
5112 $this->get_feedback_plugins(),
5113 $this->get_course_module()->id,
5114 $this->get_return_action(),
5115 $this->get_return_params(),
5116 false,
5119 return $history;
5123 * Print 2 tables of information with no action links -
5124 * the submission summary and the grading summary.
5126 * @param stdClass $user the user to print the report for
5127 * @param bool $showlinks - Return plain text or links to the profile
5128 * @return string - the html summary
5130 public function view_student_summary($user, $showlinks) {
5132 $o = '';
5134 if ($this->can_view_submission($user->id)) {
5136 if (has_capability('mod/assign:submit', $this->get_context(), $user, false)) {
5137 $submissionstatus = $this->get_assign_submission_status_renderable($user, $showlinks);
5138 $o .= $this->get_renderer()->render($submissionstatus);
5141 // If there is a visible grade, show the feedback.
5142 $feedbackstatus = $this->get_assign_feedback_status_renderable($user);
5143 if ($feedbackstatus) {
5144 $o .= $this->get_renderer()->render($feedbackstatus);
5147 // If there is more than one submission, show the history.
5148 $history = $this->get_assign_attempt_history_renderable($user);
5149 if (count($history->submissions) > 1) {
5150 $o .= $this->get_renderer()->render($history);
5153 return $o;
5157 * Returns true if the submit subsission button should be shown to the user.
5159 * @param stdClass $submission The users own submission record.
5160 * @param stdClass $teamsubmission The users team submission record if there is one
5161 * @param int $userid The user
5162 * @return bool
5164 protected function show_submit_button($submission = null, $teamsubmission = null, $userid = null) {
5165 if ($teamsubmission) {
5166 if ($teamsubmission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
5167 // The assignment submission has been completed.
5168 return false;
5169 } else if ($this->submission_empty($teamsubmission)) {
5170 // There is nothing to submit yet.
5171 return false;
5172 } else if ($submission && $submission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
5173 // The user has already clicked the submit button on the team submission.
5174 return false;
5175 } else if (
5176 !empty($this->get_instance()->preventsubmissionnotingroup)
5177 && $this->get_submission_group($userid) == false
5179 return false;
5181 } else if ($submission) {
5182 if ($submission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
5183 // The assignment submission has been completed.
5184 return false;
5185 } else if ($this->submission_empty($submission)) {
5186 // There is nothing to submit.
5187 return false;
5189 } else {
5190 // We've not got a valid submission or team submission.
5191 return false;
5193 // Last check is that this instance allows drafts.
5194 return $this->get_instance()->submissiondrafts;
5198 * Get the grades for all previous attempts.
5199 * For each grade - the grader is a full user record,
5200 * and gradefordisplay is added (rendered from grading manager).
5202 * @param int $userid If not set, $USER->id will be used.
5203 * @return array $grades All grade records for this user.
5205 protected function get_all_grades($userid) {
5206 global $DB, $USER, $PAGE;
5208 // If the userid is not null then use userid.
5209 if (!$userid) {
5210 $userid = $USER->id;
5213 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
5215 $grades = $DB->get_records('assign_grades', $params, 'attemptnumber ASC');
5217 $gradercache = array();
5218 $cangrade = has_capability('mod/assign:grade', $this->get_context());
5220 // Need gradingitem and gradingmanager.
5221 $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
5222 $controller = $gradingmanager->get_active_controller();
5224 $gradinginfo = grade_get_grades($this->get_course()->id,
5225 'mod',
5226 'assign',
5227 $this->get_instance()->id,
5228 $userid);
5230 $gradingitem = null;
5231 if (isset($gradinginfo->items[0])) {
5232 $gradingitem = $gradinginfo->items[0];
5235 foreach ($grades as $grade) {
5236 // First lookup the grader info.
5237 if (isset($gradercache[$grade->grader])) {
5238 $grade->grader = $gradercache[$grade->grader];
5239 } else {
5240 // Not in cache - need to load the grader record.
5241 $grade->grader = $DB->get_record('user', array('id'=>$grade->grader));
5242 $gradercache[$grade->grader->id] = $grade->grader;
5245 // Now get the gradefordisplay.
5246 if ($controller) {
5247 $controller->set_grade_range(make_grades_menu($this->get_instance()->grade), $this->get_instance()->grade > 0);
5248 $grade->gradefordisplay = $controller->render_grade($PAGE,
5249 $grade->id,
5250 $gradingitem,
5251 $grade->grade,
5252 $cangrade);
5253 } else {
5254 $grade->gradefordisplay = $this->display_grade($grade->grade, false);
5259 return $grades;
5263 * Get the submissions for all previous attempts.
5265 * @param int $userid If not set, $USER->id will be used.
5266 * @return array $submissions All submission records for this user (or group).
5268 protected function get_all_submissions($userid) {
5269 global $DB, $USER;
5271 // If the userid is not null then use userid.
5272 if (!$userid) {
5273 $userid = $USER->id;
5276 $params = array();
5278 if ($this->get_instance()->teamsubmission) {
5279 $groupid = 0;
5280 $group = $this->get_submission_group($userid);
5281 if ($group) {
5282 $groupid = $group->id;
5285 // Params to get the group submissions.
5286 $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
5287 } else {
5288 // Params to get the user submissions.
5289 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
5292 // Return the submissions ordered by attempt.
5293 $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber ASC');
5295 return $submissions;
5299 * Creates an assign_grading_summary renderable.
5301 * @return assign_grading_summary renderable object
5303 public function get_assign_grading_summary_renderable() {
5305 $instance = $this->get_instance();
5307 $draft = ASSIGN_SUBMISSION_STATUS_DRAFT;
5308 $submitted = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
5310 $activitygroup = groups_get_activity_group($this->get_course_module());
5312 if ($instance->teamsubmission) {
5313 $defaultteammembers = $this->get_submission_group_members(0, true);
5314 $warnofungroupedusers = (count($defaultteammembers) > 0 && $instance->preventsubmissionnotingroup);
5316 $summary = new assign_grading_summary($this->count_teams($activitygroup),
5317 $instance->submissiondrafts,
5318 $this->count_submissions_with_status($draft),
5319 $this->is_any_submission_plugin_enabled(),
5320 $this->count_submissions_with_status($submitted),
5321 $instance->cutoffdate,
5322 $instance->duedate,
5323 $this->get_course_module()->id,
5324 $this->count_submissions_need_grading(),
5325 $instance->teamsubmission,
5326 $warnofungroupedusers);
5327 } else {
5328 // The active group has already been updated in groups_print_activity_menu().
5329 $countparticipants = $this->count_participants($activitygroup);
5330 $summary = new assign_grading_summary($countparticipants,
5331 $instance->submissiondrafts,
5332 $this->count_submissions_with_status($draft),
5333 $this->is_any_submission_plugin_enabled(),
5334 $this->count_submissions_with_status($submitted),
5335 $instance->cutoffdate,
5336 $instance->duedate,
5337 $this->get_course_module()->id,
5338 $this->count_submissions_need_grading(),
5339 $instance->teamsubmission,
5340 false);
5344 return $summary;
5348 * View submissions page (contains details of current submission).
5350 * @return string
5352 protected function view_submission_page() {
5353 global $CFG, $DB, $USER, $PAGE;
5355 $instance = $this->get_instance();
5357 $this->add_grade_notices();
5359 $o = '';
5361 $postfix = '';
5362 if ($this->has_visible_attachments()) {
5363 $postfix = $this->render_area_files('mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0);
5365 $o .= $this->get_renderer()->render(new assign_header($instance,
5366 $this->get_context(),
5367 $this->show_intro(),
5368 $this->get_course_module()->id,
5369 '', '', $postfix));
5371 // Display plugin specific headers.
5372 $plugins = array_merge($this->get_submission_plugins(), $this->get_feedback_plugins());
5373 foreach ($plugins as $plugin) {
5374 if ($plugin->is_enabled() && $plugin->is_visible()) {
5375 $o .= $this->get_renderer()->render(new assign_plugin_header($plugin));
5379 if ($this->can_view_grades()) {
5380 // Group selector will only be displayed if necessary.
5381 $currenturl = new moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id));
5382 $o .= groups_print_activity_menu($this->get_course_module(), $currenturl->out(), true);
5384 $summary = $this->get_assign_grading_summary_renderable();
5385 $o .= $this->get_renderer()->render($summary);
5387 $grade = $this->get_user_grade($USER->id, false);
5388 $submission = $this->get_user_submission($USER->id, false);
5390 if ($this->can_view_submission($USER->id)) {
5391 $o .= $this->view_student_summary($USER, true);
5394 $o .= $this->view_footer();
5396 \mod_assign\event\submission_status_viewed::create_from_assign($this)->trigger();
5398 return $o;
5402 * Convert the final raw grade(s) in the grading table for the gradebook.
5404 * @param stdClass $grade
5405 * @return array
5407 protected function convert_grade_for_gradebook(stdClass $grade) {
5408 $gradebookgrade = array();
5409 if ($grade->grade >= 0) {
5410 $gradebookgrade['rawgrade'] = $grade->grade;
5412 // Allow "no grade" to be chosen.
5413 if ($grade->grade == -1) {
5414 $gradebookgrade['rawgrade'] = NULL;
5416 $gradebookgrade['userid'] = $grade->userid;
5417 $gradebookgrade['usermodified'] = $grade->grader;
5418 $gradebookgrade['datesubmitted'] = null;
5419 $gradebookgrade['dategraded'] = $grade->timemodified;
5420 if (isset($grade->feedbackformat)) {
5421 $gradebookgrade['feedbackformat'] = $grade->feedbackformat;
5423 if (isset($grade->feedbacktext)) {
5424 $gradebookgrade['feedback'] = $grade->feedbacktext;
5427 return $gradebookgrade;
5431 * Convert submission details for the gradebook.
5433 * @param stdClass $submission
5434 * @return array
5436 protected function convert_submission_for_gradebook(stdClass $submission) {
5437 $gradebookgrade = array();
5439 $gradebookgrade['userid'] = $submission->userid;
5440 $gradebookgrade['usermodified'] = $submission->userid;
5441 $gradebookgrade['datesubmitted'] = $submission->timemodified;
5443 return $gradebookgrade;
5447 * Update grades in the gradebook.
5449 * @param mixed $submission stdClass|null
5450 * @param mixed $grade stdClass|null
5451 * @return bool
5453 protected function gradebook_item_update($submission=null, $grade=null) {
5454 global $CFG;
5456 require_once($CFG->dirroot.'/mod/assign/lib.php');
5457 // Do not push grade to gradebook if blind marking is active as
5458 // the gradebook would reveal the students.
5459 if ($this->is_blind_marking()) {
5460 return false;
5463 // If marking workflow is enabled and grade is not released then remove any grade that may exist in the gradebook.
5464 if ($this->get_instance()->markingworkflow && !empty($grade) &&
5465 $this->get_grading_status($grade->userid) != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
5466 // Remove the grade (if it exists) from the gradebook as it is not 'final'.
5467 $grade->grade = -1;
5468 $grade->feedbacktext = '';
5471 if ($submission != null) {
5472 if ($submission->userid == 0) {
5473 // This is a group submission update.
5474 $team = groups_get_members($submission->groupid, 'u.id');
5476 foreach ($team as $member) {
5477 $membersubmission = clone $submission;
5478 $membersubmission->groupid = 0;
5479 $membersubmission->userid = $member->id;
5480 $this->gradebook_item_update($membersubmission, null);
5482 return;
5485 $gradebookgrade = $this->convert_submission_for_gradebook($submission);
5487 } else {
5488 $gradebookgrade = $this->convert_grade_for_gradebook($grade);
5490 // Grading is disabled, return.
5491 if ($this->grading_disabled($gradebookgrade['userid'])) {
5492 return false;
5494 $assign = clone $this->get_instance();
5495 $assign->cmidnumber = $this->get_course_module()->idnumber;
5496 // Set assign gradebook feedback plugin status (enabled and visible).
5497 $assign->gradefeedbackenabled = $this->is_gradebook_feedback_enabled();
5498 return assign_grade_item_update($assign, $gradebookgrade) == GRADE_UPDATE_OK;
5502 * Update team submission.
5504 * @param stdClass $submission
5505 * @param int $userid
5506 * @param bool $updatetime
5507 * @return bool
5509 protected function update_team_submission(stdClass $submission, $userid, $updatetime) {
5510 global $DB;
5512 if ($updatetime) {
5513 $submission->timemodified = time();
5516 // First update the submission for the current user.
5517 $mysubmission = $this->get_user_submission($userid, true, $submission->attemptnumber);
5518 $mysubmission->status = $submission->status;
5520 $this->update_submission($mysubmission, 0, $updatetime, false);
5522 // Now check the team settings to see if this assignment qualifies as submitted or draft.
5523 $team = $this->get_submission_group_members($submission->groupid, true);
5525 $allsubmitted = true;
5526 $anysubmitted = false;
5527 $result = true;
5528 if ($submission->status != ASSIGN_SUBMISSION_STATUS_REOPENED) {
5529 foreach ($team as $member) {
5530 $membersubmission = $this->get_user_submission($member->id, false, $submission->attemptnumber);
5532 // If no submission found for team member and member is active then everyone has not submitted.
5533 if (!$membersubmission || $membersubmission->status != ASSIGN_SUBMISSION_STATUS_SUBMITTED
5534 && ($this->is_active_user($member->id))) {
5535 $allsubmitted = false;
5536 if ($anysubmitted) {
5537 break;
5539 } else {
5540 $anysubmitted = true;
5543 if ($this->get_instance()->requireallteammemberssubmit) {
5544 if ($allsubmitted) {
5545 $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
5546 } else {
5547 $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
5549 $result = $DB->update_record('assign_submission', $submission);
5550 } else {
5551 if ($anysubmitted) {
5552 $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
5553 } else {
5554 $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
5556 $result = $DB->update_record('assign_submission', $submission);
5558 } else {
5559 // Set the group submission to reopened.
5560 foreach ($team as $member) {
5561 $membersubmission = $this->get_user_submission($member->id, true, $submission->attemptnumber);
5562 $membersubmission->status = ASSIGN_SUBMISSION_STATUS_REOPENED;
5563 $result = $DB->update_record('assign_submission', $membersubmission) && $result;
5565 $result = $DB->update_record('assign_submission', $submission) && $result;
5568 $this->gradebook_item_update($submission);
5569 return $result;
5573 * Update grades in the gradebook based on submission time.
5575 * @param stdClass $submission
5576 * @param int $userid
5577 * @param bool $updatetime
5578 * @param bool $teamsubmission
5579 * @return bool
5581 protected function update_submission(stdClass $submission, $userid, $updatetime, $teamsubmission) {
5582 global $DB;
5584 if ($teamsubmission) {
5585 return $this->update_team_submission($submission, $userid, $updatetime);
5588 if ($updatetime) {
5589 $submission->timemodified = time();
5591 $result= $DB->update_record('assign_submission', $submission);
5592 if ($result) {
5593 $this->gradebook_item_update($submission);
5595 return $result;
5599 * Is this assignment open for submissions?
5601 * Check the due date,
5602 * prevent late submissions,
5603 * has this person already submitted,
5604 * is the assignment locked?
5606 * @param int $userid - Optional userid so we can see if a different user can submit
5607 * @param bool $skipenrolled - Skip enrollment checks (because they have been done already)
5608 * @param stdClass $submission - Pre-fetched submission record (or false to fetch it)
5609 * @param stdClass $flags - Pre-fetched user flags record (or false to fetch it)
5610 * @param stdClass $gradinginfo - Pre-fetched user gradinginfo record (or false to fetch it)
5611 * @return bool
5613 public function submissions_open($userid = 0,
5614 $skipenrolled = false,
5615 $submission = false,
5616 $flags = false,
5617 $gradinginfo = false) {
5618 global $USER;
5620 if (!$userid) {
5621 $userid = $USER->id;
5624 $time = time();
5625 $dateopen = true;
5626 $finaldate = false;
5627 if ($this->get_instance()->cutoffdate) {
5628 $finaldate = $this->get_instance()->cutoffdate;
5631 if ($flags === false) {
5632 $flags = $this->get_user_flags($userid, false);
5634 if ($flags && $flags->locked) {
5635 return false;
5638 // User extensions.
5639 if ($finaldate) {
5640 if ($flags && $flags->extensionduedate) {
5641 // Extension can be before cut off date.
5642 if ($flags->extensionduedate > $finaldate) {
5643 $finaldate = $flags->extensionduedate;
5648 if ($finaldate) {
5649 $dateopen = ($this->get_instance()->allowsubmissionsfromdate <= $time && $time <= $finaldate);
5650 } else {
5651 $dateopen = ($this->get_instance()->allowsubmissionsfromdate <= $time);
5654 if (!$dateopen) {
5655 return false;
5658 // Now check if this user has already submitted etc.
5659 if (!$skipenrolled && !is_enrolled($this->get_course_context(), $userid)) {
5660 return false;
5662 // Note you can pass null for submission and it will not be fetched.
5663 if ($submission === false) {
5664 if ($this->get_instance()->teamsubmission) {
5665 $submission = $this->get_group_submission($userid, 0, false);
5666 } else {
5667 $submission = $this->get_user_submission($userid, false);
5670 if ($submission) {
5672 if ($this->get_instance()->submissiondrafts && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
5673 // Drafts are tracked and the student has submitted the assignment.
5674 return false;
5678 // See if this user grade is locked in the gradebook.
5679 if ($gradinginfo === false) {
5680 $gradinginfo = grade_get_grades($this->get_course()->id,
5681 'mod',
5682 'assign',
5683 $this->get_instance()->id,
5684 array($userid));
5686 if ($gradinginfo &&
5687 isset($gradinginfo->items[0]->grades[$userid]) &&
5688 $gradinginfo->items[0]->grades[$userid]->locked) {
5689 return false;
5692 return true;
5696 * Render the files in file area.
5698 * @param string $component
5699 * @param string $area
5700 * @param int $submissionid
5701 * @return string
5703 public function render_area_files($component, $area, $submissionid) {
5704 global $USER;
5706 return $this->get_renderer()->assign_files($this->context, $submissionid, $area, $component);
5711 * Capability check to make sure this grader can edit this submission.
5713 * @param int $userid - The user whose submission is to be edited
5714 * @param int $graderid (optional) - The user who will do the editing (default to $USER->id).
5715 * @return bool
5717 public function can_edit_submission($userid, $graderid = 0) {
5718 global $USER;
5720 if (empty($graderid)) {
5721 $graderid = $USER->id;
5724 $instance = $this->get_instance();
5725 if ($userid == $graderid &&
5726 $instance->teamsubmission &&
5727 $instance->preventsubmissionnotingroup &&
5728 $this->get_submission_group($userid) == false) {
5729 return false;
5732 if ($userid == $graderid) {
5733 if ($this->submissions_open($userid) &&
5734 has_capability('mod/assign:submit', $this->context, $graderid)) {
5735 // User can edit their own submission.
5736 return true;
5737 } else {
5738 // We need to return here because editothersubmission should never apply to a users own submission.
5739 return false;
5743 if (!has_capability('mod/assign:editothersubmission', $this->context, $graderid)) {
5744 return false;
5747 $cm = $this->get_course_module();
5748 if (groups_get_activity_groupmode($cm) == SEPARATEGROUPS) {
5749 $sharedgroupmembers = $this->get_shared_group_members($cm, $graderid);
5750 return in_array($userid, $sharedgroupmembers);
5752 return true;
5756 * Returns IDs of the users who share group membership with the specified user.
5758 * @param stdClass|cm_info $cm Course-module
5759 * @param int $userid User ID
5760 * @return array An array of ID of users.
5762 public function get_shared_group_members($cm, $userid) {
5763 if (!isset($this->sharedgroupmembers[$userid])) {
5764 $this->sharedgroupmembers[$userid] = array();
5765 if ($members = groups_get_activity_shared_group_members($cm, $userid)) {
5766 $this->sharedgroupmembers[$userid] = array_keys($members);
5770 return $this->sharedgroupmembers[$userid];
5774 * Returns a list of teachers that should be grading given submission.
5776 * @param int $userid The submission to grade
5777 * @return array
5779 protected function get_graders($userid) {
5780 // Potential graders should be active users only.
5781 $potentialgraders = get_enrolled_users($this->context, "mod/assign:grade", null, 'u.*', null, null, null, true);
5783 $graders = array();
5784 if (groups_get_activity_groupmode($this->get_course_module()) == SEPARATEGROUPS) {
5785 if ($groups = groups_get_all_groups($this->get_course()->id, $userid, $this->get_course_module()->groupingid)) {
5786 foreach ($groups as $group) {
5787 foreach ($potentialgraders as $grader) {
5788 if ($grader->id == $userid) {
5789 // Do not send self.
5790 continue;
5792 if (groups_is_member($group->id, $grader->id)) {
5793 $graders[$grader->id] = $grader;
5797 } else {
5798 // User not in group, try to find graders without group.
5799 foreach ($potentialgraders as $grader) {
5800 if ($grader->id == $userid) {
5801 // Do not send self.
5802 continue;
5804 if (!groups_has_membership($this->get_course_module(), $grader->id)) {
5805 $graders[$grader->id] = $grader;
5809 } else {
5810 foreach ($potentialgraders as $grader) {
5811 if ($grader->id == $userid) {
5812 // Do not send self.
5813 continue;
5815 // Must be enrolled.
5816 if (is_enrolled($this->get_course_context(), $grader->id)) {
5817 $graders[$grader->id] = $grader;
5821 return $graders;
5825 * Returns a list of users that should receive notification about given submission.
5827 * @param int $userid The submission to grade
5828 * @return array
5830 protected function get_notifiable_users($userid) {
5831 // Potential users should be active users only.
5832 $potentialusers = get_enrolled_users($this->context, "mod/assign:receivegradernotifications",
5833 null, 'u.*', null, null, null, true);
5835 $notifiableusers = array();
5836 if (groups_get_activity_groupmode($this->get_course_module()) == SEPARATEGROUPS) {
5837 if ($groups = groups_get_all_groups($this->get_course()->id, $userid, $this->get_course_module()->groupingid)) {
5838 foreach ($groups as $group) {
5839 foreach ($potentialusers as $potentialuser) {
5840 if ($potentialuser->id == $userid) {
5841 // Do not send self.
5842 continue;
5844 if (groups_is_member($group->id, $potentialuser->id)) {
5845 $notifiableusers[$potentialuser->id] = $potentialuser;
5849 } else {
5850 // User not in group, try to find graders without group.
5851 foreach ($potentialusers as $potentialuser) {
5852 if ($potentialuser->id == $userid) {
5853 // Do not send self.
5854 continue;
5856 if (!groups_has_membership($this->get_course_module(), $potentialuser->id)) {
5857 $notifiableusers[$potentialuser->id] = $potentialuser;
5861 } else {
5862 foreach ($potentialusers as $potentialuser) {
5863 if ($potentialuser->id == $userid) {
5864 // Do not send self.
5865 continue;
5867 // Must be enrolled.
5868 if (is_enrolled($this->get_course_context(), $potentialuser->id)) {
5869 $notifiableusers[$potentialuser->id] = $potentialuser;
5873 return $notifiableusers;
5877 * Format a notification for plain text.
5879 * @param string $messagetype
5880 * @param stdClass $info
5881 * @param stdClass $course
5882 * @param stdClass $context
5883 * @param string $modulename
5884 * @param string $assignmentname
5886 protected static function format_notification_message_text($messagetype,
5887 $info,
5888 $course,
5889 $context,
5890 $modulename,
5891 $assignmentname) {
5892 $formatparams = array('context' => $context->get_course_context());
5893 $posttext = format_string($course->shortname, true, $formatparams) .
5894 ' -> ' .
5895 $modulename .
5896 ' -> ' .
5897 format_string($assignmentname, true, $formatparams) . "\n";
5898 $posttext .= '---------------------------------------------------------------------' . "\n";
5899 $posttext .= get_string($messagetype . 'text', 'assign', $info)."\n";
5900 $posttext .= "\n---------------------------------------------------------------------\n";
5901 return $posttext;
5905 * Format a notification for HTML.
5907 * @param string $messagetype
5908 * @param stdClass $info
5909 * @param stdClass $course
5910 * @param stdClass $context
5911 * @param string $modulename
5912 * @param stdClass $coursemodule
5913 * @param string $assignmentname
5915 protected static function format_notification_message_html($messagetype,
5916 $info,
5917 $course,
5918 $context,
5919 $modulename,
5920 $coursemodule,
5921 $assignmentname) {
5922 global $CFG;
5923 $formatparams = array('context' => $context->get_course_context());
5924 $posthtml = '<p><font face="sans-serif">' .
5925 '<a href="' . $CFG->wwwroot . '/course/view.php?id=' . $course->id . '">' .
5926 format_string($course->shortname, true, $formatparams) .
5927 '</a> ->' .
5928 '<a href="' . $CFG->wwwroot . '/mod/assign/index.php?id=' . $course->id . '">' .
5929 $modulename .
5930 '</a> ->' .
5931 '<a href="' . $CFG->wwwroot . '/mod/assign/view.php?id=' . $coursemodule->id . '">' .
5932 format_string($assignmentname, true, $formatparams) .
5933 '</a></font></p>';
5934 $posthtml .= '<hr /><font face="sans-serif">';
5935 $posthtml .= '<p>' . get_string($messagetype . 'html', 'assign', $info) . '</p>';
5936 $posthtml .= '</font><hr />';
5937 return $posthtml;
5941 * Message someone about something (static so it can be called from cron).
5943 * @param stdClass $userfrom
5944 * @param stdClass $userto
5945 * @param string $messagetype
5946 * @param string $eventtype
5947 * @param int $updatetime
5948 * @param stdClass $coursemodule
5949 * @param stdClass $context
5950 * @param stdClass $course
5951 * @param string $modulename
5952 * @param string $assignmentname
5953 * @param bool $blindmarking
5954 * @param int $uniqueidforuser
5955 * @return void
5957 public static function send_assignment_notification($userfrom,
5958 $userto,
5959 $messagetype,
5960 $eventtype,
5961 $updatetime,
5962 $coursemodule,
5963 $context,
5964 $course,
5965 $modulename,
5966 $assignmentname,
5967 $blindmarking,
5968 $uniqueidforuser) {
5969 global $CFG;
5971 $info = new stdClass();
5972 if ($blindmarking) {
5973 $userfrom = clone($userfrom);
5974 $info->username = get_string('participant', 'assign') . ' ' . $uniqueidforuser;
5975 $userfrom->firstname = get_string('participant', 'assign');
5976 $userfrom->lastname = $uniqueidforuser;
5977 $userfrom->email = $CFG->noreplyaddress;
5978 } else {
5979 $info->username = fullname($userfrom, true);
5981 $info->assignment = format_string($assignmentname, true, array('context'=>$context));
5982 $info->url = $CFG->wwwroot.'/mod/assign/view.php?id='.$coursemodule->id;
5983 $info->timeupdated = userdate($updatetime, get_string('strftimerecentfull'));
5985 $postsubject = get_string($messagetype . 'small', 'assign', $info);
5986 $posttext = self::format_notification_message_text($messagetype,
5987 $info,
5988 $course,
5989 $context,
5990 $modulename,
5991 $assignmentname);
5992 $posthtml = '';
5993 if ($userto->mailformat == 1) {
5994 $posthtml = self::format_notification_message_html($messagetype,
5995 $info,
5996 $course,
5997 $context,
5998 $modulename,
5999 $coursemodule,
6000 $assignmentname);
6003 $eventdata = new \core\message\message();
6004 $eventdata->courseid = $course->id;
6005 $eventdata->modulename = 'assign';
6006 $eventdata->userfrom = $userfrom;
6007 $eventdata->userto = $userto;
6008 $eventdata->subject = $postsubject;
6009 $eventdata->fullmessage = $posttext;
6010 $eventdata->fullmessageformat = FORMAT_PLAIN;
6011 $eventdata->fullmessagehtml = $posthtml;
6012 $eventdata->smallmessage = $postsubject;
6014 $eventdata->name = $eventtype;
6015 $eventdata->component = 'mod_assign';
6016 $eventdata->notification = 1;
6017 $eventdata->contexturl = $info->url;
6018 $eventdata->contexturlname = $info->assignment;
6020 message_send($eventdata);
6024 * Message someone about something.
6026 * @param stdClass $userfrom
6027 * @param stdClass $userto
6028 * @param string $messagetype
6029 * @param string $eventtype
6030 * @param int $updatetime
6031 * @return void
6033 public function send_notification($userfrom, $userto, $messagetype, $eventtype, $updatetime) {
6034 global $USER;
6035 $userid = core_user::is_real_user($userfrom->id) ? $userfrom->id : $USER->id;
6036 $uniqueid = $this->get_uniqueid_for_user($userid);
6037 self::send_assignment_notification($userfrom,
6038 $userto,
6039 $messagetype,
6040 $eventtype,
6041 $updatetime,
6042 $this->get_course_module(),
6043 $this->get_context(),
6044 $this->get_course(),
6045 $this->get_module_name(),
6046 $this->get_instance()->name,
6047 $this->is_blind_marking(),
6048 $uniqueid);
6052 * Notify student upon successful submission copy.
6054 * @param stdClass $submission
6055 * @return void
6057 protected function notify_student_submission_copied(stdClass $submission) {
6058 global $DB, $USER;
6060 $adminconfig = $this->get_admin_config();
6061 // Use the same setting for this - no need for another one.
6062 if (empty($adminconfig->submissionreceipts)) {
6063 // No need to do anything.
6064 return;
6066 if ($submission->userid) {
6067 $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST);
6068 } else {
6069 $user = $USER;
6071 $this->send_notification($user,
6072 $user,
6073 'submissioncopied',
6074 'assign_notification',
6075 $submission->timemodified);
6078 * Notify student upon successful submission.
6080 * @param stdClass $submission
6081 * @return void
6083 protected function notify_student_submission_receipt(stdClass $submission) {
6084 global $DB, $USER;
6086 $adminconfig = $this->get_admin_config();
6087 if (empty($adminconfig->submissionreceipts)) {
6088 // No need to do anything.
6089 return;
6091 if ($submission->userid) {
6092 $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST);
6093 } else {
6094 $user = $USER;
6096 if ($submission->userid == $USER->id) {
6097 $this->send_notification(core_user::get_noreply_user(),
6098 $user,
6099 'submissionreceipt',
6100 'assign_notification',
6101 $submission->timemodified);
6102 } else {
6103 $this->send_notification($USER,
6104 $user,
6105 'submissionreceiptother',
6106 'assign_notification',
6107 $submission->timemodified);
6112 * Send notifications to graders upon student submissions.
6114 * @param stdClass $submission
6115 * @return void
6117 protected function notify_graders(stdClass $submission) {
6118 global $DB, $USER;
6120 $instance = $this->get_instance();
6122 $late = $instance->duedate && ($instance->duedate < time());
6124 if (!$instance->sendnotifications && !($late && $instance->sendlatenotifications)) {
6125 // No need to do anything.
6126 return;
6129 if ($submission->userid) {
6130 $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST);
6131 } else {
6132 $user = $USER;
6135 if ($notifyusers = $this->get_notifiable_users($user->id)) {
6136 foreach ($notifyusers as $notifyuser) {
6137 $this->send_notification($user,
6138 $notifyuser,
6139 'gradersubmissionupdated',
6140 'assign_notification',
6141 $submission->timemodified);
6147 * Submit a submission for grading.
6149 * @param stdClass $data - The form data
6150 * @param array $notices - List of error messages to display on an error condition.
6151 * @return bool Return false if the submission was not submitted.
6153 public function submit_for_grading($data, $notices) {
6154 global $USER;
6156 $userid = $USER->id;
6157 if (!empty($data->userid)) {
6158 $userid = $data->userid;
6160 // Need submit permission to submit an assignment.
6161 if ($userid == $USER->id) {
6162 require_capability('mod/assign:submit', $this->context);
6163 } else {
6164 if (!$this->can_edit_submission($userid, $USER->id)) {
6165 print_error('nopermission');
6169 $instance = $this->get_instance();
6171 if ($instance->teamsubmission) {
6172 $submission = $this->get_group_submission($userid, 0, true);
6173 } else {
6174 $submission = $this->get_user_submission($userid, true);
6177 if (!$this->submissions_open($userid)) {
6178 $notices[] = get_string('submissionsclosed', 'assign');
6179 return false;
6182 if ($instance->requiresubmissionstatement && empty($data->submissionstatement) && $USER->id == $userid) {
6183 return false;
6186 if ($submission->status != ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
6187 // Give each submission plugin a chance to process the submission.
6188 $plugins = $this->get_submission_plugins();
6189 foreach ($plugins as $plugin) {
6190 if ($plugin->is_enabled() && $plugin->is_visible()) {
6191 $plugin->submit_for_grading($submission);
6195 $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
6196 $this->update_submission($submission, $userid, true, $instance->teamsubmission);
6197 $completion = new completion_info($this->get_course());
6198 if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
6199 $this->update_activity_completion_records($instance->teamsubmission,
6200 $instance->requireallteammemberssubmit,
6201 $submission,
6202 $userid,
6203 COMPLETION_COMPLETE,
6204 $completion);
6207 if (!empty($data->submissionstatement) && $USER->id == $userid) {
6208 \mod_assign\event\statement_accepted::create_from_submission($this, $submission)->trigger();
6210 $this->notify_graders($submission);
6211 $this->notify_student_submission_receipt($submission);
6213 \mod_assign\event\assessable_submitted::create_from_submission($this, $submission, false)->trigger();
6215 return true;
6217 $notices[] = get_string('submissionsclosed', 'assign');
6218 return false;
6222 * A students submission is submitted for grading by a teacher.
6224 * @return bool
6226 protected function process_submit_other_for_grading($mform, $notices) {
6227 global $USER, $CFG;
6229 require_sesskey();
6231 $userid = optional_param('userid', $USER->id, PARAM_INT);
6233 if (!$this->submissions_open($userid)) {
6234 $notices[] = get_string('submissionsclosed', 'assign');
6235 return false;
6237 $data = new stdClass();
6238 $data->userid = $userid;
6239 return $this->submit_for_grading($data, $notices);
6243 * Assignment submission is processed before grading.
6245 * @param moodleform|null $mform If validation failed when submitting this form - this is the moodleform.
6246 * It can be null.
6247 * @return bool Return false if the validation fails. This affects which page is displayed next.
6249 protected function process_submit_for_grading($mform, $notices) {
6250 global $CFG;
6252 require_once($CFG->dirroot . '/mod/assign/submissionconfirmform.php');
6253 require_sesskey();
6255 if (!$this->submissions_open()) {
6256 $notices[] = get_string('submissionsclosed', 'assign');
6257 return false;
6259 $instance = $this->get_instance();
6260 $data = new stdClass();
6261 $adminconfig = $this->get_admin_config();
6262 $requiresubmissionstatement = $instance->requiresubmissionstatement &&
6263 !empty($adminconfig->submissionstatement);
6265 $submissionstatement = '';
6266 if (!empty($adminconfig->submissionstatement)) {
6267 // Format the submission statement before its sent. We turn off para because this is going within
6268 // a form element.
6269 $options = array(
6270 'context' => $this->get_context(),
6271 'para' => false
6273 $submissionstatement = format_text($adminconfig->submissionstatement, FORMAT_MOODLE, $options);
6276 if ($mform == null) {
6277 $mform = new mod_assign_confirm_submission_form(null, array($requiresubmissionstatement,
6278 $submissionstatement,
6279 $this->get_course_module()->id,
6280 $data));
6283 $data = $mform->get_data();
6284 if (!$mform->is_cancelled()) {
6285 if ($mform->get_data() == false) {
6286 return false;
6288 return $this->submit_for_grading($data, $notices);
6290 return true;
6294 * Save the extension date for a single user.
6296 * @param int $userid The user id
6297 * @param mixed $extensionduedate Either an integer date or null
6298 * @return boolean
6300 public function save_user_extension($userid, $extensionduedate) {
6301 global $DB;
6303 // Need submit permission to submit an assignment.
6304 require_capability('mod/assign:grantextension', $this->context);
6306 if (!is_enrolled($this->get_course_context(), $userid)) {
6307 return false;
6309 if (!has_capability('mod/assign:submit', $this->context, $userid)) {
6310 return false;
6313 if ($this->get_instance()->duedate && $extensionduedate) {
6314 if ($this->get_instance()->duedate > $extensionduedate) {
6315 return false;
6318 if ($this->get_instance()->allowsubmissionsfromdate && $extensionduedate) {
6319 if ($this->get_instance()->allowsubmissionsfromdate > $extensionduedate) {
6320 return false;
6324 $flags = $this->get_user_flags($userid, true);
6325 $flags->extensionduedate = $extensionduedate;
6327 $result = $this->update_user_flags($flags);
6329 if ($result) {
6330 \mod_assign\event\extension_granted::create_from_assign($this, $userid)->trigger();
6332 return $result;
6336 * Save extension date.
6338 * @param moodleform $mform The submitted form
6339 * @return boolean
6341 protected function process_save_extension(& $mform) {
6342 global $DB, $CFG;
6344 // Include extension form.
6345 require_once($CFG->dirroot . '/mod/assign/extensionform.php');
6346 require_sesskey();
6348 $users = optional_param('userid', 0, PARAM_INT);
6349 if (!$users) {
6350 $users = required_param('selectedusers', PARAM_SEQUENCE);
6352 $userlist = explode(',', $users);
6354 $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate');
6355 $maxoverride = array('allowsubmissionsfromdate' => 0, 'duedate' => 0, 'cutoffdate' => 0);
6356 foreach ($userlist as $userid) {
6357 // To validate extension date with users overrides.
6358 $override = $this->override_exists($userid);
6359 foreach ($keys as $key) {
6360 if ($override->{$key}) {
6361 if ($maxoverride[$key] < $override->{$key}) {
6362 $maxoverride[$key] = $override->{$key};
6364 } else if ($maxoverride[$key] < $this->get_instance()->{$key}) {
6365 $maxoverride[$key] = $this->get_instance()->{$key};
6369 foreach ($keys as $key) {
6370 if ($maxoverride[$key]) {
6371 $this->get_instance()->{$key} = $maxoverride[$key];
6375 $formparams = array(
6376 'instance' => $this->get_instance(),
6377 'assign' => $this,
6378 'userlist' => $userlist
6381 $mform = new mod_assign_extension_form(null, $formparams);
6383 if ($mform->is_cancelled()) {
6384 return true;
6387 if ($formdata = $mform->get_data()) {
6388 if (!empty($formdata->selectedusers)) {
6389 $users = explode(',', $formdata->selectedusers);
6390 $result = true;
6391 foreach ($users as $userid) {
6392 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
6393 $result = $this->save_user_extension($user->id, $formdata->extensionduedate) && $result;
6395 return $result;
6397 if (!empty($formdata->userid)) {
6398 $user = $DB->get_record('user', array('id' => $formdata->userid), '*', MUST_EXIST);
6399 return $this->save_user_extension($user->id, $formdata->extensionduedate);
6403 return false;
6407 * Save quick grades.
6409 * @return string The result of the save operation
6411 protected function process_save_quick_grades() {
6412 global $USER, $DB, $CFG;
6414 // Need grade permission.
6415 require_capability('mod/assign:grade', $this->context);
6416 require_sesskey();
6418 // Make sure advanced grading is disabled.
6419 $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
6420 $controller = $gradingmanager->get_active_controller();
6421 if (!empty($controller)) {
6422 return get_string('errorquickgradingvsadvancedgrading', 'assign');
6425 $users = array();
6426 // First check all the last modified values.
6427 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
6428 $participants = $this->list_participants($currentgroup, true);
6430 // Gets a list of possible users and look for values based upon that.
6431 foreach ($participants as $userid => $unused) {
6432 $modified = optional_param('grademodified_' . $userid, -1, PARAM_INT);
6433 $attemptnumber = optional_param('gradeattempt_' . $userid, -1, PARAM_INT);
6434 // Gather the userid, updated grade and last modified value.
6435 $record = new stdClass();
6436 $record->userid = $userid;
6437 if ($modified >= 0) {
6438 $record->grade = unformat_float(optional_param('quickgrade_' . $record->userid, -1, PARAM_TEXT));
6439 $record->workflowstate = optional_param('quickgrade_' . $record->userid.'_workflowstate', false, PARAM_ALPHA);
6440 $record->allocatedmarker = optional_param('quickgrade_' . $record->userid.'_allocatedmarker', false, PARAM_INT);
6441 } else {
6442 // This user was not in the grading table.
6443 continue;
6445 $record->attemptnumber = $attemptnumber;
6446 $record->lastmodified = $modified;
6447 $record->gradinginfo = grade_get_grades($this->get_course()->id,
6448 'mod',
6449 'assign',
6450 $this->get_instance()->id,
6451 array($userid));
6452 $users[$userid] = $record;
6455 if (empty($users)) {
6456 return get_string('nousersselected', 'assign');
6459 list($userids, $params) = $DB->get_in_or_equal(array_keys($users), SQL_PARAMS_NAMED);
6460 $params['assignid1'] = $this->get_instance()->id;
6461 $params['assignid2'] = $this->get_instance()->id;
6463 // Check them all for currency.
6464 $grademaxattempt = 'SELECT s.userid, s.attemptnumber AS maxattempt
6465 FROM {assign_submission} s
6466 WHERE s.assignment = :assignid1 AND s.latest = 1';
6468 $sql = 'SELECT u.id AS userid, g.grade AS grade, g.timemodified AS lastmodified,
6469 uf.workflowstate, uf.allocatedmarker, gmx.maxattempt AS attemptnumber
6470 FROM {user} u
6471 LEFT JOIN ( ' . $grademaxattempt . ' ) gmx ON u.id = gmx.userid
6472 LEFT JOIN {assign_grades} g ON
6473 u.id = g.userid AND
6474 g.assignment = :assignid2 AND
6475 g.attemptnumber = gmx.maxattempt
6476 LEFT JOIN {assign_user_flags} uf ON uf.assignment = g.assignment AND uf.userid = g.userid
6477 WHERE u.id ' . $userids;
6478 $currentgrades = $DB->get_recordset_sql($sql, $params);
6480 $modifiedusers = array();
6481 foreach ($currentgrades as $current) {
6482 $modified = $users[(int)$current->userid];
6483 $grade = $this->get_user_grade($modified->userid, false);
6484 // Check to see if the grade column was even visible.
6485 $gradecolpresent = optional_param('quickgrade_' . $modified->userid, false, PARAM_INT) !== false;
6487 // Check to see if the outcomes were modified.
6488 if ($CFG->enableoutcomes) {
6489 foreach ($modified->gradinginfo->outcomes as $outcomeid => $outcome) {
6490 $oldoutcome = $outcome->grades[$modified->userid]->grade;
6491 $paramname = 'outcome_' . $outcomeid . '_' . $modified->userid;
6492 $newoutcome = optional_param($paramname, -1, PARAM_FLOAT);
6493 // Check to see if the outcome column was even visible.
6494 $outcomecolpresent = optional_param($paramname, false, PARAM_FLOAT) !== false;
6495 if ($outcomecolpresent && ($oldoutcome != $newoutcome)) {
6496 // Can't check modified time for outcomes because it is not reported.
6497 $modifiedusers[$modified->userid] = $modified;
6498 continue;
6503 // Let plugins participate.
6504 foreach ($this->feedbackplugins as $plugin) {
6505 if ($plugin->is_visible() && $plugin->is_enabled() && $plugin->supports_quickgrading()) {
6506 // The plugins must handle is_quickgrading_modified correctly - ie
6507 // handle hidden columns.
6508 if ($plugin->is_quickgrading_modified($modified->userid, $grade)) {
6509 if ((int)$current->lastmodified > (int)$modified->lastmodified) {
6510 return get_string('errorrecordmodified', 'assign');
6511 } else {
6512 $modifiedusers[$modified->userid] = $modified;
6513 continue;
6519 if (($current->grade < 0 || $current->grade === null) &&
6520 ($modified->grade < 0 || $modified->grade === null)) {
6521 // Different ways to indicate no grade.
6522 $modified->grade = $current->grade; // Keep existing grade.
6524 // Treat 0 and null as different values.
6525 if ($current->grade !== null) {
6526 $current->grade = floatval($current->grade);
6528 $gradechanged = $gradecolpresent && grade_floats_different($current->grade, $modified->grade);
6529 $markingallocationchanged = $this->get_instance()->markingworkflow &&
6530 $this->get_instance()->markingallocation &&
6531 ($modified->allocatedmarker !== false) &&
6532 ($current->allocatedmarker != $modified->allocatedmarker);
6533 $workflowstatechanged = $this->get_instance()->markingworkflow &&
6534 ($modified->workflowstate !== false) &&
6535 ($current->workflowstate != $modified->workflowstate);
6536 if ($gradechanged || $markingallocationchanged || $workflowstatechanged) {
6537 // Grade changed.
6538 if ($this->grading_disabled($modified->userid)) {
6539 continue;
6541 $badmodified = (int)$current->lastmodified > (int)$modified->lastmodified;
6542 $badattempt = (int)$current->attemptnumber != (int)$modified->attemptnumber;
6543 if ($badmodified || $badattempt) {
6544 // Error - record has been modified since viewing the page.
6545 return get_string('errorrecordmodified', 'assign');
6546 } else {
6547 $modifiedusers[$modified->userid] = $modified;
6552 $currentgrades->close();
6554 $adminconfig = $this->get_admin_config();
6555 $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
6557 // Ok - ready to process the updates.
6558 foreach ($modifiedusers as $userid => $modified) {
6559 $grade = $this->get_user_grade($userid, true);
6560 $flags = $this->get_user_flags($userid, true);
6561 $grade->grade= grade_floatval(unformat_float($modified->grade));
6562 $grade->grader= $USER->id;
6563 $gradecolpresent = optional_param('quickgrade_' . $userid, false, PARAM_INT) !== false;
6565 // Save plugins data.
6566 foreach ($this->feedbackplugins as $plugin) {
6567 if ($plugin->is_visible() && $plugin->is_enabled() && $plugin->supports_quickgrading()) {
6568 $plugin->save_quickgrading_changes($userid, $grade);
6569 if (('assignfeedback_' . $plugin->get_type()) == $gradebookplugin) {
6570 // This is the feedback plugin chose to push comments to the gradebook.
6571 $grade->feedbacktext = $plugin->text_for_gradebook($grade);
6572 $grade->feedbackformat = $plugin->format_for_gradebook($grade);
6577 // These will be set to false if they are not present in the quickgrading
6578 // form (e.g. column hidden).
6579 $workflowstatemodified = ($modified->workflowstate !== false) &&
6580 ($flags->workflowstate != $modified->workflowstate);
6582 $allocatedmarkermodified = ($modified->allocatedmarker !== false) &&
6583 ($flags->allocatedmarker != $modified->allocatedmarker);
6585 if ($workflowstatemodified) {
6586 $flags->workflowstate = $modified->workflowstate;
6588 if ($allocatedmarkermodified) {
6589 $flags->allocatedmarker = $modified->allocatedmarker;
6591 if ($workflowstatemodified || $allocatedmarkermodified) {
6592 if ($this->update_user_flags($flags) && $workflowstatemodified) {
6593 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
6594 \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $flags->workflowstate)->trigger();
6597 $this->update_grade($grade);
6599 // Allow teachers to skip sending notifications.
6600 if (optional_param('sendstudentnotifications', true, PARAM_BOOL)) {
6601 $this->notify_grade_modified($grade, true);
6604 // Save outcomes.
6605 if ($CFG->enableoutcomes) {
6606 $data = array();
6607 foreach ($modified->gradinginfo->outcomes as $outcomeid => $outcome) {
6608 $oldoutcome = $outcome->grades[$modified->userid]->grade;
6609 $paramname = 'outcome_' . $outcomeid . '_' . $modified->userid;
6610 // This will be false if the input was not in the quickgrading
6611 // form (e.g. column hidden).
6612 $newoutcome = optional_param($paramname, false, PARAM_INT);
6613 if ($newoutcome !== false && ($oldoutcome != $newoutcome)) {
6614 $data[$outcomeid] = $newoutcome;
6617 if (count($data) > 0) {
6618 grade_update_outcomes('mod/assign',
6619 $this->course->id,
6620 'mod',
6621 'assign',
6622 $this->get_instance()->id,
6623 $userid,
6624 $data);
6629 return get_string('quickgradingchangessaved', 'assign');
6633 * Reveal student identities to markers (and the gradebook).
6635 * @return void
6637 public function reveal_identities() {
6638 global $DB;
6640 require_capability('mod/assign:revealidentities', $this->context);
6642 if ($this->get_instance()->revealidentities || empty($this->get_instance()->blindmarking)) {
6643 return false;
6646 // Update the assignment record.
6647 $update = new stdClass();
6648 $update->id = $this->get_instance()->id;
6649 $update->revealidentities = 1;
6650 $DB->update_record('assign', $update);
6652 // Refresh the instance data.
6653 $this->instance = null;
6655 // Release the grades to the gradebook.
6656 // First create the column in the gradebook.
6657 $this->update_gradebook(false, $this->get_course_module()->id);
6659 // Now release all grades.
6661 $adminconfig = $this->get_admin_config();
6662 $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
6663 $gradebookplugin = str_replace('assignfeedback_', '', $gradebookplugin);
6664 $grades = $DB->get_records('assign_grades', array('assignment'=>$this->get_instance()->id));
6666 $plugin = $this->get_feedback_plugin_by_type($gradebookplugin);
6668 foreach ($grades as $grade) {
6669 // Fetch any comments for this student.
6670 if ($plugin && $plugin->is_enabled() && $plugin->is_visible()) {
6671 $grade->feedbacktext = $plugin->text_for_gradebook($grade);
6672 $grade->feedbackformat = $plugin->format_for_gradebook($grade);
6674 $this->gradebook_item_update(null, $grade);
6677 \mod_assign\event\identities_revealed::create_from_assign($this)->trigger();
6681 * Reveal student identities to markers (and the gradebook).
6683 * @return void
6685 protected function process_reveal_identities() {
6687 if (!confirm_sesskey()) {
6688 return false;
6691 return $this->reveal_identities();
6696 * Save grading options.
6698 * @return void
6700 protected function process_save_grading_options() {
6701 global $USER, $CFG;
6703 // Include grading options form.
6704 require_once($CFG->dirroot . '/mod/assign/gradingoptionsform.php');
6706 // Need submit permission to submit an assignment.
6707 $this->require_view_grades();
6708 require_sesskey();
6710 // Is advanced grading enabled?
6711 $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
6712 $controller = $gradingmanager->get_active_controller();
6713 $showquickgrading = empty($controller);
6714 if (!is_null($this->context)) {
6715 $showonlyactiveenrolopt = has_capability('moodle/course:viewsuspendedusers', $this->context);
6716 } else {
6717 $showonlyactiveenrolopt = false;
6720 $markingallocation = $this->get_instance()->markingworkflow &&
6721 $this->get_instance()->markingallocation &&
6722 has_capability('mod/assign:manageallocations', $this->context);
6723 // Get markers to use in drop lists.
6724 $markingallocationoptions = array();
6725 if ($markingallocation) {
6726 $markingallocationoptions[''] = get_string('filternone', 'assign');
6727 $markingallocationoptions[ASSIGN_MARKER_FILTER_NO_MARKER] = get_string('markerfilternomarker', 'assign');
6728 list($sort, $params) = users_order_by_sql('u');
6729 // Only enrolled users could be assigned as potential markers.
6730 $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
6731 foreach ($markers as $marker) {
6732 $markingallocationoptions[$marker->id] = fullname($marker);
6736 // Get marking states to show in form.
6737 $markingworkflowoptions = array();
6738 if ($this->get_instance()->markingworkflow) {
6739 $notmarked = get_string('markingworkflowstatenotmarked', 'assign');
6740 $markingworkflowoptions[''] = get_string('filternone', 'assign');
6741 $markingworkflowoptions[ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED] = $notmarked;
6742 $markingworkflowoptions = array_merge($markingworkflowoptions, $this->get_marking_workflow_states_for_current_user());
6745 $gradingoptionsparams = array('cm'=>$this->get_course_module()->id,
6746 'contextid'=>$this->context->id,
6747 'userid'=>$USER->id,
6748 'submissionsenabled'=>$this->is_any_submission_plugin_enabled(),
6749 'showquickgrading'=>$showquickgrading,
6750 'quickgrading'=>false,
6751 'markingworkflowopt' => $markingworkflowoptions,
6752 'markingallocationopt' => $markingallocationoptions,
6753 'showonlyactiveenrolopt'=>$showonlyactiveenrolopt,
6754 'showonlyactiveenrol' => $this->show_only_active_users(),
6755 'downloadasfolders' => get_user_preferences('assign_downloadasfolders', 1));
6756 $mform = new mod_assign_grading_options_form(null, $gradingoptionsparams);
6757 if ($formdata = $mform->get_data()) {
6758 set_user_preference('assign_perpage', $formdata->perpage);
6759 if (isset($formdata->filter)) {
6760 set_user_preference('assign_filter', $formdata->filter);
6762 if (isset($formdata->markerfilter)) {
6763 set_user_preference('assign_markerfilter', $formdata->markerfilter);
6765 if (isset($formdata->workflowfilter)) {
6766 set_user_preference('assign_workflowfilter', $formdata->workflowfilter);
6768 if ($showquickgrading) {
6769 set_user_preference('assign_quickgrading', isset($formdata->quickgrading));
6771 if (isset($formdata->downloadasfolders)) {
6772 set_user_preference('assign_downloadasfolders', 1); // Enabled.
6773 } else {
6774 set_user_preference('assign_downloadasfolders', 0); // Disabled.
6776 if (!empty($showonlyactiveenrolopt)) {
6777 $showonlyactiveenrol = isset($formdata->showonlyactiveenrol);
6778 set_user_preference('grade_report_showonlyactiveenrol', $showonlyactiveenrol);
6779 $this->showonlyactiveenrol = $showonlyactiveenrol;
6785 * Take a grade object and print a short summary for the log file.
6786 * The size limit for the log file is 255 characters, so be careful not
6787 * to include too much information.
6789 * @deprecated since 2.7
6791 * @param stdClass $grade
6792 * @return string
6794 public function format_grade_for_log(stdClass $grade) {
6795 global $DB;
6797 $user = $DB->get_record('user', array('id' => $grade->userid), '*', MUST_EXIST);
6799 $info = get_string('gradestudent', 'assign', array('id'=>$user->id, 'fullname'=>fullname($user)));
6800 if ($grade->grade != '') {
6801 $info .= get_string('grade') . ': ' . $this->display_grade($grade->grade, false) . '. ';
6802 } else {
6803 $info .= get_string('nograde', 'assign');
6805 return $info;
6809 * Take a submission object and print a short summary for the log file.
6810 * The size limit for the log file is 255 characters, so be careful not
6811 * to include too much information.
6813 * @deprecated since 2.7
6815 * @param stdClass $submission
6816 * @return string
6818 public function format_submission_for_log(stdClass $submission) {
6819 global $DB;
6821 $info = '';
6822 if ($submission->userid) {
6823 $user = $DB->get_record('user', array('id' => $submission->userid), '*', MUST_EXIST);
6824 $name = fullname($user);
6825 } else {
6826 $group = $this->get_submission_group($submission->userid);
6827 if ($group) {
6828 $name = $group->name;
6829 } else {
6830 $name = get_string('defaultteam', 'assign');
6833 $status = get_string('submissionstatus_' . $submission->status, 'assign');
6834 $params = array('id'=>$submission->userid, 'fullname'=>$name, 'status'=>$status);
6835 $info .= get_string('submissionlog', 'assign', $params) . ' <br>';
6837 foreach ($this->submissionplugins as $plugin) {
6838 if ($plugin->is_enabled() && $plugin->is_visible()) {
6839 $info .= '<br>' . $plugin->format_for_log($submission);
6843 return $info;
6847 * Require a valid sess key and then call copy_previous_attempt.
6849 * @param array $notices Any error messages that should be shown
6850 * to the user at the top of the edit submission form.
6851 * @return bool
6853 protected function process_copy_previous_attempt(&$notices) {
6854 require_sesskey();
6856 return $this->copy_previous_attempt($notices);
6860 * Copy the current assignment submission from the last submitted attempt.
6862 * @param array $notices Any error messages that should be shown
6863 * to the user at the top of the edit submission form.
6864 * @return bool
6866 public function copy_previous_attempt(&$notices) {
6867 global $USER, $CFG;
6869 require_capability('mod/assign:submit', $this->context);
6871 $instance = $this->get_instance();
6872 if ($instance->teamsubmission) {
6873 $submission = $this->get_group_submission($USER->id, 0, true);
6874 } else {
6875 $submission = $this->get_user_submission($USER->id, true);
6877 if (!$submission || $submission->status != ASSIGN_SUBMISSION_STATUS_REOPENED) {
6878 $notices[] = get_string('submissionnotcopiedinvalidstatus', 'assign');
6879 return false;
6881 $flags = $this->get_user_flags($USER->id, false);
6883 // Get the flags to check if it is locked.
6884 if ($flags && $flags->locked) {
6885 $notices[] = get_string('submissionslocked', 'assign');
6886 return false;
6888 if ($instance->submissiondrafts) {
6889 $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
6890 } else {
6891 $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
6893 $this->update_submission($submission, $USER->id, true, $instance->teamsubmission);
6895 // Find the previous submission.
6896 if ($instance->teamsubmission) {
6897 $previoussubmission = $this->get_group_submission($USER->id, 0, true, $submission->attemptnumber - 1);
6898 } else {
6899 $previoussubmission = $this->get_user_submission($USER->id, true, $submission->attemptnumber - 1);
6902 if (!$previoussubmission) {
6903 // There was no previous submission so there is nothing else to do.
6904 return true;
6907 $pluginerror = false;
6908 foreach ($this->get_submission_plugins() as $plugin) {
6909 if ($plugin->is_visible() && $plugin->is_enabled()) {
6910 if (!$plugin->copy_submission($previoussubmission, $submission)) {
6911 $notices[] = $plugin->get_error();
6912 $pluginerror = true;
6916 if ($pluginerror) {
6917 return false;
6920 \mod_assign\event\submission_duplicated::create_from_submission($this, $submission)->trigger();
6922 $complete = COMPLETION_INCOMPLETE;
6923 if ($submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
6924 $complete = COMPLETION_COMPLETE;
6926 $completion = new completion_info($this->get_course());
6927 if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
6928 $this->update_activity_completion_records($instance->teamsubmission,
6929 $instance->requireallteammemberssubmit,
6930 $submission,
6931 $USER->id,
6932 $complete,
6933 $completion);
6936 if (!$instance->submissiondrafts) {
6937 // There is a case for not notifying the student about the submission copy,
6938 // but it provides a record of the event and if they then cancel editing it
6939 // is clear that the submission was copied.
6940 $this->notify_student_submission_copied($submission);
6941 $this->notify_graders($submission);
6943 // The same logic applies here - we could not notify teachers,
6944 // but then they would wonder why there are submitted assignments
6945 // and they haven't been notified.
6946 \mod_assign\event\assessable_submitted::create_from_submission($this, $submission, true)->trigger();
6948 return true;
6952 * Determine if the current submission is empty or not.
6954 * @param submission $submission the students submission record to check.
6955 * @return bool
6957 public function submission_empty($submission) {
6958 $allempty = true;
6960 foreach ($this->submissionplugins as $plugin) {
6961 if ($plugin->is_enabled() && $plugin->is_visible()) {
6962 if (!$allempty || !$plugin->is_empty($submission)) {
6963 $allempty = false;
6967 return $allempty;
6971 * Determine if a new submission is empty or not
6973 * @param stdClass $data Submission data
6974 * @return bool
6976 public function new_submission_empty($data) {
6977 foreach ($this->submissionplugins as $plugin) {
6978 if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->allow_submissions() &&
6979 !$plugin->submission_is_empty($data)) {
6980 return false;
6983 return true;
6987 * Save assignment submission for the current user.
6989 * @param stdClass $data
6990 * @param array $notices Any error messages that should be shown
6991 * to the user.
6992 * @return bool
6994 public function save_submission(stdClass $data, & $notices) {
6995 global $CFG, $USER, $DB;
6997 $userid = $USER->id;
6998 if (!empty($data->userid)) {
6999 $userid = $data->userid;
7002 $user = clone($USER);
7003 if ($userid == $USER->id) {
7004 require_capability('mod/assign:submit', $this->context);
7005 } else {
7006 $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
7007 if (!$this->can_edit_submission($userid, $USER->id)) {
7008 print_error('nopermission');
7011 $instance = $this->get_instance();
7013 if ($instance->teamsubmission) {
7014 $submission = $this->get_group_submission($userid, 0, true);
7015 } else {
7016 $submission = $this->get_user_submission($userid, true);
7019 if ($this->new_submission_empty($data)) {
7020 $notices[] = get_string('submissionempty', 'mod_assign');
7021 return false;
7024 // Check that no one has modified the submission since we started looking at it.
7025 if (isset($data->lastmodified) && ($submission->timemodified > $data->lastmodified)) {
7026 // Another user has submitted something. Notify the current user.
7027 if ($submission->status !== ASSIGN_SUBMISSION_STATUS_NEW) {
7028 $notices[] = $instance->teamsubmission ? get_string('submissionmodifiedgroup', 'mod_assign')
7029 : get_string('submissionmodified', 'mod_assign');
7030 return false;
7034 if ($instance->submissiondrafts) {
7035 $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
7036 } else {
7037 $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
7040 $flags = $this->get_user_flags($userid, false);
7042 // Get the flags to check if it is locked.
7043 if ($flags && $flags->locked) {
7044 print_error('submissionslocked', 'assign');
7045 return true;
7048 $pluginerror = false;
7049 foreach ($this->submissionplugins as $plugin) {
7050 if ($plugin->is_enabled() && $plugin->is_visible()) {
7051 if (!$plugin->save($submission, $data)) {
7052 $notices[] = $plugin->get_error();
7053 $pluginerror = true;
7058 $allempty = $this->submission_empty($submission);
7059 if ($pluginerror || $allempty) {
7060 if ($allempty) {
7061 $notices[] = get_string('submissionempty', 'mod_assign');
7063 return false;
7066 $this->update_submission($submission, $userid, true, $instance->teamsubmission);
7068 if ($instance->teamsubmission && !$instance->requireallteammemberssubmit) {
7069 $team = $this->get_submission_group_members($submission->groupid, true);
7071 foreach ($team as $member) {
7072 if ($member->id != $userid) {
7073 $membersubmission = clone($submission);
7074 $this->update_submission($membersubmission, $member->id, true, $instance->teamsubmission);
7079 // Logging.
7080 if (isset($data->submissionstatement) && ($userid == $USER->id)) {
7081 \mod_assign\event\statement_accepted::create_from_submission($this, $submission)->trigger();
7084 $complete = COMPLETION_INCOMPLETE;
7085 if ($submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
7086 $complete = COMPLETION_COMPLETE;
7088 $completion = new completion_info($this->get_course());
7089 if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
7090 $completion->update_state($this->get_course_module(), $complete, $userid);
7093 if (!$instance->submissiondrafts) {
7094 $this->notify_student_submission_receipt($submission);
7095 $this->notify_graders($submission);
7096 \mod_assign\event\assessable_submitted::create_from_submission($this, $submission, true)->trigger();
7098 return true;
7102 * Save assignment submission.
7104 * @param moodleform $mform
7105 * @param array $notices Any error messages that should be shown
7106 * to the user at the top of the edit submission form.
7107 * @return bool
7109 protected function process_save_submission(&$mform, &$notices) {
7110 global $CFG, $USER;
7112 // Include submission form.
7113 require_once($CFG->dirroot . '/mod/assign/submission_form.php');
7115 $userid = optional_param('userid', $USER->id, PARAM_INT);
7116 // Need submit permission to submit an assignment.
7117 require_sesskey();
7118 if (!$this->submissions_open($userid)) {
7119 $notices[] = get_string('duedatereached', 'assign');
7120 return false;
7122 $instance = $this->get_instance();
7124 $data = new stdClass();
7125 $data->userid = $userid;
7126 $mform = new mod_assign_submission_form(null, array($this, $data));
7127 if ($mform->is_cancelled()) {
7128 return true;
7130 if ($data = $mform->get_data()) {
7131 return $this->save_submission($data, $notices);
7133 return false;
7138 * Determine if this users grade can be edited.
7140 * @param int $userid - The student userid
7141 * @param bool $checkworkflow - whether to include a check for the workflow state.
7142 * @return bool $gradingdisabled
7144 public function grading_disabled($userid, $checkworkflow=true) {
7145 global $CFG;
7146 if ($checkworkflow && $this->get_instance()->markingworkflow) {
7147 $grade = $this->get_user_grade($userid, false);
7148 $validstates = $this->get_marking_workflow_states_for_current_user();
7149 if (!empty($grade) && !empty($grade->workflowstate) && !array_key_exists($grade->workflowstate, $validstates)) {
7150 return true;
7153 $gradinginfo = grade_get_grades($this->get_course()->id,
7154 'mod',
7155 'assign',
7156 $this->get_instance()->id,
7157 array($userid));
7158 if (!$gradinginfo) {
7159 return false;
7162 if (!isset($gradinginfo->items[0]->grades[$userid])) {
7163 return false;
7165 $gradingdisabled = $gradinginfo->items[0]->grades[$userid]->locked ||
7166 $gradinginfo->items[0]->grades[$userid]->overridden;
7167 return $gradingdisabled;
7172 * Get an instance of a grading form if advanced grading is enabled.
7173 * This is specific to the assignment, marker and student.
7175 * @param int $userid - The student userid
7176 * @param stdClass|false $grade - The grade record
7177 * @param bool $gradingdisabled
7178 * @return mixed gradingform_instance|null $gradinginstance
7180 protected function get_grading_instance($userid, $grade, $gradingdisabled) {
7181 global $CFG, $USER;
7183 $grademenu = make_grades_menu($this->get_instance()->grade);
7184 $allowgradedecimals = $this->get_instance()->grade > 0;
7186 $advancedgradingwarning = false;
7187 $gradingmanager = get_grading_manager($this->context, 'mod_assign', 'submissions');
7188 $gradinginstance = null;
7189 if ($gradingmethod = $gradingmanager->get_active_method()) {
7190 $controller = $gradingmanager->get_controller($gradingmethod);
7191 if ($controller->is_form_available()) {
7192 $itemid = null;
7193 if ($grade) {
7194 $itemid = $grade->id;
7196 if ($gradingdisabled && $itemid) {
7197 $gradinginstance = $controller->get_current_instance($USER->id, $itemid);
7198 } else if (!$gradingdisabled) {
7199 $instanceid = optional_param('advancedgradinginstanceid', 0, PARAM_INT);
7200 $gradinginstance = $controller->get_or_create_instance($instanceid,
7201 $USER->id,
7202 $itemid);
7204 } else {
7205 $advancedgradingwarning = $controller->form_unavailable_notification();
7208 if ($gradinginstance) {
7209 $gradinginstance->get_controller()->set_grade_range($grademenu, $allowgradedecimals);
7211 return $gradinginstance;
7215 * Add elements to grade form.
7217 * @param MoodleQuickForm $mform
7218 * @param stdClass $data
7219 * @param array $params
7220 * @return void
7222 public function add_grade_form_elements(MoodleQuickForm $mform, stdClass $data, $params) {
7223 global $USER, $CFG, $SESSION;
7224 $settings = $this->get_instance();
7226 $rownum = isset($params['rownum']) ? $params['rownum'] : 0;
7227 $last = isset($params['last']) ? $params['last'] : true;
7228 $useridlistid = isset($params['useridlistid']) ? $params['useridlistid'] : 0;
7229 $userid = isset($params['userid']) ? $params['userid'] : 0;
7230 $attemptnumber = isset($params['attemptnumber']) ? $params['attemptnumber'] : 0;
7231 $gradingpanel = !empty($params['gradingpanel']);
7232 $bothids = ($userid && $useridlistid);
7234 if (!$userid || $bothids) {
7235 $useridlistkey = $this->get_useridlist_key($useridlistid);
7236 if (empty($SESSION->mod_assign_useridlist[$useridlistkey])) {
7237 $SESSION->mod_assign_useridlist[$useridlistkey] = $this->get_grading_userid_list();
7239 $useridlist = $SESSION->mod_assign_useridlist[$useridlistkey];
7240 } else {
7241 $useridlist = array($userid);
7242 $rownum = 0;
7243 $useridlistid = '';
7246 $userid = $useridlist[$rownum];
7247 // We need to create a grade record matching this attempt number
7248 // or the feedback plugin will have no way to know what is the correct attempt.
7249 $grade = $this->get_user_grade($userid, true, $attemptnumber);
7251 $submission = null;
7252 if ($this->get_instance()->teamsubmission) {
7253 $submission = $this->get_group_submission($userid, 0, false, $attemptnumber);
7254 } else {
7255 $submission = $this->get_user_submission($userid, false, $attemptnumber);
7258 // Add advanced grading.
7259 $gradingdisabled = $this->grading_disabled($userid);
7260 $gradinginstance = $this->get_grading_instance($userid, $grade, $gradingdisabled);
7262 $mform->addElement('header', 'gradeheader', get_string('grade'));
7263 if ($gradinginstance) {
7264 $gradingelement = $mform->addElement('grading',
7265 'advancedgrading',
7266 get_string('grade').':',
7267 array('gradinginstance' => $gradinginstance));
7268 if ($gradingdisabled) {
7269 $gradingelement->freeze();
7270 } else {
7271 $mform->addElement('hidden', 'advancedgradinginstanceid', $gradinginstance->get_id());
7272 $mform->setType('advancedgradinginstanceid', PARAM_INT);
7274 } else {
7275 // Use simple direct grading.
7276 if ($this->get_instance()->grade > 0) {
7277 $name = get_string('gradeoutof', 'assign', $this->get_instance()->grade);
7278 if (!$gradingdisabled) {
7279 $gradingelement = $mform->addElement('text', 'grade', $name);
7280 $mform->addHelpButton('grade', 'gradeoutofhelp', 'assign');
7281 $mform->setType('grade', PARAM_RAW);
7282 } else {
7283 $mform->addElement('hidden', 'grade', $name);
7284 $mform->hardFreeze('grade');
7285 $mform->setType('grade', PARAM_RAW);
7286 $strgradelocked = get_string('gradelocked', 'assign');
7287 $mform->addElement('static', 'gradedisabled', $name, $strgradelocked);
7288 $mform->addHelpButton('gradedisabled', 'gradeoutofhelp', 'assign');
7290 } else {
7291 $grademenu = array(-1 => get_string("nograde")) + make_grades_menu($this->get_instance()->grade);
7292 if (count($grademenu) > 1) {
7293 $gradingelement = $mform->addElement('select', 'grade', get_string('grade') . ':', $grademenu);
7295 // The grade is already formatted with format_float so it needs to be converted back to an integer.
7296 if (!empty($data->grade)) {
7297 $data->grade = (int)unformat_float($data->grade);
7299 $mform->setType('grade', PARAM_INT);
7300 if ($gradingdisabled) {
7301 $gradingelement->freeze();
7307 $gradinginfo = grade_get_grades($this->get_course()->id,
7308 'mod',
7309 'assign',
7310 $this->get_instance()->id,
7311 $userid);
7312 if (!empty($CFG->enableoutcomes)) {
7313 foreach ($gradinginfo->outcomes as $index => $outcome) {
7314 $options = make_grades_menu(-$outcome->scaleid);
7315 $options[0] = get_string('nooutcome', 'grades');
7316 if ($outcome->grades[$userid]->locked) {
7317 $mform->addElement('static',
7318 'outcome_' . $index . '[' . $userid . ']',
7319 $outcome->name . ':',
7320 $options[$outcome->grades[$userid]->grade]);
7321 } else {
7322 $attributes = array('id' => 'menuoutcome_' . $index );
7323 $mform->addElement('select',
7324 'outcome_' . $index . '[' . $userid . ']',
7325 $outcome->name.':',
7326 $options,
7327 $attributes);
7328 $mform->setType('outcome_' . $index . '[' . $userid . ']', PARAM_INT);
7329 $mform->setDefault('outcome_' . $index . '[' . $userid . ']',
7330 $outcome->grades[$userid]->grade);
7335 $capabilitylist = array('gradereport/grader:view', 'moodle/grade:viewall');
7336 if (has_all_capabilities($capabilitylist, $this->get_course_context())) {
7337 $urlparams = array('id'=>$this->get_course()->id);
7338 $url = new moodle_url('/grade/report/grader/index.php', $urlparams);
7339 $usergrade = '-';
7340 if (isset($gradinginfo->items[0]->grades[$userid]->str_grade)) {
7341 $usergrade = $gradinginfo->items[0]->grades[$userid]->str_grade;
7343 $gradestring = $this->get_renderer()->action_link($url, $usergrade);
7344 } else {
7345 $usergrade = '-';
7346 if (isset($gradinginfo->items[0]->grades[$userid]) &&
7347 !$gradinginfo->items[0]->grades[$userid]->hidden) {
7348 $usergrade = $gradinginfo->items[0]->grades[$userid]->str_grade;
7350 $gradestring = $usergrade;
7353 if ($this->get_instance()->markingworkflow) {
7354 $states = $this->get_marking_workflow_states_for_current_user();
7355 $options = array('' => get_string('markingworkflowstatenotmarked', 'assign')) + $states;
7356 $mform->addElement('select', 'workflowstate', get_string('markingworkflowstate', 'assign'), $options);
7357 $mform->addHelpButton('workflowstate', 'markingworkflowstate', 'assign');
7360 if ($this->get_instance()->markingworkflow &&
7361 $this->get_instance()->markingallocation &&
7362 has_capability('mod/assign:manageallocations', $this->context)) {
7364 list($sort, $params) = users_order_by_sql('u');
7365 // Only enrolled users could be assigned as potential markers.
7366 $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
7367 $markerlist = array('' => get_string('choosemarker', 'assign'));
7368 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
7369 foreach ($markers as $marker) {
7370 $markerlist[$marker->id] = fullname($marker, $viewfullnames);
7372 $mform->addElement('select', 'allocatedmarker', get_string('allocatedmarker', 'assign'), $markerlist);
7373 $mform->addHelpButton('allocatedmarker', 'allocatedmarker', 'assign');
7374 $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW);
7375 $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW);
7376 $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE);
7377 $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_RELEASED);
7379 $gradestring = '<span class="currentgrade">' . $gradestring . '</span>';
7380 $mform->addElement('static', 'currentgrade', get_string('currentgrade', 'assign'), $gradestring);
7382 if (count($useridlist) > 1) {
7383 $strparams = array('current'=>$rownum+1, 'total'=>count($useridlist));
7384 $name = get_string('outof', 'assign', $strparams);
7385 $mform->addElement('static', 'gradingstudent', get_string('gradingstudent', 'assign'), $name);
7388 // Let feedback plugins add elements to the grading form.
7389 $this->add_plugin_grade_elements($grade, $mform, $data, $userid);
7391 // Hidden params.
7392 $mform->addElement('hidden', 'id', $this->get_course_module()->id);
7393 $mform->setType('id', PARAM_INT);
7394 $mform->addElement('hidden', 'rownum', $rownum);
7395 $mform->setType('rownum', PARAM_INT);
7396 $mform->setConstant('rownum', $rownum);
7397 $mform->addElement('hidden', 'useridlistid', $useridlistid);
7398 $mform->setType('useridlistid', PARAM_ALPHANUM);
7399 $mform->addElement('hidden', 'attemptnumber', $attemptnumber);
7400 $mform->setType('attemptnumber', PARAM_INT);
7401 $mform->addElement('hidden', 'ajax', optional_param('ajax', 0, PARAM_INT));
7402 $mform->setType('ajax', PARAM_INT);
7403 $mform->addElement('hidden', 'userid', optional_param('userid', 0, PARAM_INT));
7404 $mform->setType('userid', PARAM_INT);
7406 if ($this->get_instance()->teamsubmission) {
7407 $mform->addElement('header', 'groupsubmissionsettings', get_string('groupsubmissionsettings', 'assign'));
7408 $mform->addElement('selectyesno', 'applytoall', get_string('applytoteam', 'assign'));
7409 $mform->setDefault('applytoall', 1);
7412 // Do not show if we are editing a previous attempt.
7413 if (($attemptnumber == -1 ||
7414 ($attemptnumber + 1) == count($this->get_all_submissions($userid))) &&
7415 $this->get_instance()->attemptreopenmethod != ASSIGN_ATTEMPT_REOPEN_METHOD_NONE) {
7416 $mform->addElement('header', 'attemptsettings', get_string('attemptsettings', 'assign'));
7417 $attemptreopenmethod = get_string('attemptreopenmethod_' . $this->get_instance()->attemptreopenmethod, 'assign');
7418 $mform->addElement('static', 'attemptreopenmethod', get_string('attemptreopenmethod', 'assign'), $attemptreopenmethod);
7420 $attemptnumber = 0;
7421 if ($submission) {
7422 $attemptnumber = $submission->attemptnumber;
7424 $maxattempts = $this->get_instance()->maxattempts;
7425 if ($maxattempts == ASSIGN_UNLIMITED_ATTEMPTS) {
7426 $maxattempts = get_string('unlimitedattempts', 'assign');
7428 $mform->addelement('static', 'maxattemptslabel', get_string('maxattempts', 'assign'), $maxattempts);
7429 $mform->addelement('static', 'attemptnumberlabel', get_string('attemptnumber', 'assign'), $attemptnumber + 1);
7431 $ismanual = $this->get_instance()->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL;
7432 $issubmission = !empty($submission);
7433 $isunlimited = $this->get_instance()->maxattempts == ASSIGN_UNLIMITED_ATTEMPTS;
7434 $islessthanmaxattempts = $issubmission && ($submission->attemptnumber < ($this->get_instance()->maxattempts-1));
7436 if ($ismanual && (!$issubmission || $isunlimited || $islessthanmaxattempts)) {
7437 $mform->addElement('selectyesno', 'addattempt', get_string('addattempt', 'assign'));
7438 $mform->setDefault('addattempt', 0);
7441 if (!$gradingpanel) {
7442 $mform->addElement('selectyesno', 'sendstudentnotifications', get_string('sendstudentnotifications', 'assign'));
7443 } else {
7444 $mform->addElement('hidden', 'sendstudentnotifications', get_string('sendstudentnotifications', 'assign'));
7445 $mform->setType('sendstudentnotifications', PARAM_BOOL);
7447 // Get assignment visibility information for student.
7448 $modinfo = get_fast_modinfo($settings->course, $userid);
7449 $cm = $modinfo->get_cm($this->get_course_module()->id);
7451 // Don't allow notification to be sent if the student can't access the assignment,
7452 // or until in "Released" state if using marking workflow.
7453 if (!$cm->uservisible) {
7454 $mform->setDefault('sendstudentnotifications', 0);
7455 $mform->freeze('sendstudentnotifications');
7456 } else if ($this->get_instance()->markingworkflow) {
7457 $mform->setDefault('sendstudentnotifications', 0);
7458 if (!$gradingpanel) {
7459 $mform->disabledIf('sendstudentnotifications', 'workflowstate', 'neq', ASSIGN_MARKING_WORKFLOW_STATE_RELEASED);
7461 } else {
7462 $mform->setDefault('sendstudentnotifications', $this->get_instance()->sendstudentnotifications);
7465 $mform->addElement('hidden', 'action', 'submitgrade');
7466 $mform->setType('action', PARAM_ALPHA);
7468 if (!$gradingpanel) {
7470 $buttonarray = array();
7471 $name = get_string('savechanges', 'assign');
7472 $buttonarray[] = $mform->createElement('submit', 'savegrade', $name);
7473 if (!$last) {
7474 $name = get_string('savenext', 'assign');
7475 $buttonarray[] = $mform->createElement('submit', 'saveandshownext', $name);
7477 $buttonarray[] = $mform->createElement('cancel', 'cancelbutton', get_string('cancel'));
7478 $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false);
7479 $mform->closeHeaderBefore('buttonar');
7480 $buttonarray = array();
7482 if ($rownum > 0) {
7483 $name = get_string('previous', 'assign');
7484 $buttonarray[] = $mform->createElement('submit', 'nosaveandprevious', $name);
7487 if (!$last) {
7488 $name = get_string('nosavebutnext', 'assign');
7489 $buttonarray[] = $mform->createElement('submit', 'nosaveandnext', $name);
7491 if (!empty($buttonarray)) {
7492 $mform->addGroup($buttonarray, 'navar', '', array(' '), false);
7495 // The grading form does not work well with shortforms.
7496 $mform->setDisableShortforms();
7500 * Add elements in submission plugin form.
7502 * @param mixed $submission stdClass|null
7503 * @param MoodleQuickForm $mform
7504 * @param stdClass $data
7505 * @param int $userid The current userid (same as $USER->id)
7506 * @return void
7508 protected function add_plugin_submission_elements($submission,
7509 MoodleQuickForm $mform,
7510 stdClass $data,
7511 $userid) {
7512 foreach ($this->submissionplugins as $plugin) {
7513 if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->allow_submissions()) {
7514 $plugin->get_form_elements_for_user($submission, $mform, $data, $userid);
7520 * Check if feedback plugins installed are enabled.
7522 * @return bool
7524 public function is_any_feedback_plugin_enabled() {
7525 if (!isset($this->cache['any_feedback_plugin_enabled'])) {
7526 $this->cache['any_feedback_plugin_enabled'] = false;
7527 foreach ($this->feedbackplugins as $plugin) {
7528 if ($plugin->is_enabled() && $plugin->is_visible()) {
7529 $this->cache['any_feedback_plugin_enabled'] = true;
7530 break;
7535 return $this->cache['any_feedback_plugin_enabled'];
7540 * Check if submission plugins installed are enabled.
7542 * @return bool
7544 public function is_any_submission_plugin_enabled() {
7545 if (!isset($this->cache['any_submission_plugin_enabled'])) {
7546 $this->cache['any_submission_plugin_enabled'] = false;
7547 foreach ($this->submissionplugins as $plugin) {
7548 if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->allow_submissions()) {
7549 $this->cache['any_submission_plugin_enabled'] = true;
7550 break;
7555 return $this->cache['any_submission_plugin_enabled'];
7560 * Add elements to submission form.
7561 * @param MoodleQuickForm $mform
7562 * @param stdClass $data
7563 * @return void
7565 public function add_submission_form_elements(MoodleQuickForm $mform, stdClass $data) {
7566 global $USER;
7568 $userid = $data->userid;
7569 // Team submissions.
7570 if ($this->get_instance()->teamsubmission) {
7571 $submission = $this->get_group_submission($userid, 0, false);
7572 } else {
7573 $submission = $this->get_user_submission($userid, false);
7576 // Submission statement.
7577 $adminconfig = $this->get_admin_config();
7579 $requiresubmissionstatement = $this->get_instance()->requiresubmissionstatement &&
7580 !empty($adminconfig->submissionstatement);
7582 $draftsenabled = $this->get_instance()->submissiondrafts;
7584 // Only show submission statement if we are editing our own submission.
7585 if ($requiresubmissionstatement && !$draftsenabled && $userid == $USER->id) {
7587 $submissionstatement = '';
7588 if (!empty($adminconfig->submissionstatement)) {
7589 // Format the submission statement before its sent. We turn off para because this is going within
7590 // a form element.
7591 $options = array(
7592 'context' => $this->get_context(),
7593 'para' => false
7595 $submissionstatement = format_text($adminconfig->submissionstatement, FORMAT_MOODLE, $options);
7597 $mform->addElement('checkbox', 'submissionstatement', '', $submissionstatement);
7598 $mform->addRule('submissionstatement', get_string('required'), 'required', null, 'client');
7601 $this->add_plugin_submission_elements($submission, $mform, $data, $userid);
7603 // Hidden params.
7604 $mform->addElement('hidden', 'id', $this->get_course_module()->id);
7605 $mform->setType('id', PARAM_INT);
7607 $mform->addElement('hidden', 'userid', $userid);
7608 $mform->setType('userid', PARAM_INT);
7610 $mform->addElement('hidden', 'action', 'savesubmission');
7611 $mform->setType('action', PARAM_ALPHA);
7615 * Revert to draft.
7617 * @param int $userid
7618 * @return boolean
7620 public function revert_to_draft($userid) {
7621 global $DB, $USER;
7623 // Need grade permission.
7624 require_capability('mod/assign:grade', $this->context);
7626 if ($this->get_instance()->teamsubmission) {
7627 $submission = $this->get_group_submission($userid, 0, false);
7628 } else {
7629 $submission = $this->get_user_submission($userid, false);
7632 if (!$submission) {
7633 return false;
7635 $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
7636 $this->update_submission($submission, $userid, true, $this->get_instance()->teamsubmission);
7638 // Give each submission plugin a chance to process the reverting to draft.
7639 $plugins = $this->get_submission_plugins();
7640 foreach ($plugins as $plugin) {
7641 if ($plugin->is_enabled() && $plugin->is_visible()) {
7642 $plugin->revert_to_draft($submission);
7645 // Update the modified time on the grade (grader modified).
7646 $grade = $this->get_user_grade($userid, true);
7647 $grade->grader = $USER->id;
7648 $this->update_grade($grade);
7650 $completion = new completion_info($this->get_course());
7651 if ($completion->is_enabled($this->get_course_module()) &&
7652 $this->get_instance()->completionsubmit) {
7653 $completion->update_state($this->get_course_module(), COMPLETION_INCOMPLETE, $userid);
7655 \mod_assign\event\submission_status_updated::create_from_submission($this, $submission)->trigger();
7656 return true;
7660 * Revert to draft.
7661 * Uses url parameter userid if userid not supplied as a parameter.
7663 * @param int $userid
7664 * @return boolean
7666 protected function process_revert_to_draft($userid = 0) {
7667 require_sesskey();
7669 if (!$userid) {
7670 $userid = required_param('userid', PARAM_INT);
7673 return $this->revert_to_draft($userid);
7677 * Prevent student updates to this submission
7679 * @param int $userid
7680 * @return bool
7682 public function lock_submission($userid) {
7683 global $USER, $DB;
7684 // Need grade permission.
7685 require_capability('mod/assign:grade', $this->context);
7687 // Give each submission plugin a chance to process the locking.
7688 $plugins = $this->get_submission_plugins();
7689 $submission = $this->get_user_submission($userid, false);
7691 $flags = $this->get_user_flags($userid, true);
7692 $flags->locked = 1;
7693 $this->update_user_flags($flags);
7695 foreach ($plugins as $plugin) {
7696 if ($plugin->is_enabled() && $plugin->is_visible()) {
7697 $plugin->lock($submission, $flags);
7701 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
7702 \mod_assign\event\submission_locked::create_from_user($this, $user)->trigger();
7703 return true;
7708 * Set the workflow state for multiple users
7710 * @return void
7712 protected function process_set_batch_marking_workflow_state() {
7713 global $CFG, $DB;
7715 // Include batch marking workflow form.
7716 require_once($CFG->dirroot . '/mod/assign/batchsetmarkingworkflowstateform.php');
7718 $formparams = array(
7719 'userscount' => 0, // This form is never re-displayed, so we don't need to
7720 'usershtml' => '', // initialise these parameters with real information.
7721 'markingworkflowstates' => $this->get_marking_workflow_states_for_current_user()
7724 $mform = new mod_assign_batch_set_marking_workflow_state_form(null, $formparams);
7726 if ($mform->is_cancelled()) {
7727 return true;
7730 if ($formdata = $mform->get_data()) {
7731 $useridlist = explode(',', $formdata->selectedusers);
7732 $state = $formdata->markingworkflowstate;
7734 foreach ($useridlist as $userid) {
7735 $flags = $this->get_user_flags($userid, true);
7737 $flags->workflowstate = $state;
7739 // Clear the mailed flag if notification is requested, the student hasn't been
7740 // notified previously, the student can access the assignment, and the state
7741 // is "Released".
7742 $modinfo = get_fast_modinfo($this->course, $userid);
7743 $cm = $modinfo->get_cm($this->get_course_module()->id);
7744 if ($formdata->sendstudentnotifications && $cm->uservisible &&
7745 $state == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
7746 $flags->mailed = 0;
7749 $gradingdisabled = $this->grading_disabled($userid);
7751 // Will not apply update if user does not have permission to assign this workflow state.
7752 if (!$gradingdisabled && $this->update_user_flags($flags)) {
7753 if ($state == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
7754 // Update Gradebook.
7755 $assign = clone $this->get_instance();
7756 $assign->cmidnumber = $this->get_course_module()->idnumber;
7757 // Set assign gradebook feedback plugin status.
7758 $assign->gradefeedbackenabled = $this->is_gradebook_feedback_enabled();
7759 assign_update_grades($assign, $userid);
7762 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
7763 \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $state)->trigger();
7770 * Set the marking allocation for multiple users
7772 * @return void
7774 protected function process_set_batch_marking_allocation() {
7775 global $CFG, $DB;
7777 // Include batch marking allocation form.
7778 require_once($CFG->dirroot . '/mod/assign/batchsetallocatedmarkerform.php');
7780 $formparams = array(
7781 'userscount' => 0, // This form is never re-displayed, so we don't need to
7782 'usershtml' => '' // initialise these parameters with real information.
7785 list($sort, $params) = users_order_by_sql('u');
7786 // Only enrolled users could be assigned as potential markers.
7787 $markers = get_enrolled_users($this->get_context(), 'mod/assign:grade', 0, 'u.*', $sort);
7788 $markerlist = array();
7789 foreach ($markers as $marker) {
7790 $markerlist[$marker->id] = fullname($marker);
7793 $formparams['markers'] = $markerlist;
7795 $mform = new mod_assign_batch_set_allocatedmarker_form(null, $formparams);
7797 if ($mform->is_cancelled()) {
7798 return true;
7801 if ($formdata = $mform->get_data()) {
7802 $useridlist = explode(',', $formdata->selectedusers);
7803 $marker = $DB->get_record('user', array('id' => $formdata->allocatedmarker), '*', MUST_EXIST);
7805 foreach ($useridlist as $userid) {
7806 $flags = $this->get_user_flags($userid, true);
7807 if ($flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW ||
7808 $flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW ||
7809 $flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE ||
7810 $flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
7812 continue; // Allocated marker can only be changed in certain workflow states.
7815 $flags->allocatedmarker = $marker->id;
7817 if ($this->update_user_flags($flags)) {
7818 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
7819 \mod_assign\event\marker_updated::create_from_marker($this, $user, $marker)->trigger();
7827 * Prevent student updates to this submission.
7828 * Uses url parameter userid.
7830 * @param int $userid
7831 * @return void
7833 protected function process_lock_submission($userid = 0) {
7835 require_sesskey();
7837 if (!$userid) {
7838 $userid = required_param('userid', PARAM_INT);
7841 return $this->lock_submission($userid);
7845 * Unlock the student submission.
7847 * @param int $userid
7848 * @return bool
7850 public function unlock_submission($userid) {
7851 global $USER, $DB;
7853 // Need grade permission.
7854 require_capability('mod/assign:grade', $this->context);
7856 // Give each submission plugin a chance to process the unlocking.
7857 $plugins = $this->get_submission_plugins();
7858 $submission = $this->get_user_submission($userid, false);
7860 $flags = $this->get_user_flags($userid, true);
7861 $flags->locked = 0;
7862 $this->update_user_flags($flags);
7864 foreach ($plugins as $plugin) {
7865 if ($plugin->is_enabled() && $plugin->is_visible()) {
7866 $plugin->unlock($submission, $flags);
7870 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
7871 \mod_assign\event\submission_unlocked::create_from_user($this, $user)->trigger();
7872 return true;
7876 * Unlock the student submission.
7877 * Uses url parameter userid.
7879 * @param int $userid
7880 * @return bool
7882 protected function process_unlock_submission($userid = 0) {
7884 require_sesskey();
7886 if (!$userid) {
7887 $userid = required_param('userid', PARAM_INT);
7890 return $this->unlock_submission($userid);
7894 * Apply a grade from a grading form to a user (may be called multiple times for a group submission).
7896 * @param stdClass $formdata - the data from the form
7897 * @param int $userid - the user to apply the grade to
7898 * @param int $attemptnumber - The attempt number to apply the grade to.
7899 * @return void
7901 protected function apply_grade_to_user($formdata, $userid, $attemptnumber) {
7902 global $USER, $CFG, $DB;
7904 $grade = $this->get_user_grade($userid, true, $attemptnumber);
7905 $originalgrade = $grade->grade;
7906 $gradingdisabled = $this->grading_disabled($userid);
7907 $gradinginstance = $this->get_grading_instance($userid, $grade, $gradingdisabled);
7908 if (!$gradingdisabled) {
7909 if ($gradinginstance) {
7910 $grade->grade = $gradinginstance->submit_and_get_grade($formdata->advancedgrading,
7911 $grade->id);
7912 } else {
7913 // Handle the case when grade is set to No Grade.
7914 if (isset($formdata->grade)) {
7915 $grade->grade = grade_floatval(unformat_float($formdata->grade));
7918 if (isset($formdata->workflowstate) || isset($formdata->allocatedmarker)) {
7919 $flags = $this->get_user_flags($userid, true);
7920 $oldworkflowstate = $flags->workflowstate;
7921 $flags->workflowstate = isset($formdata->workflowstate) ? $formdata->workflowstate : $flags->workflowstate;
7922 $flags->allocatedmarker = isset($formdata->allocatedmarker) ? $formdata->allocatedmarker : $flags->allocatedmarker;
7923 if ($this->update_user_flags($flags) &&
7924 isset($formdata->workflowstate) &&
7925 $formdata->workflowstate !== $oldworkflowstate) {
7926 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
7927 \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $formdata->workflowstate)->trigger();
7931 $grade->grader= $USER->id;
7933 $adminconfig = $this->get_admin_config();
7934 $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
7936 $feedbackmodified = false;
7938 // Call save in plugins.
7939 foreach ($this->feedbackplugins as $plugin) {
7940 if ($plugin->is_enabled() && $plugin->is_visible()) {
7941 $gradingmodified = $plugin->is_feedback_modified($grade, $formdata);
7942 if ($gradingmodified) {
7943 if (!$plugin->save($grade, $formdata)) {
7944 $result = false;
7945 print_error($plugin->get_error());
7947 // If $feedbackmodified is true, keep it true.
7948 $feedbackmodified = $feedbackmodified || $gradingmodified;
7950 if (('assignfeedback_' . $plugin->get_type()) == $gradebookplugin) {
7951 // This is the feedback plugin chose to push comments to the gradebook.
7952 $grade->feedbacktext = $plugin->text_for_gradebook($grade);
7953 $grade->feedbackformat = $plugin->format_for_gradebook($grade);
7958 // We do not want to update the timemodified if no grade was added.
7959 if (!empty($formdata->addattempt) ||
7960 ($originalgrade !== null && $originalgrade != -1) ||
7961 ($grade->grade !== null && $grade->grade != -1) ||
7962 $feedbackmodified) {
7963 $this->update_grade($grade, !empty($formdata->addattempt));
7966 // We never send notifications if we have marking workflow and the grade is not released.
7967 if ($this->get_instance()->markingworkflow &&
7968 isset($formdata->workflowstate) &&
7969 $formdata->workflowstate != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
7970 $formdata->sendstudentnotifications = false;
7973 // Note the default if not provided for this option is true (e.g. webservices).
7974 // This is for backwards compatibility.
7975 if (!isset($formdata->sendstudentnotifications) || $formdata->sendstudentnotifications) {
7976 $this->notify_grade_modified($grade, true);
7982 * Save outcomes submitted from grading form.
7984 * @param int $userid
7985 * @param stdClass $formdata
7986 * @param int $sourceuserid The user ID under which the outcome data is accessible. This is relevant
7987 * for an outcome set to a user but applied to an entire group.
7989 protected function process_outcomes($userid, $formdata, $sourceuserid = null) {
7990 global $CFG, $USER;
7992 if (empty($CFG->enableoutcomes)) {
7993 return;
7995 if ($this->grading_disabled($userid)) {
7996 return;
7999 require_once($CFG->libdir.'/gradelib.php');
8001 $data = array();
8002 $gradinginfo = grade_get_grades($this->get_course()->id,
8003 'mod',
8004 'assign',
8005 $this->get_instance()->id,
8006 $userid);
8008 if (!empty($gradinginfo->outcomes)) {
8009 foreach ($gradinginfo->outcomes as $index => $oldoutcome) {
8010 $name = 'outcome_'.$index;
8011 $sourceuserid = $sourceuserid !== null ? $sourceuserid : $userid;
8012 if (isset($formdata->{$name}[$sourceuserid]) &&
8013 $oldoutcome->grades[$userid]->grade != $formdata->{$name}[$sourceuserid]) {
8014 $data[$index] = $formdata->{$name}[$sourceuserid];
8018 if (count($data) > 0) {
8019 grade_update_outcomes('mod/assign',
8020 $this->course->id,
8021 'mod',
8022 'assign',
8023 $this->get_instance()->id,
8024 $userid,
8025 $data);
8030 * If the requirements are met - reopen the submission for another attempt.
8031 * Only call this function when grading the latest attempt.
8033 * @param int $userid The userid.
8034 * @param stdClass $submission The submission (may be a group submission).
8035 * @param bool $addattempt - True if the "allow another attempt" checkbox was checked.
8036 * @return bool - true if another attempt was added.
8038 protected function reopen_submission_if_required($userid, $submission, $addattempt) {
8039 $instance = $this->get_instance();
8040 $maxattemptsreached = !empty($submission) &&
8041 $submission->attemptnumber >= ($instance->maxattempts - 1) &&
8042 $instance->maxattempts != ASSIGN_UNLIMITED_ATTEMPTS;
8043 $shouldreopen = false;
8044 if ($instance->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS) {
8045 // Check the gradetopass from the gradebook.
8046 $gradeitem = $this->get_grade_item();
8047 if ($gradeitem) {
8048 $gradegrade = grade_grade::fetch(array('userid' => $userid, 'itemid' => $gradeitem->id));
8050 // Do not reopen if is_passed returns null, e.g. if there is no pass criterion set.
8051 if ($gradegrade && ($gradegrade->is_passed() === false)) {
8052 $shouldreopen = true;
8056 if ($instance->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL &&
8057 !empty($addattempt)) {
8058 $shouldreopen = true;
8060 if ($shouldreopen && !$maxattemptsreached) {
8061 $this->add_attempt($userid);
8062 return true;
8064 return false;
8068 * Save grade update.
8070 * @param int $userid
8071 * @param stdClass $data
8072 * @return bool - was the grade saved
8074 public function save_grade($userid, $data) {
8076 // Need grade permission.
8077 require_capability('mod/assign:grade', $this->context);
8079 $instance = $this->get_instance();
8080 $submission = null;
8081 if ($instance->teamsubmission) {
8082 // We need to know what the most recent group submission is.
8083 // Specifically when determining if we are adding another attempt (we only want to add one attempt per team),
8084 // and when deciding if we need to update the gradebook with an edited grade.
8085 $mostrecentsubmission = $this->get_group_submission($userid, 0, false, -1);
8086 $this->set_most_recent_team_submission($mostrecentsubmission);
8087 // Get the submission that we are saving grades for. The data attempt number determines which submission attempt.
8088 $submission = $this->get_group_submission($userid, 0, false, $data->attemptnumber);
8089 } else {
8090 $submission = $this->get_user_submission($userid, false, $data->attemptnumber);
8092 if ($instance->teamsubmission && !empty($data->applytoall)) {
8093 $groupid = 0;
8094 if ($this->get_submission_group($userid)) {
8095 $group = $this->get_submission_group($userid);
8096 if ($group) {
8097 $groupid = $group->id;
8100 $members = $this->get_submission_group_members($groupid, true, $this->show_only_active_users());
8101 foreach ($members as $member) {
8102 // We only want to update the grade for this group submission attempt. The data attempt number could be
8103 // -1 which may end up in additional attempts being created for each group member instead of just one
8104 // additional attempt for the group.
8105 $this->apply_grade_to_user($data, $member->id, $submission->attemptnumber);
8106 $this->process_outcomes($member->id, $data, $userid);
8108 } else {
8109 $this->apply_grade_to_user($data, $userid, $data->attemptnumber);
8111 $this->process_outcomes($userid, $data);
8114 return true;
8118 * Save grade.
8120 * @param moodleform $mform
8121 * @return bool - was the grade saved
8123 protected function process_save_grade(&$mform) {
8124 global $CFG, $SESSION;
8125 // Include grade form.
8126 require_once($CFG->dirroot . '/mod/assign/gradeform.php');
8128 require_sesskey();
8130 $instance = $this->get_instance();
8131 $rownum = required_param('rownum', PARAM_INT);
8132 $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT);
8133 $useridlistid = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
8134 $userid = optional_param('userid', 0, PARAM_INT);
8135 if (!$userid) {
8136 if (empty($SESSION->mod_assign_useridlist[$this->get_useridlist_key($useridlistid)])) {
8137 // If the userid list is not stored we must not save, as it is possible that the user in a
8138 // given row position may not be the same now as when the grading page was generated.
8139 $url = new moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id));
8140 throw new moodle_exception('useridlistnotcached', 'mod_assign', $url);
8142 $useridlist = $SESSION->mod_assign_useridlist[$this->get_useridlist_key($useridlistid)];
8143 } else {
8144 $useridlist = array($userid);
8145 $rownum = 0;
8148 $last = false;
8149 $userid = $useridlist[$rownum];
8150 if ($rownum == count($useridlist) - 1) {
8151 $last = true;
8154 $data = new stdClass();
8156 $gradeformparams = array('rownum' => $rownum,
8157 'useridlistid' => $useridlistid,
8158 'last' => $last,
8159 'attemptnumber' => $attemptnumber,
8160 'userid' => $userid);
8161 $mform = new mod_assign_grade_form(null,
8162 array($this, $data, $gradeformparams),
8163 'post',
8165 array('class'=>'gradeform'));
8167 if ($formdata = $mform->get_data()) {
8168 return $this->save_grade($userid, $formdata);
8169 } else {
8170 return false;
8175 * This function is a static wrapper around can_upgrade.
8177 * @param string $type The plugin type
8178 * @param int $version The plugin version
8179 * @return bool
8181 public static function can_upgrade_assignment($type, $version) {
8182 $assignment = new assign(null, null, null);
8183 return $assignment->can_upgrade($type, $version);
8187 * This function returns true if it can upgrade an assignment from the 2.2 module.
8189 * @param string $type The plugin type
8190 * @param int $version The plugin version
8191 * @return bool
8193 public function can_upgrade($type, $version) {
8194 if ($type == 'offline' && $version >= 2011112900) {
8195 return true;
8197 foreach ($this->submissionplugins as $plugin) {
8198 if ($plugin->can_upgrade($type, $version)) {
8199 return true;
8202 foreach ($this->feedbackplugins as $plugin) {
8203 if ($plugin->can_upgrade($type, $version)) {
8204 return true;
8207 return false;
8211 * Copy all the files from the old assignment files area to the new one.
8212 * This is used by the plugin upgrade code.
8214 * @param int $oldcontextid The old assignment context id
8215 * @param int $oldcomponent The old assignment component ('assignment')
8216 * @param int $oldfilearea The old assignment filearea ('submissions')
8217 * @param int $olditemid The old submissionid (can be null e.g. intro)
8218 * @param int $newcontextid The new assignment context id
8219 * @param int $newcomponent The new assignment component ('assignment')
8220 * @param int $newfilearea The new assignment filearea ('submissions')
8221 * @param int $newitemid The new submissionid (can be null e.g. intro)
8222 * @return int The number of files copied
8224 public function copy_area_files_for_upgrade($oldcontextid,
8225 $oldcomponent,
8226 $oldfilearea,
8227 $olditemid,
8228 $newcontextid,
8229 $newcomponent,
8230 $newfilearea,
8231 $newitemid) {
8232 // Note, this code is based on some code in filestorage - but that code
8233 // deleted the old files (which we don't want).
8234 $count = 0;
8236 $fs = get_file_storage();
8238 $oldfiles = $fs->get_area_files($oldcontextid,
8239 $oldcomponent,
8240 $oldfilearea,
8241 $olditemid,
8242 'id',
8243 false);
8244 foreach ($oldfiles as $oldfile) {
8245 $filerecord = new stdClass();
8246 $filerecord->contextid = $newcontextid;
8247 $filerecord->component = $newcomponent;
8248 $filerecord->filearea = $newfilearea;
8249 $filerecord->itemid = $newitemid;
8250 $fs->create_file_from_storedfile($filerecord, $oldfile);
8251 $count += 1;
8254 return $count;
8258 * Add a new attempt for each user in the list - but reopen each group assignment
8259 * at most 1 time.
8261 * @param array $useridlist Array of userids to reopen.
8262 * @return bool
8264 protected function process_add_attempt_group($useridlist) {
8265 $groupsprocessed = array();
8266 $result = true;
8268 foreach ($useridlist as $userid) {
8269 $groupid = 0;
8270 $group = $this->get_submission_group($userid);
8271 if ($group) {
8272 $groupid = $group->id;
8275 if (empty($groupsprocessed[$groupid])) {
8276 // We need to know what the most recent group submission is.
8277 // Specifically when determining if we are adding another attempt (we only want to add one attempt per team),
8278 // and when deciding if we need to update the gradebook with an edited grade.
8279 $currentsubmission = $this->get_group_submission($userid, 0, false, -1);
8280 $this->set_most_recent_team_submission($currentsubmission);
8281 $result = $this->process_add_attempt($userid) && $result;
8282 $groupsprocessed[$groupid] = true;
8285 return $result;
8289 * Check for a sess key and then call add_attempt.
8291 * @param int $userid int The user to add the attempt for
8292 * @return bool - true if successful.
8294 protected function process_add_attempt($userid) {
8295 require_sesskey();
8297 return $this->add_attempt($userid);
8301 * Add a new attempt for a user.
8303 * @param int $userid int The user to add the attempt for
8304 * @return bool - true if successful.
8306 protected function add_attempt($userid) {
8307 require_capability('mod/assign:grade', $this->context);
8309 if ($this->get_instance()->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_NONE) {
8310 return false;
8313 if ($this->get_instance()->teamsubmission) {
8314 $oldsubmission = $this->get_group_submission($userid, 0, false);
8315 } else {
8316 $oldsubmission = $this->get_user_submission($userid, false);
8319 if (!$oldsubmission) {
8320 return false;
8323 // No more than max attempts allowed.
8324 if ($this->get_instance()->maxattempts != ASSIGN_UNLIMITED_ATTEMPTS &&
8325 $oldsubmission->attemptnumber >= ($this->get_instance()->maxattempts - 1)) {
8326 return false;
8329 // Create the new submission record for the group/user.
8330 if ($this->get_instance()->teamsubmission) {
8331 if (isset($this->mostrecentteamsubmission)) {
8332 // Team submissions can end up in this function for each user (via save_grade). We don't want to create
8333 // more than one attempt for the whole team.
8334 if ($this->mostrecentteamsubmission->attemptnumber == $oldsubmission->attemptnumber) {
8335 $newsubmission = $this->get_group_submission($userid, 0, true, $oldsubmission->attemptnumber + 1);
8336 } else {
8337 $newsubmission = $this->get_group_submission($userid, 0, false, $oldsubmission->attemptnumber);
8339 } else {
8340 debugging('Please use set_most_recent_team_submission() before calling add_attempt', DEBUG_DEVELOPER);
8341 $newsubmission = $this->get_group_submission($userid, 0, true, $oldsubmission->attemptnumber + 1);
8343 } else {
8344 $newsubmission = $this->get_user_submission($userid, true, $oldsubmission->attemptnumber + 1);
8347 // Set the status of the new attempt to reopened.
8348 $newsubmission->status = ASSIGN_SUBMISSION_STATUS_REOPENED;
8350 // Give each submission plugin a chance to process the add_attempt.
8351 $plugins = $this->get_submission_plugins();
8352 foreach ($plugins as $plugin) {
8353 if ($plugin->is_enabled() && $plugin->is_visible()) {
8354 $plugin->add_attempt($oldsubmission, $newsubmission);
8358 $this->update_submission($newsubmission, $userid, false, $this->get_instance()->teamsubmission);
8359 $flags = $this->get_user_flags($userid, false);
8360 if (isset($flags->locked) && $flags->locked) { // May not exist.
8361 $this->process_unlock_submission($userid);
8363 return true;
8367 * Get an upto date list of user grades and feedback for the gradebook.
8369 * @param int $userid int or 0 for all users
8370 * @return array of grade data formated for the gradebook api
8371 * The data required by the gradebook api is userid,
8372 * rawgrade,
8373 * feedback,
8374 * feedbackformat,
8375 * usermodified,
8376 * dategraded,
8377 * datesubmitted
8379 public function get_user_grades_for_gradebook($userid) {
8380 global $DB, $CFG;
8381 $grades = array();
8382 $assignmentid = $this->get_instance()->id;
8384 $adminconfig = $this->get_admin_config();
8385 $gradebookpluginname = $adminconfig->feedback_plugin_for_gradebook;
8386 $gradebookplugin = null;
8388 // Find the gradebook plugin.
8389 foreach ($this->feedbackplugins as $plugin) {
8390 if ($plugin->is_enabled() && $plugin->is_visible()) {
8391 if (('assignfeedback_' . $plugin->get_type()) == $gradebookpluginname) {
8392 $gradebookplugin = $plugin;
8396 if ($userid) {
8397 $where = ' WHERE u.id = :userid ';
8398 } else {
8399 $where = ' WHERE u.id != :userid ';
8402 // When the gradebook asks us for grades - only return the last attempt for each user.
8403 $params = array('assignid1'=>$assignmentid,
8404 'assignid2'=>$assignmentid,
8405 'userid'=>$userid);
8406 $graderesults = $DB->get_recordset_sql('SELECT
8407 u.id as userid,
8408 s.timemodified as datesubmitted,
8409 g.grade as rawgrade,
8410 g.timemodified as dategraded,
8411 g.grader as usermodified
8412 FROM {user} u
8413 LEFT JOIN {assign_submission} s
8414 ON u.id = s.userid and s.assignment = :assignid1 AND
8415 s.latest = 1
8416 JOIN {assign_grades} g
8417 ON u.id = g.userid and g.assignment = :assignid2 AND
8418 g.attemptnumber = s.attemptnumber' .
8419 $where, $params);
8421 foreach ($graderesults as $result) {
8422 $gradingstatus = $this->get_grading_status($result->userid);
8423 if (!$this->get_instance()->markingworkflow || $gradingstatus == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
8424 $gradebookgrade = clone $result;
8425 // Now get the feedback.
8426 if ($gradebookplugin) {
8427 $grade = $this->get_user_grade($result->userid, false);
8428 if ($grade) {
8429 $gradebookgrade->feedback = $gradebookplugin->text_for_gradebook($grade);
8430 $gradebookgrade->feedbackformat = $gradebookplugin->format_for_gradebook($grade);
8433 $grades[$gradebookgrade->userid] = $gradebookgrade;
8437 $graderesults->close();
8438 return $grades;
8442 * Call the static version of this function
8444 * @param int $userid The userid to lookup
8445 * @return int The unique id
8447 public function get_uniqueid_for_user($userid) {
8448 return self::get_uniqueid_for_user_static($this->get_instance()->id, $userid);
8452 * Foreach participant in the course - assign them a random id.
8454 * @param int $assignid The assignid to lookup
8456 public static function allocate_unique_ids($assignid) {
8457 global $DB;
8459 $cm = get_coursemodule_from_instance('assign', $assignid, 0, false, MUST_EXIST);
8460 $context = context_module::instance($cm->id);
8462 $currentgroup = groups_get_activity_group($cm, true);
8463 $users = get_enrolled_users($context, "mod/assign:submit", $currentgroup, 'u.id');
8465 // Shuffle the users.
8466 shuffle($users);
8468 foreach ($users as $user) {
8469 $record = $DB->get_record('assign_user_mapping',
8470 array('assignment'=>$assignid, 'userid'=>$user->id),
8471 'id');
8472 if (!$record) {
8473 $record = new stdClass();
8474 $record->assignment = $assignid;
8475 $record->userid = $user->id;
8476 $DB->insert_record('assign_user_mapping', $record);
8482 * Lookup this user id and return the unique id for this assignment.
8484 * @param int $assignid The assignment id
8485 * @param int $userid The userid to lookup
8486 * @return int The unique id
8488 public static function get_uniqueid_for_user_static($assignid, $userid) {
8489 global $DB;
8491 // Search for a record.
8492 $params = array('assignment'=>$assignid, 'userid'=>$userid);
8493 if ($record = $DB->get_record('assign_user_mapping', $params, 'id')) {
8494 return $record->id;
8497 // Be a little smart about this - there is no record for the current user.
8498 // We should ensure any unallocated ids for the current participant
8499 // list are distrubited randomly.
8500 self::allocate_unique_ids($assignid);
8502 // Retry the search for a record.
8503 if ($record = $DB->get_record('assign_user_mapping', $params, 'id')) {
8504 return $record->id;
8507 // The requested user must not be a participant. Add a record anyway.
8508 $record = new stdClass();
8509 $record->assignment = $assignid;
8510 $record->userid = $userid;
8512 return $DB->insert_record('assign_user_mapping', $record);
8516 * Call the static version of this function.
8518 * @param int $uniqueid The uniqueid to lookup
8519 * @return int The user id or false if they don't exist
8521 public function get_user_id_for_uniqueid($uniqueid) {
8522 return self::get_user_id_for_uniqueid_static($this->get_instance()->id, $uniqueid);
8526 * Lookup this unique id and return the user id for this assignment.
8528 * @param int $assignid The id of the assignment this user mapping is in
8529 * @param int $uniqueid The uniqueid to lookup
8530 * @return int The user id or false if they don't exist
8532 public static function get_user_id_for_uniqueid_static($assignid, $uniqueid) {
8533 global $DB;
8535 // Search for a record.
8536 if ($record = $DB->get_record('assign_user_mapping',
8537 array('assignment'=>$assignid, 'id'=>$uniqueid),
8538 'userid',
8539 IGNORE_MISSING)) {
8540 return $record->userid;
8543 return false;
8547 * Get the list of marking_workflow states the current user has permission to transition a grade to.
8549 * @return array of state => description
8551 public function get_marking_workflow_states_for_current_user() {
8552 if (!empty($this->markingworkflowstates)) {
8553 return $this->markingworkflowstates;
8555 $states = array();
8556 if (has_capability('mod/assign:grade', $this->context)) {
8557 $states[ASSIGN_MARKING_WORKFLOW_STATE_INMARKING] = get_string('markingworkflowstateinmarking', 'assign');
8558 $states[ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW] = get_string('markingworkflowstatereadyforreview', 'assign');
8560 if (has_any_capability(array('mod/assign:reviewgrades',
8561 'mod/assign:managegrades'), $this->context)) {
8562 $states[ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW] = get_string('markingworkflowstateinreview', 'assign');
8563 $states[ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE] = get_string('markingworkflowstatereadyforrelease', 'assign');
8565 if (has_any_capability(array('mod/assign:releasegrades',
8566 'mod/assign:managegrades'), $this->context)) {
8567 $states[ASSIGN_MARKING_WORKFLOW_STATE_RELEASED] = get_string('markingworkflowstatereleased', 'assign');
8569 $this->markingworkflowstates = $states;
8570 return $this->markingworkflowstates;
8574 * Check is only active users in course should be shown.
8576 * @return bool true if only active users should be shown.
8578 public function show_only_active_users() {
8579 global $CFG;
8581 if (is_null($this->showonlyactiveenrol)) {
8582 $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol);
8583 $this->showonlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol);
8585 if (!is_null($this->context)) {
8586 $this->showonlyactiveenrol = $this->showonlyactiveenrol ||
8587 !has_capability('moodle/course:viewsuspendedusers', $this->context);
8590 return $this->showonlyactiveenrol;
8594 * Return true is user is active user in course else false
8596 * @param int $userid
8597 * @return bool true is user is active in course.
8599 public function is_active_user($userid) {
8600 return !in_array($userid, get_suspended_userids($this->context, true));
8604 * Returns true if gradebook feedback plugin is enabled
8606 * @return bool true if gradebook feedback plugin is enabled and visible else false.
8608 public function is_gradebook_feedback_enabled() {
8609 // Get default grade book feedback plugin.
8610 $adminconfig = $this->get_admin_config();
8611 $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
8612 $gradebookplugin = str_replace('assignfeedback_', '', $gradebookplugin);
8614 // Check if default gradebook feedback is visible and enabled.
8615 $gradebookfeedbackplugin = $this->get_feedback_plugin_by_type($gradebookplugin);
8617 if (empty($gradebookfeedbackplugin)) {
8618 return false;
8621 if ($gradebookfeedbackplugin->is_visible() && $gradebookfeedbackplugin->is_enabled()) {
8622 return true;
8625 // Gradebook feedback plugin is either not visible/enabled.
8626 return false;
8630 * Returns the grading status.
8632 * @param int $userid the user id
8633 * @return string returns the grading status
8635 public function get_grading_status($userid) {
8636 if ($this->get_instance()->markingworkflow) {
8637 $flags = $this->get_user_flags($userid, false);
8638 if (!empty($flags->workflowstate)) {
8639 return $flags->workflowstate;
8641 return ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED;
8642 } else {
8643 $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT);
8644 $grade = $this->get_user_grade($userid, false, $attemptnumber);
8646 if (!empty($grade) && $grade->grade !== null && $grade->grade >= 0) {
8647 return ASSIGN_GRADING_STATUS_GRADED;
8648 } else {
8649 return ASSIGN_GRADING_STATUS_NOT_GRADED;
8655 * The id used to uniquily identify the cache for this instance of the assign object.
8657 * @return string
8659 public function get_useridlist_key_id() {
8660 return $this->useridlistid;
8664 * Generates the key that should be used for an entry in the useridlist cache.
8666 * @param string $id Generate a key for this instance (optional)
8667 * @return string The key for the id, or new entry if no $id is passed.
8669 public function get_useridlist_key($id = null) {
8670 if ($id === null) {
8671 $id = $this->get_useridlist_key_id();
8673 return $this->get_course_module()->id . '_' . $id;
8677 * Updates and creates the completion records in mdl_course_modules_completion.
8679 * @param int $teamsubmission value of 0 or 1 to indicate whether this is a group activity
8680 * @param int $requireallteammemberssubmit value of 0 or 1 to indicate whether all group members must click Submit
8681 * @param obj $submission the submission
8682 * @param int $userid the user id
8683 * @param int $complete
8684 * @param obj $completion
8686 * @return null
8688 protected function update_activity_completion_records($teamsubmission,
8689 $requireallteammemberssubmit,
8690 $submission,
8691 $userid,
8692 $complete,
8693 $completion) {
8695 if (($teamsubmission && $submission->groupid > 0 && !$requireallteammemberssubmit) ||
8696 ($teamsubmission && $submission->groupid > 0 && $requireallteammemberssubmit &&
8697 $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED)) {
8699 $members = groups_get_members($submission->groupid);
8701 foreach ($members as $member) {
8702 $completion->update_state($this->get_course_module(), $complete, $member->id);
8704 } else {
8705 $completion->update_state($this->get_course_module(), $complete, $userid);
8708 return;
8712 * Update the module completion status (set it viewed).
8714 * @since Moodle 3.2
8716 public function set_module_viewed() {
8717 $completion = new completion_info($this->get_course());
8718 $completion->set_module_viewed($this->get_course_module());
8722 * Checks for any grade notices, and adds notifications. Will display on assignment main page and grading table.
8724 * @return void The notifications API will render the notifications at the appropriate part of the page.
8726 protected function add_grade_notices() {
8727 if (has_capability('mod/assign:grade', $this->get_context()) && get_config('assign', 'has_rescaled_null_grades_' . $this->get_instance()->id)) {
8728 $link = new \moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id, 'action' => 'fixrescalednullgrades'));
8729 \core\notification::warning(get_string('fixrescalednullgrades', 'mod_assign', ['link' => $link->out()]));
8734 * View fix rescaled null grades.
8736 * @return bool True if null all grades are now fixed.
8738 protected function fix_null_grades() {
8739 global $DB;
8740 $result = $DB->set_field_select(
8741 'assign_grades',
8742 'grade',
8743 ASSIGN_GRADE_NOT_SET,
8744 'grade <> ? AND grade < 0',
8745 [ASSIGN_GRADE_NOT_SET]
8747 $assign = clone $this->get_instance();
8748 $assign->cmidnumber = $this->get_course_module()->idnumber;
8749 assign_update_grades($assign);
8750 return $result;
8754 * View fix rescaled null grades.
8756 * @return void The notifications API will render the notifications at the appropriate part of the page.
8758 protected function view_fix_rescaled_null_grades() {
8759 global $OUTPUT;
8761 $o = '';
8763 require_capability('mod/assign:grade', $this->get_context());
8765 $instance = $this->get_instance();
8767 $o .= $this->get_renderer()->render(
8768 new assign_header(
8769 $instance,
8770 $this->get_context(),
8771 $this->show_intro(),
8772 $this->get_course_module()->id
8776 $confirm = optional_param('confirm', 0, PARAM_BOOL);
8778 if ($confirm) {
8779 confirm_sesskey();
8781 // Fix the grades.
8782 $this->fix_null_grades();
8783 unset_config('has_rescaled_null_grades_' . $instance->id, 'assign');
8785 // Display the notice.
8786 $o .= $this->get_renderer()->notification(get_string('fixrescalednullgradesdone', 'assign'), 'notifysuccess');
8787 $url = new moodle_url(
8788 '/mod/assign/view.php',
8789 array(
8790 'id' => $this->get_course_module()->id,
8791 'action' => 'grading'
8794 $o .= $this->get_renderer()->continue_button($url);
8795 } else {
8796 // Ask for confirmation.
8797 $continue = new \moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id, 'action' => 'fixrescalednullgrades', 'confirm' => true, 'sesskey' => sesskey()));
8798 $cancel = new \moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id));
8799 $o .= $OUTPUT->confirm(get_string('fixrescalednullgradesconfirm', 'mod_assign'), $continue, $cancel);
8802 $o .= $this->view_footer();
8804 return $o;
8808 * Set the most recent submission for the team.
8809 * The most recent team submission is used to determine if another attempt should be created when allowing another
8810 * attempt on a group assignment, and whether the gradebook should be updated.
8812 * @since Moodle 3.4
8813 * @param stdClass $submission The most recent submission of the group.
8815 public function set_most_recent_team_submission($submission) {
8816 $this->mostrecentteamsubmission = $submission;
8821 * Portfolio caller class for mod_assign.
8823 * @package mod_assign
8824 * @copyright 2012 NetSpot {@link http://www.netspot.com.au}
8825 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
8827 class assign_portfolio_caller extends portfolio_module_caller_base {
8829 /** @var int callback arg - the id of submission we export */
8830 protected $sid;
8832 /** @var string component of the submission files we export*/
8833 protected $component;
8835 /** @var string callback arg - the area of submission files we export */
8836 protected $area;
8838 /** @var int callback arg - the id of file we export */
8839 protected $fileid;
8841 /** @var int callback arg - the cmid of the assignment we export */
8842 protected $cmid;
8844 /** @var string callback arg - the plugintype of the editor we export */
8845 protected $plugin;
8847 /** @var string callback arg - the name of the editor field we export */
8848 protected $editor;
8851 * Callback arg for a single file export.
8853 public static function expected_callbackargs() {
8854 return array(
8855 'cmid' => true,
8856 'sid' => false,
8857 'area' => false,
8858 'component' => false,
8859 'fileid' => false,
8860 'plugin' => false,
8861 'editor' => false,
8866 * The constructor.
8868 * @param array $callbackargs
8870 public function __construct($callbackargs) {
8871 parent::__construct($callbackargs);
8872 $this->cm = get_coursemodule_from_id('assign', $this->cmid, 0, false, MUST_EXIST);
8876 * Load data needed for the portfolio export.
8878 * If the assignment type implements portfolio_load_data(), the processing is delegated
8879 * to it. Otherwise, the caller must provide either fileid (to export single file) or
8880 * submissionid and filearea (to export all data attached to the given submission file area)
8881 * via callback arguments.
8883 * @throws portfolio_caller_exception
8885 public function load_data() {
8887 $context = context_module::instance($this->cmid);
8889 if (empty($this->fileid)) {
8890 if (empty($this->sid) || empty($this->area)) {
8891 throw new portfolio_caller_exception('invalidfileandsubmissionid', 'mod_assign');
8896 // Export either an area of files or a single file (see function for more detail).
8897 // The first arg is an id or null. If it is an id, the rest of the args are ignored.
8898 // If it is null, the rest of the args are used to load a list of files from get_areafiles.
8899 $this->set_file_and_format_data($this->fileid,
8900 $context->id,
8901 $this->component,
8902 $this->area,
8903 $this->sid,
8904 'timemodified',
8905 false);
8910 * Prepares the package up before control is passed to the portfolio plugin.
8912 * @throws portfolio_caller_exception
8913 * @return mixed
8915 public function prepare_package() {
8917 if ($this->plugin && $this->editor) {
8918 $options = portfolio_format_text_options();
8919 $context = context_module::instance($this->cmid);
8920 $options->context = $context;
8922 $plugin = $this->get_submission_plugin();
8924 $text = $plugin->get_editor_text($this->editor, $this->sid);
8925 $format = $plugin->get_editor_format($this->editor, $this->sid);
8927 $html = format_text($text, $format, $options);
8928 $html = portfolio_rewrite_pluginfile_urls($html,
8929 $context->id,
8930 'mod_assign',
8931 $this->area,
8932 $this->sid,
8933 $this->exporter->get('format'));
8935 $exporterclass = $this->exporter->get('formatclass');
8936 if (in_array($exporterclass, array(PORTFOLIO_FORMAT_PLAINHTML, PORTFOLIO_FORMAT_RICHHTML))) {
8937 if ($files = $this->exporter->get('caller')->get('multifiles')) {
8938 foreach ($files as $file) {
8939 $this->exporter->copy_existing_file($file);
8942 return $this->exporter->write_new_file($html, 'assignment.html', !empty($files));
8943 } else if ($this->exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) {
8944 $leapwriter = $this->exporter->get('format')->leap2a_writer();
8945 $entry = new portfolio_format_leap2a_entry($this->area . $this->cmid,
8946 $context->get_context_name(),
8947 'resource',
8948 $html);
8950 $entry->add_category('web', 'resource_type');
8951 $entry->author = $this->user;
8952 $leapwriter->add_entry($entry);
8953 if ($files = $this->exporter->get('caller')->get('multifiles')) {
8954 $leapwriter->link_files($entry, $files, $this->area . $this->cmid . 'file');
8955 foreach ($files as $file) {
8956 $this->exporter->copy_existing_file($file);
8959 return $this->exporter->write_new_file($leapwriter->to_xml(),
8960 $this->exporter->get('format')->manifest_name(),
8961 true);
8962 } else {
8963 debugging('invalid format class: ' . $this->exporter->get('formatclass'));
8968 if ($this->exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) {
8969 $leapwriter = $this->exporter->get('format')->leap2a_writer();
8970 $files = array();
8971 if ($this->singlefile) {
8972 $files[] = $this->singlefile;
8973 } else if ($this->multifiles) {
8974 $files = $this->multifiles;
8975 } else {
8976 throw new portfolio_caller_exception('invalidpreparepackagefile',
8977 'portfolio',
8978 $this->get_return_url());
8981 $entryids = array();
8982 foreach ($files as $file) {
8983 $entry = new portfolio_format_leap2a_file($file->get_filename(), $file);
8984 $entry->author = $this->user;
8985 $leapwriter->add_entry($entry);
8986 $this->exporter->copy_existing_file($file);
8987 $entryids[] = $entry->id;
8989 if (count($files) > 1) {
8990 $baseid = 'assign' . $this->cmid . $this->area;
8991 $context = context_module::instance($this->cmid);
8993 // If we have multiple files, they should be grouped together into a folder.
8994 $entry = new portfolio_format_leap2a_entry($baseid . 'group',
8995 $context->get_context_name(),
8996 'selection');
8997 $leapwriter->add_entry($entry);
8998 $leapwriter->make_selection($entry, $entryids, 'Folder');
9000 return $this->exporter->write_new_file($leapwriter->to_xml(),
9001 $this->exporter->get('format')->manifest_name(),
9002 true);
9004 return $this->prepare_package_file();
9008 * Fetch the plugin by its type.
9010 * @return assign_submission_plugin
9012 protected function get_submission_plugin() {
9013 global $CFG;
9014 if (!$this->plugin || !$this->cmid) {
9015 return null;
9018 require_once($CFG->dirroot . '/mod/assign/locallib.php');
9020 $context = context_module::instance($this->cmid);
9022 $assignment = new assign($context, null, null);
9023 return $assignment->get_submission_plugin_by_type($this->plugin);
9027 * Calculate a sha1 has of either a single file or a list
9028 * of files based on the data set by load_data.
9030 * @return string
9032 public function get_sha1() {
9034 if ($this->plugin && $this->editor) {
9035 $plugin = $this->get_submission_plugin();
9036 $options = portfolio_format_text_options();
9037 $options->context = context_module::instance($this->cmid);
9039 $text = format_text($plugin->get_editor_text($this->editor, $this->sid),
9040 $plugin->get_editor_format($this->editor, $this->sid),
9041 $options);
9042 $textsha1 = sha1($text);
9043 $filesha1 = '';
9044 try {
9045 $filesha1 = $this->get_sha1_file();
9046 } catch (portfolio_caller_exception $e) {
9047 // No files.
9049 return sha1($textsha1 . $filesha1);
9051 return $this->get_sha1_file();
9055 * Calculate the time to transfer either a single file or a list
9056 * of files based on the data set by load_data.
9058 * @return int
9060 public function expected_time() {
9061 return $this->expected_time_file();
9065 * Checking the permissions.
9067 * @return bool
9069 public function check_permissions() {
9070 $context = context_module::instance($this->cmid);
9071 return has_capability('mod/assign:exportownsubmission', $context);
9075 * Display a module name.
9077 * @return string
9079 public static function display_name() {
9080 return get_string('modulename', 'assign');
9084 * Return array of formats supported by this portfolio call back.
9086 * @return array
9088 public static function base_supported_formats() {
9089 return array(PORTFOLIO_FORMAT_FILE, PORTFOLIO_FORMAT_LEAP2A);
9094 * Logic to happen when a/some group(s) has/have been deleted in a course.
9096 * @param int $courseid The course ID.
9097 * @param int $groupid The group id if it is known
9098 * @return void
9100 function assign_process_group_deleted_in_course($courseid, $groupid = null) {
9101 global $DB;
9103 $params = array('courseid' => $courseid);
9104 if ($groupid) {
9105 $params['groupid'] = $groupid;
9106 // We just update the group that was deleted.
9107 $sql = "SELECT o.id, o.assignid
9108 FROM {assign_overrides} o
9109 JOIN {assign} assign ON assign.id = o.assignid
9110 WHERE assign.course = :courseid
9111 AND o.groupid = :groupid";
9112 } else {
9113 // No groupid, we update all orphaned group overrides for all assign in course.
9114 $sql = "SELECT o.id, o.assignid
9115 FROM {assign_overrides} o
9116 JOIN {assign} assign ON assign.id = o.assignid
9117 LEFT JOIN {groups} grp ON grp.id = o.groupid
9118 WHERE assign.course = :courseid
9119 AND o.groupid IS NOT NULL
9120 AND grp.id IS NULL";
9122 $records = $DB->get_records_sql_menu($sql, $params);
9123 if (!$records) {
9124 return; // Nothing to do.
9126 $DB->delete_records_list('assign_overrides', 'id', array_keys($records));
9130 * Change the sort order of an override
9132 * @param int $id of the override
9133 * @param string $move direction of move
9134 * @param int $assignid of the assignment
9135 * @return bool success of operation
9137 function move_group_override($id, $move, $assignid) {
9138 global $DB;
9140 // Get the override object.
9141 if (!$override = $DB->get_record('assign_overrides', array('id' => $id), 'id, sortorder')) {
9142 return false;
9144 // Count the number of group overrides.
9145 $overridecountgroup = $DB->count_records('assign_overrides', array('userid' => null, 'assignid' => $assignid));
9147 // Calculate the new sortorder.
9148 if ( ($move == 'up') and ($override->sortorder > 1)) {
9149 $neworder = $override->sortorder - 1;
9150 } else if (($move == 'down') and ($override->sortorder < $overridecountgroup)) {
9151 $neworder = $override->sortorder + 1;
9152 } else {
9153 return false;
9156 // Retrieve the override object that is currently residing in the new position.
9157 $params = array('sortorder' => $neworder, 'assignid' => $assignid);
9158 if ($swapoverride = $DB->get_record('assign_overrides', $params, 'id, sortorder')) {
9160 // Swap the sortorders.
9161 $swapoverride->sortorder = $override->sortorder;
9162 $override->sortorder = $neworder;
9164 // Update the override records.
9165 $DB->update_record('assign_overrides', $override);
9166 $DB->update_record('assign_overrides', $swapoverride);
9169 reorder_group_overrides($assignid);
9170 return true;
9174 * Reorder the overrides starting at the override at the given startorder.
9176 * @param int $assignid of the assigment
9178 function reorder_group_overrides($assignid) {
9179 global $DB;
9181 $i = 1;
9182 if ($overrides = $DB->get_records('assign_overrides', array('userid' => null, 'assignid' => $assignid), 'sortorder ASC')) {
9183 foreach ($overrides as $override) {
9184 $f = new stdClass();
9185 $f->id = $override->id;
9186 $f->sortorder = $i++;
9187 $DB->update_record('assign_overrides', $f);
9189 // Update priorities of group overrides.
9190 $params = [
9191 'modulename' => 'assign',
9192 'instance' => $override->assignid,
9193 'groupid' => $override->groupid
9195 $DB->set_field('event', 'priority', $f->sortorder, $params);