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