MDL-57222 mod_assign: show validation message on visible element
[moodle.git] / mod / assign / locallib.php
blob1658b2849ad621556047f73797e2ab147980c815
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');
41 // Marker filter for grading page.
42 define('ASSIGN_MARKER_FILTER_NO_MARKER', -1);
44 // Reopen attempt methods.
45 define('ASSIGN_ATTEMPT_REOPEN_METHOD_NONE', 'none');
46 define('ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL', 'manual');
47 define('ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS', 'untilpass');
49 // Special value means allow unlimited attempts.
50 define('ASSIGN_UNLIMITED_ATTEMPTS', -1);
52 // Grading states.
53 define('ASSIGN_GRADING_STATUS_GRADED', 'graded');
54 define('ASSIGN_GRADING_STATUS_NOT_GRADED', 'notgraded');
56 // Marking workflow states.
57 define('ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED', 'notmarked');
58 define('ASSIGN_MARKING_WORKFLOW_STATE_INMARKING', 'inmarking');
59 define('ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW', 'readyforreview');
60 define('ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW', 'inreview');
61 define('ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE', 'readyforrelease');
62 define('ASSIGN_MARKING_WORKFLOW_STATE_RELEASED', 'released');
64 /** ASSIGN_MAX_EVENT_LENGTH = 432000 ; 5 days maximum */
65 define("ASSIGN_MAX_EVENT_LENGTH", "432000");
67 // Name of file area for intro attachments.
68 define('ASSIGN_INTROATTACHMENT_FILEAREA', 'introattachment');
70 require_once($CFG->libdir . '/accesslib.php');
71 require_once($CFG->libdir . '/formslib.php');
72 require_once($CFG->dirroot . '/repository/lib.php');
73 require_once($CFG->dirroot . '/mod/assign/mod_form.php');
74 require_once($CFG->libdir . '/gradelib.php');
75 require_once($CFG->dirroot . '/grade/grading/lib.php');
76 require_once($CFG->dirroot . '/mod/assign/feedbackplugin.php');
77 require_once($CFG->dirroot . '/mod/assign/submissionplugin.php');
78 require_once($CFG->dirroot . '/mod/assign/renderable.php');
79 require_once($CFG->dirroot . '/mod/assign/gradingtable.php');
80 require_once($CFG->libdir . '/eventslib.php');
81 require_once($CFG->libdir . '/portfolio/caller.php');
83 use \mod_assign\output\grading_app;
85 /**
86 * Standard base class for mod_assign (assignment types).
88 * @package mod_assign
89 * @copyright 2012 NetSpot {@link http://www.netspot.com.au}
90 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
92 class assign {
94 /** @var stdClass the assignment record that contains the global settings for this assign instance */
95 private $instance;
97 /** @var grade_item the grade_item record for this assign instance's primary grade item. */
98 private $gradeitem;
100 /** @var context the context of the course module for this assign instance
101 * (or just the course if we are creating a new one)
103 private $context;
105 /** @var stdClass the course this assign instance belongs to */
106 private $course;
108 /** @var stdClass the admin config for all assign instances */
109 private $adminconfig;
111 /** @var assign_renderer the custom renderer for this module */
112 private $output;
114 /** @var cm_info the course module for this assign instance */
115 private $coursemodule;
117 /** @var array cache for things like the coursemodule name or the scale menu -
118 * only lives for a single request.
120 private $cache;
122 /** @var array list of the installed submission plugins */
123 private $submissionplugins;
125 /** @var array list of the installed feedback plugins */
126 private $feedbackplugins;
128 /** @var string action to be used to return to this page
129 * (without repeating any form submissions etc).
131 private $returnaction = 'view';
133 /** @var array params to be used to return to this page */
134 private $returnparams = array();
136 /** @var string modulename prevents excessive calls to get_string */
137 private static $modulename = null;
139 /** @var string modulenameplural prevents excessive calls to get_string */
140 private static $modulenameplural = null;
142 /** @var array of marking workflow states for the current user */
143 private $markingworkflowstates = null;
145 /** @var bool whether to exclude users with inactive enrolment */
146 private $showonlyactiveenrol = null;
148 /** @var string A key used to identify userlists created by this object. */
149 private $useridlistid = null;
151 /** @var array cached list of participants for this assignment. The cache key will be group, showactive and the context id */
152 private $participants = array();
154 /** @var array cached list of user groups when team submissions are enabled. The cache key will be the user. */
155 private $usersubmissiongroups = array();
157 /** @var array cached list of user groups. The cache key will be the user. */
158 private $usergroups = array();
160 /** @var array cached list of IDs of users who share group membership with the user. The cache key will be the user. */
161 private $sharedgroupmembers = array();
164 * Constructor for the base assign class.
166 * Note: For $coursemodule you can supply a stdclass if you like, but it
167 * will be more efficient to supply a cm_info object.
169 * @param mixed $coursemodulecontext context|null the course module context
170 * (or the course context if the coursemodule has not been
171 * created yet).
172 * @param mixed $coursemodule the current course module if it was already loaded,
173 * otherwise this class will load one from the context as required.
174 * @param mixed $course the current course if it was already loaded,
175 * otherwise this class will load one from the context as required.
177 public function __construct($coursemodulecontext, $coursemodule, $course) {
178 global $SESSION;
180 $this->context = $coursemodulecontext;
181 $this->course = $course;
183 // Ensure that $this->coursemodule is a cm_info object (or null).
184 $this->coursemodule = cm_info::create($coursemodule);
186 // Temporary cache only lives for a single request - used to reduce db lookups.
187 $this->cache = array();
189 $this->submissionplugins = $this->load_plugins('assignsubmission');
190 $this->feedbackplugins = $this->load_plugins('assignfeedback');
192 // Extra entropy is required for uniqid() to work on cygwin.
193 $this->useridlistid = clean_param(uniqid('', true), PARAM_ALPHANUM);
195 if (!isset($SESSION->mod_assign_useridlist)) {
196 $SESSION->mod_assign_useridlist = [];
201 * Set the action and parameters that can be used to return to the current page.
203 * @param string $action The action for the current page
204 * @param array $params An array of name value pairs which form the parameters
205 * to return to the current page.
206 * @return void
208 public function register_return_link($action, $params) {
209 global $PAGE;
210 $params['action'] = $action;
211 $cm = $this->get_course_module();
212 if ($cm) {
213 $currenturl = new moodle_url('/mod/assign/view.php', array('id' => $cm->id));
214 } else {
215 $currenturl = new moodle_url('/mod/assign/index.php', array('id' => $this->get_course()->id));
218 $currenturl->params($params);
219 $PAGE->set_url($currenturl);
223 * Return an action that can be used to get back to the current page.
225 * @return string action
227 public function get_return_action() {
228 global $PAGE;
230 // Web services don't set a URL, we should avoid debugging when ussing the url object.
231 if (!WS_SERVER) {
232 $params = $PAGE->url->params();
235 if (!empty($params['action'])) {
236 return $params['action'];
238 return '';
242 * Based on the current assignment settings should we display the intro.
244 * @return bool showintro
246 public function show_intro() {
247 if ($this->get_instance()->alwaysshowdescription ||
248 time() > $this->get_instance()->allowsubmissionsfromdate) {
249 return true;
251 return false;
255 * Return a list of parameters that can be used to get back to the current page.
257 * @return array params
259 public function get_return_params() {
260 global $PAGE;
262 $params = $PAGE->url->params();
263 unset($params['id']);
264 unset($params['action']);
265 return $params;
269 * Set the submitted form data.
271 * @param stdClass $data The form data (instance)
273 public function set_instance(stdClass $data) {
274 $this->instance = $data;
278 * Set the context.
280 * @param context $context The new context
282 public function set_context(context $context) {
283 $this->context = $context;
287 * Set the course data.
289 * @param stdClass $course The course data
291 public function set_course(stdClass $course) {
292 $this->course = $course;
296 * Get list of feedback plugins installed.
298 * @return array
300 public function get_feedback_plugins() {
301 return $this->feedbackplugins;
305 * Get list of submission plugins installed.
307 * @return array
309 public function get_submission_plugins() {
310 return $this->submissionplugins;
314 * Is blind marking enabled and reveal identities not set yet?
316 * @return bool
318 public function is_blind_marking() {
319 return $this->get_instance()->blindmarking && !$this->get_instance()->revealidentities;
323 * Does an assignment have submission(s) or grade(s) already?
325 * @return bool
327 public function has_submissions_or_grades() {
328 $allgrades = $this->count_grades();
329 $allsubmissions = $this->count_submissions();
330 if (($allgrades == 0) && ($allsubmissions == 0)) {
331 return false;
333 return true;
337 * Get a specific submission plugin by its type.
339 * @param string $subtype assignsubmission | assignfeedback
340 * @param string $type
341 * @return mixed assign_plugin|null
343 public function get_plugin_by_type($subtype, $type) {
344 $shortsubtype = substr($subtype, strlen('assign'));
345 $name = $shortsubtype . 'plugins';
346 if ($name != 'feedbackplugins' && $name != 'submissionplugins') {
347 return null;
349 $pluginlist = $this->$name;
350 foreach ($pluginlist as $plugin) {
351 if ($plugin->get_type() == $type) {
352 return $plugin;
355 return null;
359 * Get a feedback plugin by type.
361 * @param string $type - The type of plugin e.g comments
362 * @return mixed assign_feedback_plugin|null
364 public function get_feedback_plugin_by_type($type) {
365 return $this->get_plugin_by_type('assignfeedback', $type);
369 * Get a submission plugin by type.
371 * @param string $type - The type of plugin e.g comments
372 * @return mixed assign_submission_plugin|null
374 public function get_submission_plugin_by_type($type) {
375 return $this->get_plugin_by_type('assignsubmission', $type);
379 * Load the plugins from the sub folders under subtype.
381 * @param string $subtype - either submission or feedback
382 * @return array - The sorted list of plugins
384 public function load_plugins($subtype) {
385 global $CFG;
386 $result = array();
388 $names = core_component::get_plugin_list($subtype);
390 foreach ($names as $name => $path) {
391 if (file_exists($path . '/locallib.php')) {
392 require_once($path . '/locallib.php');
394 $shortsubtype = substr($subtype, strlen('assign'));
395 $pluginclass = 'assign_' . $shortsubtype . '_' . $name;
397 $plugin = new $pluginclass($this, $name);
399 if ($plugin instanceof assign_plugin) {
400 $idx = $plugin->get_sort_order();
401 while (array_key_exists($idx, $result)) {
402 $idx +=1;
404 $result[$idx] = $plugin;
408 ksort($result);
409 return $result;
413 * Display the assignment, used by view.php
415 * The assignment is displayed differently depending on your role,
416 * the settings for the assignment and the status of the assignment.
418 * @param string $action The current action if any.
419 * @param array $args Optional arguments to pass to the view (instead of getting them from GET and POST).
420 * @return string - The page output.
422 public function view($action='', $args = array()) {
423 global $PAGE;
425 $o = '';
426 $mform = null;
427 $notices = array();
428 $nextpageparams = array();
430 if (!empty($this->get_course_module()->id)) {
431 $nextpageparams['id'] = $this->get_course_module()->id;
434 // Handle form submissions first.
435 if ($action == 'savesubmission') {
436 $action = 'editsubmission';
437 if ($this->process_save_submission($mform, $notices)) {
438 $action = 'redirect';
439 $nextpageparams['action'] = 'view';
441 } else if ($action == 'editprevioussubmission') {
442 $action = 'editsubmission';
443 if ($this->process_copy_previous_attempt($notices)) {
444 $action = 'redirect';
445 $nextpageparams['action'] = 'editsubmission';
447 } else if ($action == 'lock') {
448 $this->process_lock_submission();
449 $action = 'redirect';
450 $nextpageparams['action'] = 'grading';
451 } else if ($action == 'addattempt') {
452 $this->process_add_attempt(required_param('userid', PARAM_INT));
453 $action = 'redirect';
454 $nextpageparams['action'] = 'grading';
455 } else if ($action == 'reverttodraft') {
456 $this->process_revert_to_draft();
457 $action = 'redirect';
458 $nextpageparams['action'] = 'grading';
459 } else if ($action == 'unlock') {
460 $this->process_unlock_submission();
461 $action = 'redirect';
462 $nextpageparams['action'] = 'grading';
463 } else if ($action == 'setbatchmarkingworkflowstate') {
464 $this->process_set_batch_marking_workflow_state();
465 $action = 'redirect';
466 $nextpageparams['action'] = 'grading';
467 } else if ($action == 'setbatchmarkingallocation') {
468 $this->process_set_batch_marking_allocation();
469 $action = 'redirect';
470 $nextpageparams['action'] = 'grading';
471 } else if ($action == 'confirmsubmit') {
472 $action = 'submit';
473 if ($this->process_submit_for_grading($mform, $notices)) {
474 $action = 'redirect';
475 $nextpageparams['action'] = 'view';
476 } else if ($notices) {
477 $action = 'viewsubmitforgradingerror';
479 } else if ($action == 'submitotherforgrading') {
480 if ($this->process_submit_other_for_grading($mform, $notices)) {
481 $action = 'redirect';
482 $nextpageparams['action'] = 'grading';
483 } else {
484 $action = 'viewsubmitforgradingerror';
486 } else if ($action == 'gradingbatchoperation') {
487 $action = $this->process_grading_batch_operation($mform);
488 if ($action == 'grading') {
489 $action = 'redirect';
490 $nextpageparams['action'] = 'grading';
492 } else if ($action == 'submitgrade') {
493 if (optional_param('saveandshownext', null, PARAM_RAW)) {
494 // Save and show next.
495 $action = 'grade';
496 if ($this->process_save_grade($mform)) {
497 $action = 'redirect';
498 $nextpageparams['action'] = 'grade';
499 $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) + 1;
500 $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
502 } else if (optional_param('nosaveandprevious', null, PARAM_RAW)) {
503 $action = 'redirect';
504 $nextpageparams['action'] = 'grade';
505 $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) - 1;
506 $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
507 } else if (optional_param('nosaveandnext', null, PARAM_RAW)) {
508 $action = 'redirect';
509 $nextpageparams['action'] = 'grade';
510 $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) + 1;
511 $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
512 } else if (optional_param('savegrade', null, PARAM_RAW)) {
513 // Save changes button.
514 $action = 'grade';
515 if ($this->process_save_grade($mform)) {
516 $action = 'redirect';
517 $nextpageparams['action'] = 'savegradingresult';
519 } else {
520 // Cancel button.
521 $action = 'redirect';
522 $nextpageparams['action'] = 'grading';
524 } else if ($action == 'quickgrade') {
525 $message = $this->process_save_quick_grades();
526 $action = 'quickgradingresult';
527 } else if ($action == 'saveoptions') {
528 $this->process_save_grading_options();
529 $action = 'redirect';
530 $nextpageparams['action'] = 'grading';
531 } else if ($action == 'saveextension') {
532 $action = 'grantextension';
533 if ($this->process_save_extension($mform)) {
534 $action = 'redirect';
535 $nextpageparams['action'] = 'grading';
537 } else if ($action == 'revealidentitiesconfirm') {
538 $this->process_reveal_identities();
539 $action = 'redirect';
540 $nextpageparams['action'] = 'grading';
543 $returnparams = array('rownum'=>optional_param('rownum', 0, PARAM_INT),
544 'useridlistid' => optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM));
545 $this->register_return_link($action, $returnparams);
547 // Include any page action as part of the body tag CSS id.
548 if (!empty($action)) {
549 $PAGE->set_pagetype('mod-assign-' . $action);
551 // Now show the right view page.
552 if ($action == 'redirect') {
553 $nextpageurl = new moodle_url('/mod/assign/view.php', $nextpageparams);
554 redirect($nextpageurl);
555 return;
556 } else if ($action == 'savegradingresult') {
557 $message = get_string('gradingchangessaved', 'assign');
558 $o .= $this->view_savegrading_result($message);
559 } else if ($action == 'quickgradingresult') {
560 $mform = null;
561 $o .= $this->view_quickgrading_result($message);
562 } else if ($action == 'gradingpanel') {
563 $o .= $this->view_single_grading_panel($args);
564 } else if ($action == 'grade') {
565 $o .= $this->view_single_grade_page($mform);
566 } else if ($action == 'viewpluginassignfeedback') {
567 $o .= $this->view_plugin_content('assignfeedback');
568 } else if ($action == 'viewpluginassignsubmission') {
569 $o .= $this->view_plugin_content('assignsubmission');
570 } else if ($action == 'editsubmission') {
571 $o .= $this->view_edit_submission_page($mform, $notices);
572 } else if ($action == 'grader') {
573 $o .= $this->view_grader();
574 } else if ($action == 'grading') {
575 $o .= $this->view_grading_page();
576 } else if ($action == 'downloadall') {
577 $o .= $this->download_submissions();
578 } else if ($action == 'submit') {
579 $o .= $this->check_submit_for_grading($mform);
580 } else if ($action == 'grantextension') {
581 $o .= $this->view_grant_extension($mform);
582 } else if ($action == 'revealidentities') {
583 $o .= $this->view_reveal_identities_confirm($mform);
584 } else if ($action == 'plugingradingbatchoperation') {
585 $o .= $this->view_plugin_grading_batch_operation($mform);
586 } else if ($action == 'viewpluginpage') {
587 $o .= $this->view_plugin_page();
588 } else if ($action == 'viewcourseindex') {
589 $o .= $this->view_course_index();
590 } else if ($action == 'viewbatchsetmarkingworkflowstate') {
591 $o .= $this->view_batch_set_workflow_state($mform);
592 } else if ($action == 'viewbatchmarkingallocation') {
593 $o .= $this->view_batch_markingallocation($mform);
594 } else if ($action == 'viewsubmitforgradingerror') {
595 $o .= $this->view_error_page(get_string('submitforgrading', 'assign'), $notices);
596 } else {
597 $o .= $this->view_submission_page();
600 return $o;
604 * Add this instance to the database.
606 * @param stdClass $formdata The data submitted from the form
607 * @param bool $callplugins This is used to skip the plugin code
608 * when upgrading an old assignment to a new one (the plugins get called manually)
609 * @return mixed false if an error occurs or the int id of the new instance
611 public function add_instance(stdClass $formdata, $callplugins) {
612 global $DB;
613 $adminconfig = $this->get_admin_config();
615 $err = '';
617 // Add the database record.
618 $update = new stdClass();
619 $update->name = $formdata->name;
620 $update->timemodified = time();
621 $update->timecreated = time();
622 $update->course = $formdata->course;
623 $update->courseid = $formdata->course;
624 $update->intro = $formdata->intro;
625 $update->introformat = $formdata->introformat;
626 $update->alwaysshowdescription = !empty($formdata->alwaysshowdescription);
627 $update->submissiondrafts = $formdata->submissiondrafts;
628 $update->requiresubmissionstatement = $formdata->requiresubmissionstatement;
629 $update->sendnotifications = $formdata->sendnotifications;
630 $update->sendlatenotifications = $formdata->sendlatenotifications;
631 $update->sendstudentnotifications = $adminconfig->sendstudentnotifications;
632 if (isset($formdata->sendstudentnotifications)) {
633 $update->sendstudentnotifications = $formdata->sendstudentnotifications;
635 $update->duedate = $formdata->duedate;
636 $update->cutoffdate = $formdata->cutoffdate;
637 $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate;
638 $update->grade = $formdata->grade;
639 $update->completionsubmit = !empty($formdata->completionsubmit);
640 $update->teamsubmission = $formdata->teamsubmission;
641 $update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit;
642 if (isset($formdata->teamsubmissiongroupingid)) {
643 $update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid;
645 $update->blindmarking = $formdata->blindmarking;
646 $update->attemptreopenmethod = ASSIGN_ATTEMPT_REOPEN_METHOD_NONE;
647 if (!empty($formdata->attemptreopenmethod)) {
648 $update->attemptreopenmethod = $formdata->attemptreopenmethod;
650 if (!empty($formdata->maxattempts)) {
651 $update->maxattempts = $formdata->maxattempts;
653 if (isset($formdata->preventsubmissionnotingroup)) {
654 $update->preventsubmissionnotingroup = $formdata->preventsubmissionnotingroup;
656 $update->markingworkflow = $formdata->markingworkflow;
657 $update->markingallocation = $formdata->markingallocation;
658 if (empty($update->markingworkflow)) { // If marking workflow is disabled, make sure allocation is disabled.
659 $update->markingallocation = 0;
662 $returnid = $DB->insert_record('assign', $update);
663 $this->instance = $DB->get_record('assign', array('id'=>$returnid), '*', MUST_EXIST);
664 // Cache the course record.
665 $this->course = $DB->get_record('course', array('id'=>$formdata->course), '*', MUST_EXIST);
667 $this->save_intro_draft_files($formdata);
669 if ($callplugins) {
670 // Call save_settings hook for submission plugins.
671 foreach ($this->submissionplugins as $plugin) {
672 if (!$this->update_plugin_instance($plugin, $formdata)) {
673 print_error($plugin->get_error());
674 return false;
677 foreach ($this->feedbackplugins as $plugin) {
678 if (!$this->update_plugin_instance($plugin, $formdata)) {
679 print_error($plugin->get_error());
680 return false;
684 // In the case of upgrades the coursemodule has not been set,
685 // so we need to wait before calling these two.
686 $this->update_calendar($formdata->coursemodule);
687 $this->update_gradebook(false, $formdata->coursemodule);
691 $update = new stdClass();
692 $update->id = $this->get_instance()->id;
693 $update->nosubmissions = (!$this->is_any_submission_plugin_enabled()) ? 1: 0;
694 $DB->update_record('assign', $update);
696 return $returnid;
700 * Delete all grades from the gradebook for this assignment.
702 * @return bool
704 protected function delete_grades() {
705 global $CFG;
707 $result = grade_update('mod/assign',
708 $this->get_course()->id,
709 'mod',
710 'assign',
711 $this->get_instance()->id,
713 null,
714 array('deleted'=>1));
715 return $result == GRADE_UPDATE_OK;
719 * Delete this instance from the database.
721 * @return bool false if an error occurs
723 public function delete_instance() {
724 global $DB;
725 $result = true;
727 foreach ($this->submissionplugins as $plugin) {
728 if (!$plugin->delete_instance()) {
729 print_error($plugin->get_error());
730 $result = false;
733 foreach ($this->feedbackplugins as $plugin) {
734 if (!$plugin->delete_instance()) {
735 print_error($plugin->get_error());
736 $result = false;
740 // Delete files associated with this assignment.
741 $fs = get_file_storage();
742 if (! $fs->delete_area_files($this->context->id) ) {
743 $result = false;
746 $this->delete_all_overrides();
748 // Delete_records will throw an exception if it fails - so no need for error checking here.
749 $DB->delete_records('assign_submission', array('assignment' => $this->get_instance()->id));
750 $DB->delete_records('assign_grades', array('assignment' => $this->get_instance()->id));
751 $DB->delete_records('assign_plugin_config', array('assignment' => $this->get_instance()->id));
752 $DB->delete_records('assign_user_flags', array('assignment' => $this->get_instance()->id));
753 $DB->delete_records('assign_user_mapping', array('assignment' => $this->get_instance()->id));
755 // Delete items from the gradebook.
756 if (! $this->delete_grades()) {
757 $result = false;
760 // Delete the instance.
761 $DB->delete_records('assign', array('id'=>$this->get_instance()->id));
763 return $result;
767 * Deletes a assign override from the database and clears any corresponding calendar events
769 * @param int $overrideid The id of the override being deleted
770 * @return bool true on success
772 public function delete_override($overrideid) {
773 global $CFG, $DB;
775 require_once($CFG->dirroot . '/calendar/lib.php');
777 $cm = get_coursemodule_from_instance('assign', $this->get_context()->id, $this->get_context()->course);
779 $override = $DB->get_record('assign_overrides', array('id' => $overrideid), '*', MUST_EXIST);
781 // Delete the events.
782 $conds = array('modulename' => 'assign',
783 'instance' => $this->get_context()->id);
784 if (isset($override->userid)) {
785 $conds['userid'] = $override->userid;
786 } else {
787 $conds['groupid'] = $override->groupid;
789 $events = $DB->get_records('event', $conds);
790 foreach ($events as $event) {
791 $eventold = calendar_event::load($event);
792 $eventold->delete();
795 $DB->delete_records('assign_overrides', array('id' => $overrideid));
797 // Set the common parameters for one of the events we will be triggering.
798 $params = array(
799 'objectid' => $override->id,
800 'context' => context_module::instance($cm->id),
801 'other' => array(
802 'assignid' => $override->assignid
805 // Determine which override deleted event to fire.
806 if (!empty($override->userid)) {
807 $params['relateduserid'] = $override->userid;
808 $event = \mod_assign\event\user_override_deleted::create($params);
809 } else {
810 $params['other']['groupid'] = $override->groupid;
811 $event = \mod_assign\event\group_override_deleted::create($params);
814 // Trigger the override deleted event.
815 $event->add_record_snapshot('assign_overrides', $override);
816 $event->trigger();
818 return true;
822 * Deletes all assign overrides from the database and clears any corresponding calendar events
824 public function delete_all_overrides() {
825 global $DB;
827 $overrides = $DB->get_records('assign_overrides', array('assignid' => $this->get_context()->id), 'id');
828 foreach ($overrides as $override) {
829 $this->delete_override($override->id);
834 * Updates the assign properties with override information for a user.
836 * Algorithm: For each assign setting, if there is a matching user-specific override,
837 * then use that otherwise, if there are group-specific overrides, return the most
838 * lenient combination of them. If neither applies, leave the assign setting unchanged.
840 * @param int $userid The userid.
842 public function update_effective_access($userid) {
844 $override = $this->override_exists($userid);
846 // Merge with assign defaults.
847 $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate');
848 foreach ($keys as $key) {
849 if (isset($override->{$key})) {
850 $this->get_instance()->{$key} = $override->{$key};
857 * Returns whether an assign has any overrides.
859 * @return true if any, false if not
861 public function has_overrides() {
862 global $DB;
864 $override = $DB->record_exists('assign_overrides', array('assignid' => $this->get_instance()->id));
866 if ($override) {
867 return true;
870 return false;
874 * Returns user override
876 * Algorithm: For each assign setting, if there is a matching user-specific override,
877 * then use that otherwise, if there are group-specific overrides, return the most
878 * lenient combination of them. If neither applies, leave the assign setting unchanged.
880 * @param int $userid The userid.
881 * @return override if exist
883 public function override_exists($userid) {
884 global $DB;
886 // Check for user override.
887 $override = $DB->get_record('assign_overrides', array('assignid' => $this->get_instance()->id, 'userid' => $userid));
889 if (!$override) {
890 $override = new stdClass();
891 $override->duedate = null;
892 $override->cutoffdate = null;
893 $override->allowsubmissionsfromdate = null;
896 // Check for group overrides.
897 $groupings = groups_get_user_groups($this->get_instance()->course, $userid);
899 if (!empty($groupings[0])) {
900 // Select all overrides that apply to the User's groups.
901 list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0]));
902 $sql = "SELECT * FROM {assign_overrides}
903 WHERE groupid $extra AND assignid = ?";
904 $params[] = $this->get_instance()->id;
905 $records = $DB->get_records_sql($sql, $params);
907 // Combine the overrides.
908 $duedates = array();
909 $cutoffdates = array();
910 $allowsubmissionsfromdates = array();
912 foreach ($records as $gpoverride) {
913 if (isset($gpoverride->duedate)) {
914 $duedates[] = $gpoverride->duedate;
916 if (isset($gpoverride->cutoffdate)) {
917 $cutoffdates[] = $gpoverride->cutoffdate;
919 if (isset($gpoverride->allowsubmissionsfromdate)) {
920 $allowsubmissionsfromdates[] = $gpoverride->allowsubmissionsfromdate;
923 // If there is a user override for a setting, ignore the group override.
924 if (is_null($override->allowsubmissionsfromdate) && count($allowsubmissionsfromdates)) {
925 $override->allowsubmissionsfromdate = min($allowsubmissionsfromdates);
927 if (is_null($override->cutoffdate) && count($cutoffdates)) {
928 if (in_array(0, $cutoffdates)) {
929 $override->cutoffdate = 0;
930 } else {
931 $override->cutoffdate = max($cutoffdates);
934 if (is_null($override->duedate) && count($duedates)) {
935 if (in_array(0, $duedates)) {
936 $override->duedate = 0;
937 } else {
938 $override->duedate = max($duedates);
944 return $override;
948 * Actual implementation of the reset course functionality, delete all the
949 * assignment submissions for course $data->courseid.
951 * @param stdClass $data the data submitted from the reset course.
952 * @return array status array
954 public function reset_userdata($data) {
955 global $CFG, $DB;
957 $componentstr = get_string('modulenameplural', 'assign');
958 $status = array();
960 $fs = get_file_storage();
961 if (!empty($data->reset_assign_submissions)) {
962 // Delete files associated with this assignment.
963 foreach ($this->submissionplugins as $plugin) {
964 $fileareas = array();
965 $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type();
966 $fileareas = $plugin->get_file_areas();
967 foreach ($fileareas as $filearea => $notused) {
968 $fs->delete_area_files($this->context->id, $plugincomponent, $filearea);
971 if (!$plugin->delete_instance()) {
972 $status[] = array('component'=>$componentstr,
973 'item'=>get_string('deleteallsubmissions', 'assign'),
974 'error'=>$plugin->get_error());
978 foreach ($this->feedbackplugins as $plugin) {
979 $fileareas = array();
980 $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type();
981 $fileareas = $plugin->get_file_areas();
982 foreach ($fileareas as $filearea => $notused) {
983 $fs->delete_area_files($this->context->id, $plugincomponent, $filearea);
986 if (!$plugin->delete_instance()) {
987 $status[] = array('component'=>$componentstr,
988 'item'=>get_string('deleteallsubmissions', 'assign'),
989 'error'=>$plugin->get_error());
993 $assignids = $DB->get_records('assign', array('course' => $data->courseid), '', 'id');
994 list($sql, $params) = $DB->get_in_or_equal(array_keys($assignids));
996 $DB->delete_records_select('assign_submission', "assignment $sql", $params);
997 $DB->delete_records_select('assign_user_flags', "assignment $sql", $params);
999 $status[] = array('component'=>$componentstr,
1000 'item'=>get_string('deleteallsubmissions', 'assign'),
1001 'error'=>false);
1003 if (!empty($data->reset_gradebook_grades)) {
1004 $DB->delete_records_select('assign_grades', "assignment $sql", $params);
1005 // Remove all grades from gradebook.
1006 require_once($CFG->dirroot.'/mod/assign/lib.php');
1007 assign_reset_gradebook($data->courseid);
1009 // Reset revealidentities if both submissions and grades have been reset.
1010 if ($this->get_instance()->blindmarking && $this->get_instance()->revealidentities) {
1011 $DB->set_field('assign', 'revealidentities', 0, array('id' => $this->get_instance()->id));
1016 // Remove user overrides.
1017 if (!empty($data->reset_assign_user_overrides)) {
1018 $DB->delete_records_select('assign_overrides',
1019 'assignid IN (SELECT id FROM {assign} WHERE course = ?) AND userid IS NOT NULL', array($data->courseid));
1020 $status[] = array(
1021 'component' => $componentstr,
1022 'item' => get_string('useroverridesdeleted', 'assign'),
1023 'error' => false);
1025 // Remove group overrides.
1026 if (!empty($data->reset_assign_group_overrides)) {
1027 $DB->delete_records_select('assign_overrides',
1028 'assignid IN (SELECT id FROM {assign} WHERE course = ?) AND groupid IS NOT NULL', array($data->courseid));
1029 $status[] = array(
1030 'component' => $componentstr,
1031 'item' => get_string('groupoverridesdeleted', 'assign'),
1032 'error' => false);
1035 // Updating dates - shift may be negative too.
1036 if ($data->timeshift) {
1037 $DB->execute("UPDATE {assign_overrides}
1038 SET allowsubmissionsfromdate = allowsubmissionsfromdate + ?
1039 WHERE assignid = ? AND allowsubmissionsfromdate <> 0",
1040 array($data->timeshift, $this->get_instance()->id));
1041 $DB->execute("UPDATE {assign_overrides}
1042 SET duedate = duedate + ?
1043 WHERE assignid = ? AND duedate <> 0",
1044 array($data->timeshift, $this->get_instance()->id));
1045 $DB->execute("UPDATE {assign_overrides}
1046 SET cutoffdate = cutoffdate + ?
1047 WHERE assignid =? AND cutoffdate <> 0",
1048 array($data->timeshift, $this->get_instance()->id));
1050 shift_course_mod_dates('assign',
1051 array('duedate', 'allowsubmissionsfromdate', 'cutoffdate'),
1052 $data->timeshift,
1053 $data->courseid, $this->get_instance()->id);
1054 $status[] = array('component'=>$componentstr,
1055 'item'=>get_string('datechanged'),
1056 'error'=>false);
1059 return $status;
1063 * Update the settings for a single plugin.
1065 * @param assign_plugin $plugin The plugin to update
1066 * @param stdClass $formdata The form data
1067 * @return bool false if an error occurs
1069 protected function update_plugin_instance(assign_plugin $plugin, stdClass $formdata) {
1070 if ($plugin->is_visible()) {
1071 $enabledname = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
1072 if (!empty($formdata->$enabledname)) {
1073 $plugin->enable();
1074 if (!$plugin->save_settings($formdata)) {
1075 print_error($plugin->get_error());
1076 return false;
1078 } else {
1079 $plugin->disable();
1082 return true;
1086 * Update the gradebook information for this assignment.
1088 * @param bool $reset If true, will reset all grades in the gradbook for this assignment
1089 * @param int $coursemoduleid This is required because it might not exist in the database yet
1090 * @return bool
1092 public function update_gradebook($reset, $coursemoduleid) {
1093 global $CFG;
1095 require_once($CFG->dirroot.'/mod/assign/lib.php');
1096 $assign = clone $this->get_instance();
1097 $assign->cmidnumber = $coursemoduleid;
1099 // Set assign gradebook feedback plugin status (enabled and visible).
1100 $assign->gradefeedbackenabled = $this->is_gradebook_feedback_enabled();
1102 $param = null;
1103 if ($reset) {
1104 $param = 'reset';
1107 return assign_grade_item_update($assign, $param);
1111 * Get the marking table page size
1113 * @return integer
1115 public function get_assign_perpage() {
1116 $perpage = (int) get_user_preferences('assign_perpage', 10);
1117 $adminconfig = $this->get_admin_config();
1118 $maxperpage = -1;
1119 if (isset($adminconfig->maxperpage)) {
1120 $maxperpage = $adminconfig->maxperpage;
1122 if (isset($maxperpage) &&
1123 $maxperpage != -1 &&
1124 ($perpage == -1 || $perpage > $maxperpage)) {
1125 $perpage = $maxperpage;
1127 return $perpage;
1131 * Load and cache the admin config for this module.
1133 * @return stdClass the plugin config
1135 public function get_admin_config() {
1136 if ($this->adminconfig) {
1137 return $this->adminconfig;
1139 $this->adminconfig = get_config('assign');
1140 return $this->adminconfig;
1144 * Update the calendar entries for this assignment.
1146 * @param int $coursemoduleid - Required to pass this in because it might
1147 * not exist in the database yet.
1148 * @return bool
1150 public function update_calendar($coursemoduleid) {
1151 global $DB, $CFG;
1152 require_once($CFG->dirroot.'/calendar/lib.php');
1154 // Special case for add_instance as the coursemodule has not been set yet.
1155 $instance = $this->get_instance();
1157 $eventtype = 'due';
1159 if ($instance->duedate) {
1160 $event = new stdClass();
1162 $params = array('modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => $eventtype);
1163 $event->id = $DB->get_field('event', 'id', $params);
1164 $event->name = $instance->name;
1165 $event->timestart = $instance->duedate;
1167 // Convert the links to pluginfile. It is a bit hacky but at this stage the files
1168 // might not have been saved in the module area yet.
1169 $intro = $instance->intro;
1170 if ($draftid = file_get_submitted_draft_itemid('introeditor')) {
1171 $intro = file_rewrite_urls_to_pluginfile($intro, $draftid);
1174 // We need to remove the links to files as the calendar is not ready
1175 // to support module events with file areas.
1176 $intro = strip_pluginfile_content($intro);
1177 if ($this->show_intro()) {
1178 $event->description = array(
1179 'text' => $intro,
1180 'format' => $instance->introformat
1182 } else {
1183 $event->description = array(
1184 'text' => '',
1185 'format' => $instance->introformat
1189 if ($event->id) {
1190 $calendarevent = calendar_event::load($event->id);
1191 $calendarevent->update($event);
1192 } else {
1193 unset($event->id);
1194 $event->courseid = $instance->course;
1195 $event->groupid = 0;
1196 $event->userid = 0;
1197 $event->modulename = 'assign';
1198 $event->instance = $instance->id;
1199 $event->eventtype = $eventtype;
1200 $event->timeduration = 0;
1201 calendar_event::create($event);
1203 } else {
1204 $DB->delete_records('event', array('modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => $eventtype));
1210 * Update this instance in the database.
1212 * @param stdClass $formdata - the data submitted from the form
1213 * @return bool false if an error occurs
1215 public function update_instance($formdata) {
1216 global $DB;
1217 $adminconfig = $this->get_admin_config();
1219 $update = new stdClass();
1220 $update->id = $formdata->instance;
1221 $update->name = $formdata->name;
1222 $update->timemodified = time();
1223 $update->course = $formdata->course;
1224 $update->intro = $formdata->intro;
1225 $update->introformat = $formdata->introformat;
1226 $update->alwaysshowdescription = !empty($formdata->alwaysshowdescription);
1227 $update->submissiondrafts = $formdata->submissiondrafts;
1228 $update->requiresubmissionstatement = $formdata->requiresubmissionstatement;
1229 $update->sendnotifications = $formdata->sendnotifications;
1230 $update->sendlatenotifications = $formdata->sendlatenotifications;
1231 $update->sendstudentnotifications = $adminconfig->sendstudentnotifications;
1232 if (isset($formdata->sendstudentnotifications)) {
1233 $update->sendstudentnotifications = $formdata->sendstudentnotifications;
1235 $update->duedate = $formdata->duedate;
1236 $update->cutoffdate = $formdata->cutoffdate;
1237 $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate;
1238 $update->grade = $formdata->grade;
1239 if (!empty($formdata->completionunlocked)) {
1240 $update->completionsubmit = !empty($formdata->completionsubmit);
1242 $update->teamsubmission = $formdata->teamsubmission;
1243 $update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit;
1244 if (isset($formdata->teamsubmissiongroupingid)) {
1245 $update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid;
1247 $update->blindmarking = $formdata->blindmarking;
1248 $update->attemptreopenmethod = ASSIGN_ATTEMPT_REOPEN_METHOD_NONE;
1249 if (!empty($formdata->attemptreopenmethod)) {
1250 $update->attemptreopenmethod = $formdata->attemptreopenmethod;
1252 if (!empty($formdata->maxattempts)) {
1253 $update->maxattempts = $formdata->maxattempts;
1255 if (isset($formdata->preventsubmissionnotingroup)) {
1256 $update->preventsubmissionnotingroup = $formdata->preventsubmissionnotingroup;
1258 $update->markingworkflow = $formdata->markingworkflow;
1259 $update->markingallocation = $formdata->markingallocation;
1260 if (empty($update->markingworkflow)) { // If marking workflow is disabled, make sure allocation is disabled.
1261 $update->markingallocation = 0;
1264 $result = $DB->update_record('assign', $update);
1265 $this->instance = $DB->get_record('assign', array('id'=>$update->id), '*', MUST_EXIST);
1267 $this->save_intro_draft_files($formdata);
1269 // Load the assignment so the plugins have access to it.
1271 // Call save_settings hook for submission plugins.
1272 foreach ($this->submissionplugins as $plugin) {
1273 if (!$this->update_plugin_instance($plugin, $formdata)) {
1274 print_error($plugin->get_error());
1275 return false;
1278 foreach ($this->feedbackplugins as $plugin) {
1279 if (!$this->update_plugin_instance($plugin, $formdata)) {
1280 print_error($plugin->get_error());
1281 return false;
1285 $this->update_calendar($this->get_course_module()->id);
1286 $this->update_gradebook(false, $this->get_course_module()->id);
1288 $update = new stdClass();
1289 $update->id = $this->get_instance()->id;
1290 $update->nosubmissions = (!$this->is_any_submission_plugin_enabled()) ? 1: 0;
1291 $DB->update_record('assign', $update);
1293 return $result;
1297 * Save the attachments in the draft areas.
1299 * @param stdClass $formdata
1301 protected function save_intro_draft_files($formdata) {
1302 if (isset($formdata->introattachments)) {
1303 file_save_draft_area_files($formdata->introattachments, $this->get_context()->id,
1304 'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0);
1309 * Add elements in grading plugin form.
1311 * @param mixed $grade stdClass|null
1312 * @param MoodleQuickForm $mform
1313 * @param stdClass $data
1314 * @param int $userid - The userid we are grading
1315 * @return void
1317 protected function add_plugin_grade_elements($grade, MoodleQuickForm $mform, stdClass $data, $userid) {
1318 foreach ($this->feedbackplugins as $plugin) {
1319 if ($plugin->is_enabled() && $plugin->is_visible()) {
1320 $plugin->get_form_elements_for_user($grade, $mform, $data, $userid);
1328 * Add one plugins settings to edit plugin form.
1330 * @param assign_plugin $plugin The plugin to add the settings from
1331 * @param MoodleQuickForm $mform The form to add the configuration settings to.
1332 * This form is modified directly (not returned).
1333 * @param array $pluginsenabled A list of form elements to be added to a group.
1334 * The new element is added to this array by this function.
1335 * @return void
1337 protected function add_plugin_settings(assign_plugin $plugin, MoodleQuickForm $mform, & $pluginsenabled) {
1338 global $CFG;
1339 if ($plugin->is_visible() && !$plugin->is_configurable() && $plugin->is_enabled()) {
1340 $name = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
1341 $pluginsenabled[] = $mform->createElement('hidden', $name, 1);
1342 $mform->setType($name, PARAM_BOOL);
1343 $plugin->get_settings($mform);
1344 } else if ($plugin->is_visible() && $plugin->is_configurable()) {
1345 $name = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
1346 $label = $plugin->get_name();
1347 $label .= ' ' . $this->get_renderer()->help_icon('enabled', $plugin->get_subtype() . '_' . $plugin->get_type());
1348 $pluginsenabled[] = $mform->createElement('checkbox', $name, '', $label);
1350 $default = get_config($plugin->get_subtype() . '_' . $plugin->get_type(), 'default');
1351 if ($plugin->get_config('enabled') !== false) {
1352 $default = $plugin->is_enabled();
1354 $mform->setDefault($plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled', $default);
1356 $plugin->get_settings($mform);
1362 * Add settings to edit plugin form.
1364 * @param MoodleQuickForm $mform The form to add the configuration settings to.
1365 * This form is modified directly (not returned).
1366 * @return void
1368 public function add_all_plugin_settings(MoodleQuickForm $mform) {
1369 $mform->addElement('header', 'submissiontypes', get_string('submissiontypes', 'assign'));
1371 $submissionpluginsenabled = array();
1372 $group = $mform->addGroup(array(), 'submissionplugins', get_string('submissiontypes', 'assign'), array(' '), false);
1373 foreach ($this->submissionplugins as $plugin) {
1374 $this->add_plugin_settings($plugin, $mform, $submissionpluginsenabled);
1376 $group->setElements($submissionpluginsenabled);
1378 $mform->addElement('header', 'feedbacktypes', get_string('feedbacktypes', 'assign'));
1379 $feedbackpluginsenabled = array();
1380 $group = $mform->addGroup(array(), 'feedbackplugins', get_string('feedbacktypes', 'assign'), array(' '), false);
1381 foreach ($this->feedbackplugins as $plugin) {
1382 $this->add_plugin_settings($plugin, $mform, $feedbackpluginsenabled);
1384 $group->setElements($feedbackpluginsenabled);
1385 $mform->setExpanded('submissiontypes');
1389 * Allow each plugin an opportunity to update the defaultvalues
1390 * passed in to the settings form (needed to set up draft areas for
1391 * editor and filemanager elements)
1393 * @param array $defaultvalues
1395 public function plugin_data_preprocessing(&$defaultvalues) {
1396 foreach ($this->submissionplugins as $plugin) {
1397 if ($plugin->is_visible()) {
1398 $plugin->data_preprocessing($defaultvalues);
1401 foreach ($this->feedbackplugins as $plugin) {
1402 if ($plugin->is_visible()) {
1403 $plugin->data_preprocessing($defaultvalues);
1409 * Get the name of the current module.
1411 * @return string the module name (Assignment)
1413 protected function get_module_name() {
1414 if (isset(self::$modulename)) {
1415 return self::$modulename;
1417 self::$modulename = get_string('modulename', 'assign');
1418 return self::$modulename;
1422 * Get the plural name of the current module.
1424 * @return string the module name plural (Assignments)
1426 protected function get_module_name_plural() {
1427 if (isset(self::$modulenameplural)) {
1428 return self::$modulenameplural;
1430 self::$modulenameplural = get_string('modulenameplural', 'assign');
1431 return self::$modulenameplural;
1435 * Has this assignment been constructed from an instance?
1437 * @return bool
1439 public function has_instance() {
1440 return $this->instance || $this->get_course_module();
1444 * Get the settings for the current instance of this assignment
1446 * @return stdClass The settings
1448 public function get_instance() {
1449 global $DB;
1450 if ($this->instance) {
1451 return $this->instance;
1453 if ($this->get_course_module()) {
1454 $params = array('id' => $this->get_course_module()->instance);
1455 $this->instance = $DB->get_record('assign', $params, '*', MUST_EXIST);
1457 if (!$this->instance) {
1458 throw new coding_exception('Improper use of the assignment class. ' .
1459 'Cannot load the assignment record.');
1461 return $this->instance;
1465 * Get the primary grade item for this assign instance.
1467 * @return grade_item The grade_item record
1469 public function get_grade_item() {
1470 if ($this->gradeitem) {
1471 return $this->gradeitem;
1473 $instance = $this->get_instance();
1474 $params = array('itemtype' => 'mod',
1475 'itemmodule' => 'assign',
1476 'iteminstance' => $instance->id,
1477 'courseid' => $instance->course,
1478 'itemnumber' => 0);
1479 $this->gradeitem = grade_item::fetch($params);
1480 if (!$this->gradeitem) {
1481 throw new coding_exception('Improper use of the assignment class. ' .
1482 'Cannot load the grade item.');
1484 return $this->gradeitem;
1488 * Get the context of the current course.
1490 * @return mixed context|null The course context
1492 public function get_course_context() {
1493 if (!$this->context && !$this->course) {
1494 throw new coding_exception('Improper use of the assignment class. ' .
1495 'Cannot load the course context.');
1497 if ($this->context) {
1498 return $this->context->get_course_context();
1499 } else {
1500 return context_course::instance($this->course->id);
1506 * Get the current course module.
1508 * @return cm_info|null The course module or null if not known
1510 public function get_course_module() {
1511 if ($this->coursemodule) {
1512 return $this->coursemodule;
1514 if (!$this->context) {
1515 return null;
1518 if ($this->context->contextlevel == CONTEXT_MODULE) {
1519 $modinfo = get_fast_modinfo($this->get_course());
1520 $this->coursemodule = $modinfo->get_cm($this->context->instanceid);
1521 return $this->coursemodule;
1523 return null;
1527 * Get context module.
1529 * @return context
1531 public function get_context() {
1532 return $this->context;
1536 * Get the current course.
1538 * @return mixed stdClass|null The course
1540 public function get_course() {
1541 global $DB;
1543 if ($this->course) {
1544 return $this->course;
1547 if (!$this->context) {
1548 return null;
1550 $params = array('id' => $this->get_course_context()->instanceid);
1551 $this->course = $DB->get_record('course', $params, '*', MUST_EXIST);
1553 return $this->course;
1557 * Count the number of intro attachments.
1559 * @return int
1561 protected function count_attachments() {
1563 $fs = get_file_storage();
1564 $files = $fs->get_area_files($this->get_context()->id, 'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA,
1565 0, 'id', false);
1567 return count($files);
1571 * Are there any intro attachments to display?
1573 * @return boolean
1575 protected function has_visible_attachments() {
1576 return ($this->count_attachments() > 0);
1580 * Return a grade in user-friendly form, whether it's a scale or not.
1582 * @param mixed $grade int|null
1583 * @param boolean $editing Are we allowing changes to this grade?
1584 * @param int $userid The user id the grade belongs to
1585 * @param int $modified Timestamp from when the grade was last modified
1586 * @return string User-friendly representation of grade
1588 public function display_grade($grade, $editing, $userid=0, $modified=0) {
1589 global $DB;
1591 static $scalegrades = array();
1593 $o = '';
1595 if ($this->get_instance()->grade >= 0) {
1596 // Normal number.
1597 if ($editing && $this->get_instance()->grade > 0) {
1598 if ($grade < 0) {
1599 $displaygrade = '';
1600 } else {
1601 $displaygrade = format_float($grade, $this->get_grade_item()->get_decimals());
1603 $o .= '<label class="accesshide" for="quickgrade_' . $userid . '">' .
1604 get_string('usergrade', 'assign') .
1605 '</label>';
1606 $o .= '<input type="text"
1607 id="quickgrade_' . $userid . '"
1608 name="quickgrade_' . $userid . '"
1609 value="' . $displaygrade . '"
1610 size="6"
1611 maxlength="10"
1612 class="quickgrade"/>';
1613 $o .= '&nbsp;/&nbsp;' . format_float($this->get_instance()->grade, $this->get_grade_item()->get_decimals());
1614 return $o;
1615 } else {
1616 if ($grade == -1 || $grade === null) {
1617 $o .= '-';
1618 } else {
1619 $item = $this->get_grade_item();
1620 $o .= grade_format_gradevalue($grade, $item);
1621 if ($item->get_displaytype() == GRADE_DISPLAY_TYPE_REAL) {
1622 // If displaying the raw grade, also display the total value.
1623 $o .= '&nbsp;/&nbsp;' . format_float($this->get_instance()->grade, $item->get_decimals());
1626 return $o;
1629 } else {
1630 // Scale.
1631 if (empty($this->cache['scale'])) {
1632 if ($scale = $DB->get_record('scale', array('id'=>-($this->get_instance()->grade)))) {
1633 $this->cache['scale'] = make_menu_from_list($scale->scale);
1634 } else {
1635 $o .= '-';
1636 return $o;
1639 if ($editing) {
1640 $o .= '<label class="accesshide"
1641 for="quickgrade_' . $userid . '">' .
1642 get_string('usergrade', 'assign') .
1643 '</label>';
1644 $o .= '<select name="quickgrade_' . $userid . '" class="quickgrade">';
1645 $o .= '<option value="-1">' . get_string('nograde') . '</option>';
1646 foreach ($this->cache['scale'] as $optionid => $option) {
1647 $selected = '';
1648 if ($grade == $optionid) {
1649 $selected = 'selected="selected"';
1651 $o .= '<option value="' . $optionid . '" ' . $selected . '>' . $option . '</option>';
1653 $o .= '</select>';
1654 return $o;
1655 } else {
1656 $scaleid = (int)$grade;
1657 if (isset($this->cache['scale'][$scaleid])) {
1658 $o .= $this->cache['scale'][$scaleid];
1659 return $o;
1661 $o .= '-';
1662 return $o;
1668 * Get the submission status/grading status for all submissions in this assignment for the
1669 * given paticipants.
1671 * These statuses match the available filters (requiregrading, submitted, notsubmitted).
1672 * If this is a group assignment, group info is also returned.
1674 * @param array $participants an associative array where the key is the participant id and
1675 * the value is the participant record.
1676 * @return array an associative array where the key is the participant id and the value is
1677 * the participant record.
1679 private function get_submission_info_for_participants($participants) {
1680 global $DB;
1682 if (empty($participants)) {
1683 return $participants;
1686 list($insql, $params) = $DB->get_in_or_equal(array_keys($participants), SQL_PARAMS_NAMED);
1688 $assignid = $this->get_instance()->id;
1689 $params['assignmentid1'] = $assignid;
1690 $params['assignmentid2'] = $assignid;
1692 $fields = 'SELECT u.id, s.status, s.timemodified AS stime, g.timemodified AS gtime, g.grade';
1693 $from = ' FROM {user} u
1694 LEFT JOIN {assign_submission} s
1695 ON u.id = s.userid
1696 AND s.assignment = :assignmentid1
1697 AND s.latest = 1
1698 LEFT JOIN {assign_grades} g
1699 ON u.id = g.userid
1700 AND g.assignment = :assignmentid2
1701 AND g.attemptnumber = s.attemptnumber
1703 $where = ' WHERE u.id ' . $insql;
1705 if (!empty($this->get_instance()->blindmarking)) {
1706 $from .= 'LEFT JOIN {assign_user_mapping} um
1707 ON u.id = um.userid
1708 AND um.assignment = :assignmentid3 ';
1709 $params['assignmentid3'] = $assignid;
1710 $fields .= ', um.id as recordid ';
1713 $sql = "$fields $from $where";
1715 $records = $DB->get_records_sql($sql, $params);
1717 if ($this->get_instance()->teamsubmission) {
1718 // Get all groups.
1719 $allgroups = groups_get_all_groups($this->get_course()->id,
1720 array_keys($participants),
1721 $this->get_instance()->teamsubmissiongroupingid,
1722 'DISTINCT g.id, g.name');
1725 foreach ($participants as $userid => $participant) {
1726 $participants[$userid]->fullname = $this->fullname($participant);
1727 $participants[$userid]->submitted = false;
1728 $participants[$userid]->requiregrading = false;
1731 foreach ($records as $userid => $submissioninfo) {
1732 // These filters are 100% the same as the ones in the grading table SQL.
1733 $submitted = false;
1734 $requiregrading = false;
1736 if (!empty($submissioninfo->stime) && $submissioninfo->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
1737 $submitted = true;
1740 if ($submitted && ($submissioninfo->stime >= $submissioninfo->gtime ||
1741 empty($submissioninfo->gtime) ||
1742 $submissioninfo->grade === null)) {
1743 $requiregrading = true;
1746 $participants[$userid]->submitted = $submitted;
1747 $participants[$userid]->requiregrading = $requiregrading;
1748 if ($this->get_instance()->teamsubmission) {
1749 $group = $this->get_submission_group($userid);
1750 if ($group) {
1751 $participants[$userid]->groupid = $group->id;
1752 $participants[$userid]->groupname = $group->name;
1756 return $participants;
1760 * Get the submission status/grading status for all submissions in this assignment.
1761 * These statuses match the available filters (requiregrading, submitted, notsubmitted).
1762 * If this is a group assignment, group info is also returned.
1764 * @param int $currentgroup
1765 * @return array List of user records with extra fields 'submitted', 'notsubmitted', 'requiregrading', 'groupid', 'groupname'
1767 public function list_participants_with_filter_status_and_group($currentgroup) {
1768 $participants = $this->list_participants($currentgroup, false);
1770 if (empty($participants)) {
1771 return $participants;
1772 } else {
1773 return $this->get_submission_info_for_participants($participants);
1778 * Load a list of users enrolled in the current course with the specified permission and group.
1779 * 0 for no group.
1781 * @param int $currentgroup
1782 * @param bool $idsonly
1783 * @return array List of user records
1785 public function list_participants($currentgroup, $idsonly) {
1786 global $DB;
1788 if (empty($currentgroup)) {
1789 $currentgroup = 0;
1792 $key = $this->context->id . '-' . $currentgroup . '-' . $this->show_only_active_users();
1793 if (!isset($this->participants[$key])) {
1794 list($esql, $params) = get_enrolled_sql($this->context, 'mod/assign:submit', $currentgroup,
1795 $this->show_only_active_users());
1797 $fields = 'u.*';
1798 $orderby = 'u.lastname, u.firstname, u.id';
1799 $additionaljoins = '';
1800 $instance = $this->get_instance();
1801 if (!empty($instance->blindmarking)) {
1802 $additionaljoins .= " LEFT JOIN {assign_user_mapping} um
1803 ON u.id = um.userid
1804 AND um.assignment = :assignmentid1
1805 LEFT JOIN {assign_submission} s
1806 ON u.id = s.userid
1807 AND s.assignment = :assignmentid2
1808 AND s.latest = 1
1810 $params['assignmentid1'] = (int) $instance->id;
1811 $params['assignmentid2'] = (int) $instance->id;
1812 $fields .= ', um.id as recordid ';
1814 // Sort by submission time first, then by um.id to sort reliably by the blind marking id.
1815 // Note, different DBs have different ordering of NULL values.
1816 // Therefore we coalesce the current time into the timecreated field, and the max possible integer into
1817 // the ID field.
1818 $orderby = "COALESCE(s.timecreated, " . time() . ") ASC, COALESCE(s.id, " . PHP_INT_MAX . ") ASC, um.id ASC";
1821 $sql = "SELECT $fields
1822 FROM {user} u
1823 JOIN ($esql) je ON je.id = u.id
1824 $additionaljoins
1825 WHERE u.deleted = 0
1826 ORDER BY $orderby";
1828 $users = $DB->get_records_sql($sql, $params);
1830 $cm = $this->get_course_module();
1831 $info = new \core_availability\info_module($cm);
1832 $users = $info->filter_user_list($users);
1834 $this->participants[$key] = $users;
1837 if ($idsonly) {
1838 $idslist = array();
1839 foreach ($this->participants[$key] as $id => $user) {
1840 $idslist[$id] = new stdClass();
1841 $idslist[$id]->id = $id;
1843 return $idslist;
1845 return $this->participants[$key];
1849 * Load a user if they are enrolled in the current course. Populated with submission
1850 * status for this assignment.
1852 * @param int $userid
1853 * @return null|stdClass user record
1855 public function get_participant($userid) {
1856 global $DB;
1858 $participant = $DB->get_record('user', array('id' => $userid));
1859 if (!$participant) {
1860 return null;
1863 if (!is_enrolled($this->context, $participant, 'mod/assign:submit', $this->show_only_active_users())) {
1864 return null;
1867 $result = $this->get_submission_info_for_participants(array($participant->id => $participant));
1868 return $result[$participant->id];
1872 * Load a count of valid teams for this assignment.
1874 * @param int $activitygroup Activity active group
1875 * @return int number of valid teams
1877 public function count_teams($activitygroup = 0) {
1879 $count = 0;
1881 $participants = $this->list_participants($activitygroup, true);
1883 // If a team submission grouping id is provided all good as all returned groups
1884 // are the submission teams, but if no team submission grouping was specified
1885 // $groups will contain all participants groups.
1886 if ($this->get_instance()->teamsubmissiongroupingid) {
1888 // We restrict the users to the selected group ones.
1889 $groups = groups_get_all_groups($this->get_course()->id,
1890 array_keys($participants),
1891 $this->get_instance()->teamsubmissiongroupingid,
1892 'DISTINCT g.id, g.name');
1894 $count = count($groups);
1896 // When a specific group is selected we don't count the default group users.
1897 if ($activitygroup == 0) {
1898 if (empty($this->get_instance()->preventsubmissionnotingroup)) {
1899 // See if there are any users in the default group.
1900 $defaultusers = $this->get_submission_group_members(0, true);
1901 if (count($defaultusers) > 0) {
1902 $count += 1;
1905 } else if ($activitygroup != 0 && empty($groups)) {
1906 // Set count to 1 if $groups returns empty.
1907 // It means the group is not part of $this->get_instance()->teamsubmissiongroupingid.
1908 $count = 1;
1910 } else {
1911 // It is faster to loop around participants if no grouping was specified.
1912 $groups = array();
1913 foreach ($participants as $participant) {
1914 if ($group = $this->get_submission_group($participant->id)) {
1915 $groups[$group->id] = true;
1916 } else if (empty($this->get_instance()->preventsubmissionnotingroup)) {
1917 $groups[0] = true;
1921 $count = count($groups);
1924 return $count;
1928 * Load a count of active users enrolled in the current course with the specified permission and group.
1929 * 0 for no group.
1931 * @param int $currentgroup
1932 * @return int number of matching users
1934 public function count_participants($currentgroup) {
1935 return count($this->list_participants($currentgroup, true));
1939 * Load a count of active users submissions in the current module that require grading
1940 * This means the submission modification time is more recent than the
1941 * grading modification time and the status is SUBMITTED.
1943 * @return int number of matching submissions
1945 public function count_submissions_need_grading() {
1946 global $DB;
1948 if ($this->get_instance()->teamsubmission) {
1949 // This does not make sense for group assignment because the submission is shared.
1950 return 0;
1953 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
1954 list($esql, $params) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
1956 $params['assignid'] = $this->get_instance()->id;
1957 $params['submitted'] = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
1959 $sql = 'SELECT COUNT(s.userid)
1960 FROM {assign_submission} s
1961 LEFT JOIN {assign_grades} g ON
1962 s.assignment = g.assignment AND
1963 s.userid = g.userid AND
1964 g.attemptnumber = s.attemptnumber
1965 JOIN(' . $esql . ') e ON e.id = s.userid
1966 WHERE
1967 s.latest = 1 AND
1968 s.assignment = :assignid AND
1969 s.timemodified IS NOT NULL AND
1970 s.status = :submitted AND
1971 (s.timemodified >= g.timemodified OR g.timemodified IS NULL OR g.grade IS NULL)';
1973 return $DB->count_records_sql($sql, $params);
1977 * Load a count of grades.
1979 * @return int number of grades
1981 public function count_grades() {
1982 global $DB;
1984 if (!$this->has_instance()) {
1985 return 0;
1988 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
1989 list($esql, $params) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
1991 $params['assignid'] = $this->get_instance()->id;
1993 $sql = 'SELECT COUNT(g.userid)
1994 FROM {assign_grades} g
1995 JOIN(' . $esql . ') e ON e.id = g.userid
1996 WHERE g.assignment = :assignid';
1998 return $DB->count_records_sql($sql, $params);
2002 * Load a count of submissions.
2004 * @param bool $includenew When true, also counts the submissions with status 'new'.
2005 * @return int number of submissions
2007 public function count_submissions($includenew = false) {
2008 global $DB;
2010 if (!$this->has_instance()) {
2011 return 0;
2014 $params = array();
2015 $sqlnew = '';
2017 if (!$includenew) {
2018 $sqlnew = ' AND s.status <> :status ';
2019 $params['status'] = ASSIGN_SUBMISSION_STATUS_NEW;
2022 if ($this->get_instance()->teamsubmission) {
2023 // We cannot join on the enrolment tables for group submissions (no userid).
2024 $sql = 'SELECT COUNT(DISTINCT s.groupid)
2025 FROM {assign_submission} s
2026 WHERE
2027 s.assignment = :assignid AND
2028 s.timemodified IS NOT NULL AND
2029 s.userid = :groupuserid' .
2030 $sqlnew;
2032 $params['assignid'] = $this->get_instance()->id;
2033 $params['groupuserid'] = 0;
2034 } else {
2035 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
2036 list($esql, $enrolparams) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
2038 $params = array_merge($params, $enrolparams);
2039 $params['assignid'] = $this->get_instance()->id;
2041 $sql = 'SELECT COUNT(DISTINCT s.userid)
2042 FROM {assign_submission} s
2043 JOIN(' . $esql . ') e ON e.id = s.userid
2044 WHERE
2045 s.assignment = :assignid AND
2046 s.timemodified IS NOT NULL ' .
2047 $sqlnew;
2051 return $DB->count_records_sql($sql, $params);
2055 * Load a count of submissions with a specified status.
2057 * @param string $status The submission status - should match one of the constants
2058 * @return int number of matching submissions
2060 public function count_submissions_with_status($status) {
2061 global $DB;
2063 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
2064 list($esql, $params) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
2066 $params['assignid'] = $this->get_instance()->id;
2067 $params['assignid2'] = $this->get_instance()->id;
2068 $params['submissionstatus'] = $status;
2070 if ($this->get_instance()->teamsubmission) {
2072 $groupsstr = '';
2073 if ($currentgroup != 0) {
2074 // If there is an active group we should only display the current group users groups.
2075 $participants = $this->list_participants($currentgroup, true);
2076 $groups = groups_get_all_groups($this->get_course()->id,
2077 array_keys($participants),
2078 $this->get_instance()->teamsubmissiongroupingid,
2079 'DISTINCT g.id, g.name');
2080 if (empty($groups)) {
2081 // If $groups is empty it means it is not part of $this->get_instance()->teamsubmissiongroupingid.
2082 // All submissions from students that do not belong to any of teamsubmissiongroupingid groups
2083 // count towards groupid = 0. Setting to true as only '0' key matters.
2084 $groups = [true];
2086 list($groupssql, $groupsparams) = $DB->get_in_or_equal(array_keys($groups), SQL_PARAMS_NAMED);
2087 $groupsstr = 's.groupid ' . $groupssql . ' AND';
2088 $params = $params + $groupsparams;
2090 $sql = 'SELECT COUNT(s.groupid)
2091 FROM {assign_submission} s
2092 WHERE
2093 s.latest = 1 AND
2094 s.assignment = :assignid AND
2095 s.timemodified IS NOT NULL AND
2096 s.userid = :groupuserid AND '
2097 . $groupsstr . '
2098 s.status = :submissionstatus';
2099 $params['groupuserid'] = 0;
2100 } else {
2101 $sql = 'SELECT COUNT(s.userid)
2102 FROM {assign_submission} s
2103 JOIN(' . $esql . ') e ON e.id = s.userid
2104 WHERE
2105 s.latest = 1 AND
2106 s.assignment = :assignid AND
2107 s.timemodified IS NOT NULL AND
2108 s.status = :submissionstatus';
2112 return $DB->count_records_sql($sql, $params);
2116 * Utility function to get the userid for every row in the grading table
2117 * so the order can be frozen while we iterate it.
2119 * @return array An array of userids
2121 protected function get_grading_userid_list() {
2122 $filter = get_user_preferences('assign_filter', '');
2123 $table = new assign_grading_table($this, 0, $filter, 0, false);
2125 $useridlist = $table->get_column_data('userid');
2127 return $useridlist;
2131 * Generate zip file from array of given files.
2133 * @param array $filesforzipping - array of files to pass into archive_to_pathname.
2134 * This array is indexed by the final file name and each
2135 * element in the array is an instance of a stored_file object.
2136 * @return path of temp file - note this returned file does
2137 * not have a .zip extension - it is a temp file.
2139 protected function pack_files($filesforzipping) {
2140 global $CFG;
2141 // Create path for new zip file.
2142 $tempzip = tempnam($CFG->tempdir . '/', 'assignment_');
2143 // Zip files.
2144 $zipper = new zip_packer();
2145 if ($zipper->archive_to_pathname($filesforzipping, $tempzip)) {
2146 return $tempzip;
2148 return false;
2152 * Finds all assignment notifications that have yet to be mailed out, and mails them.
2154 * Cron function to be run periodically according to the moodle cron.
2156 * @return bool
2158 public static function cron() {
2159 global $DB;
2161 // Only ever send a max of one days worth of updates.
2162 $yesterday = time() - (24 * 3600);
2163 $timenow = time();
2164 $lastcron = $DB->get_field('modules', 'lastcron', array('name' => 'assign'));
2166 // Collect all submissions that require mailing.
2167 // Submissions are included if all are true:
2168 // - The assignment is visible in the gradebook.
2169 // - No previous notification has been sent.
2170 // - If marking workflow is not enabled, the grade was updated in the past 24 hours, or
2171 // if marking workflow is enabled, the workflow state is at 'released'.
2172 $sql = "SELECT g.id as gradeid, a.course, a.name, a.blindmarking, a.revealidentities,
2173 g.*, g.timemodified as lastmodified, cm.id as cmid, um.id as recordid
2174 FROM {assign} a
2175 JOIN {assign_grades} g ON g.assignment = a.id
2176 LEFT JOIN {assign_user_flags} uf ON uf.assignment = a.id AND uf.userid = g.userid
2177 JOIN {course_modules} cm ON cm.course = a.course AND cm.instance = a.id
2178 JOIN {modules} md ON md.id = cm.module AND md.name = 'assign'
2179 JOIN {grade_items} gri ON gri.iteminstance = a.id AND gri.courseid = a.course AND gri.itemmodule = md.name
2180 LEFT JOIN {assign_user_mapping} um ON g.id = um.userid AND um.assignment = a.id
2181 WHERE ((a.markingworkflow = 0 AND g.timemodified >= :yesterday AND g.timemodified <= :today) OR
2182 (a.markingworkflow = 1 AND uf.workflowstate = :wfreleased)) AND
2183 uf.mailed = 0 AND gri.hidden = 0
2184 ORDER BY a.course, cm.id";
2186 $params = array(
2187 'yesterday' => $yesterday,
2188 'today' => $timenow,
2189 'wfreleased' => ASSIGN_MARKING_WORKFLOW_STATE_RELEASED,
2191 $submissions = $DB->get_records_sql($sql, $params);
2193 if (!empty($submissions)) {
2195 mtrace('Processing ' . count($submissions) . ' assignment submissions ...');
2197 // Preload courses we are going to need those.
2198 $courseids = array();
2199 foreach ($submissions as $submission) {
2200 $courseids[] = $submission->course;
2203 // Filter out duplicates.
2204 $courseids = array_unique($courseids);
2205 $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
2206 list($courseidsql, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
2207 $sql = 'SELECT c.*, ' . $ctxselect .
2208 ' FROM {course} c
2209 LEFT JOIN {context} ctx ON ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel
2210 WHERE c.id ' . $courseidsql;
2212 $params['contextlevel'] = CONTEXT_COURSE;
2213 $courses = $DB->get_records_sql($sql, $params);
2215 // Clean up... this could go on for a while.
2216 unset($courseids);
2217 unset($ctxselect);
2218 unset($courseidsql);
2219 unset($params);
2221 // Message students about new feedback.
2222 foreach ($submissions as $submission) {
2224 mtrace("Processing assignment submission $submission->id ...");
2226 // Do not cache user lookups - could be too many.
2227 if (!$user = $DB->get_record('user', array('id'=>$submission->userid))) {
2228 mtrace('Could not find user ' . $submission->userid);
2229 continue;
2232 // Use a cache to prevent the same DB queries happening over and over.
2233 if (!array_key_exists($submission->course, $courses)) {
2234 mtrace('Could not find course ' . $submission->course);
2235 continue;
2237 $course = $courses[$submission->course];
2238 if (isset($course->ctxid)) {
2239 // Context has not yet been preloaded. Do so now.
2240 context_helper::preload_from_record($course);
2243 // Override the language and timezone of the "current" user, so that
2244 // mail is customised for the receiver.
2245 cron_setup_user($user, $course);
2247 // Context lookups are already cached.
2248 $coursecontext = context_course::instance($course->id);
2249 if (!is_enrolled($coursecontext, $user->id)) {
2250 $courseshortname = format_string($course->shortname,
2251 true,
2252 array('context' => $coursecontext));
2253 mtrace(fullname($user) . ' not an active participant in ' . $courseshortname);
2254 continue;
2257 if (!$grader = $DB->get_record('user', array('id'=>$submission->grader))) {
2258 mtrace('Could not find grader ' . $submission->grader);
2259 continue;
2262 $modinfo = get_fast_modinfo($course, $user->id);
2263 $cm = $modinfo->get_cm($submission->cmid);
2264 // Context lookups are already cached.
2265 $contextmodule = context_module::instance($cm->id);
2267 if (!$cm->uservisible) {
2268 // Hold mail notification for assignments the user cannot access until later.
2269 continue;
2272 // Need to send this to the student.
2273 $messagetype = 'feedbackavailable';
2274 $eventtype = 'assign_notification';
2275 $updatetime = $submission->lastmodified;
2276 $modulename = get_string('modulename', 'assign');
2278 $uniqueid = 0;
2279 if ($submission->blindmarking && !$submission->revealidentities) {
2280 if (empty($submission->recordid)) {
2281 $uniqueid = self::get_uniqueid_for_user_static($submission->assignment, $user->id);
2282 } else {
2283 $uniqueid = $submission->recordid;
2286 $showusers = $submission->blindmarking && !$submission->revealidentities;
2287 self::send_assignment_notification($grader,
2288 $user,
2289 $messagetype,
2290 $eventtype,
2291 $updatetime,
2292 $cm,
2293 $contextmodule,
2294 $course,
2295 $modulename,
2296 $submission->name,
2297 $showusers,
2298 $uniqueid);
2300 $flags = $DB->get_record('assign_user_flags', array('userid'=>$user->id, 'assignment'=>$submission->assignment));
2301 if ($flags) {
2302 $flags->mailed = 1;
2303 $DB->update_record('assign_user_flags', $flags);
2304 } else {
2305 $flags = new stdClass();
2306 $flags->userid = $user->id;
2307 $flags->assignment = $submission->assignment;
2308 $flags->mailed = 1;
2309 $DB->insert_record('assign_user_flags', $flags);
2312 mtrace('Done');
2314 mtrace('Done processing ' . count($submissions) . ' assignment submissions');
2316 cron_setup_user();
2318 // Free up memory just to be sure.
2319 unset($courses);
2322 // Update calendar events to provide a description.
2323 $sql = 'SELECT id
2324 FROM {assign}
2325 WHERE
2326 allowsubmissionsfromdate >= :lastcron AND
2327 allowsubmissionsfromdate <= :timenow AND
2328 alwaysshowdescription = 0';
2329 $params = array('lastcron' => $lastcron, 'timenow' => $timenow);
2330 $newlyavailable = $DB->get_records_sql($sql, $params);
2331 foreach ($newlyavailable as $record) {
2332 $cm = get_coursemodule_from_instance('assign', $record->id, 0, false, MUST_EXIST);
2333 $context = context_module::instance($cm->id);
2335 $assignment = new assign($context, null, null);
2336 $assignment->update_calendar($cm->id);
2339 return true;
2343 * Mark in the database that this grade record should have an update notification sent by cron.
2345 * @param stdClass $grade a grade record keyed on id
2346 * @param bool $mailedoverride when true, flag notification to be sent again.
2347 * @return bool true for success
2349 public function notify_grade_modified($grade, $mailedoverride = false) {
2350 global $DB;
2352 $flags = $this->get_user_flags($grade->userid, true);
2353 if ($flags->mailed != 1 || $mailedoverride) {
2354 $flags->mailed = 0;
2357 return $this->update_user_flags($flags);
2361 * Update user flags for this user in this assignment.
2363 * @param stdClass $flags a flags record keyed on id
2364 * @return bool true for success
2366 public function update_user_flags($flags) {
2367 global $DB;
2368 if ($flags->userid <= 0 || $flags->assignment <= 0 || $flags->id <= 0) {
2369 return false;
2372 $result = $DB->update_record('assign_user_flags', $flags);
2373 return $result;
2377 * Update a grade in the grade table for the assignment and in the gradebook.
2379 * @param stdClass $grade a grade record keyed on id
2380 * @param bool $reopenattempt If the attempt reopen method is manual, allow another attempt at this assignment.
2381 * @return bool true for success
2383 public function update_grade($grade, $reopenattempt = false) {
2384 global $DB;
2386 $grade->timemodified = time();
2388 if (!empty($grade->workflowstate)) {
2389 $validstates = $this->get_marking_workflow_states_for_current_user();
2390 if (!array_key_exists($grade->workflowstate, $validstates)) {
2391 return false;
2395 if ($grade->grade && $grade->grade != -1) {
2396 if ($this->get_instance()->grade > 0) {
2397 if (!is_numeric($grade->grade)) {
2398 return false;
2399 } else if ($grade->grade > $this->get_instance()->grade) {
2400 return false;
2401 } else if ($grade->grade < 0) {
2402 return false;
2404 } else {
2405 // This is a scale.
2406 if ($scale = $DB->get_record('scale', array('id' => -($this->get_instance()->grade)))) {
2407 $scaleoptions = make_menu_from_list($scale->scale);
2408 if (!array_key_exists((int) $grade->grade, $scaleoptions)) {
2409 return false;
2415 if (empty($grade->attemptnumber)) {
2416 // Set it to the default.
2417 $grade->attemptnumber = 0;
2419 $DB->update_record('assign_grades', $grade);
2421 $submission = null;
2422 if ($this->get_instance()->teamsubmission) {
2423 $submission = $this->get_group_submission($grade->userid, 0, false);
2424 } else {
2425 $submission = $this->get_user_submission($grade->userid, false);
2428 // Only push to gradebook if the update is for the latest attempt.
2429 // Not the latest attempt.
2430 if ($submission && $submission->attemptnumber != $grade->attemptnumber) {
2431 return true;
2434 if ($this->gradebook_item_update(null, $grade)) {
2435 \mod_assign\event\submission_graded::create_from_grade($this, $grade)->trigger();
2438 // If the conditions are met, allow another attempt.
2439 if ($submission) {
2440 $this->reopen_submission_if_required($grade->userid,
2441 $submission,
2442 $reopenattempt);
2445 return true;
2449 * View the grant extension date page.
2451 * Uses url parameters 'userid'
2452 * or from parameter 'selectedusers'
2454 * @param moodleform $mform - Used for validation of the submitted data
2455 * @return string
2457 protected function view_grant_extension($mform) {
2458 global $CFG;
2459 require_once($CFG->dirroot . '/mod/assign/extensionform.php');
2461 $o = '';
2463 $data = new stdClass();
2464 $data->id = $this->get_course_module()->id;
2466 $formparams = array(
2467 'instance' => $this->get_instance(),
2468 'assign' => $this
2471 $users = optional_param('userid', 0, PARAM_INT);
2472 if (!$users) {
2473 $users = required_param('selectedusers', PARAM_SEQUENCE);
2475 $userlist = explode(',', $users);
2477 $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate');
2478 $maxoverride = array('allowsubmissionsfromdate' => 0, 'duedate' => 0, 'cutoffdate' => 0);
2479 foreach ($userlist as $userid) {
2480 // To validate extension date with users overrides.
2481 $override = $this->override_exists($userid);
2482 foreach ($keys as $key) {
2483 if ($override->{$key}) {
2484 if ($maxoverride[$key] < $override->{$key}) {
2485 $maxoverride[$key] = $override->{$key};
2487 } else if ($maxoverride[$key] < $this->get_instance()->{$key}) {
2488 $maxoverride[$key] = $this->get_instance()->{$key};
2492 foreach ($keys as $key) {
2493 if ($maxoverride[$key]) {
2494 $this->get_instance()->{$key} = $maxoverride[$key];
2498 $formparams['userlist'] = $userlist;
2500 $data->selectedusers = $users;
2501 $data->userid = 0;
2503 if (empty($mform)) {
2504 $mform = new mod_assign_extension_form(null, $formparams);
2506 $mform->set_data($data);
2507 $header = new assign_header($this->get_instance(),
2508 $this->get_context(),
2509 $this->show_intro(),
2510 $this->get_course_module()->id,
2511 get_string('grantextension', 'assign'));
2512 $o .= $this->get_renderer()->render($header);
2513 $o .= $this->get_renderer()->render(new assign_form('extensionform', $mform));
2514 $o .= $this->view_footer();
2515 return $o;
2519 * Get a list of the users in the same group as this user.
2521 * @param int $groupid The id of the group whose members we want or 0 for the default group
2522 * @param bool $onlyids Whether to retrieve only the user id's
2523 * @param bool $excludesuspended Whether to exclude suspended users
2524 * @return array The users (possibly id's only)
2526 public function get_submission_group_members($groupid, $onlyids, $excludesuspended = false) {
2527 $members = array();
2528 if ($groupid != 0) {
2529 $allusers = $this->list_participants($groupid, $onlyids);
2530 foreach ($allusers as $user) {
2531 if ($this->get_submission_group($user->id)) {
2532 $members[] = $user;
2535 } else {
2536 $allusers = $this->list_participants(null, $onlyids);
2537 foreach ($allusers as $user) {
2538 if ($this->get_submission_group($user->id) == null) {
2539 $members[] = $user;
2543 // Exclude suspended users, if user can't see them.
2544 if ($excludesuspended || !has_capability('moodle/course:viewsuspendedusers', $this->context)) {
2545 foreach ($members as $key => $member) {
2546 if (!$this->is_active_user($member->id)) {
2547 unset($members[$key]);
2552 return $members;
2556 * Get a list of the users in the same group as this user that have not submitted the assignment.
2558 * @param int $groupid The id of the group whose members we want or 0 for the default group
2559 * @param bool $onlyids Whether to retrieve only the user id's
2560 * @return array The users (possibly id's only)
2562 public function get_submission_group_members_who_have_not_submitted($groupid, $onlyids) {
2563 $instance = $this->get_instance();
2564 if (!$instance->teamsubmission || !$instance->requireallteammemberssubmit) {
2565 return array();
2567 $members = $this->get_submission_group_members($groupid, $onlyids);
2569 foreach ($members as $id => $member) {
2570 $submission = $this->get_user_submission($member->id, false);
2571 if ($submission && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
2572 unset($members[$id]);
2573 } else {
2574 if ($this->is_blind_marking()) {
2575 $members[$id]->alias = get_string('hiddenuser', 'assign') .
2576 $this->get_uniqueid_for_user($id);
2580 return $members;
2584 * Load the group submission object for a particular user, optionally creating it if required.
2586 * @param int $userid The id of the user whose submission we want
2587 * @param int $groupid The id of the group for this user - may be 0 in which
2588 * case it is determined from the userid.
2589 * @param bool $create If set to true a new submission object will be created in the database
2590 * with the status set to "new".
2591 * @param int $attemptnumber - -1 means the latest attempt
2592 * @return stdClass The submission
2594 public function get_group_submission($userid, $groupid, $create, $attemptnumber=-1) {
2595 global $DB;
2597 if ($groupid == 0) {
2598 $group = $this->get_submission_group($userid);
2599 if ($group) {
2600 $groupid = $group->id;
2604 // Now get the group submission.
2605 $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
2606 if ($attemptnumber >= 0) {
2607 $params['attemptnumber'] = $attemptnumber;
2610 // Only return the row with the highest attemptnumber.
2611 $submission = null;
2612 $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', '*', 0, 1);
2613 if ($submissions) {
2614 $submission = reset($submissions);
2617 if ($submission) {
2618 return $submission;
2620 if ($create) {
2621 $submission = new stdClass();
2622 $submission->assignment = $this->get_instance()->id;
2623 $submission->userid = 0;
2624 $submission->groupid = $groupid;
2625 $submission->timecreated = time();
2626 $submission->timemodified = $submission->timecreated;
2627 if ($attemptnumber >= 0) {
2628 $submission->attemptnumber = $attemptnumber;
2629 } else {
2630 $submission->attemptnumber = 0;
2632 // Work out if this is the latest submission.
2633 $submission->latest = 0;
2634 $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
2635 if ($attemptnumber == -1) {
2636 // This is a new submission so it must be the latest.
2637 $submission->latest = 1;
2638 } else {
2639 // We need to work this out.
2640 $result = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', 'attemptnumber', 0, 1);
2641 if ($result) {
2642 $latestsubmission = reset($result);
2644 if (!$latestsubmission || ($attemptnumber == $latestsubmission->attemptnumber)) {
2645 $submission->latest = 1;
2648 if ($submission->latest) {
2649 // This is the case when we need to set latest to 0 for all the other attempts.
2650 $DB->set_field('assign_submission', 'latest', 0, $params);
2652 $submission->status = ASSIGN_SUBMISSION_STATUS_NEW;
2653 $sid = $DB->insert_record('assign_submission', $submission);
2654 return $DB->get_record('assign_submission', array('id' => $sid));
2656 return false;
2660 * View a summary listing of all assignments in the current course.
2662 * @return string
2664 private function view_course_index() {
2665 global $USER;
2667 $o = '';
2669 $course = $this->get_course();
2670 $strplural = get_string('modulenameplural', 'assign');
2672 if (!$cms = get_coursemodules_in_course('assign', $course->id, 'm.duedate')) {
2673 $o .= $this->get_renderer()->notification(get_string('thereareno', 'moodle', $strplural));
2674 $o .= $this->get_renderer()->continue_button(new moodle_url('/course/view.php', array('id' => $course->id)));
2675 return $o;
2678 $strsectionname = '';
2679 $usesections = course_format_uses_sections($course->format);
2680 $modinfo = get_fast_modinfo($course);
2682 if ($usesections) {
2683 $strsectionname = get_string('sectionname', 'format_'.$course->format);
2684 $sections = $modinfo->get_section_info_all();
2686 $courseindexsummary = new assign_course_index_summary($usesections, $strsectionname);
2688 $timenow = time();
2690 $currentsection = '';
2691 foreach ($modinfo->instances['assign'] as $cm) {
2692 if (!$cm->uservisible) {
2693 continue;
2696 $timedue = $cms[$cm->id]->duedate;
2698 $sectionname = '';
2699 if ($usesections && $cm->sectionnum) {
2700 $sectionname = get_section_name($course, $sections[$cm->sectionnum]);
2703 $submitted = '';
2704 $context = context_module::instance($cm->id);
2706 $assignment = new assign($context, $cm, $course);
2708 // Apply overrides.
2709 $assignment->update_effective_access($USER->id);
2710 $timedue = $assignment->get_instance()->duedate;
2712 if (has_capability('mod/assign:grade', $context)) {
2713 $submitted = $assignment->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED);
2715 } else if (has_capability('mod/assign:submit', $context)) {
2716 $usersubmission = $assignment->get_user_submission($USER->id, false);
2718 if (!empty($usersubmission->status)) {
2719 $submitted = get_string('submissionstatus_' . $usersubmission->status, 'assign');
2720 } else {
2721 $submitted = get_string('submissionstatus_', 'assign');
2724 $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $cm->instance, $USER->id);
2725 if (isset($gradinginfo->items[0]->grades[$USER->id]) &&
2726 !$gradinginfo->items[0]->grades[$USER->id]->hidden ) {
2727 $grade = $gradinginfo->items[0]->grades[$USER->id]->str_grade;
2728 } else {
2729 $grade = '-';
2732 $courseindexsummary->add_assign_info($cm->id, $cm->get_formatted_name(), $sectionname, $timedue, $submitted, $grade);
2736 $o .= $this->get_renderer()->render($courseindexsummary);
2737 $o .= $this->view_footer();
2739 return $o;
2743 * View a page rendered by a plugin.
2745 * Uses url parameters 'pluginaction', 'pluginsubtype', 'plugin', and 'id'.
2747 * @return string
2749 protected function view_plugin_page() {
2750 global $USER;
2752 $o = '';
2754 $pluginsubtype = required_param('pluginsubtype', PARAM_ALPHA);
2755 $plugintype = required_param('plugin', PARAM_TEXT);
2756 $pluginaction = required_param('pluginaction', PARAM_ALPHA);
2758 $plugin = $this->get_plugin_by_type($pluginsubtype, $plugintype);
2759 if (!$plugin) {
2760 print_error('invalidformdata', '');
2761 return;
2764 $o .= $plugin->view_page($pluginaction);
2766 return $o;
2771 * This is used for team assignments to get the group for the specified user.
2772 * If the user is a member of multiple or no groups this will return false
2774 * @param int $userid The id of the user whose submission we want
2775 * @return mixed The group or false
2777 public function get_submission_group($userid) {
2779 if (isset($this->usersubmissiongroups[$userid])) {
2780 return $this->usersubmissiongroups[$userid];
2783 $groups = $this->get_all_groups($userid);
2784 if (count($groups) != 1) {
2785 $return = false;
2786 } else {
2787 $return = array_pop($groups);
2790 // Cache the user submission group.
2791 $this->usersubmissiongroups[$userid] = $return;
2793 return $return;
2797 * Gets all groups the user is a member of.
2799 * @param int $userid Teh id of the user who's groups we are checking
2800 * @return array The group objects
2802 public function get_all_groups($userid) {
2803 if (isset($this->usergroups[$userid])) {
2804 return $this->usergroups[$userid];
2807 $grouping = $this->get_instance()->teamsubmissiongroupingid;
2808 $return = groups_get_all_groups($this->get_course()->id, $userid, $grouping);
2810 $this->usergroups[$userid] = $return;
2812 return $return;
2817 * Display the submission that is used by a plugin.
2819 * Uses url parameters 'sid', 'gid' and 'plugin'.
2821 * @param string $pluginsubtype
2822 * @return string
2824 protected function view_plugin_content($pluginsubtype) {
2825 $o = '';
2827 $submissionid = optional_param('sid', 0, PARAM_INT);
2828 $gradeid = optional_param('gid', 0, PARAM_INT);
2829 $plugintype = required_param('plugin', PARAM_TEXT);
2830 $item = null;
2831 if ($pluginsubtype == 'assignsubmission') {
2832 $plugin = $this->get_submission_plugin_by_type($plugintype);
2833 if ($submissionid <= 0) {
2834 throw new coding_exception('Submission id should not be 0');
2836 $item = $this->get_submission($submissionid);
2838 // Check permissions.
2839 $this->require_view_submission($item->userid);
2840 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
2841 $this->get_context(),
2842 $this->show_intro(),
2843 $this->get_course_module()->id,
2844 $plugin->get_name()));
2845 $o .= $this->get_renderer()->render(new assign_submission_plugin_submission($plugin,
2846 $item,
2847 assign_submission_plugin_submission::FULL,
2848 $this->get_course_module()->id,
2849 $this->get_return_action(),
2850 $this->get_return_params()));
2852 // Trigger event for viewing a submission.
2853 \mod_assign\event\submission_viewed::create_from_submission($this, $item)->trigger();
2855 } else {
2856 $plugin = $this->get_feedback_plugin_by_type($plugintype);
2857 if ($gradeid <= 0) {
2858 throw new coding_exception('Grade id should not be 0');
2860 $item = $this->get_grade($gradeid);
2861 // Check permissions.
2862 $this->require_view_submission($item->userid);
2863 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
2864 $this->get_context(),
2865 $this->show_intro(),
2866 $this->get_course_module()->id,
2867 $plugin->get_name()));
2868 $o .= $this->get_renderer()->render(new assign_feedback_plugin_feedback($plugin,
2869 $item,
2870 assign_feedback_plugin_feedback::FULL,
2871 $this->get_course_module()->id,
2872 $this->get_return_action(),
2873 $this->get_return_params()));
2875 // Trigger event for viewing feedback.
2876 \mod_assign\event\feedback_viewed::create_from_grade($this, $item)->trigger();
2879 $o .= $this->view_return_links();
2881 $o .= $this->view_footer();
2883 return $o;
2887 * Rewrite plugin file urls so they resolve correctly in an exported zip.
2889 * @param string $text - The replacement text
2890 * @param stdClass $user - The user record
2891 * @param assign_plugin $plugin - The assignment plugin
2893 public function download_rewrite_pluginfile_urls($text, $user, $plugin) {
2894 $groupmode = groups_get_activity_groupmode($this->get_course_module());
2895 $groupname = '';
2896 if ($groupmode) {
2897 $groupid = groups_get_activity_group($this->get_course_module(), true);
2898 $groupname = groups_get_group_name($groupid).'-';
2901 if ($this->is_blind_marking()) {
2902 $prefix = $groupname . get_string('participant', 'assign');
2903 $prefix = str_replace('_', ' ', $prefix);
2904 $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($user->id) . '_');
2905 } else {
2906 $prefix = $groupname . fullname($user);
2907 $prefix = str_replace('_', ' ', $prefix);
2908 $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($user->id) . '_');
2911 $subtype = $plugin->get_subtype();
2912 $type = $plugin->get_type();
2913 $prefix = $prefix . $subtype . '_' . $type . '_';
2915 $result = str_replace('@@PLUGINFILE@@/', $prefix, $text);
2917 return $result;
2921 * Render the content in editor that is often used by plugin.
2923 * @param string $filearea
2924 * @param int $submissionid
2925 * @param string $plugintype
2926 * @param string $editor
2927 * @param string $component
2928 * @return string
2930 public function render_editor_content($filearea, $submissionid, $plugintype, $editor, $component) {
2931 global $CFG;
2933 $result = '';
2935 $plugin = $this->get_submission_plugin_by_type($plugintype);
2937 $text = $plugin->get_editor_text($editor, $submissionid);
2938 $format = $plugin->get_editor_format($editor, $submissionid);
2940 $finaltext = file_rewrite_pluginfile_urls($text,
2941 'pluginfile.php',
2942 $this->get_context()->id,
2943 $component,
2944 $filearea,
2945 $submissionid);
2946 $params = array('overflowdiv' => true, 'context' => $this->get_context());
2947 $result .= format_text($finaltext, $format, $params);
2949 if ($CFG->enableportfolios && has_capability('mod/assign:exportownsubmission', $this->context)) {
2950 require_once($CFG->libdir . '/portfoliolib.php');
2952 $button = new portfolio_add_button();
2953 $portfolioparams = array('cmid' => $this->get_course_module()->id,
2954 'sid' => $submissionid,
2955 'plugin' => $plugintype,
2956 'editor' => $editor,
2957 'area'=>$filearea);
2958 $button->set_callback_options('assign_portfolio_caller', $portfolioparams, 'mod_assign');
2959 $fs = get_file_storage();
2961 if ($files = $fs->get_area_files($this->context->id,
2962 $component,
2963 $filearea,
2964 $submissionid,
2965 'timemodified',
2966 false)) {
2967 $button->set_formats(PORTFOLIO_FORMAT_RICHHTML);
2968 } else {
2969 $button->set_formats(PORTFOLIO_FORMAT_PLAINHTML);
2971 $result .= $button->to_html();
2973 return $result;
2977 * Display a continue page after grading.
2979 * @param string $message - The message to display.
2980 * @return string
2982 protected function view_savegrading_result($message) {
2983 $o = '';
2984 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
2985 $this->get_context(),
2986 $this->show_intro(),
2987 $this->get_course_module()->id,
2988 get_string('savegradingresult', 'assign')));
2989 $gradingresult = new assign_gradingmessage(get_string('savegradingresult', 'assign'),
2990 $message,
2991 $this->get_course_module()->id);
2992 $o .= $this->get_renderer()->render($gradingresult);
2993 $o .= $this->view_footer();
2994 return $o;
2997 * Display a continue page after quickgrading.
2999 * @param string $message - The message to display.
3000 * @return string
3002 protected function view_quickgrading_result($message) {
3003 $o = '';
3004 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
3005 $this->get_context(),
3006 $this->show_intro(),
3007 $this->get_course_module()->id,
3008 get_string('quickgradingresult', 'assign')));
3009 $lastpage = optional_param('lastpage', null, PARAM_INT);
3010 $gradingresult = new assign_gradingmessage(get_string('quickgradingresult', 'assign'),
3011 $message,
3012 $this->get_course_module()->id,
3013 false,
3014 $lastpage);
3015 $o .= $this->get_renderer()->render($gradingresult);
3016 $o .= $this->view_footer();
3017 return $o;
3021 * Display the page footer.
3023 * @return string
3025 protected function view_footer() {
3026 // When viewing the footer during PHPUNIT tests a set_state error is thrown.
3027 if (!PHPUNIT_TEST) {
3028 return $this->get_renderer()->render_footer();
3031 return '';
3035 * Throw an error if the permissions to view this users submission are missing.
3037 * @throws required_capability_exception
3038 * @return none
3040 public function require_view_submission($userid) {
3041 if (!$this->can_view_submission($userid)) {
3042 throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', '');
3047 * Throw an error if the permissions to view grades in this assignment are missing.
3049 * @throws required_capability_exception
3050 * @return none
3052 public function require_view_grades() {
3053 if (!$this->can_view_grades()) {
3054 throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', '');
3059 * Does this user have view grade or grade permission for this assignment?
3061 * @return bool
3063 public function can_view_grades() {
3064 // Permissions check.
3065 if (!has_any_capability(array('mod/assign:viewgrades', 'mod/assign:grade'), $this->context)) {
3066 return false;
3069 return true;
3073 * Does this user have grade permission for this assignment?
3075 * @return bool
3077 public function can_grade() {
3078 // Permissions check.
3079 if (!has_capability('mod/assign:grade', $this->context)) {
3080 return false;
3083 return true;
3087 * Download a zip file of all assignment submissions.
3089 * @param array $userids Array of user ids to download assignment submissions in a zip file
3090 * @return string - If an error occurs, this will contain the error page.
3092 protected function download_submissions($userids = false) {
3093 global $CFG, $DB;
3095 // More efficient to load this here.
3096 require_once($CFG->libdir.'/filelib.php');
3098 // Increase the server timeout to handle the creation and sending of large zip files.
3099 core_php_time_limit::raise();
3101 $this->require_view_grades();
3103 // Load all users with submit.
3104 $students = get_enrolled_users($this->context, "mod/assign:submit", null, 'u.*', null, null, null,
3105 $this->show_only_active_users());
3107 // Build a list of files to zip.
3108 $filesforzipping = array();
3109 $fs = get_file_storage();
3111 $groupmode = groups_get_activity_groupmode($this->get_course_module());
3112 // All users.
3113 $groupid = 0;
3114 $groupname = '';
3115 if ($groupmode) {
3116 $groupid = groups_get_activity_group($this->get_course_module(), true);
3117 $groupname = groups_get_group_name($groupid).'-';
3120 // Construct the zip file name.
3121 $filename = clean_filename($this->get_course()->shortname . '-' .
3122 $this->get_instance()->name . '-' .
3123 $groupname.$this->get_course_module()->id . '.zip');
3125 // Get all the files for each student.
3126 foreach ($students as $student) {
3127 $userid = $student->id;
3128 // Download all assigments submission or only selected users.
3129 if ($userids and !in_array($userid, $userids)) {
3130 continue;
3133 if ((groups_is_member($groupid, $userid) or !$groupmode or !$groupid)) {
3134 // Get the plugins to add their own files to the zip.
3136 $submissiongroup = false;
3137 $groupname = '';
3138 if ($this->get_instance()->teamsubmission) {
3139 $submission = $this->get_group_submission($userid, 0, false);
3140 $submissiongroup = $this->get_submission_group($userid);
3141 if ($submissiongroup) {
3142 $groupname = $submissiongroup->name . '-';
3143 } else {
3144 $groupname = get_string('defaultteam', 'assign') . '-';
3146 } else {
3147 $submission = $this->get_user_submission($userid, false);
3150 if ($this->is_blind_marking()) {
3151 $prefix = str_replace('_', ' ', $groupname . get_string('participant', 'assign'));
3152 $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($userid));
3153 } else {
3154 $prefix = str_replace('_', ' ', $groupname . fullname($student));
3155 $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($userid));
3158 if ($submission) {
3159 $downloadasfolders = get_user_preferences('assign_downloadasfolders', 1);
3160 foreach ($this->submissionplugins as $plugin) {
3161 if ($plugin->is_enabled() && $plugin->is_visible()) {
3162 if ($downloadasfolders) {
3163 // Create a folder for each user for each assignment plugin.
3164 // This is the default behavior for version of Moodle >= 3.1.
3165 $submission->exportfullpath = true;
3166 $pluginfiles = $plugin->get_files($submission, $student);
3167 foreach ($pluginfiles as $zipfilepath => $file) {
3168 $subtype = $plugin->get_subtype();
3169 $type = $plugin->get_type();
3170 $zipfilename = basename($zipfilepath);
3171 $prefixedfilename = clean_filename($prefix .
3172 '_' .
3173 $subtype .
3174 '_' .
3175 $type .
3176 '_');
3177 if ($type == 'file') {
3178 $pathfilename = $prefixedfilename . $file->get_filepath() . $zipfilename;
3179 } else if ($type == 'onlinetext') {
3180 $pathfilename = $prefixedfilename . '/' . $zipfilename;
3181 } else {
3182 $pathfilename = $prefixedfilename . '/' . $zipfilename;
3184 $pathfilename = clean_param($pathfilename, PARAM_PATH);
3185 $filesforzipping[$pathfilename] = $file;
3187 } else {
3188 // Create a single folder for all users of all assignment plugins.
3189 // This was the default behavior for version of Moodle < 3.1.
3190 $submission->exportfullpath = false;
3191 $pluginfiles = $plugin->get_files($submission, $student);
3192 foreach ($pluginfiles as $zipfilename => $file) {
3193 $subtype = $plugin->get_subtype();
3194 $type = $plugin->get_type();
3195 $prefixedfilename = clean_filename($prefix .
3196 '_' .
3197 $subtype .
3198 '_' .
3199 $type .
3200 '_' .
3201 $zipfilename);
3202 $filesforzipping[$prefixedfilename] = $file;
3210 $result = '';
3211 if (count($filesforzipping) == 0) {
3212 $header = new assign_header($this->get_instance(),
3213 $this->get_context(),
3215 $this->get_course_module()->id,
3216 get_string('downloadall', 'assign'));
3217 $result .= $this->get_renderer()->render($header);
3218 $result .= $this->get_renderer()->notification(get_string('nosubmission', 'assign'));
3219 $url = new moodle_url('/mod/assign/view.php', array('id'=>$this->get_course_module()->id,
3220 'action'=>'grading'));
3221 $result .= $this->get_renderer()->continue_button($url);
3222 $result .= $this->view_footer();
3223 } else if ($zipfile = $this->pack_files($filesforzipping)) {
3224 \mod_assign\event\all_submissions_downloaded::create_from_assign($this)->trigger();
3225 // Send file and delete after sending.
3226 send_temp_file($zipfile, $filename);
3227 // We will not get here - send_temp_file calls exit.
3229 return $result;
3233 * Util function to add a message to the log.
3235 * @deprecated since 2.7 - Use new events system instead.
3236 * (see http://docs.moodle.org/dev/Migrating_logging_calls_in_plugins).
3238 * @param string $action The current action
3239 * @param string $info A detailed description of the change. But no more than 255 characters.
3240 * @param string $url The url to the assign module instance.
3241 * @param bool $return If true, returns the arguments, else adds to log. The purpose of this is to
3242 * retrieve the arguments to use them with the new event system (Event 2).
3243 * @return void|array
3245 public function add_to_log($action = '', $info = '', $url='', $return = false) {
3246 global $USER;
3248 $fullurl = 'view.php?id=' . $this->get_course_module()->id;
3249 if ($url != '') {
3250 $fullurl .= '&' . $url;
3253 $args = array(
3254 $this->get_course()->id,
3255 'assign',
3256 $action,
3257 $fullurl,
3258 $info,
3259 $this->get_course_module()->id
3262 if ($return) {
3263 // We only need to call debugging when returning a value. This is because the call to
3264 // call_user_func_array('add_to_log', $args) will trigger a debugging message of it's own.
3265 debugging('The mod_assign add_to_log() function is now deprecated.', DEBUG_DEVELOPER);
3266 return $args;
3268 call_user_func_array('add_to_log', $args);
3272 * Lazy load the page renderer and expose the renderer to plugins.
3274 * @return assign_renderer
3276 public function get_renderer() {
3277 global $PAGE;
3278 if ($this->output) {
3279 return $this->output;
3281 $this->output = $PAGE->get_renderer('mod_assign', null, RENDERER_TARGET_GENERAL);
3282 return $this->output;
3286 * Load the submission object for a particular user, optionally creating it if required.
3288 * For team assignments there are 2 submissions - the student submission and the team submission
3289 * All files are associated with the team submission but the status of the students contribution is
3290 * recorded separately.
3292 * @param int $userid The id of the user whose submission we want or 0 in which case USER->id is used
3293 * @param bool $create If set to true a new submission object will be created in the database with the status set to "new".
3294 * @param int $attemptnumber - -1 means the latest attempt
3295 * @return stdClass The submission
3297 public function get_user_submission($userid, $create, $attemptnumber=-1) {
3298 global $DB, $USER;
3300 if (!$userid) {
3301 $userid = $USER->id;
3303 // If the userid is not null then use userid.
3304 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid, 'groupid'=>0);
3305 if ($attemptnumber >= 0) {
3306 $params['attemptnumber'] = $attemptnumber;
3309 // Only return the row with the highest attemptnumber.
3310 $submission = null;
3311 $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', '*', 0, 1);
3312 if ($submissions) {
3313 $submission = reset($submissions);
3316 if ($submission) {
3317 return $submission;
3319 if ($create) {
3320 $submission = new stdClass();
3321 $submission->assignment = $this->get_instance()->id;
3322 $submission->userid = $userid;
3323 $submission->timecreated = time();
3324 $submission->timemodified = $submission->timecreated;
3325 $submission->status = ASSIGN_SUBMISSION_STATUS_NEW;
3326 if ($attemptnumber >= 0) {
3327 $submission->attemptnumber = $attemptnumber;
3328 } else {
3329 $submission->attemptnumber = 0;
3331 // Work out if this is the latest submission.
3332 $submission->latest = 0;
3333 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid, 'groupid'=>0);
3334 if ($attemptnumber == -1) {
3335 // This is a new submission so it must be the latest.
3336 $submission->latest = 1;
3337 } else {
3338 // We need to work this out.
3339 $result = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', 'attemptnumber', 0, 1);
3340 $latestsubmission = null;
3341 if ($result) {
3342 $latestsubmission = reset($result);
3344 if (empty($latestsubmission) || ($attemptnumber > $latestsubmission->attemptnumber)) {
3345 $submission->latest = 1;
3348 if ($submission->latest) {
3349 // This is the case when we need to set latest to 0 for all the other attempts.
3350 $DB->set_field('assign_submission', 'latest', 0, $params);
3352 $sid = $DB->insert_record('assign_submission', $submission);
3353 return $DB->get_record('assign_submission', array('id' => $sid));
3355 return false;
3359 * Load the submission object from it's id.
3361 * @param int $submissionid The id of the submission we want
3362 * @return stdClass The submission
3364 protected function get_submission($submissionid) {
3365 global $DB;
3367 $params = array('assignment'=>$this->get_instance()->id, 'id'=>$submissionid);
3368 return $DB->get_record('assign_submission', $params, '*', MUST_EXIST);
3372 * This will retrieve a user flags object from the db optionally creating it if required.
3373 * The user flags was split from the user_grades table in 2.5.
3375 * @param int $userid The user we are getting the flags for.
3376 * @param bool $create If true the flags record will be created if it does not exist
3377 * @return stdClass The flags record
3379 public function get_user_flags($userid, $create) {
3380 global $DB, $USER;
3382 // If the userid is not null then use userid.
3383 if (!$userid) {
3384 $userid = $USER->id;
3387 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
3389 $flags = $DB->get_record('assign_user_flags', $params);
3391 if ($flags) {
3392 return $flags;
3394 if ($create) {
3395 $flags = new stdClass();
3396 $flags->assignment = $this->get_instance()->id;
3397 $flags->userid = $userid;
3398 $flags->locked = 0;
3399 $flags->extensionduedate = 0;
3400 $flags->workflowstate = '';
3401 $flags->allocatedmarker = 0;
3403 // The mailed flag can be one of 3 values: 0 is unsent, 1 is sent and 2 is do not send yet.
3404 // This is because students only want to be notified about certain types of update (grades and feedback).
3405 $flags->mailed = 2;
3407 $fid = $DB->insert_record('assign_user_flags', $flags);
3408 $flags->id = $fid;
3409 return $flags;
3411 return false;
3415 * This will retrieve a grade object from the db, optionally creating it if required.
3417 * @param int $userid The user we are grading
3418 * @param bool $create If true the grade will be created if it does not exist
3419 * @param int $attemptnumber The attempt number to retrieve the grade for. -1 means the latest submission.
3420 * @return stdClass The grade record
3422 public function get_user_grade($userid, $create, $attemptnumber=-1) {
3423 global $DB, $USER;
3425 // If the userid is not null then use userid.
3426 if (!$userid) {
3427 $userid = $USER->id;
3429 $submission = null;
3431 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
3432 if ($attemptnumber < 0 || $create) {
3433 // Make sure this grade matches the latest submission attempt.
3434 if ($this->get_instance()->teamsubmission) {
3435 $submission = $this->get_group_submission($userid, 0, true, $attemptnumber);
3436 } else {
3437 $submission = $this->get_user_submission($userid, true, $attemptnumber);
3439 if ($submission) {
3440 $attemptnumber = $submission->attemptnumber;
3444 if ($attemptnumber >= 0) {
3445 $params['attemptnumber'] = $attemptnumber;
3448 $grades = $DB->get_records('assign_grades', $params, 'attemptnumber DESC', '*', 0, 1);
3450 if ($grades) {
3451 return reset($grades);
3453 if ($create) {
3454 $grade = new stdClass();
3455 $grade->assignment = $this->get_instance()->id;
3456 $grade->userid = $userid;
3457 $grade->timecreated = time();
3458 // If we are "auto-creating" a grade - and there is a submission
3459 // the new grade should not have a more recent timemodified value
3460 // than the submission.
3461 if ($submission) {
3462 $grade->timemodified = $submission->timemodified;
3463 } else {
3464 $grade->timemodified = $grade->timecreated;
3466 $grade->grade = -1;
3467 $grade->grader = $USER->id;
3468 if ($attemptnumber >= 0) {
3469 $grade->attemptnumber = $attemptnumber;
3472 $gid = $DB->insert_record('assign_grades', $grade);
3473 $grade->id = $gid;
3474 return $grade;
3476 return false;
3480 * This will retrieve a grade object from the db.
3482 * @param int $gradeid The id of the grade
3483 * @return stdClass The grade record
3485 protected function get_grade($gradeid) {
3486 global $DB;
3488 $params = array('assignment'=>$this->get_instance()->id, 'id'=>$gradeid);
3489 return $DB->get_record('assign_grades', $params, '*', MUST_EXIST);
3493 * Print the grading page for a single user submission.
3495 * @param array $args Optional args array (better than pulling args from _GET and _POST)
3496 * @return string
3498 protected function view_single_grading_panel($args) {
3499 global $DB, $CFG, $SESSION, $PAGE;
3501 $o = '';
3502 $instance = $this->get_instance();
3504 require_once($CFG->dirroot . '/mod/assign/gradeform.php');
3506 // Need submit permission to submit an assignment.
3507 require_capability('mod/assign:grade', $this->context);
3509 // If userid is passed - we are only grading a single student.
3510 $userid = $args['userid'];
3511 $attemptnumber = $args['attemptnumber'];
3513 // Apply overrides.
3514 $this->update_effective_access($userid);
3516 $rownum = 0;
3517 $useridlist = array($userid);
3519 $last = true;
3520 // This variation on the url will link direct to this student, with no next/previous links.
3521 // The benefit is the url will be the same every time for this student, so Atto autosave drafts can match up.
3522 $returnparams = array('userid' => $userid, 'rownum' => 0, 'useridlistid' => 0);
3523 $this->register_return_link('grade', $returnparams);
3525 $user = $DB->get_record('user', array('id' => $userid));
3526 $submission = $this->get_user_submission($userid, false, $attemptnumber);
3527 $submissiongroup = null;
3528 $teamsubmission = null;
3529 $notsubmitted = array();
3530 if ($instance->teamsubmission) {
3531 $teamsubmission = $this->get_group_submission($userid, 0, false, $attemptnumber);
3532 $submissiongroup = $this->get_submission_group($userid);
3533 $groupid = 0;
3534 if ($submissiongroup) {
3535 $groupid = $submissiongroup->id;
3537 $notsubmitted = $this->get_submission_group_members_who_have_not_submitted($groupid, false);
3541 // Get the requested grade.
3542 $grade = $this->get_user_grade($userid, false, $attemptnumber);
3543 $flags = $this->get_user_flags($userid, false);
3544 if ($this->can_view_submission($userid)) {
3545 $gradelocked = ($flags && $flags->locked) || $this->grading_disabled($userid);
3546 $extensionduedate = null;
3547 if ($flags) {
3548 $extensionduedate = $flags->extensionduedate;
3550 $showedit = $this->submissions_open($userid) && ($this->is_any_submission_plugin_enabled());
3551 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_course_context());
3552 $usergroups = $this->get_all_groups($user->id);
3554 $submissionstatus = new assign_submission_status_compact($instance->allowsubmissionsfromdate,
3555 $instance->alwaysshowdescription,
3556 $submission,
3557 $instance->teamsubmission,
3558 $teamsubmission,
3559 $submissiongroup,
3560 $notsubmitted,
3561 $this->is_any_submission_plugin_enabled(),
3562 $gradelocked,
3563 $this->is_graded($userid),
3564 $instance->duedate,
3565 $instance->cutoffdate,
3566 $this->get_submission_plugins(),
3567 $this->get_return_action(),
3568 $this->get_return_params(),
3569 $this->get_course_module()->id,
3570 $this->get_course()->id,
3571 assign_submission_status::GRADER_VIEW,
3572 $showedit,
3573 false,
3574 $viewfullnames,
3575 $extensionduedate,
3576 $this->get_context(),
3577 $this->is_blind_marking(),
3579 $instance->attemptreopenmethod,
3580 $instance->maxattempts,
3581 $this->get_grading_status($userid),
3582 $instance->preventsubmissionnotingroup,
3583 $usergroups);
3584 $o .= $this->get_renderer()->render($submissionstatus);
3587 if ($grade) {
3588 $data = new stdClass();
3589 if ($grade->grade !== null && $grade->grade >= 0) {
3590 $data->grade = format_float($grade->grade, $this->get_grade_item()->get_decimals());
3592 } else {
3593 $data = new stdClass();
3594 $data->grade = '';
3597 if (!empty($flags->workflowstate)) {
3598 $data->workflowstate = $flags->workflowstate;
3600 if (!empty($flags->allocatedmarker)) {
3601 $data->allocatedmarker = $flags->allocatedmarker;
3604 // Warning if required.
3605 $allsubmissions = $this->get_all_submissions($userid);
3607 if ($attemptnumber != -1 && ($attemptnumber + 1) != count($allsubmissions)) {
3608 $params = array('attemptnumber' => $attemptnumber + 1,
3609 'totalattempts' => count($allsubmissions));
3610 $message = get_string('editingpreviousfeedbackwarning', 'assign', $params);
3611 $o .= $this->get_renderer()->notification($message);
3614 $pagination = array('rownum' => $rownum,
3615 'useridlistid' => 0,
3616 'last' => $last,
3617 'userid' => $userid,
3618 'attemptnumber' => $attemptnumber,
3619 'gradingpanel' => true);
3621 if (!empty($args['formdata'])) {
3622 $data = (array) $data;
3623 $data = (object) array_merge($data, $args['formdata']);
3625 $formparams = array($this, $data, $pagination);
3626 $mform = new mod_assign_grade_form(null,
3627 $formparams,
3628 'post',
3630 array('class' => 'gradeform'));
3632 if (!empty($args['formdata'])) {
3633 // If we were passed form data - we want the form to check the data
3634 // and show errors.
3635 $mform->is_validated();
3637 $o .= $this->get_renderer()->heading(get_string('grade'), 3);
3638 $o .= $this->get_renderer()->render(new assign_form('gradingform', $mform));
3640 if (count($allsubmissions) > 1) {
3641 $allgrades = $this->get_all_grades($userid);
3642 $history = new assign_attempt_history_chooser($allsubmissions,
3643 $allgrades,
3644 $this->get_course_module()->id,
3645 $userid);
3647 $o .= $this->get_renderer()->render($history);
3650 \mod_assign\event\grading_form_viewed::create_from_user($this, $user)->trigger();
3652 return $o;
3656 * Print the grading page for a single user submission.
3658 * @param moodleform $mform
3659 * @return string
3661 protected function view_single_grade_page($mform) {
3662 global $DB, $CFG, $SESSION;
3664 $o = '';
3665 $instance = $this->get_instance();
3667 require_once($CFG->dirroot . '/mod/assign/gradeform.php');
3669 // Need submit permission to submit an assignment.
3670 require_capability('mod/assign:grade', $this->context);
3672 $header = new assign_header($instance,
3673 $this->get_context(),
3674 false,
3675 $this->get_course_module()->id,
3676 get_string('grading', 'assign'));
3677 $o .= $this->get_renderer()->render($header);
3679 // If userid is passed - we are only grading a single student.
3680 $rownum = optional_param('rownum', 0, PARAM_INT);
3681 $useridlistid = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
3682 $userid = optional_param('userid', 0, PARAM_INT);
3683 $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT);
3685 if (!$userid) {
3686 $useridlistkey = $this->get_useridlist_key($useridlistid);
3687 if (empty($SESSION->mod_assign_useridlist[$useridlistkey])) {
3688 $SESSION->mod_assign_useridlist[$useridlistkey] = $this->get_grading_userid_list();
3690 $useridlist = $SESSION->mod_assign_useridlist[$useridlistkey];
3691 } else {
3692 $rownum = 0;
3693 $useridlistid = 0;
3694 $useridlist = array($userid);
3697 if ($rownum < 0 || $rownum > count($useridlist)) {
3698 throw new coding_exception('Row is out of bounds for the current grading table: ' . $rownum);
3701 $last = false;
3702 $userid = $useridlist[$rownum];
3703 if ($rownum == count($useridlist) - 1) {
3704 $last = true;
3706 // This variation on the url will link direct to this student, with no next/previous links.
3707 // The benefit is the url will be the same every time for this student, so Atto autosave drafts can match up.
3708 $returnparams = array('userid' => $userid, 'rownum' => 0, 'useridlistid' => 0);
3709 $this->register_return_link('grade', $returnparams);
3711 $user = $DB->get_record('user', array('id' => $userid));
3712 if ($user) {
3713 $this->update_effective_access($userid);
3714 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_course_context());
3715 $usersummary = new assign_user_summary($user,
3716 $this->get_course()->id,
3717 $viewfullnames,
3718 $this->is_blind_marking(),
3719 $this->get_uniqueid_for_user($user->id),
3720 get_extra_user_fields($this->get_context()),
3721 !$this->is_active_user($userid));
3722 $o .= $this->get_renderer()->render($usersummary);
3724 $submission = $this->get_user_submission($userid, false, $attemptnumber);
3725 $submissiongroup = null;
3726 $teamsubmission = null;
3727 $notsubmitted = array();
3728 if ($instance->teamsubmission) {
3729 $teamsubmission = $this->get_group_submission($userid, 0, false, $attemptnumber);
3730 $submissiongroup = $this->get_submission_group($userid);
3731 $groupid = 0;
3732 if ($submissiongroup) {
3733 $groupid = $submissiongroup->id;
3735 $notsubmitted = $this->get_submission_group_members_who_have_not_submitted($groupid, false);
3739 // Get the requested grade.
3740 $grade = $this->get_user_grade($userid, false, $attemptnumber);
3741 $flags = $this->get_user_flags($userid, false);
3742 if ($this->can_view_submission($userid)) {
3743 $gradelocked = ($flags && $flags->locked) || $this->grading_disabled($userid);
3744 $extensionduedate = null;
3745 if ($flags) {
3746 $extensionduedate = $flags->extensionduedate;
3748 $showedit = $this->submissions_open($userid) && ($this->is_any_submission_plugin_enabled());
3749 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_course_context());
3750 $usergroups = $this->get_all_groups($user->id);
3752 $submissionstatus = new assign_submission_status($instance->allowsubmissionsfromdate,
3753 $instance->alwaysshowdescription,
3754 $submission,
3755 $instance->teamsubmission,
3756 $teamsubmission,
3757 $submissiongroup,
3758 $notsubmitted,
3759 $this->is_any_submission_plugin_enabled(),
3760 $gradelocked,
3761 $this->is_graded($userid),
3762 $instance->duedate,
3763 $instance->cutoffdate,
3764 $this->get_submission_plugins(),
3765 $this->get_return_action(),
3766 $this->get_return_params(),
3767 $this->get_course_module()->id,
3768 $this->get_course()->id,
3769 assign_submission_status::GRADER_VIEW,
3770 $showedit,
3771 false,
3772 $viewfullnames,
3773 $extensionduedate,
3774 $this->get_context(),
3775 $this->is_blind_marking(),
3777 $instance->attemptreopenmethod,
3778 $instance->maxattempts,
3779 $this->get_grading_status($userid),
3780 $instance->preventsubmissionnotingroup,
3781 $usergroups);
3782 $o .= $this->get_renderer()->render($submissionstatus);
3785 if ($grade) {
3786 $data = new stdClass();
3787 if ($grade->grade !== null && $grade->grade >= 0) {
3788 $data->grade = format_float($grade->grade, $this->get_grade_item()->get_decimals());
3790 } else {
3791 $data = new stdClass();
3792 $data->grade = '';
3795 if (!empty($flags->workflowstate)) {
3796 $data->workflowstate = $flags->workflowstate;
3798 if (!empty($flags->allocatedmarker)) {
3799 $data->allocatedmarker = $flags->allocatedmarker;
3802 // Warning if required.
3803 $allsubmissions = $this->get_all_submissions($userid);
3805 if ($attemptnumber != -1 && ($attemptnumber + 1) != count($allsubmissions)) {
3806 $params = array('attemptnumber'=>$attemptnumber + 1,
3807 'totalattempts'=>count($allsubmissions));
3808 $message = get_string('editingpreviousfeedbackwarning', 'assign', $params);
3809 $o .= $this->get_renderer()->notification($message);
3812 // Now show the grading form.
3813 if (!$mform) {
3814 $pagination = array('rownum' => $rownum,
3815 'useridlistid' => $useridlistid,
3816 'last' => $last,
3817 'userid' => $userid,
3818 'attemptnumber' => $attemptnumber);
3819 $formparams = array($this, $data, $pagination);
3820 $mform = new mod_assign_grade_form(null,
3821 $formparams,
3822 'post',
3824 array('class'=>'gradeform'));
3826 $o .= $this->get_renderer()->heading(get_string('grade'), 3);
3827 $o .= $this->get_renderer()->render(new assign_form('gradingform', $mform));
3829 if (count($allsubmissions) > 1 && $attemptnumber == -1) {
3830 $allgrades = $this->get_all_grades($userid);
3831 $history = new assign_attempt_history($allsubmissions,
3832 $allgrades,
3833 $this->get_submission_plugins(),
3834 $this->get_feedback_plugins(),
3835 $this->get_course_module()->id,
3836 $this->get_return_action(),
3837 $this->get_return_params(),
3838 true,
3839 $useridlistid,
3840 $rownum);
3842 $o .= $this->get_renderer()->render($history);
3845 \mod_assign\event\grading_form_viewed::create_from_user($this, $user)->trigger();
3847 $o .= $this->view_footer();
3848 return $o;
3852 * Show a confirmation page to make sure they want to release student identities.
3854 * @return string
3856 protected function view_reveal_identities_confirm() {
3857 require_capability('mod/assign:revealidentities', $this->get_context());
3859 $o = '';
3860 $header = new assign_header($this->get_instance(),
3861 $this->get_context(),
3862 false,
3863 $this->get_course_module()->id);
3864 $o .= $this->get_renderer()->render($header);
3866 $urlparams = array('id'=>$this->get_course_module()->id,
3867 'action'=>'revealidentitiesconfirm',
3868 'sesskey'=>sesskey());
3869 $confirmurl = new moodle_url('/mod/assign/view.php', $urlparams);
3871 $urlparams = array('id'=>$this->get_course_module()->id,
3872 'action'=>'grading');
3873 $cancelurl = new moodle_url('/mod/assign/view.php', $urlparams);
3875 $o .= $this->get_renderer()->confirm(get_string('revealidentitiesconfirm', 'assign'),
3876 $confirmurl,
3877 $cancelurl);
3878 $o .= $this->view_footer();
3880 \mod_assign\event\reveal_identities_confirmation_page_viewed::create_from_assign($this)->trigger();
3882 return $o;
3886 * View a link to go back to the previous page. Uses url parameters returnaction and returnparams.
3888 * @return string
3890 protected function view_return_links() {
3891 $returnaction = optional_param('returnaction', '', PARAM_ALPHA);
3892 $returnparams = optional_param('returnparams', '', PARAM_TEXT);
3894 $params = array();
3895 $returnparams = str_replace('&amp;', '&', $returnparams);
3896 parse_str($returnparams, $params);
3897 $newparams = array('id' => $this->get_course_module()->id, 'action' => $returnaction);
3898 $params = array_merge($newparams, $params);
3900 $url = new moodle_url('/mod/assign/view.php', $params);
3901 return $this->get_renderer()->single_button($url, get_string('back'), 'get');
3905 * View the grading table of all submissions for this assignment.
3907 * @return string
3909 protected function view_grading_table() {
3910 global $USER, $CFG, $SESSION;
3912 // Include grading options form.
3913 require_once($CFG->dirroot . '/mod/assign/gradingoptionsform.php');
3914 require_once($CFG->dirroot . '/mod/assign/quickgradingform.php');
3915 require_once($CFG->dirroot . '/mod/assign/gradingbatchoperationsform.php');
3916 $o = '';
3917 $cmid = $this->get_course_module()->id;
3919 $links = array();
3920 if (has_capability('gradereport/grader:view', $this->get_course_context()) &&
3921 has_capability('moodle/grade:viewall', $this->get_course_context())) {
3922 $gradebookurl = '/grade/report/grader/index.php?id=' . $this->get_course()->id;
3923 $links[$gradebookurl] = get_string('viewgradebook', 'assign');
3925 if ($this->is_any_submission_plugin_enabled() && $this->count_submissions()) {
3926 $downloadurl = '/mod/assign/view.php?id=' . $cmid . '&action=downloadall';
3927 $links[$downloadurl] = get_string('downloadall', 'assign');
3929 if ($this->is_blind_marking() &&
3930 has_capability('mod/assign:revealidentities', $this->get_context())) {
3931 $revealidentitiesurl = '/mod/assign/view.php?id=' . $cmid . '&action=revealidentities';
3932 $links[$revealidentitiesurl] = get_string('revealidentities', 'assign');
3934 foreach ($this->get_feedback_plugins() as $plugin) {
3935 if ($plugin->is_enabled() && $plugin->is_visible()) {
3936 foreach ($plugin->get_grading_actions() as $action => $description) {
3937 $url = '/mod/assign/view.php' .
3938 '?id=' . $cmid .
3939 '&plugin=' . $plugin->get_type() .
3940 '&pluginsubtype=assignfeedback' .
3941 '&action=viewpluginpage&pluginaction=' . $action;
3942 $links[$url] = $description;
3947 // Sort links alphabetically based on the link description.
3948 core_collator::asort($links);
3950 $gradingactions = new url_select($links);
3951 $gradingactions->set_label(get_string('choosegradingaction', 'assign'));
3953 $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
3955 $perpage = $this->get_assign_perpage();
3956 $filter = get_user_preferences('assign_filter', '');
3957 $markerfilter = get_user_preferences('assign_markerfilter', '');
3958 $workflowfilter = get_user_preferences('assign_workflowfilter', '');
3959 $controller = $gradingmanager->get_active_controller();
3960 $showquickgrading = empty($controller) && $this->can_grade();
3961 $quickgrading = get_user_preferences('assign_quickgrading', false);
3962 $showonlyactiveenrolopt = has_capability('moodle/course:viewsuspendedusers', $this->context);
3963 $downloadasfolders = get_user_preferences('assign_downloadasfolders', 1);
3965 $markingallocation = $this->get_instance()->markingworkflow &&
3966 $this->get_instance()->markingallocation &&
3967 has_capability('mod/assign:manageallocations', $this->context);
3968 // Get markers to use in drop lists.
3969 $markingallocationoptions = array();
3970 if ($markingallocation) {
3971 list($sort, $params) = users_order_by_sql();
3972 $markers = get_users_by_capability($this->context, 'mod/assign:grade', '', $sort);
3973 $markingallocationoptions[''] = get_string('filternone', 'assign');
3974 $markingallocationoptions[ASSIGN_MARKER_FILTER_NO_MARKER] = get_string('markerfilternomarker', 'assign');
3975 foreach ($markers as $marker) {
3976 $markingallocationoptions[$marker->id] = fullname($marker);
3980 $markingworkflow = $this->get_instance()->markingworkflow;
3981 // Get marking states to show in form.
3982 $markingworkflowoptions = array();
3983 if ($markingworkflow) {
3984 $notmarked = get_string('markingworkflowstatenotmarked', 'assign');
3985 $markingworkflowoptions[''] = get_string('filternone', 'assign');
3986 $markingworkflowoptions[ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED] = $notmarked;
3987 $markingworkflowoptions = array_merge($markingworkflowoptions, $this->get_marking_workflow_states_for_current_user());
3990 // Print options for changing the filter and changing the number of results per page.
3991 $gradingoptionsformparams = array('cm'=>$cmid,
3992 'contextid'=>$this->context->id,
3993 'userid'=>$USER->id,
3994 'submissionsenabled'=>$this->is_any_submission_plugin_enabled(),
3995 'showquickgrading'=>$showquickgrading,
3996 'quickgrading'=>$quickgrading,
3997 'markingworkflowopt'=>$markingworkflowoptions,
3998 'markingallocationopt'=>$markingallocationoptions,
3999 'showonlyactiveenrolopt'=>$showonlyactiveenrolopt,
4000 'showonlyactiveenrol' => $this->show_only_active_users(),
4001 'downloadasfolders' => $downloadasfolders);
4003 $classoptions = array('class'=>'gradingoptionsform');
4004 $gradingoptionsform = new mod_assign_grading_options_form(null,
4005 $gradingoptionsformparams,
4006 'post',
4008 $classoptions);
4010 $batchformparams = array('cm'=>$cmid,
4011 'submissiondrafts'=>$this->get_instance()->submissiondrafts,
4012 'duedate'=>$this->get_instance()->duedate,
4013 'attemptreopenmethod'=>$this->get_instance()->attemptreopenmethod,
4014 'feedbackplugins'=>$this->get_feedback_plugins(),
4015 'context'=>$this->get_context(),
4016 'markingworkflow'=>$markingworkflow,
4017 'markingallocation'=>$markingallocation);
4018 $classoptions = array('class'=>'gradingbatchoperationsform');
4020 $gradingbatchoperationsform = new mod_assign_grading_batch_operations_form(null,
4021 $batchformparams,
4022 'post',
4024 $classoptions);
4026 $gradingoptionsdata = new stdClass();
4027 $gradingoptionsdata->perpage = $perpage;
4028 $gradingoptionsdata->filter = $filter;
4029 $gradingoptionsdata->markerfilter = $markerfilter;
4030 $gradingoptionsdata->workflowfilter = $workflowfilter;
4031 $gradingoptionsform->set_data($gradingoptionsdata);
4033 $actionformtext = $this->get_renderer()->render($gradingactions);
4034 $header = new assign_header($this->get_instance(),
4035 $this->get_context(),
4036 false,
4037 $this->get_course_module()->id,
4038 get_string('grading', 'assign'),
4039 $actionformtext);
4040 $o .= $this->get_renderer()->render($header);
4042 $currenturl = $CFG->wwwroot .
4043 '/mod/assign/view.php?id=' .
4044 $this->get_course_module()->id .
4045 '&action=grading';
4047 $o .= groups_print_activity_menu($this->get_course_module(), $currenturl, true);
4049 // Plagiarism update status apearring in the grading book.
4050 if (!empty($CFG->enableplagiarism)) {
4051 require_once($CFG->libdir . '/plagiarismlib.php');
4052 $o .= plagiarism_update_status($this->get_course(), $this->get_course_module());
4055 if ($this->is_blind_marking() && has_capability('mod/assign:viewblinddetails', $this->get_context())) {
4056 $o .= $this->get_renderer()->notification(get_string('blindmarkingenabledwarning', 'assign'), 'notifymessage');
4059 // Load and print the table of submissions.
4060 if ($showquickgrading && $quickgrading) {
4061 $gradingtable = new assign_grading_table($this, $perpage, $filter, 0, true);
4062 $table = $this->get_renderer()->render($gradingtable);
4063 $page = optional_param('page', null, PARAM_INT);
4064 $quickformparams = array('cm'=>$this->get_course_module()->id,
4065 'gradingtable'=>$table,
4066 'sendstudentnotifications' => $this->get_instance()->sendstudentnotifications,
4067 'page' => $page);
4068 $quickgradingform = new mod_assign_quick_grading_form(null, $quickformparams);
4070 $o .= $this->get_renderer()->render(new assign_form('quickgradingform', $quickgradingform));
4071 } else {
4072 $gradingtable = new assign_grading_table($this, $perpage, $filter, 0, false);
4073 $o .= $this->get_renderer()->render($gradingtable);
4076 if ($this->can_grade()) {
4077 // We need to store the order of uses in the table as the person may wish to grade them.
4078 // This is done based on the row number of the user.
4079 $useridlist = $gradingtable->get_column_data('userid');
4080 $SESSION->mod_assign_useridlist[$this->get_useridlist_key()] = $useridlist;
4083 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
4084 $users = array_keys($this->list_participants($currentgroup, true));
4085 if (count($users) != 0 && $this->can_grade()) {
4086 // If no enrolled user in a course then don't display the batch operations feature.
4087 $assignform = new assign_form('gradingbatchoperationsform', $gradingbatchoperationsform);
4088 $o .= $this->get_renderer()->render($assignform);
4090 $assignform = new assign_form('gradingoptionsform',
4091 $gradingoptionsform,
4092 'M.mod_assign.init_grading_options');
4093 $o .= $this->get_renderer()->render($assignform);
4094 return $o;
4098 * View entire grader app.
4100 * @return string
4102 protected function view_grader() {
4103 global $USER, $PAGE;
4105 $o = '';
4106 // Need submit permission to submit an assignment.
4107 $this->require_view_grades();
4109 $PAGE->set_pagelayout('embedded');
4111 $PAGE->set_title($this->get_context()->get_context_name());
4113 $o .= $this->get_renderer()->header();
4115 $userid = optional_param('userid', 0, PARAM_INT);
4116 $blindid = optional_param('blindid', 0, PARAM_INT);
4118 if (!$userid && $blindid) {
4119 $userid = $this->get_user_id_for_uniqueid($blindid);
4122 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
4123 $framegrader = new grading_app($userid, $currentgroup, $this);
4125 $this->update_effective_access($userid);
4127 $o .= $this->get_renderer()->render($framegrader);
4129 $o .= $this->view_footer();
4131 \mod_assign\event\grading_table_viewed::create_from_assign($this)->trigger();
4133 return $o;
4136 * View entire grading page.
4138 * @return string
4140 protected function view_grading_page() {
4141 global $CFG;
4143 $o = '';
4144 // Need submit permission to submit an assignment.
4145 $this->require_view_grades();
4146 require_once($CFG->dirroot . '/mod/assign/gradeform.php');
4148 // Only load this if it is.
4149 $o .= $this->view_grading_table();
4151 $o .= $this->view_footer();
4153 \mod_assign\event\grading_table_viewed::create_from_assign($this)->trigger();
4155 return $o;
4159 * Capture the output of the plagiarism plugins disclosures and return it as a string.
4161 * @return string
4163 protected function plagiarism_print_disclosure() {
4164 global $CFG;
4165 $o = '';
4167 if (!empty($CFG->enableplagiarism)) {
4168 require_once($CFG->libdir . '/plagiarismlib.php');
4170 $o .= plagiarism_print_disclosure($this->get_course_module()->id);
4173 return $o;
4177 * Message for students when assignment submissions have been closed.
4179 * @param string $title The page title
4180 * @param array $notices The array of notices to show.
4181 * @return string
4183 protected function view_notices($title, $notices) {
4184 global $CFG;
4186 $o = '';
4188 $header = new assign_header($this->get_instance(),
4189 $this->get_context(),
4190 $this->show_intro(),
4191 $this->get_course_module()->id,
4192 $title);
4193 $o .= $this->get_renderer()->render($header);
4195 foreach ($notices as $notice) {
4196 $o .= $this->get_renderer()->notification($notice);
4199 $url = new moodle_url('/mod/assign/view.php', array('id'=>$this->get_course_module()->id, 'action'=>'view'));
4200 $o .= $this->get_renderer()->continue_button($url);
4202 $o .= $this->view_footer();
4204 return $o;
4208 * Get the name for a user - hiding their real name if blind marking is on.
4210 * @param stdClass $user The user record as required by fullname()
4211 * @return string The name.
4213 public function fullname($user) {
4214 if ($this->is_blind_marking()) {
4215 $hasviewblind = has_capability('mod/assign:viewblinddetails', $this->get_context());
4216 if (empty($user->recordid)) {
4217 $uniqueid = $this->get_uniqueid_for_user($user->id);
4218 } else {
4219 $uniqueid = $user->recordid;
4221 if ($hasviewblind) {
4222 return get_string('participant', 'assign') . ' ' . $uniqueid . ' (' . fullname($user) . ')';
4223 } else {
4224 return get_string('participant', 'assign') . ' ' . $uniqueid;
4226 } else {
4227 return fullname($user);
4232 * View edit submissions page.
4234 * @param moodleform $mform
4235 * @param array $notices A list of notices to display at the top of the
4236 * edit submission form (e.g. from plugins).
4237 * @return string The page output.
4239 protected function view_edit_submission_page($mform, $notices) {
4240 global $CFG, $USER, $DB;
4242 $o = '';
4243 require_once($CFG->dirroot . '/mod/assign/submission_form.php');
4244 // Need submit permission to submit an assignment.
4245 $userid = optional_param('userid', $USER->id, PARAM_INT);
4246 $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
4248 // This variation on the url will link direct to this student.
4249 // The benefit is the url will be the same every time for this student, so Atto autosave drafts can match up.
4250 $returnparams = array('userid' => $userid, 'rownum' => 0, 'useridlistid' => 0);
4251 $this->register_return_link('editsubmission', $returnparams);
4253 if ($userid == $USER->id) {
4254 if (!$this->can_edit_submission($userid, $USER->id)) {
4255 print_error('nopermission');
4257 // User is editing their own submission.
4258 require_capability('mod/assign:submit', $this->context);
4259 $title = get_string('editsubmission', 'assign');
4260 } else {
4261 // User is editing another user's submission.
4262 if (!$this->can_edit_submission($userid, $USER->id)) {
4263 print_error('nopermission');
4266 $name = $this->fullname($user);
4267 $title = get_string('editsubmissionother', 'assign', $name);
4270 if (!$this->submissions_open($userid)) {
4271 $message = array(get_string('submissionsclosed', 'assign'));
4272 return $this->view_notices($title, $message);
4275 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
4276 $this->get_context(),
4277 $this->show_intro(),
4278 $this->get_course_module()->id,
4279 $title));
4280 if ($userid == $USER->id) {
4281 // We only show this if it their submission.
4282 $o .= $this->plagiarism_print_disclosure();
4284 $data = new stdClass();
4285 $data->userid = $userid;
4286 if (!$mform) {
4287 $mform = new mod_assign_submission_form(null, array($this, $data));
4290 foreach ($notices as $notice) {
4291 $o .= $this->get_renderer()->notification($notice);
4294 $o .= $this->get_renderer()->render(new assign_form('editsubmissionform', $mform));
4296 $o .= $this->view_footer();
4298 \mod_assign\event\submission_form_viewed::create_from_user($this, $user)->trigger();
4300 return $o;
4304 * See if this assignment has a grade yet.
4306 * @param int $userid
4307 * @return bool
4309 protected function is_graded($userid) {
4310 $grade = $this->get_user_grade($userid, false);
4311 if ($grade) {
4312 return ($grade->grade !== null && $grade->grade >= 0);
4314 return false;
4318 * Perform an access check to see if the current $USER can view this group submission.
4320 * @param int $groupid
4321 * @return bool
4323 public function can_view_group_submission($groupid) {
4324 global $USER;
4326 $members = $this->get_submission_group_members($groupid, true);
4327 foreach ($members as $member) {
4328 // If we can view any members submission, we can view the submission for the group.
4329 if ($this->can_view_submission($member->id)) {
4330 return true;
4333 return false;
4337 * Perform an access check to see if the current $USER can view this users submission.
4339 * @param int $userid
4340 * @return bool
4342 public function can_view_submission($userid) {
4343 global $USER;
4345 if (!$this->is_active_user($userid) && !has_capability('moodle/course:viewsuspendedusers', $this->context)) {
4346 return false;
4348 if (!is_enrolled($this->get_course_context(), $userid)) {
4349 return false;
4351 if (has_any_capability(array('mod/assign:viewgrades', 'mod/assign:grade'), $this->context)) {
4352 return true;
4354 if ($userid == $USER->id && has_capability('mod/assign:submit', $this->context)) {
4355 return true;
4357 return false;
4361 * Allows the plugin to show a batch grading operation page.
4363 * @param moodleform $mform
4364 * @return none
4366 protected function view_plugin_grading_batch_operation($mform) {
4367 require_capability('mod/assign:grade', $this->context);
4368 $prefix = 'plugingradingbatchoperation_';
4370 if ($data = $mform->get_data()) {
4371 $tail = substr($data->operation, strlen($prefix));
4372 list($plugintype, $action) = explode('_', $tail, 2);
4374 $plugin = $this->get_feedback_plugin_by_type($plugintype);
4375 if ($plugin) {
4376 $users = $data->selectedusers;
4377 $userlist = explode(',', $users);
4378 echo $plugin->grading_batch_operation($action, $userlist);
4379 return;
4382 print_error('invalidformdata', '');
4386 * Ask the user to confirm they want to perform this batch operation
4388 * @param moodleform $mform Set to a grading batch operations form
4389 * @return string - the page to view after processing these actions
4391 protected function process_grading_batch_operation(& $mform) {
4392 global $CFG;
4393 require_once($CFG->dirroot . '/mod/assign/gradingbatchoperationsform.php');
4394 require_sesskey();
4396 $markingallocation = $this->get_instance()->markingworkflow &&
4397 $this->get_instance()->markingallocation &&
4398 has_capability('mod/assign:manageallocations', $this->context);
4400 $batchformparams = array('cm'=>$this->get_course_module()->id,
4401 'submissiondrafts'=>$this->get_instance()->submissiondrafts,
4402 'duedate'=>$this->get_instance()->duedate,
4403 'attemptreopenmethod'=>$this->get_instance()->attemptreopenmethod,
4404 'feedbackplugins'=>$this->get_feedback_plugins(),
4405 'context'=>$this->get_context(),
4406 'markingworkflow'=>$this->get_instance()->markingworkflow,
4407 'markingallocation'=>$markingallocation);
4408 $formclasses = array('class'=>'gradingbatchoperationsform');
4409 $mform = new mod_assign_grading_batch_operations_form(null,
4410 $batchformparams,
4411 'post',
4413 $formclasses);
4415 if ($data = $mform->get_data()) {
4416 // Get the list of users.
4417 $users = $data->selectedusers;
4418 $userlist = explode(',', $users);
4420 $prefix = 'plugingradingbatchoperation_';
4422 if ($data->operation == 'grantextension') {
4423 // Reset the form so the grant extension page will create the extension form.
4424 $mform = null;
4425 return 'grantextension';
4426 } else if ($data->operation == 'setmarkingworkflowstate') {
4427 return 'viewbatchsetmarkingworkflowstate';
4428 } else if ($data->operation == 'setmarkingallocation') {
4429 return 'viewbatchmarkingallocation';
4430 } else if (strpos($data->operation, $prefix) === 0) {
4431 $tail = substr($data->operation, strlen($prefix));
4432 list($plugintype, $action) = explode('_', $tail, 2);
4434 $plugin = $this->get_feedback_plugin_by_type($plugintype);
4435 if ($plugin) {
4436 return 'plugingradingbatchoperation';
4440 if ($data->operation == 'downloadselected') {
4441 $this->download_submissions($userlist);
4442 } else {
4443 foreach ($userlist as $userid) {
4444 if ($data->operation == 'lock') {
4445 $this->process_lock_submission($userid);
4446 } else if ($data->operation == 'unlock') {
4447 $this->process_unlock_submission($userid);
4448 } else if ($data->operation == 'reverttodraft') {
4449 $this->process_revert_to_draft($userid);
4450 } else if ($data->operation == 'addattempt') {
4451 if (!$this->get_instance()->teamsubmission) {
4452 $this->process_add_attempt($userid);
4457 if ($this->get_instance()->teamsubmission && $data->operation == 'addattempt') {
4458 // This needs to be handled separately so that each team submission is only re-opened one time.
4459 $this->process_add_attempt_group($userlist);
4463 return 'grading';
4467 * Shows a form that allows the workflow state for selected submissions to be changed.
4469 * @param moodleform $mform Set to a grading batch operations form
4470 * @return string - the page to view after processing these actions
4472 protected function view_batch_set_workflow_state($mform) {
4473 global $CFG, $DB;
4475 require_once($CFG->dirroot . '/mod/assign/batchsetmarkingworkflowstateform.php');
4477 $o = '';
4479 $submitteddata = $mform->get_data();
4480 $users = $submitteddata->selectedusers;
4481 $userlist = explode(',', $users);
4483 $formdata = array('id' => $this->get_course_module()->id,
4484 'selectedusers' => $users);
4486 $usershtml = '';
4488 $usercount = 0;
4489 $extrauserfields = get_extra_user_fields($this->get_context());
4490 foreach ($userlist as $userid) {
4491 if ($usercount >= 5) {
4492 $usershtml .= get_string('moreusers', 'assign', count($userlist) - 5);
4493 break;
4495 $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
4497 $usershtml .= $this->get_renderer()->render(new assign_user_summary($user,
4498 $this->get_course()->id,
4499 has_capability('moodle/site:viewfullnames',
4500 $this->get_course_context()),
4501 $this->is_blind_marking(),
4502 $this->get_uniqueid_for_user($user->id),
4503 $extrauserfields,
4504 !$this->is_active_user($userid)));
4505 $usercount += 1;
4508 $formparams = array(
4509 'userscount' => count($userlist),
4510 'usershtml' => $usershtml,
4511 'markingworkflowstates' => $this->get_marking_workflow_states_for_current_user()
4514 $mform = new mod_assign_batch_set_marking_workflow_state_form(null, $formparams);
4515 $mform->set_data($formdata); // Initialises the hidden elements.
4516 $header = new assign_header($this->get_instance(),
4517 $this->get_context(),
4518 $this->show_intro(),
4519 $this->get_course_module()->id,
4520 get_string('setmarkingworkflowstate', 'assign'));
4521 $o .= $this->get_renderer()->render($header);
4522 $o .= $this->get_renderer()->render(new assign_form('setworkflowstate', $mform));
4523 $o .= $this->view_footer();
4525 \mod_assign\event\batch_set_workflow_state_viewed::create_from_assign($this)->trigger();
4527 return $o;
4531 * Shows a form that allows the allocated marker for selected submissions to be changed.
4533 * @param moodleform $mform Set to a grading batch operations form
4534 * @return string - the page to view after processing these actions
4536 public function view_batch_markingallocation($mform) {
4537 global $CFG, $DB;
4539 require_once($CFG->dirroot . '/mod/assign/batchsetallocatedmarkerform.php');
4541 $o = '';
4543 $submitteddata = $mform->get_data();
4544 $users = $submitteddata->selectedusers;
4545 $userlist = explode(',', $users);
4547 $formdata = array('id' => $this->get_course_module()->id,
4548 'selectedusers' => $users);
4550 $usershtml = '';
4552 $usercount = 0;
4553 $extrauserfields = get_extra_user_fields($this->get_context());
4554 foreach ($userlist as $userid) {
4555 if ($usercount >= 5) {
4556 $usershtml .= get_string('moreusers', 'assign', count($userlist) - 5);
4557 break;
4559 $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
4561 $usershtml .= $this->get_renderer()->render(new assign_user_summary($user,
4562 $this->get_course()->id,
4563 has_capability('moodle/site:viewfullnames',
4564 $this->get_course_context()),
4565 $this->is_blind_marking(),
4566 $this->get_uniqueid_for_user($user->id),
4567 $extrauserfields,
4568 !$this->is_active_user($userid)));
4569 $usercount += 1;
4572 $formparams = array(
4573 'userscount' => count($userlist),
4574 'usershtml' => $usershtml,
4577 list($sort, $params) = users_order_by_sql();
4578 $markers = get_users_by_capability($this->get_context(), 'mod/assign:grade', '', $sort);
4579 $markerlist = array();
4580 foreach ($markers as $marker) {
4581 $markerlist[$marker->id] = fullname($marker);
4584 $formparams['markers'] = $markerlist;
4586 $mform = new mod_assign_batch_set_allocatedmarker_form(null, $formparams);
4587 $mform->set_data($formdata); // Initialises the hidden elements.
4588 $header = new assign_header($this->get_instance(),
4589 $this->get_context(),
4590 $this->show_intro(),
4591 $this->get_course_module()->id,
4592 get_string('setmarkingallocation', 'assign'));
4593 $o .= $this->get_renderer()->render($header);
4594 $o .= $this->get_renderer()->render(new assign_form('setworkflowstate', $mform));
4595 $o .= $this->view_footer();
4597 \mod_assign\event\batch_set_marker_allocation_viewed::create_from_assign($this)->trigger();
4599 return $o;
4603 * Ask the user to confirm they want to submit their work for grading.
4605 * @param moodleform $mform - null unless form validation has failed
4606 * @return string
4608 protected function check_submit_for_grading($mform) {
4609 global $USER, $CFG;
4611 require_once($CFG->dirroot . '/mod/assign/submissionconfirmform.php');
4613 // Check that all of the submission plugins are ready for this submission.
4614 $notifications = array();
4615 $submission = $this->get_user_submission($USER->id, false);
4616 $plugins = $this->get_submission_plugins();
4617 foreach ($plugins as $plugin) {
4618 if ($plugin->is_enabled() && $plugin->is_visible()) {
4619 $check = $plugin->precheck_submission($submission);
4620 if ($check !== true) {
4621 $notifications[] = $check;
4626 $data = new stdClass();
4627 $adminconfig = $this->get_admin_config();
4628 $requiresubmissionstatement = $this->get_instance()->requiresubmissionstatement &&
4629 !empty($adminconfig->submissionstatement);
4631 $submissionstatement = '';
4632 if (!empty($adminconfig->submissionstatement)) {
4633 // Format the submission statement before its sent. We turn off para because this is going within
4634 // a form element.
4635 $options = array(
4636 'context' => $this->get_context(),
4637 'para' => false
4639 $submissionstatement = format_text($adminconfig->submissionstatement, FORMAT_MOODLE, $options);
4642 if ($mform == null) {
4643 $mform = new mod_assign_confirm_submission_form(null, array($requiresubmissionstatement,
4644 $submissionstatement,
4645 $this->get_course_module()->id,
4646 $data));
4648 $o = '';
4649 $o .= $this->get_renderer()->header();
4650 $submitforgradingpage = new assign_submit_for_grading_page($notifications,
4651 $this->get_course_module()->id,
4652 $mform);
4653 $o .= $this->get_renderer()->render($submitforgradingpage);
4654 $o .= $this->view_footer();
4656 \mod_assign\event\submission_confirmation_form_viewed::create_from_assign($this)->trigger();
4658 return $o;
4662 * Creates an assign_submission_status renderable.
4664 * @param stdClass $user the user to get the report for
4665 * @param bool $showlinks return plain text or links to the profile
4666 * @return assign_submission_status renderable object
4668 public function get_assign_submission_status_renderable($user, $showlinks) {
4669 global $PAGE;
4671 $instance = $this->get_instance();
4672 $flags = $this->get_user_flags($user->id, false);
4673 $submission = $this->get_user_submission($user->id, false);
4675 $teamsubmission = null;
4676 $submissiongroup = null;
4677 $notsubmitted = array();
4678 if ($instance->teamsubmission) {
4679 $teamsubmission = $this->get_group_submission($user->id, 0, false);
4680 $submissiongroup = $this->get_submission_group($user->id);
4681 $groupid = 0;
4682 if ($submissiongroup) {
4683 $groupid = $submissiongroup->id;
4685 $notsubmitted = $this->get_submission_group_members_who_have_not_submitted($groupid, false);
4688 $showedit = $showlinks &&
4689 ($this->is_any_submission_plugin_enabled()) &&
4690 $this->can_edit_submission($user->id);
4692 $gradelocked = ($flags && $flags->locked) || $this->grading_disabled($user->id, false);
4694 // Grading criteria preview.
4695 $gradingmanager = get_grading_manager($this->context, 'mod_assign', 'submissions');
4696 $gradingcontrollerpreview = '';
4697 if ($gradingmethod = $gradingmanager->get_active_method()) {
4698 $controller = $gradingmanager->get_controller($gradingmethod);
4699 if ($controller->is_form_defined()) {
4700 $gradingcontrollerpreview = $controller->render_preview($PAGE);
4704 $showsubmit = ($showlinks && $this->submissions_open($user->id));
4705 $showsubmit = ($showsubmit && $this->show_submit_button($submission, $teamsubmission, $user->id));
4707 $extensionduedate = null;
4708 if ($flags) {
4709 $extensionduedate = $flags->extensionduedate;
4711 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_course_context());
4713 $gradingstatus = $this->get_grading_status($user->id);
4714 $usergroups = $this->get_all_groups($user->id);
4715 $submissionstatus = new assign_submission_status($instance->allowsubmissionsfromdate,
4716 $instance->alwaysshowdescription,
4717 $submission,
4718 $instance->teamsubmission,
4719 $teamsubmission,
4720 $submissiongroup,
4721 $notsubmitted,
4722 $this->is_any_submission_plugin_enabled(),
4723 $gradelocked,
4724 $this->is_graded($user->id),
4725 $instance->duedate,
4726 $instance->cutoffdate,
4727 $this->get_submission_plugins(),
4728 $this->get_return_action(),
4729 $this->get_return_params(),
4730 $this->get_course_module()->id,
4731 $this->get_course()->id,
4732 assign_submission_status::STUDENT_VIEW,
4733 $showedit,
4734 $showsubmit,
4735 $viewfullnames,
4736 $extensionduedate,
4737 $this->get_context(),
4738 $this->is_blind_marking(),
4739 $gradingcontrollerpreview,
4740 $instance->attemptreopenmethod,
4741 $instance->maxattempts,
4742 $gradingstatus,
4743 $instance->preventsubmissionnotingroup,
4744 $usergroups);
4745 return $submissionstatus;
4750 * Creates an assign_feedback_status renderable.
4752 * @param stdClass $user the user to get the report for
4753 * @return assign_feedback_status renderable object
4755 public function get_assign_feedback_status_renderable($user) {
4756 global $CFG, $DB, $PAGE;
4758 require_once($CFG->libdir.'/gradelib.php');
4759 require_once($CFG->dirroot.'/grade/grading/lib.php');
4761 $instance = $this->get_instance();
4762 $grade = $this->get_user_grade($user->id, false);
4763 $gradingstatus = $this->get_grading_status($user->id);
4765 $gradinginfo = grade_get_grades($this->get_course()->id,
4766 'mod',
4767 'assign',
4768 $instance->id,
4769 $user->id);
4771 $gradingitem = null;
4772 $gradebookgrade = null;
4773 if (isset($gradinginfo->items[0])) {
4774 $gradingitem = $gradinginfo->items[0];
4775 $gradebookgrade = $gradingitem->grades[$user->id];
4778 // Check to see if all feedback plugins are empty.
4779 $emptyplugins = true;
4780 if ($grade) {
4781 foreach ($this->get_feedback_plugins() as $plugin) {
4782 if ($plugin->is_visible() && $plugin->is_enabled()) {
4783 if (!$plugin->is_empty($grade)) {
4784 $emptyplugins = false;
4790 if ($this->get_instance()->markingworkflow && $gradingstatus != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
4791 $emptyplugins = true; // Don't show feedback plugins until released either.
4794 $cangrade = has_capability('mod/assign:grade', $this->get_context());
4795 // If there is a visible grade, show the summary.
4796 if (!is_null($gradebookgrade) && (!is_null($gradebookgrade->grade) || !$emptyplugins)
4797 && ($cangrade || !$gradebookgrade->hidden)) {
4799 $gradefordisplay = null;
4800 $gradeddate = null;
4801 $grader = null;
4802 $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
4804 // Only show the grade if it is not hidden in gradebook.
4805 if (!is_null($gradebookgrade->grade) && ($cangrade || !$gradebookgrade->hidden)) {
4806 if ($controller = $gradingmanager->get_active_controller()) {
4807 $menu = make_grades_menu($this->get_instance()->grade);
4808 $controller->set_grade_range($menu, $this->get_instance()->grade > 0);
4809 $gradefordisplay = $controller->render_grade($PAGE,
4810 $grade->id,
4811 $gradingitem,
4812 $gradebookgrade->str_long_grade,
4813 $cangrade);
4814 } else {
4815 $gradefordisplay = $this->display_grade($gradebookgrade->grade, false);
4817 $gradeddate = $gradebookgrade->dategraded;
4818 if (isset($grade->grader)) {
4819 $grader = $DB->get_record('user', array('id' => $grade->grader));
4823 $feedbackstatus = new assign_feedback_status($gradefordisplay,
4824 $gradeddate,
4825 $grader,
4826 $this->get_feedback_plugins(),
4827 $grade,
4828 $this->get_course_module()->id,
4829 $this->get_return_action(),
4830 $this->get_return_params());
4831 return $feedbackstatus;
4833 return;
4837 * Creates an assign_attempt_history renderable.
4839 * @param stdClass $user the user to get the report for
4840 * @return assign_attempt_history renderable object
4842 public function get_assign_attempt_history_renderable($user) {
4844 $allsubmissions = $this->get_all_submissions($user->id);
4845 $allgrades = $this->get_all_grades($user->id);
4847 $history = new assign_attempt_history($allsubmissions,
4848 $allgrades,
4849 $this->get_submission_plugins(),
4850 $this->get_feedback_plugins(),
4851 $this->get_course_module()->id,
4852 $this->get_return_action(),
4853 $this->get_return_params(),
4854 false,
4857 return $history;
4861 * Print 2 tables of information with no action links -
4862 * the submission summary and the grading summary.
4864 * @param stdClass $user the user to print the report for
4865 * @param bool $showlinks - Return plain text or links to the profile
4866 * @return string - the html summary
4868 public function view_student_summary($user, $showlinks) {
4870 $o = '';
4872 if ($this->can_view_submission($user->id)) {
4874 if (has_capability('mod/assign:submit', $this->get_context(), $user, false)) {
4875 $submissionstatus = $this->get_assign_submission_status_renderable($user, $showlinks);
4876 $o .= $this->get_renderer()->render($submissionstatus);
4879 // If there is a visible grade, show the feedback.
4880 $feedbackstatus = $this->get_assign_feedback_status_renderable($user);
4881 if ($feedbackstatus) {
4882 $o .= $this->get_renderer()->render($feedbackstatus);
4885 // If there is more than one submission, show the history.
4886 $history = $this->get_assign_attempt_history_renderable($user);
4887 if (count($history->submissions) > 1) {
4888 $o .= $this->get_renderer()->render($history);
4891 return $o;
4895 * Returns true if the submit subsission button should be shown to the user.
4897 * @param stdClass $submission The users own submission record.
4898 * @param stdClass $teamsubmission The users team submission record if there is one
4899 * @param int $userid The user
4900 * @return bool
4902 protected function show_submit_button($submission = null, $teamsubmission = null, $userid = null) {
4903 if ($teamsubmission) {
4904 if ($teamsubmission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
4905 // The assignment submission has been completed.
4906 return false;
4907 } else if ($this->submission_empty($teamsubmission)) {
4908 // There is nothing to submit yet.
4909 return false;
4910 } else if ($submission && $submission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
4911 // The user has already clicked the submit button on the team submission.
4912 return false;
4913 } else if (
4914 !empty($this->get_instance()->preventsubmissionnotingroup)
4915 && $this->get_submission_group($userid) == false
4917 return false;
4919 } else if ($submission) {
4920 if ($submission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
4921 // The assignment submission has been completed.
4922 return false;
4923 } else if ($this->submission_empty($submission)) {
4924 // There is nothing to submit.
4925 return false;
4927 } else {
4928 // We've not got a valid submission or team submission.
4929 return false;
4931 // Last check is that this instance allows drafts.
4932 return $this->get_instance()->submissiondrafts;
4936 * Get the grades for all previous attempts.
4937 * For each grade - the grader is a full user record,
4938 * and gradefordisplay is added (rendered from grading manager).
4940 * @param int $userid If not set, $USER->id will be used.
4941 * @return array $grades All grade records for this user.
4943 protected function get_all_grades($userid) {
4944 global $DB, $USER, $PAGE;
4946 // If the userid is not null then use userid.
4947 if (!$userid) {
4948 $userid = $USER->id;
4951 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
4953 $grades = $DB->get_records('assign_grades', $params, 'attemptnumber ASC');
4955 $gradercache = array();
4956 $cangrade = has_capability('mod/assign:grade', $this->get_context());
4958 // Need gradingitem and gradingmanager.
4959 $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
4960 $controller = $gradingmanager->get_active_controller();
4962 $gradinginfo = grade_get_grades($this->get_course()->id,
4963 'mod',
4964 'assign',
4965 $this->get_instance()->id,
4966 $userid);
4968 $gradingitem = null;
4969 if (isset($gradinginfo->items[0])) {
4970 $gradingitem = $gradinginfo->items[0];
4973 foreach ($grades as $grade) {
4974 // First lookup the grader info.
4975 if (isset($gradercache[$grade->grader])) {
4976 $grade->grader = $gradercache[$grade->grader];
4977 } else {
4978 // Not in cache - need to load the grader record.
4979 $grade->grader = $DB->get_record('user', array('id'=>$grade->grader));
4980 $gradercache[$grade->grader->id] = $grade->grader;
4983 // Now get the gradefordisplay.
4984 if ($controller) {
4985 $controller->set_grade_range(make_grades_menu($this->get_instance()->grade), $this->get_instance()->grade > 0);
4986 $grade->gradefordisplay = $controller->render_grade($PAGE,
4987 $grade->id,
4988 $gradingitem,
4989 $grade->grade,
4990 $cangrade);
4991 } else {
4992 $grade->gradefordisplay = $this->display_grade($grade->grade, false);
4997 return $grades;
5001 * Get the submissions for all previous attempts.
5003 * @param int $userid If not set, $USER->id will be used.
5004 * @return array $submissions All submission records for this user (or group).
5006 protected function get_all_submissions($userid) {
5007 global $DB, $USER;
5009 // If the userid is not null then use userid.
5010 if (!$userid) {
5011 $userid = $USER->id;
5014 $params = array();
5016 if ($this->get_instance()->teamsubmission) {
5017 $groupid = 0;
5018 $group = $this->get_submission_group($userid);
5019 if ($group) {
5020 $groupid = $group->id;
5023 // Params to get the group submissions.
5024 $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
5025 } else {
5026 // Params to get the user submissions.
5027 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
5030 // Return the submissions ordered by attempt.
5031 $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber ASC');
5033 return $submissions;
5037 * Creates an assign_grading_summary renderable.
5039 * @return assign_grading_summary renderable object
5041 public function get_assign_grading_summary_renderable() {
5043 $instance = $this->get_instance();
5045 $draft = ASSIGN_SUBMISSION_STATUS_DRAFT;
5046 $submitted = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
5048 $activitygroup = groups_get_activity_group($this->get_course_module());
5050 if ($instance->teamsubmission) {
5051 $defaultteammembers = $this->get_submission_group_members(0, true);
5052 $warnofungroupedusers = (count($defaultteammembers) > 0 && $instance->preventsubmissionnotingroup);
5054 $summary = new assign_grading_summary($this->count_teams($activitygroup),
5055 $instance->submissiondrafts,
5056 $this->count_submissions_with_status($draft),
5057 $this->is_any_submission_plugin_enabled(),
5058 $this->count_submissions_with_status($submitted),
5059 $instance->cutoffdate,
5060 $instance->duedate,
5061 $this->get_course_module()->id,
5062 $this->count_submissions_need_grading(),
5063 $instance->teamsubmission,
5064 $warnofungroupedusers);
5065 } else {
5066 // The active group has already been updated in groups_print_activity_menu().
5067 $countparticipants = $this->count_participants($activitygroup);
5068 $summary = new assign_grading_summary($countparticipants,
5069 $instance->submissiondrafts,
5070 $this->count_submissions_with_status($draft),
5071 $this->is_any_submission_plugin_enabled(),
5072 $this->count_submissions_with_status($submitted),
5073 $instance->cutoffdate,
5074 $instance->duedate,
5075 $this->get_course_module()->id,
5076 $this->count_submissions_need_grading(),
5077 $instance->teamsubmission,
5078 false);
5082 return $summary;
5086 * View submissions page (contains details of current submission).
5088 * @return string
5090 protected function view_submission_page() {
5091 global $CFG, $DB, $USER, $PAGE;
5093 $instance = $this->get_instance();
5095 $o = '';
5097 $postfix = '';
5098 if ($this->has_visible_attachments()) {
5099 $postfix = $this->render_area_files('mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0);
5101 $o .= $this->get_renderer()->render(new assign_header($instance,
5102 $this->get_context(),
5103 $this->show_intro(),
5104 $this->get_course_module()->id,
5105 '', '', $postfix));
5107 // Display plugin specific headers.
5108 $plugins = array_merge($this->get_submission_plugins(), $this->get_feedback_plugins());
5109 foreach ($plugins as $plugin) {
5110 if ($plugin->is_enabled() && $plugin->is_visible()) {
5111 $o .= $this->get_renderer()->render(new assign_plugin_header($plugin));
5115 if ($this->can_view_grades()) {
5116 // Group selector will only be displayed if necessary.
5117 $currenturl = new moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id));
5118 $o .= groups_print_activity_menu($this->get_course_module(), $currenturl->out(), true);
5120 $summary = $this->get_assign_grading_summary_renderable();
5121 $o .= $this->get_renderer()->render($summary);
5123 $grade = $this->get_user_grade($USER->id, false);
5124 $submission = $this->get_user_submission($USER->id, false);
5126 if ($this->can_view_submission($USER->id)) {
5127 $o .= $this->view_student_summary($USER, true);
5130 $o .= $this->view_footer();
5132 \mod_assign\event\submission_status_viewed::create_from_assign($this)->trigger();
5134 return $o;
5138 * Convert the final raw grade(s) in the grading table for the gradebook.
5140 * @param stdClass $grade
5141 * @return array
5143 protected function convert_grade_for_gradebook(stdClass $grade) {
5144 $gradebookgrade = array();
5145 if ($grade->grade >= 0) {
5146 $gradebookgrade['rawgrade'] = $grade->grade;
5148 // Allow "no grade" to be chosen.
5149 if ($grade->grade == -1) {
5150 $gradebookgrade['rawgrade'] = NULL;
5152 $gradebookgrade['userid'] = $grade->userid;
5153 $gradebookgrade['usermodified'] = $grade->grader;
5154 $gradebookgrade['datesubmitted'] = null;
5155 $gradebookgrade['dategraded'] = $grade->timemodified;
5156 if (isset($grade->feedbackformat)) {
5157 $gradebookgrade['feedbackformat'] = $grade->feedbackformat;
5159 if (isset($grade->feedbacktext)) {
5160 $gradebookgrade['feedback'] = $grade->feedbacktext;
5163 return $gradebookgrade;
5167 * Convert submission details for the gradebook.
5169 * @param stdClass $submission
5170 * @return array
5172 protected function convert_submission_for_gradebook(stdClass $submission) {
5173 $gradebookgrade = array();
5175 $gradebookgrade['userid'] = $submission->userid;
5176 $gradebookgrade['usermodified'] = $submission->userid;
5177 $gradebookgrade['datesubmitted'] = $submission->timemodified;
5179 return $gradebookgrade;
5183 * Update grades in the gradebook.
5185 * @param mixed $submission stdClass|null
5186 * @param mixed $grade stdClass|null
5187 * @return bool
5189 protected function gradebook_item_update($submission=null, $grade=null) {
5190 global $CFG;
5192 require_once($CFG->dirroot.'/mod/assign/lib.php');
5193 // Do not push grade to gradebook if blind marking is active as
5194 // the gradebook would reveal the students.
5195 if ($this->is_blind_marking()) {
5196 return false;
5199 // If marking workflow is enabled and grade is not released then remove any grade that may exist in the gradebook.
5200 if ($this->get_instance()->markingworkflow && !empty($grade) &&
5201 $this->get_grading_status($grade->userid) != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
5202 // Remove the grade (if it exists) from the gradebook as it is not 'final'.
5203 $grade->grade = -1;
5204 $grade->feedbacktext = '';
5207 if ($submission != null) {
5208 if ($submission->userid == 0) {
5209 // This is a group submission update.
5210 $team = groups_get_members($submission->groupid, 'u.id');
5212 foreach ($team as $member) {
5213 $membersubmission = clone $submission;
5214 $membersubmission->groupid = 0;
5215 $membersubmission->userid = $member->id;
5216 $this->gradebook_item_update($membersubmission, null);
5218 return;
5221 $gradebookgrade = $this->convert_submission_for_gradebook($submission);
5223 } else {
5224 $gradebookgrade = $this->convert_grade_for_gradebook($grade);
5226 // Grading is disabled, return.
5227 if ($this->grading_disabled($gradebookgrade['userid'])) {
5228 return false;
5230 $assign = clone $this->get_instance();
5231 $assign->cmidnumber = $this->get_course_module()->idnumber;
5232 // Set assign gradebook feedback plugin status (enabled and visible).
5233 $assign->gradefeedbackenabled = $this->is_gradebook_feedback_enabled();
5234 return assign_grade_item_update($assign, $gradebookgrade) == GRADE_UPDATE_OK;
5238 * Update team submission.
5240 * @param stdClass $submission
5241 * @param int $userid
5242 * @param bool $updatetime
5243 * @return bool
5245 protected function update_team_submission(stdClass $submission, $userid, $updatetime) {
5246 global $DB;
5248 if ($updatetime) {
5249 $submission->timemodified = time();
5252 // First update the submission for the current user.
5253 $mysubmission = $this->get_user_submission($userid, true, $submission->attemptnumber);
5254 $mysubmission->status = $submission->status;
5256 $this->update_submission($mysubmission, 0, $updatetime, false);
5258 // Now check the team settings to see if this assignment qualifies as submitted or draft.
5259 $team = $this->get_submission_group_members($submission->groupid, true);
5261 $allsubmitted = true;
5262 $anysubmitted = false;
5263 $result = true;
5264 if ($submission->status != ASSIGN_SUBMISSION_STATUS_REOPENED) {
5265 foreach ($team as $member) {
5266 $membersubmission = $this->get_user_submission($member->id, false, $submission->attemptnumber);
5268 // If no submission found for team member and member is active then everyone has not submitted.
5269 if (!$membersubmission || $membersubmission->status != ASSIGN_SUBMISSION_STATUS_SUBMITTED
5270 && ($this->is_active_user($member->id))) {
5271 $allsubmitted = false;
5272 if ($anysubmitted) {
5273 break;
5275 } else {
5276 $anysubmitted = true;
5279 if ($this->get_instance()->requireallteammemberssubmit) {
5280 if ($allsubmitted) {
5281 $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
5282 } else {
5283 $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
5285 $result = $DB->update_record('assign_submission', $submission);
5286 } else {
5287 if ($anysubmitted) {
5288 $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
5289 } else {
5290 $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
5292 $result = $DB->update_record('assign_submission', $submission);
5294 } else {
5295 // Set the group submission to reopened.
5296 foreach ($team as $member) {
5297 $membersubmission = $this->get_user_submission($member->id, true, $submission->attemptnumber);
5298 $membersubmission->status = ASSIGN_SUBMISSION_STATUS_REOPENED;
5299 $result = $DB->update_record('assign_submission', $membersubmission) && $result;
5301 $result = $DB->update_record('assign_submission', $submission) && $result;
5304 $this->gradebook_item_update($submission);
5305 return $result;
5309 * Update grades in the gradebook based on submission time.
5311 * @param stdClass $submission
5312 * @param int $userid
5313 * @param bool $updatetime
5314 * @param bool $teamsubmission
5315 * @return bool
5317 protected function update_submission(stdClass $submission, $userid, $updatetime, $teamsubmission) {
5318 global $DB;
5320 if ($teamsubmission) {
5321 return $this->update_team_submission($submission, $userid, $updatetime);
5324 if ($updatetime) {
5325 $submission->timemodified = time();
5327 $result= $DB->update_record('assign_submission', $submission);
5328 if ($result) {
5329 $this->gradebook_item_update($submission);
5331 return $result;
5335 * Is this assignment open for submissions?
5337 * Check the due date,
5338 * prevent late submissions,
5339 * has this person already submitted,
5340 * is the assignment locked?
5342 * @param int $userid - Optional userid so we can see if a different user can submit
5343 * @param bool $skipenrolled - Skip enrollment checks (because they have been done already)
5344 * @param stdClass $submission - Pre-fetched submission record (or false to fetch it)
5345 * @param stdClass $flags - Pre-fetched user flags record (or false to fetch it)
5346 * @param stdClass $gradinginfo - Pre-fetched user gradinginfo record (or false to fetch it)
5347 * @return bool
5349 public function submissions_open($userid = 0,
5350 $skipenrolled = false,
5351 $submission = false,
5352 $flags = false,
5353 $gradinginfo = false) {
5354 global $USER;
5356 if (!$userid) {
5357 $userid = $USER->id;
5360 $time = time();
5361 $dateopen = true;
5362 $finaldate = false;
5363 if ($this->get_instance()->cutoffdate) {
5364 $finaldate = $this->get_instance()->cutoffdate;
5367 if ($flags === false) {
5368 $flags = $this->get_user_flags($userid, false);
5370 if ($flags && $flags->locked) {
5371 return false;
5374 // User extensions.
5375 if ($finaldate) {
5376 if ($flags && $flags->extensionduedate) {
5377 // Extension can be before cut off date.
5378 if ($flags->extensionduedate > $finaldate) {
5379 $finaldate = $flags->extensionduedate;
5384 if ($finaldate) {
5385 $dateopen = ($this->get_instance()->allowsubmissionsfromdate <= $time && $time <= $finaldate);
5386 } else {
5387 $dateopen = ($this->get_instance()->allowsubmissionsfromdate <= $time);
5390 if (!$dateopen) {
5391 return false;
5394 // Now check if this user has already submitted etc.
5395 if (!$skipenrolled && !is_enrolled($this->get_course_context(), $userid)) {
5396 return false;
5398 // Note you can pass null for submission and it will not be fetched.
5399 if ($submission === false) {
5400 if ($this->get_instance()->teamsubmission) {
5401 $submission = $this->get_group_submission($userid, 0, false);
5402 } else {
5403 $submission = $this->get_user_submission($userid, false);
5406 if ($submission) {
5408 if ($this->get_instance()->submissiondrafts && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
5409 // Drafts are tracked and the student has submitted the assignment.
5410 return false;
5414 // See if this user grade is locked in the gradebook.
5415 if ($gradinginfo === false) {
5416 $gradinginfo = grade_get_grades($this->get_course()->id,
5417 'mod',
5418 'assign',
5419 $this->get_instance()->id,
5420 array($userid));
5422 if ($gradinginfo &&
5423 isset($gradinginfo->items[0]->grades[$userid]) &&
5424 $gradinginfo->items[0]->grades[$userid]->locked) {
5425 return false;
5428 return true;
5432 * Render the files in file area.
5434 * @param string $component
5435 * @param string $area
5436 * @param int $submissionid
5437 * @return string
5439 public function render_area_files($component, $area, $submissionid) {
5440 global $USER;
5442 return $this->get_renderer()->assign_files($this->context, $submissionid, $area, $component);
5447 * Capability check to make sure this grader can edit this submission.
5449 * @param int $userid - The user whose submission is to be edited
5450 * @param int $graderid (optional) - The user who will do the editing (default to $USER->id).
5451 * @return bool
5453 public function can_edit_submission($userid, $graderid = 0) {
5454 global $USER;
5456 if (empty($graderid)) {
5457 $graderid = $USER->id;
5460 $instance = $this->get_instance();
5461 if ($userid == $graderid &&
5462 $instance->teamsubmission &&
5463 $instance->preventsubmissionnotingroup &&
5464 $this->get_submission_group($userid) == false) {
5465 return false;
5468 if ($userid == $graderid &&
5469 $this->submissions_open($userid) &&
5470 has_capability('mod/assign:submit', $this->context, $graderid)) {
5471 // User can edit their own submission.
5472 return true;
5475 if (!has_capability('mod/assign:editothersubmission', $this->context, $graderid)) {
5476 return false;
5479 $cm = $this->get_course_module();
5480 if (groups_get_activity_groupmode($cm) == SEPARATEGROUPS) {
5481 $sharedgroupmembers = $this->get_shared_group_members($cm, $graderid);
5482 return in_array($userid, $sharedgroupmembers);
5484 return true;
5488 * Returns IDs of the users who share group membership with the specified user.
5490 * @param stdClass|cm_info $cm Course-module
5491 * @param int $userid User ID
5492 * @return array An array of ID of users.
5494 public function get_shared_group_members($cm, $userid) {
5495 if (!isset($this->sharedgroupmembers[$userid])) {
5496 $this->sharedgroupmembers[$userid] = array();
5497 $groupsids = array_keys(groups_get_activity_allowed_groups($cm, $userid));
5498 foreach ($groupsids as $groupid) {
5499 $members = array_keys(groups_get_members($groupid, 'u.id'));
5500 $this->sharedgroupmembers[$userid] = array_merge($this->sharedgroupmembers[$userid], $members);
5504 return $this->sharedgroupmembers[$userid];
5508 * Returns a list of teachers that should be grading given submission.
5510 * @param int $userid The submission to grade
5511 * @return array
5513 protected function get_graders($userid) {
5514 // Potential graders should be active users only.
5515 $potentialgraders = get_enrolled_users($this->context, "mod/assign:grade", null, 'u.*', null, null, null, true);
5517 $graders = array();
5518 if (groups_get_activity_groupmode($this->get_course_module()) == SEPARATEGROUPS) {
5519 if ($groups = groups_get_all_groups($this->get_course()->id, $userid, $this->get_course_module()->groupingid)) {
5520 foreach ($groups as $group) {
5521 foreach ($potentialgraders as $grader) {
5522 if ($grader->id == $userid) {
5523 // Do not send self.
5524 continue;
5526 if (groups_is_member($group->id, $grader->id)) {
5527 $graders[$grader->id] = $grader;
5531 } else {
5532 // User not in group, try to find graders without group.
5533 foreach ($potentialgraders as $grader) {
5534 if ($grader->id == $userid) {
5535 // Do not send self.
5536 continue;
5538 if (!groups_has_membership($this->get_course_module(), $grader->id)) {
5539 $graders[$grader->id] = $grader;
5543 } else {
5544 foreach ($potentialgraders as $grader) {
5545 if ($grader->id == $userid) {
5546 // Do not send self.
5547 continue;
5549 // Must be enrolled.
5550 if (is_enrolled($this->get_course_context(), $grader->id)) {
5551 $graders[$grader->id] = $grader;
5555 return $graders;
5559 * Returns a list of users that should receive notification about given submission.
5561 * @param int $userid The submission to grade
5562 * @return array
5564 protected function get_notifiable_users($userid) {
5565 // Potential users should be active users only.
5566 $potentialusers = get_enrolled_users($this->context, "mod/assign:receivegradernotifications",
5567 null, 'u.*', null, null, null, true);
5569 $notifiableusers = array();
5570 if (groups_get_activity_groupmode($this->get_course_module()) == SEPARATEGROUPS) {
5571 if ($groups = groups_get_all_groups($this->get_course()->id, $userid, $this->get_course_module()->groupingid)) {
5572 foreach ($groups as $group) {
5573 foreach ($potentialusers as $potentialuser) {
5574 if ($potentialuser->id == $userid) {
5575 // Do not send self.
5576 continue;
5578 if (groups_is_member($group->id, $potentialuser->id)) {
5579 $notifiableusers[$potentialuser->id] = $potentialuser;
5583 } else {
5584 // User not in group, try to find graders without group.
5585 foreach ($potentialusers as $potentialuser) {
5586 if ($potentialuser->id == $userid) {
5587 // Do not send self.
5588 continue;
5590 if (!groups_has_membership($this->get_course_module(), $potentialuser->id)) {
5591 $notifiableusers[$potentialuser->id] = $potentialuser;
5595 } else {
5596 foreach ($potentialusers as $potentialuser) {
5597 if ($potentialuser->id == $userid) {
5598 // Do not send self.
5599 continue;
5601 // Must be enrolled.
5602 if (is_enrolled($this->get_course_context(), $potentialuser->id)) {
5603 $notifiableusers[$potentialuser->id] = $potentialuser;
5607 return $notifiableusers;
5611 * Format a notification for plain text.
5613 * @param string $messagetype
5614 * @param stdClass $info
5615 * @param stdClass $course
5616 * @param stdClass $context
5617 * @param string $modulename
5618 * @param string $assignmentname
5620 protected static function format_notification_message_text($messagetype,
5621 $info,
5622 $course,
5623 $context,
5624 $modulename,
5625 $assignmentname) {
5626 $formatparams = array('context' => $context->get_course_context());
5627 $posttext = format_string($course->shortname, true, $formatparams) .
5628 ' -> ' .
5629 $modulename .
5630 ' -> ' .
5631 format_string($assignmentname, true, $formatparams) . "\n";
5632 $posttext .= '---------------------------------------------------------------------' . "\n";
5633 $posttext .= get_string($messagetype . 'text', 'assign', $info)."\n";
5634 $posttext .= "\n---------------------------------------------------------------------\n";
5635 return $posttext;
5639 * Format a notification for HTML.
5641 * @param string $messagetype
5642 * @param stdClass $info
5643 * @param stdClass $course
5644 * @param stdClass $context
5645 * @param string $modulename
5646 * @param stdClass $coursemodule
5647 * @param string $assignmentname
5649 protected static function format_notification_message_html($messagetype,
5650 $info,
5651 $course,
5652 $context,
5653 $modulename,
5654 $coursemodule,
5655 $assignmentname) {
5656 global $CFG;
5657 $formatparams = array('context' => $context->get_course_context());
5658 $posthtml = '<p><font face="sans-serif">' .
5659 '<a href="' . $CFG->wwwroot . '/course/view.php?id=' . $course->id . '">' .
5660 format_string($course->shortname, true, $formatparams) .
5661 '</a> ->' .
5662 '<a href="' . $CFG->wwwroot . '/mod/assign/index.php?id=' . $course->id . '">' .
5663 $modulename .
5664 '</a> ->' .
5665 '<a href="' . $CFG->wwwroot . '/mod/assign/view.php?id=' . $coursemodule->id . '">' .
5666 format_string($assignmentname, true, $formatparams) .
5667 '</a></font></p>';
5668 $posthtml .= '<hr /><font face="sans-serif">';
5669 $posthtml .= '<p>' . get_string($messagetype . 'html', 'assign', $info) . '</p>';
5670 $posthtml .= '</font><hr />';
5671 return $posthtml;
5675 * Message someone about something (static so it can be called from cron).
5677 * @param stdClass $userfrom
5678 * @param stdClass $userto
5679 * @param string $messagetype
5680 * @param string $eventtype
5681 * @param int $updatetime
5682 * @param stdClass $coursemodule
5683 * @param stdClass $context
5684 * @param stdClass $course
5685 * @param string $modulename
5686 * @param string $assignmentname
5687 * @param bool $blindmarking
5688 * @param int $uniqueidforuser
5689 * @return void
5691 public static function send_assignment_notification($userfrom,
5692 $userto,
5693 $messagetype,
5694 $eventtype,
5695 $updatetime,
5696 $coursemodule,
5697 $context,
5698 $course,
5699 $modulename,
5700 $assignmentname,
5701 $blindmarking,
5702 $uniqueidforuser) {
5703 global $CFG;
5705 $info = new stdClass();
5706 if ($blindmarking) {
5707 $userfrom = clone($userfrom);
5708 $info->username = get_string('participant', 'assign') . ' ' . $uniqueidforuser;
5709 $userfrom->firstname = get_string('participant', 'assign');
5710 $userfrom->lastname = $uniqueidforuser;
5711 $userfrom->email = $CFG->noreplyaddress;
5712 } else {
5713 $info->username = fullname($userfrom, true);
5715 $info->assignment = format_string($assignmentname, true, array('context'=>$context));
5716 $info->url = $CFG->wwwroot.'/mod/assign/view.php?id='.$coursemodule->id;
5717 $info->timeupdated = userdate($updatetime, get_string('strftimerecentfull'));
5719 $postsubject = get_string($messagetype . 'small', 'assign', $info);
5720 $posttext = self::format_notification_message_text($messagetype,
5721 $info,
5722 $course,
5723 $context,
5724 $modulename,
5725 $assignmentname);
5726 $posthtml = '';
5727 if ($userto->mailformat == 1) {
5728 $posthtml = self::format_notification_message_html($messagetype,
5729 $info,
5730 $course,
5731 $context,
5732 $modulename,
5733 $coursemodule,
5734 $assignmentname);
5737 $eventdata = new \core\message\message();
5738 $eventdata->courseid = $course->id;
5739 $eventdata->modulename = 'assign';
5740 $eventdata->userfrom = $userfrom;
5741 $eventdata->userto = $userto;
5742 $eventdata->subject = $postsubject;
5743 $eventdata->fullmessage = $posttext;
5744 $eventdata->fullmessageformat = FORMAT_PLAIN;
5745 $eventdata->fullmessagehtml = $posthtml;
5746 $eventdata->smallmessage = $postsubject;
5748 $eventdata->name = $eventtype;
5749 $eventdata->component = 'mod_assign';
5750 $eventdata->notification = 1;
5751 $eventdata->contexturl = $info->url;
5752 $eventdata->contexturlname = $info->assignment;
5754 message_send($eventdata);
5758 * Message someone about something.
5760 * @param stdClass $userfrom
5761 * @param stdClass $userto
5762 * @param string $messagetype
5763 * @param string $eventtype
5764 * @param int $updatetime
5765 * @return void
5767 public function send_notification($userfrom, $userto, $messagetype, $eventtype, $updatetime) {
5768 global $USER;
5769 $userid = core_user::is_real_user($userfrom->id) ? $userfrom->id : $USER->id;
5770 $uniqueid = $this->get_uniqueid_for_user($userid);
5771 self::send_assignment_notification($userfrom,
5772 $userto,
5773 $messagetype,
5774 $eventtype,
5775 $updatetime,
5776 $this->get_course_module(),
5777 $this->get_context(),
5778 $this->get_course(),
5779 $this->get_module_name(),
5780 $this->get_instance()->name,
5781 $this->is_blind_marking(),
5782 $uniqueid);
5786 * Notify student upon successful submission copy.
5788 * @param stdClass $submission
5789 * @return void
5791 protected function notify_student_submission_copied(stdClass $submission) {
5792 global $DB, $USER;
5794 $adminconfig = $this->get_admin_config();
5795 // Use the same setting for this - no need for another one.
5796 if (empty($adminconfig->submissionreceipts)) {
5797 // No need to do anything.
5798 return;
5800 if ($submission->userid) {
5801 $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST);
5802 } else {
5803 $user = $USER;
5805 $this->send_notification($user,
5806 $user,
5807 'submissioncopied',
5808 'assign_notification',
5809 $submission->timemodified);
5812 * Notify student upon successful submission.
5814 * @param stdClass $submission
5815 * @return void
5817 protected function notify_student_submission_receipt(stdClass $submission) {
5818 global $DB, $USER;
5820 $adminconfig = $this->get_admin_config();
5821 if (empty($adminconfig->submissionreceipts)) {
5822 // No need to do anything.
5823 return;
5825 if ($submission->userid) {
5826 $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST);
5827 } else {
5828 $user = $USER;
5830 if ($submission->userid == $USER->id) {
5831 $this->send_notification(core_user::get_noreply_user(),
5832 $user,
5833 'submissionreceipt',
5834 'assign_notification',
5835 $submission->timemodified);
5836 } else {
5837 $this->send_notification($USER,
5838 $user,
5839 'submissionreceiptother',
5840 'assign_notification',
5841 $submission->timemodified);
5846 * Send notifications to graders upon student submissions.
5848 * @param stdClass $submission
5849 * @return void
5851 protected function notify_graders(stdClass $submission) {
5852 global $DB, $USER;
5854 $instance = $this->get_instance();
5856 $late = $instance->duedate && ($instance->duedate < time());
5858 if (!$instance->sendnotifications && !($late && $instance->sendlatenotifications)) {
5859 // No need to do anything.
5860 return;
5863 if ($submission->userid) {
5864 $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST);
5865 } else {
5866 $user = $USER;
5869 if ($notifyusers = $this->get_notifiable_users($user->id)) {
5870 foreach ($notifyusers as $notifyuser) {
5871 $this->send_notification($user,
5872 $notifyuser,
5873 'gradersubmissionupdated',
5874 'assign_notification',
5875 $submission->timemodified);
5881 * Submit a submission for grading.
5883 * @param stdClass $data - The form data
5884 * @param array $notices - List of error messages to display on an error condition.
5885 * @return bool Return false if the submission was not submitted.
5887 public function submit_for_grading($data, $notices) {
5888 global $USER;
5890 $userid = $USER->id;
5891 if (!empty($data->userid)) {
5892 $userid = $data->userid;
5894 // Need submit permission to submit an assignment.
5895 if ($userid == $USER->id) {
5896 require_capability('mod/assign:submit', $this->context);
5897 } else {
5898 if (!$this->can_edit_submission($userid, $USER->id)) {
5899 print_error('nopermission');
5903 $instance = $this->get_instance();
5905 if ($instance->teamsubmission) {
5906 $submission = $this->get_group_submission($userid, 0, true);
5907 } else {
5908 $submission = $this->get_user_submission($userid, true);
5911 if (!$this->submissions_open($userid)) {
5912 $notices[] = get_string('submissionsclosed', 'assign');
5913 return false;
5916 if ($instance->requiresubmissionstatement && empty($data->submissionstatement) && $USER->id == $userid) {
5917 return false;
5920 if ($submission->status != ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
5921 // Give each submission plugin a chance to process the submission.
5922 $plugins = $this->get_submission_plugins();
5923 foreach ($plugins as $plugin) {
5924 if ($plugin->is_enabled() && $plugin->is_visible()) {
5925 $plugin->submit_for_grading($submission);
5929 $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
5930 $this->update_submission($submission, $userid, true, $instance->teamsubmission);
5931 $completion = new completion_info($this->get_course());
5932 if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
5933 $this->update_activity_completion_records($instance->teamsubmission,
5934 $instance->requireallteammemberssubmit,
5935 $submission,
5936 $userid,
5937 COMPLETION_COMPLETE,
5938 $completion);
5941 if (!empty($data->submissionstatement) && $USER->id == $userid) {
5942 \mod_assign\event\statement_accepted::create_from_submission($this, $submission)->trigger();
5944 $this->notify_graders($submission);
5945 $this->notify_student_submission_receipt($submission);
5947 \mod_assign\event\assessable_submitted::create_from_submission($this, $submission, false)->trigger();
5949 return true;
5951 $notices[] = get_string('submissionsclosed', 'assign');
5952 return false;
5956 * A students submission is submitted for grading by a teacher.
5958 * @return bool
5960 protected function process_submit_other_for_grading($mform, $notices) {
5961 global $USER, $CFG;
5963 require_sesskey();
5965 $userid = optional_param('userid', $USER->id, PARAM_INT);
5967 if (!$this->submissions_open($userid)) {
5968 $notices[] = get_string('submissionsclosed', 'assign');
5969 return false;
5971 $data = new stdClass();
5972 $data->userid = $userid;
5973 return $this->submit_for_grading($data, $notices);
5977 * Assignment submission is processed before grading.
5979 * @param moodleform|null $mform If validation failed when submitting this form - this is the moodleform.
5980 * It can be null.
5981 * @return bool Return false if the validation fails. This affects which page is displayed next.
5983 protected function process_submit_for_grading($mform, $notices) {
5984 global $CFG;
5986 require_once($CFG->dirroot . '/mod/assign/submissionconfirmform.php');
5987 require_sesskey();
5989 if (!$this->submissions_open()) {
5990 $notices[] = get_string('submissionsclosed', 'assign');
5991 return false;
5993 $instance = $this->get_instance();
5994 $data = new stdClass();
5995 $adminconfig = $this->get_admin_config();
5996 $requiresubmissionstatement = $instance->requiresubmissionstatement &&
5997 !empty($adminconfig->submissionstatement);
5999 $submissionstatement = '';
6000 if (!empty($adminconfig->submissionstatement)) {
6001 // Format the submission statement before its sent. We turn off para because this is going within
6002 // a form element.
6003 $options = array(
6004 'context' => $this->get_context(),
6005 'para' => false
6007 $submissionstatement = format_text($adminconfig->submissionstatement, FORMAT_MOODLE, $options);
6010 if ($mform == null) {
6011 $mform = new mod_assign_confirm_submission_form(null, array($requiresubmissionstatement,
6012 $submissionstatement,
6013 $this->get_course_module()->id,
6014 $data));
6017 $data = $mform->get_data();
6018 if (!$mform->is_cancelled()) {
6019 if ($mform->get_data() == false) {
6020 return false;
6022 return $this->submit_for_grading($data, $notices);
6024 return true;
6028 * Save the extension date for a single user.
6030 * @param int $userid The user id
6031 * @param mixed $extensionduedate Either an integer date or null
6032 * @return boolean
6034 public function save_user_extension($userid, $extensionduedate) {
6035 global $DB;
6037 // Need submit permission to submit an assignment.
6038 require_capability('mod/assign:grantextension', $this->context);
6040 if (!is_enrolled($this->get_course_context(), $userid)) {
6041 return false;
6043 if (!has_capability('mod/assign:submit', $this->context, $userid)) {
6044 return false;
6047 if ($this->get_instance()->duedate && $extensionduedate) {
6048 if ($this->get_instance()->duedate > $extensionduedate) {
6049 return false;
6052 if ($this->get_instance()->allowsubmissionsfromdate && $extensionduedate) {
6053 if ($this->get_instance()->allowsubmissionsfromdate > $extensionduedate) {
6054 return false;
6058 $flags = $this->get_user_flags($userid, true);
6059 $flags->extensionduedate = $extensionduedate;
6061 $result = $this->update_user_flags($flags);
6063 if ($result) {
6064 \mod_assign\event\extension_granted::create_from_assign($this, $userid)->trigger();
6066 return $result;
6070 * Save extension date.
6072 * @param moodleform $mform The submitted form
6073 * @return boolean
6075 protected function process_save_extension(& $mform) {
6076 global $DB, $CFG;
6078 // Include extension form.
6079 require_once($CFG->dirroot . '/mod/assign/extensionform.php');
6080 require_sesskey();
6082 $users = optional_param('userid', 0, PARAM_INT);
6083 if (!$users) {
6084 $users = required_param('selectedusers', PARAM_SEQUENCE);
6086 $userlist = explode(',', $users);
6088 $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate');
6089 $maxoverride = array('allowsubmissionsfromdate' => 0, 'duedate' => 0, 'cutoffdate' => 0);
6090 foreach ($userlist as $userid) {
6091 // To validate extension date with users overrides.
6092 $override = $this->override_exists($userid);
6093 foreach ($keys as $key) {
6094 if ($override->{$key}) {
6095 if ($maxoverride[$key] < $override->{$key}) {
6096 $maxoverride[$key] = $override->{$key};
6098 } else if ($maxoverride[$key] < $this->get_instance()->{$key}) {
6099 $maxoverride[$key] = $this->get_instance()->{$key};
6103 foreach ($keys as $key) {
6104 if ($maxoverride[$key]) {
6105 $this->get_instance()->{$key} = $maxoverride[$key];
6109 $formparams = array(
6110 'instance' => $this->get_instance(),
6111 'assign' => $this,
6112 'userlist' => $userlist
6115 $mform = new mod_assign_extension_form(null, $formparams);
6117 if ($mform->is_cancelled()) {
6118 return true;
6121 if ($formdata = $mform->get_data()) {
6122 if (!empty($formdata->selectedusers)) {
6123 $users = explode(',', $formdata->selectedusers);
6124 $result = true;
6125 foreach ($users as $userid) {
6126 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
6127 $result = $this->save_user_extension($user->id, $formdata->extensionduedate) && $result;
6129 return $result;
6131 if (!empty($formdata->userid)) {
6132 $user = $DB->get_record('user', array('id' => $formdata->userid), '*', MUST_EXIST);
6133 return $this->save_user_extension($user->id, $formdata->extensionduedate);
6137 return false;
6141 * Save quick grades.
6143 * @return string The result of the save operation
6145 protected function process_save_quick_grades() {
6146 global $USER, $DB, $CFG;
6148 // Need grade permission.
6149 require_capability('mod/assign:grade', $this->context);
6150 require_sesskey();
6152 // Make sure advanced grading is disabled.
6153 $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
6154 $controller = $gradingmanager->get_active_controller();
6155 if (!empty($controller)) {
6156 return get_string('errorquickgradingvsadvancedgrading', 'assign');
6159 $users = array();
6160 // First check all the last modified values.
6161 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
6162 $participants = $this->list_participants($currentgroup, true);
6164 // Gets a list of possible users and look for values based upon that.
6165 foreach ($participants as $userid => $unused) {
6166 $modified = optional_param('grademodified_' . $userid, -1, PARAM_INT);
6167 $attemptnumber = optional_param('gradeattempt_' . $userid, -1, PARAM_INT);
6168 // Gather the userid, updated grade and last modified value.
6169 $record = new stdClass();
6170 $record->userid = $userid;
6171 if ($modified >= 0) {
6172 $record->grade = unformat_float(optional_param('quickgrade_' . $record->userid, -1, PARAM_TEXT));
6173 $record->workflowstate = optional_param('quickgrade_' . $record->userid.'_workflowstate', false, PARAM_TEXT);
6174 $record->allocatedmarker = optional_param('quickgrade_' . $record->userid.'_allocatedmarker', false, PARAM_INT);
6175 } else {
6176 // This user was not in the grading table.
6177 continue;
6179 $record->attemptnumber = $attemptnumber;
6180 $record->lastmodified = $modified;
6181 $record->gradinginfo = grade_get_grades($this->get_course()->id,
6182 'mod',
6183 'assign',
6184 $this->get_instance()->id,
6185 array($userid));
6186 $users[$userid] = $record;
6189 if (empty($users)) {
6190 return get_string('nousersselected', 'assign');
6193 list($userids, $params) = $DB->get_in_or_equal(array_keys($users), SQL_PARAMS_NAMED);
6194 $params['assignid1'] = $this->get_instance()->id;
6195 $params['assignid2'] = $this->get_instance()->id;
6197 // Check them all for currency.
6198 $grademaxattempt = 'SELECT s.userid, s.attemptnumber AS maxattempt
6199 FROM {assign_submission} s
6200 WHERE s.assignment = :assignid1 AND s.latest = 1';
6202 $sql = 'SELECT u.id AS userid, g.grade AS grade, g.timemodified AS lastmodified,
6203 uf.workflowstate, uf.allocatedmarker, gmx.maxattempt AS attemptnumber
6204 FROM {user} u
6205 LEFT JOIN ( ' . $grademaxattempt . ' ) gmx ON u.id = gmx.userid
6206 LEFT JOIN {assign_grades} g ON
6207 u.id = g.userid AND
6208 g.assignment = :assignid2 AND
6209 g.attemptnumber = gmx.maxattempt
6210 LEFT JOIN {assign_user_flags} uf ON uf.assignment = g.assignment AND uf.userid = g.userid
6211 WHERE u.id ' . $userids;
6212 $currentgrades = $DB->get_recordset_sql($sql, $params);
6214 $modifiedusers = array();
6215 foreach ($currentgrades as $current) {
6216 $modified = $users[(int)$current->userid];
6217 $grade = $this->get_user_grade($modified->userid, false);
6218 // Check to see if the grade column was even visible.
6219 $gradecolpresent = optional_param('quickgrade_' . $modified->userid, false, PARAM_INT) !== false;
6221 // Check to see if the outcomes were modified.
6222 if ($CFG->enableoutcomes) {
6223 foreach ($modified->gradinginfo->outcomes as $outcomeid => $outcome) {
6224 $oldoutcome = $outcome->grades[$modified->userid]->grade;
6225 $paramname = 'outcome_' . $outcomeid . '_' . $modified->userid;
6226 $newoutcome = optional_param($paramname, -1, PARAM_FLOAT);
6227 // Check to see if the outcome column was even visible.
6228 $outcomecolpresent = optional_param($paramname, false, PARAM_FLOAT) !== false;
6229 if ($outcomecolpresent && ($oldoutcome != $newoutcome)) {
6230 // Can't check modified time for outcomes because it is not reported.
6231 $modifiedusers[$modified->userid] = $modified;
6232 continue;
6237 // Let plugins participate.
6238 foreach ($this->feedbackplugins as $plugin) {
6239 if ($plugin->is_visible() && $plugin->is_enabled() && $plugin->supports_quickgrading()) {
6240 // The plugins must handle is_quickgrading_modified correctly - ie
6241 // handle hidden columns.
6242 if ($plugin->is_quickgrading_modified($modified->userid, $grade)) {
6243 if ((int)$current->lastmodified > (int)$modified->lastmodified) {
6244 return get_string('errorrecordmodified', 'assign');
6245 } else {
6246 $modifiedusers[$modified->userid] = $modified;
6247 continue;
6253 if (($current->grade < 0 || $current->grade === null) &&
6254 ($modified->grade < 0 || $modified->grade === null)) {
6255 // Different ways to indicate no grade.
6256 $modified->grade = $current->grade; // Keep existing grade.
6258 // Treat 0 and null as different values.
6259 if ($current->grade !== null) {
6260 $current->grade = floatval($current->grade);
6262 $gradechanged = $gradecolpresent && grade_floats_different($current->grade, $modified->grade);
6263 $markingallocationchanged = $this->get_instance()->markingworkflow &&
6264 $this->get_instance()->markingallocation &&
6265 ($modified->allocatedmarker !== false) &&
6266 ($current->allocatedmarker != $modified->allocatedmarker);
6267 $workflowstatechanged = $this->get_instance()->markingworkflow &&
6268 ($modified->workflowstate !== false) &&
6269 ($current->workflowstate != $modified->workflowstate);
6270 if ($gradechanged || $markingallocationchanged || $workflowstatechanged) {
6271 // Grade changed.
6272 if ($this->grading_disabled($modified->userid)) {
6273 continue;
6275 $badmodified = (int)$current->lastmodified > (int)$modified->lastmodified;
6276 $badattempt = (int)$current->attemptnumber != (int)$modified->attemptnumber;
6277 if ($badmodified || $badattempt) {
6278 // Error - record has been modified since viewing the page.
6279 return get_string('errorrecordmodified', 'assign');
6280 } else {
6281 $modifiedusers[$modified->userid] = $modified;
6286 $currentgrades->close();
6288 $adminconfig = $this->get_admin_config();
6289 $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
6291 // Ok - ready to process the updates.
6292 foreach ($modifiedusers as $userid => $modified) {
6293 $grade = $this->get_user_grade($userid, true);
6294 $flags = $this->get_user_flags($userid, true);
6295 $grade->grade= grade_floatval(unformat_float($modified->grade));
6296 $grade->grader= $USER->id;
6297 $gradecolpresent = optional_param('quickgrade_' . $userid, false, PARAM_INT) !== false;
6299 // Save plugins data.
6300 foreach ($this->feedbackplugins as $plugin) {
6301 if ($plugin->is_visible() && $plugin->is_enabled() && $plugin->supports_quickgrading()) {
6302 $plugin->save_quickgrading_changes($userid, $grade);
6303 if (('assignfeedback_' . $plugin->get_type()) == $gradebookplugin) {
6304 // This is the feedback plugin chose to push comments to the gradebook.
6305 $grade->feedbacktext = $plugin->text_for_gradebook($grade);
6306 $grade->feedbackformat = $plugin->format_for_gradebook($grade);
6311 // These will be set to false if they are not present in the quickgrading
6312 // form (e.g. column hidden).
6313 $workflowstatemodified = ($modified->workflowstate !== false) &&
6314 ($flags->workflowstate != $modified->workflowstate);
6316 $allocatedmarkermodified = ($modified->allocatedmarker !== false) &&
6317 ($flags->allocatedmarker != $modified->allocatedmarker);
6319 if ($workflowstatemodified) {
6320 $flags->workflowstate = $modified->workflowstate;
6322 if ($allocatedmarkermodified) {
6323 $flags->allocatedmarker = $modified->allocatedmarker;
6325 if ($workflowstatemodified || $allocatedmarkermodified) {
6326 if ($this->update_user_flags($flags) && $workflowstatemodified) {
6327 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
6328 \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $flags->workflowstate)->trigger();
6331 $this->update_grade($grade);
6333 // Allow teachers to skip sending notifications.
6334 if (optional_param('sendstudentnotifications', true, PARAM_BOOL)) {
6335 $this->notify_grade_modified($grade, true);
6338 // Save outcomes.
6339 if ($CFG->enableoutcomes) {
6340 $data = array();
6341 foreach ($modified->gradinginfo->outcomes as $outcomeid => $outcome) {
6342 $oldoutcome = $outcome->grades[$modified->userid]->grade;
6343 $paramname = 'outcome_' . $outcomeid . '_' . $modified->userid;
6344 // This will be false if the input was not in the quickgrading
6345 // form (e.g. column hidden).
6346 $newoutcome = optional_param($paramname, false, PARAM_INT);
6347 if ($newoutcome !== false && ($oldoutcome != $newoutcome)) {
6348 $data[$outcomeid] = $newoutcome;
6351 if (count($data) > 0) {
6352 grade_update_outcomes('mod/assign',
6353 $this->course->id,
6354 'mod',
6355 'assign',
6356 $this->get_instance()->id,
6357 $userid,
6358 $data);
6363 return get_string('quickgradingchangessaved', 'assign');
6367 * Reveal student identities to markers (and the gradebook).
6369 * @return void
6371 public function reveal_identities() {
6372 global $DB;
6374 require_capability('mod/assign:revealidentities', $this->context);
6376 if ($this->get_instance()->revealidentities || empty($this->get_instance()->blindmarking)) {
6377 return false;
6380 // Update the assignment record.
6381 $update = new stdClass();
6382 $update->id = $this->get_instance()->id;
6383 $update->revealidentities = 1;
6384 $DB->update_record('assign', $update);
6386 // Refresh the instance data.
6387 $this->instance = null;
6389 // Release the grades to the gradebook.
6390 // First create the column in the gradebook.
6391 $this->update_gradebook(false, $this->get_course_module()->id);
6393 // Now release all grades.
6395 $adminconfig = $this->get_admin_config();
6396 $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
6397 $gradebookplugin = str_replace('assignfeedback_', '', $gradebookplugin);
6398 $grades = $DB->get_records('assign_grades', array('assignment'=>$this->get_instance()->id));
6400 $plugin = $this->get_feedback_plugin_by_type($gradebookplugin);
6402 foreach ($grades as $grade) {
6403 // Fetch any comments for this student.
6404 if ($plugin && $plugin->is_enabled() && $plugin->is_visible()) {
6405 $grade->feedbacktext = $plugin->text_for_gradebook($grade);
6406 $grade->feedbackformat = $plugin->format_for_gradebook($grade);
6408 $this->gradebook_item_update(null, $grade);
6411 \mod_assign\event\identities_revealed::create_from_assign($this)->trigger();
6415 * Reveal student identities to markers (and the gradebook).
6417 * @return void
6419 protected function process_reveal_identities() {
6421 if (!confirm_sesskey()) {
6422 return false;
6425 return $this->reveal_identities();
6430 * Save grading options.
6432 * @return void
6434 protected function process_save_grading_options() {
6435 global $USER, $CFG;
6437 // Include grading options form.
6438 require_once($CFG->dirroot . '/mod/assign/gradingoptionsform.php');
6440 // Need submit permission to submit an assignment.
6441 $this->require_view_grades();
6442 require_sesskey();
6444 // Is advanced grading enabled?
6445 $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
6446 $controller = $gradingmanager->get_active_controller();
6447 $showquickgrading = empty($controller);
6448 if (!is_null($this->context)) {
6449 $showonlyactiveenrolopt = has_capability('moodle/course:viewsuspendedusers', $this->context);
6450 } else {
6451 $showonlyactiveenrolopt = false;
6454 $markingallocation = $this->get_instance()->markingworkflow &&
6455 $this->get_instance()->markingallocation &&
6456 has_capability('mod/assign:manageallocations', $this->context);
6457 // Get markers to use in drop lists.
6458 $markingallocationoptions = array();
6459 if ($markingallocation) {
6460 $markingallocationoptions[''] = get_string('filternone', 'assign');
6461 $markingallocationoptions[ASSIGN_MARKER_FILTER_NO_MARKER] = get_string('markerfilternomarker', 'assign');
6462 list($sort, $params) = users_order_by_sql();
6463 $markers = get_users_by_capability($this->context, 'mod/assign:grade', '', $sort);
6464 foreach ($markers as $marker) {
6465 $markingallocationoptions[$marker->id] = fullname($marker);
6469 // Get marking states to show in form.
6470 $markingworkflowoptions = array();
6471 if ($this->get_instance()->markingworkflow) {
6472 $notmarked = get_string('markingworkflowstatenotmarked', 'assign');
6473 $markingworkflowoptions[''] = get_string('filternone', 'assign');
6474 $markingworkflowoptions[ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED] = $notmarked;
6475 $markingworkflowoptions = array_merge($markingworkflowoptions, $this->get_marking_workflow_states_for_current_user());
6478 $gradingoptionsparams = array('cm'=>$this->get_course_module()->id,
6479 'contextid'=>$this->context->id,
6480 'userid'=>$USER->id,
6481 'submissionsenabled'=>$this->is_any_submission_plugin_enabled(),
6482 'showquickgrading'=>$showquickgrading,
6483 'quickgrading'=>false,
6484 'markingworkflowopt' => $markingworkflowoptions,
6485 'markingallocationopt' => $markingallocationoptions,
6486 'showonlyactiveenrolopt'=>$showonlyactiveenrolopt,
6487 'showonlyactiveenrol' => $this->show_only_active_users(),
6488 'downloadasfolders' => get_user_preferences('assign_downloadasfolders', 1));
6489 $mform = new mod_assign_grading_options_form(null, $gradingoptionsparams);
6490 if ($formdata = $mform->get_data()) {
6491 set_user_preference('assign_perpage', $formdata->perpage);
6492 if (isset($formdata->filter)) {
6493 set_user_preference('assign_filter', $formdata->filter);
6495 if (isset($formdata->markerfilter)) {
6496 set_user_preference('assign_markerfilter', $formdata->markerfilter);
6498 if (isset($formdata->workflowfilter)) {
6499 set_user_preference('assign_workflowfilter', $formdata->workflowfilter);
6501 if ($showquickgrading) {
6502 set_user_preference('assign_quickgrading', isset($formdata->quickgrading));
6504 if (isset($formdata->downloadasfolders)) {
6505 set_user_preference('assign_downloadasfolders', 1); // Enabled.
6506 } else {
6507 set_user_preference('assign_downloadasfolders', 0); // Disabled.
6509 if (!empty($showonlyactiveenrolopt)) {
6510 $showonlyactiveenrol = isset($formdata->showonlyactiveenrol);
6511 set_user_preference('grade_report_showonlyactiveenrol', $showonlyactiveenrol);
6512 $this->showonlyactiveenrol = $showonlyactiveenrol;
6518 * Take a grade object and print a short summary for the log file.
6519 * The size limit for the log file is 255 characters, so be careful not
6520 * to include too much information.
6522 * @deprecated since 2.7
6524 * @param stdClass $grade
6525 * @return string
6527 public function format_grade_for_log(stdClass $grade) {
6528 global $DB;
6530 $user = $DB->get_record('user', array('id' => $grade->userid), '*', MUST_EXIST);
6532 $info = get_string('gradestudent', 'assign', array('id'=>$user->id, 'fullname'=>fullname($user)));
6533 if ($grade->grade != '') {
6534 $info .= get_string('grade') . ': ' . $this->display_grade($grade->grade, false) . '. ';
6535 } else {
6536 $info .= get_string('nograde', 'assign');
6538 return $info;
6542 * Take a submission object and print a short summary for the log file.
6543 * The size limit for the log file is 255 characters, so be careful not
6544 * to include too much information.
6546 * @deprecated since 2.7
6548 * @param stdClass $submission
6549 * @return string
6551 public function format_submission_for_log(stdClass $submission) {
6552 global $DB;
6554 $info = '';
6555 if ($submission->userid) {
6556 $user = $DB->get_record('user', array('id' => $submission->userid), '*', MUST_EXIST);
6557 $name = fullname($user);
6558 } else {
6559 $group = $this->get_submission_group($submission->userid);
6560 if ($group) {
6561 $name = $group->name;
6562 } else {
6563 $name = get_string('defaultteam', 'assign');
6566 $status = get_string('submissionstatus_' . $submission->status, 'assign');
6567 $params = array('id'=>$submission->userid, 'fullname'=>$name, 'status'=>$status);
6568 $info .= get_string('submissionlog', 'assign', $params) . ' <br>';
6570 foreach ($this->submissionplugins as $plugin) {
6571 if ($plugin->is_enabled() && $plugin->is_visible()) {
6572 $info .= '<br>' . $plugin->format_for_log($submission);
6576 return $info;
6580 * Require a valid sess key and then call copy_previous_attempt.
6582 * @param array $notices Any error messages that should be shown
6583 * to the user at the top of the edit submission form.
6584 * @return bool
6586 protected function process_copy_previous_attempt(&$notices) {
6587 require_sesskey();
6589 return $this->copy_previous_attempt($notices);
6593 * Copy the current assignment submission from the last submitted attempt.
6595 * @param array $notices Any error messages that should be shown
6596 * to the user at the top of the edit submission form.
6597 * @return bool
6599 public function copy_previous_attempt(&$notices) {
6600 global $USER, $CFG;
6602 require_capability('mod/assign:submit', $this->context);
6604 $instance = $this->get_instance();
6605 if ($instance->teamsubmission) {
6606 $submission = $this->get_group_submission($USER->id, 0, true);
6607 } else {
6608 $submission = $this->get_user_submission($USER->id, true);
6610 if (!$submission || $submission->status != ASSIGN_SUBMISSION_STATUS_REOPENED) {
6611 $notices[] = get_string('submissionnotcopiedinvalidstatus', 'assign');
6612 return false;
6614 $flags = $this->get_user_flags($USER->id, false);
6616 // Get the flags to check if it is locked.
6617 if ($flags && $flags->locked) {
6618 $notices[] = get_string('submissionslocked', 'assign');
6619 return false;
6621 if ($instance->submissiondrafts) {
6622 $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
6623 } else {
6624 $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
6626 $this->update_submission($submission, $USER->id, true, $instance->teamsubmission);
6628 // Find the previous submission.
6629 if ($instance->teamsubmission) {
6630 $previoussubmission = $this->get_group_submission($USER->id, 0, true, $submission->attemptnumber - 1);
6631 } else {
6632 $previoussubmission = $this->get_user_submission($USER->id, true, $submission->attemptnumber - 1);
6635 if (!$previoussubmission) {
6636 // There was no previous submission so there is nothing else to do.
6637 return true;
6640 $pluginerror = false;
6641 foreach ($this->get_submission_plugins() as $plugin) {
6642 if ($plugin->is_visible() && $plugin->is_enabled()) {
6643 if (!$plugin->copy_submission($previoussubmission, $submission)) {
6644 $notices[] = $plugin->get_error();
6645 $pluginerror = true;
6649 if ($pluginerror) {
6650 return false;
6653 \mod_assign\event\submission_duplicated::create_from_submission($this, $submission)->trigger();
6655 $complete = COMPLETION_INCOMPLETE;
6656 if ($submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
6657 $complete = COMPLETION_COMPLETE;
6659 $completion = new completion_info($this->get_course());
6660 if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
6661 $this->update_activity_completion_records($instance->teamsubmission,
6662 $instance->requireallteammemberssubmit,
6663 $submission,
6664 $USER->id,
6665 $complete,
6666 $completion);
6669 if (!$instance->submissiondrafts) {
6670 // There is a case for not notifying the student about the submission copy,
6671 // but it provides a record of the event and if they then cancel editing it
6672 // is clear that the submission was copied.
6673 $this->notify_student_submission_copied($submission);
6674 $this->notify_graders($submission);
6676 // The same logic applies here - we could not notify teachers,
6677 // but then they would wonder why there are submitted assignments
6678 // and they haven't been notified.
6679 \mod_assign\event\assessable_submitted::create_from_submission($this, $submission, true)->trigger();
6681 return true;
6685 * Determine if the current submission is empty or not.
6687 * @param submission $submission the students submission record to check.
6688 * @return bool
6690 public function submission_empty($submission) {
6691 $allempty = true;
6693 foreach ($this->submissionplugins as $plugin) {
6694 if ($plugin->is_enabled() && $plugin->is_visible()) {
6695 if (!$allempty || !$plugin->is_empty($submission)) {
6696 $allempty = false;
6700 return $allempty;
6704 * Determine if a new submission is empty or not
6706 * @param stdClass $data Submission data
6707 * @return bool
6709 public function new_submission_empty($data) {
6710 foreach ($this->submissionplugins as $plugin) {
6711 if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->allow_submissions() &&
6712 !$plugin->submission_is_empty($data)) {
6713 return false;
6716 return true;
6720 * Save assignment submission for the current user.
6722 * @param stdClass $data
6723 * @param array $notices Any error messages that should be shown
6724 * to the user.
6725 * @return bool
6727 public function save_submission(stdClass $data, & $notices) {
6728 global $CFG, $USER, $DB;
6730 $userid = $USER->id;
6731 if (!empty($data->userid)) {
6732 $userid = $data->userid;
6735 $user = clone($USER);
6736 if ($userid == $USER->id) {
6737 require_capability('mod/assign:submit', $this->context);
6738 } else {
6739 $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
6740 if (!$this->can_edit_submission($userid, $USER->id)) {
6741 print_error('nopermission');
6744 $instance = $this->get_instance();
6746 if ($instance->teamsubmission) {
6747 $submission = $this->get_group_submission($userid, 0, true);
6748 } else {
6749 $submission = $this->get_user_submission($userid, true);
6752 if ($this->new_submission_empty($data)) {
6753 $notices[] = get_string('submissionempty', 'mod_assign');
6754 return false;
6757 // Check that no one has modified the submission since we started looking at it.
6758 if (isset($data->lastmodified) && ($submission->timemodified > $data->lastmodified)) {
6759 // Another user has submitted something. Notify the current user.
6760 if ($submission->status !== ASSIGN_SUBMISSION_STATUS_NEW) {
6761 $notices[] = $instance->teamsubmission ? get_string('submissionmodifiedgroup', 'mod_assign')
6762 : get_string('submissionmodified', 'mod_assign');
6763 return false;
6767 if ($instance->submissiondrafts) {
6768 $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
6769 } else {
6770 $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
6773 $flags = $this->get_user_flags($userid, false);
6775 // Get the flags to check if it is locked.
6776 if ($flags && $flags->locked) {
6777 print_error('submissionslocked', 'assign');
6778 return true;
6781 $pluginerror = false;
6782 foreach ($this->submissionplugins as $plugin) {
6783 if ($plugin->is_enabled() && $plugin->is_visible()) {
6784 if (!$plugin->save($submission, $data)) {
6785 $notices[] = $plugin->get_error();
6786 $pluginerror = true;
6791 $allempty = $this->submission_empty($submission);
6792 if ($pluginerror || $allempty) {
6793 if ($allempty) {
6794 $notices[] = get_string('submissionempty', 'mod_assign');
6796 return false;
6799 $this->update_submission($submission, $userid, true, $instance->teamsubmission);
6801 if ($instance->teamsubmission && !$instance->requireallteammemberssubmit) {
6802 $team = $this->get_submission_group_members($submission->groupid, true);
6804 foreach ($team as $member) {
6805 if ($member->id != $userid) {
6806 $membersubmission = clone($submission);
6807 $this->update_submission($membersubmission, $member->id, true, $instance->teamsubmission);
6812 // Logging.
6813 if (isset($data->submissionstatement) && ($userid == $USER->id)) {
6814 \mod_assign\event\statement_accepted::create_from_submission($this, $submission)->trigger();
6817 $complete = COMPLETION_INCOMPLETE;
6818 if ($submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
6819 $complete = COMPLETION_COMPLETE;
6821 $completion = new completion_info($this->get_course());
6822 if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
6823 $completion->update_state($this->get_course_module(), $complete, $userid);
6826 if (!$instance->submissiondrafts) {
6827 $this->notify_student_submission_receipt($submission);
6828 $this->notify_graders($submission);
6829 \mod_assign\event\assessable_submitted::create_from_submission($this, $submission, true)->trigger();
6831 return true;
6835 * Save assignment submission.
6837 * @param moodleform $mform
6838 * @param array $notices Any error messages that should be shown
6839 * to the user at the top of the edit submission form.
6840 * @return bool
6842 protected function process_save_submission(&$mform, &$notices) {
6843 global $CFG, $USER;
6845 // Include submission form.
6846 require_once($CFG->dirroot . '/mod/assign/submission_form.php');
6848 $userid = optional_param('userid', $USER->id, PARAM_INT);
6849 // Need submit permission to submit an assignment.
6850 require_sesskey();
6851 if (!$this->submissions_open($userid)) {
6852 $notices[] = get_string('duedatereached', 'assign');
6853 return false;
6855 $instance = $this->get_instance();
6857 $data = new stdClass();
6858 $data->userid = $userid;
6859 $mform = new mod_assign_submission_form(null, array($this, $data));
6860 if ($mform->is_cancelled()) {
6861 return true;
6863 if ($data = $mform->get_data()) {
6864 return $this->save_submission($data, $notices);
6866 return false;
6871 * Determine if this users grade can be edited.
6873 * @param int $userid - The student userid
6874 * @param bool $checkworkflow - whether to include a check for the workflow state.
6875 * @return bool $gradingdisabled
6877 public function grading_disabled($userid, $checkworkflow=true) {
6878 global $CFG;
6879 if ($checkworkflow && $this->get_instance()->markingworkflow) {
6880 $grade = $this->get_user_grade($userid, false);
6881 $validstates = $this->get_marking_workflow_states_for_current_user();
6882 if (!empty($grade) && !empty($grade->workflowstate) && !array_key_exists($grade->workflowstate, $validstates)) {
6883 return true;
6886 $gradinginfo = grade_get_grades($this->get_course()->id,
6887 'mod',
6888 'assign',
6889 $this->get_instance()->id,
6890 array($userid));
6891 if (!$gradinginfo) {
6892 return false;
6895 if (!isset($gradinginfo->items[0]->grades[$userid])) {
6896 return false;
6898 $gradingdisabled = $gradinginfo->items[0]->grades[$userid]->locked ||
6899 $gradinginfo->items[0]->grades[$userid]->overridden;
6900 return $gradingdisabled;
6905 * Get an instance of a grading form if advanced grading is enabled.
6906 * This is specific to the assignment, marker and student.
6908 * @param int $userid - The student userid
6909 * @param stdClass|false $grade - The grade record
6910 * @param bool $gradingdisabled
6911 * @return mixed gradingform_instance|null $gradinginstance
6913 protected function get_grading_instance($userid, $grade, $gradingdisabled) {
6914 global $CFG, $USER;
6916 $grademenu = make_grades_menu($this->get_instance()->grade);
6917 $allowgradedecimals = $this->get_instance()->grade > 0;
6919 $advancedgradingwarning = false;
6920 $gradingmanager = get_grading_manager($this->context, 'mod_assign', 'submissions');
6921 $gradinginstance = null;
6922 if ($gradingmethod = $gradingmanager->get_active_method()) {
6923 $controller = $gradingmanager->get_controller($gradingmethod);
6924 if ($controller->is_form_available()) {
6925 $itemid = null;
6926 if ($grade) {
6927 $itemid = $grade->id;
6929 if ($gradingdisabled && $itemid) {
6930 $gradinginstance = $controller->get_current_instance($USER->id, $itemid);
6931 } else if (!$gradingdisabled) {
6932 $instanceid = optional_param('advancedgradinginstanceid', 0, PARAM_INT);
6933 $gradinginstance = $controller->get_or_create_instance($instanceid,
6934 $USER->id,
6935 $itemid);
6937 } else {
6938 $advancedgradingwarning = $controller->form_unavailable_notification();
6941 if ($gradinginstance) {
6942 $gradinginstance->get_controller()->set_grade_range($grademenu, $allowgradedecimals);
6944 return $gradinginstance;
6948 * Add elements to grade form.
6950 * @param MoodleQuickForm $mform
6951 * @param stdClass $data
6952 * @param array $params
6953 * @return void
6955 public function add_grade_form_elements(MoodleQuickForm $mform, stdClass $data, $params) {
6956 global $USER, $CFG, $SESSION;
6957 $settings = $this->get_instance();
6959 $rownum = isset($params['rownum']) ? $params['rownum'] : 0;
6960 $last = isset($params['last']) ? $params['last'] : true;
6961 $useridlistid = isset($params['useridlistid']) ? $params['useridlistid'] : 0;
6962 $userid = isset($params['userid']) ? $params['userid'] : 0;
6963 $attemptnumber = isset($params['attemptnumber']) ? $params['attemptnumber'] : 0;
6964 $gradingpanel = !empty($params['gradingpanel']);
6965 $bothids = ($userid && $useridlistid);
6967 if (!$userid || $bothids) {
6968 $useridlistkey = $this->get_useridlist_key($useridlistid);
6969 if (empty($SESSION->mod_assign_useridlist[$useridlistkey])) {
6970 $SESSION->mod_assign_useridlist[$useridlistkey] = $this->get_grading_userid_list();
6972 $useridlist = $SESSION->mod_assign_useridlist[$useridlistkey];
6973 } else {
6974 $useridlist = array($userid);
6975 $rownum = 0;
6976 $useridlistid = '';
6979 $userid = $useridlist[$rownum];
6980 // We need to create a grade record matching this attempt number
6981 // or the feedback plugin will have no way to know what is the correct attempt.
6982 $grade = $this->get_user_grade($userid, true, $attemptnumber);
6984 $submission = null;
6985 if ($this->get_instance()->teamsubmission) {
6986 $submission = $this->get_group_submission($userid, 0, false, $attemptnumber);
6987 } else {
6988 $submission = $this->get_user_submission($userid, false, $attemptnumber);
6991 // Add advanced grading.
6992 $gradingdisabled = $this->grading_disabled($userid);
6993 $gradinginstance = $this->get_grading_instance($userid, $grade, $gradingdisabled);
6995 $mform->addElement('header', 'gradeheader', get_string('grade'));
6996 if ($gradinginstance) {
6997 $gradingelement = $mform->addElement('grading',
6998 'advancedgrading',
6999 get_string('grade').':',
7000 array('gradinginstance' => $gradinginstance));
7001 if ($gradingdisabled) {
7002 $gradingelement->freeze();
7003 } else {
7004 $mform->addElement('hidden', 'advancedgradinginstanceid', $gradinginstance->get_id());
7005 $mform->setType('advancedgradinginstanceid', PARAM_INT);
7007 } else {
7008 // Use simple direct grading.
7009 if ($this->get_instance()->grade > 0) {
7010 $name = get_string('gradeoutof', 'assign', $this->get_instance()->grade);
7011 if (!$gradingdisabled) {
7012 $gradingelement = $mform->addElement('text', 'grade', $name);
7013 $mform->addHelpButton('grade', 'gradeoutofhelp', 'assign');
7014 $mform->setType('grade', PARAM_RAW);
7015 } else {
7016 $mform->addElement('hidden', 'grade', $name);
7017 $mform->hardFreeze('grade');
7018 $mform->setType('grade', PARAM_RAW);
7019 $strgradelocked = get_string('gradelocked', 'assign');
7020 $mform->addElement('static', 'gradedisabled', $name, $strgradelocked);
7021 $mform->addHelpButton('gradedisabled', 'gradeoutofhelp', 'assign');
7023 } else {
7024 $grademenu = array(-1 => get_string("nograde")) + make_grades_menu($this->get_instance()->grade);
7025 if (count($grademenu) > 1) {
7026 $gradingelement = $mform->addElement('select', 'grade', get_string('grade') . ':', $grademenu);
7028 // The grade is already formatted with format_float so it needs to be converted back to an integer.
7029 if (!empty($data->grade)) {
7030 $data->grade = (int)unformat_float($data->grade);
7032 $mform->setType('grade', PARAM_INT);
7033 if ($gradingdisabled) {
7034 $gradingelement->freeze();
7040 $gradinginfo = grade_get_grades($this->get_course()->id,
7041 'mod',
7042 'assign',
7043 $this->get_instance()->id,
7044 $userid);
7045 if (!empty($CFG->enableoutcomes)) {
7046 foreach ($gradinginfo->outcomes as $index => $outcome) {
7047 $options = make_grades_menu(-$outcome->scaleid);
7048 $options[0] = get_string('nooutcome', 'grades');
7049 if ($outcome->grades[$userid]->locked) {
7050 $mform->addElement('static',
7051 'outcome_' . $index . '[' . $userid . ']',
7052 $outcome->name . ':',
7053 $options[$outcome->grades[$userid]->grade]);
7054 } else {
7055 $attributes = array('id' => 'menuoutcome_' . $index );
7056 $mform->addElement('select',
7057 'outcome_' . $index . '[' . $userid . ']',
7058 $outcome->name.':',
7059 $options,
7060 $attributes);
7061 $mform->setType('outcome_' . $index . '[' . $userid . ']', PARAM_INT);
7062 $mform->setDefault('outcome_' . $index . '[' . $userid . ']',
7063 $outcome->grades[$userid]->grade);
7068 $capabilitylist = array('gradereport/grader:view', 'moodle/grade:viewall');
7069 if (has_all_capabilities($capabilitylist, $this->get_course_context())) {
7070 $urlparams = array('id'=>$this->get_course()->id);
7071 $url = new moodle_url('/grade/report/grader/index.php', $urlparams);
7072 $usergrade = '-';
7073 if (isset($gradinginfo->items[0]->grades[$userid]->str_grade)) {
7074 $usergrade = $gradinginfo->items[0]->grades[$userid]->str_grade;
7076 $gradestring = $this->get_renderer()->action_link($url, $usergrade);
7077 } else {
7078 $usergrade = '-';
7079 if (isset($gradinginfo->items[0]->grades[$userid]) &&
7080 !$gradinginfo->items[0]->grades[$userid]->hidden) {
7081 $usergrade = $gradinginfo->items[0]->grades[$userid]->str_grade;
7083 $gradestring = $usergrade;
7086 if ($this->get_instance()->markingworkflow) {
7087 $states = $this->get_marking_workflow_states_for_current_user();
7088 $options = array('' => get_string('markingworkflowstatenotmarked', 'assign')) + $states;
7089 $mform->addElement('select', 'workflowstate', get_string('markingworkflowstate', 'assign'), $options);
7090 $mform->addHelpButton('workflowstate', 'markingworkflowstate', 'assign');
7093 if ($this->get_instance()->markingworkflow &&
7094 $this->get_instance()->markingallocation &&
7095 has_capability('mod/assign:manageallocations', $this->context)) {
7097 list($sort, $params) = users_order_by_sql();
7098 $markers = get_users_by_capability($this->context, 'mod/assign:grade', '', $sort);
7099 $markerlist = array('' => get_string('choosemarker', 'assign'));
7100 foreach ($markers as $marker) {
7101 $markerlist[$marker->id] = fullname($marker);
7103 $mform->addElement('select', 'allocatedmarker', get_string('allocatedmarker', 'assign'), $markerlist);
7104 $mform->addHelpButton('allocatedmarker', 'allocatedmarker', 'assign');
7105 $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW);
7106 $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW);
7107 $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE);
7108 $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_RELEASED);
7110 $gradestring = '<span class="currentgrade">' . $gradestring . '</span>';
7111 $mform->addElement('static', 'currentgrade', get_string('currentgrade', 'assign'), $gradestring);
7113 if (count($useridlist) > 1) {
7114 $strparams = array('current'=>$rownum+1, 'total'=>count($useridlist));
7115 $name = get_string('outof', 'assign', $strparams);
7116 $mform->addElement('static', 'gradingstudent', get_string('gradingstudent', 'assign'), $name);
7119 // Let feedback plugins add elements to the grading form.
7120 $this->add_plugin_grade_elements($grade, $mform, $data, $userid);
7122 // Hidden params.
7123 $mform->addElement('hidden', 'id', $this->get_course_module()->id);
7124 $mform->setType('id', PARAM_INT);
7125 $mform->addElement('hidden', 'rownum', $rownum);
7126 $mform->setType('rownum', PARAM_INT);
7127 $mform->setConstant('rownum', $rownum);
7128 $mform->addElement('hidden', 'useridlistid', $useridlistid);
7129 $mform->setType('useridlistid', PARAM_ALPHANUM);
7130 $mform->addElement('hidden', 'attemptnumber', $attemptnumber);
7131 $mform->setType('attemptnumber', PARAM_INT);
7132 $mform->addElement('hidden', 'ajax', optional_param('ajax', 0, PARAM_INT));
7133 $mform->setType('ajax', PARAM_INT);
7134 $mform->addElement('hidden', 'userid', optional_param('userid', 0, PARAM_INT));
7135 $mform->setType('userid', PARAM_INT);
7137 if ($this->get_instance()->teamsubmission) {
7138 $mform->addElement('header', 'groupsubmissionsettings', get_string('groupsubmissionsettings', 'assign'));
7139 $mform->addElement('selectyesno', 'applytoall', get_string('applytoteam', 'assign'));
7140 $mform->setDefault('applytoall', 1);
7143 // Do not show if we are editing a previous attempt.
7144 if ($attemptnumber == -1 && $this->get_instance()->attemptreopenmethod != ASSIGN_ATTEMPT_REOPEN_METHOD_NONE) {
7145 $mform->addElement('header', 'attemptsettings', get_string('attemptsettings', 'assign'));
7146 $attemptreopenmethod = get_string('attemptreopenmethod_' . $this->get_instance()->attemptreopenmethod, 'assign');
7147 $mform->addElement('static', 'attemptreopenmethod', get_string('attemptreopenmethod', 'assign'), $attemptreopenmethod);
7149 $attemptnumber = 0;
7150 if ($submission) {
7151 $attemptnumber = $submission->attemptnumber;
7153 $maxattempts = $this->get_instance()->maxattempts;
7154 if ($maxattempts == ASSIGN_UNLIMITED_ATTEMPTS) {
7155 $maxattempts = get_string('unlimitedattempts', 'assign');
7157 $mform->addelement('static', 'maxattemptslabel', get_string('maxattempts', 'assign'), $maxattempts);
7158 $mform->addelement('static', 'attemptnumberlabel', get_string('attemptnumber', 'assign'), $attemptnumber + 1);
7160 $ismanual = $this->get_instance()->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL;
7161 $issubmission = !empty($submission);
7162 $isunlimited = $this->get_instance()->maxattempts == ASSIGN_UNLIMITED_ATTEMPTS;
7163 $islessthanmaxattempts = $issubmission && ($submission->attemptnumber < ($this->get_instance()->maxattempts-1));
7165 if ($ismanual && (!$issubmission || $isunlimited || $islessthanmaxattempts)) {
7166 $mform->addElement('selectyesno', 'addattempt', get_string('addattempt', 'assign'));
7167 $mform->setDefault('addattempt', 0);
7170 if (!$gradingpanel) {
7171 $mform->addElement('selectyesno', 'sendstudentnotifications', get_string('sendstudentnotifications', 'assign'));
7172 } else {
7173 $mform->addElement('hidden', 'sendstudentnotifications', get_string('sendstudentnotifications', 'assign'));
7174 $mform->setType('sendstudentnotifications', PARAM_BOOL);
7176 // Get assignment visibility information for student.
7177 $modinfo = get_fast_modinfo($settings->course, $userid);
7178 $cm = $modinfo->get_cm($this->get_course_module()->id);
7180 // Don't allow notification to be sent if the student can't access the assignment,
7181 // or until in "Released" state if using marking workflow.
7182 if (!$cm->uservisible) {
7183 $mform->setDefault('sendstudentnotifications', 0);
7184 $mform->freeze('sendstudentnotifications');
7185 } else if ($this->get_instance()->markingworkflow) {
7186 $mform->setDefault('sendstudentnotifications', 0);
7187 if (!$gradingpanel) {
7188 $mform->disabledIf('sendstudentnotifications', 'workflowstate', 'neq', ASSIGN_MARKING_WORKFLOW_STATE_RELEASED);
7190 } else {
7191 $mform->setDefault('sendstudentnotifications', $this->get_instance()->sendstudentnotifications);
7194 $mform->addElement('hidden', 'action', 'submitgrade');
7195 $mform->setType('action', PARAM_ALPHA);
7197 if (!$gradingpanel) {
7199 $buttonarray = array();
7200 $name = get_string('savechanges', 'assign');
7201 $buttonarray[] = $mform->createElement('submit', 'savegrade', $name);
7202 if (!$last) {
7203 $name = get_string('savenext', 'assign');
7204 $buttonarray[] = $mform->createElement('submit', 'saveandshownext', $name);
7206 $buttonarray[] = $mform->createElement('cancel', 'cancelbutton', get_string('cancel'));
7207 $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false);
7208 $mform->closeHeaderBefore('buttonar');
7209 $buttonarray = array();
7211 if ($rownum > 0) {
7212 $name = get_string('previous', 'assign');
7213 $buttonarray[] = $mform->createElement('submit', 'nosaveandprevious', $name);
7216 if (!$last) {
7217 $name = get_string('nosavebutnext', 'assign');
7218 $buttonarray[] = $mform->createElement('submit', 'nosaveandnext', $name);
7220 if (!empty($buttonarray)) {
7221 $mform->addGroup($buttonarray, 'navar', '', array(' '), false);
7224 // The grading form does not work well with shortforms.
7225 $mform->setDisableShortforms();
7229 * Add elements in submission plugin form.
7231 * @param mixed $submission stdClass|null
7232 * @param MoodleQuickForm $mform
7233 * @param stdClass $data
7234 * @param int $userid The current userid (same as $USER->id)
7235 * @return void
7237 protected function add_plugin_submission_elements($submission,
7238 MoodleQuickForm $mform,
7239 stdClass $data,
7240 $userid) {
7241 foreach ($this->submissionplugins as $plugin) {
7242 if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->allow_submissions()) {
7243 $plugin->get_form_elements_for_user($submission, $mform, $data, $userid);
7249 * Check if feedback plugins installed are enabled.
7251 * @return bool
7253 public function is_any_feedback_plugin_enabled() {
7254 if (!isset($this->cache['any_feedback_plugin_enabled'])) {
7255 $this->cache['any_feedback_plugin_enabled'] = false;
7256 foreach ($this->feedbackplugins as $plugin) {
7257 if ($plugin->is_enabled() && $plugin->is_visible()) {
7258 $this->cache['any_feedback_plugin_enabled'] = true;
7259 break;
7264 return $this->cache['any_feedback_plugin_enabled'];
7269 * Check if submission plugins installed are enabled.
7271 * @return bool
7273 public function is_any_submission_plugin_enabled() {
7274 if (!isset($this->cache['any_submission_plugin_enabled'])) {
7275 $this->cache['any_submission_plugin_enabled'] = false;
7276 foreach ($this->submissionplugins as $plugin) {
7277 if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->allow_submissions()) {
7278 $this->cache['any_submission_plugin_enabled'] = true;
7279 break;
7284 return $this->cache['any_submission_plugin_enabled'];
7289 * Add elements to submission form.
7290 * @param MoodleQuickForm $mform
7291 * @param stdClass $data
7292 * @return void
7294 public function add_submission_form_elements(MoodleQuickForm $mform, stdClass $data) {
7295 global $USER;
7297 $userid = $data->userid;
7298 // Team submissions.
7299 if ($this->get_instance()->teamsubmission) {
7300 $submission = $this->get_group_submission($userid, 0, false);
7301 } else {
7302 $submission = $this->get_user_submission($userid, false);
7305 // Submission statement.
7306 $adminconfig = $this->get_admin_config();
7308 $requiresubmissionstatement = $this->get_instance()->requiresubmissionstatement &&
7309 !empty($adminconfig->submissionstatement);
7311 $draftsenabled = $this->get_instance()->submissiondrafts;
7313 // Only show submission statement if we are editing our own submission.
7314 if ($requiresubmissionstatement && !$draftsenabled && $userid == $USER->id) {
7316 $submissionstatement = '';
7317 if (!empty($adminconfig->submissionstatement)) {
7318 // Format the submission statement before its sent. We turn off para because this is going within
7319 // a form element.
7320 $options = array(
7321 'context' => $this->get_context(),
7322 'para' => false
7324 $submissionstatement = format_text($adminconfig->submissionstatement, FORMAT_MOODLE, $options);
7326 $mform->addElement('checkbox', 'submissionstatement', '', $submissionstatement);
7327 $mform->addRule('submissionstatement', get_string('required'), 'required', null, 'client');
7330 $this->add_plugin_submission_elements($submission, $mform, $data, $userid);
7332 // Hidden params.
7333 $mform->addElement('hidden', 'id', $this->get_course_module()->id);
7334 $mform->setType('id', PARAM_INT);
7336 $mform->addElement('hidden', 'userid', $userid);
7337 $mform->setType('userid', PARAM_INT);
7339 $mform->addElement('hidden', 'action', 'savesubmission');
7340 $mform->setType('action', PARAM_TEXT);
7344 * Revert to draft.
7346 * @param int $userid
7347 * @return boolean
7349 public function revert_to_draft($userid) {
7350 global $DB, $USER;
7352 // Need grade permission.
7353 require_capability('mod/assign:grade', $this->context);
7355 if ($this->get_instance()->teamsubmission) {
7356 $submission = $this->get_group_submission($userid, 0, false);
7357 } else {
7358 $submission = $this->get_user_submission($userid, false);
7361 if (!$submission) {
7362 return false;
7364 $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
7365 $this->update_submission($submission, $userid, true, $this->get_instance()->teamsubmission);
7367 // Give each submission plugin a chance to process the reverting to draft.
7368 $plugins = $this->get_submission_plugins();
7369 foreach ($plugins as $plugin) {
7370 if ($plugin->is_enabled() && $plugin->is_visible()) {
7371 $plugin->revert_to_draft($submission);
7374 // Update the modified time on the grade (grader modified).
7375 $grade = $this->get_user_grade($userid, true);
7376 $grade->grader = $USER->id;
7377 $this->update_grade($grade);
7379 $completion = new completion_info($this->get_course());
7380 if ($completion->is_enabled($this->get_course_module()) &&
7381 $this->get_instance()->completionsubmit) {
7382 $completion->update_state($this->get_course_module(), COMPLETION_INCOMPLETE, $userid);
7384 \mod_assign\event\submission_status_updated::create_from_submission($this, $submission)->trigger();
7385 return true;
7389 * Revert to draft.
7390 * Uses url parameter userid if userid not supplied as a parameter.
7392 * @param int $userid
7393 * @return boolean
7395 protected function process_revert_to_draft($userid = 0) {
7396 require_sesskey();
7398 if (!$userid) {
7399 $userid = required_param('userid', PARAM_INT);
7402 return $this->revert_to_draft($userid);
7406 * Prevent student updates to this submission
7408 * @param int $userid
7409 * @return bool
7411 public function lock_submission($userid) {
7412 global $USER, $DB;
7413 // Need grade permission.
7414 require_capability('mod/assign:grade', $this->context);
7416 // Give each submission plugin a chance to process the locking.
7417 $plugins = $this->get_submission_plugins();
7418 $submission = $this->get_user_submission($userid, false);
7420 $flags = $this->get_user_flags($userid, true);
7421 $flags->locked = 1;
7422 $this->update_user_flags($flags);
7424 foreach ($plugins as $plugin) {
7425 if ($plugin->is_enabled() && $plugin->is_visible()) {
7426 $plugin->lock($submission, $flags);
7430 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
7431 \mod_assign\event\submission_locked::create_from_user($this, $user)->trigger();
7432 return true;
7437 * Set the workflow state for multiple users
7439 * @return void
7441 protected function process_set_batch_marking_workflow_state() {
7442 global $CFG, $DB;
7444 // Include batch marking workflow form.
7445 require_once($CFG->dirroot . '/mod/assign/batchsetmarkingworkflowstateform.php');
7447 $formparams = array(
7448 'userscount' => 0, // This form is never re-displayed, so we don't need to
7449 'usershtml' => '', // initialise these parameters with real information.
7450 'markingworkflowstates' => $this->get_marking_workflow_states_for_current_user()
7453 $mform = new mod_assign_batch_set_marking_workflow_state_form(null, $formparams);
7455 if ($mform->is_cancelled()) {
7456 return true;
7459 if ($formdata = $mform->get_data()) {
7460 $useridlist = explode(',', $formdata->selectedusers);
7461 $state = $formdata->markingworkflowstate;
7463 foreach ($useridlist as $userid) {
7464 $flags = $this->get_user_flags($userid, true);
7466 $flags->workflowstate = $state;
7468 // Clear the mailed flag if notification is requested, the student hasn't been
7469 // notified previously, the student can access the assignment, and the state
7470 // is "Released".
7471 $modinfo = get_fast_modinfo($this->course, $userid);
7472 $cm = $modinfo->get_cm($this->get_course_module()->id);
7473 if ($formdata->sendstudentnotifications && $cm->uservisible &&
7474 $state == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
7475 $flags->mailed = 0;
7478 $gradingdisabled = $this->grading_disabled($userid);
7480 // Will not apply update if user does not have permission to assign this workflow state.
7481 if (!$gradingdisabled && $this->update_user_flags($flags)) {
7482 if ($state == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
7483 // Update Gradebook.
7484 $assign = clone $this->get_instance();
7485 $assign->cmidnumber = $this->get_course_module()->idnumber;
7486 // Set assign gradebook feedback plugin status.
7487 $assign->gradefeedbackenabled = $this->is_gradebook_feedback_enabled();
7488 assign_update_grades($assign, $userid);
7491 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
7492 \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $state)->trigger();
7499 * Set the marking allocation for multiple users
7501 * @return void
7503 protected function process_set_batch_marking_allocation() {
7504 global $CFG, $DB;
7506 // Include batch marking allocation form.
7507 require_once($CFG->dirroot . '/mod/assign/batchsetallocatedmarkerform.php');
7509 $formparams = array(
7510 'userscount' => 0, // This form is never re-displayed, so we don't need to
7511 'usershtml' => '' // initialise these parameters with real information.
7514 list($sort, $params) = users_order_by_sql();
7515 $markers = get_users_by_capability($this->get_context(), 'mod/assign:grade', '', $sort);
7516 $markerlist = array();
7517 foreach ($markers as $marker) {
7518 $markerlist[$marker->id] = fullname($marker);
7521 $formparams['markers'] = $markerlist;
7523 $mform = new mod_assign_batch_set_allocatedmarker_form(null, $formparams);
7525 if ($mform->is_cancelled()) {
7526 return true;
7529 if ($formdata = $mform->get_data()) {
7530 $useridlist = explode(',', $formdata->selectedusers);
7531 $marker = $DB->get_record('user', array('id' => $formdata->allocatedmarker), '*', MUST_EXIST);
7533 foreach ($useridlist as $userid) {
7534 $flags = $this->get_user_flags($userid, true);
7535 if ($flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW ||
7536 $flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW ||
7537 $flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE ||
7538 $flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
7540 continue; // Allocated marker can only be changed in certain workflow states.
7543 $flags->allocatedmarker = $marker->id;
7545 if ($this->update_user_flags($flags)) {
7546 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
7547 \mod_assign\event\marker_updated::create_from_marker($this, $user, $marker)->trigger();
7555 * Prevent student updates to this submission.
7556 * Uses url parameter userid.
7558 * @param int $userid
7559 * @return void
7561 protected function process_lock_submission($userid = 0) {
7563 require_sesskey();
7565 if (!$userid) {
7566 $userid = required_param('userid', PARAM_INT);
7569 return $this->lock_submission($userid);
7573 * Unlock the student submission.
7575 * @param int $userid
7576 * @return bool
7578 public function unlock_submission($userid) {
7579 global $USER, $DB;
7581 // Need grade permission.
7582 require_capability('mod/assign:grade', $this->context);
7584 // Give each submission plugin a chance to process the unlocking.
7585 $plugins = $this->get_submission_plugins();
7586 $submission = $this->get_user_submission($userid, false);
7588 $flags = $this->get_user_flags($userid, true);
7589 $flags->locked = 0;
7590 $this->update_user_flags($flags);
7592 foreach ($plugins as $plugin) {
7593 if ($plugin->is_enabled() && $plugin->is_visible()) {
7594 $plugin->unlock($submission, $flags);
7598 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
7599 \mod_assign\event\submission_unlocked::create_from_user($this, $user)->trigger();
7600 return true;
7604 * Unlock the student submission.
7605 * Uses url parameter userid.
7607 * @param int $userid
7608 * @return bool
7610 protected function process_unlock_submission($userid = 0) {
7612 require_sesskey();
7614 if (!$userid) {
7615 $userid = required_param('userid', PARAM_INT);
7618 return $this->unlock_submission($userid);
7622 * Apply a grade from a grading form to a user (may be called multiple times for a group submission).
7624 * @param stdClass $formdata - the data from the form
7625 * @param int $userid - the user to apply the grade to
7626 * @param int $attemptnumber - The attempt number to apply the grade to.
7627 * @return void
7629 protected function apply_grade_to_user($formdata, $userid, $attemptnumber) {
7630 global $USER, $CFG, $DB;
7632 $grade = $this->get_user_grade($userid, true, $attemptnumber);
7633 $originalgrade = $grade->grade;
7634 $gradingdisabled = $this->grading_disabled($userid);
7635 $gradinginstance = $this->get_grading_instance($userid, $grade, $gradingdisabled);
7636 if (!$gradingdisabled) {
7637 if ($gradinginstance) {
7638 $grade->grade = $gradinginstance->submit_and_get_grade($formdata->advancedgrading,
7639 $grade->id);
7640 } else {
7641 // Handle the case when grade is set to No Grade.
7642 if (isset($formdata->grade)) {
7643 $grade->grade = grade_floatval(unformat_float($formdata->grade));
7646 if (isset($formdata->workflowstate) || isset($formdata->allocatedmarker)) {
7647 $flags = $this->get_user_flags($userid, true);
7648 $oldworkflowstate = $flags->workflowstate;
7649 $flags->workflowstate = isset($formdata->workflowstate) ? $formdata->workflowstate : $flags->workflowstate;
7650 $flags->allocatedmarker = isset($formdata->allocatedmarker) ? $formdata->allocatedmarker : $flags->allocatedmarker;
7651 if ($this->update_user_flags($flags) &&
7652 isset($formdata->workflowstate) &&
7653 $formdata->workflowstate !== $oldworkflowstate) {
7654 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
7655 \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $formdata->workflowstate)->trigger();
7659 $grade->grader= $USER->id;
7661 $adminconfig = $this->get_admin_config();
7662 $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
7664 $feedbackmodified = false;
7666 // Call save in plugins.
7667 foreach ($this->feedbackplugins as $plugin) {
7668 if ($plugin->is_enabled() && $plugin->is_visible()) {
7669 $gradingmodified = $plugin->is_feedback_modified($grade, $formdata);
7670 if ($gradingmodified) {
7671 if (!$plugin->save($grade, $formdata)) {
7672 $result = false;
7673 print_error($plugin->get_error());
7675 // If $feedbackmodified is true, keep it true.
7676 $feedbackmodified = $feedbackmodified || $gradingmodified;
7678 if (('assignfeedback_' . $plugin->get_type()) == $gradebookplugin) {
7679 // This is the feedback plugin chose to push comments to the gradebook.
7680 $grade->feedbacktext = $plugin->text_for_gradebook($grade);
7681 $grade->feedbackformat = $plugin->format_for_gradebook($grade);
7686 // We do not want to update the timemodified if no grade was added.
7687 if (!empty($formdata->addattempt) ||
7688 ($originalgrade !== null && $originalgrade != -1) ||
7689 ($grade->grade !== null && $grade->grade != -1) ||
7690 $feedbackmodified) {
7691 $this->update_grade($grade, !empty($formdata->addattempt));
7693 // Note the default if not provided for this option is true (e.g. webservices).
7694 // This is for backwards compatibility.
7695 if (!isset($formdata->sendstudentnotifications) || $formdata->sendstudentnotifications) {
7696 $this->notify_grade_modified($grade, true);
7702 * Save outcomes submitted from grading form.
7704 * @param int $userid
7705 * @param stdClass $formdata
7706 * @param int $sourceuserid The user ID under which the outcome data is accessible. This is relevant
7707 * for an outcome set to a user but applied to an entire group.
7709 protected function process_outcomes($userid, $formdata, $sourceuserid = null) {
7710 global $CFG, $USER;
7712 if (empty($CFG->enableoutcomes)) {
7713 return;
7715 if ($this->grading_disabled($userid)) {
7716 return;
7719 require_once($CFG->libdir.'/gradelib.php');
7721 $data = array();
7722 $gradinginfo = grade_get_grades($this->get_course()->id,
7723 'mod',
7724 'assign',
7725 $this->get_instance()->id,
7726 $userid);
7728 if (!empty($gradinginfo->outcomes)) {
7729 foreach ($gradinginfo->outcomes as $index => $oldoutcome) {
7730 $name = 'outcome_'.$index;
7731 $sourceuserid = $sourceuserid !== null ? $sourceuserid : $userid;
7732 if (isset($formdata->{$name}[$sourceuserid]) &&
7733 $oldoutcome->grades[$userid]->grade != $formdata->{$name}[$sourceuserid]) {
7734 $data[$index] = $formdata->{$name}[$sourceuserid];
7738 if (count($data) > 0) {
7739 grade_update_outcomes('mod/assign',
7740 $this->course->id,
7741 'mod',
7742 'assign',
7743 $this->get_instance()->id,
7744 $userid,
7745 $data);
7750 * If the requirements are met - reopen the submission for another attempt.
7751 * Only call this function when grading the latest attempt.
7753 * @param int $userid The userid.
7754 * @param stdClass $submission The submission (may be a group submission).
7755 * @param bool $addattempt - True if the "allow another attempt" checkbox was checked.
7756 * @return bool - true if another attempt was added.
7758 protected function reopen_submission_if_required($userid, $submission, $addattempt) {
7759 $instance = $this->get_instance();
7760 $maxattemptsreached = !empty($submission) &&
7761 $submission->attemptnumber >= ($instance->maxattempts - 1) &&
7762 $instance->maxattempts != ASSIGN_UNLIMITED_ATTEMPTS;
7763 $shouldreopen = false;
7764 if ($instance->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS) {
7765 // Check the gradetopass from the gradebook.
7766 $gradeitem = $this->get_grade_item();
7767 if ($gradeitem) {
7768 $gradegrade = grade_grade::fetch(array('userid' => $userid, 'itemid' => $gradeitem->id));
7770 // Do not reopen if is_passed returns null, e.g. if there is no pass criterion set.
7771 if ($gradegrade && ($gradegrade->is_passed() === false)) {
7772 $shouldreopen = true;
7776 if ($instance->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL &&
7777 !empty($addattempt)) {
7778 $shouldreopen = true;
7780 if ($shouldreopen && !$maxattemptsreached) {
7781 $this->add_attempt($userid);
7782 return true;
7784 return false;
7788 * Save grade update.
7790 * @param int $userid
7791 * @param stdClass $data
7792 * @return bool - was the grade saved
7794 public function save_grade($userid, $data) {
7796 // Need grade permission.
7797 require_capability('mod/assign:grade', $this->context);
7799 $instance = $this->get_instance();
7800 $submission = null;
7801 if ($instance->teamsubmission) {
7802 $submission = $this->get_group_submission($userid, 0, false, $data->attemptnumber);
7803 } else {
7804 $submission = $this->get_user_submission($userid, false, $data->attemptnumber);
7806 if ($instance->teamsubmission && !empty($data->applytoall)) {
7807 $groupid = 0;
7808 if ($this->get_submission_group($userid)) {
7809 $group = $this->get_submission_group($userid);
7810 if ($group) {
7811 $groupid = $group->id;
7814 $members = $this->get_submission_group_members($groupid, true, $this->show_only_active_users());
7815 foreach ($members as $member) {
7816 // User may exist in multple groups (which should put them in the default group).
7817 $this->apply_grade_to_user($data, $member->id, $data->attemptnumber);
7818 $this->process_outcomes($member->id, $data, $userid);
7820 } else {
7821 $this->apply_grade_to_user($data, $userid, $data->attemptnumber);
7823 $this->process_outcomes($userid, $data);
7826 return true;
7830 * Save grade.
7832 * @param moodleform $mform
7833 * @return bool - was the grade saved
7835 protected function process_save_grade(&$mform) {
7836 global $CFG, $SESSION;
7837 // Include grade form.
7838 require_once($CFG->dirroot . '/mod/assign/gradeform.php');
7840 require_sesskey();
7842 $instance = $this->get_instance();
7843 $rownum = required_param('rownum', PARAM_INT);
7844 $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT);
7845 $useridlistid = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
7846 $userid = optional_param('userid', 0, PARAM_INT);
7847 if (!$userid) {
7848 if (empty($SESSION->mod_assign_useridlist[$this->get_useridlist_key($useridlistid)])) {
7849 // If the userid list is not stored we must not save, as it is possible that the user in a
7850 // given row position may not be the same now as when the grading page was generated.
7851 $url = new moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id));
7852 throw new moodle_exception('useridlistnotcached', 'mod_assign', $url);
7854 $useridlist = $SESSION->mod_assign_useridlist[$this->get_useridlist_key($useridlistid)];
7855 } else {
7856 $useridlist = array($userid);
7857 $rownum = 0;
7860 $last = false;
7861 $userid = $useridlist[$rownum];
7862 if ($rownum == count($useridlist) - 1) {
7863 $last = true;
7866 $data = new stdClass();
7868 $gradeformparams = array('rownum' => $rownum,
7869 'useridlistid' => $useridlistid,
7870 'last' => $last,
7871 'attemptnumber' => $attemptnumber,
7872 'userid' => $userid);
7873 $mform = new mod_assign_grade_form(null,
7874 array($this, $data, $gradeformparams),
7875 'post',
7877 array('class'=>'gradeform'));
7879 if ($formdata = $mform->get_data()) {
7880 return $this->save_grade($userid, $formdata);
7881 } else {
7882 return false;
7887 * This function is a static wrapper around can_upgrade.
7889 * @param string $type The plugin type
7890 * @param int $version The plugin version
7891 * @return bool
7893 public static function can_upgrade_assignment($type, $version) {
7894 $assignment = new assign(null, null, null);
7895 return $assignment->can_upgrade($type, $version);
7899 * This function returns true if it can upgrade an assignment from the 2.2 module.
7901 * @param string $type The plugin type
7902 * @param int $version The plugin version
7903 * @return bool
7905 public function can_upgrade($type, $version) {
7906 if ($type == 'offline' && $version >= 2011112900) {
7907 return true;
7909 foreach ($this->submissionplugins as $plugin) {
7910 if ($plugin->can_upgrade($type, $version)) {
7911 return true;
7914 foreach ($this->feedbackplugins as $plugin) {
7915 if ($plugin->can_upgrade($type, $version)) {
7916 return true;
7919 return false;
7923 * Copy all the files from the old assignment files area to the new one.
7924 * This is used by the plugin upgrade code.
7926 * @param int $oldcontextid The old assignment context id
7927 * @param int $oldcomponent The old assignment component ('assignment')
7928 * @param int $oldfilearea The old assignment filearea ('submissions')
7929 * @param int $olditemid The old submissionid (can be null e.g. intro)
7930 * @param int $newcontextid The new assignment context id
7931 * @param int $newcomponent The new assignment component ('assignment')
7932 * @param int $newfilearea The new assignment filearea ('submissions')
7933 * @param int $newitemid The new submissionid (can be null e.g. intro)
7934 * @return int The number of files copied
7936 public function copy_area_files_for_upgrade($oldcontextid,
7937 $oldcomponent,
7938 $oldfilearea,
7939 $olditemid,
7940 $newcontextid,
7941 $newcomponent,
7942 $newfilearea,
7943 $newitemid) {
7944 // Note, this code is based on some code in filestorage - but that code
7945 // deleted the old files (which we don't want).
7946 $count = 0;
7948 $fs = get_file_storage();
7950 $oldfiles = $fs->get_area_files($oldcontextid,
7951 $oldcomponent,
7952 $oldfilearea,
7953 $olditemid,
7954 'id',
7955 false);
7956 foreach ($oldfiles as $oldfile) {
7957 $filerecord = new stdClass();
7958 $filerecord->contextid = $newcontextid;
7959 $filerecord->component = $newcomponent;
7960 $filerecord->filearea = $newfilearea;
7961 $filerecord->itemid = $newitemid;
7962 $fs->create_file_from_storedfile($filerecord, $oldfile);
7963 $count += 1;
7966 return $count;
7970 * Add a new attempt for each user in the list - but reopen each group assignment
7971 * at most 1 time.
7973 * @param array $useridlist Array of userids to reopen.
7974 * @return bool
7976 protected function process_add_attempt_group($useridlist) {
7977 $groupsprocessed = array();
7978 $result = true;
7980 foreach ($useridlist as $userid) {
7981 $groupid = 0;
7982 $group = $this->get_submission_group($userid);
7983 if ($group) {
7984 $groupid = $group->id;
7987 if (empty($groupsprocessed[$groupid])) {
7988 $result = $this->process_add_attempt($userid) && $result;
7989 $groupsprocessed[$groupid] = true;
7992 return $result;
7996 * Check for a sess key and then call add_attempt.
7998 * @param int $userid int The user to add the attempt for
7999 * @return bool - true if successful.
8001 protected function process_add_attempt($userid) {
8002 require_sesskey();
8004 return $this->add_attempt($userid);
8008 * Add a new attempt for a user.
8010 * @param int $userid int The user to add the attempt for
8011 * @return bool - true if successful.
8013 protected function add_attempt($userid) {
8014 require_capability('mod/assign:grade', $this->context);
8016 if ($this->get_instance()->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_NONE) {
8017 return false;
8020 if ($this->get_instance()->teamsubmission) {
8021 $oldsubmission = $this->get_group_submission($userid, 0, false);
8022 } else {
8023 $oldsubmission = $this->get_user_submission($userid, false);
8026 if (!$oldsubmission) {
8027 return false;
8030 // No more than max attempts allowed.
8031 if ($this->get_instance()->maxattempts != ASSIGN_UNLIMITED_ATTEMPTS &&
8032 $oldsubmission->attemptnumber >= ($this->get_instance()->maxattempts - 1)) {
8033 return false;
8036 // Create the new submission record for the group/user.
8037 if ($this->get_instance()->teamsubmission) {
8038 $newsubmission = $this->get_group_submission($userid, 0, true, $oldsubmission->attemptnumber + 1);
8039 } else {
8040 $newsubmission = $this->get_user_submission($userid, true, $oldsubmission->attemptnumber + 1);
8043 // Set the status of the new attempt to reopened.
8044 $newsubmission->status = ASSIGN_SUBMISSION_STATUS_REOPENED;
8046 // Give each submission plugin a chance to process the add_attempt.
8047 $plugins = $this->get_submission_plugins();
8048 foreach ($plugins as $plugin) {
8049 if ($plugin->is_enabled() && $plugin->is_visible()) {
8050 $plugin->add_attempt($oldsubmission, $newsubmission);
8054 $this->update_submission($newsubmission, $userid, false, $this->get_instance()->teamsubmission);
8055 $flags = $this->get_user_flags($userid, false);
8056 if (isset($flags->locked) && $flags->locked) { // May not exist.
8057 $this->process_unlock_submission($userid);
8059 return true;
8063 * Get an upto date list of user grades and feedback for the gradebook.
8065 * @param int $userid int or 0 for all users
8066 * @return array of grade data formated for the gradebook api
8067 * The data required by the gradebook api is userid,
8068 * rawgrade,
8069 * feedback,
8070 * feedbackformat,
8071 * usermodified,
8072 * dategraded,
8073 * datesubmitted
8075 public function get_user_grades_for_gradebook($userid) {
8076 global $DB, $CFG;
8077 $grades = array();
8078 $assignmentid = $this->get_instance()->id;
8080 $adminconfig = $this->get_admin_config();
8081 $gradebookpluginname = $adminconfig->feedback_plugin_for_gradebook;
8082 $gradebookplugin = null;
8084 // Find the gradebook plugin.
8085 foreach ($this->feedbackplugins as $plugin) {
8086 if ($plugin->is_enabled() && $plugin->is_visible()) {
8087 if (('assignfeedback_' . $plugin->get_type()) == $gradebookpluginname) {
8088 $gradebookplugin = $plugin;
8092 if ($userid) {
8093 $where = ' WHERE u.id = :userid ';
8094 } else {
8095 $where = ' WHERE u.id != :userid ';
8098 // When the gradebook asks us for grades - only return the last attempt for each user.
8099 $params = array('assignid1'=>$assignmentid,
8100 'assignid2'=>$assignmentid,
8101 'userid'=>$userid);
8102 $graderesults = $DB->get_recordset_sql('SELECT
8103 u.id as userid,
8104 s.timemodified as datesubmitted,
8105 g.grade as rawgrade,
8106 g.timemodified as dategraded,
8107 g.grader as usermodified
8108 FROM {user} u
8109 LEFT JOIN {assign_submission} s
8110 ON u.id = s.userid and s.assignment = :assignid1 AND
8111 s.latest = 1
8112 JOIN {assign_grades} g
8113 ON u.id = g.userid and g.assignment = :assignid2 AND
8114 g.attemptnumber = s.attemptnumber' .
8115 $where, $params);
8117 foreach ($graderesults as $result) {
8118 $gradingstatus = $this->get_grading_status($result->userid);
8119 if (!$this->get_instance()->markingworkflow || $gradingstatus == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
8120 $gradebookgrade = clone $result;
8121 // Now get the feedback.
8122 if ($gradebookplugin) {
8123 $grade = $this->get_user_grade($result->userid, false);
8124 if ($grade) {
8125 $gradebookgrade->feedback = $gradebookplugin->text_for_gradebook($grade);
8126 $gradebookgrade->feedbackformat = $gradebookplugin->format_for_gradebook($grade);
8129 $grades[$gradebookgrade->userid] = $gradebookgrade;
8133 $graderesults->close();
8134 return $grades;
8138 * Call the static version of this function
8140 * @param int $userid The userid to lookup
8141 * @return int The unique id
8143 public function get_uniqueid_for_user($userid) {
8144 return self::get_uniqueid_for_user_static($this->get_instance()->id, $userid);
8148 * Foreach participant in the course - assign them a random id.
8150 * @param int $assignid The assignid to lookup
8152 public static function allocate_unique_ids($assignid) {
8153 global $DB;
8155 $cm = get_coursemodule_from_instance('assign', $assignid, 0, false, MUST_EXIST);
8156 $context = context_module::instance($cm->id);
8158 $currentgroup = groups_get_activity_group($cm, true);
8159 $users = get_enrolled_users($context, "mod/assign:submit", $currentgroup, 'u.id');
8161 // Shuffle the users.
8162 shuffle($users);
8164 foreach ($users as $user) {
8165 $record = $DB->get_record('assign_user_mapping',
8166 array('assignment'=>$assignid, 'userid'=>$user->id),
8167 'id');
8168 if (!$record) {
8169 $record = new stdClass();
8170 $record->assignment = $assignid;
8171 $record->userid = $user->id;
8172 $DB->insert_record('assign_user_mapping', $record);
8178 * Lookup this user id and return the unique id for this assignment.
8180 * @param int $assignid The assignment id
8181 * @param int $userid The userid to lookup
8182 * @return int The unique id
8184 public static function get_uniqueid_for_user_static($assignid, $userid) {
8185 global $DB;
8187 // Search for a record.
8188 $params = array('assignment'=>$assignid, 'userid'=>$userid);
8189 if ($record = $DB->get_record('assign_user_mapping', $params, 'id')) {
8190 return $record->id;
8193 // Be a little smart about this - there is no record for the current user.
8194 // We should ensure any unallocated ids for the current participant
8195 // list are distrubited randomly.
8196 self::allocate_unique_ids($assignid);
8198 // Retry the search for a record.
8199 if ($record = $DB->get_record('assign_user_mapping', $params, 'id')) {
8200 return $record->id;
8203 // The requested user must not be a participant. Add a record anyway.
8204 $record = new stdClass();
8205 $record->assignment = $assignid;
8206 $record->userid = $userid;
8208 return $DB->insert_record('assign_user_mapping', $record);
8212 * Call the static version of this function.
8214 * @param int $uniqueid The uniqueid to lookup
8215 * @return int The user id or false if they don't exist
8217 public function get_user_id_for_uniqueid($uniqueid) {
8218 return self::get_user_id_for_uniqueid_static($this->get_instance()->id, $uniqueid);
8222 * Lookup this unique id and return the user id for this assignment.
8224 * @param int $assignid The id of the assignment this user mapping is in
8225 * @param int $uniqueid The uniqueid to lookup
8226 * @return int The user id or false if they don't exist
8228 public static function get_user_id_for_uniqueid_static($assignid, $uniqueid) {
8229 global $DB;
8231 // Search for a record.
8232 if ($record = $DB->get_record('assign_user_mapping',
8233 array('assignment'=>$assignid, 'id'=>$uniqueid),
8234 'userid',
8235 IGNORE_MISSING)) {
8236 return $record->userid;
8239 return false;
8243 * Get the list of marking_workflow states the current user has permission to transition a grade to.
8245 * @return array of state => description
8247 public function get_marking_workflow_states_for_current_user() {
8248 if (!empty($this->markingworkflowstates)) {
8249 return $this->markingworkflowstates;
8251 $states = array();
8252 if (has_capability('mod/assign:grade', $this->context)) {
8253 $states[ASSIGN_MARKING_WORKFLOW_STATE_INMARKING] = get_string('markingworkflowstateinmarking', 'assign');
8254 $states[ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW] = get_string('markingworkflowstatereadyforreview', 'assign');
8256 if (has_any_capability(array('mod/assign:reviewgrades',
8257 'mod/assign:managegrades'), $this->context)) {
8258 $states[ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW] = get_string('markingworkflowstateinreview', 'assign');
8259 $states[ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE] = get_string('markingworkflowstatereadyforrelease', 'assign');
8261 if (has_any_capability(array('mod/assign:releasegrades',
8262 'mod/assign:managegrades'), $this->context)) {
8263 $states[ASSIGN_MARKING_WORKFLOW_STATE_RELEASED] = get_string('markingworkflowstatereleased', 'assign');
8265 $this->markingworkflowstates = $states;
8266 return $this->markingworkflowstates;
8270 * Check is only active users in course should be shown.
8272 * @return bool true if only active users should be shown.
8274 public function show_only_active_users() {
8275 global $CFG;
8277 if (is_null($this->showonlyactiveenrol)) {
8278 $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol);
8279 $this->showonlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol);
8281 if (!is_null($this->context)) {
8282 $this->showonlyactiveenrol = $this->showonlyactiveenrol ||
8283 !has_capability('moodle/course:viewsuspendedusers', $this->context);
8286 return $this->showonlyactiveenrol;
8290 * Return true is user is active user in course else false
8292 * @param int $userid
8293 * @return bool true is user is active in course.
8295 public function is_active_user($userid) {
8296 return !in_array($userid, get_suspended_userids($this->context, true));
8300 * Returns true if gradebook feedback plugin is enabled
8302 * @return bool true if gradebook feedback plugin is enabled and visible else false.
8304 public function is_gradebook_feedback_enabled() {
8305 // Get default grade book feedback plugin.
8306 $adminconfig = $this->get_admin_config();
8307 $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
8308 $gradebookplugin = str_replace('assignfeedback_', '', $gradebookplugin);
8310 // Check if default gradebook feedback is visible and enabled.
8311 $gradebookfeedbackplugin = $this->get_feedback_plugin_by_type($gradebookplugin);
8313 if (empty($gradebookfeedbackplugin)) {
8314 return false;
8317 if ($gradebookfeedbackplugin->is_visible() && $gradebookfeedbackplugin->is_enabled()) {
8318 return true;
8321 // Gradebook feedback plugin is either not visible/enabled.
8322 return false;
8326 * Returns the grading status.
8328 * @param int $userid the user id
8329 * @return string returns the grading status
8331 public function get_grading_status($userid) {
8332 if ($this->get_instance()->markingworkflow) {
8333 $flags = $this->get_user_flags($userid, false);
8334 if (!empty($flags->workflowstate)) {
8335 return $flags->workflowstate;
8337 return ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED;
8338 } else {
8339 $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT);
8340 $grade = $this->get_user_grade($userid, false, $attemptnumber);
8342 if (!empty($grade) && $grade->grade !== null && $grade->grade >= 0) {
8343 return ASSIGN_GRADING_STATUS_GRADED;
8344 } else {
8345 return ASSIGN_GRADING_STATUS_NOT_GRADED;
8351 * The id used to uniquily identify the cache for this instance of the assign object.
8353 * @return string
8355 public function get_useridlist_key_id() {
8356 return $this->useridlistid;
8360 * Generates the key that should be used for an entry in the useridlist cache.
8362 * @param string $id Generate a key for this instance (optional)
8363 * @return string The key for the id, or new entry if no $id is passed.
8365 public function get_useridlist_key($id = null) {
8366 if ($id === null) {
8367 $id = $this->get_useridlist_key_id();
8369 return $this->get_course_module()->id . '_' . $id;
8373 * Updates and creates the completion records in mdl_course_modules_completion.
8375 * @param int $teamsubmission value of 0 or 1 to indicate whether this is a group activity
8376 * @param int $requireallteammemberssubmit value of 0 or 1 to indicate whether all group members must click Submit
8377 * @param obj $submission the submission
8378 * @param int $userid the user id
8379 * @param int $complete
8380 * @param obj $completion
8382 * @return null
8384 protected function update_activity_completion_records($teamsubmission,
8385 $requireallteammemberssubmit,
8386 $submission,
8387 $userid,
8388 $complete,
8389 $completion) {
8391 if (($teamsubmission && $submission->groupid > 0 && !$requireallteammemberssubmit) ||
8392 ($teamsubmission && $submission->groupid > 0 && $requireallteammemberssubmit &&
8393 $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED)) {
8395 $members = groups_get_members($submission->groupid);
8397 foreach ($members as $member) {
8398 $completion->update_state($this->get_course_module(), $complete, $member->id);
8400 } else {
8401 $completion->update_state($this->get_course_module(), $complete, $userid);
8404 return;
8408 * Update the module completion status (set it viewed).
8410 * @since Moodle 3.2
8412 public function set_module_viewed() {
8413 $completion = new completion_info($this->get_course());
8414 $completion->set_module_viewed($this->get_course_module());
8419 * Portfolio caller class for mod_assign.
8421 * @package mod_assign
8422 * @copyright 2012 NetSpot {@link http://www.netspot.com.au}
8423 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
8425 class assign_portfolio_caller extends portfolio_module_caller_base {
8427 /** @var int callback arg - the id of submission we export */
8428 protected $sid;
8430 /** @var string component of the submission files we export*/
8431 protected $component;
8433 /** @var string callback arg - the area of submission files we export */
8434 protected $area;
8436 /** @var int callback arg - the id of file we export */
8437 protected $fileid;
8439 /** @var int callback arg - the cmid of the assignment we export */
8440 protected $cmid;
8442 /** @var string callback arg - the plugintype of the editor we export */
8443 protected $plugin;
8445 /** @var string callback arg - the name of the editor field we export */
8446 protected $editor;
8449 * Callback arg for a single file export.
8451 public static function expected_callbackargs() {
8452 return array(
8453 'cmid' => true,
8454 'sid' => false,
8455 'area' => false,
8456 'component' => false,
8457 'fileid' => false,
8458 'plugin' => false,
8459 'editor' => false,
8464 * The constructor.
8466 * @param array $callbackargs
8468 public function __construct($callbackargs) {
8469 parent::__construct($callbackargs);
8470 $this->cm = get_coursemodule_from_id('assign', $this->cmid, 0, false, MUST_EXIST);
8474 * Load data needed for the portfolio export.
8476 * If the assignment type implements portfolio_load_data(), the processing is delegated
8477 * to it. Otherwise, the caller must provide either fileid (to export single file) or
8478 * submissionid and filearea (to export all data attached to the given submission file area)
8479 * via callback arguments.
8481 * @throws portfolio_caller_exception
8483 public function load_data() {
8485 $context = context_module::instance($this->cmid);
8487 if (empty($this->fileid)) {
8488 if (empty($this->sid) || empty($this->area)) {
8489 throw new portfolio_caller_exception('invalidfileandsubmissionid', 'mod_assign');
8494 // Export either an area of files or a single file (see function for more detail).
8495 // The first arg is an id or null. If it is an id, the rest of the args are ignored.
8496 // If it is null, the rest of the args are used to load a list of files from get_areafiles.
8497 $this->set_file_and_format_data($this->fileid,
8498 $context->id,
8499 $this->component,
8500 $this->area,
8501 $this->sid,
8502 'timemodified',
8503 false);
8508 * Prepares the package up before control is passed to the portfolio plugin.
8510 * @throws portfolio_caller_exception
8511 * @return mixed
8513 public function prepare_package() {
8515 if ($this->plugin && $this->editor) {
8516 $options = portfolio_format_text_options();
8517 $context = context_module::instance($this->cmid);
8518 $options->context = $context;
8520 $plugin = $this->get_submission_plugin();
8522 $text = $plugin->get_editor_text($this->editor, $this->sid);
8523 $format = $plugin->get_editor_format($this->editor, $this->sid);
8525 $html = format_text($text, $format, $options);
8526 $html = portfolio_rewrite_pluginfile_urls($html,
8527 $context->id,
8528 'mod_assign',
8529 $this->area,
8530 $this->sid,
8531 $this->exporter->get('format'));
8533 $exporterclass = $this->exporter->get('formatclass');
8534 if (in_array($exporterclass, array(PORTFOLIO_FORMAT_PLAINHTML, PORTFOLIO_FORMAT_RICHHTML))) {
8535 if ($files = $this->exporter->get('caller')->get('multifiles')) {
8536 foreach ($files as $file) {
8537 $this->exporter->copy_existing_file($file);
8540 return $this->exporter->write_new_file($html, 'assignment.html', !empty($files));
8541 } else if ($this->exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) {
8542 $leapwriter = $this->exporter->get('format')->leap2a_writer();
8543 $entry = new portfolio_format_leap2a_entry($this->area . $this->cmid,
8544 $context->get_context_name(),
8545 'resource',
8546 $html);
8548 $entry->add_category('web', 'resource_type');
8549 $entry->author = $this->user;
8550 $leapwriter->add_entry($entry);
8551 if ($files = $this->exporter->get('caller')->get('multifiles')) {
8552 $leapwriter->link_files($entry, $files, $this->area . $this->cmid . 'file');
8553 foreach ($files as $file) {
8554 $this->exporter->copy_existing_file($file);
8557 return $this->exporter->write_new_file($leapwriter->to_xml(),
8558 $this->exporter->get('format')->manifest_name(),
8559 true);
8560 } else {
8561 debugging('invalid format class: ' . $this->exporter->get('formatclass'));
8566 if ($this->exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) {
8567 $leapwriter = $this->exporter->get('format')->leap2a_writer();
8568 $files = array();
8569 if ($this->singlefile) {
8570 $files[] = $this->singlefile;
8571 } else if ($this->multifiles) {
8572 $files = $this->multifiles;
8573 } else {
8574 throw new portfolio_caller_exception('invalidpreparepackagefile',
8575 'portfolio',
8576 $this->get_return_url());
8579 $entryids = array();
8580 foreach ($files as $file) {
8581 $entry = new portfolio_format_leap2a_file($file->get_filename(), $file);
8582 $entry->author = $this->user;
8583 $leapwriter->add_entry($entry);
8584 $this->exporter->copy_existing_file($file);
8585 $entryids[] = $entry->id;
8587 if (count($files) > 1) {
8588 $baseid = 'assign' . $this->cmid . $this->area;
8589 $context = context_module::instance($this->cmid);
8591 // If we have multiple files, they should be grouped together into a folder.
8592 $entry = new portfolio_format_leap2a_entry($baseid . 'group',
8593 $context->get_context_name(),
8594 'selection');
8595 $leapwriter->add_entry($entry);
8596 $leapwriter->make_selection($entry, $entryids, 'Folder');
8598 return $this->exporter->write_new_file($leapwriter->to_xml(),
8599 $this->exporter->get('format')->manifest_name(),
8600 true);
8602 return $this->prepare_package_file();
8606 * Fetch the plugin by its type.
8608 * @return assign_submission_plugin
8610 protected function get_submission_plugin() {
8611 global $CFG;
8612 if (!$this->plugin || !$this->cmid) {
8613 return null;
8616 require_once($CFG->dirroot . '/mod/assign/locallib.php');
8618 $context = context_module::instance($this->cmid);
8620 $assignment = new assign($context, null, null);
8621 return $assignment->get_submission_plugin_by_type($this->plugin);
8625 * Calculate a sha1 has of either a single file or a list
8626 * of files based on the data set by load_data.
8628 * @return string
8630 public function get_sha1() {
8632 if ($this->plugin && $this->editor) {
8633 $plugin = $this->get_submission_plugin();
8634 $options = portfolio_format_text_options();
8635 $options->context = context_module::instance($this->cmid);
8637 $text = format_text($plugin->get_editor_text($this->editor, $this->sid),
8638 $plugin->get_editor_format($this->editor, $this->sid),
8639 $options);
8640 $textsha1 = sha1($text);
8641 $filesha1 = '';
8642 try {
8643 $filesha1 = $this->get_sha1_file();
8644 } catch (portfolio_caller_exception $e) {
8645 // No files.
8647 return sha1($textsha1 . $filesha1);
8649 return $this->get_sha1_file();
8653 * Calculate the time to transfer either a single file or a list
8654 * of files based on the data set by load_data.
8656 * @return int
8658 public function expected_time() {
8659 return $this->expected_time_file();
8663 * Checking the permissions.
8665 * @return bool
8667 public function check_permissions() {
8668 $context = context_module::instance($this->cmid);
8669 return has_capability('mod/assign:exportownsubmission', $context);
8673 * Display a module name.
8675 * @return string
8677 public static function display_name() {
8678 return get_string('modulename', 'assign');
8682 * Return array of formats supported by this portfolio call back.
8684 * @return array
8686 public static function base_supported_formats() {
8687 return array(PORTFOLIO_FORMAT_FILE, PORTFOLIO_FORMAT_LEAP2A);
8692 * Logic to happen when a/some group(s) has/have been deleted in a course.
8694 * @param int $courseid The course ID.
8695 * @param int $groupid The group id if it is known
8696 * @return void
8698 function assign_process_group_deleted_in_course($courseid, $groupid = null) {
8699 global $DB;
8701 $params = array('courseid' => $courseid);
8702 if ($groupid) {
8703 $params['groupid'] = $groupid;
8704 // We just update the group that was deleted.
8705 $sql = "SELECT o.id, o.assignid
8706 FROM {assign_overrides} o
8707 JOIN {assign} assign ON assign.id = o.assignid
8708 WHERE assign.course = :courseid
8709 AND o.groupid = :groupid";
8710 } else {
8711 // No groupid, we update all orphaned group overrides for all assign in course.
8712 $sql = "SELECT o.id, o.assignid
8713 FROM {assign_overrides} o
8714 JOIN {assign} assign ON assign.id = o.assignid
8715 LEFT JOIN {groups} grp ON grp.id = o.groupid
8716 WHERE assign.course = :courseid
8717 AND o.groupid IS NOT NULL
8718 AND grp.id IS NULL";
8720 $records = $DB->get_records_sql_menu($sql, $params);
8721 if (!$records) {
8722 return; // Nothing to do.
8724 $DB->delete_records_list('assign_overrides', 'id', array_keys($records));
8728 * Change the sort order of an override
8730 * @param int $id of the override
8731 * @param string $move direction of move
8732 * @param int $assignid of the assignment
8733 * @return bool success of operation
8735 function move_group_override($id, $move, $assignid) {
8736 global $DB;
8738 // Get the override object.
8739 if (!$override = $DB->get_record('assign_overrides', array('id' => $id), 'id, sortorder')) {
8740 return false;
8742 // Count the number of group overrides.
8743 $overridecountgroup = $DB->count_records('assign_overrides', array('userid' => null, 'assignid' => $assignid));
8745 // Calculate the new sortorder.
8746 if ( ($move == 'up') and ($override->sortorder > 1)) {
8747 $neworder = $override->sortorder - 1;
8748 } else if (($move == 'down') and ($override->sortorder < $overridecountgroup)) {
8749 $neworder = $override->sortorder + 1;
8750 } else {
8751 return false;
8754 // Retrieve the override object that is currently residing in the new position.
8755 $params = array('sortorder' => $neworder, 'assignid' => $assignid);
8756 if ($swapoverride = $DB->get_record('assign_overrides', $params, 'id, sortorder')) {
8758 // Swap the sortorders.
8759 $swapoverride->sortorder = $override->sortorder;
8760 $override->sortorder = $neworder;
8762 // Update the override records.
8763 $DB->update_record('assign_overrides', $override);
8764 $DB->update_record('assign_overrides', $swapoverride);
8767 reorder_group_overrides($assignid);
8768 return true;
8772 * Reorder the overrides starting at the override at the given startorder.
8774 * @param int $assignid of the assigment
8776 function reorder_group_overrides($assignid) {
8777 global $DB;
8779 $i = 1;
8780 if ($overrides = $DB->get_records('assign_overrides', array('userid' => null, 'assignid' => $assignid), 'sortorder ASC')) {
8781 foreach ($overrides as $override) {
8782 $f = new stdClass();
8783 $f->id = $override->id;
8784 $f->sortorder = $i++;
8785 $DB->update_record('assign_overrides', $f);