Merge branch 'MDL-50724' of git://github.com/andrewhancox/moodle
[moodle.git] / mod / assign / locallib.php
blobd6ba3826b92d1942d351be72ed9d228f9203790d
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 // Name of file area for intro attachments.
65 define('ASSIGN_INTROATTACHMENT_FILEAREA', 'introattachment');
67 require_once($CFG->libdir . '/accesslib.php');
68 require_once($CFG->libdir . '/formslib.php');
69 require_once($CFG->dirroot . '/repository/lib.php');
70 require_once($CFG->dirroot . '/mod/assign/mod_form.php');
71 require_once($CFG->libdir . '/gradelib.php');
72 require_once($CFG->dirroot . '/grade/grading/lib.php');
73 require_once($CFG->dirroot . '/mod/assign/feedbackplugin.php');
74 require_once($CFG->dirroot . '/mod/assign/submissionplugin.php');
75 require_once($CFG->dirroot . '/mod/assign/renderable.php');
76 require_once($CFG->dirroot . '/mod/assign/gradingtable.php');
77 require_once($CFG->libdir . '/eventslib.php');
78 require_once($CFG->libdir . '/portfolio/caller.php');
80 /**
81 * Standard base class for mod_assign (assignment types).
83 * @package mod_assign
84 * @copyright 2012 NetSpot {@link http://www.netspot.com.au}
85 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
87 class assign {
89 /** @var stdClass the assignment record that contains the global settings for this assign instance */
90 private $instance;
92 /** @var stdClass the grade_item record for this assign instance's primary grade item. */
93 private $gradeitem;
95 /** @var context the context of the course module for this assign instance
96 * (or just the course if we are creating a new one)
98 private $context;
100 /** @var stdClass the course this assign instance belongs to */
101 private $course;
103 /** @var stdClass the admin config for all assign instances */
104 private $adminconfig;
106 /** @var assign_renderer the custom renderer for this module */
107 private $output;
109 /** @var cm_info the course module for this assign instance */
110 private $coursemodule;
112 /** @var array cache for things like the coursemodule name or the scale menu -
113 * only lives for a single request.
115 private $cache;
117 /** @var array list of the installed submission plugins */
118 private $submissionplugins;
120 /** @var array list of the installed feedback plugins */
121 private $feedbackplugins;
123 /** @var string action to be used to return to this page
124 * (without repeating any form submissions etc).
126 private $returnaction = 'view';
128 /** @var array params to be used to return to this page */
129 private $returnparams = array();
131 /** @var string modulename prevents excessive calls to get_string */
132 private static $modulename = null;
134 /** @var string modulenameplural prevents excessive calls to get_string */
135 private static $modulenameplural = null;
137 /** @var array of marking workflow states for the current user */
138 private $markingworkflowstates = null;
140 /** @var bool whether to exclude users with inactive enrolment */
141 private $showonlyactiveenrol = null;
143 /** @var array cached list of participants for this assignment. The cache key will be group, showactive and the context id */
144 private $participants = array();
146 /** @var array cached list of user groups when team submissions are enabled. The cache key will be the user. */
147 private $usersubmissiongroups = array();
149 /** @var array cached list of user groups. The cache key will be the user. */
150 private $usergroups = array();
153 * Constructor for the base assign class.
155 * Note: For $coursemodule you can supply a stdclass if you like, but it
156 * will be more efficient to supply a cm_info object.
158 * @param mixed $coursemodulecontext context|null the course module context
159 * (or the course context if the coursemodule has not been
160 * created yet).
161 * @param mixed $coursemodule the current course module if it was already loaded,
162 * otherwise this class will load one from the context as required.
163 * @param mixed $course the current course if it was already loaded,
164 * otherwise this class will load one from the context as required.
166 public function __construct($coursemodulecontext, $coursemodule, $course) {
167 $this->context = $coursemodulecontext;
168 $this->course = $course;
170 // Ensure that $this->coursemodule is a cm_info object (or null).
171 $this->coursemodule = cm_info::create($coursemodule);
173 // Temporary cache only lives for a single request - used to reduce db lookups.
174 $this->cache = array();
176 $this->submissionplugins = $this->load_plugins('assignsubmission');
177 $this->feedbackplugins = $this->load_plugins('assignfeedback');
181 * Set the action and parameters that can be used to return to the current page.
183 * @param string $action The action for the current page
184 * @param array $params An array of name value pairs which form the parameters
185 * to return to the current page.
186 * @return void
188 public function register_return_link($action, $params) {
189 global $PAGE;
190 $params['action'] = $action;
191 $currenturl = $PAGE->url;
193 $currenturl->params($params);
194 $PAGE->set_url($currenturl);
198 * Return an action that can be used to get back to the current page.
200 * @return string action
202 public function get_return_action() {
203 global $PAGE;
205 $params = $PAGE->url->params();
207 if (!empty($params['action'])) {
208 return $params['action'];
210 return '';
214 * Based on the current assignment settings should we display the intro.
216 * @return bool showintro
218 public function show_intro() {
219 if ($this->get_instance()->alwaysshowdescription ||
220 time() > $this->get_instance()->allowsubmissionsfromdate) {
221 return true;
223 return false;
227 * Return a list of parameters that can be used to get back to the current page.
229 * @return array params
231 public function get_return_params() {
232 global $PAGE;
234 $params = $PAGE->url->params();
235 unset($params['id']);
236 unset($params['action']);
237 return $params;
241 * Set the submitted form data.
243 * @param stdClass $data The form data (instance)
245 public function set_instance(stdClass $data) {
246 $this->instance = $data;
250 * Set the context.
252 * @param context $context The new context
254 public function set_context(context $context) {
255 $this->context = $context;
259 * Set the course data.
261 * @param stdClass $course The course data
263 public function set_course(stdClass $course) {
264 $this->course = $course;
268 * Get list of feedback plugins installed.
270 * @return array
272 public function get_feedback_plugins() {
273 return $this->feedbackplugins;
277 * Get list of submission plugins installed.
279 * @return array
281 public function get_submission_plugins() {
282 return $this->submissionplugins;
286 * Is blind marking enabled and reveal identities not set yet?
288 * @return bool
290 public function is_blind_marking() {
291 return $this->get_instance()->blindmarking && !$this->get_instance()->revealidentities;
295 * Does an assignment have submission(s) or grade(s) already?
297 * @return bool
299 public function has_submissions_or_grades() {
300 $allgrades = $this->count_grades();
301 $allsubmissions = $this->count_submissions();
302 if (($allgrades == 0) && ($allsubmissions == 0)) {
303 return false;
305 return true;
309 * Get a specific submission plugin by its type.
311 * @param string $subtype assignsubmission | assignfeedback
312 * @param string $type
313 * @return mixed assign_plugin|null
315 public function get_plugin_by_type($subtype, $type) {
316 $shortsubtype = substr($subtype, strlen('assign'));
317 $name = $shortsubtype . 'plugins';
318 if ($name != 'feedbackplugins' && $name != 'submissionplugins') {
319 return null;
321 $pluginlist = $this->$name;
322 foreach ($pluginlist as $plugin) {
323 if ($plugin->get_type() == $type) {
324 return $plugin;
327 return null;
331 * Get a feedback plugin by type.
333 * @param string $type - The type of plugin e.g comments
334 * @return mixed assign_feedback_plugin|null
336 public function get_feedback_plugin_by_type($type) {
337 return $this->get_plugin_by_type('assignfeedback', $type);
341 * Get a submission plugin by type.
343 * @param string $type - The type of plugin e.g comments
344 * @return mixed assign_submission_plugin|null
346 public function get_submission_plugin_by_type($type) {
347 return $this->get_plugin_by_type('assignsubmission', $type);
351 * Load the plugins from the sub folders under subtype.
353 * @param string $subtype - either submission or feedback
354 * @return array - The sorted list of plugins
356 protected function load_plugins($subtype) {
357 global $CFG;
358 $result = array();
360 $names = core_component::get_plugin_list($subtype);
362 foreach ($names as $name => $path) {
363 if (file_exists($path . '/locallib.php')) {
364 require_once($path . '/locallib.php');
366 $shortsubtype = substr($subtype, strlen('assign'));
367 $pluginclass = 'assign_' . $shortsubtype . '_' . $name;
369 $plugin = new $pluginclass($this, $name);
371 if ($plugin instanceof assign_plugin) {
372 $idx = $plugin->get_sort_order();
373 while (array_key_exists($idx, $result)) {
374 $idx +=1;
376 $result[$idx] = $plugin;
380 ksort($result);
381 return $result;
385 * Display the assignment, used by view.php
387 * The assignment is displayed differently depending on your role,
388 * the settings for the assignment and the status of the assignment.
390 * @param string $action The current action if any.
391 * @return string - The page output.
393 public function view($action='') {
395 $o = '';
396 $mform = null;
397 $notices = array();
398 $nextpageparams = array();
400 if (!empty($this->get_course_module()->id)) {
401 $nextpageparams['id'] = $this->get_course_module()->id;
404 // Handle form submissions first.
405 if ($action == 'savesubmission') {
406 $action = 'editsubmission';
407 if ($this->process_save_submission($mform, $notices)) {
408 $action = 'redirect';
409 $nextpageparams['action'] = 'view';
411 } else if ($action == 'editprevioussubmission') {
412 $action = 'editsubmission';
413 if ($this->process_copy_previous_attempt($notices)) {
414 $action = 'redirect';
415 $nextpageparams['action'] = 'editsubmission';
417 } else if ($action == 'lock') {
418 $this->process_lock_submission();
419 $action = 'redirect';
420 $nextpageparams['action'] = 'grading';
421 } else if ($action == 'addattempt') {
422 $this->process_add_attempt(required_param('userid', PARAM_INT));
423 $action = 'redirect';
424 $nextpageparams['action'] = 'grading';
425 } else if ($action == 'reverttodraft') {
426 $this->process_revert_to_draft();
427 $action = 'redirect';
428 $nextpageparams['action'] = 'grading';
429 } else if ($action == 'unlock') {
430 $this->process_unlock_submission();
431 $action = 'redirect';
432 $nextpageparams['action'] = 'grading';
433 } else if ($action == 'setbatchmarkingworkflowstate') {
434 $this->process_set_batch_marking_workflow_state();
435 $action = 'redirect';
436 $nextpageparams['action'] = 'grading';
437 } else if ($action == 'setbatchmarkingallocation') {
438 $this->process_set_batch_marking_allocation();
439 $action = 'redirect';
440 $nextpageparams['action'] = 'grading';
441 } else if ($action == 'confirmsubmit') {
442 $action = 'submit';
443 if ($this->process_submit_for_grading($mform, $notices)) {
444 $action = 'redirect';
445 $nextpageparams['action'] = 'view';
446 } else if ($notices) {
447 $action = 'viewsubmitforgradingerror';
449 } else if ($action == 'submitotherforgrading') {
450 if ($this->process_submit_other_for_grading($mform, $notices)) {
451 $action = 'redirect';
452 $nextpageparams['action'] = 'grading';
453 } else {
454 $action = 'viewsubmitforgradingerror';
456 } else if ($action == 'gradingbatchoperation') {
457 $action = $this->process_grading_batch_operation($mform);
458 if ($action == 'grading') {
459 $action = 'redirect';
460 $nextpageparams['action'] = 'grading';
462 } else if ($action == 'submitgrade') {
463 if (optional_param('saveandshownext', null, PARAM_RAW)) {
464 // Save and show next.
465 $action = 'grade';
466 if ($this->process_save_grade($mform)) {
467 $action = 'redirect';
468 $nextpageparams['action'] = 'grade';
469 $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) + 1;
470 $nextpageparams['useridlistid'] = optional_param('useridlistid', time(), PARAM_INT);
472 } else if (optional_param('nosaveandprevious', null, PARAM_RAW)) {
473 $action = 'redirect';
474 $nextpageparams['action'] = 'grade';
475 $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) - 1;
476 $nextpageparams['useridlistid'] = optional_param('useridlistid', time(), PARAM_INT);
477 } else if (optional_param('nosaveandnext', null, PARAM_RAW)) {
478 $action = 'redirect';
479 $nextpageparams['action'] = 'grade';
480 $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) + 1;
481 $nextpageparams['useridlistid'] = optional_param('useridlistid', time(), PARAM_INT);
482 } else if (optional_param('savegrade', null, PARAM_RAW)) {
483 // Save changes button.
484 $action = 'grade';
485 if ($this->process_save_grade($mform)) {
486 $action = 'redirect';
487 $nextpageparams['action'] = 'savegradingresult';
489 } else {
490 // Cancel button.
491 $action = 'redirect';
492 $nextpageparams['action'] = 'grading';
494 } else if ($action == 'quickgrade') {
495 $message = $this->process_save_quick_grades();
496 $action = 'quickgradingresult';
497 } else if ($action == 'saveoptions') {
498 $this->process_save_grading_options();
499 $action = 'redirect';
500 $nextpageparams['action'] = 'grading';
501 } else if ($action == 'saveextension') {
502 $action = 'grantextension';
503 if ($this->process_save_extension($mform)) {
504 $action = 'redirect';
505 $nextpageparams['action'] = 'grading';
507 } else if ($action == 'revealidentitiesconfirm') {
508 $this->process_reveal_identities();
509 $action = 'redirect';
510 $nextpageparams['action'] = 'grading';
513 $returnparams = array('rownum'=>optional_param('rownum', 0, PARAM_INT),
514 'useridlistid'=>optional_param('useridlistid', 0, PARAM_INT));
515 $this->register_return_link($action, $returnparams);
517 // Now show the right view page.
518 if ($action == 'redirect') {
519 $nextpageurl = new moodle_url('/mod/assign/view.php', $nextpageparams);
520 redirect($nextpageurl);
521 return;
522 } else if ($action == 'savegradingresult') {
523 $message = get_string('gradingchangessaved', 'assign');
524 $o .= $this->view_savegrading_result($message);
525 } else if ($action == 'quickgradingresult') {
526 $mform = null;
527 $o .= $this->view_quickgrading_result($message);
528 } else if ($action == 'grade') {
529 $o .= $this->view_single_grade_page($mform);
530 } else if ($action == 'viewpluginassignfeedback') {
531 $o .= $this->view_plugin_content('assignfeedback');
532 } else if ($action == 'viewpluginassignsubmission') {
533 $o .= $this->view_plugin_content('assignsubmission');
534 } else if ($action == 'editsubmission') {
535 $o .= $this->view_edit_submission_page($mform, $notices);
536 } else if ($action == 'grading') {
537 $o .= $this->view_grading_page();
538 } else if ($action == 'downloadall') {
539 $o .= $this->download_submissions();
540 } else if ($action == 'submit') {
541 $o .= $this->check_submit_for_grading($mform);
542 } else if ($action == 'grantextension') {
543 $o .= $this->view_grant_extension($mform);
544 } else if ($action == 'revealidentities') {
545 $o .= $this->view_reveal_identities_confirm($mform);
546 } else if ($action == 'plugingradingbatchoperation') {
547 $o .= $this->view_plugin_grading_batch_operation($mform);
548 } else if ($action == 'viewpluginpage') {
549 $o .= $this->view_plugin_page();
550 } else if ($action == 'viewcourseindex') {
551 $o .= $this->view_course_index();
552 } else if ($action == 'viewbatchsetmarkingworkflowstate') {
553 $o .= $this->view_batch_set_workflow_state($mform);
554 } else if ($action == 'viewbatchmarkingallocation') {
555 $o .= $this->view_batch_markingallocation($mform);
556 } else if ($action == 'viewsubmitforgradingerror') {
557 $o .= $this->view_error_page(get_string('submitforgrading', 'assign'), $notices);
558 } else {
559 $o .= $this->view_submission_page();
562 return $o;
566 * Add this instance to the database.
568 * @param stdClass $formdata The data submitted from the form
569 * @param bool $callplugins This is used to skip the plugin code
570 * when upgrading an old assignment to a new one (the plugins get called manually)
571 * @return mixed false if an error occurs or the int id of the new instance
573 public function add_instance(stdClass $formdata, $callplugins) {
574 global $DB;
575 $adminconfig = $this->get_admin_config();
577 $err = '';
579 // Add the database record.
580 $update = new stdClass();
581 $update->name = $formdata->name;
582 $update->timemodified = time();
583 $update->timecreated = time();
584 $update->course = $formdata->course;
585 $update->courseid = $formdata->course;
586 $update->intro = $formdata->intro;
587 $update->introformat = $formdata->introformat;
588 $update->alwaysshowdescription = !empty($formdata->alwaysshowdescription);
589 $update->submissiondrafts = $formdata->submissiondrafts;
590 $update->requiresubmissionstatement = $formdata->requiresubmissionstatement;
591 $update->sendnotifications = $formdata->sendnotifications;
592 $update->sendlatenotifications = $formdata->sendlatenotifications;
593 $update->sendstudentnotifications = $adminconfig->sendstudentnotifications;
594 if (isset($formdata->sendstudentnotifications)) {
595 $update->sendstudentnotifications = $formdata->sendstudentnotifications;
597 $update->duedate = $formdata->duedate;
598 $update->cutoffdate = $formdata->cutoffdate;
599 $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate;
600 $update->grade = $formdata->grade;
601 $update->completionsubmit = !empty($formdata->completionsubmit);
602 $update->teamsubmission = $formdata->teamsubmission;
603 $update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit;
604 if (isset($formdata->teamsubmissiongroupingid)) {
605 $update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid;
607 $update->blindmarking = $formdata->blindmarking;
608 $update->attemptreopenmethod = ASSIGN_ATTEMPT_REOPEN_METHOD_NONE;
609 if (!empty($formdata->attemptreopenmethod)) {
610 $update->attemptreopenmethod = $formdata->attemptreopenmethod;
612 if (!empty($formdata->maxattempts)) {
613 $update->maxattempts = $formdata->maxattempts;
615 if (isset($formdata->preventsubmissionnotingroup)) {
616 $update->preventsubmissionnotingroup = $formdata->preventsubmissionnotingroup;
618 $update->markingworkflow = $formdata->markingworkflow;
619 $update->markingallocation = $formdata->markingallocation;
620 if (empty($update->markingworkflow)) { // If marking workflow is disabled, make sure allocation is disabled.
621 $update->markingallocation = 0;
624 $returnid = $DB->insert_record('assign', $update);
625 $this->instance = $DB->get_record('assign', array('id'=>$returnid), '*', MUST_EXIST);
626 // Cache the course record.
627 $this->course = $DB->get_record('course', array('id'=>$formdata->course), '*', MUST_EXIST);
629 $this->save_intro_draft_files($formdata);
631 if ($callplugins) {
632 // Call save_settings hook for submission plugins.
633 foreach ($this->submissionplugins as $plugin) {
634 if (!$this->update_plugin_instance($plugin, $formdata)) {
635 print_error($plugin->get_error());
636 return false;
639 foreach ($this->feedbackplugins as $plugin) {
640 if (!$this->update_plugin_instance($plugin, $formdata)) {
641 print_error($plugin->get_error());
642 return false;
646 // In the case of upgrades the coursemodule has not been set,
647 // so we need to wait before calling these two.
648 $this->update_calendar($formdata->coursemodule);
649 $this->update_gradebook(false, $formdata->coursemodule);
653 $update = new stdClass();
654 $update->id = $this->get_instance()->id;
655 $update->nosubmissions = (!$this->is_any_submission_plugin_enabled()) ? 1: 0;
656 $DB->update_record('assign', $update);
658 return $returnid;
662 * Delete all grades from the gradebook for this assignment.
664 * @return bool
666 protected function delete_grades() {
667 global $CFG;
669 $result = grade_update('mod/assign',
670 $this->get_course()->id,
671 'mod',
672 'assign',
673 $this->get_instance()->id,
675 null,
676 array('deleted'=>1));
677 return $result == GRADE_UPDATE_OK;
681 * Delete this instance from the database.
683 * @return bool false if an error occurs
685 public function delete_instance() {
686 global $DB;
687 $result = true;
689 foreach ($this->submissionplugins as $plugin) {
690 if (!$plugin->delete_instance()) {
691 print_error($plugin->get_error());
692 $result = false;
695 foreach ($this->feedbackplugins as $plugin) {
696 if (!$plugin->delete_instance()) {
697 print_error($plugin->get_error());
698 $result = false;
702 // Delete files associated with this assignment.
703 $fs = get_file_storage();
704 if (! $fs->delete_area_files($this->context->id) ) {
705 $result = false;
708 // Delete_records will throw an exception if it fails - so no need for error checking here.
709 $DB->delete_records('assign_submission', array('assignment' => $this->get_instance()->id));
710 $DB->delete_records('assign_grades', array('assignment' => $this->get_instance()->id));
711 $DB->delete_records('assign_plugin_config', array('assignment' => $this->get_instance()->id));
712 $DB->delete_records('assign_user_flags', array('assignment' => $this->get_instance()->id));
713 $DB->delete_records('assign_user_mapping', array('assignment' => $this->get_instance()->id));
715 // Delete items from the gradebook.
716 if (! $this->delete_grades()) {
717 $result = false;
720 // Delete the instance.
721 $DB->delete_records('assign', array('id'=>$this->get_instance()->id));
723 return $result;
727 * Actual implementation of the reset course functionality, delete all the
728 * assignment submissions for course $data->courseid.
730 * @param stdClass $data the data submitted from the reset course.
731 * @return array status array
733 public function reset_userdata($data) {
734 global $CFG, $DB;
736 $componentstr = get_string('modulenameplural', 'assign');
737 $status = array();
739 $fs = get_file_storage();
740 if (!empty($data->reset_assign_submissions)) {
741 // Delete files associated with this assignment.
742 foreach ($this->submissionplugins as $plugin) {
743 $fileareas = array();
744 $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type();
745 $fileareas = $plugin->get_file_areas();
746 foreach ($fileareas as $filearea => $notused) {
747 $fs->delete_area_files($this->context->id, $plugincomponent, $filearea);
750 if (!$plugin->delete_instance()) {
751 $status[] = array('component'=>$componentstr,
752 'item'=>get_string('deleteallsubmissions', 'assign'),
753 'error'=>$plugin->get_error());
757 foreach ($this->feedbackplugins as $plugin) {
758 $fileareas = array();
759 $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type();
760 $fileareas = $plugin->get_file_areas();
761 foreach ($fileareas as $filearea => $notused) {
762 $fs->delete_area_files($this->context->id, $plugincomponent, $filearea);
765 if (!$plugin->delete_instance()) {
766 $status[] = array('component'=>$componentstr,
767 'item'=>get_string('deleteallsubmissions', 'assign'),
768 'error'=>$plugin->get_error());
772 $assignids = $DB->get_records('assign', array('course' => $data->courseid), '', 'id');
773 list($sql, $params) = $DB->get_in_or_equal(array_keys($assignids));
775 $DB->delete_records_select('assign_submission', "assignment $sql", $params);
776 $DB->delete_records_select('assign_user_flags', "assignment $sql", $params);
778 $status[] = array('component'=>$componentstr,
779 'item'=>get_string('deleteallsubmissions', 'assign'),
780 'error'=>false);
782 if (!empty($data->reset_gradebook_grades)) {
783 $DB->delete_records_select('assign_grades', "assignment $sql", $params);
784 // Remove all grades from gradebook.
785 require_once($CFG->dirroot.'/mod/assign/lib.php');
786 assign_reset_gradebook($data->courseid);
788 // Reset revealidentities if both submissions and grades have been reset.
789 if ($this->get_instance()->blindmarking && $this->get_instance()->revealidentities) {
790 $DB->set_field('assign', 'revealidentities', 0, array('id' => $this->get_instance()->id));
794 // Updating dates - shift may be negative too.
795 if ($data->timeshift) {
796 shift_course_mod_dates('assign',
797 array('duedate', 'allowsubmissionsfromdate', 'cutoffdate'),
798 $data->timeshift,
799 $data->courseid, $this->get_instance()->id);
800 $status[] = array('component'=>$componentstr,
801 'item'=>get_string('datechanged'),
802 'error'=>false);
805 return $status;
809 * Update the settings for a single plugin.
811 * @param assign_plugin $plugin The plugin to update
812 * @param stdClass $formdata The form data
813 * @return bool false if an error occurs
815 protected function update_plugin_instance(assign_plugin $plugin, stdClass $formdata) {
816 if ($plugin->is_visible()) {
817 $enabledname = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
818 if (!empty($formdata->$enabledname)) {
819 $plugin->enable();
820 if (!$plugin->save_settings($formdata)) {
821 print_error($plugin->get_error());
822 return false;
824 } else {
825 $plugin->disable();
828 return true;
832 * Update the gradebook information for this assignment.
834 * @param bool $reset If true, will reset all grades in the gradbook for this assignment
835 * @param int $coursemoduleid This is required because it might not exist in the database yet
836 * @return bool
838 public function update_gradebook($reset, $coursemoduleid) {
839 global $CFG;
841 require_once($CFG->dirroot.'/mod/assign/lib.php');
842 $assign = clone $this->get_instance();
843 $assign->cmidnumber = $coursemoduleid;
845 // Set assign gradebook feedback plugin status (enabled and visible).
846 $assign->gradefeedbackenabled = $this->is_gradebook_feedback_enabled();
848 $param = null;
849 if ($reset) {
850 $param = 'reset';
853 return assign_grade_item_update($assign, $param);
857 * Load and cache the admin config for this module.
859 * @return stdClass the plugin config
861 public function get_admin_config() {
862 if ($this->adminconfig) {
863 return $this->adminconfig;
865 $this->adminconfig = get_config('assign');
866 return $this->adminconfig;
870 * Update the calendar entries for this assignment.
872 * @param int $coursemoduleid - Required to pass this in because it might
873 * not exist in the database yet.
874 * @return bool
876 public function update_calendar($coursemoduleid) {
877 global $DB, $CFG;
878 require_once($CFG->dirroot.'/calendar/lib.php');
880 // Special case for add_instance as the coursemodule has not been set yet.
881 $instance = $this->get_instance();
883 if ($instance->duedate) {
884 $event = new stdClass();
886 $params = array('modulename'=>'assign', 'instance'=>$instance->id);
887 $event->id = $DB->get_field('event', 'id', $params);
888 $event->name = $instance->name;
889 $event->timestart = $instance->duedate;
891 // Convert the links to pluginfile. It is a bit hacky but at this stage the files
892 // might not have been saved in the module area yet.
893 $intro = $instance->intro;
894 if ($draftid = file_get_submitted_draft_itemid('introeditor')) {
895 $intro = file_rewrite_urls_to_pluginfile($intro, $draftid);
898 // We need to remove the links to files as the calendar is not ready
899 // to support module events with file areas.
900 $intro = strip_pluginfile_content($intro);
901 if ($this->show_intro()) {
902 $event->description = array(
903 'text' => $intro,
904 'format' => $instance->introformat
906 } else {
907 $event->description = array(
908 'text' => '',
909 'format' => $instance->introformat
913 if ($event->id) {
914 $calendarevent = calendar_event::load($event->id);
915 $calendarevent->update($event);
916 } else {
917 unset($event->id);
918 $event->courseid = $instance->course;
919 $event->groupid = 0;
920 $event->userid = 0;
921 $event->modulename = 'assign';
922 $event->instance = $instance->id;
923 $event->eventtype = 'due';
924 $event->timeduration = 0;
925 calendar_event::create($event);
927 } else {
928 $DB->delete_records('event', array('modulename'=>'assign', 'instance'=>$instance->id));
934 * Update this instance in the database.
936 * @param stdClass $formdata - the data submitted from the form
937 * @return bool false if an error occurs
939 public function update_instance($formdata) {
940 global $DB;
941 $adminconfig = $this->get_admin_config();
943 $update = new stdClass();
944 $update->id = $formdata->instance;
945 $update->name = $formdata->name;
946 $update->timemodified = time();
947 $update->course = $formdata->course;
948 $update->intro = $formdata->intro;
949 $update->introformat = $formdata->introformat;
950 $update->alwaysshowdescription = !empty($formdata->alwaysshowdescription);
951 $update->submissiondrafts = $formdata->submissiondrafts;
952 $update->requiresubmissionstatement = $formdata->requiresubmissionstatement;
953 $update->sendnotifications = $formdata->sendnotifications;
954 $update->sendlatenotifications = $formdata->sendlatenotifications;
955 $update->sendstudentnotifications = $adminconfig->sendstudentnotifications;
956 if (isset($formdata->sendstudentnotifications)) {
957 $update->sendstudentnotifications = $formdata->sendstudentnotifications;
959 $update->duedate = $formdata->duedate;
960 $update->cutoffdate = $formdata->cutoffdate;
961 $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate;
962 $update->grade = $formdata->grade;
963 if (!empty($formdata->completionunlocked)) {
964 $update->completionsubmit = !empty($formdata->completionsubmit);
966 $update->teamsubmission = $formdata->teamsubmission;
967 $update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit;
968 if (isset($formdata->teamsubmissiongroupingid)) {
969 $update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid;
971 $update->blindmarking = $formdata->blindmarking;
972 $update->attemptreopenmethod = ASSIGN_ATTEMPT_REOPEN_METHOD_NONE;
973 if (!empty($formdata->attemptreopenmethod)) {
974 $update->attemptreopenmethod = $formdata->attemptreopenmethod;
976 if (!empty($formdata->maxattempts)) {
977 $update->maxattempts = $formdata->maxattempts;
979 if (isset($formdata->preventsubmissionnotingroup)) {
980 $update->preventsubmissionnotingroup = $formdata->preventsubmissionnotingroup;
982 $update->markingworkflow = $formdata->markingworkflow;
983 $update->markingallocation = $formdata->markingallocation;
984 if (empty($update->markingworkflow)) { // If marking workflow is disabled, make sure allocation is disabled.
985 $update->markingallocation = 0;
988 $result = $DB->update_record('assign', $update);
989 $this->instance = $DB->get_record('assign', array('id'=>$update->id), '*', MUST_EXIST);
991 $this->save_intro_draft_files($formdata);
993 // Load the assignment so the plugins have access to it.
995 // Call save_settings hook for submission plugins.
996 foreach ($this->submissionplugins as $plugin) {
997 if (!$this->update_plugin_instance($plugin, $formdata)) {
998 print_error($plugin->get_error());
999 return false;
1002 foreach ($this->feedbackplugins as $plugin) {
1003 if (!$this->update_plugin_instance($plugin, $formdata)) {
1004 print_error($plugin->get_error());
1005 return false;
1009 $this->update_calendar($this->get_course_module()->id);
1010 $this->update_gradebook(false, $this->get_course_module()->id);
1012 $update = new stdClass();
1013 $update->id = $this->get_instance()->id;
1014 $update->nosubmissions = (!$this->is_any_submission_plugin_enabled()) ? 1: 0;
1015 $DB->update_record('assign', $update);
1017 return $result;
1021 * Save the attachments in the draft areas.
1023 * @param stdClass $formdata
1025 protected function save_intro_draft_files($formdata) {
1026 if (isset($formdata->introattachments)) {
1027 file_save_draft_area_files($formdata->introattachments, $this->get_context()->id,
1028 'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0);
1033 * Add elements in grading plugin form.
1035 * @param mixed $grade stdClass|null
1036 * @param MoodleQuickForm $mform
1037 * @param stdClass $data
1038 * @param int $userid - The userid we are grading
1039 * @return void
1041 protected function add_plugin_grade_elements($grade, MoodleQuickForm $mform, stdClass $data, $userid) {
1042 foreach ($this->feedbackplugins as $plugin) {
1043 if ($plugin->is_enabled() && $plugin->is_visible()) {
1044 $plugin->get_form_elements_for_user($grade, $mform, $data, $userid);
1052 * Add one plugins settings to edit plugin form.
1054 * @param assign_plugin $plugin The plugin to add the settings from
1055 * @param MoodleQuickForm $mform The form to add the configuration settings to.
1056 * This form is modified directly (not returned).
1057 * @param array $pluginsenabled A list of form elements to be added to a group.
1058 * The new element is added to this array by this function.
1059 * @return void
1061 protected function add_plugin_settings(assign_plugin $plugin, MoodleQuickForm $mform, & $pluginsenabled) {
1062 global $CFG;
1063 if ($plugin->is_visible() && !$plugin->is_configurable() && $plugin->is_enabled()) {
1064 $name = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
1065 $pluginsenabled[] = $mform->createElement('hidden', $name, 1);
1066 $mform->setType($name, PARAM_BOOL);
1067 $plugin->get_settings($mform);
1068 } else if ($plugin->is_visible() && $plugin->is_configurable()) {
1069 $name = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
1070 $label = $plugin->get_name();
1071 $label .= ' ' . $this->get_renderer()->help_icon('enabled', $plugin->get_subtype() . '_' . $plugin->get_type());
1072 $pluginsenabled[] = $mform->createElement('checkbox', $name, '', $label);
1074 $default = get_config($plugin->get_subtype() . '_' . $plugin->get_type(), 'default');
1075 if ($plugin->get_config('enabled') !== false) {
1076 $default = $plugin->is_enabled();
1078 $mform->setDefault($plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled', $default);
1080 $plugin->get_settings($mform);
1086 * Add settings to edit plugin form.
1088 * @param MoodleQuickForm $mform The form to add the configuration settings to.
1089 * This form is modified directly (not returned).
1090 * @return void
1092 public function add_all_plugin_settings(MoodleQuickForm $mform) {
1093 $mform->addElement('header', 'submissiontypes', get_string('submissiontypes', 'assign'));
1095 $submissionpluginsenabled = array();
1096 $group = $mform->addGroup(array(), 'submissionplugins', get_string('submissiontypes', 'assign'), array(' '), false);
1097 foreach ($this->submissionplugins as $plugin) {
1098 $this->add_plugin_settings($plugin, $mform, $submissionpluginsenabled);
1100 $group->setElements($submissionpluginsenabled);
1102 $mform->addElement('header', 'feedbacktypes', get_string('feedbacktypes', 'assign'));
1103 $feedbackpluginsenabled = array();
1104 $group = $mform->addGroup(array(), 'feedbackplugins', get_string('feedbacktypes', 'assign'), array(' '), false);
1105 foreach ($this->feedbackplugins as $plugin) {
1106 $this->add_plugin_settings($plugin, $mform, $feedbackpluginsenabled);
1108 $group->setElements($feedbackpluginsenabled);
1109 $mform->setExpanded('submissiontypes');
1113 * Allow each plugin an opportunity to update the defaultvalues
1114 * passed in to the settings form (needed to set up draft areas for
1115 * editor and filemanager elements)
1117 * @param array $defaultvalues
1119 public function plugin_data_preprocessing(&$defaultvalues) {
1120 foreach ($this->submissionplugins as $plugin) {
1121 if ($plugin->is_visible()) {
1122 $plugin->data_preprocessing($defaultvalues);
1125 foreach ($this->feedbackplugins as $plugin) {
1126 if ($plugin->is_visible()) {
1127 $plugin->data_preprocessing($defaultvalues);
1133 * Get the name of the current module.
1135 * @return string the module name (Assignment)
1137 protected function get_module_name() {
1138 if (isset(self::$modulename)) {
1139 return self::$modulename;
1141 self::$modulename = get_string('modulename', 'assign');
1142 return self::$modulename;
1146 * Get the plural name of the current module.
1148 * @return string the module name plural (Assignments)
1150 protected function get_module_name_plural() {
1151 if (isset(self::$modulenameplural)) {
1152 return self::$modulenameplural;
1154 self::$modulenameplural = get_string('modulenameplural', 'assign');
1155 return self::$modulenameplural;
1159 * Has this assignment been constructed from an instance?
1161 * @return bool
1163 public function has_instance() {
1164 return $this->instance || $this->get_course_module();
1168 * Get the settings for the current instance of this assignment
1170 * @return stdClass The settings
1172 public function get_instance() {
1173 global $DB;
1174 if ($this->instance) {
1175 return $this->instance;
1177 if ($this->get_course_module()) {
1178 $params = array('id' => $this->get_course_module()->instance);
1179 $this->instance = $DB->get_record('assign', $params, '*', MUST_EXIST);
1181 if (!$this->instance) {
1182 throw new coding_exception('Improper use of the assignment class. ' .
1183 'Cannot load the assignment record.');
1185 return $this->instance;
1189 * Get the primary grade item for this assign instance.
1191 * @return stdClass The grade_item record
1193 public function get_grade_item() {
1194 if ($this->gradeitem) {
1195 return $this->gradeitem;
1197 $instance = $this->get_instance();
1198 $params = array('itemtype' => 'mod',
1199 'itemmodule' => 'assign',
1200 'iteminstance' => $instance->id,
1201 'courseid' => $instance->course,
1202 'itemnumber' => 0);
1203 $this->gradeitem = grade_item::fetch($params);
1204 if (!$this->gradeitem) {
1205 throw new coding_exception('Improper use of the assignment class. ' .
1206 'Cannot load the grade item.');
1208 return $this->gradeitem;
1212 * Get the context of the current course.
1214 * @return mixed context|null The course context
1216 public function get_course_context() {
1217 if (!$this->context && !$this->course) {
1218 throw new coding_exception('Improper use of the assignment class. ' .
1219 'Cannot load the course context.');
1221 if ($this->context) {
1222 return $this->context->get_course_context();
1223 } else {
1224 return context_course::instance($this->course->id);
1230 * Get the current course module.
1232 * @return cm_info|null The course module or null if not known
1234 public function get_course_module() {
1235 if ($this->coursemodule) {
1236 return $this->coursemodule;
1238 if (!$this->context) {
1239 return null;
1242 if ($this->context->contextlevel == CONTEXT_MODULE) {
1243 $modinfo = get_fast_modinfo($this->get_course());
1244 $this->coursemodule = $modinfo->get_cm($this->context->instanceid);
1245 return $this->coursemodule;
1247 return null;
1251 * Get context module.
1253 * @return context
1255 public function get_context() {
1256 return $this->context;
1260 * Get the current course.
1262 * @return mixed stdClass|null The course
1264 public function get_course() {
1265 global $DB;
1267 if ($this->course) {
1268 return $this->course;
1271 if (!$this->context) {
1272 return null;
1274 $params = array('id' => $this->get_course_context()->instanceid);
1275 $this->course = $DB->get_record('course', $params, '*', MUST_EXIST);
1277 return $this->course;
1281 * Count the number of intro attachments.
1283 * @return int
1285 protected function count_attachments() {
1287 $fs = get_file_storage();
1288 $files = $fs->get_area_files($this->get_context()->id, 'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA,
1289 0, 'id', false);
1291 return count($files);
1295 * Are there any intro attachments to display?
1297 * @return boolean
1299 protected function has_visible_attachments() {
1300 return ($this->count_attachments() > 0);
1304 * Return a grade in user-friendly form, whether it's a scale or not.
1306 * @param mixed $grade int|null
1307 * @param boolean $editing Are we allowing changes to this grade?
1308 * @param int $userid The user id the grade belongs to
1309 * @param int $modified Timestamp from when the grade was last modified
1310 * @return string User-friendly representation of grade
1312 public function display_grade($grade, $editing, $userid=0, $modified=0) {
1313 global $DB;
1315 static $scalegrades = array();
1317 $o = '';
1319 if ($this->get_instance()->grade >= 0) {
1320 // Normal number.
1321 if ($editing && $this->get_instance()->grade > 0) {
1322 if ($grade < 0) {
1323 $displaygrade = '';
1324 } else {
1325 $displaygrade = format_float($grade, 2);
1327 $o .= '<label class="accesshide" for="quickgrade_' . $userid . '">' .
1328 get_string('usergrade', 'assign') .
1329 '</label>';
1330 $o .= '<input type="text"
1331 id="quickgrade_' . $userid . '"
1332 name="quickgrade_' . $userid . '"
1333 value="' . $displaygrade . '"
1334 size="6"
1335 maxlength="10"
1336 class="quickgrade"/>';
1337 $o .= '&nbsp;/&nbsp;' . format_float($this->get_instance()->grade, 2);
1338 return $o;
1339 } else {
1340 if ($grade == -1 || $grade === null) {
1341 $o .= '-';
1342 } else {
1343 $item = $this->get_grade_item();
1344 $o .= grade_format_gradevalue($grade, $item);
1345 if ($item->get_displaytype() == GRADE_DISPLAY_TYPE_REAL) {
1346 // If displaying the raw grade, also display the total value.
1347 $o .= '&nbsp;/&nbsp;' . format_float($this->get_instance()->grade, 2);
1350 return $o;
1353 } else {
1354 // Scale.
1355 if (empty($this->cache['scale'])) {
1356 if ($scale = $DB->get_record('scale', array('id'=>-($this->get_instance()->grade)))) {
1357 $this->cache['scale'] = make_menu_from_list($scale->scale);
1358 } else {
1359 $o .= '-';
1360 return $o;
1363 if ($editing) {
1364 $o .= '<label class="accesshide"
1365 for="quickgrade_' . $userid . '">' .
1366 get_string('usergrade', 'assign') .
1367 '</label>';
1368 $o .= '<select name="quickgrade_' . $userid . '" class="quickgrade">';
1369 $o .= '<option value="-1">' . get_string('nograde') . '</option>';
1370 foreach ($this->cache['scale'] as $optionid => $option) {
1371 $selected = '';
1372 if ($grade == $optionid) {
1373 $selected = 'selected="selected"';
1375 $o .= '<option value="' . $optionid . '" ' . $selected . '>' . $option . '</option>';
1377 $o .= '</select>';
1378 return $o;
1379 } else {
1380 $scaleid = (int)$grade;
1381 if (isset($this->cache['scale'][$scaleid])) {
1382 $o .= $this->cache['scale'][$scaleid];
1383 return $o;
1385 $o .= '-';
1386 return $o;
1392 * Load a list of users enrolled in the current course with the specified permission and group.
1393 * 0 for no group.
1395 * @param int $currentgroup
1396 * @param bool $idsonly
1397 * @return array List of user records
1399 public function list_participants($currentgroup, $idsonly) {
1401 if (empty($currentgroup)) {
1402 $currentgroup = 0;
1405 $key = $this->context->id . '-' . $currentgroup . '-' . $this->show_only_active_users();
1406 if (!isset($this->participants[$key])) {
1407 $users = get_enrolled_users($this->context, 'mod/assign:submit', $currentgroup, 'u.*', null, null, null,
1408 $this->show_only_active_users());
1410 $cm = $this->get_course_module();
1411 $info = new \core_availability\info_module($cm);
1412 $users = $info->filter_user_list($users);
1414 $this->participants[$key] = $users;
1417 if ($idsonly) {
1418 $idslist = array();
1419 foreach ($this->participants[$key] as $id => $user) {
1420 $idslist[$id] = new stdClass();
1421 $idslist[$id]->id = $id;
1423 return $idslist;
1425 return $this->participants[$key];
1429 * Load a count of valid teams for this assignment.
1431 * @param int $activitygroup Activity active group
1432 * @return int number of valid teams
1434 public function count_teams($activitygroup = 0) {
1436 $count = 0;
1438 $participants = $this->list_participants($activitygroup, true);
1440 // If a team submission grouping id is provided all good as all returned groups
1441 // are the submission teams, but if no team submission grouping was specified
1442 // $groups will contain all participants groups.
1443 if ($this->get_instance()->teamsubmissiongroupingid) {
1445 // We restrict the users to the selected group ones.
1446 $groups = groups_get_all_groups($this->get_course()->id,
1447 array_keys($participants),
1448 $this->get_instance()->teamsubmissiongroupingid,
1449 'DISTINCT g.id, g.name');
1451 $count = count($groups);
1453 // When a specific group is selected we don't count the default group users.
1454 if ($activitygroup == 0) {
1455 if (empty($this->get_instance()->preventsubmissionnotingroup)) {
1456 // See if there are any users in the default group.
1457 $defaultusers = $this->get_submission_group_members(0, true);
1458 if (count($defaultusers) > 0) {
1459 $count += 1;
1463 } else {
1464 // It is faster to loop around participants if no grouping was specified.
1465 $groups = array();
1466 foreach ($participants as $participant) {
1467 if ($group = $this->get_submission_group($participant->id)) {
1468 $groups[$group->id] = true;
1469 } else if (empty($this->get_instance()->preventsubmissionnotingroup)) {
1470 $groups[0] = true;
1474 $count = count($groups);
1477 return $count;
1481 * Load a count of active users enrolled in the current course with the specified permission and group.
1482 * 0 for no group.
1484 * @param int $currentgroup
1485 * @return int number of matching users
1487 public function count_participants($currentgroup) {
1488 return count($this->list_participants($currentgroup, true));
1492 * Load a count of active users submissions in the current module that require grading
1493 * This means the submission modification time is more recent than the
1494 * grading modification time and the status is SUBMITTED.
1496 * @return int number of matching submissions
1498 public function count_submissions_need_grading() {
1499 global $DB;
1501 if ($this->get_instance()->teamsubmission) {
1502 // This does not make sense for group assignment because the submission is shared.
1503 return 0;
1506 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
1507 list($esql, $params) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
1509 $params['assignid'] = $this->get_instance()->id;
1510 $params['submitted'] = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
1512 $sql = 'SELECT COUNT(s.userid)
1513 FROM {assign_submission} s
1514 LEFT JOIN {assign_grades} g ON
1515 s.assignment = g.assignment AND
1516 s.userid = g.userid AND
1517 g.attemptnumber = s.attemptnumber
1518 JOIN(' . $esql . ') e ON e.id = s.userid
1519 WHERE
1520 s.latest = 1 AND
1521 s.assignment = :assignid AND
1522 s.timemodified IS NOT NULL AND
1523 s.status = :submitted AND
1524 (s.timemodified >= g.timemodified OR g.timemodified IS NULL OR g.grade IS NULL)';
1526 return $DB->count_records_sql($sql, $params);
1530 * Load a count of grades.
1532 * @return int number of grades
1534 public function count_grades() {
1535 global $DB;
1537 if (!$this->has_instance()) {
1538 return 0;
1541 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
1542 list($esql, $params) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
1544 $params['assignid'] = $this->get_instance()->id;
1546 $sql = 'SELECT COUNT(g.userid)
1547 FROM {assign_grades} g
1548 JOIN(' . $esql . ') e ON e.id = g.userid
1549 WHERE g.assignment = :assignid';
1551 return $DB->count_records_sql($sql, $params);
1555 * Load a count of submissions.
1557 * @param bool $includenew When true, also counts the submissions with status 'new'.
1558 * @return int number of submissions
1560 public function count_submissions($includenew = false) {
1561 global $DB;
1563 if (!$this->has_instance()) {
1564 return 0;
1567 $params = array();
1568 $sqlnew = '';
1570 if (!$includenew) {
1571 $sqlnew = ' AND s.status <> :status ';
1572 $params['status'] = ASSIGN_SUBMISSION_STATUS_NEW;
1575 if ($this->get_instance()->teamsubmission) {
1576 // We cannot join on the enrolment tables for group submissions (no userid).
1577 $sql = 'SELECT COUNT(DISTINCT s.groupid)
1578 FROM {assign_submission} s
1579 WHERE
1580 s.assignment = :assignid AND
1581 s.timemodified IS NOT NULL AND
1582 s.userid = :groupuserid' .
1583 $sqlnew;
1585 $params['assignid'] = $this->get_instance()->id;
1586 $params['groupuserid'] = 0;
1587 } else {
1588 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
1589 list($esql, $enrolparams) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
1591 $params = array_merge($params, $enrolparams);
1592 $params['assignid'] = $this->get_instance()->id;
1594 $sql = 'SELECT COUNT(DISTINCT s.userid)
1595 FROM {assign_submission} s
1596 JOIN(' . $esql . ') e ON e.id = s.userid
1597 WHERE
1598 s.assignment = :assignid AND
1599 s.timemodified IS NOT NULL ' .
1600 $sqlnew;
1604 return $DB->count_records_sql($sql, $params);
1608 * Load a count of submissions with a specified status.
1610 * @param string $status The submission status - should match one of the constants
1611 * @return int number of matching submissions
1613 public function count_submissions_with_status($status) {
1614 global $DB;
1616 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
1617 list($esql, $params) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
1619 $params['assignid'] = $this->get_instance()->id;
1620 $params['assignid2'] = $this->get_instance()->id;
1621 $params['submissionstatus'] = $status;
1623 if ($this->get_instance()->teamsubmission) {
1625 $groupsstr = '';
1626 if ($currentgroup != 0) {
1627 // If there is an active group we should only display the current group users groups.
1628 $participants = $this->list_participants($currentgroup, true);
1629 $groups = groups_get_all_groups($this->get_course()->id,
1630 array_keys($participants),
1631 $this->get_instance()->teamsubmissiongroupingid,
1632 'DISTINCT g.id, g.name');
1633 list($groupssql, $groupsparams) = $DB->get_in_or_equal(array_keys($groups), SQL_PARAMS_NAMED);
1634 $groupsstr = 's.groupid ' . $groupssql . ' AND';
1635 $params = $params + $groupsparams;
1637 $sql = 'SELECT COUNT(s.groupid)
1638 FROM {assign_submission} s
1639 WHERE
1640 s.latest = 1 AND
1641 s.assignment = :assignid AND
1642 s.timemodified IS NOT NULL AND
1643 s.userid = :groupuserid AND '
1644 . $groupsstr . '
1645 s.status = :submissionstatus';
1646 $params['groupuserid'] = 0;
1647 } else {
1648 $sql = 'SELECT COUNT(s.userid)
1649 FROM {assign_submission} s
1650 JOIN(' . $esql . ') e ON e.id = s.userid
1651 WHERE
1652 s.latest = 1 AND
1653 s.assignment = :assignid AND
1654 s.timemodified IS NOT NULL AND
1655 s.status = :submissionstatus';
1659 return $DB->count_records_sql($sql, $params);
1663 * Utility function to get the userid for every row in the grading table
1664 * so the order can be frozen while we iterate it.
1666 * @return array An array of userids
1668 protected function get_grading_userid_list() {
1669 $filter = get_user_preferences('assign_filter', '');
1670 $table = new assign_grading_table($this, 0, $filter, 0, false);
1672 $useridlist = $table->get_column_data('userid');
1674 return $useridlist;
1678 * Generate zip file from array of given files.
1680 * @param array $filesforzipping - array of files to pass into archive_to_pathname.
1681 * This array is indexed by the final file name and each
1682 * element in the array is an instance of a stored_file object.
1683 * @return path of temp file - note this returned file does
1684 * not have a .zip extension - it is a temp file.
1686 protected function pack_files($filesforzipping) {
1687 global $CFG;
1688 // Create path for new zip file.
1689 $tempzip = tempnam($CFG->tempdir . '/', 'assignment_');
1690 // Zip files.
1691 $zipper = new zip_packer();
1692 if ($zipper->archive_to_pathname($filesforzipping, $tempzip)) {
1693 return $tempzip;
1695 return false;
1699 * Finds all assignment notifications that have yet to be mailed out, and mails them.
1701 * Cron function to be run periodically according to the moodle cron.
1703 * @return bool
1705 public static function cron() {
1706 global $DB;
1708 // Only ever send a max of one days worth of updates.
1709 $yesterday = time() - (24 * 3600);
1710 $timenow = time();
1711 $lastcron = $DB->get_field('modules', 'lastcron', array('name' => 'assign'));
1713 // Collect all submissions that require mailing.
1714 // Submissions are included if all are true:
1715 // - The assignment is visible in the gradebook.
1716 // - No previous notification has been sent.
1717 // - If marking workflow is not enabled, the grade was updated in the past 24 hours, or
1718 // if marking workflow is enabled, the workflow state is at 'released'.
1719 $sql = "SELECT g.id as gradeid, a.course, a.name, a.blindmarking, a.revealidentities,
1720 g.*, g.timemodified as lastmodified, cm.id as cmid
1721 FROM {assign} a
1722 JOIN {assign_grades} g ON g.assignment = a.id
1723 LEFT JOIN {assign_user_flags} uf ON uf.assignment = a.id AND uf.userid = g.userid
1724 JOIN {course_modules} cm ON cm.course = a.course AND cm.instance = a.id
1725 JOIN {modules} md ON md.id = cm.module AND md.name = 'assign'
1726 JOIN {grade_items} gri ON gri.iteminstance = a.id AND gri.courseid = a.course AND gri.itemmodule = md.name
1727 WHERE ((a.markingworkflow = 0 AND g.timemodified >= :yesterday AND g.timemodified <= :today) OR
1728 (a.markingworkflow = 1 AND uf.workflowstate = :wfreleased)) AND
1729 uf.mailed = 0 AND gri.hidden = 0
1730 ORDER BY a.course, cm.id";
1732 $params = array(
1733 'yesterday' => $yesterday,
1734 'today' => $timenow,
1735 'wfreleased' => ASSIGN_MARKING_WORKFLOW_STATE_RELEASED,
1737 $submissions = $DB->get_records_sql($sql, $params);
1739 if (!empty($submissions)) {
1741 mtrace('Processing ' . count($submissions) . ' assignment submissions ...');
1743 // Preload courses we are going to need those.
1744 $courseids = array();
1745 foreach ($submissions as $submission) {
1746 $courseids[] = $submission->course;
1749 // Filter out duplicates.
1750 $courseids = array_unique($courseids);
1751 $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
1752 list($courseidsql, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
1753 $sql = 'SELECT c.*, ' . $ctxselect .
1754 ' FROM {course} c
1755 LEFT JOIN {context} ctx ON ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel
1756 WHERE c.id ' . $courseidsql;
1758 $params['contextlevel'] = CONTEXT_COURSE;
1759 $courses = $DB->get_records_sql($sql, $params);
1761 // Clean up... this could go on for a while.
1762 unset($courseids);
1763 unset($ctxselect);
1764 unset($courseidsql);
1765 unset($params);
1767 // Message students about new feedback.
1768 foreach ($submissions as $submission) {
1770 mtrace("Processing assignment submission $submission->id ...");
1772 // Do not cache user lookups - could be too many.
1773 if (!$user = $DB->get_record('user', array('id'=>$submission->userid))) {
1774 mtrace('Could not find user ' . $submission->userid);
1775 continue;
1778 // Use a cache to prevent the same DB queries happening over and over.
1779 if (!array_key_exists($submission->course, $courses)) {
1780 mtrace('Could not find course ' . $submission->course);
1781 continue;
1783 $course = $courses[$submission->course];
1784 if (isset($course->ctxid)) {
1785 // Context has not yet been preloaded. Do so now.
1786 context_helper::preload_from_record($course);
1789 // Override the language and timezone of the "current" user, so that
1790 // mail is customised for the receiver.
1791 cron_setup_user($user, $course);
1793 // Context lookups are already cached.
1794 $coursecontext = context_course::instance($course->id);
1795 if (!is_enrolled($coursecontext, $user->id)) {
1796 $courseshortname = format_string($course->shortname,
1797 true,
1798 array('context' => $coursecontext));
1799 mtrace(fullname($user) . ' not an active participant in ' . $courseshortname);
1800 continue;
1803 if (!$grader = $DB->get_record('user', array('id'=>$submission->grader))) {
1804 mtrace('Could not find grader ' . $submission->grader);
1805 continue;
1808 $modinfo = get_fast_modinfo($course, $user->id);
1809 $cm = $modinfo->get_cm($submission->cmid);
1810 // Context lookups are already cached.
1811 $contextmodule = context_module::instance($cm->id);
1813 if (!$cm->uservisible) {
1814 // Hold mail notification for assignments the user cannot access until later.
1815 continue;
1818 // Need to send this to the student.
1819 $messagetype = 'feedbackavailable';
1820 $eventtype = 'assign_notification';
1821 $updatetime = $submission->lastmodified;
1822 $modulename = get_string('modulename', 'assign');
1824 $uniqueid = 0;
1825 if ($submission->blindmarking && !$submission->revealidentities) {
1826 $uniqueid = self::get_uniqueid_for_user_static($submission->assignment, $user->id);
1828 $showusers = $submission->blindmarking && !$submission->revealidentities;
1829 self::send_assignment_notification($grader,
1830 $user,
1831 $messagetype,
1832 $eventtype,
1833 $updatetime,
1834 $cm,
1835 $contextmodule,
1836 $course,
1837 $modulename,
1838 $submission->name,
1839 $showusers,
1840 $uniqueid);
1842 $flags = $DB->get_record('assign_user_flags', array('userid'=>$user->id, 'assignment'=>$submission->assignment));
1843 if ($flags) {
1844 $flags->mailed = 1;
1845 $DB->update_record('assign_user_flags', $flags);
1846 } else {
1847 $flags = new stdClass();
1848 $flags->userid = $user->id;
1849 $flags->assignment = $submission->assignment;
1850 $flags->mailed = 1;
1851 $DB->insert_record('assign_user_flags', $flags);
1854 mtrace('Done');
1856 mtrace('Done processing ' . count($submissions) . ' assignment submissions');
1858 cron_setup_user();
1860 // Free up memory just to be sure.
1861 unset($courses);
1864 // Update calendar events to provide a description.
1865 $sql = 'SELECT id
1866 FROM {assign}
1867 WHERE
1868 allowsubmissionsfromdate >= :lastcron AND
1869 allowsubmissionsfromdate <= :timenow AND
1870 alwaysshowdescription = 0';
1871 $params = array('lastcron' => $lastcron, 'timenow' => $timenow);
1872 $newlyavailable = $DB->get_records_sql($sql, $params);
1873 foreach ($newlyavailable as $record) {
1874 $cm = get_coursemodule_from_instance('assign', $record->id, 0, false, MUST_EXIST);
1875 $context = context_module::instance($cm->id);
1877 $assignment = new assign($context, null, null);
1878 $assignment->update_calendar($cm->id);
1881 return true;
1885 * Mark in the database that this grade record should have an update notification sent by cron.
1887 * @param stdClass $grade a grade record keyed on id
1888 * @param bool $mailedoverride when true, flag notification to be sent again.
1889 * @return bool true for success
1891 public function notify_grade_modified($grade, $mailedoverride = false) {
1892 global $DB;
1894 $flags = $this->get_user_flags($grade->userid, true);
1895 if ($flags->mailed != 1 || $mailedoverride) {
1896 $flags->mailed = 0;
1899 return $this->update_user_flags($flags);
1903 * Update user flags for this user in this assignment.
1905 * @param stdClass $flags a flags record keyed on id
1906 * @return bool true for success
1908 public function update_user_flags($flags) {
1909 global $DB;
1910 if ($flags->userid <= 0 || $flags->assignment <= 0 || $flags->id <= 0) {
1911 return false;
1914 $result = $DB->update_record('assign_user_flags', $flags);
1915 return $result;
1919 * Update a grade in the grade table for the assignment and in the gradebook.
1921 * @param stdClass $grade a grade record keyed on id
1922 * @param bool $reopenattempt If the attempt reopen method is manual, allow another attempt at this assignment.
1923 * @return bool true for success
1925 public function update_grade($grade, $reopenattempt = false) {
1926 global $DB;
1928 $grade->timemodified = time();
1930 if (!empty($grade->workflowstate)) {
1931 $validstates = $this->get_marking_workflow_states_for_current_user();
1932 if (!array_key_exists($grade->workflowstate, $validstates)) {
1933 return false;
1937 if ($grade->grade && $grade->grade != -1) {
1938 if ($this->get_instance()->grade > 0) {
1939 if (!is_numeric($grade->grade)) {
1940 return false;
1941 } else if ($grade->grade > $this->get_instance()->grade) {
1942 return false;
1943 } else if ($grade->grade < 0) {
1944 return false;
1946 } else {
1947 // This is a scale.
1948 if ($scale = $DB->get_record('scale', array('id' => -($this->get_instance()->grade)))) {
1949 $scaleoptions = make_menu_from_list($scale->scale);
1950 if (!array_key_exists((int) $grade->grade, $scaleoptions)) {
1951 return false;
1957 if (empty($grade->attemptnumber)) {
1958 // Set it to the default.
1959 $grade->attemptnumber = 0;
1961 $DB->update_record('assign_grades', $grade);
1963 $submission = null;
1964 if ($this->get_instance()->teamsubmission) {
1965 $submission = $this->get_group_submission($grade->userid, 0, false);
1966 } else {
1967 $submission = $this->get_user_submission($grade->userid, false);
1970 // Only push to gradebook if the update is for the latest attempt.
1971 // Not the latest attempt.
1972 if ($submission && $submission->attemptnumber != $grade->attemptnumber) {
1973 return true;
1976 if ($this->gradebook_item_update(null, $grade)) {
1977 \mod_assign\event\submission_graded::create_from_grade($this, $grade)->trigger();
1980 // If the conditions are met, allow another attempt.
1981 if ($submission) {
1982 $this->reopen_submission_if_required($grade->userid,
1983 $submission,
1984 $reopenattempt);
1987 return true;
1991 * View the grant extension date page.
1993 * Uses url parameters 'userid'
1994 * or from parameter 'selectedusers'
1996 * @param moodleform $mform - Used for validation of the submitted data
1997 * @return string
1999 protected function view_grant_extension($mform) {
2000 global $DB, $CFG;
2001 require_once($CFG->dirroot . '/mod/assign/extensionform.php');
2003 $o = '';
2004 $batchusers = optional_param('selectedusers', '', PARAM_SEQUENCE);
2005 $data = new stdClass();
2006 $data->extensionduedate = null;
2007 $userid = 0;
2008 if (!$batchusers) {
2009 $userid = required_param('userid', PARAM_INT);
2011 $flags = $this->get_user_flags($userid, false);
2013 $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
2015 if ($flags) {
2016 $data->extensionduedate = $flags->extensionduedate;
2018 $data->userid = $userid;
2019 } else {
2020 $data->batchusers = $batchusers;
2022 $header = new assign_header($this->get_instance(),
2023 $this->get_context(),
2024 $this->show_intro(),
2025 $this->get_course_module()->id,
2026 get_string('grantextension', 'assign'));
2027 $o .= $this->get_renderer()->render($header);
2029 if (!$mform) {
2030 $formparams = array($this->get_course_module()->id,
2031 $userid,
2032 $batchusers,
2033 $this->get_instance(),
2034 $data);
2035 $mform = new mod_assign_extension_form(null, $formparams);
2037 $o .= $this->get_renderer()->render(new assign_form('extensionform', $mform));
2038 $o .= $this->view_footer();
2039 return $o;
2043 * Get a list of the users in the same group as this user.
2045 * @param int $groupid The id of the group whose members we want or 0 for the default group
2046 * @param bool $onlyids Whether to retrieve only the user id's
2047 * @param bool $excludesuspended Whether to exclude suspended users
2048 * @return array The users (possibly id's only)
2050 public function get_submission_group_members($groupid, $onlyids, $excludesuspended = false) {
2051 $members = array();
2052 if ($groupid != 0) {
2053 if ($onlyids) {
2054 $allusers = groups_get_members($groupid, 'u.id');
2055 } else {
2056 $allusers = groups_get_members($groupid);
2058 foreach ($allusers as $user) {
2059 if ($this->get_submission_group($user->id)) {
2060 $members[] = $user;
2063 } else {
2064 $allusers = $this->list_participants(null, $onlyids);
2065 foreach ($allusers as $user) {
2066 if ($this->get_submission_group($user->id) == null) {
2067 $members[] = $user;
2071 // Exclude suspended users, if user can't see them.
2072 if ($excludesuspended || !has_capability('moodle/course:viewsuspendedusers', $this->context)) {
2073 foreach ($members as $key => $member) {
2074 if (!$this->is_active_user($member->id)) {
2075 unset($members[$key]);
2080 return $members;
2084 * Get a list of the users in the same group as this user that have not submitted the assignment.
2086 * @param int $groupid The id of the group whose members we want or 0 for the default group
2087 * @param bool $onlyids Whether to retrieve only the user id's
2088 * @return array The users (possibly id's only)
2090 public function get_submission_group_members_who_have_not_submitted($groupid, $onlyids) {
2091 $instance = $this->get_instance();
2092 if (!$instance->teamsubmission || !$instance->requireallteammemberssubmit) {
2093 return array();
2095 $members = $this->get_submission_group_members($groupid, $onlyids);
2097 foreach ($members as $id => $member) {
2098 $submission = $this->get_user_submission($member->id, false);
2099 if ($submission && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
2100 unset($members[$id]);
2101 } else {
2102 if ($this->is_blind_marking()) {
2103 $members[$id]->alias = get_string('hiddenuser', 'assign') .
2104 $this->get_uniqueid_for_user($id);
2108 return $members;
2112 * Load the group submission object for a particular user, optionally creating it if required.
2114 * @param int $userid The id of the user whose submission we want
2115 * @param int $groupid The id of the group for this user - may be 0 in which
2116 * case it is determined from the userid.
2117 * @param bool $create If set to true a new submission object will be created in the database
2118 * with the status set to "new".
2119 * @param int $attemptnumber - -1 means the latest attempt
2120 * @return stdClass The submission
2122 public function get_group_submission($userid, $groupid, $create, $attemptnumber=-1) {
2123 global $DB;
2125 if ($groupid == 0) {
2126 $group = $this->get_submission_group($userid);
2127 if ($group) {
2128 $groupid = $group->id;
2132 // Now get the group submission.
2133 $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
2134 if ($attemptnumber >= 0) {
2135 $params['attemptnumber'] = $attemptnumber;
2138 // Only return the row with the highest attemptnumber.
2139 $submission = null;
2140 $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', '*', 0, 1);
2141 if ($submissions) {
2142 $submission = reset($submissions);
2145 if ($submission) {
2146 return $submission;
2148 if ($create) {
2149 $submission = new stdClass();
2150 $submission->assignment = $this->get_instance()->id;
2151 $submission->userid = 0;
2152 $submission->groupid = $groupid;
2153 $submission->timecreated = time();
2154 $submission->timemodified = $submission->timecreated;
2155 if ($attemptnumber >= 0) {
2156 $submission->attemptnumber = $attemptnumber;
2157 } else {
2158 $submission->attemptnumber = 0;
2160 // Work out if this is the latest submission.
2161 $submission->latest = 0;
2162 $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
2163 if ($attemptnumber == -1) {
2164 // This is a new submission so it must be the latest.
2165 $submission->latest = 1;
2166 } else {
2167 // We need to work this out.
2168 $result = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', 'attemptnumber', 0, 1);
2169 if ($result) {
2170 $latestsubmission = reset($result);
2172 if (!$latestsubmission || ($attemptnumber == $latestsubmission->attemptnumber)) {
2173 $submission->latest = 1;
2176 if ($submission->latest) {
2177 // This is the case when we need to set latest to 0 for all the other attempts.
2178 $DB->set_field('assign_submission', 'latest', 0, $params);
2180 $submission->status = ASSIGN_SUBMISSION_STATUS_NEW;
2181 $sid = $DB->insert_record('assign_submission', $submission);
2182 return $DB->get_record('assign_submission', array('id' => $sid));
2184 return false;
2188 * View a summary listing of all assignments in the current course.
2190 * @return string
2192 private function view_course_index() {
2193 global $USER;
2195 $o = '';
2197 $course = $this->get_course();
2198 $strplural = get_string('modulenameplural', 'assign');
2200 if (!$cms = get_coursemodules_in_course('assign', $course->id, 'm.duedate')) {
2201 $o .= $this->get_renderer()->notification(get_string('thereareno', 'moodle', $strplural));
2202 $o .= $this->get_renderer()->continue_button(new moodle_url('/course/view.php', array('id' => $course->id)));
2203 return $o;
2206 $strsectionname = '';
2207 $usesections = course_format_uses_sections($course->format);
2208 $modinfo = get_fast_modinfo($course);
2210 if ($usesections) {
2211 $strsectionname = get_string('sectionname', 'format_'.$course->format);
2212 $sections = $modinfo->get_section_info_all();
2214 $courseindexsummary = new assign_course_index_summary($usesections, $strsectionname);
2216 $timenow = time();
2218 $currentsection = '';
2219 foreach ($modinfo->instances['assign'] as $cm) {
2220 if (!$cm->uservisible) {
2221 continue;
2224 $timedue = $cms[$cm->id]->duedate;
2226 $sectionname = '';
2227 if ($usesections && $cm->sectionnum) {
2228 $sectionname = get_section_name($course, $sections[$cm->sectionnum]);
2231 $submitted = '';
2232 $context = context_module::instance($cm->id);
2234 $assignment = new assign($context, $cm, $course);
2236 if (has_capability('mod/assign:grade', $context)) {
2237 $submitted = $assignment->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED);
2239 } else if (has_capability('mod/assign:submit', $context)) {
2240 $usersubmission = $assignment->get_user_submission($USER->id, false);
2242 if (!empty($usersubmission->status)) {
2243 $submitted = get_string('submissionstatus_' . $usersubmission->status, 'assign');
2244 } else {
2245 $submitted = get_string('submissionstatus_', 'assign');
2248 $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $cm->instance, $USER->id);
2249 if (isset($gradinginfo->items[0]->grades[$USER->id]) &&
2250 !$gradinginfo->items[0]->grades[$USER->id]->hidden ) {
2251 $grade = $gradinginfo->items[0]->grades[$USER->id]->str_grade;
2252 } else {
2253 $grade = '-';
2256 $courseindexsummary->add_assign_info($cm->id, $cm->name, $sectionname, $timedue, $submitted, $grade);
2260 $o .= $this->get_renderer()->render($courseindexsummary);
2261 $o .= $this->view_footer();
2263 return $o;
2267 * View a page rendered by a plugin.
2269 * Uses url parameters 'pluginaction', 'pluginsubtype', 'plugin', and 'id'.
2271 * @return string
2273 protected function view_plugin_page() {
2274 global $USER;
2276 $o = '';
2278 $pluginsubtype = required_param('pluginsubtype', PARAM_ALPHA);
2279 $plugintype = required_param('plugin', PARAM_TEXT);
2280 $pluginaction = required_param('pluginaction', PARAM_ALPHA);
2282 $plugin = $this->get_plugin_by_type($pluginsubtype, $plugintype);
2283 if (!$plugin) {
2284 print_error('invalidformdata', '');
2285 return;
2288 $o .= $plugin->view_page($pluginaction);
2290 return $o;
2295 * This is used for team assignments to get the group for the specified user.
2296 * If the user is a member of multiple or no groups this will return false
2298 * @param int $userid The id of the user whose submission we want
2299 * @return mixed The group or false
2301 public function get_submission_group($userid) {
2303 if (isset($this->usersubmissiongroups[$userid])) {
2304 return $this->usersubmissiongroups[$userid];
2307 $groups = $this->get_all_groups($userid);
2308 if (count($groups) != 1) {
2309 $return = false;
2310 } else {
2311 $return = array_pop($groups);
2314 // Cache the user submission group.
2315 $this->usersubmissiongroups[$userid] = $return;
2317 return $return;
2321 * Gets all groups the user is a member of.
2323 * @param int $userid Teh id of the user who's groups we are checking
2324 * @return array The group objects
2326 public function get_all_groups($userid) {
2327 if (isset($this->usergroups[$userid])) {
2328 return $this->usergroups[$userid];
2331 $grouping = $this->get_instance()->teamsubmissiongroupingid;
2332 $return = groups_get_all_groups($this->get_course()->id, $userid, $grouping);
2334 $this->usergroups[$userid] = $return;
2336 return $return;
2341 * Display the submission that is used by a plugin.
2343 * Uses url parameters 'sid', 'gid' and 'plugin'.
2345 * @param string $pluginsubtype
2346 * @return string
2348 protected function view_plugin_content($pluginsubtype) {
2349 $o = '';
2351 $submissionid = optional_param('sid', 0, PARAM_INT);
2352 $gradeid = optional_param('gid', 0, PARAM_INT);
2353 $plugintype = required_param('plugin', PARAM_TEXT);
2354 $item = null;
2355 if ($pluginsubtype == 'assignsubmission') {
2356 $plugin = $this->get_submission_plugin_by_type($plugintype);
2357 if ($submissionid <= 0) {
2358 throw new coding_exception('Submission id should not be 0');
2360 $item = $this->get_submission($submissionid);
2362 // Check permissions.
2363 $this->require_view_submission($item->userid);
2364 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
2365 $this->get_context(),
2366 $this->show_intro(),
2367 $this->get_course_module()->id,
2368 $plugin->get_name()));
2369 $o .= $this->get_renderer()->render(new assign_submission_plugin_submission($plugin,
2370 $item,
2371 assign_submission_plugin_submission::FULL,
2372 $this->get_course_module()->id,
2373 $this->get_return_action(),
2374 $this->get_return_params()));
2376 // Trigger event for viewing a submission.
2377 \mod_assign\event\submission_viewed::create_from_submission($this, $item)->trigger();
2379 } else {
2380 $plugin = $this->get_feedback_plugin_by_type($plugintype);
2381 if ($gradeid <= 0) {
2382 throw new coding_exception('Grade id should not be 0');
2384 $item = $this->get_grade($gradeid);
2385 // Check permissions.
2386 $this->require_view_submission($item->userid);
2387 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
2388 $this->get_context(),
2389 $this->show_intro(),
2390 $this->get_course_module()->id,
2391 $plugin->get_name()));
2392 $o .= $this->get_renderer()->render(new assign_feedback_plugin_feedback($plugin,
2393 $item,
2394 assign_feedback_plugin_feedback::FULL,
2395 $this->get_course_module()->id,
2396 $this->get_return_action(),
2397 $this->get_return_params()));
2399 // Trigger event for viewing feedback.
2400 \mod_assign\event\feedback_viewed::create_from_grade($this, $item)->trigger();
2403 $o .= $this->view_return_links();
2405 $o .= $this->view_footer();
2407 return $o;
2411 * Rewrite plugin file urls so they resolve correctly in an exported zip.
2413 * @param string $text - The replacement text
2414 * @param stdClass $user - The user record
2415 * @param assign_plugin $plugin - The assignment plugin
2417 public function download_rewrite_pluginfile_urls($text, $user, $plugin) {
2418 $groupmode = groups_get_activity_groupmode($this->get_course_module());
2419 $groupname = '';
2420 if ($groupmode) {
2421 $groupid = groups_get_activity_group($this->get_course_module(), true);
2422 $groupname = groups_get_group_name($groupid).'-';
2425 if ($this->is_blind_marking()) {
2426 $prefix = $groupname . get_string('participant', 'assign');
2427 $prefix = str_replace('_', ' ', $prefix);
2428 $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($user->id) . '_');
2429 } else {
2430 $prefix = $groupname . fullname($user);
2431 $prefix = str_replace('_', ' ', $prefix);
2432 $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($user->id) . '_');
2435 $subtype = $plugin->get_subtype();
2436 $type = $plugin->get_type();
2437 $prefix = $prefix . $subtype . '_' . $type . '_';
2439 $result = str_replace('@@PLUGINFILE@@/', $prefix, $text);
2441 return $result;
2445 * Render the content in editor that is often used by plugin.
2447 * @param string $filearea
2448 * @param int $submissionid
2449 * @param string $plugintype
2450 * @param string $editor
2451 * @param string $component
2452 * @return string
2454 public function render_editor_content($filearea, $submissionid, $plugintype, $editor, $component) {
2455 global $CFG;
2457 $result = '';
2459 $plugin = $this->get_submission_plugin_by_type($plugintype);
2461 $text = $plugin->get_editor_text($editor, $submissionid);
2462 $format = $plugin->get_editor_format($editor, $submissionid);
2464 $finaltext = file_rewrite_pluginfile_urls($text,
2465 'pluginfile.php',
2466 $this->get_context()->id,
2467 $component,
2468 $filearea,
2469 $submissionid);
2470 $params = array('overflowdiv' => true, 'context' => $this->get_context());
2471 $result .= format_text($finaltext, $format, $params);
2473 if ($CFG->enableportfolios && has_capability('mod/assign:exportownsubmission', $this->context)) {
2474 require_once($CFG->libdir . '/portfoliolib.php');
2476 $button = new portfolio_add_button();
2477 $portfolioparams = array('cmid' => $this->get_course_module()->id,
2478 'sid' => $submissionid,
2479 'plugin' => $plugintype,
2480 'editor' => $editor,
2481 'area'=>$filearea);
2482 $button->set_callback_options('assign_portfolio_caller', $portfolioparams, 'mod_assign');
2483 $fs = get_file_storage();
2485 if ($files = $fs->get_area_files($this->context->id,
2486 $component,
2487 $filearea,
2488 $submissionid,
2489 'timemodified',
2490 false)) {
2491 $button->set_formats(PORTFOLIO_FORMAT_RICHHTML);
2492 } else {
2493 $button->set_formats(PORTFOLIO_FORMAT_PLAINHTML);
2495 $result .= $button->to_html();
2497 return $result;
2501 * Display a continue page after grading.
2503 * @param string $message - The message to display.
2504 * @return string
2506 protected function view_savegrading_result($message) {
2507 $o = '';
2508 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
2509 $this->get_context(),
2510 $this->show_intro(),
2511 $this->get_course_module()->id,
2512 get_string('savegradingresult', 'assign')));
2513 $gradingresult = new assign_gradingmessage(get_string('savegradingresult', 'assign'),
2514 $message,
2515 $this->get_course_module()->id);
2516 $o .= $this->get_renderer()->render($gradingresult);
2517 $o .= $this->view_footer();
2518 return $o;
2521 * Display a continue page after quickgrading.
2523 * @param string $message - The message to display.
2524 * @return string
2526 protected function view_quickgrading_result($message) {
2527 $o = '';
2528 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
2529 $this->get_context(),
2530 $this->show_intro(),
2531 $this->get_course_module()->id,
2532 get_string('quickgradingresult', 'assign')));
2533 $lastpage = optional_param('lastpage', null, PARAM_INT);
2534 $gradingresult = new assign_gradingmessage(get_string('quickgradingresult', 'assign'),
2535 $message,
2536 $this->get_course_module()->id,
2537 false,
2538 $lastpage);
2539 $o .= $this->get_renderer()->render($gradingresult);
2540 $o .= $this->view_footer();
2541 return $o;
2545 * Display the page footer.
2547 * @return string
2549 protected function view_footer() {
2550 // When viewing the footer during PHPUNIT tests a set_state error is thrown.
2551 if (!PHPUNIT_TEST) {
2552 return $this->get_renderer()->render_footer();
2555 return '';
2559 * Throw an error if the permissions to view this users submission are missing.
2561 * @throws required_capability_exception
2562 * @return none
2564 public function require_view_submission($userid) {
2565 if (!$this->can_view_submission($userid)) {
2566 throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', '');
2571 * Throw an error if the permissions to view grades in this assignment are missing.
2573 * @throws required_capability_exception
2574 * @return none
2576 public function require_view_grades() {
2577 if (!$this->can_view_grades()) {
2578 throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', '');
2583 * Does this user have view grade or grade permission for this assignment?
2585 * @return bool
2587 public function can_view_grades() {
2588 // Permissions check.
2589 if (!has_any_capability(array('mod/assign:viewgrades', 'mod/assign:grade'), $this->context)) {
2590 return false;
2593 return true;
2597 * Does this user have grade permission for this assignment?
2599 * @return bool
2601 public function can_grade() {
2602 // Permissions check.
2603 if (!has_capability('mod/assign:grade', $this->context)) {
2604 return false;
2607 return true;
2611 * Download a zip file of all assignment submissions.
2613 * @return string - If an error occurs, this will contain the error page.
2615 protected function download_submissions() {
2616 global $CFG, $DB;
2618 // More efficient to load this here.
2619 require_once($CFG->libdir.'/filelib.php');
2621 $this->require_view_grades();
2623 // Load all users with submit.
2624 $students = get_enrolled_users($this->context, "mod/assign:submit", null, 'u.*', null, null, null,
2625 $this->show_only_active_users());
2627 // Build a list of files to zip.
2628 $filesforzipping = array();
2629 $fs = get_file_storage();
2631 $groupmode = groups_get_activity_groupmode($this->get_course_module());
2632 // All users.
2633 $groupid = 0;
2634 $groupname = '';
2635 if ($groupmode) {
2636 $groupid = groups_get_activity_group($this->get_course_module(), true);
2637 $groupname = groups_get_group_name($groupid).'-';
2640 // Construct the zip file name.
2641 $filename = clean_filename($this->get_course()->shortname . '-' .
2642 $this->get_instance()->name . '-' .
2643 $groupname.$this->get_course_module()->id . '.zip');
2645 // Get all the files for each student.
2646 foreach ($students as $student) {
2647 $userid = $student->id;
2649 if ((groups_is_member($groupid, $userid) or !$groupmode or !$groupid)) {
2650 // Get the plugins to add their own files to the zip.
2652 $submissiongroup = false;
2653 $groupname = '';
2654 if ($this->get_instance()->teamsubmission) {
2655 $submission = $this->get_group_submission($userid, 0, false);
2656 $submissiongroup = $this->get_submission_group($userid);
2657 if ($submissiongroup) {
2658 $groupname = $submissiongroup->name . '-';
2659 } else {
2660 $groupname = get_string('defaultteam', 'assign') . '-';
2662 } else {
2663 $submission = $this->get_user_submission($userid, false);
2666 if ($this->is_blind_marking()) {
2667 $prefix = str_replace('_', ' ', $groupname . get_string('participant', 'assign'));
2668 $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($userid) . '_');
2669 } else {
2670 $prefix = str_replace('_', ' ', $groupname . fullname($student));
2671 $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($userid) . '_');
2674 if ($submission) {
2675 foreach ($this->submissionplugins as $plugin) {
2676 if ($plugin->is_enabled() && $plugin->is_visible()) {
2677 $pluginfiles = $plugin->get_files($submission, $student);
2678 foreach ($pluginfiles as $zipfilename => $file) {
2679 $subtype = $plugin->get_subtype();
2680 $type = $plugin->get_type();
2681 $prefixedfilename = clean_filename($prefix .
2682 $subtype .
2683 '_' .
2684 $type .
2685 '_' .
2686 $zipfilename);
2687 $filesforzipping[$prefixedfilename] = $file;
2694 $result = '';
2695 if (count($filesforzipping) == 0) {
2696 $header = new assign_header($this->get_instance(),
2697 $this->get_context(),
2699 $this->get_course_module()->id,
2700 get_string('downloadall', 'assign'));
2701 $result .= $this->get_renderer()->render($header);
2702 $result .= $this->get_renderer()->notification(get_string('nosubmission', 'assign'));
2703 $url = new moodle_url('/mod/assign/view.php', array('id'=>$this->get_course_module()->id,
2704 'action'=>'grading'));
2705 $result .= $this->get_renderer()->continue_button($url);
2706 $result .= $this->view_footer();
2707 } else if ($zipfile = $this->pack_files($filesforzipping)) {
2708 \mod_assign\event\all_submissions_downloaded::create_from_assign($this)->trigger();
2709 // Send file and delete after sending.
2710 send_temp_file($zipfile, $filename);
2711 // We will not get here - send_temp_file calls exit.
2713 return $result;
2717 * Util function to add a message to the log.
2719 * @deprecated since 2.7 - Use new events system instead.
2720 * (see http://docs.moodle.org/dev/Migrating_logging_calls_in_plugins).
2722 * @param string $action The current action
2723 * @param string $info A detailed description of the change. But no more than 255 characters.
2724 * @param string $url The url to the assign module instance.
2725 * @param bool $return If true, returns the arguments, else adds to log. The purpose of this is to
2726 * retrieve the arguments to use them with the new event system (Event 2).
2727 * @return void|array
2729 public function add_to_log($action = '', $info = '', $url='', $return = false) {
2730 global $USER;
2732 $fullurl = 'view.php?id=' . $this->get_course_module()->id;
2733 if ($url != '') {
2734 $fullurl .= '&' . $url;
2737 $args = array(
2738 $this->get_course()->id,
2739 'assign',
2740 $action,
2741 $fullurl,
2742 $info,
2743 $this->get_course_module()->id
2746 if ($return) {
2747 // We only need to call debugging when returning a value. This is because the call to
2748 // call_user_func_array('add_to_log', $args) will trigger a debugging message of it's own.
2749 debugging('The mod_assign add_to_log() function is now deprecated.', DEBUG_DEVELOPER);
2750 return $args;
2752 call_user_func_array('add_to_log', $args);
2756 * Lazy load the page renderer and expose the renderer to plugins.
2758 * @return assign_renderer
2760 public function get_renderer() {
2761 global $PAGE;
2762 if ($this->output) {
2763 return $this->output;
2765 $this->output = $PAGE->get_renderer('mod_assign');
2766 return $this->output;
2770 * Load the submission object for a particular user, optionally creating it if required.
2772 * For team assignments there are 2 submissions - the student submission and the team submission
2773 * All files are associated with the team submission but the status of the students contribution is
2774 * recorded separately.
2776 * @param int $userid The id of the user whose submission we want or 0 in which case USER->id is used
2777 * @param bool $create optional - defaults to false. If set to true a new submission object
2778 * will be created in the database with the status set to "new".
2779 * @param int $attemptnumber - -1 means the latest attempt
2780 * @return stdClass The submission
2782 public function get_user_submission($userid, $create, $attemptnumber=-1) {
2783 global $DB, $USER;
2785 if (!$userid) {
2786 $userid = $USER->id;
2788 // If the userid is not null then use userid.
2789 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid, 'groupid'=>0);
2790 if ($attemptnumber >= 0) {
2791 $params['attemptnumber'] = $attemptnumber;
2794 // Only return the row with the highest attemptnumber.
2795 $submission = null;
2796 $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', '*', 0, 1);
2797 if ($submissions) {
2798 $submission = reset($submissions);
2801 if ($submission) {
2802 return $submission;
2804 if ($create) {
2805 $submission = new stdClass();
2806 $submission->assignment = $this->get_instance()->id;
2807 $submission->userid = $userid;
2808 $submission->timecreated = time();
2809 $submission->timemodified = $submission->timecreated;
2810 $submission->status = ASSIGN_SUBMISSION_STATUS_NEW;
2811 if ($attemptnumber >= 0) {
2812 $submission->attemptnumber = $attemptnumber;
2813 } else {
2814 $submission->attemptnumber = 0;
2816 // Work out if this is the latest submission.
2817 $submission->latest = 0;
2818 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid, 'groupid'=>0);
2819 if ($attemptnumber == -1) {
2820 // This is a new submission so it must be the latest.
2821 $submission->latest = 1;
2822 } else {
2823 // We need to work this out.
2824 $result = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', 'attemptnumber', 0, 1);
2825 $latestsubmission = null;
2826 if ($result) {
2827 $latestsubmission = reset($result);
2829 if (empty($latestsubmission) || ($attemptnumber > $latestsubmission->attemptnumber)) {
2830 $submission->latest = 1;
2833 if ($submission->latest) {
2834 // This is the case when we need to set latest to 0 for all the other attempts.
2835 $DB->set_field('assign_submission', 'latest', 0, $params);
2837 $sid = $DB->insert_record('assign_submission', $submission);
2838 return $DB->get_record('assign_submission', array('id' => $sid));
2840 return false;
2844 * Load the submission object from it's id.
2846 * @param int $submissionid The id of the submission we want
2847 * @return stdClass The submission
2849 protected function get_submission($submissionid) {
2850 global $DB;
2852 $params = array('assignment'=>$this->get_instance()->id, 'id'=>$submissionid);
2853 return $DB->get_record('assign_submission', $params, '*', MUST_EXIST);
2857 * This will retrieve a user flags object from the db optionally creating it if required.
2858 * The user flags was split from the user_grades table in 2.5.
2860 * @param int $userid The user we are getting the flags for.
2861 * @param bool $create If true the flags record will be created if it does not exist
2862 * @return stdClass The flags record
2864 public function get_user_flags($userid, $create) {
2865 global $DB, $USER;
2867 // If the userid is not null then use userid.
2868 if (!$userid) {
2869 $userid = $USER->id;
2872 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
2874 $flags = $DB->get_record('assign_user_flags', $params);
2876 if ($flags) {
2877 return $flags;
2879 if ($create) {
2880 $flags = new stdClass();
2881 $flags->assignment = $this->get_instance()->id;
2882 $flags->userid = $userid;
2883 $flags->locked = 0;
2884 $flags->extensionduedate = 0;
2885 $flags->workflowstate = '';
2886 $flags->allocatedmarker = 0;
2888 // The mailed flag can be one of 3 values: 0 is unsent, 1 is sent and 2 is do not send yet.
2889 // This is because students only want to be notified about certain types of update (grades and feedback).
2890 $flags->mailed = 2;
2892 $fid = $DB->insert_record('assign_user_flags', $flags);
2893 $flags->id = $fid;
2894 return $flags;
2896 return false;
2900 * This will retrieve a grade object from the db, optionally creating it if required.
2902 * @param int $userid The user we are grading
2903 * @param bool $create If true the grade will be created if it does not exist
2904 * @param int $attemptnumber The attempt number to retrieve the grade for. -1 means the latest submission.
2905 * @return stdClass The grade record
2907 public function get_user_grade($userid, $create, $attemptnumber=-1) {
2908 global $DB, $USER;
2910 // If the userid is not null then use userid.
2911 if (!$userid) {
2912 $userid = $USER->id;
2914 $submission = null;
2916 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
2917 if ($attemptnumber < 0 || $create) {
2918 // Make sure this grade matches the latest submission attempt.
2919 if ($this->get_instance()->teamsubmission) {
2920 $submission = $this->get_group_submission($userid, 0, true);
2921 } else {
2922 $submission = $this->get_user_submission($userid, true);
2924 if ($submission) {
2925 $attemptnumber = $submission->attemptnumber;
2929 if ($attemptnumber >= 0) {
2930 $params['attemptnumber'] = $attemptnumber;
2933 $grades = $DB->get_records('assign_grades', $params, 'attemptnumber DESC', '*', 0, 1);
2935 if ($grades) {
2936 return reset($grades);
2938 if ($create) {
2939 $grade = new stdClass();
2940 $grade->assignment = $this->get_instance()->id;
2941 $grade->userid = $userid;
2942 $grade->timecreated = time();
2943 // If we are "auto-creating" a grade - and there is a submission
2944 // the new grade should not have a more recent timemodified value
2945 // than the submission.
2946 if ($submission) {
2947 $grade->timemodified = $submission->timemodified;
2948 } else {
2949 $grade->timemodified = $grade->timecreated;
2951 $grade->grade = -1;
2952 $grade->grader = $USER->id;
2953 if ($attemptnumber >= 0) {
2954 $grade->attemptnumber = $attemptnumber;
2957 $gid = $DB->insert_record('assign_grades', $grade);
2958 $grade->id = $gid;
2959 return $grade;
2961 return false;
2965 * This will retrieve a grade object from the db.
2967 * @param int $gradeid The id of the grade
2968 * @return stdClass The grade record
2970 protected function get_grade($gradeid) {
2971 global $DB;
2973 $params = array('assignment'=>$this->get_instance()->id, 'id'=>$gradeid);
2974 return $DB->get_record('assign_grades', $params, '*', MUST_EXIST);
2978 * Print the grading page for a single user submission.
2980 * @param moodleform $mform
2981 * @return string
2983 protected function view_single_grade_page($mform) {
2984 global $DB, $CFG;
2986 $o = '';
2987 $instance = $this->get_instance();
2989 require_once($CFG->dirroot . '/mod/assign/gradeform.php');
2991 // Need submit permission to submit an assignment.
2992 require_capability('mod/assign:grade', $this->context);
2994 $header = new assign_header($instance,
2995 $this->get_context(),
2996 false,
2997 $this->get_course_module()->id,
2998 get_string('grading', 'assign'));
2999 $o .= $this->get_renderer()->render($header);
3001 // If userid is passed - we are only grading a single student.
3002 $rownum = required_param('rownum', PARAM_INT);
3003 $useridlistid = optional_param('useridlistid', time(), PARAM_INT);
3004 $userid = optional_param('userid', 0, PARAM_INT);
3005 $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT);
3007 $cache = cache::make_from_params(cache_store::MODE_SESSION, 'mod_assign', 'useridlist');
3008 if (!$userid) {
3009 if (!$useridlist = $cache->get($this->get_course_module()->id . '_' . $useridlistid)) {
3010 $useridlist = $this->get_grading_userid_list();
3012 $cache->set($this->get_course_module()->id . '_' . $useridlistid, $useridlist);
3013 } else {
3014 $rownum = 0;
3015 $useridlist = array($userid);
3018 if ($rownum < 0 || $rownum > count($useridlist)) {
3019 throw new coding_exception('Row is out of bounds for the current grading table: ' . $rownum);
3022 $last = false;
3023 $userid = $useridlist[$rownum];
3024 if ($rownum == count($useridlist) - 1) {
3025 $last = true;
3027 $user = $DB->get_record('user', array('id' => $userid));
3028 if ($user) {
3029 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_course_context());
3030 $usersummary = new assign_user_summary($user,
3031 $this->get_course()->id,
3032 $viewfullnames,
3033 $this->is_blind_marking(),
3034 $this->get_uniqueid_for_user($user->id),
3035 get_extra_user_fields($this->get_context()),
3036 !$this->is_active_user($userid));
3037 $o .= $this->get_renderer()->render($usersummary);
3039 $submission = $this->get_user_submission($userid, false, $attemptnumber);
3040 $submissiongroup = null;
3041 $teamsubmission = null;
3042 $notsubmitted = array();
3043 if ($instance->teamsubmission) {
3044 $teamsubmission = $this->get_group_submission($userid, 0, false, $attemptnumber);
3045 $submissiongroup = $this->get_submission_group($userid);
3046 $groupid = 0;
3047 if ($submissiongroup) {
3048 $groupid = $submissiongroup->id;
3050 $notsubmitted = $this->get_submission_group_members_who_have_not_submitted($groupid, false);
3054 // Get the requested grade.
3055 $grade = $this->get_user_grade($userid, false, $attemptnumber);
3056 $flags = $this->get_user_flags($userid, false);
3057 if ($this->can_view_submission($userid)) {
3058 $gradelocked = ($flags && $flags->locked) || $this->grading_disabled($userid);
3059 $extensionduedate = null;
3060 if ($flags) {
3061 $extensionduedate = $flags->extensionduedate;
3063 $showedit = $this->submissions_open($userid) && ($this->is_any_submission_plugin_enabled());
3064 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_course_context());
3065 $usergroups = $this->get_all_groups($user->id);
3067 $submissionstatus = new assign_submission_status($instance->allowsubmissionsfromdate,
3068 $instance->alwaysshowdescription,
3069 $submission,
3070 $instance->teamsubmission,
3071 $teamsubmission,
3072 $submissiongroup,
3073 $notsubmitted,
3074 $this->is_any_submission_plugin_enabled(),
3075 $gradelocked,
3076 $this->is_graded($userid),
3077 $instance->duedate,
3078 $instance->cutoffdate,
3079 $this->get_submission_plugins(),
3080 $this->get_return_action(),
3081 $this->get_return_params(),
3082 $this->get_course_module()->id,
3083 $this->get_course()->id,
3084 assign_submission_status::GRADER_VIEW,
3085 $showedit,
3086 false,
3087 $viewfullnames,
3088 $extensionduedate,
3089 $this->get_context(),
3090 $this->is_blind_marking(),
3092 $instance->attemptreopenmethod,
3093 $instance->maxattempts,
3094 $this->get_grading_status($userid),
3095 $instance->preventsubmissionnotingroup,
3096 $usergroups);
3097 $o .= $this->get_renderer()->render($submissionstatus);
3100 if ($grade) {
3101 $data = new stdClass();
3102 if ($grade->grade !== null && $grade->grade >= 0) {
3103 $data->grade = format_float($grade->grade, 2);
3105 } else {
3106 $data = new stdClass();
3107 $data->grade = '';
3110 if (!empty($flags->workflowstate)) {
3111 $data->workflowstate = $flags->workflowstate;
3113 if (!empty($flags->allocatedmarker)) {
3114 $data->allocatedmarker = $flags->allocatedmarker;
3117 // Warning if required.
3118 $allsubmissions = $this->get_all_submissions($userid);
3120 if ($attemptnumber != -1) {
3121 $params = array('attemptnumber'=>$attemptnumber + 1,
3122 'totalattempts'=>count($allsubmissions));
3123 $message = get_string('editingpreviousfeedbackwarning', 'assign', $params);
3124 $o .= $this->get_renderer()->notification($message);
3127 // Now show the grading form.
3128 if (!$mform) {
3129 $pagination = array('rownum'=>$rownum,
3130 'useridlistid'=>$useridlistid,
3131 'last'=>$last,
3132 'userid'=>optional_param('userid', 0, PARAM_INT),
3133 'attemptnumber'=>$attemptnumber);
3134 $formparams = array($this, $data, $pagination);
3135 $mform = new mod_assign_grade_form(null,
3136 $formparams,
3137 'post',
3139 array('class'=>'gradeform'));
3141 $o .= $this->get_renderer()->heading(get_string('grade'), 3);
3142 $o .= $this->get_renderer()->render(new assign_form('gradingform', $mform));
3144 if (count($allsubmissions) > 1 && $attemptnumber == -1) {
3145 $allgrades = $this->get_all_grades($userid);
3146 $history = new assign_attempt_history($allsubmissions,
3147 $allgrades,
3148 $this->get_submission_plugins(),
3149 $this->get_feedback_plugins(),
3150 $this->get_course_module()->id,
3151 $this->get_return_action(),
3152 $this->get_return_params(),
3153 true,
3154 $useridlistid,
3155 $rownum);
3157 $o .= $this->get_renderer()->render($history);
3160 \mod_assign\event\grading_form_viewed::create_from_user($this, $user)->trigger();
3162 $o .= $this->view_footer();
3163 return $o;
3167 * Show a confirmation page to make sure they want to release student identities.
3169 * @return string
3171 protected function view_reveal_identities_confirm() {
3172 require_capability('mod/assign:revealidentities', $this->get_context());
3174 $o = '';
3175 $header = new assign_header($this->get_instance(),
3176 $this->get_context(),
3177 false,
3178 $this->get_course_module()->id);
3179 $o .= $this->get_renderer()->render($header);
3181 $urlparams = array('id'=>$this->get_course_module()->id,
3182 'action'=>'revealidentitiesconfirm',
3183 'sesskey'=>sesskey());
3184 $confirmurl = new moodle_url('/mod/assign/view.php', $urlparams);
3186 $urlparams = array('id'=>$this->get_course_module()->id,
3187 'action'=>'grading');
3188 $cancelurl = new moodle_url('/mod/assign/view.php', $urlparams);
3190 $o .= $this->get_renderer()->confirm(get_string('revealidentitiesconfirm', 'assign'),
3191 $confirmurl,
3192 $cancelurl);
3193 $o .= $this->view_footer();
3195 \mod_assign\event\reveal_identities_confirmation_page_viewed::create_from_assign($this)->trigger();
3197 return $o;
3201 * View a link to go back to the previous page. Uses url parameters returnaction and returnparams.
3203 * @return string
3205 protected function view_return_links() {
3206 $returnaction = optional_param('returnaction', '', PARAM_ALPHA);
3207 $returnparams = optional_param('returnparams', '', PARAM_TEXT);
3209 $params = array();
3210 $returnparams = str_replace('&amp;', '&', $returnparams);
3211 parse_str($returnparams, $params);
3212 $newparams = array('id' => $this->get_course_module()->id, 'action' => $returnaction);
3213 $params = array_merge($newparams, $params);
3215 $url = new moodle_url('/mod/assign/view.php', $params);
3216 return $this->get_renderer()->single_button($url, get_string('back'), 'get');
3220 * View the grading table of all submissions for this assignment.
3222 * @return string
3224 protected function view_grading_table() {
3225 global $USER, $CFG;
3227 // Include grading options form.
3228 require_once($CFG->dirroot . '/mod/assign/gradingoptionsform.php');
3229 require_once($CFG->dirroot . '/mod/assign/quickgradingform.php');
3230 require_once($CFG->dirroot . '/mod/assign/gradingbatchoperationsform.php');
3231 $o = '';
3232 $cmid = $this->get_course_module()->id;
3234 $links = array();
3235 if (has_capability('gradereport/grader:view', $this->get_course_context()) &&
3236 has_capability('moodle/grade:viewall', $this->get_course_context())) {
3237 $gradebookurl = '/grade/report/grader/index.php?id=' . $this->get_course()->id;
3238 $links[$gradebookurl] = get_string('viewgradebook', 'assign');
3240 if ($this->is_any_submission_plugin_enabled() && $this->count_submissions()) {
3241 $downloadurl = '/mod/assign/view.php?id=' . $cmid . '&action=downloadall';
3242 $links[$downloadurl] = get_string('downloadall', 'assign');
3244 if ($this->is_blind_marking() &&
3245 has_capability('mod/assign:revealidentities', $this->get_context())) {
3246 $revealidentitiesurl = '/mod/assign/view.php?id=' . $cmid . '&action=revealidentities';
3247 $links[$revealidentitiesurl] = get_string('revealidentities', 'assign');
3249 foreach ($this->get_feedback_plugins() as $plugin) {
3250 if ($plugin->is_enabled() && $plugin->is_visible()) {
3251 foreach ($plugin->get_grading_actions() as $action => $description) {
3252 $url = '/mod/assign/view.php' .
3253 '?id=' . $cmid .
3254 '&plugin=' . $plugin->get_type() .
3255 '&pluginsubtype=assignfeedback' .
3256 '&action=viewpluginpage&pluginaction=' . $action;
3257 $links[$url] = $description;
3262 // Sort links alphabetically based on the link description.
3263 core_collator::asort($links);
3265 $gradingactions = new url_select($links);
3266 $gradingactions->set_label(get_string('choosegradingaction', 'assign'));
3268 $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
3270 $perpage = (int) get_user_preferences('assign_perpage', 10);
3271 $filter = get_user_preferences('assign_filter', '');
3272 $markerfilter = get_user_preferences('assign_markerfilter', '');
3273 $workflowfilter = get_user_preferences('assign_workflowfilter', '');
3274 $controller = $gradingmanager->get_active_controller();
3275 $showquickgrading = empty($controller) && $this->can_grade();
3276 $quickgrading = get_user_preferences('assign_quickgrading', false);
3277 $showonlyactiveenrolopt = has_capability('moodle/course:viewsuspendedusers', $this->context);
3279 $markingallocation = $this->get_instance()->markingworkflow &&
3280 $this->get_instance()->markingallocation &&
3281 has_capability('mod/assign:manageallocations', $this->context);
3282 // Get markers to use in drop lists.
3283 $markingallocationoptions = array();
3284 if ($markingallocation) {
3285 list($sort, $params) = users_order_by_sql();
3286 $markers = get_users_by_capability($this->context, 'mod/assign:grade', '', $sort);
3287 $markingallocationoptions[''] = get_string('filternone', 'assign');
3288 $markingallocationoptions[ASSIGN_MARKER_FILTER_NO_MARKER] = get_string('markerfilternomarker', 'assign');
3289 foreach ($markers as $marker) {
3290 $markingallocationoptions[$marker->id] = fullname($marker);
3294 $markingworkflow = $this->get_instance()->markingworkflow;
3295 // Get marking states to show in form.
3296 $markingworkflowoptions = array();
3297 if ($markingworkflow) {
3298 $notmarked = get_string('markingworkflowstatenotmarked', 'assign');
3299 $markingworkflowoptions[''] = get_string('filternone', 'assign');
3300 $markingworkflowoptions[ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED] = $notmarked;
3301 $markingworkflowoptions = array_merge($markingworkflowoptions, $this->get_marking_workflow_states_for_current_user());
3304 // Print options for changing the filter and changing the number of results per page.
3305 $gradingoptionsformparams = array('cm'=>$cmid,
3306 'contextid'=>$this->context->id,
3307 'userid'=>$USER->id,
3308 'submissionsenabled'=>$this->is_any_submission_plugin_enabled(),
3309 'showquickgrading'=>$showquickgrading,
3310 'quickgrading'=>$quickgrading,
3311 'markingworkflowopt'=>$markingworkflowoptions,
3312 'markingallocationopt'=>$markingallocationoptions,
3313 'showonlyactiveenrolopt'=>$showonlyactiveenrolopt,
3314 'showonlyactiveenrol'=>$this->show_only_active_users());
3316 $classoptions = array('class'=>'gradingoptionsform');
3317 $gradingoptionsform = new mod_assign_grading_options_form(null,
3318 $gradingoptionsformparams,
3319 'post',
3321 $classoptions);
3323 $batchformparams = array('cm'=>$cmid,
3324 'submissiondrafts'=>$this->get_instance()->submissiondrafts,
3325 'duedate'=>$this->get_instance()->duedate,
3326 'attemptreopenmethod'=>$this->get_instance()->attemptreopenmethod,
3327 'feedbackplugins'=>$this->get_feedback_plugins(),
3328 'context'=>$this->get_context(),
3329 'markingworkflow'=>$markingworkflow,
3330 'markingallocation'=>$markingallocation);
3331 $classoptions = array('class'=>'gradingbatchoperationsform');
3333 $gradingbatchoperationsform = new mod_assign_grading_batch_operations_form(null,
3334 $batchformparams,
3335 'post',
3337 $classoptions);
3339 $gradingoptionsdata = new stdClass();
3340 $gradingoptionsdata->perpage = $perpage;
3341 $gradingoptionsdata->filter = $filter;
3342 $gradingoptionsdata->markerfilter = $markerfilter;
3343 $gradingoptionsdata->workflowfilter = $workflowfilter;
3344 $gradingoptionsform->set_data($gradingoptionsdata);
3346 $actionformtext = $this->get_renderer()->render($gradingactions);
3347 $header = new assign_header($this->get_instance(),
3348 $this->get_context(),
3349 false,
3350 $this->get_course_module()->id,
3351 get_string('grading', 'assign'),
3352 $actionformtext);
3353 $o .= $this->get_renderer()->render($header);
3355 $currenturl = $CFG->wwwroot .
3356 '/mod/assign/view.php?id=' .
3357 $this->get_course_module()->id .
3358 '&action=grading';
3360 $o .= groups_print_activity_menu($this->get_course_module(), $currenturl, true);
3362 // Plagiarism update status apearring in the grading book.
3363 if (!empty($CFG->enableplagiarism)) {
3364 require_once($CFG->libdir . '/plagiarismlib.php');
3365 $o .= plagiarism_update_status($this->get_course(), $this->get_course_module());
3368 // Load and print the table of submissions.
3369 if ($showquickgrading && $quickgrading) {
3370 $gradingtable = new assign_grading_table($this, $perpage, $filter, 0, true);
3371 $table = $this->get_renderer()->render($gradingtable);
3372 $page = optional_param('page', null, PARAM_INT);
3373 $quickformparams = array('cm'=>$this->get_course_module()->id,
3374 'gradingtable'=>$table,
3375 'sendstudentnotifications' => $this->get_instance()->sendstudentnotifications,
3376 'page' => $page);
3377 $quickgradingform = new mod_assign_quick_grading_form(null, $quickformparams);
3379 $o .= $this->get_renderer()->render(new assign_form('quickgradingform', $quickgradingform));
3380 } else {
3381 $gradingtable = new assign_grading_table($this, $perpage, $filter, 0, false);
3382 $o .= $this->get_renderer()->render($gradingtable);
3385 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
3386 $users = array_keys($this->list_participants($currentgroup, true));
3387 if (count($users) != 0 && $this->can_grade()) {
3388 // If no enrolled user in a course then don't display the batch operations feature.
3389 $assignform = new assign_form('gradingbatchoperationsform', $gradingbatchoperationsform);
3390 $o .= $this->get_renderer()->render($assignform);
3392 $assignform = new assign_form('gradingoptionsform',
3393 $gradingoptionsform,
3394 'M.mod_assign.init_grading_options');
3395 $o .= $this->get_renderer()->render($assignform);
3396 return $o;
3400 * View entire grading page.
3402 * @return string
3404 protected function view_grading_page() {
3405 global $CFG;
3407 $o = '';
3408 // Need submit permission to submit an assignment.
3409 $this->require_view_grades();
3410 require_once($CFG->dirroot . '/mod/assign/gradeform.php');
3412 // Only load this if it is.
3413 $o .= $this->view_grading_table();
3415 $o .= $this->view_footer();
3417 \mod_assign\event\grading_table_viewed::create_from_assign($this)->trigger();
3419 return $o;
3423 * Capture the output of the plagiarism plugins disclosures and return it as a string.
3425 * @return string
3427 protected function plagiarism_print_disclosure() {
3428 global $CFG;
3429 $o = '';
3431 if (!empty($CFG->enableplagiarism)) {
3432 require_once($CFG->libdir . '/plagiarismlib.php');
3434 $o .= plagiarism_print_disclosure($this->get_course_module()->id);
3437 return $o;
3441 * Message for students when assignment submissions have been closed.
3443 * @param string $title The page title
3444 * @param array $notices The array of notices to show.
3445 * @return string
3447 protected function view_notices($title, $notices) {
3448 global $CFG;
3450 $o = '';
3452 $header = new assign_header($this->get_instance(),
3453 $this->get_context(),
3454 $this->show_intro(),
3455 $this->get_course_module()->id,
3456 $title);
3457 $o .= $this->get_renderer()->render($header);
3459 foreach ($notices as $notice) {
3460 $o .= $this->get_renderer()->notification($notice);
3463 $url = new moodle_url('/mod/assign/view.php', array('id'=>$this->get_course_module()->id, 'action'=>'view'));
3464 $o .= $this->get_renderer()->continue_button($url);
3466 $o .= $this->view_footer();
3468 return $o;
3472 * Get the name for a user - hiding their real name if blind marking is on.
3474 * @param stdClass $user The user record as required by fullname()
3475 * @return string The name.
3477 public function fullname($user) {
3478 if ($this->is_blind_marking()) {
3479 $hasviewblind = has_capability('mod/assign:viewblinddetails', $this->get_context());
3480 if ($hasviewblind) {
3481 return fullname($user);
3482 } else {
3483 $uniqueid = $this->get_uniqueid_for_user($user->id);
3484 return get_string('participant', 'assign') . ' ' . $uniqueid;
3486 } else {
3487 return fullname($user);
3492 * View edit submissions page.
3494 * @param moodleform $mform
3495 * @param array $notices A list of notices to display at the top of the
3496 * edit submission form (e.g. from plugins).
3497 * @return string The page output.
3499 protected function view_edit_submission_page($mform, $notices) {
3500 global $CFG, $USER, $DB;
3502 $o = '';
3503 require_once($CFG->dirroot . '/mod/assign/submission_form.php');
3504 // Need submit permission to submit an assignment.
3505 $userid = optional_param('userid', $USER->id, PARAM_INT);
3506 $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
3507 if ($userid == $USER->id) {
3508 if (!$this->can_edit_submission($userid, $USER->id)) {
3509 print_error('nopermission');
3511 // User is editing their own submission.
3512 require_capability('mod/assign:submit', $this->context);
3513 $title = get_string('editsubmission', 'assign');
3514 } else {
3515 // User is editing another user's submission.
3516 if (!$this->can_edit_submission($userid, $USER->id)) {
3517 print_error('nopermission');
3520 $name = $this->fullname($user);
3521 $title = get_string('editsubmissionother', 'assign', $name);
3524 if (!$this->submissions_open($userid)) {
3525 $message = array(get_string('submissionsclosed', 'assign'));
3526 return $this->view_notices($title, $message);
3529 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
3530 $this->get_context(),
3531 $this->show_intro(),
3532 $this->get_course_module()->id,
3533 $title));
3534 if ($userid == $USER->id) {
3535 // We only show this if it their submission.
3536 $o .= $this->plagiarism_print_disclosure();
3538 $data = new stdClass();
3539 $data->userid = $userid;
3540 if (!$mform) {
3541 $mform = new mod_assign_submission_form(null, array($this, $data));
3544 foreach ($notices as $notice) {
3545 $o .= $this->get_renderer()->notification($notice);
3548 $o .= $this->get_renderer()->render(new assign_form('editsubmissionform', $mform));
3550 $o .= $this->view_footer();
3552 \mod_assign\event\submission_form_viewed::create_from_user($this, $user)->trigger();
3554 return $o;
3558 * See if this assignment has a grade yet.
3560 * @param int $userid
3561 * @return bool
3563 protected function is_graded($userid) {
3564 $grade = $this->get_user_grade($userid, false);
3565 if ($grade) {
3566 return ($grade->grade !== null && $grade->grade >= 0);
3568 return false;
3572 * Perform an access check to see if the current $USER can view this group submission.
3574 * @param int $groupid
3575 * @return bool
3577 public function can_view_group_submission($groupid) {
3578 global $USER;
3580 if (has_capability('mod/assign:grade', $this->context)) {
3581 return true;
3583 if (!is_enrolled($this->get_course_context(), $USER->id)) {
3584 return false;
3586 $members = $this->get_submission_group_members($groupid, true);
3587 foreach ($members as $member) {
3588 if ($member->id == $USER->id) {
3589 return true;
3592 return false;
3596 * Perform an access check to see if the current $USER can view this users submission.
3598 * @param int $userid
3599 * @return bool
3601 public function can_view_submission($userid) {
3602 global $USER;
3604 if (!$this->is_active_user($userid) && !has_capability('moodle/course:viewsuspendedusers', $this->context)) {
3605 return false;
3607 if (has_any_capability(array('mod/assign:viewgrades', 'mod/assign:grade'), $this->context)) {
3608 return true;
3610 if (!is_enrolled($this->get_course_context(), $userid)) {
3611 return false;
3613 if ($userid == $USER->id && has_capability('mod/assign:submit', $this->context)) {
3614 return true;
3616 return false;
3620 * Allows the plugin to show a batch grading operation page.
3622 * @param moodleform $mform
3623 * @return none
3625 protected function view_plugin_grading_batch_operation($mform) {
3626 require_capability('mod/assign:grade', $this->context);
3627 $prefix = 'plugingradingbatchoperation_';
3629 if ($data = $mform->get_data()) {
3630 $tail = substr($data->operation, strlen($prefix));
3631 list($plugintype, $action) = explode('_', $tail, 2);
3633 $plugin = $this->get_feedback_plugin_by_type($plugintype);
3634 if ($plugin) {
3635 $users = $data->selectedusers;
3636 $userlist = explode(',', $users);
3637 echo $plugin->grading_batch_operation($action, $userlist);
3638 return;
3641 print_error('invalidformdata', '');
3645 * Ask the user to confirm they want to perform this batch operation
3647 * @param moodleform $mform Set to a grading batch operations form
3648 * @return string - the page to view after processing these actions
3650 protected function process_grading_batch_operation(& $mform) {
3651 global $CFG;
3652 require_once($CFG->dirroot . '/mod/assign/gradingbatchoperationsform.php');
3653 require_sesskey();
3655 $markingallocation = $this->get_instance()->markingworkflow &&
3656 $this->get_instance()->markingallocation &&
3657 has_capability('mod/assign:manageallocations', $this->context);
3659 $batchformparams = array('cm'=>$this->get_course_module()->id,
3660 'submissiondrafts'=>$this->get_instance()->submissiondrafts,
3661 'duedate'=>$this->get_instance()->duedate,
3662 'attemptreopenmethod'=>$this->get_instance()->attemptreopenmethod,
3663 'feedbackplugins'=>$this->get_feedback_plugins(),
3664 'context'=>$this->get_context(),
3665 'markingworkflow'=>$this->get_instance()->markingworkflow,
3666 'markingallocation'=>$markingallocation);
3667 $formclasses = array('class'=>'gradingbatchoperationsform');
3668 $mform = new mod_assign_grading_batch_operations_form(null,
3669 $batchformparams,
3670 'post',
3672 $formclasses);
3674 if ($data = $mform->get_data()) {
3675 // Get the list of users.
3676 $users = $data->selectedusers;
3677 $userlist = explode(',', $users);
3679 $prefix = 'plugingradingbatchoperation_';
3681 if ($data->operation == 'grantextension') {
3682 // Reset the form so the grant extension page will create the extension form.
3683 $mform = null;
3684 return 'grantextension';
3685 } else if ($data->operation == 'setmarkingworkflowstate') {
3686 return 'viewbatchsetmarkingworkflowstate';
3687 } else if ($data->operation == 'setmarkingallocation') {
3688 return 'viewbatchmarkingallocation';
3689 } else if (strpos($data->operation, $prefix) === 0) {
3690 $tail = substr($data->operation, strlen($prefix));
3691 list($plugintype, $action) = explode('_', $tail, 2);
3693 $plugin = $this->get_feedback_plugin_by_type($plugintype);
3694 if ($plugin) {
3695 return 'plugingradingbatchoperation';
3699 foreach ($userlist as $userid) {
3700 if ($data->operation == 'lock') {
3701 $this->process_lock_submission($userid);
3702 } else if ($data->operation == 'unlock') {
3703 $this->process_unlock_submission($userid);
3704 } else if ($data->operation == 'reverttodraft') {
3705 $this->process_revert_to_draft($userid);
3706 } else if ($data->operation == 'addattempt') {
3707 if (!$this->get_instance()->teamsubmission) {
3708 $this->process_add_attempt($userid);
3712 if ($this->get_instance()->teamsubmission && $data->operation == 'addattempt') {
3713 // This needs to be handled separately so that each team submission is only re-opened one time.
3714 $this->process_add_attempt_group($userlist);
3718 return 'grading';
3722 * Shows a form that allows the workflow state for selected submissions to be changed.
3724 * @param moodleform $mform Set to a grading batch operations form
3725 * @return string - the page to view after processing these actions
3727 protected function view_batch_set_workflow_state($mform) {
3728 global $CFG, $DB;
3730 require_once($CFG->dirroot . '/mod/assign/batchsetmarkingworkflowstateform.php');
3732 $o = '';
3734 $submitteddata = $mform->get_data();
3735 $users = $submitteddata->selectedusers;
3736 $userlist = explode(',', $users);
3738 $formdata = array('id' => $this->get_course_module()->id,
3739 'selectedusers' => $users);
3741 $usershtml = '';
3743 $usercount = 0;
3744 $extrauserfields = get_extra_user_fields($this->get_context());
3745 foreach ($userlist as $userid) {
3746 if ($usercount >= 5) {
3747 $usershtml .= get_string('moreusers', 'assign', count($userlist) - 5);
3748 break;
3750 $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
3752 $usershtml .= $this->get_renderer()->render(new assign_user_summary($user,
3753 $this->get_course()->id,
3754 has_capability('moodle/site:viewfullnames',
3755 $this->get_course_context()),
3756 $this->is_blind_marking(),
3757 $this->get_uniqueid_for_user($user->id),
3758 $extrauserfields,
3759 !$this->is_active_user($userid)));
3760 $usercount += 1;
3763 $formparams = array(
3764 'userscount' => count($userlist),
3765 'usershtml' => $usershtml,
3766 'markingworkflowstates' => $this->get_marking_workflow_states_for_current_user()
3769 $mform = new mod_assign_batch_set_marking_workflow_state_form(null, $formparams);
3770 $mform->set_data($formdata); // Initialises the hidden elements.
3771 $header = new assign_header($this->get_instance(),
3772 $this->get_context(),
3773 $this->show_intro(),
3774 $this->get_course_module()->id,
3775 get_string('setmarkingworkflowstate', 'assign'));
3776 $o .= $this->get_renderer()->render($header);
3777 $o .= $this->get_renderer()->render(new assign_form('setworkflowstate', $mform));
3778 $o .= $this->view_footer();
3780 \mod_assign\event\batch_set_workflow_state_viewed::create_from_assign($this)->trigger();
3782 return $o;
3786 * Shows a form that allows the allocated marker for selected submissions to be changed.
3788 * @param moodleform $mform Set to a grading batch operations form
3789 * @return string - the page to view after processing these actions
3791 public function view_batch_markingallocation($mform) {
3792 global $CFG, $DB;
3794 require_once($CFG->dirroot . '/mod/assign/batchsetallocatedmarkerform.php');
3796 $o = '';
3798 $submitteddata = $mform->get_data();
3799 $users = $submitteddata->selectedusers;
3800 $userlist = explode(',', $users);
3802 $formdata = array('id' => $this->get_course_module()->id,
3803 'selectedusers' => $users);
3805 $usershtml = '';
3807 $usercount = 0;
3808 $extrauserfields = get_extra_user_fields($this->get_context());
3809 foreach ($userlist as $userid) {
3810 if ($usercount >= 5) {
3811 $usershtml .= get_string('moreusers', 'assign', count($userlist) - 5);
3812 break;
3814 $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
3816 $usershtml .= $this->get_renderer()->render(new assign_user_summary($user,
3817 $this->get_course()->id,
3818 has_capability('moodle/site:viewfullnames',
3819 $this->get_course_context()),
3820 $this->is_blind_marking(),
3821 $this->get_uniqueid_for_user($user->id),
3822 $extrauserfields,
3823 !$this->is_active_user($userid)));
3824 $usercount += 1;
3827 $formparams = array(
3828 'userscount' => count($userlist),
3829 'usershtml' => $usershtml,
3832 list($sort, $params) = users_order_by_sql();
3833 $markers = get_users_by_capability($this->get_context(), 'mod/assign:grade', '', $sort);
3834 $markerlist = array();
3835 foreach ($markers as $marker) {
3836 $markerlist[$marker->id] = fullname($marker);
3839 $formparams['markers'] = $markerlist;
3841 $mform = new mod_assign_batch_set_allocatedmarker_form(null, $formparams);
3842 $mform->set_data($formdata); // Initialises the hidden elements.
3843 $header = new assign_header($this->get_instance(),
3844 $this->get_context(),
3845 $this->show_intro(),
3846 $this->get_course_module()->id,
3847 get_string('setmarkingallocation', 'assign'));
3848 $o .= $this->get_renderer()->render($header);
3849 $o .= $this->get_renderer()->render(new assign_form('setworkflowstate', $mform));
3850 $o .= $this->view_footer();
3852 \mod_assign\event\batch_set_marker_allocation_viewed::create_from_assign($this)->trigger();
3854 return $o;
3858 * Ask the user to confirm they want to submit their work for grading.
3860 * @param moodleform $mform - null unless form validation has failed
3861 * @return string
3863 protected function check_submit_for_grading($mform) {
3864 global $USER, $CFG;
3866 require_once($CFG->dirroot . '/mod/assign/submissionconfirmform.php');
3868 // Check that all of the submission plugins are ready for this submission.
3869 $notifications = array();
3870 $submission = $this->get_user_submission($USER->id, false);
3871 $plugins = $this->get_submission_plugins();
3872 foreach ($plugins as $plugin) {
3873 if ($plugin->is_enabled() && $plugin->is_visible()) {
3874 $check = $plugin->precheck_submission($submission);
3875 if ($check !== true) {
3876 $notifications[] = $check;
3881 $data = new stdClass();
3882 $adminconfig = $this->get_admin_config();
3883 $requiresubmissionstatement = $this->get_instance()->requiresubmissionstatement &&
3884 !empty($adminconfig->submissionstatement);
3886 $submissionstatement = '';
3887 if (!empty($adminconfig->submissionstatement)) {
3888 // Format the submission statement before its sent. We turn off para because this is going within
3889 // a form element.
3890 $options = array(
3891 'context' => $this->get_context(),
3892 'para' => false
3894 $submissionstatement = format_text($adminconfig->submissionstatement, FORMAT_MOODLE, $options);
3897 if ($mform == null) {
3898 $mform = new mod_assign_confirm_submission_form(null, array($requiresubmissionstatement,
3899 $submissionstatement,
3900 $this->get_course_module()->id,
3901 $data));
3903 $o = '';
3904 $o .= $this->get_renderer()->header();
3905 $submitforgradingpage = new assign_submit_for_grading_page($notifications,
3906 $this->get_course_module()->id,
3907 $mform);
3908 $o .= $this->get_renderer()->render($submitforgradingpage);
3909 $o .= $this->view_footer();
3911 \mod_assign\event\submission_confirmation_form_viewed::create_from_assign($this)->trigger();
3913 return $o;
3917 * Print 2 tables of information with no action links -
3918 * the submission summary and the grading summary.
3920 * @param stdClass $user the user to print the report for
3921 * @param bool $showlinks - Return plain text or links to the profile
3922 * @return string - the html summary
3924 public function view_student_summary($user, $showlinks) {
3925 global $CFG, $DB, $PAGE;
3927 $instance = $this->get_instance();
3928 $grade = $this->get_user_grade($user->id, false);
3929 $flags = $this->get_user_flags($user->id, false);
3930 $submission = $this->get_user_submission($user->id, false);
3931 $o = '';
3933 $teamsubmission = null;
3934 $submissiongroup = null;
3935 $notsubmitted = array();
3936 if ($instance->teamsubmission) {
3937 $teamsubmission = $this->get_group_submission($user->id, 0, false);
3938 $submissiongroup = $this->get_submission_group($user->id);
3939 $groupid = 0;
3940 if ($submissiongroup) {
3941 $groupid = $submissiongroup->id;
3943 $notsubmitted = $this->get_submission_group_members_who_have_not_submitted($groupid, false);
3946 if ($this->can_view_submission($user->id)) {
3947 $showedit = $showlinks &&
3948 ($this->is_any_submission_plugin_enabled()) &&
3949 $this->can_edit_submission($user->id);
3951 $gradelocked = ($flags && $flags->locked) || $this->grading_disabled($user->id, false);
3953 // Grading criteria preview.
3954 $gradingmanager = get_grading_manager($this->context, 'mod_assign', 'submissions');
3955 $gradingcontrollerpreview = '';
3956 if ($gradingmethod = $gradingmanager->get_active_method()) {
3957 $controller = $gradingmanager->get_controller($gradingmethod);
3958 if ($controller->is_form_defined()) {
3959 $gradingcontrollerpreview = $controller->render_preview($PAGE);
3963 $showsubmit = ($showlinks && $this->submissions_open($user->id));
3964 $showsubmit = ($showsubmit && $this->show_submit_button($submission, $teamsubmission, $user->id));
3966 $extensionduedate = null;
3967 if ($flags) {
3968 $extensionduedate = $flags->extensionduedate;
3970 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_course_context());
3972 $gradingstatus = $this->get_grading_status($user->id);
3973 $usergroups = $this->get_all_groups($user->id);
3974 $submissionstatus = new assign_submission_status($instance->allowsubmissionsfromdate,
3975 $instance->alwaysshowdescription,
3976 $submission,
3977 $instance->teamsubmission,
3978 $teamsubmission,
3979 $submissiongroup,
3980 $notsubmitted,
3981 $this->is_any_submission_plugin_enabled(),
3982 $gradelocked,
3983 $this->is_graded($user->id),
3984 $instance->duedate,
3985 $instance->cutoffdate,
3986 $this->get_submission_plugins(),
3987 $this->get_return_action(),
3988 $this->get_return_params(),
3989 $this->get_course_module()->id,
3990 $this->get_course()->id,
3991 assign_submission_status::STUDENT_VIEW,
3992 $showedit,
3993 $showsubmit,
3994 $viewfullnames,
3995 $extensionduedate,
3996 $this->get_context(),
3997 $this->is_blind_marking(),
3998 $gradingcontrollerpreview,
3999 $instance->attemptreopenmethod,
4000 $instance->maxattempts,
4001 $gradingstatus,
4002 $instance->preventsubmissionnotingroup,
4003 $usergroups);
4004 if (has_capability('mod/assign:submit', $this->get_context(), $user)) {
4005 $o .= $this->get_renderer()->render($submissionstatus);
4008 require_once($CFG->libdir.'/gradelib.php');
4009 require_once($CFG->dirroot.'/grade/grading/lib.php');
4011 $gradinginfo = grade_get_grades($this->get_course()->id,
4012 'mod',
4013 'assign',
4014 $instance->id,
4015 $user->id);
4017 $gradingitem = null;
4018 $gradebookgrade = null;
4019 if (isset($gradinginfo->items[0])) {
4020 $gradingitem = $gradinginfo->items[0];
4021 $gradebookgrade = $gradingitem->grades[$user->id];
4024 // Check to see if all feedback plugins are empty.
4025 $emptyplugins = true;
4026 if ($grade) {
4027 foreach ($this->get_feedback_plugins() as $plugin) {
4028 if ($plugin->is_visible() && $plugin->is_enabled()) {
4029 if (!$plugin->is_empty($grade)) {
4030 $emptyplugins = false;
4036 if ($this->get_instance()->markingworkflow && $gradingstatus != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
4037 $emptyplugins = true; // Don't show feedback plugins until released either.
4040 $cangrade = has_capability('mod/assign:grade', $this->get_context());
4041 // If there is a visible grade, show the summary.
4042 if ((!is_null($gradebookgrade->grade) || !$emptyplugins)
4043 && ($cangrade || !$gradebookgrade->hidden)) {
4045 $gradefordisplay = null;
4046 $gradeddate = null;
4047 $grader = null;
4048 $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
4050 // Only show the grade if it is not hidden in gradebook.
4051 if (!is_null($gradebookgrade->grade) && ($cangrade || !$gradebookgrade->hidden)) {
4052 if ($controller = $gradingmanager->get_active_controller()) {
4053 $menu = make_grades_menu($this->get_instance()->grade);
4054 $controller->set_grade_range($menu, $this->get_instance()->grade > 0);
4055 $gradefordisplay = $controller->render_grade($PAGE,
4056 $grade->id,
4057 $gradingitem,
4058 $gradebookgrade->str_long_grade,
4059 $cangrade);
4060 } else {
4061 $gradefordisplay = $this->display_grade($gradebookgrade->grade, false);
4063 $gradeddate = $gradebookgrade->dategraded;
4064 if (isset($grade->grader)) {
4065 $grader = $DB->get_record('user', array('id'=>$grade->grader));
4069 $feedbackstatus = new assign_feedback_status($gradefordisplay,
4070 $gradeddate,
4071 $grader,
4072 $this->get_feedback_plugins(),
4073 $grade,
4074 $this->get_course_module()->id,
4075 $this->get_return_action(),
4076 $this->get_return_params());
4078 $o .= $this->get_renderer()->render($feedbackstatus);
4081 $allsubmissions = $this->get_all_submissions($user->id);
4083 if (count($allsubmissions) > 1) {
4084 $allgrades = $this->get_all_grades($user->id);
4085 $history = new assign_attempt_history($allsubmissions,
4086 $allgrades,
4087 $this->get_submission_plugins(),
4088 $this->get_feedback_plugins(),
4089 $this->get_course_module()->id,
4090 $this->get_return_action(),
4091 $this->get_return_params(),
4092 false,
4096 $o .= $this->get_renderer()->render($history);
4100 return $o;
4104 * Returns true if the submit subsission button should be shown to the user.
4106 * @param stdClass $submission The users own submission record.
4107 * @param stdClass $teamsubmission The users team submission record if there is one
4108 * @param int $userid The user
4109 * @return bool
4111 protected function show_submit_button($submission = null, $teamsubmission = null, $userid = null) {
4112 if ($teamsubmission) {
4113 if ($teamsubmission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
4114 // The assignment submission has been completed.
4115 return false;
4116 } else if ($this->submission_empty($teamsubmission)) {
4117 // There is nothing to submit yet.
4118 return false;
4119 } else if ($submission && $submission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
4120 // The user has already clicked the submit button on the team submission.
4121 return false;
4122 } else if (
4123 !empty($this->get_instance()->preventsubmissionnotingroup)
4124 && $this->get_submission_group($userid) == false
4126 return false;
4128 } else if ($submission) {
4129 if ($submission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
4130 // The assignment submission has been completed.
4131 return false;
4132 } else if ($this->submission_empty($submission)) {
4133 // There is nothing to submit.
4134 return false;
4136 } else {
4137 // We've not got a valid submission or team submission.
4138 return false;
4140 // Last check is that this instance allows drafts.
4141 return $this->get_instance()->submissiondrafts;
4145 * Get the grades for all previous attempts.
4146 * For each grade - the grader is a full user record,
4147 * and gradefordisplay is added (rendered from grading manager).
4149 * @param int $userid If not set, $USER->id will be used.
4150 * @return array $grades All grade records for this user.
4152 protected function get_all_grades($userid) {
4153 global $DB, $USER, $PAGE;
4155 // If the userid is not null then use userid.
4156 if (!$userid) {
4157 $userid = $USER->id;
4160 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
4162 $grades = $DB->get_records('assign_grades', $params, 'attemptnumber ASC');
4164 $gradercache = array();
4165 $cangrade = has_capability('mod/assign:grade', $this->get_context());
4167 // Need gradingitem and gradingmanager.
4168 $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
4169 $controller = $gradingmanager->get_active_controller();
4171 $gradinginfo = grade_get_grades($this->get_course()->id,
4172 'mod',
4173 'assign',
4174 $this->get_instance()->id,
4175 $userid);
4177 $gradingitem = null;
4178 if (isset($gradinginfo->items[0])) {
4179 $gradingitem = $gradinginfo->items[0];
4182 foreach ($grades as $grade) {
4183 // First lookup the grader info.
4184 if (isset($gradercache[$grade->grader])) {
4185 $grade->grader = $gradercache[$grade->grader];
4186 } else {
4187 // Not in cache - need to load the grader record.
4188 $grade->grader = $DB->get_record('user', array('id'=>$grade->grader));
4189 $gradercache[$grade->grader->id] = $grade->grader;
4192 // Now get the gradefordisplay.
4193 if ($controller) {
4194 $controller->set_grade_range(make_grades_menu($this->get_instance()->grade), $this->get_instance()->grade > 0);
4195 $grade->gradefordisplay = $controller->render_grade($PAGE,
4196 $grade->id,
4197 $gradingitem,
4198 $grade->grade,
4199 $cangrade);
4200 } else {
4201 $grade->gradefordisplay = $this->display_grade($grade->grade, false);
4206 return $grades;
4210 * Get the submissions for all previous attempts.
4212 * @param int $userid If not set, $USER->id will be used.
4213 * @return array $submissions All submission records for this user (or group).
4215 protected function get_all_submissions($userid) {
4216 global $DB, $USER;
4218 // If the userid is not null then use userid.
4219 if (!$userid) {
4220 $userid = $USER->id;
4223 $params = array();
4225 if ($this->get_instance()->teamsubmission) {
4226 $groupid = 0;
4227 $group = $this->get_submission_group($userid);
4228 if ($group) {
4229 $groupid = $group->id;
4232 // Params to get the group submissions.
4233 $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
4234 } else {
4235 // Params to get the user submissions.
4236 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
4239 // Return the submissions ordered by attempt.
4240 $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber ASC');
4242 return $submissions;
4246 * View submissions page (contains details of current submission).
4248 * @return string
4250 protected function view_submission_page() {
4251 global $CFG, $DB, $USER, $PAGE;
4253 $instance = $this->get_instance();
4255 $o = '';
4257 $postfix = '';
4258 if ($this->has_visible_attachments()) {
4259 $postfix = $this->render_area_files('mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0);
4261 $o .= $this->get_renderer()->render(new assign_header($instance,
4262 $this->get_context(),
4263 $this->show_intro(),
4264 $this->get_course_module()->id,
4265 '', '', $postfix));
4267 // Display plugin specific headers.
4268 $plugins = array_merge($this->get_submission_plugins(), $this->get_feedback_plugins());
4269 foreach ($plugins as $plugin) {
4270 if ($plugin->is_enabled() && $plugin->is_visible()) {
4271 $o .= $this->get_renderer()->render(new assign_plugin_header($plugin));
4275 if ($this->can_view_grades()) {
4276 $draft = ASSIGN_SUBMISSION_STATUS_DRAFT;
4277 $submitted = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
4279 // Group selector will only be displayed if necessary.
4280 $currenturl = new moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id));
4281 $o .= groups_print_activity_menu($this->get_course_module(), $currenturl->out(), true);
4283 $activitygroup = groups_get_activity_group($this->get_course_module());
4285 if ($instance->teamsubmission) {
4286 $defaultteammembers = $this->get_submission_group_members(0, true);
4287 $warnofungroupedusers = (count($defaultteammembers) > 0 && $instance->preventsubmissionnotingroup);
4289 $summary = new assign_grading_summary($this->count_teams($activitygroup),
4290 $instance->submissiondrafts,
4291 $this->count_submissions_with_status($draft),
4292 $this->is_any_submission_plugin_enabled(),
4293 $this->count_submissions_with_status($submitted),
4294 $instance->cutoffdate,
4295 $instance->duedate,
4296 $this->get_course_module()->id,
4297 $this->count_submissions_need_grading(),
4298 $instance->teamsubmission,
4299 $warnofungroupedusers);
4300 $o .= $this->get_renderer()->render($summary);
4301 } else {
4302 // The active group has already been updated in groups_print_activity_menu().
4303 $countparticipants = $this->count_participants($activitygroup);
4304 $summary = new assign_grading_summary($countparticipants,
4305 $instance->submissiondrafts,
4306 $this->count_submissions_with_status($draft),
4307 $this->is_any_submission_plugin_enabled(),
4308 $this->count_submissions_with_status($submitted),
4309 $instance->cutoffdate,
4310 $instance->duedate,
4311 $this->get_course_module()->id,
4312 $this->count_submissions_need_grading(),
4313 $instance->teamsubmission,
4314 false);
4315 $o .= $this->get_renderer()->render($summary);
4318 $grade = $this->get_user_grade($USER->id, false);
4319 $submission = $this->get_user_submission($USER->id, false);
4321 if ($this->can_view_submission($USER->id)) {
4322 $o .= $this->view_student_summary($USER, true);
4325 $o .= $this->view_footer();
4327 \mod_assign\event\submission_status_viewed::create_from_assign($this)->trigger();
4329 return $o;
4333 * Convert the final raw grade(s) in the grading table for the gradebook.
4335 * @param stdClass $grade
4336 * @return array
4338 protected function convert_grade_for_gradebook(stdClass $grade) {
4339 $gradebookgrade = array();
4340 if ($grade->grade >= 0) {
4341 $gradebookgrade['rawgrade'] = $grade->grade;
4343 // Allow "no grade" to be chosen.
4344 if ($grade->grade == -1) {
4345 $gradebookgrade['rawgrade'] = NULL;
4347 $gradebookgrade['userid'] = $grade->userid;
4348 $gradebookgrade['usermodified'] = $grade->grader;
4349 $gradebookgrade['datesubmitted'] = null;
4350 $gradebookgrade['dategraded'] = $grade->timemodified;
4351 if (isset($grade->feedbackformat)) {
4352 $gradebookgrade['feedbackformat'] = $grade->feedbackformat;
4354 if (isset($grade->feedbacktext)) {
4355 $gradebookgrade['feedback'] = $grade->feedbacktext;
4358 return $gradebookgrade;
4362 * Convert submission details for the gradebook.
4364 * @param stdClass $submission
4365 * @return array
4367 protected function convert_submission_for_gradebook(stdClass $submission) {
4368 $gradebookgrade = array();
4370 $gradebookgrade['userid'] = $submission->userid;
4371 $gradebookgrade['usermodified'] = $submission->userid;
4372 $gradebookgrade['datesubmitted'] = $submission->timemodified;
4374 return $gradebookgrade;
4378 * Update grades in the gradebook.
4380 * @param mixed $submission stdClass|null
4381 * @param mixed $grade stdClass|null
4382 * @return bool
4384 protected function gradebook_item_update($submission=null, $grade=null) {
4385 global $CFG;
4387 require_once($CFG->dirroot.'/mod/assign/lib.php');
4388 // Do not push grade to gradebook if blind marking is active as
4389 // the gradebook would reveal the students.
4390 if ($this->is_blind_marking()) {
4391 return false;
4394 // If marking workflow is enabled and grade is not released then remove any grade that may exist in the gradebook.
4395 if ($this->get_instance()->markingworkflow && !empty($grade) &&
4396 $this->get_grading_status($grade->userid) != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
4397 // Remove the grade (if it exists) from the gradebook as it is not 'final'.
4398 $grade->grade = -1;
4399 $grade->feedbacktext = '';
4402 if ($submission != null) {
4403 if ($submission->userid == 0) {
4404 // This is a group submission update.
4405 $team = groups_get_members($submission->groupid, 'u.id');
4407 foreach ($team as $member) {
4408 $membersubmission = clone $submission;
4409 $membersubmission->groupid = 0;
4410 $membersubmission->userid = $member->id;
4411 $this->gradebook_item_update($membersubmission, null);
4413 return;
4416 $gradebookgrade = $this->convert_submission_for_gradebook($submission);
4418 } else {
4419 $gradebookgrade = $this->convert_grade_for_gradebook($grade);
4421 // Grading is disabled, return.
4422 if ($this->grading_disabled($gradebookgrade['userid'])) {
4423 return false;
4425 $assign = clone $this->get_instance();
4426 $assign->cmidnumber = $this->get_course_module()->idnumber;
4427 // Set assign gradebook feedback plugin status (enabled and visible).
4428 $assign->gradefeedbackenabled = $this->is_gradebook_feedback_enabled();
4429 return assign_grade_item_update($assign, $gradebookgrade) == GRADE_UPDATE_OK;
4433 * Update team submission.
4435 * @param stdClass $submission
4436 * @param int $userid
4437 * @param bool $updatetime
4438 * @return bool
4440 protected function update_team_submission(stdClass $submission, $userid, $updatetime) {
4441 global $DB;
4443 if ($updatetime) {
4444 $submission->timemodified = time();
4447 // First update the submission for the current user.
4448 $mysubmission = $this->get_user_submission($userid, true, $submission->attemptnumber);
4449 $mysubmission->status = $submission->status;
4451 $this->update_submission($mysubmission, 0, $updatetime, false);
4453 // Now check the team settings to see if this assignment qualifies as submitted or draft.
4454 $team = $this->get_submission_group_members($submission->groupid, true);
4456 $allsubmitted = true;
4457 $anysubmitted = false;
4458 $result = true;
4459 if ($submission->status != ASSIGN_SUBMISSION_STATUS_REOPENED) {
4460 foreach ($team as $member) {
4461 $membersubmission = $this->get_user_submission($member->id, false, $submission->attemptnumber);
4463 // If no submission found for team member and member is active then everyone has not submitted.
4464 if (!$membersubmission || $membersubmission->status != ASSIGN_SUBMISSION_STATUS_SUBMITTED
4465 && ($this->is_active_user($member->id))) {
4466 $allsubmitted = false;
4467 if ($anysubmitted) {
4468 break;
4470 } else {
4471 $anysubmitted = true;
4474 if ($this->get_instance()->requireallteammemberssubmit) {
4475 if ($allsubmitted) {
4476 $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
4477 } else {
4478 $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
4480 $result = $DB->update_record('assign_submission', $submission);
4481 } else {
4482 if ($anysubmitted) {
4483 $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
4484 } else {
4485 $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
4487 $result = $DB->update_record('assign_submission', $submission);
4489 } else {
4490 // Set the group submission to reopened.
4491 foreach ($team as $member) {
4492 $membersubmission = $this->get_user_submission($member->id, true, $submission->attemptnumber);
4493 $membersubmission->status = ASSIGN_SUBMISSION_STATUS_REOPENED;
4494 $result = $DB->update_record('assign_submission', $membersubmission) && $result;
4496 $result = $DB->update_record('assign_submission', $submission) && $result;
4499 $this->gradebook_item_update($submission);
4500 return $result;
4504 * Update grades in the gradebook based on submission time.
4506 * @param stdClass $submission
4507 * @param int $userid
4508 * @param bool $updatetime
4509 * @param bool $teamsubmission
4510 * @return bool
4512 protected function update_submission(stdClass $submission, $userid, $updatetime, $teamsubmission) {
4513 global $DB;
4515 if ($teamsubmission) {
4516 return $this->update_team_submission($submission, $userid, $updatetime);
4519 if ($updatetime) {
4520 $submission->timemodified = time();
4522 $result= $DB->update_record('assign_submission', $submission);
4523 if ($result) {
4524 $this->gradebook_item_update($submission);
4526 return $result;
4530 * Is this assignment open for submissions?
4532 * Check the due date,
4533 * prevent late submissions,
4534 * has this person already submitted,
4535 * is the assignment locked?
4537 * @param int $userid - Optional userid so we can see if a different user can submit
4538 * @param bool $skipenrolled - Skip enrollment checks (because they have been done already)
4539 * @param stdClass $submission - Pre-fetched submission record (or false to fetch it)
4540 * @param stdClass $flags - Pre-fetched user flags record (or false to fetch it)
4541 * @param stdClass $gradinginfo - Pre-fetched user gradinginfo record (or false to fetch it)
4542 * @return bool
4544 public function submissions_open($userid = 0,
4545 $skipenrolled = false,
4546 $submission = false,
4547 $flags = false,
4548 $gradinginfo = false) {
4549 global $USER;
4551 if (!$userid) {
4552 $userid = $USER->id;
4555 $time = time();
4556 $dateopen = true;
4557 $finaldate = false;
4558 if ($this->get_instance()->cutoffdate) {
4559 $finaldate = $this->get_instance()->cutoffdate;
4562 if ($flags === false) {
4563 $flags = $this->get_user_flags($userid, false);
4565 if ($flags && $flags->locked) {
4566 return false;
4569 // User extensions.
4570 if ($finaldate) {
4571 if ($flags && $flags->extensionduedate) {
4572 // Extension can be before cut off date.
4573 if ($flags->extensionduedate > $finaldate) {
4574 $finaldate = $flags->extensionduedate;
4579 if ($finaldate) {
4580 $dateopen = ($this->get_instance()->allowsubmissionsfromdate <= $time && $time <= $finaldate);
4581 } else {
4582 $dateopen = ($this->get_instance()->allowsubmissionsfromdate <= $time);
4585 if (!$dateopen) {
4586 return false;
4589 // Now check if this user has already submitted etc.
4590 if (!$skipenrolled && !is_enrolled($this->get_course_context(), $userid)) {
4591 return false;
4593 // Note you can pass null for submission and it will not be fetched.
4594 if ($submission === false) {
4595 if ($this->get_instance()->teamsubmission) {
4596 $submission = $this->get_group_submission($userid, 0, false);
4597 } else {
4598 $submission = $this->get_user_submission($userid, false);
4601 if ($submission) {
4603 if ($this->get_instance()->submissiondrafts && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
4604 // Drafts are tracked and the student has submitted the assignment.
4605 return false;
4609 // See if this user grade is locked in the gradebook.
4610 if ($gradinginfo === false) {
4611 $gradinginfo = grade_get_grades($this->get_course()->id,
4612 'mod',
4613 'assign',
4614 $this->get_instance()->id,
4615 array($userid));
4617 if ($gradinginfo &&
4618 isset($gradinginfo->items[0]->grades[$userid]) &&
4619 $gradinginfo->items[0]->grades[$userid]->locked) {
4620 return false;
4623 return true;
4627 * Render the files in file area.
4629 * @param string $component
4630 * @param string $area
4631 * @param int $submissionid
4632 * @return string
4634 public function render_area_files($component, $area, $submissionid) {
4635 global $USER;
4637 return $this->get_renderer()->assign_files($this->context, $submissionid, $area, $component);
4642 * Capability check to make sure this grader can edit this submission.
4644 * @param int $userid - The user whose submission is to be edited
4645 * @param int $graderid (optional) - The user who will do the editing (default to $USER->id).
4646 * @return bool
4648 public function can_edit_submission($userid, $graderid = 0) {
4649 global $USER;
4651 if (empty($graderid)) {
4652 $graderid = $USER->id;
4655 $instance = $this->get_instance();
4656 if ($userid == $graderid &&
4657 $instance->teamsubmission &&
4658 $instance->preventsubmissionnotingroup &&
4659 $this->get_submission_group($userid) == false) {
4660 return false;
4663 if ($userid == $graderid &&
4664 $this->submissions_open($userid) &&
4665 has_capability('mod/assign:submit', $this->context, $graderid)) {
4666 // User can edit their own submission.
4667 return true;
4670 if (!has_capability('mod/assign:editothersubmission', $this->context, $graderid)) {
4671 return false;
4674 $cm = $this->get_course_module();
4675 if (groups_get_activity_groupmode($cm) == SEPARATEGROUPS) {
4676 // These arrays are indexed by groupid.
4677 $studentgroups = array_keys(groups_get_activity_allowed_groups($cm, $userid));
4678 $gradergroups = array_keys(groups_get_activity_allowed_groups($cm, $graderid));
4680 return count(array_intersect($studentgroups, $gradergroups)) > 0;
4682 return true;
4686 * Returns a list of teachers that should be grading given submission.
4688 * @param int $userid The submission to grade
4689 * @return array
4691 protected function get_graders($userid) {
4692 // Potential graders should be active users only.
4693 $potentialgraders = get_enrolled_users($this->context, "mod/assign:grade", null, 'u.*', null, null, null, true);
4695 $graders = array();
4696 if (groups_get_activity_groupmode($this->get_course_module()) == SEPARATEGROUPS) {
4697 if ($groups = groups_get_all_groups($this->get_course()->id, $userid, $this->get_course_module()->groupingid)) {
4698 foreach ($groups as $group) {
4699 foreach ($potentialgraders as $grader) {
4700 if ($grader->id == $userid) {
4701 // Do not send self.
4702 continue;
4704 if (groups_is_member($group->id, $grader->id)) {
4705 $graders[$grader->id] = $grader;
4709 } else {
4710 // User not in group, try to find graders without group.
4711 foreach ($potentialgraders as $grader) {
4712 if ($grader->id == $userid) {
4713 // Do not send self.
4714 continue;
4716 if (!groups_has_membership($this->get_course_module(), $grader->id)) {
4717 $graders[$grader->id] = $grader;
4721 } else {
4722 foreach ($potentialgraders as $grader) {
4723 if ($grader->id == $userid) {
4724 // Do not send self.
4725 continue;
4727 // Must be enrolled.
4728 if (is_enrolled($this->get_course_context(), $grader->id)) {
4729 $graders[$grader->id] = $grader;
4733 return $graders;
4737 * Returns a list of users that should receive notification about given submission.
4739 * @param int $userid The submission to grade
4740 * @return array
4742 protected function get_notifiable_users($userid) {
4743 // Potential users should be active users only.
4744 $potentialusers = get_enrolled_users($this->context, "mod/assign:receivegradernotifications",
4745 null, 'u.*', null, null, null, true);
4747 $notifiableusers = array();
4748 if (groups_get_activity_groupmode($this->get_course_module()) == SEPARATEGROUPS) {
4749 if ($groups = groups_get_all_groups($this->get_course()->id, $userid, $this->get_course_module()->groupingid)) {
4750 foreach ($groups as $group) {
4751 foreach ($potentialusers as $potentialuser) {
4752 if ($potentialuser->id == $userid) {
4753 // Do not send self.
4754 continue;
4756 if (groups_is_member($group->id, $potentialuser->id)) {
4757 $notifiableusers[$potentialuser->id] = $potentialuser;
4761 } else {
4762 // User not in group, try to find graders without group.
4763 foreach ($potentialusers as $potentialuser) {
4764 if ($potentialuser->id == $userid) {
4765 // Do not send self.
4766 continue;
4768 if (!groups_has_membership($this->get_course_module(), $potentialuser->id)) {
4769 $notifiableusers[$potentialuser->id] = $potentialuser;
4773 } else {
4774 foreach ($potentialusers as $potentialuser) {
4775 if ($potentialuser->id == $userid) {
4776 // Do not send self.
4777 continue;
4779 // Must be enrolled.
4780 if (is_enrolled($this->get_course_context(), $potentialuser->id)) {
4781 $notifiableusers[$potentialuser->id] = $potentialuser;
4785 return $notifiableusers;
4789 * Format a notification for plain text.
4791 * @param string $messagetype
4792 * @param stdClass $info
4793 * @param stdClass $course
4794 * @param stdClass $context
4795 * @param string $modulename
4796 * @param string $assignmentname
4798 protected static function format_notification_message_text($messagetype,
4799 $info,
4800 $course,
4801 $context,
4802 $modulename,
4803 $assignmentname) {
4804 $formatparams = array('context' => $context->get_course_context());
4805 $posttext = format_string($course->shortname, true, $formatparams) .
4806 ' -> ' .
4807 $modulename .
4808 ' -> ' .
4809 format_string($assignmentname, true, $formatparams) . "\n";
4810 $posttext .= '---------------------------------------------------------------------' . "\n";
4811 $posttext .= get_string($messagetype . 'text', 'assign', $info)."\n";
4812 $posttext .= "\n---------------------------------------------------------------------\n";
4813 return $posttext;
4817 * Format a notification for HTML.
4819 * @param string $messagetype
4820 * @param stdClass $info
4821 * @param stdClass $course
4822 * @param stdClass $context
4823 * @param string $modulename
4824 * @param stdClass $coursemodule
4825 * @param string $assignmentname
4827 protected static function format_notification_message_html($messagetype,
4828 $info,
4829 $course,
4830 $context,
4831 $modulename,
4832 $coursemodule,
4833 $assignmentname) {
4834 global $CFG;
4835 $formatparams = array('context' => $context->get_course_context());
4836 $posthtml = '<p><font face="sans-serif">' .
4837 '<a href="' . $CFG->wwwroot . '/course/view.php?id=' . $course->id . '">' .
4838 format_string($course->shortname, true, $formatparams) .
4839 '</a> ->' .
4840 '<a href="' . $CFG->wwwroot . '/mod/assign/index.php?id=' . $course->id . '">' .
4841 $modulename .
4842 '</a> ->' .
4843 '<a href="' . $CFG->wwwroot . '/mod/assign/view.php?id=' . $coursemodule->id . '">' .
4844 format_string($assignmentname, true, $formatparams) .
4845 '</a></font></p>';
4846 $posthtml .= '<hr /><font face="sans-serif">';
4847 $posthtml .= '<p>' . get_string($messagetype . 'html', 'assign', $info) . '</p>';
4848 $posthtml .= '</font><hr />';
4849 return $posthtml;
4853 * Message someone about something (static so it can be called from cron).
4855 * @param stdClass $userfrom
4856 * @param stdClass $userto
4857 * @param string $messagetype
4858 * @param string $eventtype
4859 * @param int $updatetime
4860 * @param stdClass $coursemodule
4861 * @param stdClass $context
4862 * @param stdClass $course
4863 * @param string $modulename
4864 * @param string $assignmentname
4865 * @param bool $blindmarking
4866 * @param int $uniqueidforuser
4867 * @return void
4869 public static function send_assignment_notification($userfrom,
4870 $userto,
4871 $messagetype,
4872 $eventtype,
4873 $updatetime,
4874 $coursemodule,
4875 $context,
4876 $course,
4877 $modulename,
4878 $assignmentname,
4879 $blindmarking,
4880 $uniqueidforuser) {
4881 global $CFG;
4883 $info = new stdClass();
4884 if ($blindmarking) {
4885 $userfrom = clone($userfrom);
4886 $info->username = get_string('participant', 'assign') . ' ' . $uniqueidforuser;
4887 $userfrom->firstname = get_string('participant', 'assign');
4888 $userfrom->lastname = $uniqueidforuser;
4889 $userfrom->email = $CFG->noreplyaddress;
4890 } else {
4891 $info->username = fullname($userfrom, true);
4893 $info->assignment = format_string($assignmentname, true, array('context'=>$context));
4894 $info->url = $CFG->wwwroot.'/mod/assign/view.php?id='.$coursemodule->id;
4895 $info->timeupdated = userdate($updatetime, get_string('strftimerecentfull'));
4897 $postsubject = get_string($messagetype . 'small', 'assign', $info);
4898 $posttext = self::format_notification_message_text($messagetype,
4899 $info,
4900 $course,
4901 $context,
4902 $modulename,
4903 $assignmentname);
4904 $posthtml = '';
4905 if ($userto->mailformat == 1) {
4906 $posthtml = self::format_notification_message_html($messagetype,
4907 $info,
4908 $course,
4909 $context,
4910 $modulename,
4911 $coursemodule,
4912 $assignmentname);
4915 $eventdata = new stdClass();
4916 $eventdata->modulename = 'assign';
4917 $eventdata->userfrom = $userfrom;
4918 $eventdata->userto = $userto;
4919 $eventdata->subject = $postsubject;
4920 $eventdata->fullmessage = $posttext;
4921 $eventdata->fullmessageformat = FORMAT_PLAIN;
4922 $eventdata->fullmessagehtml = $posthtml;
4923 $eventdata->smallmessage = $postsubject;
4925 $eventdata->name = $eventtype;
4926 $eventdata->component = 'mod_assign';
4927 $eventdata->notification = 1;
4928 $eventdata->contexturl = $info->url;
4929 $eventdata->contexturlname = $info->assignment;
4931 message_send($eventdata);
4935 * Message someone about something.
4937 * @param stdClass $userfrom
4938 * @param stdClass $userto
4939 * @param string $messagetype
4940 * @param string $eventtype
4941 * @param int $updatetime
4942 * @return void
4944 public function send_notification($userfrom,
4945 $userto,
4946 $messagetype,
4947 $eventtype,
4948 $updatetime) {
4949 self::send_assignment_notification($userfrom,
4950 $userto,
4951 $messagetype,
4952 $eventtype,
4953 $updatetime,
4954 $this->get_course_module(),
4955 $this->get_context(),
4956 $this->get_course(),
4957 $this->get_module_name(),
4958 $this->get_instance()->name,
4959 $this->is_blind_marking(),
4960 $this->get_uniqueid_for_user($userfrom->id));
4964 * Notify student upon successful submission copy.
4966 * @param stdClass $submission
4967 * @return void
4969 protected function notify_student_submission_copied(stdClass $submission) {
4970 global $DB, $USER;
4972 $adminconfig = $this->get_admin_config();
4973 // Use the same setting for this - no need for another one.
4974 if (empty($adminconfig->submissionreceipts)) {
4975 // No need to do anything.
4976 return;
4978 if ($submission->userid) {
4979 $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST);
4980 } else {
4981 $user = $USER;
4983 $this->send_notification($user,
4984 $user,
4985 'submissioncopied',
4986 'assign_notification',
4987 $submission->timemodified);
4990 * Notify student upon successful submission.
4992 * @param stdClass $submission
4993 * @return void
4995 protected function notify_student_submission_receipt(stdClass $submission) {
4996 global $DB, $USER;
4998 $adminconfig = $this->get_admin_config();
4999 if (empty($adminconfig->submissionreceipts)) {
5000 // No need to do anything.
5001 return;
5003 if ($submission->userid) {
5004 $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST);
5005 } else {
5006 $user = $USER;
5008 if ($submission->userid == $USER->id) {
5009 $this->send_notification(core_user::get_noreply_user(),
5010 $user,
5011 'submissionreceipt',
5012 'assign_notification',
5013 $submission->timemodified);
5014 } else {
5015 $this->send_notification($USER,
5016 $user,
5017 'submissionreceiptother',
5018 'assign_notification',
5019 $submission->timemodified);
5024 * Send notifications to graders upon student submissions.
5026 * @param stdClass $submission
5027 * @return void
5029 protected function notify_graders(stdClass $submission) {
5030 global $DB, $USER;
5032 $instance = $this->get_instance();
5034 $late = $instance->duedate && ($instance->duedate < time());
5036 if (!$instance->sendnotifications && !($late && $instance->sendlatenotifications)) {
5037 // No need to do anything.
5038 return;
5041 if ($submission->userid) {
5042 $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST);
5043 } else {
5044 $user = $USER;
5047 if ($notifyusers = $this->get_notifiable_users($user->id)) {
5048 foreach ($notifyusers as $notifyuser) {
5049 $this->send_notification($user,
5050 $notifyuser,
5051 'gradersubmissionupdated',
5052 'assign_notification',
5053 $submission->timemodified);
5059 * Submit a submission for grading.
5061 * @param stdClass $data - The form data
5062 * @param array $notices - List of error messages to display on an error condition.
5063 * @return bool Return false if the submission was not submitted.
5065 public function submit_for_grading($data, $notices) {
5066 global $USER;
5068 $userid = $USER->id;
5069 if (!empty($data->userid)) {
5070 $userid = $data->userid;
5072 // Need submit permission to submit an assignment.
5073 if ($userid == $USER->id) {
5074 require_capability('mod/assign:submit', $this->context);
5075 } else {
5076 if (!$this->can_edit_submission($userid, $USER->id)) {
5077 print_error('nopermission');
5081 $instance = $this->get_instance();
5083 if ($instance->teamsubmission) {
5084 $submission = $this->get_group_submission($userid, 0, true);
5085 } else {
5086 $submission = $this->get_user_submission($userid, true);
5089 if (!$this->submissions_open($userid)) {
5090 $notices[] = get_string('submissionsclosed', 'assign');
5091 return false;
5094 if ($instance->requiresubmissionstatement && empty($data->submissionstatement) && $USER->id == $userid) {
5095 return false;
5098 if ($submission->status != ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
5099 // Give each submission plugin a chance to process the submission.
5100 $plugins = $this->get_submission_plugins();
5101 foreach ($plugins as $plugin) {
5102 if ($plugin->is_enabled() && $plugin->is_visible()) {
5103 $plugin->submit_for_grading($submission);
5107 $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
5108 $this->update_submission($submission, $userid, true, $instance->teamsubmission);
5109 $completion = new completion_info($this->get_course());
5110 if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
5111 $completion->update_state($this->get_course_module(), COMPLETION_COMPLETE, $userid);
5114 if (!empty($data->submissionstatement) && $USER->id == $userid) {
5115 \mod_assign\event\statement_accepted::create_from_submission($this, $submission)->trigger();
5117 $this->notify_graders($submission);
5118 $this->notify_student_submission_receipt($submission);
5120 \mod_assign\event\assessable_submitted::create_from_submission($this, $submission, false)->trigger();
5122 return true;
5124 $notices[] = get_string('submissionsclosed', 'assign');
5125 return false;
5129 * A students submission is submitted for grading by a teacher.
5131 * @return bool
5133 protected function process_submit_other_for_grading($mform, $notices) {
5134 global $USER, $CFG;
5136 require_sesskey();
5138 $userid = optional_param('userid', $USER->id, PARAM_INT);
5140 if (!$this->submissions_open($userid)) {
5141 $notices[] = get_string('submissionsclosed', 'assign');
5142 return false;
5144 $data = new stdClass();
5145 $data->userid = $userid;
5146 return $this->submit_for_grading($data, $notices);
5150 * Assignment submission is processed before grading.
5152 * @param moodleform|null $mform If validation failed when submitting this form - this is the moodleform.
5153 * It can be null.
5154 * @return bool Return false if the validation fails. This affects which page is displayed next.
5156 protected function process_submit_for_grading($mform, $notices) {
5157 global $CFG;
5159 require_once($CFG->dirroot . '/mod/assign/submissionconfirmform.php');
5160 require_sesskey();
5162 if (!$this->submissions_open()) {
5163 $notices[] = get_string('submissionsclosed', 'assign');
5164 return false;
5166 $instance = $this->get_instance();
5167 $data = new stdClass();
5168 $adminconfig = $this->get_admin_config();
5169 $requiresubmissionstatement = $instance->requiresubmissionstatement &&
5170 !empty($adminconfig->submissionstatement);
5172 $submissionstatement = '';
5173 if (!empty($adminconfig->submissionstatement)) {
5174 // Format the submission statement before its sent. We turn off para because this is going within
5175 // a form element.
5176 $options = array(
5177 'context' => $this->get_context(),
5178 'para' => false
5180 $submissionstatement = format_text($adminconfig->submissionstatement, FORMAT_MOODLE, $options);
5183 if ($mform == null) {
5184 $mform = new mod_assign_confirm_submission_form(null, array($requiresubmissionstatement,
5185 $submissionstatement,
5186 $this->get_course_module()->id,
5187 $data));
5190 $data = $mform->get_data();
5191 if (!$mform->is_cancelled()) {
5192 if ($mform->get_data() == false) {
5193 return false;
5195 return $this->submit_for_grading($data, $notices);
5197 return true;
5201 * Save the extension date for a single user.
5203 * @param int $userid The user id
5204 * @param mixed $extensionduedate Either an integer date or null
5205 * @return boolean
5207 public function save_user_extension($userid, $extensionduedate) {
5208 global $DB;
5210 // Need submit permission to submit an assignment.
5211 require_capability('mod/assign:grantextension', $this->context);
5213 if (!is_enrolled($this->get_course_context(), $userid)) {
5214 return false;
5216 if (!has_capability('mod/assign:submit', $this->context, $userid)) {
5217 return false;
5220 if ($this->get_instance()->duedate && $extensionduedate) {
5221 if ($this->get_instance()->duedate > $extensionduedate) {
5222 return false;
5225 if ($this->get_instance()->allowsubmissionsfromdate && $extensionduedate) {
5226 if ($this->get_instance()->allowsubmissionsfromdate > $extensionduedate) {
5227 return false;
5231 $flags = $this->get_user_flags($userid, true);
5232 $flags->extensionduedate = $extensionduedate;
5234 $result = $this->update_user_flags($flags);
5236 if ($result) {
5237 \mod_assign\event\extension_granted::create_from_assign($this, $userid)->trigger();
5239 return $result;
5243 * Save extension date.
5245 * @param moodleform $mform The submitted form
5246 * @return boolean
5248 protected function process_save_extension(& $mform) {
5249 global $DB, $CFG;
5251 // Include extension form.
5252 require_once($CFG->dirroot . '/mod/assign/extensionform.php');
5253 require_sesskey();
5255 $batchusers = optional_param('selectedusers', '', PARAM_SEQUENCE);
5256 $userid = 0;
5257 if (!$batchusers) {
5258 $userid = required_param('userid', PARAM_INT);
5259 $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
5261 $mform = new mod_assign_extension_form(null, array($this->get_course_module()->id,
5262 $userid,
5263 $batchusers,
5264 $this->get_instance(),
5265 null));
5267 if ($mform->is_cancelled()) {
5268 return true;
5271 if ($formdata = $mform->get_data()) {
5272 if ($batchusers) {
5273 $users = explode(',', $batchusers);
5274 $result = true;
5275 foreach ($users as $userid) {
5276 $result = $this->save_user_extension($userid, $formdata->extensionduedate) && $result;
5278 return $result;
5279 } else {
5280 return $this->save_user_extension($userid, $formdata->extensionduedate);
5283 return false;
5288 * Save quick grades.
5290 * @return string The result of the save operation
5292 protected function process_save_quick_grades() {
5293 global $USER, $DB, $CFG;
5295 // Need grade permission.
5296 require_capability('mod/assign:grade', $this->context);
5297 require_sesskey();
5299 // Make sure advanced grading is disabled.
5300 $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
5301 $controller = $gradingmanager->get_active_controller();
5302 if (!empty($controller)) {
5303 return get_string('errorquickgradingvsadvancedgrading', 'assign');
5306 $users = array();
5307 // First check all the last modified values.
5308 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
5309 $participants = $this->list_participants($currentgroup, true);
5311 // Gets a list of possible users and look for values based upon that.
5312 foreach ($participants as $userid => $unused) {
5313 $modified = optional_param('grademodified_' . $userid, -1, PARAM_INT);
5314 // Gather the userid, updated grade and last modified value.
5315 $record = new stdClass();
5316 $record->userid = $userid;
5317 if ($modified >= 0) {
5318 $record->grade = unformat_float(optional_param('quickgrade_' . $record->userid, -1, PARAM_TEXT));
5319 $record->workflowstate = optional_param('quickgrade_' . $record->userid.'_workflowstate', false, PARAM_TEXT);
5320 $record->allocatedmarker = optional_param('quickgrade_' . $record->userid.'_allocatedmarker', false, PARAM_INT);
5321 } else {
5322 // This user was not in the grading table.
5323 continue;
5325 $record->lastmodified = $modified;
5326 $record->gradinginfo = grade_get_grades($this->get_course()->id,
5327 'mod',
5328 'assign',
5329 $this->get_instance()->id,
5330 array($userid));
5331 $users[$userid] = $record;
5334 if (empty($users)) {
5335 return get_string('nousersselected', 'assign');
5338 list($userids, $params) = $DB->get_in_or_equal(array_keys($users), SQL_PARAMS_NAMED);
5339 $params['assignid1'] = $this->get_instance()->id;
5340 $params['assignid2'] = $this->get_instance()->id;
5342 // Check them all for currency.
5343 $grademaxattempt = 'SELECT mxg.userid, MAX(mxg.attemptnumber) AS maxattempt
5344 FROM {assign_grades} mxg
5345 WHERE mxg.assignment = :assignid1 GROUP BY mxg.userid';
5347 $sql = 'SELECT u.id as userid, g.grade as grade, g.timemodified as lastmodified, uf.workflowstate, uf.allocatedmarker
5348 FROM {user} u
5349 LEFT JOIN ( ' . $grademaxattempt . ' ) gmx ON u.id = gmx.userid
5350 LEFT JOIN {assign_grades} g ON
5351 u.id = g.userid AND
5352 g.assignment = :assignid2 AND
5353 g.attemptnumber = gmx.maxattempt
5354 LEFT JOIN {assign_user_flags} uf ON uf.assignment = g.assignment AND uf.userid = g.userid
5355 WHERE u.id ' . $userids;
5356 $currentgrades = $DB->get_recordset_sql($sql, $params);
5358 $modifiedusers = array();
5359 foreach ($currentgrades as $current) {
5360 $modified = $users[(int)$current->userid];
5361 $grade = $this->get_user_grade($modified->userid, false);
5362 // Check to see if the grade column was even visible.
5363 $gradecolpresent = optional_param('quickgrade_' . $modified->userid, false, PARAM_INT) !== false;
5365 // Check to see if the outcomes were modified.
5366 if ($CFG->enableoutcomes) {
5367 foreach ($modified->gradinginfo->outcomes as $outcomeid => $outcome) {
5368 $oldoutcome = $outcome->grades[$modified->userid]->grade;
5369 $paramname = 'outcome_' . $outcomeid . '_' . $modified->userid;
5370 $newoutcome = optional_param($paramname, -1, PARAM_FLOAT);
5371 // Check to see if the outcome column was even visible.
5372 $outcomecolpresent = optional_param($paramname, false, PARAM_FLOAT) !== false;
5373 if ($outcomecolpresent && ($oldoutcome != $newoutcome)) {
5374 // Can't check modified time for outcomes because it is not reported.
5375 $modifiedusers[$modified->userid] = $modified;
5376 continue;
5381 // Let plugins participate.
5382 foreach ($this->feedbackplugins as $plugin) {
5383 if ($plugin->is_visible() && $plugin->is_enabled() && $plugin->supports_quickgrading()) {
5384 // The plugins must handle is_quickgrading_modified correctly - ie
5385 // handle hidden columns.
5386 if ($plugin->is_quickgrading_modified($modified->userid, $grade)) {
5387 if ((int)$current->lastmodified > (int)$modified->lastmodified) {
5388 return get_string('errorrecordmodified', 'assign');
5389 } else {
5390 $modifiedusers[$modified->userid] = $modified;
5391 continue;
5397 if (($current->grade < 0 || $current->grade === null) &&
5398 ($modified->grade < 0 || $modified->grade === null)) {
5399 // Different ways to indicate no grade.
5400 $modified->grade = $current->grade; // Keep existing grade.
5402 // Treat 0 and null as different values.
5403 if ($current->grade !== null) {
5404 $current->grade = floatval($current->grade);
5406 $gradechanged = $gradecolpresent && $current->grade !== $modified->grade;
5407 $markingallocationchanged = $this->get_instance()->markingworkflow &&
5408 $this->get_instance()->markingallocation &&
5409 ($modified->allocatedmarker !== false) &&
5410 ($current->allocatedmarker != $modified->allocatedmarker);
5411 $workflowstatechanged = $this->get_instance()->markingworkflow &&
5412 ($modified->workflowstate !== false) &&
5413 ($current->workflowstate != $modified->workflowstate);
5414 if ($gradechanged || $markingallocationchanged || $workflowstatechanged) {
5415 // Grade changed.
5416 if ($this->grading_disabled($modified->userid)) {
5417 continue;
5419 if ((int)$current->lastmodified > (int)$modified->lastmodified) {
5420 // Error - record has been modified since viewing the page.
5421 return get_string('errorrecordmodified', 'assign');
5422 } else {
5423 $modifiedusers[$modified->userid] = $modified;
5428 $currentgrades->close();
5430 $adminconfig = $this->get_admin_config();
5431 $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
5433 // Ok - ready to process the updates.
5434 foreach ($modifiedusers as $userid => $modified) {
5435 $grade = $this->get_user_grade($userid, true);
5436 $flags = $this->get_user_flags($userid, true);
5437 $grade->grade= grade_floatval(unformat_float($modified->grade));
5438 $grade->grader= $USER->id;
5439 $gradecolpresent = optional_param('quickgrade_' . $userid, false, PARAM_INT) !== false;
5441 // Save plugins data.
5442 foreach ($this->feedbackplugins as $plugin) {
5443 if ($plugin->is_visible() && $plugin->is_enabled() && $plugin->supports_quickgrading()) {
5444 $plugin->save_quickgrading_changes($userid, $grade);
5445 if (('assignfeedback_' . $plugin->get_type()) == $gradebookplugin) {
5446 // This is the feedback plugin chose to push comments to the gradebook.
5447 $grade->feedbacktext = $plugin->text_for_gradebook($grade);
5448 $grade->feedbackformat = $plugin->format_for_gradebook($grade);
5453 // These will be set to false if they are not present in the quickgrading
5454 // form (e.g. column hidden).
5455 $workflowstatemodified = ($modified->workflowstate !== false) &&
5456 ($flags->workflowstate != $modified->workflowstate);
5458 $allocatedmarkermodified = ($modified->allocatedmarker !== false) &&
5459 ($flags->allocatedmarker != $modified->allocatedmarker);
5461 if ($workflowstatemodified) {
5462 $flags->workflowstate = $modified->workflowstate;
5464 if ($allocatedmarkermodified) {
5465 $flags->allocatedmarker = $modified->allocatedmarker;
5467 if ($workflowstatemodified || $allocatedmarkermodified) {
5468 if ($this->update_user_flags($flags) && $workflowstatemodified) {
5469 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
5470 \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $flags->workflowstate)->trigger();
5473 $this->update_grade($grade);
5475 // Allow teachers to skip sending notifications.
5476 if (optional_param('sendstudentnotifications', true, PARAM_BOOL)) {
5477 $this->notify_grade_modified($grade, true);
5480 // Save outcomes.
5481 if ($CFG->enableoutcomes) {
5482 $data = array();
5483 foreach ($modified->gradinginfo->outcomes as $outcomeid => $outcome) {
5484 $oldoutcome = $outcome->grades[$modified->userid]->grade;
5485 $paramname = 'outcome_' . $outcomeid . '_' . $modified->userid;
5486 // This will be false if the input was not in the quickgrading
5487 // form (e.g. column hidden).
5488 $newoutcome = optional_param($paramname, false, PARAM_INT);
5489 if ($newoutcome !== false && ($oldoutcome != $newoutcome)) {
5490 $data[$outcomeid] = $newoutcome;
5493 if (count($data) > 0) {
5494 grade_update_outcomes('mod/assign',
5495 $this->course->id,
5496 'mod',
5497 'assign',
5498 $this->get_instance()->id,
5499 $userid,
5500 $data);
5505 return get_string('quickgradingchangessaved', 'assign');
5509 * Reveal student identities to markers (and the gradebook).
5511 * @return void
5513 public function reveal_identities() {
5514 global $DB;
5516 require_capability('mod/assign:revealidentities', $this->context);
5518 if ($this->get_instance()->revealidentities || empty($this->get_instance()->blindmarking)) {
5519 return false;
5522 // Update the assignment record.
5523 $update = new stdClass();
5524 $update->id = $this->get_instance()->id;
5525 $update->revealidentities = 1;
5526 $DB->update_record('assign', $update);
5528 // Refresh the instance data.
5529 $this->instance = null;
5531 // Release the grades to the gradebook.
5532 // First create the column in the gradebook.
5533 $this->update_gradebook(false, $this->get_course_module()->id);
5535 // Now release all grades.
5537 $adminconfig = $this->get_admin_config();
5538 $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
5539 $gradebookplugin = str_replace('assignfeedback_', '', $gradebookplugin);
5540 $grades = $DB->get_records('assign_grades', array('assignment'=>$this->get_instance()->id));
5542 $plugin = $this->get_feedback_plugin_by_type($gradebookplugin);
5544 foreach ($grades as $grade) {
5545 // Fetch any comments for this student.
5546 if ($plugin && $plugin->is_enabled() && $plugin->is_visible()) {
5547 $grade->feedbacktext = $plugin->text_for_gradebook($grade);
5548 $grade->feedbackformat = $plugin->format_for_gradebook($grade);
5550 $this->gradebook_item_update(null, $grade);
5553 \mod_assign\event\identities_revealed::create_from_assign($this)->trigger();
5557 * Reveal student identities to markers (and the gradebook).
5559 * @return void
5561 protected function process_reveal_identities() {
5563 if (!confirm_sesskey()) {
5564 return false;
5567 return $this->reveal_identities();
5572 * Save grading options.
5574 * @return void
5576 protected function process_save_grading_options() {
5577 global $USER, $CFG;
5579 // Include grading options form.
5580 require_once($CFG->dirroot . '/mod/assign/gradingoptionsform.php');
5582 // Need submit permission to submit an assignment.
5583 require_capability('mod/assign:grade', $this->context);
5584 require_sesskey();
5586 // Is advanced grading enabled?
5587 $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
5588 $controller = $gradingmanager->get_active_controller();
5589 $showquickgrading = empty($controller);
5590 if (!is_null($this->context)) {
5591 $showonlyactiveenrolopt = has_capability('moodle/course:viewsuspendedusers', $this->context);
5592 } else {
5593 $showonlyactiveenrolopt = false;
5596 $markingallocation = $this->get_instance()->markingworkflow &&
5597 $this->get_instance()->markingallocation &&
5598 has_capability('mod/assign:manageallocations', $this->context);
5599 // Get markers to use in drop lists.
5600 $markingallocationoptions = array();
5601 if ($markingallocation) {
5602 $markingallocationoptions[''] = get_string('filternone', 'assign');
5603 $markingallocationoptions[ASSIGN_MARKER_FILTER_NO_MARKER] = get_string('markerfilternomarker', 'assign');
5604 list($sort, $params) = users_order_by_sql();
5605 $markers = get_users_by_capability($this->context, 'mod/assign:grade', '', $sort);
5606 foreach ($markers as $marker) {
5607 $markingallocationoptions[$marker->id] = fullname($marker);
5611 // Get marking states to show in form.
5612 $markingworkflowoptions = array();
5613 if ($this->get_instance()->markingworkflow) {
5614 $notmarked = get_string('markingworkflowstatenotmarked', 'assign');
5615 $markingworkflowoptions[''] = get_string('filternone', 'assign');
5616 $markingworkflowoptions[ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED] = $notmarked;
5617 $markingworkflowoptions = array_merge($markingworkflowoptions, $this->get_marking_workflow_states_for_current_user());
5620 $gradingoptionsparams = array('cm'=>$this->get_course_module()->id,
5621 'contextid'=>$this->context->id,
5622 'userid'=>$USER->id,
5623 'submissionsenabled'=>$this->is_any_submission_plugin_enabled(),
5624 'showquickgrading'=>$showquickgrading,
5625 'quickgrading'=>false,
5626 'markingworkflowopt' => $markingworkflowoptions,
5627 'markingallocationopt' => $markingallocationoptions,
5628 'showonlyactiveenrolopt'=>$showonlyactiveenrolopt,
5629 'showonlyactiveenrol'=>$this->show_only_active_users());
5631 $mform = new mod_assign_grading_options_form(null, $gradingoptionsparams);
5632 if ($formdata = $mform->get_data()) {
5633 set_user_preference('assign_perpage', $formdata->perpage);
5634 if (isset($formdata->filter)) {
5635 set_user_preference('assign_filter', $formdata->filter);
5637 if (isset($formdata->markerfilter)) {
5638 set_user_preference('assign_markerfilter', $formdata->markerfilter);
5640 if (isset($formdata->workflowfilter)) {
5641 set_user_preference('assign_workflowfilter', $formdata->workflowfilter);
5643 if ($showquickgrading) {
5644 set_user_preference('assign_quickgrading', isset($formdata->quickgrading));
5646 if (!empty($showonlyactiveenrolopt)) {
5647 $showonlyactiveenrol = isset($formdata->showonlyactiveenrol);
5648 set_user_preference('grade_report_showonlyactiveenrol', $showonlyactiveenrol);
5649 $this->showonlyactiveenrol = $showonlyactiveenrol;
5655 * Take a grade object and print a short summary for the log file.
5656 * The size limit for the log file is 255 characters, so be careful not
5657 * to include too much information.
5659 * @deprecated since 2.7
5661 * @param stdClass $grade
5662 * @return string
5664 public function format_grade_for_log(stdClass $grade) {
5665 global $DB;
5667 $user = $DB->get_record('user', array('id' => $grade->userid), '*', MUST_EXIST);
5669 $info = get_string('gradestudent', 'assign', array('id'=>$user->id, 'fullname'=>fullname($user)));
5670 if ($grade->grade != '') {
5671 $info .= get_string('grade') . ': ' . $this->display_grade($grade->grade, false) . '. ';
5672 } else {
5673 $info .= get_string('nograde', 'assign');
5675 return $info;
5679 * Take a submission object and print a short summary for the log file.
5680 * The size limit for the log file is 255 characters, so be careful not
5681 * to include too much information.
5683 * @deprecated since 2.7
5685 * @param stdClass $submission
5686 * @return string
5688 public function format_submission_for_log(stdClass $submission) {
5689 global $DB;
5691 $info = '';
5692 if ($submission->userid) {
5693 $user = $DB->get_record('user', array('id' => $submission->userid), '*', MUST_EXIST);
5694 $name = fullname($user);
5695 } else {
5696 $group = $this->get_submission_group($submission->userid);
5697 if ($group) {
5698 $name = $group->name;
5699 } else {
5700 $name = get_string('defaultteam', 'assign');
5703 $status = get_string('submissionstatus_' . $submission->status, 'assign');
5704 $params = array('id'=>$submission->userid, 'fullname'=>$name, 'status'=>$status);
5705 $info .= get_string('submissionlog', 'assign', $params) . ' <br>';
5707 foreach ($this->submissionplugins as $plugin) {
5708 if ($plugin->is_enabled() && $plugin->is_visible()) {
5709 $info .= '<br>' . $plugin->format_for_log($submission);
5713 return $info;
5717 * Require a valid sess key and then call copy_previous_attempt.
5719 * @param array $notices Any error messages that should be shown
5720 * to the user at the top of the edit submission form.
5721 * @return bool
5723 protected function process_copy_previous_attempt(&$notices) {
5724 require_sesskey();
5726 return $this->copy_previous_attempt($notices);
5730 * Copy the current assignment submission from the last submitted attempt.
5732 * @param array $notices Any error messages that should be shown
5733 * to the user at the top of the edit submission form.
5734 * @return bool
5736 public function copy_previous_attempt(&$notices) {
5737 global $USER, $CFG;
5739 require_capability('mod/assign:submit', $this->context);
5741 $instance = $this->get_instance();
5742 if ($instance->teamsubmission) {
5743 $submission = $this->get_group_submission($USER->id, 0, true);
5744 } else {
5745 $submission = $this->get_user_submission($USER->id, true);
5747 if (!$submission || $submission->status != ASSIGN_SUBMISSION_STATUS_REOPENED) {
5748 $notices[] = get_string('submissionnotcopiedinvalidstatus', 'assign');
5749 return false;
5751 $flags = $this->get_user_flags($USER->id, false);
5753 // Get the flags to check if it is locked.
5754 if ($flags && $flags->locked) {
5755 $notices[] = get_string('submissionslocked', 'assign');
5756 return false;
5758 if ($instance->submissiondrafts) {
5759 $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
5760 } else {
5761 $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
5763 $this->update_submission($submission, $USER->id, true, $instance->teamsubmission);
5765 // Find the previous submission.
5766 if ($instance->teamsubmission) {
5767 $previoussubmission = $this->get_group_submission($USER->id, 0, true, $submission->attemptnumber - 1);
5768 } else {
5769 $previoussubmission = $this->get_user_submission($USER->id, true, $submission->attemptnumber - 1);
5772 if (!$previoussubmission) {
5773 // There was no previous submission so there is nothing else to do.
5774 return true;
5777 $pluginerror = false;
5778 foreach ($this->get_submission_plugins() as $plugin) {
5779 if ($plugin->is_visible() && $plugin->is_enabled()) {
5780 if (!$plugin->copy_submission($previoussubmission, $submission)) {
5781 $notices[] = $plugin->get_error();
5782 $pluginerror = true;
5786 if ($pluginerror) {
5787 return false;
5790 \mod_assign\event\submission_duplicated::create_from_submission($this, $submission)->trigger();
5792 $complete = COMPLETION_INCOMPLETE;
5793 if ($submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
5794 $complete = COMPLETION_COMPLETE;
5796 $completion = new completion_info($this->get_course());
5797 if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
5798 $completion->update_state($this->get_course_module(), $complete, $USER->id);
5801 if (!$instance->submissiondrafts) {
5802 // There is a case for not notifying the student about the submission copy,
5803 // but it provides a record of the event and if they then cancel editing it
5804 // is clear that the submission was copied.
5805 $this->notify_student_submission_copied($submission);
5806 $this->notify_graders($submission);
5808 // The same logic applies here - we could not notify teachers,
5809 // but then they would wonder why there are submitted assignments
5810 // and they haven't been notified.
5811 \mod_assign\event\assessable_submitted::create_from_submission($this, $submission, true)->trigger();
5813 return true;
5817 * Determine if the current submission is empty or not.
5819 * @param submission $submission the students submission record to check.
5820 * @return bool
5822 public function submission_empty($submission) {
5823 $allempty = true;
5825 foreach ($this->submissionplugins as $plugin) {
5826 if ($plugin->is_enabled() && $plugin->is_visible()) {
5827 if (!$allempty || !$plugin->is_empty($submission)) {
5828 $allempty = false;
5832 return $allempty;
5836 * Save assignment submission for the current user.
5838 * @param stdClass $data
5839 * @param array $notices Any error messages that should be shown
5840 * to the user.
5841 * @return bool
5843 public function save_submission(stdClass $data, & $notices) {
5844 global $CFG, $USER, $DB;
5846 $userid = $USER->id;
5847 if (!empty($data->userid)) {
5848 $userid = $data->userid;
5851 $user = clone($USER);
5852 if ($userid == $USER->id) {
5853 require_capability('mod/assign:submit', $this->context);
5854 } else {
5855 $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
5856 if (!$this->can_edit_submission($userid, $USER->id)) {
5857 print_error('nopermission');
5860 $instance = $this->get_instance();
5862 if ($instance->teamsubmission) {
5863 $submission = $this->get_group_submission($userid, 0, true);
5864 } else {
5865 $submission = $this->get_user_submission($userid, true);
5867 if ($instance->submissiondrafts) {
5868 $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
5869 } else {
5870 $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
5873 $flags = $this->get_user_flags($userid, false);
5875 // Get the flags to check if it is locked.
5876 if ($flags && $flags->locked) {
5877 print_error('submissionslocked', 'assign');
5878 return true;
5881 $pluginerror = false;
5882 foreach ($this->submissionplugins as $plugin) {
5883 if ($plugin->is_enabled() && $plugin->is_visible()) {
5884 if (!$plugin->save($submission, $data)) {
5885 $notices[] = $plugin->get_error();
5886 $pluginerror = true;
5891 $allempty = $this->submission_empty($submission);
5892 if ($pluginerror || $allempty) {
5893 if ($allempty) {
5894 $notices[] = get_string('submissionempty', 'mod_assign');
5896 return false;
5899 $this->update_submission($submission, $userid, true, $instance->teamsubmission);
5901 // Logging.
5902 if (isset($data->submissionstatement) && ($userid == $USER->id)) {
5903 \mod_assign\event\statement_accepted::create_from_submission($this, $submission)->trigger();
5906 $complete = COMPLETION_INCOMPLETE;
5907 if ($submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
5908 $complete = COMPLETION_COMPLETE;
5910 $completion = new completion_info($this->get_course());
5911 if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
5912 $completion->update_state($this->get_course_module(), $complete, $userid);
5915 if (!$instance->submissiondrafts) {
5916 $this->notify_student_submission_receipt($submission);
5917 $this->notify_graders($submission);
5918 \mod_assign\event\assessable_submitted::create_from_submission($this, $submission, true)->trigger();
5920 return true;
5924 * Save assignment submission.
5926 * @param moodleform $mform
5927 * @param array $notices Any error messages that should be shown
5928 * to the user at the top of the edit submission form.
5929 * @return bool
5931 protected function process_save_submission(&$mform, &$notices) {
5932 global $CFG, $USER;
5934 // Include submission form.
5935 require_once($CFG->dirroot . '/mod/assign/submission_form.php');
5937 $userid = optional_param('userid', $USER->id, PARAM_INT);
5938 // Need submit permission to submit an assignment.
5939 require_sesskey();
5940 if (!$this->submissions_open($userid)) {
5941 $notices[] = get_string('duedatereached', 'assign');
5942 return false;
5944 $instance = $this->get_instance();
5946 $data = new stdClass();
5947 $data->userid = $userid;
5948 $mform = new mod_assign_submission_form(null, array($this, $data));
5949 if ($mform->is_cancelled()) {
5950 return true;
5952 if ($data = $mform->get_data()) {
5953 return $this->save_submission($data, $notices);
5955 return false;
5960 * Determine if this users grade can be edited.
5962 * @param int $userid - The student userid
5963 * @param bool $checkworkflow - whether to include a check for the workflow state.
5964 * @return bool $gradingdisabled
5966 public function grading_disabled($userid, $checkworkflow=true) {
5967 global $CFG;
5968 if ($checkworkflow && $this->get_instance()->markingworkflow) {
5969 $grade = $this->get_user_grade($userid, false);
5970 $validstates = $this->get_marking_workflow_states_for_current_user();
5971 if (!empty($grade) && !empty($grade->workflowstate) && !array_key_exists($grade->workflowstate, $validstates)) {
5972 return true;
5975 $gradinginfo = grade_get_grades($this->get_course()->id,
5976 'mod',
5977 'assign',
5978 $this->get_instance()->id,
5979 array($userid));
5980 if (!$gradinginfo) {
5981 return false;
5984 if (!isset($gradinginfo->items[0]->grades[$userid])) {
5985 return false;
5987 $gradingdisabled = $gradinginfo->items[0]->grades[$userid]->locked ||
5988 $gradinginfo->items[0]->grades[$userid]->overridden;
5989 return $gradingdisabled;
5994 * Get an instance of a grading form if advanced grading is enabled.
5995 * This is specific to the assignment, marker and student.
5997 * @param int $userid - The student userid
5998 * @param stdClass|false $grade - The grade record
5999 * @param bool $gradingdisabled
6000 * @return mixed gradingform_instance|null $gradinginstance
6002 protected function get_grading_instance($userid, $grade, $gradingdisabled) {
6003 global $CFG, $USER;
6005 $grademenu = make_grades_menu($this->get_instance()->grade);
6006 $allowgradedecimals = $this->get_instance()->grade > 0;
6008 $advancedgradingwarning = false;
6009 $gradingmanager = get_grading_manager($this->context, 'mod_assign', 'submissions');
6010 $gradinginstance = null;
6011 if ($gradingmethod = $gradingmanager->get_active_method()) {
6012 $controller = $gradingmanager->get_controller($gradingmethod);
6013 if ($controller->is_form_available()) {
6014 $itemid = null;
6015 if ($grade) {
6016 $itemid = $grade->id;
6018 if ($gradingdisabled && $itemid) {
6019 $gradinginstance = $controller->get_current_instance($USER->id, $itemid);
6020 } else if (!$gradingdisabled) {
6021 $instanceid = optional_param('advancedgradinginstanceid', 0, PARAM_INT);
6022 $gradinginstance = $controller->get_or_create_instance($instanceid,
6023 $USER->id,
6024 $itemid);
6026 } else {
6027 $advancedgradingwarning = $controller->form_unavailable_notification();
6030 if ($gradinginstance) {
6031 $gradinginstance->get_controller()->set_grade_range($grademenu, $allowgradedecimals);
6033 return $gradinginstance;
6037 * Add elements to grade form.
6039 * @param MoodleQuickForm $mform
6040 * @param stdClass $data
6041 * @param array $params
6042 * @return void
6044 public function add_grade_form_elements(MoodleQuickForm $mform, stdClass $data, $params) {
6045 global $USER, $CFG;
6046 $settings = $this->get_instance();
6048 $rownum = $params['rownum'];
6049 $last = $params['last'];
6050 $useridlistid = $params['useridlistid'];
6051 $userid = $params['userid'];
6052 $attemptnumber = $params['attemptnumber'];
6053 if (!$userid) {
6054 $cache = cache::make_from_params(cache_store::MODE_SESSION, 'mod_assign', 'useridlist');
6055 if (!$useridlist = $cache->get($this->get_course_module()->id . '_' . $useridlistid)) {
6056 $useridlist = $this->get_grading_userid_list();
6057 $cache->set($this->get_course_module()->id . '_' . $useridlistid, $useridlist);
6059 } else {
6060 $useridlist = array($userid);
6061 $rownum = 0;
6062 $useridlistid = '';
6065 $userid = $useridlist[$rownum];
6066 $grade = $this->get_user_grade($userid, false, $attemptnumber);
6068 $submission = null;
6069 if ($this->get_instance()->teamsubmission) {
6070 $submission = $this->get_group_submission($userid, 0, false, $attemptnumber);
6071 } else {
6072 $submission = $this->get_user_submission($userid, false, $attemptnumber);
6075 // Add advanced grading.
6076 $gradingdisabled = $this->grading_disabled($userid);
6077 $gradinginstance = $this->get_grading_instance($userid, $grade, $gradingdisabled);
6079 $mform->addElement('header', 'gradeheader', get_string('grade'));
6080 if ($gradinginstance) {
6081 $gradingelement = $mform->addElement('grading',
6082 'advancedgrading',
6083 get_string('grade').':',
6084 array('gradinginstance' => $gradinginstance));
6085 if ($gradingdisabled) {
6086 $gradingelement->freeze();
6087 } else {
6088 $mform->addElement('hidden', 'advancedgradinginstanceid', $gradinginstance->get_id());
6089 $mform->setType('advancedgradinginstanceid', PARAM_INT);
6091 } else {
6092 // Use simple direct grading.
6093 if ($this->get_instance()->grade > 0) {
6094 $name = get_string('gradeoutof', 'assign', $this->get_instance()->grade);
6095 if (!$gradingdisabled) {
6096 $gradingelement = $mform->addElement('text', 'grade', $name);
6097 $mform->addHelpButton('grade', 'gradeoutofhelp', 'assign');
6098 $mform->setType('grade', PARAM_RAW);
6099 } else {
6100 $mform->addElement('hidden', 'grade', $name);
6101 $mform->hardFreeze('grade');
6102 $mform->setType('grade', PARAM_RAW);
6103 $strgradelocked = get_string('gradelocked', 'assign');
6104 $mform->addElement('static', 'gradedisabled', $name, $strgradelocked);
6105 $mform->addHelpButton('gradedisabled', 'gradeoutofhelp', 'assign');
6107 } else {
6108 $grademenu = array(-1 => get_string("nograde")) + make_grades_menu($this->get_instance()->grade);
6109 if (count($grademenu) > 1) {
6110 $gradingelement = $mform->addElement('select', 'grade', get_string('grade') . ':', $grademenu);
6112 // The grade is already formatted with format_float so it needs to be converted back to an integer.
6113 if (!empty($data->grade)) {
6114 $data->grade = (int)unformat_float($data->grade);
6116 $mform->setType('grade', PARAM_INT);
6117 if ($gradingdisabled) {
6118 $gradingelement->freeze();
6124 $gradinginfo = grade_get_grades($this->get_course()->id,
6125 'mod',
6126 'assign',
6127 $this->get_instance()->id,
6128 $userid);
6129 if (!empty($CFG->enableoutcomes)) {
6130 foreach ($gradinginfo->outcomes as $index => $outcome) {
6131 $options = make_grades_menu(-$outcome->scaleid);
6132 if ($outcome->grades[$userid]->locked) {
6133 $options[0] = get_string('nooutcome', 'grades');
6134 $mform->addElement('static',
6135 'outcome_' . $index . '[' . $userid . ']',
6136 $outcome->name . ':',
6137 $options[$outcome->grades[$userid]->grade]);
6138 } else {
6139 $options[''] = get_string('nooutcome', 'grades');
6140 $attributes = array('id' => 'menuoutcome_' . $index );
6141 $mform->addElement('select',
6142 'outcome_' . $index . '[' . $userid . ']',
6143 $outcome->name.':',
6144 $options,
6145 $attributes);
6146 $mform->setType('outcome_' . $index . '[' . $userid . ']', PARAM_INT);
6147 $mform->setDefault('outcome_' . $index . '[' . $userid . ']',
6148 $outcome->grades[$userid]->grade);
6153 $capabilitylist = array('gradereport/grader:view', 'moodle/grade:viewall');
6154 if (has_all_capabilities($capabilitylist, $this->get_course_context())) {
6155 $urlparams = array('id'=>$this->get_course()->id);
6156 $url = new moodle_url('/grade/report/grader/index.php', $urlparams);
6157 $usergrade = '-';
6158 if (isset($gradinginfo->items[0]->grades[$userid]->str_grade)) {
6159 $usergrade = $gradinginfo->items[0]->grades[$userid]->str_grade;
6161 $gradestring = $this->get_renderer()->action_link($url, $usergrade);
6162 } else {
6163 $usergrade = '-';
6164 if (isset($gradinginfo->items[0]->grades[$userid]) &&
6165 !$gradinginfo->items[0]->grades[$userid]->hidden) {
6166 $usergrade = $gradinginfo->items[0]->grades[$userid]->str_grade;
6168 $gradestring = $usergrade;
6171 if ($this->get_instance()->markingworkflow) {
6172 $states = $this->get_marking_workflow_states_for_current_user();
6173 $options = array('' => get_string('markingworkflowstatenotmarked', 'assign')) + $states;
6174 $mform->addElement('select', 'workflowstate', get_string('markingworkflowstate', 'assign'), $options);
6175 $mform->addHelpButton('workflowstate', 'markingworkflowstate', 'assign');
6178 if ($this->get_instance()->markingworkflow &&
6179 $this->get_instance()->markingallocation &&
6180 has_capability('mod/assign:manageallocations', $this->context)) {
6182 list($sort, $params) = users_order_by_sql();
6183 $markers = get_users_by_capability($this->context, 'mod/assign:grade', '', $sort);
6184 $markerlist = array('' => get_string('choosemarker', 'assign'));
6185 foreach ($markers as $marker) {
6186 $markerlist[$marker->id] = fullname($marker);
6188 $mform->addElement('select', 'allocatedmarker', get_string('allocatedmarker', 'assign'), $markerlist);
6189 $mform->addHelpButton('allocatedmarker', 'allocatedmarker', 'assign');
6190 $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW);
6191 $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW);
6192 $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE);
6193 $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_RELEASED);
6196 $mform->addElement('static', 'currentgrade', get_string('currentgrade', 'assign'), $gradestring);
6198 if (count($useridlist) > 1) {
6199 $strparams = array('current'=>$rownum+1, 'total'=>count($useridlist));
6200 $name = get_string('outof', 'assign', $strparams);
6201 $mform->addElement('static', 'gradingstudent', get_string('gradingstudent', 'assign'), $name);
6204 // Let feedback plugins add elements to the grading form.
6205 $this->add_plugin_grade_elements($grade, $mform, $data, $userid);
6207 // Hidden params.
6208 $mform->addElement('hidden', 'id', $this->get_course_module()->id);
6209 $mform->setType('id', PARAM_INT);
6210 $mform->addElement('hidden', 'rownum', $rownum);
6211 $mform->setType('rownum', PARAM_INT);
6212 $mform->setConstant('rownum', $rownum);
6213 $mform->addElement('hidden', 'useridlistid', $useridlistid);
6214 $mform->setType('useridlistid', PARAM_INT);
6215 $mform->addElement('hidden', 'attemptnumber', $attemptnumber);
6216 $mform->setType('attemptnumber', PARAM_INT);
6217 $mform->addElement('hidden', 'ajax', optional_param('ajax', 0, PARAM_INT));
6218 $mform->setType('ajax', PARAM_INT);
6220 if ($this->get_instance()->teamsubmission) {
6221 $mform->addElement('header', 'groupsubmissionsettings', get_string('groupsubmissionsettings', 'assign'));
6222 $mform->addElement('selectyesno', 'applytoall', get_string('applytoteam', 'assign'));
6223 $mform->setDefault('applytoall', 1);
6226 // Do not show if we are editing a previous attempt.
6227 if ($attemptnumber == -1 && $this->get_instance()->attemptreopenmethod != ASSIGN_ATTEMPT_REOPEN_METHOD_NONE) {
6228 $mform->addElement('header', 'attemptsettings', get_string('attemptsettings', 'assign'));
6229 $attemptreopenmethod = get_string('attemptreopenmethod_' . $this->get_instance()->attemptreopenmethod, 'assign');
6230 $mform->addElement('static', 'attemptreopenmethod', get_string('attemptreopenmethod', 'assign'), $attemptreopenmethod);
6232 $attemptnumber = 0;
6233 if ($submission) {
6234 $attemptnumber = $submission->attemptnumber;
6236 $maxattempts = $this->get_instance()->maxattempts;
6237 if ($maxattempts == ASSIGN_UNLIMITED_ATTEMPTS) {
6238 $maxattempts = get_string('unlimitedattempts', 'assign');
6240 $mform->addelement('static', 'maxattemptslabel', get_string('maxattempts', 'assign'), $maxattempts);
6241 $mform->addelement('static', 'attemptnumberlabel', get_string('attemptnumber', 'assign'), $attemptnumber + 1);
6243 $ismanual = $this->get_instance()->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL;
6244 $issubmission = !empty($submission);
6245 $isunlimited = $this->get_instance()->maxattempts == ASSIGN_UNLIMITED_ATTEMPTS;
6246 $islessthanmaxattempts = $issubmission && ($submission->attemptnumber < ($this->get_instance()->maxattempts-1));
6248 if ($ismanual && (!$issubmission || $isunlimited || $islessthanmaxattempts)) {
6249 $mform->addElement('selectyesno', 'addattempt', get_string('addattempt', 'assign'));
6250 $mform->setDefault('addattempt', 0);
6253 $mform->addElement('selectyesno', 'sendstudentnotifications', get_string('sendstudentnotifications', 'assign'));
6254 // Get assignment visibility information for student.
6255 $modinfo = get_fast_modinfo($settings->course, $userid);
6256 $cm = $modinfo->get_cm($this->get_course_module()->id);
6258 // Don't allow notification to be sent if the student can't access the assignment,
6259 // or until in "Released" state if using marking workflow.
6260 if (!$cm->uservisible) {
6261 $mform->setDefault('sendstudentnotifications', 0);
6262 $mform->freeze('sendstudentnotifications');
6263 } else if ($this->get_instance()->markingworkflow) {
6264 $mform->setDefault('sendstudentnotifications', 0);
6265 $mform->disabledIf('sendstudentnotifications', 'workflowstate', 'neq', ASSIGN_MARKING_WORKFLOW_STATE_RELEASED);
6266 } else {
6267 $mform->setDefault('sendstudentnotifications', $this->get_instance()->sendstudentnotifications);
6270 $mform->addElement('hidden', 'action', 'submitgrade');
6271 $mform->setType('action', PARAM_ALPHA);
6273 $buttonarray=array();
6274 $name = get_string('savechanges', 'assign');
6275 $buttonarray[] = $mform->createElement('submit', 'savegrade', $name);
6276 if (!$last) {
6277 $name = get_string('savenext', 'assign');
6278 $buttonarray[] = $mform->createElement('submit', 'saveandshownext', $name);
6280 $buttonarray[] = $mform->createElement('cancel', 'cancelbutton', get_string('cancel'));
6281 $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false);
6282 $mform->closeHeaderBefore('buttonar');
6283 $buttonarray=array();
6285 if ($rownum > 0) {
6286 $name = get_string('previous', 'assign');
6287 $buttonarray[] = $mform->createElement('submit', 'nosaveandprevious', $name);
6290 if (!$last) {
6291 $name = get_string('nosavebutnext', 'assign');
6292 $buttonarray[] = $mform->createElement('submit', 'nosaveandnext', $name);
6294 if (!empty($buttonarray)) {
6295 $mform->addGroup($buttonarray, 'navar', '', array(' '), false);
6297 // The grading form does not work well with shortforms.
6298 $mform->setDisableShortforms();
6302 * Add elements in submission plugin form.
6304 * @param mixed $submission stdClass|null
6305 * @param MoodleQuickForm $mform
6306 * @param stdClass $data
6307 * @param int $userid The current userid (same as $USER->id)
6308 * @return void
6310 protected function add_plugin_submission_elements($submission,
6311 MoodleQuickForm $mform,
6312 stdClass $data,
6313 $userid) {
6314 foreach ($this->submissionplugins as $plugin) {
6315 if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->allow_submissions()) {
6316 $plugin->get_form_elements_for_user($submission, $mform, $data, $userid);
6322 * Check if feedback plugins installed are enabled.
6324 * @return bool
6326 public function is_any_feedback_plugin_enabled() {
6327 if (!isset($this->cache['any_feedback_plugin_enabled'])) {
6328 $this->cache['any_feedback_plugin_enabled'] = false;
6329 foreach ($this->feedbackplugins as $plugin) {
6330 if ($plugin->is_enabled() && $plugin->is_visible()) {
6331 $this->cache['any_feedback_plugin_enabled'] = true;
6332 break;
6337 return $this->cache['any_feedback_plugin_enabled'];
6342 * Check if submission plugins installed are enabled.
6344 * @return bool
6346 public function is_any_submission_plugin_enabled() {
6347 if (!isset($this->cache['any_submission_plugin_enabled'])) {
6348 $this->cache['any_submission_plugin_enabled'] = false;
6349 foreach ($this->submissionplugins as $plugin) {
6350 if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->allow_submissions()) {
6351 $this->cache['any_submission_plugin_enabled'] = true;
6352 break;
6357 return $this->cache['any_submission_plugin_enabled'];
6362 * Add elements to submission form.
6363 * @param MoodleQuickForm $mform
6364 * @param stdClass $data
6365 * @return void
6367 public function add_submission_form_elements(MoodleQuickForm $mform, stdClass $data) {
6368 global $USER;
6370 $userid = $data->userid;
6371 // Team submissions.
6372 if ($this->get_instance()->teamsubmission) {
6373 $submission = $this->get_group_submission($userid, 0, false);
6374 } else {
6375 $submission = $this->get_user_submission($userid, false);
6378 // Submission statement.
6379 $adminconfig = $this->get_admin_config();
6381 $requiresubmissionstatement = $this->get_instance()->requiresubmissionstatement &&
6382 !empty($adminconfig->submissionstatement);
6384 $draftsenabled = $this->get_instance()->submissiondrafts;
6386 // Only show submission statement if we are editing our own submission.
6387 if ($requiresubmissionstatement && !$draftsenabled && $userid == $USER->id) {
6389 $submissionstatement = '';
6390 if (!empty($adminconfig->submissionstatement)) {
6391 // Format the submission statement before its sent. We turn off para because this is going within
6392 // a form element.
6393 $options = array(
6394 'context' => $this->get_context(),
6395 'para' => false
6397 $submissionstatement = format_text($adminconfig->submissionstatement, FORMAT_MOODLE, $options);
6399 $mform->addElement('checkbox', 'submissionstatement', '', $submissionstatement);
6400 $mform->addRule('submissionstatement', get_string('required'), 'required', null, 'client');
6403 $this->add_plugin_submission_elements($submission, $mform, $data, $userid);
6405 // Hidden params.
6406 $mform->addElement('hidden', 'id', $this->get_course_module()->id);
6407 $mform->setType('id', PARAM_INT);
6409 $mform->addElement('hidden', 'userid', $userid);
6410 $mform->setType('userid', PARAM_INT);
6412 $mform->addElement('hidden', 'action', 'savesubmission');
6413 $mform->setType('action', PARAM_TEXT);
6417 * Revert to draft.
6419 * @param int $userid
6420 * @return boolean
6422 public function revert_to_draft($userid) {
6423 global $DB, $USER;
6425 // Need grade permission.
6426 require_capability('mod/assign:grade', $this->context);
6428 if ($this->get_instance()->teamsubmission) {
6429 $submission = $this->get_group_submission($userid, 0, false);
6430 } else {
6431 $submission = $this->get_user_submission($userid, false);
6434 if (!$submission) {
6435 return false;
6437 $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
6438 $this->update_submission($submission, $userid, true, $this->get_instance()->teamsubmission);
6440 // Give each submission plugin a chance to process the reverting to draft.
6441 $plugins = $this->get_submission_plugins();
6442 foreach ($plugins as $plugin) {
6443 if ($plugin->is_enabled() && $plugin->is_visible()) {
6444 $plugin->revert_to_draft($submission);
6447 // Update the modified time on the grade (grader modified).
6448 $grade = $this->get_user_grade($userid, true);
6449 $grade->grader = $USER->id;
6450 $this->update_grade($grade);
6452 $completion = new completion_info($this->get_course());
6453 if ($completion->is_enabled($this->get_course_module()) &&
6454 $this->get_instance()->completionsubmit) {
6455 $completion->update_state($this->get_course_module(), COMPLETION_INCOMPLETE, $userid);
6457 \mod_assign\event\submission_status_updated::create_from_submission($this, $submission)->trigger();
6458 return true;
6462 * Revert to draft.
6463 * Uses url parameter userid if userid not supplied as a parameter.
6465 * @param int $userid
6466 * @return boolean
6468 protected function process_revert_to_draft($userid = 0) {
6469 require_sesskey();
6471 if (!$userid) {
6472 $userid = required_param('userid', PARAM_INT);
6475 return $this->revert_to_draft($userid);
6479 * Prevent student updates to this submission
6481 * @param int $userid
6482 * @return bool
6484 public function lock_submission($userid) {
6485 global $USER, $DB;
6486 // Need grade permission.
6487 require_capability('mod/assign:grade', $this->context);
6489 // Give each submission plugin a chance to process the locking.
6490 $plugins = $this->get_submission_plugins();
6491 $submission = $this->get_user_submission($userid, false);
6493 $flags = $this->get_user_flags($userid, true);
6494 $flags->locked = 1;
6495 $this->update_user_flags($flags);
6497 foreach ($plugins as $plugin) {
6498 if ($plugin->is_enabled() && $plugin->is_visible()) {
6499 $plugin->lock($submission, $flags);
6503 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
6504 \mod_assign\event\submission_locked::create_from_user($this, $user)->trigger();
6505 return true;
6510 * Set the workflow state for multiple users
6512 * @return void
6514 protected function process_set_batch_marking_workflow_state() {
6515 global $CFG, $DB;
6517 // Include batch marking workflow form.
6518 require_once($CFG->dirroot . '/mod/assign/batchsetmarkingworkflowstateform.php');
6520 $formparams = array(
6521 'userscount' => 0, // This form is never re-displayed, so we don't need to
6522 'usershtml' => '', // initialise these parameters with real information.
6523 'markingworkflowstates' => $this->get_marking_workflow_states_for_current_user()
6526 $mform = new mod_assign_batch_set_marking_workflow_state_form(null, $formparams);
6528 if ($mform->is_cancelled()) {
6529 return true;
6532 if ($formdata = $mform->get_data()) {
6533 $useridlist = explode(',', $formdata->selectedusers);
6534 $state = $formdata->markingworkflowstate;
6536 foreach ($useridlist as $userid) {
6537 $flags = $this->get_user_flags($userid, true);
6539 $flags->workflowstate = $state;
6541 // Clear the mailed flag if notification is requested, the student hasn't been
6542 // notified previously, the student can access the assignment, and the state
6543 // is "Released".
6544 $modinfo = get_fast_modinfo($this->course, $userid);
6545 $cm = $modinfo->get_cm($this->get_course_module()->id);
6546 if ($formdata->sendstudentnotifications && $cm->uservisible &&
6547 $state == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
6548 $flags->mailed = 0;
6551 $gradingdisabled = $this->grading_disabled($userid);
6553 // Will not apply update if user does not have permission to assign this workflow state.
6554 if (!$gradingdisabled && $this->update_user_flags($flags)) {
6555 if ($state == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
6556 // Update Gradebook.
6557 $assign = clone $this->get_instance();
6558 $assign->cmidnumber = $this->get_course_module()->idnumber;
6559 // Set assign gradebook feedback plugin status.
6560 $assign->gradefeedbackenabled = $this->is_gradebook_feedback_enabled();
6561 assign_update_grades($assign, $userid);
6564 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
6565 \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $state)->trigger();
6572 * Set the marking allocation for multiple users
6574 * @return void
6576 protected function process_set_batch_marking_allocation() {
6577 global $CFG, $DB;
6579 // Include batch marking allocation form.
6580 require_once($CFG->dirroot . '/mod/assign/batchsetallocatedmarkerform.php');
6582 $formparams = array(
6583 'userscount' => 0, // This form is never re-displayed, so we don't need to
6584 'usershtml' => '' // initialise these parameters with real information.
6587 list($sort, $params) = users_order_by_sql();
6588 $markers = get_users_by_capability($this->get_context(), 'mod/assign:grade', '', $sort);
6589 $markerlist = array();
6590 foreach ($markers as $marker) {
6591 $markerlist[$marker->id] = fullname($marker);
6594 $formparams['markers'] = $markerlist;
6596 $mform = new mod_assign_batch_set_allocatedmarker_form(null, $formparams);
6598 if ($mform->is_cancelled()) {
6599 return true;
6602 if ($formdata = $mform->get_data()) {
6603 $useridlist = explode(',', $formdata->selectedusers);
6604 $marker = $DB->get_record('user', array('id' => $formdata->allocatedmarker), '*', MUST_EXIST);
6606 foreach ($useridlist as $userid) {
6607 $flags = $this->get_user_flags($userid, true);
6608 if ($flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW ||
6609 $flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW ||
6610 $flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE ||
6611 $flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
6613 continue; // Allocated marker can only be changed in certain workflow states.
6616 $flags->allocatedmarker = $marker->id;
6618 if ($this->update_user_flags($flags)) {
6619 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
6620 \mod_assign\event\marker_updated::create_from_marker($this, $user, $marker)->trigger();
6628 * Prevent student updates to this submission.
6629 * Uses url parameter userid.
6631 * @param int $userid
6632 * @return void
6634 protected function process_lock_submission($userid = 0) {
6636 require_sesskey();
6638 if (!$userid) {
6639 $userid = required_param('userid', PARAM_INT);
6642 return $this->lock_submission($userid);
6646 * Unlock the student submission.
6648 * @param int $userid
6649 * @return bool
6651 public function unlock_submission($userid) {
6652 global $USER, $DB;
6654 // Need grade permission.
6655 require_capability('mod/assign:grade', $this->context);
6657 // Give each submission plugin a chance to process the unlocking.
6658 $plugins = $this->get_submission_plugins();
6659 $submission = $this->get_user_submission($userid, false);
6661 $flags = $this->get_user_flags($userid, true);
6662 $flags->locked = 0;
6663 $this->update_user_flags($flags);
6665 foreach ($plugins as $plugin) {
6666 if ($plugin->is_enabled() && $plugin->is_visible()) {
6667 $plugin->unlock($submission, $flags);
6671 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
6672 \mod_assign\event\submission_unlocked::create_from_user($this, $user)->trigger();
6673 return true;
6677 * Unlock the student submission.
6678 * Uses url parameter userid.
6680 * @param int $userid
6681 * @return bool
6683 protected function process_unlock_submission($userid = 0) {
6685 require_sesskey();
6687 if (!$userid) {
6688 $userid = required_param('userid', PARAM_INT);
6691 return $this->unlock_submission($userid);
6695 * Apply a grade from a grading form to a user (may be called multiple times for a group submission).
6697 * @param stdClass $formdata - the data from the form
6698 * @param int $userid - the user to apply the grade to
6699 * @param int $attemptnumber - The attempt number to apply the grade to.
6700 * @return void
6702 protected function apply_grade_to_user($formdata, $userid, $attemptnumber) {
6703 global $USER, $CFG, $DB;
6705 $grade = $this->get_user_grade($userid, true, $attemptnumber);
6706 $originalgrade = $grade->grade;
6707 $gradingdisabled = $this->grading_disabled($userid);
6708 $gradinginstance = $this->get_grading_instance($userid, $grade, $gradingdisabled);
6709 if (!$gradingdisabled) {
6710 if ($gradinginstance) {
6711 $grade->grade = $gradinginstance->submit_and_get_grade($formdata->advancedgrading,
6712 $grade->id);
6713 } else {
6714 // Handle the case when grade is set to No Grade.
6715 if (isset($formdata->grade)) {
6716 $grade->grade = grade_floatval(unformat_float($formdata->grade));
6719 if (isset($formdata->workflowstate) || isset($formdata->allocatedmarker)) {
6720 $flags = $this->get_user_flags($userid, true);
6721 $oldworkflowstate = $flags->workflowstate;
6722 $flags->workflowstate = isset($formdata->workflowstate) ? $formdata->workflowstate : $flags->workflowstate;
6723 $flags->allocatedmarker = isset($formdata->allocatedmarker) ? $formdata->allocatedmarker : $flags->allocatedmarker;
6724 if ($this->update_user_flags($flags) &&
6725 isset($formdata->workflowstate) &&
6726 $formdata->workflowstate !== $oldworkflowstate) {
6727 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
6728 \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $formdata->workflowstate)->trigger();
6732 $grade->grader= $USER->id;
6734 $adminconfig = $this->get_admin_config();
6735 $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
6737 // Call save in plugins.
6738 foreach ($this->feedbackplugins as $plugin) {
6739 if ($plugin->is_enabled() && $plugin->is_visible()) {
6740 if (!$plugin->save($grade, $formdata)) {
6741 $result = false;
6742 print_error($plugin->get_error());
6744 if (('assignfeedback_' . $plugin->get_type()) == $gradebookplugin) {
6745 // This is the feedback plugin chose to push comments to the gradebook.
6746 $grade->feedbacktext = $plugin->text_for_gradebook($grade);
6747 $grade->feedbackformat = $plugin->format_for_gradebook($grade);
6751 // We do not want to update the timemodified if no grade was added.
6752 if (!empty($formdata->addattempt) ||
6753 ($originalgrade !== null && $originalgrade != -1) ||
6754 ($grade->grade !== null && $grade->grade != -1)) {
6755 $this->update_grade($grade, !empty($formdata->addattempt));
6757 // Note the default if not provided for this option is true (e.g. webservices).
6758 // This is for backwards compatibility.
6759 if (!isset($formdata->sendstudentnotifications) || $formdata->sendstudentnotifications) {
6760 $this->notify_grade_modified($grade, true);
6766 * Save outcomes submitted from grading form.
6768 * @param int $userid
6769 * @param stdClass $formdata
6770 * @param int $sourceuserid The user ID under which the outcome data is accessible. This is relevant
6771 * for an outcome set to a user but applied to an entire group.
6773 protected function process_outcomes($userid, $formdata, $sourceuserid = null) {
6774 global $CFG, $USER;
6776 if (empty($CFG->enableoutcomes)) {
6777 return;
6779 if ($this->grading_disabled($userid)) {
6780 return;
6783 require_once($CFG->libdir.'/gradelib.php');
6785 $data = array();
6786 $gradinginfo = grade_get_grades($this->get_course()->id,
6787 'mod',
6788 'assign',
6789 $this->get_instance()->id,
6790 $userid);
6792 if (!empty($gradinginfo->outcomes)) {
6793 foreach ($gradinginfo->outcomes as $index => $oldoutcome) {
6794 $name = 'outcome_'.$index;
6795 $sourceuserid = $sourceuserid !== null ? $sourceuserid : $userid;
6796 if (isset($formdata->{$name}[$sourceuserid]) &&
6797 $oldoutcome->grades[$userid]->grade != $formdata->{$name}[$sourceuserid]) {
6798 $data[$index] = $formdata->{$name}[$sourceuserid];
6802 if (count($data) > 0) {
6803 grade_update_outcomes('mod/assign',
6804 $this->course->id,
6805 'mod',
6806 'assign',
6807 $this->get_instance()->id,
6808 $userid,
6809 $data);
6814 * If the requirements are met - reopen the submission for another attempt.
6815 * Only call this function when grading the latest attempt.
6817 * @param int $userid The userid.
6818 * @param stdClass $submission The submission (may be a group submission).
6819 * @param bool $addattempt - True if the "allow another attempt" checkbox was checked.
6820 * @return bool - true if another attempt was added.
6822 protected function reopen_submission_if_required($userid, $submission, $addattempt) {
6823 $instance = $this->get_instance();
6824 $maxattemptsreached = !empty($submission) &&
6825 $submission->attemptnumber >= ($instance->maxattempts - 1) &&
6826 $instance->maxattempts != ASSIGN_UNLIMITED_ATTEMPTS;
6827 $shouldreopen = false;
6828 if ($instance->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS) {
6829 // Check the gradetopass from the gradebook.
6830 $gradeitem = $this->get_grade_item();
6831 if ($gradeitem) {
6832 $gradegrade = grade_grade::fetch(array('userid' => $userid, 'itemid' => $gradeitem->id));
6834 // Do not reopen if is_passed returns null, e.g. if there is no pass criterion set.
6835 if ($gradegrade && ($gradegrade->is_passed() === false)) {
6836 $shouldreopen = true;
6840 if ($instance->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL &&
6841 !empty($addattempt)) {
6842 $shouldreopen = true;
6844 if ($shouldreopen && !$maxattemptsreached) {
6845 $this->add_attempt($userid);
6846 return true;
6848 return false;
6852 * Save grade update.
6854 * @param int $userid
6855 * @param stdClass $data
6856 * @return bool - was the grade saved
6858 public function save_grade($userid, $data) {
6860 // Need grade permission.
6861 require_capability('mod/assign:grade', $this->context);
6863 $instance = $this->get_instance();
6864 $submission = null;
6865 if ($instance->teamsubmission) {
6866 $submission = $this->get_group_submission($userid, 0, false, $data->attemptnumber);
6867 } else {
6868 $submission = $this->get_user_submission($userid, false, $data->attemptnumber);
6870 if ($instance->teamsubmission && $data->applytoall) {
6871 $groupid = 0;
6872 if ($this->get_submission_group($userid)) {
6873 $group = $this->get_submission_group($userid);
6874 if ($group) {
6875 $groupid = $group->id;
6878 $members = $this->get_submission_group_members($groupid, true, $this->show_only_active_users());
6879 foreach ($members as $member) {
6880 // User may exist in multple groups (which should put them in the default group).
6881 $this->apply_grade_to_user($data, $member->id, $data->attemptnumber);
6882 $this->process_outcomes($member->id, $data, $userid);
6884 } else {
6885 $this->apply_grade_to_user($data, $userid, $data->attemptnumber);
6887 $this->process_outcomes($userid, $data);
6890 return true;
6894 * Save grade.
6896 * @param moodleform $mform
6897 * @return bool - was the grade saved
6899 protected function process_save_grade(&$mform) {
6900 global $CFG;
6901 // Include grade form.
6902 require_once($CFG->dirroot . '/mod/assign/gradeform.php');
6904 require_sesskey();
6906 $instance = $this->get_instance();
6907 $rownum = required_param('rownum', PARAM_INT);
6908 $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT);
6909 $useridlistid = optional_param('useridlistid', time(), PARAM_INT);
6910 $userid = optional_param('userid', 0, PARAM_INT);
6911 $cache = cache::make_from_params(cache_store::MODE_SESSION, 'mod_assign', 'useridlist');
6912 if (!$userid) {
6913 if (!$useridlist = $cache->get($this->get_course_module()->id . '_' . $useridlistid)) {
6914 $useridlist = $this->get_grading_userid_list();
6915 $cache->set($this->get_course_module()->id . '_' . $useridlistid, $useridlist);
6917 } else {
6918 $useridlist = array($userid);
6919 $rownum = 0;
6922 $last = false;
6923 $userid = $useridlist[$rownum];
6924 if ($rownum == count($useridlist) - 1) {
6925 $last = true;
6928 $data = new stdClass();
6930 $gradeformparams = array('rownum'=>$rownum,
6931 'useridlistid'=>$useridlistid,
6932 'last'=>false,
6933 'attemptnumber'=>$attemptnumber,
6934 'userid'=>optional_param('userid', 0, PARAM_INT));
6935 $mform = new mod_assign_grade_form(null,
6936 array($this, $data, $gradeformparams),
6937 'post',
6939 array('class'=>'gradeform'));
6941 if ($formdata = $mform->get_data()) {
6942 return $this->save_grade($userid, $formdata);
6943 } else {
6944 return false;
6949 * This function is a static wrapper around can_upgrade.
6951 * @param string $type The plugin type
6952 * @param int $version The plugin version
6953 * @return bool
6955 public static function can_upgrade_assignment($type, $version) {
6956 $assignment = new assign(null, null, null);
6957 return $assignment->can_upgrade($type, $version);
6961 * This function returns true if it can upgrade an assignment from the 2.2 module.
6963 * @param string $type The plugin type
6964 * @param int $version The plugin version
6965 * @return bool
6967 public function can_upgrade($type, $version) {
6968 if ($type == 'offline' && $version >= 2011112900) {
6969 return true;
6971 foreach ($this->submissionplugins as $plugin) {
6972 if ($plugin->can_upgrade($type, $version)) {
6973 return true;
6976 foreach ($this->feedbackplugins as $plugin) {
6977 if ($plugin->can_upgrade($type, $version)) {
6978 return true;
6981 return false;
6985 * Copy all the files from the old assignment files area to the new one.
6986 * This is used by the plugin upgrade code.
6988 * @param int $oldcontextid The old assignment context id
6989 * @param int $oldcomponent The old assignment component ('assignment')
6990 * @param int $oldfilearea The old assignment filearea ('submissions')
6991 * @param int $olditemid The old submissionid (can be null e.g. intro)
6992 * @param int $newcontextid The new assignment context id
6993 * @param int $newcomponent The new assignment component ('assignment')
6994 * @param int $newfilearea The new assignment filearea ('submissions')
6995 * @param int $newitemid The new submissionid (can be null e.g. intro)
6996 * @return int The number of files copied
6998 public function copy_area_files_for_upgrade($oldcontextid,
6999 $oldcomponent,
7000 $oldfilearea,
7001 $olditemid,
7002 $newcontextid,
7003 $newcomponent,
7004 $newfilearea,
7005 $newitemid) {
7006 // Note, this code is based on some code in filestorage - but that code
7007 // deleted the old files (which we don't want).
7008 $count = 0;
7010 $fs = get_file_storage();
7012 $oldfiles = $fs->get_area_files($oldcontextid,
7013 $oldcomponent,
7014 $oldfilearea,
7015 $olditemid,
7016 'id',
7017 false);
7018 foreach ($oldfiles as $oldfile) {
7019 $filerecord = new stdClass();
7020 $filerecord->contextid = $newcontextid;
7021 $filerecord->component = $newcomponent;
7022 $filerecord->filearea = $newfilearea;
7023 $filerecord->itemid = $newitemid;
7024 $fs->create_file_from_storedfile($filerecord, $oldfile);
7025 $count += 1;
7028 return $count;
7032 * Add a new attempt for each user in the list - but reopen each group assignment
7033 * at most 1 time.
7035 * @param array $useridlist Array of userids to reopen.
7036 * @return bool
7038 protected function process_add_attempt_group($useridlist) {
7039 $groupsprocessed = array();
7040 $result = true;
7042 foreach ($useridlist as $userid) {
7043 $groupid = 0;
7044 $group = $this->get_submission_group($userid);
7045 if ($group) {
7046 $groupid = $group->id;
7049 if (empty($groupsprocessed[$groupid])) {
7050 $result = $this->process_add_attempt($userid) && $result;
7051 $groupsprocessed[$groupid] = true;
7054 return $result;
7058 * Check for a sess key and then call add_attempt.
7060 * @param int $userid int The user to add the attempt for
7061 * @return bool - true if successful.
7063 protected function process_add_attempt($userid) {
7064 require_sesskey();
7066 return $this->add_attempt($userid);
7070 * Add a new attempt for a user.
7072 * @param int $userid int The user to add the attempt for
7073 * @return bool - true if successful.
7075 protected function add_attempt($userid) {
7076 require_capability('mod/assign:grade', $this->context);
7078 if ($this->get_instance()->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_NONE) {
7079 return false;
7082 if ($this->get_instance()->teamsubmission) {
7083 $oldsubmission = $this->get_group_submission($userid, 0, false);
7084 } else {
7085 $oldsubmission = $this->get_user_submission($userid, false);
7088 if (!$oldsubmission) {
7089 return false;
7092 // No more than max attempts allowed.
7093 if ($this->get_instance()->maxattempts != ASSIGN_UNLIMITED_ATTEMPTS &&
7094 $oldsubmission->attemptnumber >= ($this->get_instance()->maxattempts - 1)) {
7095 return false;
7098 // Create the new submission record for the group/user.
7099 if ($this->get_instance()->teamsubmission) {
7100 $newsubmission = $this->get_group_submission($userid, 0, true, $oldsubmission->attemptnumber + 1);
7101 } else {
7102 $newsubmission = $this->get_user_submission($userid, true, $oldsubmission->attemptnumber + 1);
7105 // Set the status of the new attempt to reopened.
7106 $newsubmission->status = ASSIGN_SUBMISSION_STATUS_REOPENED;
7108 // Give each submission plugin a chance to process the add_attempt.
7109 $plugins = $this->get_submission_plugins();
7110 foreach ($plugins as $plugin) {
7111 if ($plugin->is_enabled() && $plugin->is_visible()) {
7112 $plugin->add_attempt($oldsubmission, $newsubmission);
7116 $this->update_submission($newsubmission, $userid, false, $this->get_instance()->teamsubmission);
7117 return true;
7121 * Get an upto date list of user grades and feedback for the gradebook.
7123 * @param int $userid int or 0 for all users
7124 * @return array of grade data formated for the gradebook api
7125 * The data required by the gradebook api is userid,
7126 * rawgrade,
7127 * feedback,
7128 * feedbackformat,
7129 * usermodified,
7130 * dategraded,
7131 * datesubmitted
7133 public function get_user_grades_for_gradebook($userid) {
7134 global $DB, $CFG;
7135 $grades = array();
7136 $assignmentid = $this->get_instance()->id;
7138 $adminconfig = $this->get_admin_config();
7139 $gradebookpluginname = $adminconfig->feedback_plugin_for_gradebook;
7140 $gradebookplugin = null;
7142 // Find the gradebook plugin.
7143 foreach ($this->feedbackplugins as $plugin) {
7144 if ($plugin->is_enabled() && $plugin->is_visible()) {
7145 if (('assignfeedback_' . $plugin->get_type()) == $gradebookpluginname) {
7146 $gradebookplugin = $plugin;
7150 if ($userid) {
7151 $where = ' WHERE u.id = :userid ';
7152 } else {
7153 $where = ' WHERE u.id != :userid ';
7156 // When the gradebook asks us for grades - only return the last attempt for each user.
7157 $params = array('assignid1'=>$assignmentid,
7158 'assignid2'=>$assignmentid,
7159 'userid'=>$userid);
7160 $graderesults = $DB->get_recordset_sql('SELECT
7161 u.id as userid,
7162 s.timemodified as datesubmitted,
7163 g.grade as rawgrade,
7164 g.timemodified as dategraded,
7165 g.grader as usermodified
7166 FROM {user} u
7167 LEFT JOIN {assign_submission} s
7168 ON u.id = s.userid and s.assignment = :assignid1 AND
7169 s.latest = 1
7170 JOIN {assign_grades} g
7171 ON u.id = g.userid and g.assignment = :assignid2 AND
7172 g.attemptnumber = s.attemptnumber' .
7173 $where, $params);
7175 foreach ($graderesults as $result) {
7176 $gradingstatus = $this->get_grading_status($result->userid);
7177 if (!$this->get_instance()->markingworkflow || $gradingstatus == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
7178 $gradebookgrade = clone $result;
7179 // Now get the feedback.
7180 if ($gradebookplugin) {
7181 $grade = $this->get_user_grade($result->userid, false);
7182 if ($grade) {
7183 $gradebookgrade->feedback = $gradebookplugin->text_for_gradebook($grade);
7184 $gradebookgrade->feedbackformat = $gradebookplugin->format_for_gradebook($grade);
7187 $grades[$gradebookgrade->userid] = $gradebookgrade;
7191 $graderesults->close();
7192 return $grades;
7196 * Call the static version of this function
7198 * @param int $userid The userid to lookup
7199 * @return int The unique id
7201 public function get_uniqueid_for_user($userid) {
7202 return self::get_uniqueid_for_user_static($this->get_instance()->id, $userid);
7206 * Foreach participant in the course - assign them a random id.
7208 * @param int $assignid The assignid to lookup
7210 public static function allocate_unique_ids($assignid) {
7211 global $DB;
7213 $cm = get_coursemodule_from_instance('assign', $assignid, 0, false, MUST_EXIST);
7214 $context = context_module::instance($cm->id);
7216 $currentgroup = groups_get_activity_group($cm, true);
7217 $users = get_enrolled_users($context, "mod/assign:submit", $currentgroup, 'u.id');
7219 // Shuffle the users.
7220 shuffle($users);
7222 foreach ($users as $user) {
7223 $record = $DB->get_record('assign_user_mapping',
7224 array('assignment'=>$assignid, 'userid'=>$user->id),
7225 'id');
7226 if (!$record) {
7227 $record = new stdClass();
7228 $record->assignment = $assignid;
7229 $record->userid = $user->id;
7230 $DB->insert_record('assign_user_mapping', $record);
7236 * Lookup this user id and return the unique id for this assignment.
7238 * @param int $assignid The assignment id
7239 * @param int $userid The userid to lookup
7240 * @return int The unique id
7242 public static function get_uniqueid_for_user_static($assignid, $userid) {
7243 global $DB;
7245 // Search for a record.
7246 $params = array('assignment'=>$assignid, 'userid'=>$userid);
7247 if ($record = $DB->get_record('assign_user_mapping', $params, 'id')) {
7248 return $record->id;
7251 // Be a little smart about this - there is no record for the current user.
7252 // We should ensure any unallocated ids for the current participant
7253 // list are distrubited randomly.
7254 self::allocate_unique_ids($assignid);
7256 // Retry the search for a record.
7257 if ($record = $DB->get_record('assign_user_mapping', $params, 'id')) {
7258 return $record->id;
7261 // The requested user must not be a participant. Add a record anyway.
7262 $record = new stdClass();
7263 $record->assignment = $assignid;
7264 $record->userid = $userid;
7266 return $DB->insert_record('assign_user_mapping', $record);
7270 * Call the static version of this function.
7272 * @param int $uniqueid The uniqueid to lookup
7273 * @return int The user id or false if they don't exist
7275 public function get_user_id_for_uniqueid($uniqueid) {
7276 return self::get_user_id_for_uniqueid_static($this->get_instance()->id, $uniqueid);
7280 * Lookup this unique id and return the user id for this assignment.
7282 * @param int $assignid The id of the assignment this user mapping is in
7283 * @param int $uniqueid The uniqueid to lookup
7284 * @return int The user id or false if they don't exist
7286 public static function get_user_id_for_uniqueid_static($assignid, $uniqueid) {
7287 global $DB;
7289 // Search for a record.
7290 if ($record = $DB->get_record('assign_user_mapping',
7291 array('assignment'=>$assignid, 'id'=>$uniqueid),
7292 'userid',
7293 IGNORE_MISSING)) {
7294 return $record->userid;
7297 return false;
7301 * Get the list of marking_workflow states the current user has permission to transition a grade to.
7303 * @return array of state => description
7305 public function get_marking_workflow_states_for_current_user() {
7306 if (!empty($this->markingworkflowstates)) {
7307 return $this->markingworkflowstates;
7309 $states = array();
7310 if (has_capability('mod/assign:grade', $this->context)) {
7311 $states[ASSIGN_MARKING_WORKFLOW_STATE_INMARKING] = get_string('markingworkflowstateinmarking', 'assign');
7312 $states[ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW] = get_string('markingworkflowstatereadyforreview', 'assign');
7314 if (has_any_capability(array('mod/assign:reviewgrades',
7315 'mod/assign:managegrades'), $this->context)) {
7316 $states[ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW] = get_string('markingworkflowstateinreview', 'assign');
7317 $states[ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE] = get_string('markingworkflowstatereadyforrelease', 'assign');
7319 if (has_any_capability(array('mod/assign:releasegrades',
7320 'mod/assign:managegrades'), $this->context)) {
7321 $states[ASSIGN_MARKING_WORKFLOW_STATE_RELEASED] = get_string('markingworkflowstatereleased', 'assign');
7323 $this->markingworkflowstates = $states;
7324 return $this->markingworkflowstates;
7328 * Check is only active users in course should be shown.
7330 * @return bool true if only active users should be shown.
7332 public function show_only_active_users() {
7333 global $CFG;
7335 if (is_null($this->showonlyactiveenrol)) {
7336 $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol);
7337 $this->showonlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol);
7339 if (!is_null($this->context)) {
7340 $this->showonlyactiveenrol = $this->showonlyactiveenrol ||
7341 !has_capability('moodle/course:viewsuspendedusers', $this->context);
7344 return $this->showonlyactiveenrol;
7348 * Return true is user is active user in course else false
7350 * @param int $userid
7351 * @return bool true is user is active in course.
7353 public function is_active_user($userid) {
7354 return !in_array($userid, get_suspended_userids($this->context, true));
7358 * Returns true if gradebook feedback plugin is enabled
7360 * @return bool true if gradebook feedback plugin is enabled and visible else false.
7362 public function is_gradebook_feedback_enabled() {
7363 // Get default grade book feedback plugin.
7364 $adminconfig = $this->get_admin_config();
7365 $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
7366 $gradebookplugin = str_replace('assignfeedback_', '', $gradebookplugin);
7368 // Check if default gradebook feedback is visible and enabled.
7369 $gradebookfeedbackplugin = $this->get_feedback_plugin_by_type($gradebookplugin);
7371 if ($gradebookfeedbackplugin->is_visible() && $gradebookfeedbackplugin->is_enabled()) {
7372 return true;
7375 // Gradebook feedback plugin is either not visible/enabled.
7376 return false;
7380 * Returns the grading status.
7382 * @param int $userid the user id
7383 * @return string returns the grading status
7385 public function get_grading_status($userid) {
7386 if ($this->get_instance()->markingworkflow) {
7387 $flags = $this->get_user_flags($userid, false);
7388 if (!empty($flags->workflowstate)) {
7389 return $flags->workflowstate;
7391 return ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED;
7392 } else {
7393 $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT);
7394 $grade = $this->get_user_grade($userid, false, $attemptnumber);
7396 if (!empty($grade) && $grade->grade !== null && $grade->grade >= 0) {
7397 return ASSIGN_GRADING_STATUS_GRADED;
7398 } else {
7399 return ASSIGN_GRADING_STATUS_NOT_GRADED;
7406 * Portfolio caller class for mod_assign.
7408 * @package mod_assign
7409 * @copyright 2012 NetSpot {@link http://www.netspot.com.au}
7410 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
7412 class assign_portfolio_caller extends portfolio_module_caller_base {
7414 /** @var int callback arg - the id of submission we export */
7415 protected $sid;
7417 /** @var string component of the submission files we export*/
7418 protected $component;
7420 /** @var string callback arg - the area of submission files we export */
7421 protected $area;
7423 /** @var int callback arg - the id of file we export */
7424 protected $fileid;
7426 /** @var int callback arg - the cmid of the assignment we export */
7427 protected $cmid;
7429 /** @var string callback arg - the plugintype of the editor we export */
7430 protected $plugin;
7432 /** @var string callback arg - the name of the editor field we export */
7433 protected $editor;
7436 * Callback arg for a single file export.
7438 public static function expected_callbackargs() {
7439 return array(
7440 'cmid' => true,
7441 'sid' => false,
7442 'area' => false,
7443 'component' => false,
7444 'fileid' => false,
7445 'plugin' => false,
7446 'editor' => false,
7451 * The constructor.
7453 * @param array $callbackargs
7455 public function __construct($callbackargs) {
7456 parent::__construct($callbackargs);
7457 $this->cm = get_coursemodule_from_id('assign', $this->cmid, 0, false, MUST_EXIST);
7461 * Load data needed for the portfolio export.
7463 * If the assignment type implements portfolio_load_data(), the processing is delegated
7464 * to it. Otherwise, the caller must provide either fileid (to export single file) or
7465 * submissionid and filearea (to export all data attached to the given submission file area)
7466 * via callback arguments.
7468 * @throws portfolio_caller_exception
7470 public function load_data() {
7472 $context = context_module::instance($this->cmid);
7474 if (empty($this->fileid)) {
7475 if (empty($this->sid) || empty($this->area)) {
7476 throw new portfolio_caller_exception('invalidfileandsubmissionid', 'mod_assign');
7481 // Export either an area of files or a single file (see function for more detail).
7482 // The first arg is an id or null. If it is an id, the rest of the args are ignored.
7483 // If it is null, the rest of the args are used to load a list of files from get_areafiles.
7484 $this->set_file_and_format_data($this->fileid,
7485 $context->id,
7486 $this->component,
7487 $this->area,
7488 $this->sid,
7489 'timemodified',
7490 false);
7495 * Prepares the package up before control is passed to the portfolio plugin.
7497 * @throws portfolio_caller_exception
7498 * @return mixed
7500 public function prepare_package() {
7502 if ($this->plugin && $this->editor) {
7503 $options = portfolio_format_text_options();
7504 $context = context_module::instance($this->cmid);
7505 $options->context = $context;
7507 $plugin = $this->get_submission_plugin();
7509 $text = $plugin->get_editor_text($this->editor, $this->sid);
7510 $format = $plugin->get_editor_format($this->editor, $this->sid);
7512 $html = format_text($text, $format, $options);
7513 $html = portfolio_rewrite_pluginfile_urls($html,
7514 $context->id,
7515 'mod_assign',
7516 $this->area,
7517 $this->sid,
7518 $this->exporter->get('format'));
7520 $exporterclass = $this->exporter->get('formatclass');
7521 if (in_array($exporterclass, array(PORTFOLIO_FORMAT_PLAINHTML, PORTFOLIO_FORMAT_RICHHTML))) {
7522 if ($files = $this->exporter->get('caller')->get('multifiles')) {
7523 foreach ($files as $file) {
7524 $this->exporter->copy_existing_file($file);
7527 return $this->exporter->write_new_file($html, 'assignment.html', !empty($files));
7528 } else if ($this->exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) {
7529 $leapwriter = $this->exporter->get('format')->leap2a_writer();
7530 $entry = new portfolio_format_leap2a_entry($this->area . $this->cmid,
7531 $context->get_context_name(),
7532 'resource',
7533 $html);
7535 $entry->add_category('web', 'resource_type');
7536 $entry->author = $this->user;
7537 $leapwriter->add_entry($entry);
7538 if ($files = $this->exporter->get('caller')->get('multifiles')) {
7539 $leapwriter->link_files($entry, $files, $this->area . $this->cmid . 'file');
7540 foreach ($files as $file) {
7541 $this->exporter->copy_existing_file($file);
7544 return $this->exporter->write_new_file($leapwriter->to_xml(),
7545 $this->exporter->get('format')->manifest_name(),
7546 true);
7547 } else {
7548 debugging('invalid format class: ' . $this->exporter->get('formatclass'));
7553 if ($this->exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) {
7554 $leapwriter = $this->exporter->get('format')->leap2a_writer();
7555 $files = array();
7556 if ($this->singlefile) {
7557 $files[] = $this->singlefile;
7558 } else if ($this->multifiles) {
7559 $files = $this->multifiles;
7560 } else {
7561 throw new portfolio_caller_exception('invalidpreparepackagefile',
7562 'portfolio',
7563 $this->get_return_url());
7566 $entryids = array();
7567 foreach ($files as $file) {
7568 $entry = new portfolio_format_leap2a_file($file->get_filename(), $file);
7569 $entry->author = $this->user;
7570 $leapwriter->add_entry($entry);
7571 $this->exporter->copy_existing_file($file);
7572 $entryids[] = $entry->id;
7574 if (count($files) > 1) {
7575 $baseid = 'assign' . $this->cmid . $this->area;
7576 $context = context_module::instance($this->cmid);
7578 // If we have multiple files, they should be grouped together into a folder.
7579 $entry = new portfolio_format_leap2a_entry($baseid . 'group',
7580 $context->get_context_name(),
7581 'selection');
7582 $leapwriter->add_entry($entry);
7583 $leapwriter->make_selection($entry, $entryids, 'Folder');
7585 return $this->exporter->write_new_file($leapwriter->to_xml(),
7586 $this->exporter->get('format')->manifest_name(),
7587 true);
7589 return $this->prepare_package_file();
7593 * Fetch the plugin by its type.
7595 * @return assign_submission_plugin
7597 protected function get_submission_plugin() {
7598 global $CFG;
7599 if (!$this->plugin || !$this->cmid) {
7600 return null;
7603 require_once($CFG->dirroot . '/mod/assign/locallib.php');
7605 $context = context_module::instance($this->cmid);
7607 $assignment = new assign($context, null, null);
7608 return $assignment->get_submission_plugin_by_type($this->plugin);
7612 * Calculate a sha1 has of either a single file or a list
7613 * of files based on the data set by load_data.
7615 * @return string
7617 public function get_sha1() {
7619 if ($this->plugin && $this->editor) {
7620 $plugin = $this->get_submission_plugin();
7621 $options = portfolio_format_text_options();
7622 $options->context = context_module::instance($this->cmid);
7624 $text = format_text($plugin->get_editor_text($this->editor, $this->sid),
7625 $plugin->get_editor_format($this->editor, $this->sid),
7626 $options);
7627 $textsha1 = sha1($text);
7628 $filesha1 = '';
7629 try {
7630 $filesha1 = $this->get_sha1_file();
7631 } catch (portfolio_caller_exception $e) {
7632 // No files.
7634 return sha1($textsha1 . $filesha1);
7636 return $this->get_sha1_file();
7640 * Calculate the time to transfer either a single file or a list
7641 * of files based on the data set by load_data.
7643 * @return int
7645 public function expected_time() {
7646 return $this->expected_time_file();
7650 * Checking the permissions.
7652 * @return bool
7654 public function check_permissions() {
7655 $context = context_module::instance($this->cmid);
7656 return has_capability('mod/assign:exportownsubmission', $context);
7660 * Display a module name.
7662 * @return string
7664 public static function display_name() {
7665 return get_string('modulename', 'assign');
7669 * Return array of formats supported by this portfolio call back.
7671 * @return array
7673 public static function base_supported_formats() {
7674 return array(PORTFOLIO_FORMAT_FILE, PORTFOLIO_FORMAT_LEAP2A);