See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 401 and 403]
1 <?php 2 3 // This file is part of Moodle - http://moodle.org/ 4 // 5 // Moodle is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // Moodle is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU General Public License for more details. 14 // 15 // You should have received a copy of the GNU General Public License 16 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 17 18 /** 19 * Library of workshop module functions needed by Moodle core and other subsystems 20 * 21 * All the functions neeeded by Moodle core, gradebook, file subsystem etc 22 * are placed here. 23 * 24 * @package mod_workshop 25 * @copyright 2009 David Mudrak <david.mudrak@gmail.com> 26 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 27 */ 28 29 defined('MOODLE_INTERNAL') || die(); 30 31 require_once($CFG->dirroot . '/calendar/lib.php'); 32 33 define('WORKSHOP_EVENT_TYPE_SUBMISSION_OPEN', 'opensubmission'); 34 define('WORKSHOP_EVENT_TYPE_SUBMISSION_CLOSE', 'closesubmission'); 35 define('WORKSHOP_EVENT_TYPE_ASSESSMENT_OPEN', 'openassessment'); 36 define('WORKSHOP_EVENT_TYPE_ASSESSMENT_CLOSE', 'closeassessment'); 37 define('WORKSHOP_SUBMISSION_TYPE_DISABLED', 0); 38 define('WORKSHOP_SUBMISSION_TYPE_AVAILABLE', 1); 39 define('WORKSHOP_SUBMISSION_TYPE_REQUIRED', 2); 40 41 //////////////////////////////////////////////////////////////////////////////// 42 // Moodle core API // 43 //////////////////////////////////////////////////////////////////////////////// 44 45 /** 46 * Returns the information if the module supports a feature 47 * 48 * @see plugin_supports() in lib/moodlelib.php 49 * @param string $feature FEATURE_xx constant for requested feature 50 * @return mixed True if module supports feature, false if not, null if doesn't know or string for the module purpose. 51 */ 52 function workshop_supports($feature) { 53 switch($feature) { 54 case FEATURE_GRADE_HAS_GRADE: return true; 55 case FEATURE_GROUPS: return true; 56 case FEATURE_GROUPINGS: return true; 57 case FEATURE_MOD_INTRO: return true; 58 case FEATURE_BACKUP_MOODLE2: return true; 59 case FEATURE_COMPLETION_TRACKS_VIEWS: 60 return true; 61 case FEATURE_SHOW_DESCRIPTION: return true; 62 case FEATURE_PLAGIARISM: return true; 63 case FEATURE_MOD_PURPOSE: return MOD_PURPOSE_ASSESSMENT; 64 default: return null; 65 } 66 } 67 68 /** 69 * Saves a new instance of the workshop into the database 70 * 71 * Given an object containing all the necessary data, 72 * (defined by the form in mod_form.php) this function 73 * will save a new instance and return the id number 74 * of the new instance. 75 * 76 * @param stdClass $workshop An object from the form in mod_form.php 77 * @return int The id of the newly inserted workshop record 78 */ 79 function workshop_add_instance(stdclass $workshop) { 80 global $CFG, $DB; 81 require_once (__DIR__ . '/locallib.php'); 82 83 $workshop->phase = workshop::PHASE_SETUP; 84 $workshop->timecreated = time(); 85 $workshop->timemodified = $workshop->timecreated; 86 $workshop->useexamples = (int)!empty($workshop->useexamples); 87 $workshop->usepeerassessment = 1; 88 $workshop->useselfassessment = (int)!empty($workshop->useselfassessment); 89 $workshop->latesubmissions = (int)!empty($workshop->latesubmissions); 90 $workshop->phaseswitchassessment = (int)!empty($workshop->phaseswitchassessment); 91 $workshop->evaluation = 'best'; 92 93 if (isset($workshop->gradinggradepass)) { 94 $workshop->gradinggradepass = (float)unformat_float($workshop->gradinggradepass); 95 } 96 97 if (isset($workshop->submissiongradepass)) { 98 $workshop->submissiongradepass = (float)unformat_float($workshop->submissiongradepass); 99 } 100 101 if (isset($workshop->submissionfiletypes)) { 102 $filetypesutil = new \core_form\filetypes_util(); 103 $submissionfiletypes = $filetypesutil->normalize_file_types($workshop->submissionfiletypes); 104 $workshop->submissionfiletypes = implode(' ', $submissionfiletypes); 105 } 106 107 if (isset($workshop->overallfeedbackfiletypes)) { 108 $filetypesutil = new \core_form\filetypes_util(); 109 $overallfeedbackfiletypes = $filetypesutil->normalize_file_types($workshop->overallfeedbackfiletypes); 110 $workshop->overallfeedbackfiletypes = implode(' ', $overallfeedbackfiletypes); 111 } 112 113 // insert the new record so we get the id 114 $workshop->id = $DB->insert_record('workshop', $workshop); 115 116 // we need to use context now, so we need to make sure all needed info is already in db 117 $cmid = $workshop->coursemodule; 118 $DB->set_field('course_modules', 'instance', $workshop->id, array('id' => $cmid)); 119 $context = context_module::instance($cmid); 120 121 // process the custom wysiwyg editors 122 if ($draftitemid = $workshop->instructauthorseditor['itemid']) { 123 $workshop->instructauthors = file_save_draft_area_files($draftitemid, $context->id, 'mod_workshop', 'instructauthors', 124 0, workshop::instruction_editors_options($context), $workshop->instructauthorseditor['text']); 125 $workshop->instructauthorsformat = $workshop->instructauthorseditor['format']; 126 } 127 128 if ($draftitemid = $workshop->instructreviewerseditor['itemid']) { 129 $workshop->instructreviewers = file_save_draft_area_files($draftitemid, $context->id, 'mod_workshop', 'instructreviewers', 130 0, workshop::instruction_editors_options($context), $workshop->instructreviewerseditor['text']); 131 $workshop->instructreviewersformat = $workshop->instructreviewerseditor['format']; 132 } 133 134 if ($draftitemid = $workshop->conclusioneditor['itemid']) { 135 $workshop->conclusion = file_save_draft_area_files($draftitemid, $context->id, 'mod_workshop', 'conclusion', 136 0, workshop::instruction_editors_options($context), $workshop->conclusioneditor['text']); 137 $workshop->conclusionformat = $workshop->conclusioneditor['format']; 138 } 139 140 // re-save the record with the replaced URLs in editor fields 141 $DB->update_record('workshop', $workshop); 142 143 // create gradebook items 144 workshop_grade_item_update($workshop); 145 workshop_grade_item_category_update($workshop); 146 147 // create calendar events 148 workshop_calendar_update($workshop, $workshop->coursemodule); 149 if (!empty($workshop->completionexpected)) { 150 \core_completion\api::update_completion_date_event($cmid, 'workshop', $workshop->id, $workshop->completionexpected); 151 } 152 153 return $workshop->id; 154 } 155 156 /** 157 * Given an object containing all the necessary data, 158 * (defined by the form in mod_form.php) this function 159 * will update an existing instance with new data. 160 * 161 * @param stdClass $workshop An object from the form in mod_form.php 162 * @return bool success 163 */ 164 function workshop_update_instance(stdclass $workshop) { 165 global $CFG, $DB; 166 require_once (__DIR__ . '/locallib.php'); 167 168 $workshop->timemodified = time(); 169 $workshop->id = $workshop->instance; 170 $workshop->useexamples = (int)!empty($workshop->useexamples); 171 $workshop->usepeerassessment = 1; 172 $workshop->useselfassessment = (int)!empty($workshop->useselfassessment); 173 $workshop->latesubmissions = (int)!empty($workshop->latesubmissions); 174 $workshop->phaseswitchassessment = (int)!empty($workshop->phaseswitchassessment); 175 176 if (isset($workshop->gradinggradepass)) { 177 $workshop->gradinggradepass = (float)unformat_float($workshop->gradinggradepass); 178 } 179 180 if (isset($workshop->submissiongradepass)) { 181 $workshop->submissiongradepass = (float)unformat_float($workshop->submissiongradepass); 182 } 183 184 if (isset($workshop->submissionfiletypes)) { 185 $filetypesutil = new \core_form\filetypes_util(); 186 $submissionfiletypes = $filetypesutil->normalize_file_types($workshop->submissionfiletypes); 187 $workshop->submissionfiletypes = implode(' ', $submissionfiletypes); 188 } 189 190 if (isset($workshop->overallfeedbackfiletypes)) { 191 $filetypesutil = new \core_form\filetypes_util(); 192 $overallfeedbackfiletypes = $filetypesutil->normalize_file_types($workshop->overallfeedbackfiletypes); 193 $workshop->overallfeedbackfiletypes = implode(' ', $overallfeedbackfiletypes); 194 } 195 196 // todo - if the grading strategy is being changed, we may want to replace all aggregated peer grades with nulls 197 198 $DB->update_record('workshop', $workshop); 199 $context = context_module::instance($workshop->coursemodule); 200 201 // process the custom wysiwyg editors 202 if ($draftitemid = $workshop->instructauthorseditor['itemid']) { 203 $workshop->instructauthors = file_save_draft_area_files($draftitemid, $context->id, 'mod_workshop', 'instructauthors', 204 0, workshop::instruction_editors_options($context), $workshop->instructauthorseditor['text']); 205 $workshop->instructauthorsformat = $workshop->instructauthorseditor['format']; 206 } 207 208 if ($draftitemid = $workshop->instructreviewerseditor['itemid']) { 209 $workshop->instructreviewers = file_save_draft_area_files($draftitemid, $context->id, 'mod_workshop', 'instructreviewers', 210 0, workshop::instruction_editors_options($context), $workshop->instructreviewerseditor['text']); 211 $workshop->instructreviewersformat = $workshop->instructreviewerseditor['format']; 212 } 213 214 if ($draftitemid = $workshop->conclusioneditor['itemid']) { 215 $workshop->conclusion = file_save_draft_area_files($draftitemid, $context->id, 'mod_workshop', 'conclusion', 216 0, workshop::instruction_editors_options($context), $workshop->conclusioneditor['text']); 217 $workshop->conclusionformat = $workshop->conclusioneditor['format']; 218 } 219 220 // re-save the record with the replaced URLs in editor fields 221 $DB->update_record('workshop', $workshop); 222 223 // update gradebook items 224 workshop_grade_item_update($workshop); 225 workshop_grade_item_category_update($workshop); 226 227 // update calendar events 228 workshop_calendar_update($workshop, $workshop->coursemodule); 229 $completionexpected = (!empty($workshop->completionexpected)) ? $workshop->completionexpected : null; 230 \core_completion\api::update_completion_date_event($workshop->coursemodule, 'workshop', $workshop->id, $completionexpected); 231 232 return true; 233 } 234 235 /** 236 * Given an ID of an instance of this module, 237 * this function will permanently delete the instance 238 * and any data that depends on it. 239 * 240 * @param int $id Id of the module instance 241 * @return boolean Success/Failure 242 */ 243 function workshop_delete_instance($id) { 244 global $CFG, $DB; 245 require_once($CFG->libdir.'/gradelib.php'); 246 247 if (! $workshop = $DB->get_record('workshop', array('id' => $id))) { 248 return false; 249 } 250 251 // delete all associated aggregations 252 $DB->delete_records('workshop_aggregations', array('workshopid' => $workshop->id)); 253 254 // get the list of ids of all submissions 255 $submissions = $DB->get_records('workshop_submissions', array('workshopid' => $workshop->id), '', 'id'); 256 257 // get the list of all allocated assessments 258 $assessments = $DB->get_records_list('workshop_assessments', 'submissionid', array_keys($submissions), '', 'id'); 259 260 // delete the associated records from the workshop core tables 261 $DB->delete_records_list('workshop_grades', 'assessmentid', array_keys($assessments)); 262 $DB->delete_records_list('workshop_assessments', 'id', array_keys($assessments)); 263 $DB->delete_records_list('workshop_submissions', 'id', array_keys($submissions)); 264 265 // call the static clean-up methods of all available subplugins 266 $strategies = core_component::get_plugin_list('workshopform'); 267 foreach ($strategies as $strategy => $path) { 268 require_once ($path.'/lib.php'); 269 $classname = 'workshop_'.$strategy.'_strategy'; 270 call_user_func($classname.'::delete_instance', $workshop->id); 271 } 272 273 $allocators = core_component::get_plugin_list('workshopallocation'); 274 foreach ($allocators as $allocator => $path) { 275 require_once ($path.'/lib.php'); 276 $classname = 'workshop_'.$allocator.'_allocator'; 277 call_user_func($classname.'::delete_instance', $workshop->id); 278 } 279 280 $evaluators = core_component::get_plugin_list('workshopeval'); 281 foreach ($evaluators as $evaluator => $path) { 282 require_once ($path.'/lib.php'); 283 $classname = 'workshop_'.$evaluator.'_evaluation'; 284 call_user_func($classname.'::delete_instance', $workshop->id); 285 } 286 287 // delete the calendar events 288 $events = $DB->get_records('event', array('modulename' => 'workshop', 'instance' => $workshop->id)); 289 foreach ($events as $event) { 290 $event = calendar_event::load($event); 291 $event->delete(); 292 } 293 294 // gradebook cleanup 295 grade_update('mod/workshop', $workshop->course, 'mod', 'workshop', $workshop->id, 0, null, array('deleted' => true)); 296 grade_update('mod/workshop', $workshop->course, 'mod', 'workshop', $workshop->id, 1, null, array('deleted' => true)); 297 298 // finally remove the workshop record itself 299 // We must delete the module record after we delete the grade item. 300 $DB->delete_records('workshop', array('id' => $workshop->id)); 301 302 return true; 303 } 304 305 /** 306 * This standard function will check all instances of this module 307 * and make sure there are up-to-date events created for each of them. 308 * If courseid = 0, then every workshop event in the site is checked, else 309 * only workshop events belonging to the course specified are checked. 310 * 311 * @param integer $courseid The Course ID. 312 * @param int|stdClass $instance workshop module instance or ID. 313 * @param int|stdClass $cm Course module object or ID. 314 * @return bool Returns true if the calendar events were successfully updated. 315 */ 316 function workshop_refresh_events($courseid = 0, $instance = null, $cm = null) { 317 global $DB; 318 319 // If we have instance information then we can just update the one event instead of updating all events. 320 if (isset($instance)) { 321 if (!is_object($instance)) { 322 $instance = $DB->get_record('workshop', array('id' => $instance), '*', MUST_EXIST); 323 } 324 if (isset($cm)) { 325 if (!is_object($cm)) { 326 $cm = (object)array('id' => $cm); 327 } 328 } else { 329 $cm = get_coursemodule_from_instance('workshop', $instance->id); 330 } 331 workshop_calendar_update($instance, $cm->id); 332 return true; 333 } 334 335 if ($courseid) { 336 // Make sure that the course id is numeric. 337 if (!is_numeric($courseid)) { 338 return false; 339 } 340 if (!$workshops = $DB->get_records('workshop', array('course' => $courseid))) { 341 return false; 342 } 343 } else { 344 if (!$workshops = $DB->get_records('workshop')) { 345 return false; 346 } 347 } 348 foreach ($workshops as $workshop) { 349 if (!$cm = get_coursemodule_from_instance('workshop', $workshop->id, $courseid, false)) { 350 continue; 351 } 352 workshop_calendar_update($workshop, $cm->id); 353 } 354 return true; 355 } 356 357 /** 358 * List the actions that correspond to a view of this module. 359 * This is used by the participation report. 360 * 361 * Note: This is not used by new logging system. Event with 362 * crud = 'r' and edulevel = LEVEL_PARTICIPATING will 363 * be considered as view action. 364 * 365 * @return array 366 */ 367 function workshop_get_view_actions() { 368 return array('view', 'view all', 'view submission', 'view example'); 369 } 370 371 /** 372 * List the actions that correspond to a post of this module. 373 * This is used by the participation report. 374 * 375 * Note: This is not used by new logging system. Event with 376 * crud = ('c' || 'u' || 'd') and edulevel = LEVEL_PARTICIPATING 377 * will be considered as post action. 378 * 379 * @return array 380 */ 381 function workshop_get_post_actions() { 382 return array('add', 'add assessment', 'add example', 'add submission', 383 'update', 'update assessment', 'update example', 'update submission'); 384 } 385 386 /** 387 * Return a small object with summary information about what a 388 * user has done with a given particular instance of this module 389 * Used for user activity reports. 390 * $return->time = the time they did it 391 * $return->info = a short text description 392 * 393 * @param stdClass $course The course record. 394 * @param stdClass $user The user record. 395 * @param cm_info|stdClass $mod The course module info object or record. 396 * @param stdClass $workshop The workshop instance record. 397 * @return stdclass|null 398 */ 399 function workshop_user_outline($course, $user, $mod, $workshop) { 400 global $CFG, $DB; 401 require_once($CFG->libdir.'/gradelib.php'); 402 403 $grades = grade_get_grades($course->id, 'mod', 'workshop', $workshop->id, $user->id); 404 405 $submissiongrade = null; 406 $assessmentgrade = null; 407 408 $info = ''; 409 $time = 0; 410 411 if (!empty($grades->items[0]->grades)) { 412 $submissiongrade = reset($grades->items[0]->grades); 413 $time = max($time, $submissiongrade->dategraded); 414 if (!$submissiongrade->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) { 415 $info .= get_string('submissiongrade', 'workshop') . ': ' . $submissiongrade->str_long_grade 416 . html_writer::empty_tag('br'); 417 } else { 418 $info .= get_string('submissiongrade', 'workshop') . ': ' . get_string('hidden', 'grades') 419 . html_writer::empty_tag('br'); 420 } 421 } 422 if (!empty($grades->items[1]->grades)) { 423 $assessmentgrade = reset($grades->items[1]->grades); 424 $time = max($time, $assessmentgrade->dategraded); 425 if (!$assessmentgrade->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) { 426 $info .= get_string('gradinggrade', 'workshop') . ': ' . $assessmentgrade->str_long_grade; 427 } else { 428 $info .= get_string('gradinggrade', 'workshop') . ': ' . get_string('hidden', 'grades'); 429 } 430 } 431 432 if (!empty($info) and !empty($time)) { 433 $return = new stdclass(); 434 $return->time = $time; 435 $return->info = $info; 436 return $return; 437 } 438 439 return null; 440 } 441 442 /** 443 * Print a detailed representation of what a user has done with 444 * a given particular instance of this module, for user activity reports. 445 * 446 * @param stdClass $course The course record. 447 * @param stdClass $user The user record. 448 * @param cm_info|stdClass $mod The course module info object or record. 449 * @param stdClass $workshop The workshop instance record. 450 * @return string HTML 451 */ 452 function workshop_user_complete($course, $user, $mod, $workshop) { 453 global $CFG, $DB, $OUTPUT; 454 require_once (__DIR__.'/locallib.php'); 455 require_once($CFG->libdir.'/gradelib.php'); 456 457 $workshop = new workshop($workshop, $mod, $course); 458 $grades = grade_get_grades($course->id, 'mod', 'workshop', $workshop->id, $user->id); 459 460 if (!empty($grades->items[0]->grades)) { 461 $submissiongrade = reset($grades->items[0]->grades); 462 if (!$submissiongrade->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) { 463 $info = get_string('submissiongrade', 'workshop') . ': ' . $submissiongrade->str_long_grade; 464 } else { 465 $info = get_string('submissiongrade', 'workshop') . ': ' . get_string('hidden', 'grades'); 466 } 467 echo html_writer::tag('li', $info, array('class'=>'submissiongrade')); 468 } 469 if (!empty($grades->items[1]->grades)) { 470 $assessmentgrade = reset($grades->items[1]->grades); 471 if (!$assessmentgrade->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) { 472 $info = get_string('gradinggrade', 'workshop') . ': ' . $assessmentgrade->str_long_grade; 473 } else { 474 $info = get_string('gradinggrade', 'workshop') . ': ' . get_string('hidden', 'grades'); 475 } 476 echo html_writer::tag('li', $info, array('class'=>'gradinggrade')); 477 } 478 479 if (has_capability('mod/workshop:viewallsubmissions', $workshop->context)) { 480 $canviewsubmission = true; 481 if (groups_get_activity_groupmode($workshop->cm) == SEPARATEGROUPS) { 482 // user must have accessallgroups or share at least one group with the submission author 483 if (!has_capability('moodle/site:accessallgroups', $workshop->context)) { 484 $usersgroups = groups_get_activity_allowed_groups($workshop->cm); 485 $authorsgroups = groups_get_all_groups($workshop->course->id, $user->id, $workshop->cm->groupingid, 'g.id'); 486 $sharedgroups = array_intersect_key($usersgroups, $authorsgroups); 487 if (empty($sharedgroups)) { 488 $canviewsubmission = false; 489 } 490 } 491 } 492 if ($canviewsubmission and $submission = $workshop->get_submission_by_author($user->id)) { 493 $title = format_string($submission->title); 494 $url = $workshop->submission_url($submission->id); 495 $link = html_writer::link($url, $title); 496 $info = get_string('submission', 'workshop').': '.$link; 497 echo html_writer::tag('li', $info, array('class'=>'submission')); 498 } 499 } 500 501 if (has_capability('mod/workshop:viewallassessments', $workshop->context)) { 502 if ($assessments = $workshop->get_assessments_by_reviewer($user->id)) { 503 foreach ($assessments as $assessment) { 504 $a = new stdclass(); 505 $a->submissionurl = $workshop->submission_url($assessment->submissionid)->out(); 506 $a->assessmenturl = $workshop->assess_url($assessment->id)->out(); 507 $a->submissiontitle = s($assessment->submissiontitle); 508 echo html_writer::tag('li', get_string('assessmentofsubmission', 'workshop', $a)); 509 } 510 } 511 } 512 } 513 514 /** 515 * Given a course and a time, this module should find recent activity 516 * that has occurred in workshop activities and print it out. 517 * Return true if there was output, or false is there was none. 518 * 519 * @param stdClass $course 520 * @param bool $viewfullnames 521 * @param int $timestart 522 * @return boolean 523 */ 524 function workshop_print_recent_activity($course, $viewfullnames, $timestart) { 525 global $CFG, $USER, $DB, $OUTPUT; 526 527 $userfieldsapi = \core_user\fields::for_name(); 528 $authoramefields = $userfieldsapi->get_sql('author', false, 'author', '', false)->selects; 529 $reviewerfields = $userfieldsapi->get_sql('reviewer', false, 'reviewer', '', false)->selects; 530 531 $sql = "SELECT s.id AS submissionid, s.title AS submissiontitle, s.timemodified AS submissionmodified, 532 author.id AS authorid, $authoramefields, a.id AS assessmentid, a.timemodified AS assessmentmodified, 533 reviewer.id AS reviewerid, $reviewerfields, cm.id AS cmid 534 FROM {workshop} w 535 INNER JOIN {course_modules} cm ON cm.instance = w.id 536 INNER JOIN {modules} md ON md.id = cm.module 537 INNER JOIN {workshop_submissions} s ON s.workshopid = w.id 538 INNER JOIN {user} author ON s.authorid = author.id 539 LEFT JOIN {workshop_assessments} a ON a.submissionid = s.id 540 LEFT JOIN {user} reviewer ON a.reviewerid = reviewer.id 541 WHERE cm.course = ? 542 AND md.name = 'workshop' 543 AND s.example = 0 544 AND (s.timemodified > ? OR a.timemodified > ?) 545 ORDER BY s.timemodified"; 546 547 $rs = $DB->get_recordset_sql($sql, array($course->id, $timestart, $timestart)); 548 549 $modinfo = get_fast_modinfo($course); // reference needed because we might load the groups 550 551 $submissions = array(); // recent submissions indexed by submission id 552 $assessments = array(); // recent assessments indexed by assessment id 553 $users = array(); 554 555 foreach ($rs as $activity) { 556 if (!array_key_exists($activity->cmid, $modinfo->cms)) { 557 // this should not happen but just in case 558 continue; 559 } 560 561 $cm = $modinfo->cms[$activity->cmid]; 562 if (!$cm->uservisible) { 563 continue; 564 } 565 566 // remember all user names we can use later 567 if (empty($users[$activity->authorid])) { 568 $u = new stdclass(); 569 $users[$activity->authorid] = username_load_fields_from_object($u, $activity, 'author'); 570 } 571 if ($activity->reviewerid and empty($users[$activity->reviewerid])) { 572 $u = new stdclass(); 573 $users[$activity->reviewerid] = username_load_fields_from_object($u, $activity, 'reviewer'); 574 } 575 576 $context = context_module::instance($cm->id); 577 $groupmode = groups_get_activity_groupmode($cm, $course); 578 579 if ($activity->submissionmodified > $timestart and empty($submissions[$activity->submissionid])) { 580 $s = new stdclass(); 581 $s->title = $activity->submissiontitle; 582 $s->authorid = $activity->authorid; 583 $s->timemodified = $activity->submissionmodified; 584 $s->cmid = $activity->cmid; 585 if ($activity->authorid == $USER->id || has_capability('mod/workshop:viewauthornames', $context)) { 586 $s->authornamevisible = true; 587 } else { 588 $s->authornamevisible = false; 589 } 590 591 // the following do-while wrapper allows to break from deeply nested if-statements 592 do { 593 if ($s->authorid === $USER->id) { 594 // own submissions always visible 595 $submissions[$activity->submissionid] = $s; 596 break; 597 } 598 599 if (has_capability('mod/workshop:viewallsubmissions', $context)) { 600 if ($groupmode == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) { 601 if (isguestuser()) { 602 // shortcut - guest user does not belong into any group 603 break; 604 } 605 606 // this might be slow - show only submissions by users who share group with me in this cm 607 if (!$modinfo->get_groups($cm->groupingid)) { 608 break; 609 } 610 $authorsgroups = groups_get_all_groups($course->id, $s->authorid, $cm->groupingid); 611 if (is_array($authorsgroups)) { 612 $authorsgroups = array_keys($authorsgroups); 613 $intersect = array_intersect($authorsgroups, $modinfo->get_groups($cm->groupingid)); 614 if (empty($intersect)) { 615 break; 616 } else { 617 // can see all submissions and shares a group with the author 618 $submissions[$activity->submissionid] = $s; 619 break; 620 } 621 } 622 623 } else { 624 // can see all submissions from all groups 625 $submissions[$activity->submissionid] = $s; 626 } 627 } 628 } while (0); 629 } 630 631 if ($activity->assessmentmodified > $timestart and empty($assessments[$activity->assessmentid])) { 632 $a = new stdclass(); 633 $a->submissionid = $activity->submissionid; 634 $a->submissiontitle = $activity->submissiontitle; 635 $a->reviewerid = $activity->reviewerid; 636 $a->timemodified = $activity->assessmentmodified; 637 $a->cmid = $activity->cmid; 638 if ($activity->reviewerid == $USER->id || has_capability('mod/workshop:viewreviewernames', $context)) { 639 $a->reviewernamevisible = true; 640 } else { 641 $a->reviewernamevisible = false; 642 } 643 644 // the following do-while wrapper allows to break from deeply nested if-statements 645 do { 646 if ($a->reviewerid === $USER->id) { 647 // own assessments always visible 648 $assessments[$activity->assessmentid] = $a; 649 break; 650 } 651 652 if (has_capability('mod/workshop:viewallassessments', $context)) { 653 if ($groupmode == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) { 654 if (isguestuser()) { 655 // shortcut - guest user does not belong into any group 656 break; 657 } 658 659 // this might be slow - show only submissions by users who share group with me in this cm 660 if (!$modinfo->get_groups($cm->groupingid)) { 661 break; 662 } 663 $reviewersgroups = groups_get_all_groups($course->id, $a->reviewerid, $cm->groupingid); 664 if (is_array($reviewersgroups)) { 665 $reviewersgroups = array_keys($reviewersgroups); 666 $intersect = array_intersect($reviewersgroups, $modinfo->get_groups($cm->groupingid)); 667 if (empty($intersect)) { 668 break; 669 } else { 670 // can see all assessments and shares a group with the reviewer 671 $assessments[$activity->assessmentid] = $a; 672 break; 673 } 674 } 675 676 } else { 677 // can see all assessments from all groups 678 $assessments[$activity->assessmentid] = $a; 679 } 680 } 681 } while (0); 682 } 683 } 684 $rs->close(); 685 686 $shown = false; 687 688 if (!empty($submissions)) { 689 $shown = true; 690 echo $OUTPUT->heading(get_string('recentsubmissions', 'workshop') . ':', 6); 691 foreach ($submissions as $id => $submission) { 692 $link = new moodle_url('/mod/workshop/submission.php', array('id'=>$id, 'cmid'=>$submission->cmid)); 693 if ($submission->authornamevisible) { 694 $author = $users[$submission->authorid]; 695 } else { 696 $author = null; 697 } 698 print_recent_activity_note($submission->timemodified, $author, $submission->title, $link->out(), false, $viewfullnames); 699 } 700 } 701 702 if (!empty($assessments)) { 703 $shown = true; 704 echo $OUTPUT->heading(get_string('recentassessments', 'workshop') . ':', 6); 705 core_collator::asort_objects_by_property($assessments, 'timemodified'); 706 foreach ($assessments as $id => $assessment) { 707 $link = new moodle_url('/mod/workshop/assessment.php', array('asid' => $id)); 708 if ($assessment->reviewernamevisible) { 709 $reviewer = $users[$assessment->reviewerid]; 710 } else { 711 $reviewer = null; 712 } 713 print_recent_activity_note($assessment->timemodified, $reviewer, $assessment->submissiontitle, $link->out(), false, $viewfullnames); 714 } 715 } 716 717 if ($shown) { 718 return true; 719 } 720 721 return false; 722 } 723 724 /** 725 * Returns all activity in course workshops since a given time 726 * 727 * @param array $activities sequentially indexed array of objects 728 * @param int $index 729 * @param int $timestart 730 * @param int $courseid 731 * @param int $cmid 732 * @param int $userid defaults to 0 733 * @param int $groupid defaults to 0 734 * @return void adds items into $activities and increases $index 735 */ 736 function workshop_get_recent_mod_activity(&$activities, &$index, $timestart, $courseid, $cmid, $userid=0, $groupid=0) { 737 global $CFG, $COURSE, $USER, $DB; 738 739 if ($COURSE->id == $courseid) { 740 $course = $COURSE; 741 } else { 742 $course = $DB->get_record('course', array('id'=>$courseid)); 743 } 744 745 $modinfo = get_fast_modinfo($course); 746 747 $cm = $modinfo->cms[$cmid]; 748 749 $params = array(); 750 if ($userid) { 751 $userselect = "AND (author.id = :authorid OR reviewer.id = :reviewerid)"; 752 $params['authorid'] = $userid; 753 $params['reviewerid'] = $userid; 754 } else { 755 $userselect = ""; 756 } 757 758 if ($groupid) { 759 $groupselect = "AND (authorgroupmembership.groupid = :authorgroupid OR reviewergroupmembership.groupid = :reviewergroupid)"; 760 $groupjoin = "LEFT JOIN {groups_members} authorgroupmembership ON authorgroupmembership.userid = author.id 761 LEFT JOIN {groups_members} reviewergroupmembership ON reviewergroupmembership.userid = reviewer.id"; 762 $params['authorgroupid'] = $groupid; 763 $params['reviewergroupid'] = $groupid; 764 } else { 765 $groupselect = ""; 766 $groupjoin = ""; 767 } 768 769 $params['cminstance'] = $cm->instance; 770 $params['submissionmodified'] = $timestart; 771 $params['assessmentmodified'] = $timestart; 772 773 $userfieldsapi = \core_user\fields::for_name(); 774 $authornamefields = $userfieldsapi->get_sql('author', false, 'author', '', false)->selects; 775 $reviewerfields = $userfieldsapi->get_sql('reviewer', false, 'reviewer', '', false)->selects; 776 777 $sql = "SELECT s.id AS submissionid, s.title AS submissiontitle, s.timemodified AS submissionmodified, 778 author.id AS authorid, $authornamefields, author.picture AS authorpicture, author.imagealt AS authorimagealt, 779 author.email AS authoremail, a.id AS assessmentid, a.timemodified AS assessmentmodified, 780 reviewer.id AS reviewerid, $reviewerfields, reviewer.picture AS reviewerpicture, 781 reviewer.imagealt AS reviewerimagealt, reviewer.email AS revieweremail 782 FROM {workshop_submissions} s 783 INNER JOIN {workshop} w ON s.workshopid = w.id 784 INNER JOIN {user} author ON s.authorid = author.id 785 LEFT JOIN {workshop_assessments} a ON a.submissionid = s.id 786 LEFT JOIN {user} reviewer ON a.reviewerid = reviewer.id 787 $groupjoin 788 WHERE w.id = :cminstance 789 AND s.example = 0 790 $userselect $groupselect 791 AND (s.timemodified > :submissionmodified OR a.timemodified > :assessmentmodified) 792 ORDER BY s.timemodified ASC, a.timemodified ASC"; 793 794 $rs = $DB->get_recordset_sql($sql, $params); 795 796 $groupmode = groups_get_activity_groupmode($cm, $course); 797 $context = context_module::instance($cm->id); 798 $grader = has_capability('moodle/grade:viewall', $context); 799 $accessallgroups = has_capability('moodle/site:accessallgroups', $context); 800 $viewauthors = has_capability('mod/workshop:viewauthornames', $context); 801 $viewreviewers = has_capability('mod/workshop:viewreviewernames', $context); 802 803 $submissions = array(); // recent submissions indexed by submission id 804 $assessments = array(); // recent assessments indexed by assessment id 805 $users = array(); 806 807 foreach ($rs as $activity) { 808 809 // remember all user names we can use later 810 if (empty($users[$activity->authorid])) { 811 $u = new stdclass(); 812 $additionalfields = explode(',', implode(',', \core_user\fields::get_picture_fields())); 813 $u = username_load_fields_from_object($u, $activity, 'author', $additionalfields); 814 $users[$activity->authorid] = $u; 815 } 816 if ($activity->reviewerid and empty($users[$activity->reviewerid])) { 817 $u = new stdclass(); 818 $additionalfields = explode(',', implode(',', \core_user\fields::get_picture_fields())); 819 $u = username_load_fields_from_object($u, $activity, 'reviewer', $additionalfields); 820 $users[$activity->reviewerid] = $u; 821 } 822 823 if ($activity->submissionmodified > $timestart and empty($submissions[$activity->submissionid])) { 824 $s = new stdclass(); 825 $s->id = $activity->submissionid; 826 $s->title = $activity->submissiontitle; 827 $s->authorid = $activity->authorid; 828 $s->timemodified = $activity->submissionmodified; 829 if ($activity->authorid == $USER->id || has_capability('mod/workshop:viewauthornames', $context)) { 830 $s->authornamevisible = true; 831 } else { 832 $s->authornamevisible = false; 833 } 834 835 // the following do-while wrapper allows to break from deeply nested if-statements 836 do { 837 if ($s->authorid === $USER->id) { 838 // own submissions always visible 839 $submissions[$activity->submissionid] = $s; 840 break; 841 } 842 843 if (has_capability('mod/workshop:viewallsubmissions', $context)) { 844 if ($groupmode == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) { 845 if (isguestuser()) { 846 // shortcut - guest user does not belong into any group 847 break; 848 } 849 850 // this might be slow - show only submissions by users who share group with me in this cm 851 if (!$modinfo->get_groups($cm->groupingid)) { 852 break; 853 } 854 $authorsgroups = groups_get_all_groups($course->id, $s->authorid, $cm->groupingid); 855 if (is_array($authorsgroups)) { 856 $authorsgroups = array_keys($authorsgroups); 857 $intersect = array_intersect($authorsgroups, $modinfo->get_groups($cm->groupingid)); 858 if (empty($intersect)) { 859 break; 860 } else { 861 // can see all submissions and shares a group with the author 862 $submissions[$activity->submissionid] = $s; 863 break; 864 } 865 } 866 867 } else { 868 // can see all submissions from all groups 869 $submissions[$activity->submissionid] = $s; 870 } 871 } 872 } while (0); 873 } 874 875 if ($activity->assessmentmodified > $timestart and empty($assessments[$activity->assessmentid])) { 876 $a = new stdclass(); 877 $a->id = $activity->assessmentid; 878 $a->submissionid = $activity->submissionid; 879 $a->submissiontitle = $activity->submissiontitle; 880 $a->reviewerid = $activity->reviewerid; 881 $a->timemodified = $activity->assessmentmodified; 882 if ($activity->reviewerid == $USER->id || has_capability('mod/workshop:viewreviewernames', $context)) { 883 $a->reviewernamevisible = true; 884 } else { 885 $a->reviewernamevisible = false; 886 } 887 888 // the following do-while wrapper allows to break from deeply nested if-statements 889 do { 890 if ($a->reviewerid === $USER->id) { 891 // own assessments always visible 892 $assessments[$activity->assessmentid] = $a; 893 break; 894 } 895 896 if (has_capability('mod/workshop:viewallassessments', $context)) { 897 if ($groupmode == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) { 898 if (isguestuser()) { 899 // shortcut - guest user does not belong into any group 900 break; 901 } 902 903 // this might be slow - show only submissions by users who share group with me in this cm 904 if (!$modinfo->get_groups($cm->groupingid)) { 905 break; 906 } 907 $reviewersgroups = groups_get_all_groups($course->id, $a->reviewerid, $cm->groupingid); 908 if (is_array($reviewersgroups)) { 909 $reviewersgroups = array_keys($reviewersgroups); 910 $intersect = array_intersect($reviewersgroups, $modinfo->get_groups($cm->groupingid)); 911 if (empty($intersect)) { 912 break; 913 } else { 914 // can see all assessments and shares a group with the reviewer 915 $assessments[$activity->assessmentid] = $a; 916 break; 917 } 918 } 919 920 } else { 921 // can see all assessments from all groups 922 $assessments[$activity->assessmentid] = $a; 923 } 924 } 925 } while (0); 926 } 927 } 928 $rs->close(); 929 930 $workshopname = format_string($cm->name, true); 931 932 if ($grader) { 933 require_once($CFG->libdir.'/gradelib.php'); 934 $grades = grade_get_grades($courseid, 'mod', 'workshop', $cm->instance, array_keys($users)); 935 } 936 937 foreach ($submissions as $submission) { 938 $tmpactivity = new stdclass(); 939 $tmpactivity->type = 'workshop'; 940 $tmpactivity->cmid = $cm->id; 941 $tmpactivity->name = $workshopname; 942 $tmpactivity->sectionnum = $cm->sectionnum; 943 $tmpactivity->timestamp = $submission->timemodified; 944 $tmpactivity->subtype = 'submission'; 945 $tmpactivity->content = $submission; 946 if ($grader) { 947 $tmpactivity->grade = $grades->items[0]->grades[$submission->authorid]->str_long_grade; 948 } 949 if ($submission->authornamevisible and !empty($users[$submission->authorid])) { 950 $tmpactivity->user = $users[$submission->authorid]; 951 } 952 $activities[$index++] = $tmpactivity; 953 } 954 955 foreach ($assessments as $assessment) { 956 $tmpactivity = new stdclass(); 957 $tmpactivity->type = 'workshop'; 958 $tmpactivity->cmid = $cm->id; 959 $tmpactivity->name = $workshopname; 960 $tmpactivity->sectionnum = $cm->sectionnum; 961 $tmpactivity->timestamp = $assessment->timemodified; 962 $tmpactivity->subtype = 'assessment'; 963 $tmpactivity->content = $assessment; 964 if ($grader) { 965 $tmpactivity->grade = $grades->items[1]->grades[$assessment->reviewerid]->str_long_grade; 966 } 967 if ($assessment->reviewernamevisible and !empty($users[$assessment->reviewerid])) { 968 $tmpactivity->user = $users[$assessment->reviewerid]; 969 } 970 $activities[$index++] = $tmpactivity; 971 } 972 } 973 974 /** 975 * Print single activity item prepared by {@see workshop_get_recent_mod_activity()} 976 */ 977 function workshop_print_recent_mod_activity($activity, $courseid, $detail, $modnames, $viewfullnames) { 978 global $CFG, $OUTPUT; 979 980 if (!empty($activity->user)) { 981 echo html_writer::tag('div', $OUTPUT->user_picture($activity->user, array('courseid'=>$courseid)), 982 array('style' => 'float: left; padding: 7px;')); 983 } 984 985 if ($activity->subtype == 'submission') { 986 echo html_writer::start_tag('div', array('class'=>'submission', 'style'=>'padding: 7px; float:left;')); 987 988 if ($detail) { 989 echo html_writer::start_tag('h4', array('class'=>'workshop')); 990 $url = new moodle_url('/mod/workshop/view.php', array('id'=>$activity->cmid)); 991 $name = s($activity->name); 992 echo $OUTPUT->image_icon('monologo', $name, $activity->type); 993 echo ' ' . $modnames[$activity->type]; 994 echo html_writer::link($url, $name, array('class'=>'name', 'style'=>'margin-left: 5px')); 995 echo html_writer::end_tag('h4'); 996 } 997 998 echo html_writer::start_tag('div', array('class'=>'title')); 999 $url = new moodle_url('/mod/workshop/submission.php', array('cmid'=>$activity->cmid, 'id'=>$activity->content->id)); 1000 $name = s($activity->content->title); 1001 echo html_writer::tag('strong', html_writer::link($url, $name)); 1002 echo html_writer::end_tag('div'); 1003 1004 if (!empty($activity->user)) { 1005 echo html_writer::start_tag('div', array('class'=>'user')); 1006 $url = new moodle_url('/user/view.php', array('id'=>$activity->user->id, 'course'=>$courseid)); 1007 $name = fullname($activity->user); 1008 $link = html_writer::link($url, $name); 1009 echo get_string('submissionby', 'workshop', $link); 1010 echo ' - '.userdate($activity->timestamp); 1011 echo html_writer::end_tag('div'); 1012 } else { 1013 echo html_writer::start_tag('div', array('class'=>'anonymous')); 1014 echo get_string('submission', 'workshop'); 1015 echo ' - '.userdate($activity->timestamp); 1016 echo html_writer::end_tag('div'); 1017 } 1018 1019 echo html_writer::end_tag('div'); 1020 } 1021 1022 if ($activity->subtype == 'assessment') { 1023 echo html_writer::start_tag('div', array('class'=>'assessment', 'style'=>'padding: 7px; float:left;')); 1024 1025 if ($detail) { 1026 echo html_writer::start_tag('h4', array('class'=>'workshop')); 1027 $url = new moodle_url('/mod/workshop/view.php', array('id'=>$activity->cmid)); 1028 $name = s($activity->name); 1029 echo $OUTPUT->image_icon('monologo', $name, $activity->type); 1030 echo ' ' . $modnames[$activity->type]; 1031 echo html_writer::link($url, $name, array('class'=>'name', 'style'=>'margin-left: 5px')); 1032 echo html_writer::end_tag('h4'); 1033 } 1034 1035 echo html_writer::start_tag('div', array('class'=>'title')); 1036 $url = new moodle_url('/mod/workshop/assessment.php', array('asid'=>$activity->content->id)); 1037 $name = s($activity->content->submissiontitle); 1038 echo html_writer::tag('em', html_writer::link($url, $name)); 1039 echo html_writer::end_tag('div'); 1040 1041 if (!empty($activity->user)) { 1042 echo html_writer::start_tag('div', array('class'=>'user')); 1043 $url = new moodle_url('/user/view.php', array('id'=>$activity->user->id, 'course'=>$courseid)); 1044 $name = fullname($activity->user); 1045 $link = html_writer::link($url, $name); 1046 echo get_string('assessmentbyfullname', 'workshop', $link); 1047 echo ' - '.userdate($activity->timestamp); 1048 echo html_writer::end_tag('div'); 1049 } else { 1050 echo html_writer::start_tag('div', array('class'=>'anonymous')); 1051 echo get_string('assessment', 'workshop'); 1052 echo ' - '.userdate($activity->timestamp); 1053 echo html_writer::end_tag('div'); 1054 } 1055 1056 echo html_writer::end_tag('div'); 1057 } 1058 1059 echo html_writer::empty_tag('br', array('style'=>'clear:both')); 1060 } 1061 1062 /** 1063 * @deprecated since Moodle 3.8 1064 */ 1065 function workshop_scale_used() { 1066 throw new coding_exception('workshop_scale_used() can not be used anymore. Plugins can implement ' . 1067 '<modname>_scale_used_anywhere, all implementations of <modname>_scale_used are now ignored'); 1068 } 1069 1070 /** 1071 * Is a given scale used by any instance of workshop? 1072 * 1073 * The function asks all installed grading strategy subplugins. The workshop 1074 * core itself does not use scales. Both grade for submission and grade for 1075 * assessments do not use scales. 1076 * 1077 * @param int $scaleid id of the scale to check 1078 * @return bool 1079 */ 1080 function workshop_scale_used_anywhere($scaleid) { 1081 global $CFG; // other files included from here 1082 1083 $strategies = core_component::get_plugin_list('workshopform'); 1084 foreach ($strategies as $strategy => $strategypath) { 1085 $strategylib = $strategypath . '/lib.php'; 1086 if (is_readable($strategylib)) { 1087 require_once($strategylib); 1088 } else { 1089 throw new coding_exception('the grading forms subplugin must contain library ' . $strategylib); 1090 } 1091 $classname = 'workshop_' . $strategy . '_strategy'; 1092 if (method_exists($classname, 'scale_used')) { 1093 if (call_user_func(array($classname, 'scale_used'), $scaleid)) { 1094 // no need to include any other files - scale is used 1095 return true; 1096 } 1097 } 1098 } 1099 1100 return false; 1101 } 1102 1103 //////////////////////////////////////////////////////////////////////////////// 1104 // Gradebook API // 1105 //////////////////////////////////////////////////////////////////////////////// 1106 1107 /** 1108 * Creates or updates grade items for the give workshop instance 1109 * 1110 * Needed by grade_update_mod_grades() in lib/gradelib.php. Also used by 1111 * {@link workshop_update_grades()}. 1112 * 1113 * @param stdClass $workshop instance object with extra cmidnumber property 1114 * @param stdClass $submissiongrades data for the first grade item 1115 * @param stdClass $assessmentgrades data for the second grade item 1116 * @return void 1117 */ 1118 function workshop_grade_item_update(stdclass $workshop, $submissiongrades=null, $assessmentgrades=null) { 1119 global $CFG; 1120 require_once($CFG->libdir.'/gradelib.php'); 1121 1122 $a = new stdclass(); 1123 $a->workshopname = clean_param($workshop->name, PARAM_NOTAGS); 1124 1125 $item = array(); 1126 $item['itemname'] = get_string('gradeitemsubmission', 'workshop', $a); 1127 $item['gradetype'] = GRADE_TYPE_VALUE; 1128 $item['grademax'] = $workshop->grade; 1129 $item['grademin'] = 0; 1130 grade_update('mod/workshop', $workshop->course, 'mod', 'workshop', $workshop->id, 0, $submissiongrades , $item); 1131 1132 $item = array(); 1133 $item['itemname'] = get_string('gradeitemassessment', 'workshop', $a); 1134 $item['gradetype'] = GRADE_TYPE_VALUE; 1135 $item['grademax'] = $workshop->gradinggrade; 1136 $item['grademin'] = 0; 1137 grade_update('mod/workshop', $workshop->course, 'mod', 'workshop', $workshop->id, 1, $assessmentgrades, $item); 1138 } 1139 1140 /** 1141 * Update workshop grades in the gradebook 1142 * 1143 * Needed by grade_update_mod_grades() in lib/gradelib.php 1144 * 1145 * @category grade 1146 * @param stdClass $workshop instance object with extra cmidnumber and modname property 1147 * @param int $userid update grade of specific user only, 0 means all participants 1148 * @return void 1149 */ 1150 function workshop_update_grades(stdclass $workshop, $userid=0) { 1151 global $CFG, $DB; 1152 require_once($CFG->libdir.'/gradelib.php'); 1153 1154 $whereuser = $userid ? ' AND authorid = :userid' : ''; 1155 $params = array('workshopid' => $workshop->id, 'userid' => $userid); 1156 $sql = 'SELECT authorid, grade, gradeover, gradeoverby, feedbackauthor, feedbackauthorformat, timemodified, timegraded 1157 FROM {workshop_submissions} 1158 WHERE workshopid = :workshopid AND example=0' . $whereuser; 1159 $records = $DB->get_records_sql($sql, $params); 1160 $submissiongrades = array(); 1161 foreach ($records as $record) { 1162 $grade = new stdclass(); 1163 $grade->userid = $record->authorid; 1164 if (!is_null($record->gradeover)) { 1165 $grade->rawgrade = grade_floatval($workshop->grade * $record->gradeover / 100); 1166 $grade->usermodified = $record->gradeoverby; 1167 } else { 1168 $grade->rawgrade = grade_floatval($workshop->grade * $record->grade / 100); 1169 } 1170 $grade->feedback = $record->feedbackauthor; 1171 $grade->feedbackformat = $record->feedbackauthorformat; 1172 $grade->datesubmitted = $record->timemodified; 1173 $grade->dategraded = $record->timegraded; 1174 $submissiongrades[$record->authorid] = $grade; 1175 } 1176 1177 $whereuser = $userid ? ' AND userid = :userid' : ''; 1178 $params = array('workshopid' => $workshop->id, 'userid' => $userid); 1179 $sql = 'SELECT userid, gradinggrade, timegraded 1180 FROM {workshop_aggregations} 1181 WHERE workshopid = :workshopid' . $whereuser; 1182 $records = $DB->get_records_sql($sql, $params); 1183 $assessmentgrades = array(); 1184 foreach ($records as $record) { 1185 $grade = new stdclass(); 1186 $grade->userid = $record->userid; 1187 $grade->rawgrade = grade_floatval($workshop->gradinggrade * $record->gradinggrade / 100); 1188 $grade->dategraded = $record->timegraded; 1189 $assessmentgrades[$record->userid] = $grade; 1190 } 1191 1192 workshop_grade_item_update($workshop, $submissiongrades, $assessmentgrades); 1193 } 1194 1195 /** 1196 * Update the grade items categories if they are changed via mod_form.php 1197 * 1198 * We must do it manually here in the workshop module because modedit supports only 1199 * single grade item while we use two. 1200 * 1201 * @param stdClass $workshop An object from the form in mod_form.php 1202 */ 1203 function workshop_grade_item_category_update($workshop) { 1204 1205 $gradeitems = grade_item::fetch_all(array( 1206 'itemtype' => 'mod', 1207 'itemmodule' => 'workshop', 1208 'iteminstance' => $workshop->id, 1209 'courseid' => $workshop->course)); 1210 1211 if (!empty($gradeitems)) { 1212 foreach ($gradeitems as $gradeitem) { 1213 if ($gradeitem->itemnumber == 0) { 1214 if (isset($workshop->submissiongradepass) && 1215 $gradeitem->gradepass != $workshop->submissiongradepass) { 1216 $gradeitem->gradepass = $workshop->submissiongradepass; 1217 $gradeitem->update(); 1218 } 1219 if ($gradeitem->categoryid != $workshop->gradecategory) { 1220 $gradeitem->set_parent($workshop->gradecategory); 1221 } 1222 } else if ($gradeitem->itemnumber == 1) { 1223 if (isset($workshop->gradinggradepass) && 1224 $gradeitem->gradepass != $workshop->gradinggradepass) { 1225 $gradeitem->gradepass = $workshop->gradinggradepass; 1226 $gradeitem->update(); 1227 } 1228 if ($gradeitem->categoryid != $workshop->gradinggradecategory) { 1229 $gradeitem->set_parent($workshop->gradinggradecategory); 1230 } 1231 } 1232 } 1233 } 1234 } 1235 1236 //////////////////////////////////////////////////////////////////////////////// 1237 // File API // 1238 //////////////////////////////////////////////////////////////////////////////// 1239 1240 /** 1241 * Returns the lists of all browsable file areas within the given module context 1242 * 1243 * The file area workshop_intro for the activity introduction field is added automatically 1244 * by {@link file_browser::get_file_info_context_module()} 1245 * 1246 * @package mod_workshop 1247 * @category files 1248 * 1249 * @param stdClass $course 1250 * @param stdClass $cm 1251 * @param stdClass $context 1252 * @return array of [(string)filearea] => (string)description 1253 */ 1254 function workshop_get_file_areas($course, $cm, $context) { 1255 $areas = array(); 1256 $areas['instructauthors'] = get_string('areainstructauthors', 'workshop'); 1257 $areas['instructreviewers'] = get_string('areainstructreviewers', 'workshop'); 1258 $areas['submission_content'] = get_string('areasubmissioncontent', 'workshop'); 1259 $areas['submission_attachment'] = get_string('areasubmissionattachment', 'workshop'); 1260 $areas['conclusion'] = get_string('areaconclusion', 'workshop'); 1261 $areas['overallfeedback_content'] = get_string('areaoverallfeedbackcontent', 'workshop'); 1262 $areas['overallfeedback_attachment'] = get_string('areaoverallfeedbackattachment', 'workshop'); 1263 1264 return $areas; 1265 } 1266 1267 /** 1268 * Serves the files from the workshop file areas 1269 * 1270 * Apart from module intro (handled by pluginfile.php automatically), workshop files may be 1271 * media inserted into submission content (like images) and submission attachments. For these two, 1272 * the fileareas submission_content and submission_attachment are used. 1273 * Besides that, areas instructauthors, instructreviewers and conclusion contain the media 1274 * embedded using the mod_form.php. 1275 * 1276 * @package mod_workshop 1277 * @category files 1278 * 1279 * @param stdClass $course the course object 1280 * @param stdClass $cm the course module object 1281 * @param stdClass $context the workshop's context 1282 * @param string $filearea the name of the file area 1283 * @param array $args extra arguments (itemid, path) 1284 * @param bool $forcedownload whether or not force download 1285 * @param array $options additional options affecting the file serving 1286 * @return bool false if the file not found, just send the file otherwise and do not return anything 1287 */ 1288 function workshop_pluginfile($course, $cm, $context, $filearea, array $args, $forcedownload, array $options=array()) { 1289 global $DB, $CFG, $USER; 1290 1291 if ($context->contextlevel != CONTEXT_MODULE) { 1292 return false; 1293 } 1294 1295 require_login($course, true, $cm); 1296 1297 if ($filearea === 'instructauthors' or $filearea === 'instructreviewers' or $filearea === 'conclusion') { 1298 // The $args are supposed to contain just the path, not the item id. 1299 $relativepath = implode('/', $args); 1300 $fullpath = "/$context->id/mod_workshop/$filearea/0/$relativepath"; 1301 1302 $fs = get_file_storage(); 1303 if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) { 1304 send_file_not_found(); 1305 } 1306 send_stored_file($file, null, 0, $forcedownload, $options); 1307 1308 } else if ($filearea === 'submission_content' or $filearea === 'submission_attachment') { 1309 $itemid = (int)array_shift($args); 1310 if (!$workshop = $DB->get_record('workshop', array('id' => $cm->instance))) { 1311 return false; 1312 } 1313 if (!$submission = $DB->get_record('workshop_submissions', array('id' => $itemid, 'workshopid' => $workshop->id))) { 1314 return false; 1315 } 1316 1317 // make sure the user is allowed to see the file 1318 if (empty($submission->example)) { 1319 if ($USER->id != $submission->authorid) { 1320 if ($submission->published == 1 and $workshop->phase == 50 1321 and has_capability('mod/workshop:viewpublishedsubmissions', $context)) { 1322 // Published submission, we can go (workshop does not take the group mode 1323 // into account in this case yet). 1324 } else if (!$DB->record_exists('workshop_assessments', array('submissionid' => $submission->id, 'reviewerid' => $USER->id))) { 1325 if (!has_capability('mod/workshop:viewallsubmissions', $context)) { 1326 send_file_not_found(); 1327 } else { 1328 $gmode = groups_get_activity_groupmode($cm, $course); 1329 if ($gmode == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) { 1330 // check there is at least one common group with both the $USER 1331 // and the submission author 1332 $sql = "SELECT 'x' 1333 FROM {workshop_submissions} s 1334 JOIN {user} a ON (a.id = s.authorid) 1335 JOIN {groups_members} agm ON (a.id = agm.userid) 1336 JOIN {user} u ON (u.id = ?) 1337 JOIN {groups_members} ugm ON (u.id = ugm.userid) 1338 WHERE s.example = 0 AND s.workshopid = ? AND s.id = ? AND agm.groupid = ugm.groupid"; 1339 $params = array($USER->id, $workshop->id, $submission->id); 1340 if (!$DB->record_exists_sql($sql, $params)) { 1341 send_file_not_found(); 1342 } 1343 } 1344 } 1345 } 1346 } 1347 } 1348 1349 $fs = get_file_storage(); 1350 $relativepath = implode('/', $args); 1351 $fullpath = "/$context->id/mod_workshop/$filearea/$itemid/$relativepath"; 1352 if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) { 1353 return false; 1354 } 1355 // finally send the file 1356 // these files are uploaded by students - forcing download for security reasons 1357 send_stored_file($file, 0, 0, true, $options); 1358 1359 } else if ($filearea === 'overallfeedback_content' or $filearea === 'overallfeedback_attachment') { 1360 $itemid = (int)array_shift($args); 1361 if (!$workshop = $DB->get_record('workshop', array('id' => $cm->instance))) { 1362 return false; 1363 } 1364 if (!$assessment = $DB->get_record('workshop_assessments', array('id' => $itemid))) { 1365 return false; 1366 } 1367 if (!$submission = $DB->get_record('workshop_submissions', array('id' => $assessment->submissionid, 'workshopid' => $workshop->id))) { 1368 return false; 1369 } 1370 1371 if ($USER->id == $assessment->reviewerid) { 1372 // Reviewers can always see their own files. 1373 } else if ($USER->id == $submission->authorid and $workshop->phase == 50) { 1374 // Authors can see the feedback once the workshop is closed. 1375 } else if (!empty($submission->example) and $assessment->weight == 1) { 1376 // Reference assessments of example submissions can be displayed. 1377 } else if (!has_capability('mod/workshop:viewallassessments', $context)) { 1378 send_file_not_found(); 1379 } else { 1380 $gmode = groups_get_activity_groupmode($cm, $course); 1381 if ($gmode == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) { 1382 // Check there is at least one common group with both the $USER 1383 // and the submission author. 1384 $sql = "SELECT 'x' 1385 FROM {workshop_submissions} s 1386 JOIN {user} a ON (a.id = s.authorid) 1387 JOIN {groups_members} agm ON (a.id = agm.userid) 1388 JOIN {user} u ON (u.id = ?) 1389 JOIN {groups_members} ugm ON (u.id = ugm.userid) 1390 WHERE s.example = 0 AND s.workshopid = ? AND s.id = ? AND agm.groupid = ugm.groupid"; 1391 $params = array($USER->id, $workshop->id, $submission->id); 1392 if (!$DB->record_exists_sql($sql, $params)) { 1393 send_file_not_found(); 1394 } 1395 } 1396 } 1397 1398 $fs = get_file_storage(); 1399 $relativepath = implode('/', $args); 1400 $fullpath = "/$context->id/mod_workshop/$filearea/$itemid/$relativepath"; 1401 if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) { 1402 return false; 1403 } 1404 // finally send the file 1405 // these files are uploaded by students - forcing download for security reasons 1406 send_stored_file($file, 0, 0, true, $options); 1407 } 1408 1409 return false; 1410 } 1411 1412 /** 1413 * File browsing support for workshop file areas 1414 * 1415 * @package mod_workshop 1416 * @category files 1417 * 1418 * @param file_browser $browser 1419 * @param array $areas 1420 * @param stdClass $course 1421 * @param stdClass $cm 1422 * @param stdClass $context 1423 * @param string $filearea 1424 * @param int $itemid 1425 * @param string $filepath 1426 * @param string $filename 1427 * @return file_info instance or null if not found 1428 */ 1429 function workshop_get_file_info($browser, $areas, $course, $cm, $context, $filearea, $itemid, $filepath, $filename) { 1430 global $CFG, $DB, $USER; 1431 1432 /** @var array internal cache for author names */ 1433 static $submissionauthors = array(); 1434 1435 $fs = get_file_storage(); 1436 1437 if ($filearea === 'submission_content' or $filearea === 'submission_attachment') { 1438 1439 if (!has_capability('mod/workshop:viewallsubmissions', $context)) { 1440 return null; 1441 } 1442 1443 if (is_null($itemid)) { 1444 // no itemid (submissionid) passed, display the list of all submissions 1445 require_once($CFG->dirroot . '/mod/workshop/fileinfolib.php'); 1446 return new workshop_file_info_submissions_container($browser, $course, $cm, $context, $areas, $filearea); 1447 } 1448 1449 // make sure the user can see the particular submission in separate groups mode 1450 $gmode = groups_get_activity_groupmode($cm, $course); 1451 1452 if ($gmode == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) { 1453 // check there is at least one common group with both the $USER 1454 // and the submission author (this is not expected to be a frequent 1455 // usecase so we can live with pretty ineffective one query per submission here...) 1456 $sql = "SELECT 'x' 1457 FROM {workshop_submissions} s 1458 JOIN {user} a ON (a.id = s.authorid) 1459 JOIN {groups_members} agm ON (a.id = agm.userid) 1460 JOIN {user} u ON (u.id = ?) 1461 JOIN {groups_members} ugm ON (u.id = ugm.userid) 1462 WHERE s.example = 0 AND s.workshopid = ? AND s.id = ? AND agm.groupid = ugm.groupid"; 1463 $params = array($USER->id, $cm->instance, $itemid); 1464 if (!$DB->record_exists_sql($sql, $params)) { 1465 return null; 1466 } 1467 } 1468 1469 // we are inside some particular submission container 1470 1471 $filepath = is_null($filepath) ? '/' : $filepath; 1472 $filename = is_null($filename) ? '.' : $filename; 1473 1474 if (!$storedfile = $fs->get_file($context->id, 'mod_workshop', $filearea, $itemid, $filepath, $filename)) { 1475 if ($filepath === '/' and $filename === '.') { 1476 $storedfile = new virtual_root_file($context->id, 'mod_workshop', $filearea, $itemid); 1477 } else { 1478 // not found 1479 return null; 1480 } 1481 } 1482 1483 // Checks to see if the user can manage files or is the owner. 1484 // TODO MDL-33805 - Do not use userid here and move the capability check above. 1485 if (!has_capability('moodle/course:managefiles', $context) && $storedfile->get_userid() != $USER->id) { 1486 return null; 1487 } 1488 1489 // let us display the author's name instead of itemid (submission id) 1490 1491 if (isset($submissionauthors[$itemid])) { 1492 $topvisiblename = $submissionauthors[$itemid]; 1493 1494 } else { 1495 1496 $userfieldsapi = \core_user\fields::for_name(); 1497 $userfields = $userfieldsapi->get_sql('u', false, '', '', false)->selects; 1498 $sql = "SELECT s.id, $userfields 1499 FROM {workshop_submissions} s 1500 JOIN {user} u ON (s.authorid = u.id) 1501 WHERE s.example = 0 AND s.workshopid = ?"; 1502 $params = array($cm->instance); 1503 $rs = $DB->get_recordset_sql($sql, $params); 1504 1505 foreach ($rs as $submissionauthor) { 1506 $title = s(fullname($submissionauthor)); // this is generally not unique... 1507 $submissionauthors[$submissionauthor->id] = $title; 1508 } 1509 $rs->close(); 1510 1511 if (!isset($submissionauthors[$itemid])) { 1512 // should not happen 1513 return null; 1514 } else { 1515 $topvisiblename = $submissionauthors[$itemid]; 1516 } 1517 } 1518 1519 $urlbase = $CFG->wwwroot . '/pluginfile.php'; 1520 // do not allow manual modification of any files! 1521 return new file_info_stored($browser, $context, $storedfile, $urlbase, $topvisiblename, true, true, false, false); 1522 } 1523 1524 if ($filearea === 'overallfeedback_content' or $filearea === 'overallfeedback_attachment') { 1525 1526 if (!has_capability('mod/workshop:viewallassessments', $context)) { 1527 return null; 1528 } 1529 1530 if (is_null($itemid)) { 1531 // No itemid (assessmentid) passed, display the list of all assessments. 1532 require_once($CFG->dirroot . '/mod/workshop/fileinfolib.php'); 1533 return new workshop_file_info_overallfeedback_container($browser, $course, $cm, $context, $areas, $filearea); 1534 } 1535 1536 // Make sure the user can see the particular assessment in separate groups mode. 1537 $gmode = groups_get_activity_groupmode($cm, $course); 1538 if ($gmode == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) { 1539 // Check there is at least one common group with both the $USER 1540 // and the submission author. 1541 $sql = "SELECT 'x' 1542 FROM {workshop_submissions} s 1543 JOIN {user} a ON (a.id = s.authorid) 1544 JOIN {groups_members} agm ON (a.id = agm.userid) 1545 JOIN {user} u ON (u.id = ?) 1546 JOIN {groups_members} ugm ON (u.id = ugm.userid) 1547 WHERE s.example = 0 AND s.workshopid = ? AND s.id = ? AND agm.groupid = ugm.groupid"; 1548 $params = array($USER->id, $cm->instance, $itemid); 1549 if (!$DB->record_exists_sql($sql, $params)) { 1550 return null; 1551 } 1552 } 1553 1554 // We are inside a particular assessment container. 1555 $filepath = is_null($filepath) ? '/' : $filepath; 1556 $filename = is_null($filename) ? '.' : $filename; 1557 1558 if (!$storedfile = $fs->get_file($context->id, 'mod_workshop', $filearea, $itemid, $filepath, $filename)) { 1559 if ($filepath === '/' and $filename === '.') { 1560 $storedfile = new virtual_root_file($context->id, 'mod_workshop', $filearea, $itemid); 1561 } else { 1562 // Not found 1563 return null; 1564 } 1565 } 1566 1567 // Check to see if the user can manage files or is the owner. 1568 if (!has_capability('moodle/course:managefiles', $context) and $storedfile->get_userid() != $USER->id) { 1569 return null; 1570 } 1571 1572 $urlbase = $CFG->wwwroot . '/pluginfile.php'; 1573 1574 // Do not allow manual modification of any files. 1575 return new file_info_stored($browser, $context, $storedfile, $urlbase, $itemid, true, true, false, false); 1576 } 1577 1578 if ($filearea == 'instructauthors' or $filearea == 'instructreviewers' or $filearea == 'conclusion') { 1579 // always only itemid 0 1580 1581 $filepath = is_null($filepath) ? '/' : $filepath; 1582 $filename = is_null($filename) ? '.' : $filename; 1583 1584 $urlbase = $CFG->wwwroot.'/pluginfile.php'; 1585 if (!$storedfile = $fs->get_file($context->id, 'mod_workshop', $filearea, 0, $filepath, $filename)) { 1586 if ($filepath === '/' and $filename === '.') { 1587 $storedfile = new virtual_root_file($context->id, 'mod_workshop', $filearea, 0); 1588 } else { 1589 // not found 1590 return null; 1591 } 1592 } 1593 return new file_info_stored($browser, $context, $storedfile, $urlbase, $areas[$filearea], false, true, true, false); 1594 } 1595 } 1596 1597 //////////////////////////////////////////////////////////////////////////////// 1598 // Navigation API // 1599 //////////////////////////////////////////////////////////////////////////////// 1600 1601 /** 1602 * Extends the global navigation tree by adding workshop nodes if there is a relevant content 1603 * 1604 * This can be called by an AJAX request so do not rely on $PAGE as it might not be set up properly. 1605 * 1606 * @param navigation_node $navref An object representing the navigation tree node of the workshop module instance 1607 * @param stdClass $course 1608 * @param stdClass $module 1609 * @param cm_info $cm 1610 */ 1611 function workshop_extend_navigation(navigation_node $navref, stdclass $course, stdclass $module, cm_info $cm) { 1612 global $CFG; 1613 1614 if (has_capability('mod/workshop:submit', context_module::instance($cm->id))) { 1615 $url = new moodle_url('/mod/workshop/submission.php', array('cmid' => $cm->id)); 1616 $mysubmission = $navref->add(get_string('mysubmission', 'workshop'), $url); 1617 $mysubmission->mainnavonly = true; 1618 } 1619 } 1620 1621 /** 1622 * Extends the settings navigation with the Workshop settings 1623 1624 * This function is called when the context for the page is a workshop module. This is not called by AJAX 1625 * so it is safe to rely on the $PAGE. 1626 * 1627 * @param settings_navigation $settingsnav {@link settings_navigation} 1628 * @param navigation_node $workshopnode {@link navigation_node} 1629 */ 1630 function workshop_extend_settings_navigation(settings_navigation $settingsnav, navigation_node $workshopnode=null) { 1631 if (has_capability('mod/workshop:editdimensions', $settingsnav->get_page()->cm->context)) { 1632 $url = new moodle_url('/mod/workshop/editform.php', array('cmid' => $settingsnav->get_page()->cm->id)); 1633 $workshopnode->add(get_string('assessmentform', 'workshop'), $url, 1634 settings_navigation::TYPE_SETTING, null, 'workshopassessement'); 1635 } 1636 if (has_capability('mod/workshop:allocate', $settingsnav->get_page()->cm->context)) { 1637 $url = new moodle_url('/mod/workshop/allocation.php', array('cmid' => $settingsnav->get_page()->cm->id)); 1638 $workshopnode->add(get_string('submissionsallocation', 'workshop'), $url, settings_navigation::TYPE_SETTING); 1639 } 1640 } 1641 1642 /** 1643 * Return a list of page types 1644 * @param string $pagetype current page type 1645 * @param stdClass $parentcontext Block's parent context 1646 * @param stdClass $currentcontext Current context of block 1647 */ 1648 function workshop_page_type_list($pagetype, $parentcontext, $currentcontext) { 1649 $module_pagetype = array('mod-workshop-*'=>get_string('page-mod-workshop-x', 'workshop')); 1650 return $module_pagetype; 1651 } 1652 1653 //////////////////////////////////////////////////////////////////////////////// 1654 // Calendar API // 1655 //////////////////////////////////////////////////////////////////////////////// 1656 1657 /** 1658 * Updates the calendar events associated to the given workshop 1659 * 1660 * @param stdClass $workshop the workshop instance record 1661 * @param int $cmid course module id 1662 */ 1663 function workshop_calendar_update(stdClass $workshop, $cmid) { 1664 global $DB; 1665 1666 // get the currently registered events so that we can re-use their ids 1667 $currentevents = $DB->get_records('event', array('modulename' => 'workshop', 'instance' => $workshop->id)); 1668 1669 // the common properties for all events 1670 $base = new stdClass(); 1671 $base->description = format_module_intro('workshop', $workshop, $cmid, false); 1672 $base->format = FORMAT_HTML; 1673 $base->courseid = $workshop->course; 1674 $base->groupid = 0; 1675 $base->userid = 0; 1676 $base->modulename = 'workshop'; 1677 $base->instance = $workshop->id; 1678 $base->visible = instance_is_visible('workshop', $workshop); 1679 $base->timeduration = 0; 1680 1681 if ($workshop->submissionstart) { 1682 $event = clone($base); 1683 $event->name = get_string('submissionstartevent', 'mod_workshop', $workshop->name); 1684 $event->eventtype = WORKSHOP_EVENT_TYPE_SUBMISSION_OPEN; 1685 $event->type = empty($workshop->submissionend) ? CALENDAR_EVENT_TYPE_ACTION : CALENDAR_EVENT_TYPE_STANDARD; 1686 $event->timestart = $workshop->submissionstart; 1687 $event->timesort = $workshop->submissionstart; 1688 if ($reusedevent = array_shift($currentevents)) { 1689 $event->id = $reusedevent->id; 1690 } else { 1691 // should not be set but just in case 1692 unset($event->id); 1693 } 1694 // update() will reuse a db record if the id field is set 1695 $eventobj = new calendar_event($event); 1696 $eventobj->update($event, false); 1697 } 1698 1699 if ($workshop->submissionend) { 1700 $event = clone($base); 1701 $event->name = get_string('submissionendevent', 'mod_workshop', $workshop->name); 1702 $event->eventtype = WORKSHOP_EVENT_TYPE_SUBMISSION_CLOSE; 1703 $event->type = CALENDAR_EVENT_TYPE_ACTION; 1704 $event->timestart = $workshop->submissionend; 1705 $event->timesort = $workshop->submissionend; 1706 if ($reusedevent = array_shift($currentevents)) { 1707 $event->id = $reusedevent->id; 1708 } else { 1709 // should not be set but just in case 1710 unset($event->id); 1711 } 1712 // update() will reuse a db record if the id field is set 1713 $eventobj = new calendar_event($event); 1714 $eventobj->update($event, false); 1715 } 1716 1717 if ($workshop->assessmentstart) { 1718 $event = clone($base); 1719 $event->name = get_string('assessmentstartevent', 'mod_workshop', $workshop->name); 1720 $event->eventtype = WORKSHOP_EVENT_TYPE_ASSESSMENT_OPEN; 1721 $event->type = empty($workshop->assessmentend) ? CALENDAR_EVENT_TYPE_ACTION : CALENDAR_EVENT_TYPE_STANDARD; 1722 $event->timestart = $workshop->assessmentstart; 1723 $event->timesort = $workshop->assessmentstart; 1724 if ($reusedevent = array_shift($currentevents)) { 1725 $event->id = $reusedevent->id; 1726 } else { 1727 // should not be set but just in case 1728 unset($event->id); 1729 } 1730 // update() will reuse a db record if the id field is set 1731 $eventobj = new calendar_event($event); 1732 $eventobj->update($event, false); 1733 } 1734 1735 if ($workshop->assessmentend) { 1736 $event = clone($base); 1737 $event->name = get_string('assessmentendevent', 'mod_workshop', $workshop->name); 1738 $event->eventtype = WORKSHOP_EVENT_TYPE_ASSESSMENT_CLOSE; 1739 $event->type = CALENDAR_EVENT_TYPE_ACTION; 1740 $event->timestart = $workshop->assessmentend; 1741 $event->timesort = $workshop->assessmentend; 1742 if ($reusedevent = array_shift($currentevents)) { 1743 $event->id = $reusedevent->id; 1744 } else { 1745 // should not be set but just in case 1746 unset($event->id); 1747 } 1748 // update() will reuse a db record if the id field is set 1749 $eventobj = new calendar_event($event); 1750 $eventobj->update($event, false); 1751 } 1752 1753 // delete any leftover events 1754 foreach ($currentevents as $oldevent) { 1755 $oldevent = calendar_event::load($oldevent); 1756 $oldevent->delete(); 1757 } 1758 } 1759 1760 /** 1761 * This function receives a calendar event and returns the action associated with it, or null if there is none. 1762 * 1763 * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event 1764 * is not displayed on the block. 1765 * 1766 * @param calendar_event $event 1767 * @param \core_calendar\action_factory $factory 1768 * @param int $userid User id to use for all capability checks, etc. Set to 0 for current user (default). 1769 * @return \core_calendar\local\event\entities\action_interface|null 1770 */ 1771 function mod_workshop_core_calendar_provide_event_action(calendar_event $event, 1772 \core_calendar\action_factory $factory, int $userid = 0) { 1773 global $USER; 1774 1775 if (!$userid) { 1776 $userid = $USER->id; 1777 } 1778 1779 $cm = get_fast_modinfo($event->courseid, $userid)->instances['workshop'][$event->instance]; 1780 1781 if (!$cm->uservisible) { 1782 // The module is not visible to the user for any reason. 1783 return null; 1784 } 1785 1786 $completion = new \completion_info($cm->get_course()); 1787 1788 $completiondata = $completion->get_data($cm, false, $userid); 1789 1790 if ($completiondata->completionstate != COMPLETION_INCOMPLETE) { 1791 return null; 1792 } 1793 1794 return $factory->create_instance( 1795 get_string('viewworkshopsummary', 'workshop'), 1796 new \moodle_url('/mod/workshop/view.php', array('id' => $cm->id)), 1797 1, 1798 true 1799 ); 1800 } 1801 1802 /** 1803 * This function calculates the minimum and maximum cutoff values for the timestart of 1804 * the given event. 1805 * 1806 * It will return an array with two values, the first being the minimum cutoff value and 1807 * the second being the maximum cutoff value. Either or both values can be null, which 1808 * indicates there is no minimum or maximum, respectively. 1809 * 1810 * If a cutoff is required then the function must return an array containing the cutoff 1811 * timestamp and error string to display to the user if the cutoff value is violated. 1812 * 1813 * A minimum and maximum cutoff return value will look like: 1814 * [ 1815 * [1505704373, 'The date must be after this date'], 1816 * [1506741172, 'The date must be before this date'] 1817 * ] 1818 * 1819 * @param calendar_event $event The calendar event to get the time range for 1820 * @param stdClass $workshop The module instance to get the range from 1821 * @return array Returns an array with min and max date. 1822 */ 1823 function mod_workshop_core_calendar_get_valid_event_timestart_range(\calendar_event $event, \stdClass $workshop) : array { 1824 $mindate = null; 1825 $maxdate = null; 1826 1827 $phasesubmissionend = max($workshop->submissionstart, $workshop->submissionend); 1828 $phaseassessmentstart = min($workshop->assessmentstart, $workshop->assessmentend); 1829 if ($phaseassessmentstart == 0) { 1830 $phaseassessmentstart = max($workshop->assessmentstart, $workshop->assessmentend); 1831 } 1832 1833 switch ($event->eventtype) { 1834 case WORKSHOP_EVENT_TYPE_SUBMISSION_OPEN: 1835 if (!empty($workshop->submissionend)) { 1836 $maxdate = [ 1837 $workshop->submissionend - 1, // The submissionstart and submissionend cannot be exactly the same. 1838 get_string('submissionendbeforestart', 'mod_workshop') 1839 ]; 1840 } else if ($phaseassessmentstart) { 1841 $maxdate = [ 1842 $phaseassessmentstart, 1843 get_string('phasesoverlap', 'mod_workshop') 1844 ]; 1845 } 1846 break; 1847 case WORKSHOP_EVENT_TYPE_SUBMISSION_CLOSE: 1848 if (!empty($workshop->submissionstart)) { 1849 $mindate = [ 1850 $workshop->submissionstart + 1, // The submissionstart and submissionend cannot be exactly the same. 1851 get_string('submissionendbeforestart', 'mod_workshop') 1852 ]; 1853 } 1854 if ($phaseassessmentstart) { 1855 $maxdate = [ 1856 $phaseassessmentstart, 1857 get_string('phasesoverlap', 'mod_workshop') 1858 ]; 1859 } 1860 break; 1861 case WORKSHOP_EVENT_TYPE_ASSESSMENT_OPEN: 1862 if ($phasesubmissionend) { 1863 $mindate = [ 1864 $phasesubmissionend, 1865 get_string('phasesoverlap', 'mod_workshop') 1866 ]; 1867 } 1868 if (!empty($workshop->assessmentend)) { 1869 $maxdate = [ 1870 $workshop->assessmentend - 1, // The assessmentstart and assessmentend cannot be exactly the same. 1871 get_string('assessmentendbeforestart', 'mod_workshop') 1872 ]; 1873 } 1874 break; 1875 case WORKSHOP_EVENT_TYPE_ASSESSMENT_CLOSE: 1876 if (!empty($workshop->assessmentstart)) { 1877 $mindate = [ 1878 $workshop->assessmentstart + 1, // The assessmentstart and assessmentend cannot be exactly the same. 1879 get_string('assessmentendbeforestart', 'mod_workshop') 1880 ]; 1881 } else if ($phasesubmissionend) { 1882 $mindate = [ 1883 $phasesubmissionend, 1884 get_string('phasesoverlap', 'mod_workshop') 1885 ]; 1886 } 1887 break; 1888 } 1889 1890 return [$mindate, $maxdate]; 1891 } 1892 1893 /** 1894 * This function will update the workshop module according to the 1895 * event that has been modified. 1896 * 1897 * @param \calendar_event $event 1898 * @param stdClass $workshop The module instance to get the range from 1899 */ 1900 function mod_workshop_core_calendar_event_timestart_updated(\calendar_event $event, \stdClass $workshop) : void { 1901 global $DB; 1902 1903 $courseid = $event->courseid; 1904 $modulename = $event->modulename; 1905 $instanceid = $event->instance; 1906 1907 // Something weird going on. The event is for a different module so 1908 // we should ignore it. 1909 if ($modulename != 'workshop') { 1910 return; 1911 } 1912 1913 if ($workshop->id != $instanceid) { 1914 return; 1915 } 1916 1917 if (!in_array( 1918 $event->eventtype, 1919 [ 1920 WORKSHOP_EVENT_TYPE_SUBMISSION_OPEN, 1921 WORKSHOP_EVENT_TYPE_SUBMISSION_CLOSE, 1922 WORKSHOP_EVENT_TYPE_ASSESSMENT_OPEN, 1923 WORKSHOP_EVENT_TYPE_ASSESSMENT_CLOSE 1924 ] 1925 )) { 1926 return; 1927 } 1928 1929 $coursemodule = get_fast_modinfo($courseid)->instances[$modulename][$instanceid]; 1930 $context = context_module::instance($coursemodule->id); 1931 1932 // The user does not have the capability to modify this activity. 1933 if (!has_capability('moodle/course:manageactivities', $context)) { 1934 return; 1935 } 1936 1937 $modified = false; 1938 1939 switch ($event->eventtype) { 1940 case WORKSHOP_EVENT_TYPE_SUBMISSION_OPEN: 1941 if ($event->timestart != $workshop->submissionstart) { 1942 $workshop->submissionstart = $event->timestart; 1943 $modified = true; 1944 } 1945 break; 1946 case WORKSHOP_EVENT_TYPE_SUBMISSION_CLOSE: 1947 if ($event->timestart != $workshop->submissionend) { 1948 $workshop->submissionend = $event->timestart; 1949 $modified = true; 1950 } 1951 break; 1952 case WORKSHOP_EVENT_TYPE_ASSESSMENT_OPEN: 1953 if ($event->timestart != $workshop->assessmentstart) { 1954 $workshop->assessmentstart = $event->timestart; 1955 $modified = true; 1956 } 1957 break; 1958 case WORKSHOP_EVENT_TYPE_ASSESSMENT_CLOSE: 1959 if ($event->timestart != $workshop->assessmentend) { 1960 $workshop->assessmentend = $event->timestart; 1961 $modified = true; 1962 } 1963 break; 1964 } 1965 1966 if ($modified) { 1967 $workshop->timemodified = time(); 1968 // Persist the assign instance changes. 1969 $DB->update_record('workshop', $workshop); 1970 $event = \core\event\course_module_updated::create_from_cm($coursemodule, $context); 1971 $event->trigger(); 1972 } 1973 } 1974 1975 //////////////////////////////////////////////////////////////////////////////// 1976 // Course reset API // 1977 //////////////////////////////////////////////////////////////////////////////// 1978 1979 /** 1980 * Extends the course reset form with workshop specific settings. 1981 * 1982 * @param MoodleQuickForm $mform 1983 */ 1984 function workshop_reset_course_form_definition($mform) { 1985 1986 $mform->addElement('header', 'workshopheader', get_string('modulenameplural', 'mod_workshop')); 1987 1988 $mform->addElement('advcheckbox', 'reset_workshop_submissions', get_string('resetsubmissions', 'mod_workshop')); 1989 $mform->addHelpButton('reset_workshop_submissions', 'resetsubmissions', 'mod_workshop'); 1990 1991 $mform->addElement('advcheckbox', 'reset_workshop_assessments', get_string('resetassessments', 'mod_workshop')); 1992 $mform->addHelpButton('reset_workshop_assessments', 'resetassessments', 'mod_workshop'); 1993 $mform->disabledIf('reset_workshop_assessments', 'reset_workshop_submissions', 'checked'); 1994 1995 $mform->addElement('advcheckbox', 'reset_workshop_phase', get_string('resetphase', 'mod_workshop')); 1996 $mform->addHelpButton('reset_workshop_phase', 'resetphase', 'mod_workshop'); 1997 } 1998 1999 /** 2000 * Provides default values for the workshop settings in the course reset form. 2001 * 2002 * @param stdClass $course The course to be reset. 2003 */ 2004 function workshop_reset_course_form_defaults(stdClass $course) { 2005 2006 $defaults = array( 2007 'reset_workshop_submissions' => 1, 2008 'reset_workshop_assessments' => 1, 2009 'reset_workshop_phase' => 1, 2010 ); 2011 2012 return $defaults; 2013 } 2014 2015 /** 2016 * Performs the reset of all workshop instances in the course. 2017 * 2018 * @param stdClass $data The actual course reset settings. 2019 * @return array List of results, each being array[(string)component, (string)item, (string)error] 2020 */ 2021 function workshop_reset_userdata(stdClass $data) { 2022 global $CFG, $DB; 2023 2024 // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset. 2025 // See MDL-9367. 2026 shift_course_mod_dates('workshop', array('submissionstart', 'submissionend', 'assessmentstart', 'assessmentend'), 2027 $data->timeshift, $data->courseid); 2028 $status = array(); 2029 $status[] = array('component' => get_string('modulenameplural', 'workshop'), 'item' => get_string('datechanged'), 2030 'error' => false); 2031 2032 if (empty($data->reset_workshop_submissions) 2033 and empty($data->reset_workshop_assessments) 2034 and empty($data->reset_workshop_phase) ) { 2035 // Nothing to do here. 2036 return $status; 2037 } 2038 2039 $workshoprecords = $DB->get_records('workshop', array('course' => $data->courseid)); 2040 2041 if (empty($workshoprecords)) { 2042 // What a boring course - no workshops here! 2043 return $status; 2044 } 2045 2046 require_once($CFG->dirroot . '/mod/workshop/locallib.php'); 2047 2048 $course = $DB->get_record('course', array('id' => $data->courseid), '*', MUST_EXIST); 2049 2050 foreach ($workshoprecords as $workshoprecord) { 2051 $cm = get_coursemodule_from_instance('workshop', $workshoprecord->id, $course->id, false, MUST_EXIST); 2052 $workshop = new workshop($workshoprecord, $cm, $course); 2053 $status = array_merge($status, $workshop->reset_userdata($data)); 2054 } 2055 2056 return $status; 2057 } 2058 2059 /** 2060 * Get icon mapping for font-awesome. 2061 */ 2062 function mod_workshop_get_fontawesome_icon_map() { 2063 return [ 2064 'mod_workshop:userplan/task-info' => 'fa-info text-info', 2065 'mod_workshop:userplan/task-todo' => 'fa-square-o', 2066 'mod_workshop:userplan/task-done' => 'fa-check text-success', 2067 'mod_workshop:userplan/task-fail' => 'fa-remove text-danger', 2068 ]; 2069 } 2070 2071 /** 2072 * Check if the module has any update that affects the current user since a given time. 2073 * 2074 * @param cm_info $cm course module data 2075 * @param int $from the time to check updates from 2076 * @param array $filter if we need to check only specific updates 2077 * @return stdClass an object with the different type of areas indicating if they were updated or not 2078 * @since Moodle 3.4 2079 */ 2080 function workshop_check_updates_since(cm_info $cm, $from, $filter = array()) { 2081 global $DB, $USER; 2082 2083 $updates = course_check_module_updates_since($cm, $from, array('instructauthors', 'instructreviewers', 'conclusion'), $filter); 2084 2085 // Check if there are new submissions, assessments or assessments grades in the workshop. 2086 $updates->submissions = (object) array('updated' => false); 2087 $updates->assessments = (object) array('updated' => false); 2088 $updates->assessmentgrades = (object) array('updated' => false); 2089 2090 $select = 'workshopid = ? AND authorid = ? AND (timecreated > ? OR timegraded > ? OR timemodified > ?)'; 2091 $params = array($cm->instance, $USER->id, $from, $from, $from); 2092 $submissions = $DB->get_records_select('workshop_submissions', $select, $params, '', 'id'); 2093 if (!empty($submissions)) { 2094 $updates->submissions->updated = true; 2095 $updates->submissions->itemids = array_keys($submissions); 2096 } 2097 2098 // Get assessments updates (both submissions reviewed by me or reviews by others). 2099 $select = "SELECT a.id 2100 FROM {workshop_assessments} a 2101 JOIN {workshop_submissions} s ON a.submissionid = s.id 2102 WHERE s.workshopid = ? AND (a.timecreated > ? OR a.timemodified > ?) AND (s.authorid = ? OR a.reviewerid = ?)"; 2103 $params = array($cm->instance, $from, $from, $USER->id, $USER->id); 2104 $assessments = $DB->get_records_sql($select, $params); 2105 if (!empty($assessments)) { 2106 $updates->assessments->updated = true; 2107 $updates->assessments->itemids = array_keys($assessments); 2108 } 2109 // Finally assessment aggregated grades. 2110 $select = 'workshopid = ? AND userid = ? AND timegraded > ?'; 2111 $params = array($cm->instance, $USER->id, $from); 2112 $assessmentgrades = $DB->get_records_select('workshop_aggregations', $select, $params, '', 'id'); 2113 if (!empty($assessmentgrades)) { 2114 $updates->assessmentgrades->updated = true; 2115 $updates->assessmentgrades->itemids = array_keys($assessmentgrades); 2116 } 2117 2118 // Now, teachers should see other students updates. 2119 $canviewallsubmissions = has_capability('mod/workshop:viewallsubmissions', $cm->context); 2120 $canviewallassessments = has_capability('mod/workshop:viewallassessments', $cm->context); 2121 if ($canviewallsubmissions || $canviewallassessments) { 2122 2123 $insql = ''; 2124 $inparams = array(); 2125 // To filter by users in my groups when separated groups are forced. 2126 if (groups_get_activity_groupmode($cm) == SEPARATEGROUPS) { 2127 $groupusers = array_keys(groups_get_activity_shared_group_members($cm)); 2128 if (empty($groupusers)) { 2129 return $updates; 2130 } 2131 list($insql, $inparams) = $DB->get_in_or_equal($groupusers); 2132 } 2133 2134 if ($canviewallsubmissions) { 2135 $updates->usersubmissions = (object) array('updated' => false); 2136 $select = 'workshopid = ? AND (timecreated > ? OR timegraded > ? OR timemodified > ?)'; 2137 $params = array($cm->instance, $from, $from, $from); 2138 if (!empty($insql)) { 2139 $select .= " AND authorid $insql"; 2140 $params = array_merge($params, $inparams); 2141 } 2142 $usersubmissions = $DB->get_records_select('workshop_submissions', $select, $params, '', 'id'); 2143 if (!empty($usersubmissions)) { 2144 $updates->usersubmissions->updated = true; 2145 $updates->usersubmissions->itemids = array_keys($usersubmissions); 2146 } 2147 } 2148 2149 if ($canviewallassessments) { 2150 $updates->userassessments = (object) array('updated' => false); 2151 $select = "SELECT a.id 2152 FROM {workshop_assessments} a 2153 JOIN {workshop_submissions} s ON a.submissionid = s.id 2154 WHERE s.workshopid = ? AND (a.timecreated > ? OR a.timemodified > ?)"; 2155 $params = array($cm->instance, $from, $from); 2156 if (!empty($insql)) { 2157 $select .= " AND s.reviewerid $insql"; 2158 $params = array_merge($params, $inparams); 2159 } 2160 $userassessments = $DB->get_records_sql($select, $params); 2161 if (!empty($userassessments)) { 2162 $updates->userassessments->updated = true; 2163 $updates->userassessments->itemids = array_keys($userassessments); 2164 } 2165 2166 $updates->userassessmentgrades = (object) array('updated' => false); 2167 $select = 'workshopid = ? AND timegraded > ?'; 2168 $params = array($cm->instance, $USER->id); 2169 if (!empty($insql)) { 2170 $select .= " AND userid $insql"; 2171 $params = array_merge($params, $inparams); 2172 } 2173 $userassessmentgrades = $DB->get_records_select('workshop_aggregations', $select, $params, '', 'id'); 2174 if (!empty($userassessmentgrades)) { 2175 $updates->userassessmentgrades->updated = true; 2176 $updates->userassessmentgrades->itemids = array_keys($userassessmentgrades); 2177 } 2178 } 2179 } 2180 return $updates; 2181 } 2182 2183 /** 2184 * Given an array with a file path, it returns the itemid and the filepath for the defined filearea. 2185 * 2186 * @param string $filearea The filearea. 2187 * @param array $args The path (the part after the filearea and before the filename). 2188 * @return array|null The itemid and the filepath inside the $args path, for the defined filearea. 2189 */ 2190 function mod_workshop_get_path_from_pluginfile(string $filearea, array $args) : ?array { 2191 if ($filearea !== 'instructauthors' && $filearea !== 'instructreviewers' && $filearea !== 'conclusion') { 2192 return null; 2193 } 2194 2195 // Workshop only has empty itemid for some of the fileareas. 2196 array_shift($args); 2197 2198 // Get the filepath. 2199 if (empty($args)) { 2200 $filepath = '/'; 2201 } else { 2202 $filepath = '/' . implode('/', $args) . '/'; 2203 } 2204 2205 return [ 2206 'itemid' => 0, 2207 'filepath' => $filepath, 2208 ]; 2209 } 2210 2211 /** 2212 * Add a get_coursemodule_info function in case any feedback type wants to add 'extra' information 2213 * for the course (see resource). 2214 * 2215 * Given a course_module object, this function returns any "extra" information that may be needed 2216 * when printing this activity in a course listing. See get_array_of_activities() in course/lib.php. 2217 * 2218 * @param stdClass $coursemodule The coursemodule object (record). 2219 * @return cached_cm_info|false An object on information that the courses will know about (most noticeably, an icon). 2220 */ 2221 function workshop_get_coursemodule_info($coursemodule) { 2222 global $DB; 2223 2224 $dbparams = ['id' => $coursemodule->instance]; 2225 $fields = 'id, name, intro, introformat, submissionstart, submissionend, assessmentstart, assessmentend'; 2226 if (!$workshop = $DB->get_record('workshop', $dbparams, $fields)) { 2227 return false; 2228 } 2229 2230 $result = new cached_cm_info(); 2231 $result->name = $workshop->name; 2232 2233 if ($coursemodule->showdescription) { 2234 // Convert intro to html. Do not filter cached version, filters run at display time. 2235 $result->content = format_module_intro('workshop', $workshop, $coursemodule->id, false); 2236 } 2237 2238 // Populate some other values that can be used in calendar or on dashboard. 2239 if ($workshop->submissionstart) { 2240 $result->customdata['submissionstart'] = $workshop->submissionstart; 2241 } 2242 if ($workshop->submissionend) { 2243 $result->customdata['submissionend'] = $workshop->submissionend; 2244 } 2245 if ($workshop->assessmentstart) { 2246 $result->customdata['assessmentstart'] = $workshop->assessmentstart; 2247 } 2248 if ($workshop->assessmentend) { 2249 $result->customdata['assessmentend'] = $workshop->assessmentend; 2250 } 2251 2252 return $result; 2253 } 2254 2255 /** 2256 * Callback to fetch the activity event type lang string. 2257 * 2258 * @param string $eventtype The event type. 2259 * @return lang_string The event type lang string. 2260 */ 2261 function mod_workshop_core_calendar_get_event_action_string($eventtype): string { 2262 $modulename = get_string('modulename', 'workshop'); 2263 2264 switch ($eventtype) { 2265 case WORKSHOP_EVENT_TYPE_SUBMISSION_OPEN: 2266 $identifier = 'submissionstartevent'; 2267 break; 2268 case WORKSHOP_EVENT_TYPE_SUBMISSION_CLOSE: 2269 $identifier = 'submissionendevent'; 2270 break; 2271 case WORKSHOP_EVENT_TYPE_ASSESSMENT_OPEN: 2272 $identifier = 'assessmentstartevent'; 2273 break; 2274 case WORKSHOP_EVENT_TYPE_ASSESSMENT_CLOSE; 2275 $identifier = 'assessmentendevent'; 2276 break; 2277 default: 2278 return get_string('requiresaction', 'calendar', $modulename); 2279 } 2280 2281 return get_string($identifier, 'workshop', $modulename); 2282 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body