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