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