Differences Between: [Versions 310 and 311] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]
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 * Library of internal classes and functions for module workshop 19 * 20 * All the workshop specific functions, needed to implement the module 21 * logic, should go to here. Instead of having bunch of function named 22 * workshop_something() taking the workshop instance as the first 23 * parameter, we use a class workshop that provides all methods. 24 * 25 * @package mod_workshop 26 * @copyright 2009 David Mudrak <david.mudrak@gmail.com> 27 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 28 */ 29 30 defined('MOODLE_INTERNAL') || die(); 31 32 require_once (__DIR__.'/lib.php'); // we extend this library here 33 require_once($CFG->libdir . '/gradelib.php'); // we use some rounding and comparing routines here 34 require_once($CFG->libdir . '/filelib.php'); 35 36 /** 37 * Full-featured workshop API 38 * 39 * This wraps the workshop database record with a set of methods that are called 40 * from the module itself. The class should be initialized right after you get 41 * $workshop, $cm and $course records at the begining of the script. 42 */ 43 class workshop { 44 45 /** error status of the {@link self::add_allocation()} */ 46 const ALLOCATION_EXISTS = -9999; 47 48 /** the internal code of the workshop phases as are stored in the database */ 49 const PHASE_SETUP = 10; 50 const PHASE_SUBMISSION = 20; 51 const PHASE_ASSESSMENT = 30; 52 const PHASE_EVALUATION = 40; 53 const PHASE_CLOSED = 50; 54 55 /** the internal code of the examples modes as are stored in the database */ 56 const EXAMPLES_VOLUNTARY = 0; 57 const EXAMPLES_BEFORE_SUBMISSION = 1; 58 const EXAMPLES_BEFORE_ASSESSMENT = 2; 59 60 /** @var stdclass workshop record from database */ 61 public $dbrecord; 62 63 /** @var cm_info course module record */ 64 public $cm; 65 66 /** @var stdclass course record */ 67 public $course; 68 69 /** @var stdclass context object */ 70 public $context; 71 72 /** @var int workshop instance identifier */ 73 public $id; 74 75 /** @var string workshop activity name */ 76 public $name; 77 78 /** @var string introduction or description of the activity */ 79 public $intro; 80 81 /** @var int format of the {@link $intro} */ 82 public $introformat; 83 84 /** @var string instructions for the submission phase */ 85 public $instructauthors; 86 87 /** @var int format of the {@link $instructauthors} */ 88 public $instructauthorsformat; 89 90 /** @var string instructions for the assessment phase */ 91 public $instructreviewers; 92 93 /** @var int format of the {@link $instructreviewers} */ 94 public $instructreviewersformat; 95 96 /** @var int timestamp of when the module was modified */ 97 public $timemodified; 98 99 /** @var int current phase of workshop, for example {@link workshop::PHASE_SETUP} */ 100 public $phase; 101 102 /** @var bool optional feature: students practise evaluating on example submissions from teacher */ 103 public $useexamples; 104 105 /** @var bool optional feature: students perform peer assessment of others' work (deprecated, consider always enabled) */ 106 public $usepeerassessment; 107 108 /** @var bool optional feature: students perform self assessment of their own work */ 109 public $useselfassessment; 110 111 /** @var float number (10, 5) unsigned, the maximum grade for submission */ 112 public $grade; 113 114 /** @var float number (10, 5) unsigned, the maximum grade for assessment */ 115 public $gradinggrade; 116 117 /** @var string type of the current grading strategy used in this workshop, for example 'accumulative' */ 118 public $strategy; 119 120 /** @var string the name of the evaluation plugin to use for grading grades calculation */ 121 public $evaluation; 122 123 /** @var int number of digits that should be shown after the decimal point when displaying grades */ 124 public $gradedecimals; 125 126 /** @var int number of allowed submission attachments and the files embedded into submission */ 127 public $nattachments; 128 129 /** @var string list of allowed file types that are allowed to be embedded into submission */ 130 public $submissionfiletypes = null; 131 132 /** @var bool allow submitting the work after the deadline */ 133 public $latesubmissions; 134 135 /** @var int maximum size of the one attached file in bytes */ 136 public $maxbytes; 137 138 /** @var int mode of example submissions support, for example {@link workshop::EXAMPLES_VOLUNTARY} */ 139 public $examplesmode; 140 141 /** @var int if greater than 0 then the submission is not allowed before this timestamp */ 142 public $submissionstart; 143 144 /** @var int if greater than 0 then the submission is not allowed after this timestamp */ 145 public $submissionend; 146 147 /** @var int if greater than 0 then the peer assessment is not allowed before this timestamp */ 148 public $assessmentstart; 149 150 /** @var int if greater than 0 then the peer assessment is not allowed after this timestamp */ 151 public $assessmentend; 152 153 /** @var bool automatically switch to the assessment phase after the submissions deadline */ 154 public $phaseswitchassessment; 155 156 /** @var string conclusion text to be displayed at the end of the activity */ 157 public $conclusion; 158 159 /** @var int format of the conclusion text */ 160 public $conclusionformat; 161 162 /** @var int the mode of the overall feedback */ 163 public $overallfeedbackmode; 164 165 /** @var int maximum number of overall feedback attachments */ 166 public $overallfeedbackfiles; 167 168 /** @var string list of allowed file types that can be attached to the overall feedback */ 169 public $overallfeedbackfiletypes = null; 170 171 /** @var int maximum size of one file attached to the overall feedback */ 172 public $overallfeedbackmaxbytes; 173 174 /** @var int Should the submission form show the text field? */ 175 public $submissiontypetext; 176 177 /** @var int Should the submission form show the file attachment field? */ 178 public $submissiontypefile; 179 180 /** 181 * @var workshop_strategy grading strategy instance 182 * Do not use directly, get the instance using {@link workshop::grading_strategy_instance()} 183 */ 184 protected $strategyinstance = null; 185 186 /** 187 * @var workshop_evaluation grading evaluation instance 188 * Do not use directly, get the instance using {@link workshop::grading_evaluation_instance()} 189 */ 190 protected $evaluationinstance = null; 191 192 /** 193 * Initializes the workshop API instance using the data from DB 194 * 195 * Makes deep copy of all passed records properties. 196 * 197 * For unit testing only, $cm and $course may be set to null. This is so that 198 * you can test without having any real database objects if you like. Not all 199 * functions will work in this situation. 200 * 201 * @param stdClass $dbrecord Workshop instance data from {workshop} table 202 * @param stdClass|cm_info $cm Course module record 203 * @param stdClass $course Course record from {course} table 204 * @param stdClass $context The context of the workshop instance 205 */ 206 public function __construct(stdclass $dbrecord, $cm, $course, stdclass $context=null) { 207 $this->dbrecord = $dbrecord; 208 foreach ($this->dbrecord as $field => $value) { 209 if (property_exists('workshop', $field)) { 210 $this->{$field} = $value; 211 } 212 } 213 if (is_null($cm) || is_null($course)) { 214 throw new coding_exception('Must specify $cm and $course'); 215 } 216 $this->course = $course; 217 if ($cm instanceof cm_info) { 218 $this->cm = $cm; 219 } else { 220 $modinfo = get_fast_modinfo($course); 221 $this->cm = $modinfo->get_cm($cm->id); 222 } 223 if (is_null($context)) { 224 $this->context = context_module::instance($this->cm->id); 225 } else { 226 $this->context = $context; 227 } 228 } 229 230 //////////////////////////////////////////////////////////////////////////////// 231 // Static methods // 232 //////////////////////////////////////////////////////////////////////////////// 233 234 /** 235 * Return list of available allocation methods 236 * 237 * @return array Array ['string' => 'string'] of localized allocation method names 238 */ 239 public static function installed_allocators() { 240 $installed = core_component::get_plugin_list('workshopallocation'); 241 $forms = array(); 242 foreach ($installed as $allocation => $allocationpath) { 243 if (file_exists($allocationpath . '/lib.php')) { 244 $forms[$allocation] = get_string('pluginname', 'workshopallocation_' . $allocation); 245 } 246 } 247 // usability - make sure that manual allocation appears the first 248 if (isset($forms['manual'])) { 249 $m = array('manual' => $forms['manual']); 250 unset($forms['manual']); 251 $forms = array_merge($m, $forms); 252 } 253 return $forms; 254 } 255 256 /** 257 * Returns an array of options for the editors that are used for submitting and assessing instructions 258 * 259 * @param stdClass $context 260 * @uses EDITOR_UNLIMITED_FILES hard-coded value for the 'maxfiles' option 261 * @return array 262 */ 263 public static function instruction_editors_options(stdclass $context) { 264 return array('subdirs' => 1, 'maxbytes' => 0, 'maxfiles' => -1, 265 'changeformat' => 1, 'context' => $context, 'noclean' => 1, 'trusttext' => 0); 266 } 267 268 /** 269 * Given the percent and the total, returns the number 270 * 271 * @param float $percent from 0 to 100 272 * @param float $total the 100% value 273 * @return float 274 */ 275 public static function percent_to_value($percent, $total) { 276 if ($percent < 0 or $percent > 100) { 277 throw new coding_exception('The percent can not be less than 0 or higher than 100'); 278 } 279 280 return $total * $percent / 100; 281 } 282 283 /** 284 * Returns an array of numeric values that can be used as maximum grades 285 * 286 * @return array Array of integers 287 */ 288 public static function available_maxgrades_list() { 289 $grades = array(); 290 for ($i=100; $i>=0; $i--) { 291 $grades[$i] = $i; 292 } 293 return $grades; 294 } 295 296 /** 297 * Returns the localized list of supported examples modes 298 * 299 * @return array 300 */ 301 public static function available_example_modes_list() { 302 $options = array(); 303 $options[self::EXAMPLES_VOLUNTARY] = get_string('examplesvoluntary', 'workshop'); 304 $options[self::EXAMPLES_BEFORE_SUBMISSION] = get_string('examplesbeforesubmission', 'workshop'); 305 $options[self::EXAMPLES_BEFORE_ASSESSMENT] = get_string('examplesbeforeassessment', 'workshop'); 306 return $options; 307 } 308 309 /** 310 * Returns the list of available grading strategy methods 311 * 312 * @return array ['string' => 'string'] 313 */ 314 public static function available_strategies_list() { 315 $installed = core_component::get_plugin_list('workshopform'); 316 $forms = array(); 317 foreach ($installed as $strategy => $strategypath) { 318 if (file_exists($strategypath . '/lib.php')) { 319 $forms[$strategy] = get_string('pluginname', 'workshopform_' . $strategy); 320 } 321 } 322 return $forms; 323 } 324 325 /** 326 * Returns the list of available grading evaluation methods 327 * 328 * @return array of (string)name => (string)localized title 329 */ 330 public static function available_evaluators_list() { 331 $evals = array(); 332 foreach (core_component::get_plugin_list_with_file('workshopeval', 'lib.php', false) as $eval => $evalpath) { 333 $evals[$eval] = get_string('pluginname', 'workshopeval_' . $eval); 334 } 335 return $evals; 336 } 337 338 /** 339 * Return an array of possible values of assessment dimension weight 340 * 341 * @return array of integers 0, 1, 2, ..., 16 342 */ 343 public static function available_dimension_weights_list() { 344 $weights = array(); 345 for ($i=16; $i>=0; $i--) { 346 $weights[$i] = $i; 347 } 348 return $weights; 349 } 350 351 /** 352 * Return an array of possible values of assessment weight 353 * 354 * Note there is no real reason why the maximum value here is 16. It used to be 10 in 355 * workshop 1.x and I just decided to use the same number as in the maximum weight of 356 * a single assessment dimension. 357 * The value looks reasonable, though. Teachers who would want to assign themselves 358 * higher weight probably do not want peer assessment really... 359 * 360 * @return array of integers 0, 1, 2, ..., 16 361 */ 362 public static function available_assessment_weights_list() { 363 $weights = array(); 364 for ($i=16; $i>=0; $i--) { 365 $weights[$i] = $i; 366 } 367 return $weights; 368 } 369 370 /** 371 * Helper function returning the greatest common divisor 372 * 373 * @param int $a 374 * @param int $b 375 * @return int 376 */ 377 public static function gcd($a, $b) { 378 return ($b == 0) ? ($a):(self::gcd($b, $a % $b)); 379 } 380 381 /** 382 * Helper function returning the least common multiple 383 * 384 * @param int $a 385 * @param int $b 386 * @return int 387 */ 388 public static function lcm($a, $b) { 389 return ($a / self::gcd($a,$b)) * $b; 390 } 391 392 /** 393 * Returns an object suitable for strings containing dates/times 394 * 395 * The returned object contains properties date, datefullshort, datetime, ... containing the given 396 * timestamp formatted using strftimedate, strftimedatefullshort, strftimedatetime, ... from the 397 * current lang's langconfig.php 398 * This allows translators and administrators customize the date/time format. 399 * 400 * @param int $timestamp the timestamp in UTC 401 * @return stdclass 402 */ 403 public static function timestamp_formats($timestamp) { 404 $formats = array('date', 'datefullshort', 'dateshort', 'datetime', 405 'datetimeshort', 'daydate', 'daydatetime', 'dayshort', 'daytime', 406 'monthyear', 'recent', 'recentfull', 'time'); 407 $a = new stdclass(); 408 foreach ($formats as $format) { 409 $a->{$format} = userdate($timestamp, get_string('strftime'.$format, 'langconfig')); 410 } 411 $day = userdate($timestamp, '%Y%m%d', 99, false); 412 $today = userdate(time(), '%Y%m%d', 99, false); 413 $tomorrow = userdate(time() + DAYSECS, '%Y%m%d', 99, false); 414 $yesterday = userdate(time() - DAYSECS, '%Y%m%d', 99, false); 415 $distance = (int)round(abs(time() - $timestamp) / DAYSECS); 416 if ($day == $today) { 417 $a->distanceday = get_string('daystoday', 'workshop'); 418 } elseif ($day == $yesterday) { 419 $a->distanceday = get_string('daysyesterday', 'workshop'); 420 } elseif ($day < $today) { 421 $a->distanceday = get_string('daysago', 'workshop', $distance); 422 } elseif ($day == $tomorrow) { 423 $a->distanceday = get_string('daystomorrow', 'workshop'); 424 } elseif ($day > $today) { 425 $a->distanceday = get_string('daysleft', 'workshop', $distance); 426 } 427 return $a; 428 } 429 430 /** 431 * Converts the argument into an array (list) of file extensions. 432 * 433 * The list can be separated by whitespace, end of lines, commas colons and semicolons. 434 * Empty values are not returned. Values are converted to lowercase. 435 * Duplicates are removed. Glob evaluation is not supported. 436 * 437 * @deprecated since Moodle 3.4 MDL-56486 - please use the {@link core_form\filetypes_util} 438 * @param string|array $extensions list of file extensions 439 * @return array of strings 440 */ 441 public static function normalize_file_extensions($extensions) { 442 443 debugging('The method workshop::normalize_file_extensions() is deprecated. 444 Please use the methods provided by the \core_form\filetypes_util class.', DEBUG_DEVELOPER); 445 446 if ($extensions === '') { 447 return array(); 448 } 449 450 if (!is_array($extensions)) { 451 $extensions = preg_split('/[\s,;:"\']+/', $extensions, null, PREG_SPLIT_NO_EMPTY); 452 } 453 454 foreach ($extensions as $i => $extension) { 455 $extension = str_replace('*.', '', $extension); 456 $extension = strtolower($extension); 457 $extension = ltrim($extension, '.'); 458 $extension = trim($extension); 459 $extensions[$i] = $extension; 460 } 461 462 foreach ($extensions as $i => $extension) { 463 if (strpos($extension, '*') !== false or strpos($extension, '?') !== false) { 464 unset($extensions[$i]); 465 } 466 } 467 468 $extensions = array_filter($extensions, 'strlen'); 469 $extensions = array_keys(array_flip($extensions)); 470 471 foreach ($extensions as $i => $extension) { 472 $extensions[$i] = '.'.$extension; 473 } 474 475 return $extensions; 476 } 477 478 /** 479 * Cleans the user provided list of file extensions. 480 * 481 * @deprecated since Moodle 3.4 MDL-56486 - please use the {@link core_form\filetypes_util} 482 * @param string $extensions 483 * @return string 484 */ 485 public static function clean_file_extensions($extensions) { 486 487 debugging('The method workshop::clean_file_extensions() is deprecated. 488 Please use the methods provided by the \core_form\filetypes_util class.', DEBUG_DEVELOPER); 489 490 $extensions = self::normalize_file_extensions($extensions); 491 492 foreach ($extensions as $i => $extension) { 493 $extensions[$i] = ltrim($extension, '.'); 494 } 495 496 return implode(', ', $extensions); 497 } 498 499 /** 500 * Check given file types and return invalid/unknown ones. 501 * 502 * Empty allowlist is interpretted as "any extension is valid". 503 * 504 * @deprecated since Moodle 3.4 MDL-56486 - please use the {@link core_form\filetypes_util} 505 * @param string|array $extensions list of file extensions 506 * @param string|array $allowlist list of valid extensions 507 * @return array list of invalid extensions not found in the allowlist 508 */ 509 public static function invalid_file_extensions($extensions, $allowlist) { 510 511 debugging('The method workshop::invalid_file_extensions() is deprecated. 512 Please use the methods provided by the \core_form\filetypes_util class.', DEBUG_DEVELOPER); 513 514 $extensions = self::normalize_file_extensions($extensions); 515 $allowlist = self::normalize_file_extensions($allowlist); 516 517 if (empty($extensions) or empty($allowlist)) { 518 return array(); 519 } 520 521 // Return those items from $extensions that are not present in $allowlist. 522 return array_keys(array_diff_key(array_flip($extensions), array_flip($allowlist))); 523 } 524 525 /** 526 * Is the file have allowed to be uploaded to the workshop? 527 * 528 * Empty allowlist is interpretted as "any file type is allowed" rather 529 * than "no file can be uploaded". 530 * 531 * @deprecated since Moodle 3.4 MDL-56486 - please use the {@link core_form\filetypes_util} 532 * @param string $filename the file name 533 * @param string|array $allowlist list of allowed file extensions 534 * @return false 535 */ 536 public static function is_allowed_file_type($filename, $allowlist) { 537 538 debugging('The method workshop::is_allowed_file_type() is deprecated. 539 Please use the methods provided by the \core_form\filetypes_util class.', DEBUG_DEVELOPER); 540 541 $allowlist = self::normalize_file_extensions($allowlist); 542 543 if (empty($allowlist)) { 544 return true; 545 } 546 547 $haystack = strrev(trim(strtolower($filename))); 548 549 foreach ($allowlist as $extension) { 550 if (strpos($haystack, strrev($extension)) === 0) { 551 // The file name ends with the extension. 552 return true; 553 } 554 } 555 556 return false; 557 } 558 559 //////////////////////////////////////////////////////////////////////////////// 560 // Workshop API // 561 //////////////////////////////////////////////////////////////////////////////// 562 563 /** 564 * Fetches all enrolled users with the capability mod/workshop:submit in the current workshop 565 * 566 * The returned objects contain properties required by user_picture and are ordered by lastname, firstname. 567 * Only users with the active enrolment are returned. 568 * 569 * @param bool $musthavesubmission if true, return only users who have already submitted 570 * @param int $groupid 0 means ignore groups, any other value limits the result by group id 571 * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set) 572 * @param int $limitnum return a subset containing this number of records (optional, required if $limitfrom is set) 573 * @return array array[userid] => stdClass 574 */ 575 public function get_potential_authors($musthavesubmission=true, $groupid=0, $limitfrom=0, $limitnum=0) { 576 global $DB; 577 578 list($sql, $params) = $this->get_users_with_capability_sql('mod/workshop:submit', $musthavesubmission, $groupid); 579 580 if (empty($sql)) { 581 return array(); 582 } 583 584 list($sort, $sortparams) = users_order_by_sql('tmp'); 585 $sql = "SELECT * 586 FROM ($sql) tmp 587 ORDER BY $sort"; 588 589 return $DB->get_records_sql($sql, array_merge($params, $sortparams), $limitfrom, $limitnum); 590 } 591 592 /** 593 * Returns the total number of users that would be fetched by {@link self::get_potential_authors()} 594 * 595 * @param bool $musthavesubmission if true, count only users who have already submitted 596 * @param int $groupid 0 means ignore groups, any other value limits the result by group id 597 * @return int 598 */ 599 public function count_potential_authors($musthavesubmission=true, $groupid=0) { 600 global $DB; 601 602 list($sql, $params) = $this->get_users_with_capability_sql('mod/workshop:submit', $musthavesubmission, $groupid); 603 604 if (empty($sql)) { 605 return 0; 606 } 607 608 $sql = "SELECT COUNT(*) 609 FROM ($sql) tmp"; 610 611 return $DB->count_records_sql($sql, $params); 612 } 613 614 /** 615 * Fetches all enrolled users with the capability mod/workshop:peerassess in the current workshop 616 * 617 * The returned objects contain properties required by user_picture and are ordered by lastname, firstname. 618 * Only users with the active enrolment are returned. 619 * 620 * @param bool $musthavesubmission if true, return only users who have already submitted 621 * @param int $groupid 0 means ignore groups, any other value limits the result by group id 622 * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set) 623 * @param int $limitnum return a subset containing this number of records (optional, required if $limitfrom is set) 624 * @return array array[userid] => stdClass 625 */ 626 public function get_potential_reviewers($musthavesubmission=false, $groupid=0, $limitfrom=0, $limitnum=0) { 627 global $DB; 628 629 list($sql, $params) = $this->get_users_with_capability_sql('mod/workshop:peerassess', $musthavesubmission, $groupid); 630 631 if (empty($sql)) { 632 return array(); 633 } 634 635 list($sort, $sortparams) = users_order_by_sql('tmp'); 636 $sql = "SELECT * 637 FROM ($sql) tmp 638 ORDER BY $sort"; 639 640 return $DB->get_records_sql($sql, array_merge($params, $sortparams), $limitfrom, $limitnum); 641 } 642 643 /** 644 * Returns the total number of users that would be fetched by {@link self::get_potential_reviewers()} 645 * 646 * @param bool $musthavesubmission if true, count only users who have already submitted 647 * @param int $groupid 0 means ignore groups, any other value limits the result by group id 648 * @return int 649 */ 650 public function count_potential_reviewers($musthavesubmission=false, $groupid=0) { 651 global $DB; 652 653 list($sql, $params) = $this->get_users_with_capability_sql('mod/workshop:peerassess', $musthavesubmission, $groupid); 654 655 if (empty($sql)) { 656 return 0; 657 } 658 659 $sql = "SELECT COUNT(*) 660 FROM ($sql) tmp"; 661 662 return $DB->count_records_sql($sql, $params); 663 } 664 665 /** 666 * Fetches all enrolled users that are authors or reviewers (or both) in the current workshop 667 * 668 * The returned objects contain properties required by user_picture and are ordered by lastname, firstname. 669 * Only users with the active enrolment are returned. 670 * 671 * @see self::get_potential_authors() 672 * @see self::get_potential_reviewers() 673 * @param bool $musthavesubmission if true, return only users who have already submitted 674 * @param int $groupid 0 means ignore groups, any other value limits the result by group id 675 * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set) 676 * @param int $limitnum return a subset containing this number of records (optional, required if $limitfrom is set) 677 * @return array array[userid] => stdClass 678 */ 679 public function get_participants($musthavesubmission=false, $groupid=0, $limitfrom=0, $limitnum=0) { 680 global $DB; 681 682 list($sql, $params) = $this->get_participants_sql($musthavesubmission, $groupid); 683 684 if (empty($sql)) { 685 return array(); 686 } 687 688 list($sort, $sortparams) = users_order_by_sql('tmp'); 689 $sql = "SELECT * 690 FROM ($sql) tmp 691 ORDER BY $sort"; 692 693 return $DB->get_records_sql($sql, array_merge($params, $sortparams), $limitfrom, $limitnum); 694 } 695 696 /** 697 * Returns the total number of records that would be returned by {@link self::get_participants()} 698 * 699 * @param bool $musthavesubmission if true, return only users who have already submitted 700 * @param int $groupid 0 means ignore groups, any other value limits the result by group id 701 * @return int 702 */ 703 public function count_participants($musthavesubmission=false, $groupid=0) { 704 global $DB; 705 706 list($sql, $params) = $this->get_participants_sql($musthavesubmission, $groupid); 707 708 if (empty($sql)) { 709 return 0; 710 } 711 712 $sql = "SELECT COUNT(*) 713 FROM ($sql) tmp"; 714 715 return $DB->count_records_sql($sql, $params); 716 } 717 718 /** 719 * Checks if the given user is an actively enrolled participant in the workshop 720 * 721 * @param int $userid, defaults to the current $USER 722 * @return boolean 723 */ 724 public function is_participant($userid=null) { 725 global $USER, $DB; 726 727 if (is_null($userid)) { 728 $userid = $USER->id; 729 } 730 731 list($sql, $params) = $this->get_participants_sql(); 732 733 if (empty($sql)) { 734 return false; 735 } 736 737 $sql = "SELECT COUNT(*) 738 FROM {user} uxx 739 JOIN ({$sql}) pxx ON uxx.id = pxx.id 740 WHERE uxx.id = :uxxid"; 741 $params['uxxid'] = $userid; 742 743 if ($DB->count_records_sql($sql, $params)) { 744 return true; 745 } 746 747 return false; 748 } 749 750 /** 751 * Groups the given users by the group membership 752 * 753 * This takes the module grouping settings into account. If a grouping is 754 * set, returns only groups withing the course module grouping. Always 755 * returns group [0] with all the given users. 756 * 757 * @param array $users array[userid] => stdclass{->id ->lastname ->firstname} 758 * @return array array[groupid][userid] => stdclass{->id ->lastname ->firstname} 759 */ 760 public function get_grouped($users) { 761 global $DB; 762 global $CFG; 763 764 $grouped = array(); // grouped users to be returned 765 if (empty($users)) { 766 return $grouped; 767 } 768 if ($this->cm->groupingid) { 769 // Group workshop set to specified grouping - only consider groups 770 // within this grouping, and leave out users who aren't members of 771 // this grouping. 772 $groupingid = $this->cm->groupingid; 773 // All users that are members of at least one group will be 774 // added into a virtual group id 0 775 $grouped[0] = array(); 776 } else { 777 $groupingid = 0; 778 // there is no need to be member of a group so $grouped[0] will contain 779 // all users 780 $grouped[0] = $users; 781 } 782 $gmemberships = groups_get_all_groups($this->cm->course, array_keys($users), $groupingid, 783 'gm.id,gm.groupid,gm.userid'); 784 foreach ($gmemberships as $gmembership) { 785 if (!isset($grouped[$gmembership->groupid])) { 786 $grouped[$gmembership->groupid] = array(); 787 } 788 $grouped[$gmembership->groupid][$gmembership->userid] = $users[$gmembership->userid]; 789 $grouped[0][$gmembership->userid] = $users[$gmembership->userid]; 790 } 791 return $grouped; 792 } 793 794 /** 795 * Returns the list of all allocations (i.e. assigned assessments) in the workshop 796 * 797 * Assessments of example submissions are ignored 798 * 799 * @return array 800 */ 801 public function get_allocations() { 802 global $DB; 803 804 $sql = 'SELECT a.id, a.submissionid, a.reviewerid, s.authorid 805 FROM {workshop_assessments} a 806 INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id) 807 WHERE s.example = 0 AND s.workshopid = :workshopid'; 808 $params = array('workshopid' => $this->id); 809 810 return $DB->get_records_sql($sql, $params); 811 } 812 813 /** 814 * Returns the total number of records that would be returned by {@link self::get_submissions()} 815 * 816 * @param mixed $authorid int|array|'all' If set to [array of] integer, return submission[s] of the given user[s] only 817 * @param int $groupid If non-zero, return only submissions by authors in the specified group 818 * @return int number of records 819 */ 820 public function count_submissions($authorid='all', $groupid=0) { 821 global $DB; 822 823 $params = array('workshopid' => $this->id); 824 $sql = "SELECT COUNT(s.id) 825 FROM {workshop_submissions} s 826 JOIN {user} u ON (s.authorid = u.id)"; 827 if ($groupid) { 828 $sql .= " JOIN {groups_members} gm ON (gm.userid = u.id AND gm.groupid = :groupid)"; 829 $params['groupid'] = $groupid; 830 } 831 $sql .= " WHERE s.example = 0 AND s.workshopid = :workshopid"; 832 833 if ('all' === $authorid) { 834 // no additional conditions 835 } elseif (!empty($authorid)) { 836 list($usql, $uparams) = $DB->get_in_or_equal($authorid, SQL_PARAMS_NAMED); 837 $sql .= " AND authorid $usql"; 838 $params = array_merge($params, $uparams); 839 } else { 840 // $authorid is empty 841 return 0; 842 } 843 844 return $DB->count_records_sql($sql, $params); 845 } 846 847 848 /** 849 * Returns submissions from this workshop 850 * 851 * Fetches data from {workshop_submissions} and adds some useful information from other 852 * tables. Does not return textual fields to prevent possible memory lack issues. 853 * 854 * @see self::count_submissions() 855 * @param mixed $authorid int|array|'all' If set to [array of] integer, return submission[s] of the given user[s] only 856 * @param int $groupid If non-zero, return only submissions by authors in the specified group 857 * @param int $limitfrom Return a subset of records, starting at this point (optional) 858 * @param int $limitnum Return a subset containing this many records in total (optional, required if $limitfrom is set) 859 * @return array of records or an empty array 860 */ 861 public function get_submissions($authorid='all', $groupid=0, $limitfrom=0, $limitnum=0) { 862 global $DB; 863 864 $userfieldsapi = \core_user\fields::for_userpic(); 865 $authorfields = $userfieldsapi->get_sql('u', false, 'author', 'authoridx', false)->selects; 866 $gradeoverbyfields = $userfieldsapi->get_sql('t', false, 'over', 'gradeoverbyx', false)->selects; 867 $params = array('workshopid' => $this->id); 868 $sql = "SELECT s.id, s.workshopid, s.example, s.authorid, s.timecreated, s.timemodified, 869 s.title, s.grade, s.gradeover, s.gradeoverby, s.published, 870 $authorfields, $gradeoverbyfields 871 FROM {workshop_submissions} s 872 JOIN {user} u ON (s.authorid = u.id)"; 873 if ($groupid) { 874 $sql .= " JOIN {groups_members} gm ON (gm.userid = u.id AND gm.groupid = :groupid)"; 875 $params['groupid'] = $groupid; 876 } 877 $sql .= " LEFT JOIN {user} t ON (s.gradeoverby = t.id) 878 WHERE s.example = 0 AND s.workshopid = :workshopid"; 879 880 if ('all' === $authorid) { 881 // no additional conditions 882 } elseif (!empty($authorid)) { 883 list($usql, $uparams) = $DB->get_in_or_equal($authorid, SQL_PARAMS_NAMED); 884 $sql .= " AND authorid $usql"; 885 $params = array_merge($params, $uparams); 886 } else { 887 // $authorid is empty 888 return array(); 889 } 890 list($sort, $sortparams) = users_order_by_sql('u'); 891 $sql .= " ORDER BY $sort"; 892 893 return $DB->get_records_sql($sql, array_merge($params, $sortparams), $limitfrom, $limitnum); 894 } 895 896 /** 897 * Returns submissions from this workshop that are viewable by the current user (except example submissions). 898 * 899 * @param mixed $authorid int|array If set to [array of] integer, return submission[s] of the given user[s] only 900 * @param int $groupid If non-zero, return only submissions by authors in the specified group. 0 for all groups. 901 * @param int $limitfrom Return a subset of records, starting at this point (optional) 902 * @param int $limitnum Return a subset containing this many records in total (optional, required if $limitfrom is set) 903 * @return array of records and the total submissions count 904 * @since Moodle 3.4 905 */ 906 public function get_visible_submissions($authorid = 0, $groupid = 0, $limitfrom = 0, $limitnum = 0) { 907 global $DB, $USER; 908 909 $submissions = array(); 910 $select = "SELECT s.*"; 911 $selectcount = "SELECT COUNT(s.id)"; 912 $from = " FROM {workshop_submissions} s"; 913 $params = array('workshopid' => $this->id); 914 915 // Check if the passed group (or all groups when groupid is 0) is visible by the current user. 916 if (!groups_group_visible($groupid, $this->course, $this->cm)) { 917 return array($submissions, 0); 918 } 919 920 if ($groupid) { 921 $from .= " JOIN {groups_members} gm ON (gm.userid = s.authorid AND gm.groupid = :groupid)"; 922 $params['groupid'] = $groupid; 923 } 924 $where = " WHERE s.workshopid = :workshopid AND s.example = 0"; 925 926 if (!has_capability('mod/workshop:viewallsubmissions', $this->context)) { 927 // Check published submissions. 928 $workshopclosed = $this->phase == self::PHASE_CLOSED; 929 $canviewpublished = has_capability('mod/workshop:viewpublishedsubmissions', $this->context); 930 if ($workshopclosed && $canviewpublished) { 931 $published = " OR s.published = 1"; 932 } else { 933 $published = ''; 934 } 935 936 // Always get submissions I did or I provided feedback to. 937 $where .= " AND (s.authorid = :authorid OR s.gradeoverby = :graderid $published)"; 938 $params['authorid'] = $USER->id; 939 $params['graderid'] = $USER->id; 940 } 941 942 // Now, user filtering. 943 if (!empty($authorid)) { 944 list($usql, $uparams) = $DB->get_in_or_equal($authorid, SQL_PARAMS_NAMED); 945 $where .= " AND s.authorid $usql"; 946 $params = array_merge($params, $uparams); 947 } 948 949 $order = " ORDER BY s.timecreated"; 950 951 $totalcount = $DB->count_records_sql($selectcount.$from.$where, $params); 952 if ($totalcount) { 953 $submissions = $DB->get_records_sql($select.$from.$where.$order, $params, $limitfrom, $limitnum); 954 } 955 return array($submissions, $totalcount); 956 } 957 958 959 /** 960 * Returns a submission record with the author's data 961 * 962 * @param int $id submission id 963 * @return stdclass 964 */ 965 public function get_submission_by_id($id) { 966 global $DB; 967 968 // we intentionally check the workshopid here, too, so the workshop can't touch submissions 969 // from other instances 970 $userfieldsapi = \core_user\fields::for_userpic(); 971 $authorfields = $userfieldsapi->get_sql('u', false, 'author', 'authoridx', false)->selects; 972 $gradeoverbyfields = $userfieldsapi->get_sql('g', false, 'gradeoverby', 'gradeoverbyx', false)->selects; 973 $sql = "SELECT s.*, $authorfields, $gradeoverbyfields 974 FROM {workshop_submissions} s 975 INNER JOIN {user} u ON (s.authorid = u.id) 976 LEFT JOIN {user} g ON (s.gradeoverby = g.id) 977 WHERE s.example = 0 AND s.workshopid = :workshopid AND s.id = :id"; 978 $params = array('workshopid' => $this->id, 'id' => $id); 979 return $DB->get_record_sql($sql, $params, MUST_EXIST); 980 } 981 982 /** 983 * Returns a submission submitted by the given author 984 * 985 * @param int $id author id 986 * @return stdclass|false 987 */ 988 public function get_submission_by_author($authorid) { 989 global $DB; 990 991 if (empty($authorid)) { 992 return false; 993 } 994 $userfieldsapi = \core_user\fields::for_userpic(); 995 $authorfields = $userfieldsapi->get_sql('u', false, 'author', 'authoridx', false)->selects; 996 $gradeoverbyfields = $userfieldsapi->get_sql('g', false, 'gradeoverby', 'gradeoverbyx', false)->selects; 997 $sql = "SELECT s.*, $authorfields, $gradeoverbyfields 998 FROM {workshop_submissions} s 999 INNER JOIN {user} u ON (s.authorid = u.id) 1000 LEFT JOIN {user} g ON (s.gradeoverby = g.id) 1001 WHERE s.example = 0 AND s.workshopid = :workshopid AND s.authorid = :authorid"; 1002 $params = array('workshopid' => $this->id, 'authorid' => $authorid); 1003 return $DB->get_record_sql($sql, $params); 1004 } 1005 1006 /** 1007 * Returns published submissions with their authors data 1008 * 1009 * @return array of stdclass 1010 */ 1011 public function get_published_submissions($orderby='finalgrade DESC') { 1012 global $DB; 1013 1014 $userfieldsapi = \core_user\fields::for_userpic(); 1015 $authorfields = $userfieldsapi->get_sql('u', false, 'author', 'authoridx', false)->selects; 1016 $sql = "SELECT s.id, s.authorid, s.timecreated, s.timemodified, 1017 s.title, s.grade, s.gradeover, COALESCE(s.gradeover,s.grade) AS finalgrade, 1018 $authorfields 1019 FROM {workshop_submissions} s 1020 INNER JOIN {user} u ON (s.authorid = u.id) 1021 WHERE s.example = 0 AND s.workshopid = :workshopid AND s.published = 1 1022 ORDER BY $orderby"; 1023 $params = array('workshopid' => $this->id); 1024 return $DB->get_records_sql($sql, $params); 1025 } 1026 1027 /** 1028 * Returns full record of the given example submission 1029 * 1030 * @param int $id example submission od 1031 * @return object 1032 */ 1033 public function get_example_by_id($id) { 1034 global $DB; 1035 return $DB->get_record('workshop_submissions', 1036 array('id' => $id, 'workshopid' => $this->id, 'example' => 1), '*', MUST_EXIST); 1037 } 1038 1039 /** 1040 * Returns the list of example submissions in this workshop with reference assessments attached 1041 * 1042 * @return array of objects or an empty array 1043 * @see workshop::prepare_example_summary() 1044 */ 1045 public function get_examples_for_manager() { 1046 global $DB; 1047 1048 $sql = 'SELECT s.id, s.title, 1049 a.id AS assessmentid, a.grade, a.gradinggrade 1050 FROM {workshop_submissions} s 1051 LEFT JOIN {workshop_assessments} a ON (a.submissionid = s.id AND a.weight = 1) 1052 WHERE s.example = 1 AND s.workshopid = :workshopid 1053 ORDER BY s.title'; 1054 return $DB->get_records_sql($sql, array('workshopid' => $this->id)); 1055 } 1056 1057 /** 1058 * Returns the list of all example submissions in this workshop with the information of assessments done by the given user 1059 * 1060 * @param int $reviewerid user id 1061 * @return array of objects, indexed by example submission id 1062 * @see workshop::prepare_example_summary() 1063 */ 1064 public function get_examples_for_reviewer($reviewerid) { 1065 global $DB; 1066 1067 if (empty($reviewerid)) { 1068 return false; 1069 } 1070 $sql = 'SELECT s.id, s.title, 1071 a.id AS assessmentid, a.grade, a.gradinggrade 1072 FROM {workshop_submissions} s 1073 LEFT JOIN {workshop_assessments} a ON (a.submissionid = s.id AND a.reviewerid = :reviewerid AND a.weight = 0) 1074 WHERE s.example = 1 AND s.workshopid = :workshopid 1075 ORDER BY s.title'; 1076 return $DB->get_records_sql($sql, array('workshopid' => $this->id, 'reviewerid' => $reviewerid)); 1077 } 1078 1079 /** 1080 * Prepares renderable submission component 1081 * 1082 * @param stdClass $record required by {@see workshop_submission} 1083 * @param bool $showauthor show the author-related information 1084 * @return workshop_submission 1085 */ 1086 public function prepare_submission(stdClass $record, $showauthor = false) { 1087 1088 $submission = new workshop_submission($this, $record, $showauthor); 1089 $submission->url = $this->submission_url($record->id); 1090 1091 return $submission; 1092 } 1093 1094 /** 1095 * Prepares renderable submission summary component 1096 * 1097 * @param stdClass $record required by {@see workshop_submission_summary} 1098 * @param bool $showauthor show the author-related information 1099 * @return workshop_submission_summary 1100 */ 1101 public function prepare_submission_summary(stdClass $record, $showauthor = false) { 1102 1103 $summary = new workshop_submission_summary($this, $record, $showauthor); 1104 $summary->url = $this->submission_url($record->id); 1105 1106 return $summary; 1107 } 1108 1109 /** 1110 * Prepares renderable example submission component 1111 * 1112 * @param stdClass $record required by {@see workshop_example_submission} 1113 * @return workshop_example_submission 1114 */ 1115 public function prepare_example_submission(stdClass $record) { 1116 1117 $example = new workshop_example_submission($this, $record); 1118 1119 return $example; 1120 } 1121 1122 /** 1123 * Prepares renderable example submission summary component 1124 * 1125 * If the example is editable, the caller must set the 'editable' flag explicitly. 1126 * 1127 * @param stdClass $example as returned by {@link workshop::get_examples_for_manager()} or {@link workshop::get_examples_for_reviewer()} 1128 * @return workshop_example_submission_summary to be rendered 1129 */ 1130 public function prepare_example_summary(stdClass $example) { 1131 1132 $summary = new workshop_example_submission_summary($this, $example); 1133 1134 if (is_null($example->grade)) { 1135 $summary->status = 'notgraded'; 1136 $summary->assesslabel = get_string('assess', 'workshop'); 1137 } else { 1138 $summary->status = 'graded'; 1139 $summary->assesslabel = get_string('reassess', 'workshop'); 1140 } 1141 1142 $summary->gradeinfo = new stdclass(); 1143 $summary->gradeinfo->received = $this->real_grade($example->grade); 1144 $summary->gradeinfo->max = $this->real_grade(100); 1145 1146 $summary->url = new moodle_url($this->exsubmission_url($example->id)); 1147 $summary->editurl = new moodle_url($this->exsubmission_url($example->id), array('edit' => 'on')); 1148 $summary->assessurl = new moodle_url($this->exsubmission_url($example->id), array('assess' => 'on', 'sesskey' => sesskey())); 1149 1150 return $summary; 1151 } 1152 1153 /** 1154 * Prepares renderable assessment component 1155 * 1156 * The $options array supports the following keys: 1157 * showauthor - should the author user info be available for the renderer 1158 * showreviewer - should the reviewer user info be available for the renderer 1159 * showform - show the assessment form if it is available 1160 * showweight - should the assessment weight be available for the renderer 1161 * 1162 * @param stdClass $record as returned by eg {@link self::get_assessment_by_id()} 1163 * @param workshop_assessment_form|null $form as returned by {@link workshop_strategy::get_assessment_form()} 1164 * @param array $options 1165 * @return workshop_assessment 1166 */ 1167 public function prepare_assessment(stdClass $record, $form, array $options = array()) { 1168 1169 $assessment = new workshop_assessment($this, $record, $options); 1170 $assessment->url = $this->assess_url($record->id); 1171 $assessment->maxgrade = $this->real_grade(100); 1172 1173 if (!empty($options['showform']) and !($form instanceof workshop_assessment_form)) { 1174 debugging('Not a valid instance of workshop_assessment_form supplied', DEBUG_DEVELOPER); 1175 } 1176 1177 if (!empty($options['showform']) and ($form instanceof workshop_assessment_form)) { 1178 $assessment->form = $form; 1179 } 1180 1181 if (empty($options['showweight'])) { 1182 $assessment->weight = null; 1183 } 1184 1185 if (!is_null($record->grade)) { 1186 $assessment->realgrade = $this->real_grade($record->grade); 1187 } 1188 1189 return $assessment; 1190 } 1191 1192 /** 1193 * Prepares renderable example submission's assessment component 1194 * 1195 * The $options array supports the following keys: 1196 * showauthor - should the author user info be available for the renderer 1197 * showreviewer - should the reviewer user info be available for the renderer 1198 * showform - show the assessment form if it is available 1199 * 1200 * @param stdClass $record as returned by eg {@link self::get_assessment_by_id()} 1201 * @param workshop_assessment_form|null $form as returned by {@link workshop_strategy::get_assessment_form()} 1202 * @param array $options 1203 * @return workshop_example_assessment 1204 */ 1205 public function prepare_example_assessment(stdClass $record, $form = null, array $options = array()) { 1206 1207 $assessment = new workshop_example_assessment($this, $record, $options); 1208 $assessment->url = $this->exassess_url($record->id); 1209 $assessment->maxgrade = $this->real_grade(100); 1210 1211 if (!empty($options['showform']) and !($form instanceof workshop_assessment_form)) { 1212 debugging('Not a valid instance of workshop_assessment_form supplied', DEBUG_DEVELOPER); 1213 } 1214 1215 if (!empty($options['showform']) and ($form instanceof workshop_assessment_form)) { 1216 $assessment->form = $form; 1217 } 1218 1219 if (!is_null($record->grade)) { 1220 $assessment->realgrade = $this->real_grade($record->grade); 1221 } 1222 1223 $assessment->weight = null; 1224 1225 return $assessment; 1226 } 1227 1228 /** 1229 * Prepares renderable example submission's reference assessment component 1230 * 1231 * The $options array supports the following keys: 1232 * showauthor - should the author user info be available for the renderer 1233 * showreviewer - should the reviewer user info be available for the renderer 1234 * showform - show the assessment form if it is available 1235 * 1236 * @param stdClass $record as returned by eg {@link self::get_assessment_by_id()} 1237 * @param workshop_assessment_form|null $form as returned by {@link workshop_strategy::get_assessment_form()} 1238 * @param array $options 1239 * @return workshop_example_reference_assessment 1240 */ 1241 public function prepare_example_reference_assessment(stdClass $record, $form = null, array $options = array()) { 1242 1243 $assessment = new workshop_example_reference_assessment($this, $record, $options); 1244 $assessment->maxgrade = $this->real_grade(100); 1245 1246 if (!empty($options['showform']) and !($form instanceof workshop_assessment_form)) { 1247 debugging('Not a valid instance of workshop_assessment_form supplied', DEBUG_DEVELOPER); 1248 } 1249 1250 if (!empty($options['showform']) and ($form instanceof workshop_assessment_form)) { 1251 $assessment->form = $form; 1252 } 1253 1254 if (!is_null($record->grade)) { 1255 $assessment->realgrade = $this->real_grade($record->grade); 1256 } 1257 1258 $assessment->weight = null; 1259 1260 return $assessment; 1261 } 1262 1263 /** 1264 * Removes the submission and all relevant data 1265 * 1266 * @param stdClass $submission record to delete 1267 * @return void 1268 */ 1269 public function delete_submission(stdclass $submission) { 1270 global $DB; 1271 1272 $assessments = $DB->get_records('workshop_assessments', array('submissionid' => $submission->id), '', 'id'); 1273 $this->delete_assessment(array_keys($assessments)); 1274 1275 $fs = get_file_storage(); 1276 $fs->delete_area_files($this->context->id, 'mod_workshop', 'submission_content', $submission->id); 1277 $fs->delete_area_files($this->context->id, 'mod_workshop', 'submission_attachment', $submission->id); 1278 1279 $DB->delete_records('workshop_submissions', array('id' => $submission->id)); 1280 1281 // Event information. 1282 $params = array( 1283 'context' => $this->context, 1284 'courseid' => $this->course->id, 1285 'relateduserid' => $submission->authorid, 1286 'other' => array( 1287 'submissiontitle' => $submission->title 1288 ) 1289 ); 1290 $params['objectid'] = $submission->id; 1291 $event = \mod_workshop\event\submission_deleted::create($params); 1292 $event->add_record_snapshot('workshop', $this->dbrecord); 1293 $event->trigger(); 1294 } 1295 1296 /** 1297 * Returns the list of all assessments in the workshop with some data added 1298 * 1299 * Fetches data from {workshop_assessments} and adds some useful information from other 1300 * tables. The returned object does not contain textual fields (i.e. comments) to prevent memory 1301 * lack issues. 1302 * 1303 * @return array [assessmentid] => assessment stdclass 1304 */ 1305 public function get_all_assessments() { 1306 global $DB; 1307 1308 $userfieldsapi = \core_user\fields::for_userpic(); 1309 $reviewerfields = $userfieldsapi->get_sql('reviewer', false, '', 'revieweridx', false)->selects; 1310 $authorfields = $userfieldsapi->get_sql('author', false, 'author', 'authorid', false)->selects; 1311 $overbyfields = $userfieldsapi->get_sql('overby', false, 'overby', 'gradinggradeoverbyx', false)->selects; 1312 list($sort, $params) = users_order_by_sql('reviewer'); 1313 $sql = "SELECT a.id, a.submissionid, a.reviewerid, a.timecreated, a.timemodified, 1314 a.grade, a.gradinggrade, a.gradinggradeover, a.gradinggradeoverby, 1315 $reviewerfields, $authorfields, $overbyfields, 1316 s.title 1317 FROM {workshop_assessments} a 1318 INNER JOIN {user} reviewer ON (a.reviewerid = reviewer.id) 1319 INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id) 1320 INNER JOIN {user} author ON (s.authorid = author.id) 1321 LEFT JOIN {user} overby ON (a.gradinggradeoverby = overby.id) 1322 WHERE s.workshopid = :workshopid AND s.example = 0 1323 ORDER BY $sort"; 1324 $params['workshopid'] = $this->id; 1325 1326 return $DB->get_records_sql($sql, $params); 1327 } 1328 1329 /** 1330 * Get the complete information about the given assessment 1331 * 1332 * @param int $id Assessment ID 1333 * @return stdclass 1334 */ 1335 public function get_assessment_by_id($id) { 1336 global $DB; 1337 1338 $userfieldsapi = \core_user\fields::for_userpic(); 1339 $reviewerfields = $userfieldsapi->get_sql('reviewer', false, 'reviewer', 'revieweridx', false)->selects; 1340 $authorfields = $userfieldsapi->get_sql('author', false, 'author', 'authorid', false)->selects; 1341 $overbyfields = $userfieldsapi->get_sql('overby', false, 'overby', 'gradinggradeoverbyx', false)->selects; 1342 $sql = "SELECT a.*, s.title, $reviewerfields, $authorfields, $overbyfields 1343 FROM {workshop_assessments} a 1344 INNER JOIN {user} reviewer ON (a.reviewerid = reviewer.id) 1345 INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id) 1346 INNER JOIN {user} author ON (s.authorid = author.id) 1347 LEFT JOIN {user} overby ON (a.gradinggradeoverby = overby.id) 1348 WHERE a.id = :id AND s.workshopid = :workshopid"; 1349 $params = array('id' => $id, 'workshopid' => $this->id); 1350 1351 return $DB->get_record_sql($sql, $params, MUST_EXIST); 1352 } 1353 1354 /** 1355 * Get the complete information about the user's assessment of the given submission 1356 * 1357 * @param int $sid submission ID 1358 * @param int $uid user ID of the reviewer 1359 * @return false|stdclass false if not found, stdclass otherwise 1360 */ 1361 public function get_assessment_of_submission_by_user($submissionid, $reviewerid) { 1362 global $DB; 1363 1364 $userfieldsapi = \core_user\fields::for_userpic(); 1365 $reviewerfields = $userfieldsapi->get_sql('reviewer', false, 'reviewer', 'revieweridx', false)->selects; 1366 $authorfields = $userfieldsapi->get_sql('author', false, 'author', 'authorid', false)->selects; 1367 $overbyfields = $userfieldsapi->get_sql('overby', false, 'overby', 'gradinggradeoverbyx', false)->selects; 1368 $sql = "SELECT a.*, s.title, $reviewerfields, $authorfields, $overbyfields 1369 FROM {workshop_assessments} a 1370 INNER JOIN {user} reviewer ON (a.reviewerid = reviewer.id) 1371 INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id AND s.example = 0) 1372 INNER JOIN {user} author ON (s.authorid = author.id) 1373 LEFT JOIN {user} overby ON (a.gradinggradeoverby = overby.id) 1374 WHERE s.id = :sid AND reviewer.id = :rid AND s.workshopid = :workshopid"; 1375 $params = array('sid' => $submissionid, 'rid' => $reviewerid, 'workshopid' => $this->id); 1376 1377 return $DB->get_record_sql($sql, $params, IGNORE_MISSING); 1378 } 1379 1380 /** 1381 * Get the complete information about all assessments of the given submission 1382 * 1383 * @param int $submissionid 1384 * @return array 1385 */ 1386 public function get_assessments_of_submission($submissionid) { 1387 global $DB; 1388 1389 $userfieldsapi = \core_user\fields::for_userpic(); 1390 $reviewerfields = $userfieldsapi->get_sql('reviewer', false, 'reviewer', 'revieweridx', false)->selects; 1391 $overbyfields = $userfieldsapi->get_sql('overby', false, 'overby', 'gradinggradeoverbyx', false)->selects; 1392 list($sort, $params) = users_order_by_sql('reviewer'); 1393 $sql = "SELECT a.*, s.title, $reviewerfields, $overbyfields 1394 FROM {workshop_assessments} a 1395 INNER JOIN {user} reviewer ON (a.reviewerid = reviewer.id) 1396 INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id) 1397 LEFT JOIN {user} overby ON (a.gradinggradeoverby = overby.id) 1398 WHERE s.example = 0 AND s.id = :submissionid AND s.workshopid = :workshopid 1399 ORDER BY $sort"; 1400 $params['submissionid'] = $submissionid; 1401 $params['workshopid'] = $this->id; 1402 1403 return $DB->get_records_sql($sql, $params); 1404 } 1405 1406 /** 1407 * Get the complete information about all assessments allocated to the given reviewer 1408 * 1409 * @param int $reviewerid 1410 * @return array 1411 */ 1412 public function get_assessments_by_reviewer($reviewerid) { 1413 global $DB; 1414 1415 $userfieldsapi = \core_user\fields::for_userpic(); 1416 $reviewerfields = $userfieldsapi->get_sql('reviewer', false, 'reviewer', 'revieweridx', false)->selects; 1417 $authorfields = $userfieldsapi->get_sql('author', false, 'author', 'authorid', false)->selects; 1418 $overbyfields = $userfieldsapi->get_sql('overby', false, 'overby', 'gradinggradeoverbyx', false)->selects; 1419 $sql = "SELECT a.*, $reviewerfields, $authorfields, $overbyfields, 1420 s.id AS submissionid, s.title AS submissiontitle, s.timecreated AS submissioncreated, 1421 s.timemodified AS submissionmodified 1422 FROM {workshop_assessments} a 1423 INNER JOIN {user} reviewer ON (a.reviewerid = reviewer.id) 1424 INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id) 1425 INNER JOIN {user} author ON (s.authorid = author.id) 1426 LEFT JOIN {user} overby ON (a.gradinggradeoverby = overby.id) 1427 WHERE s.example = 0 AND reviewer.id = :reviewerid AND s.workshopid = :workshopid"; 1428 $params = array('reviewerid' => $reviewerid, 'workshopid' => $this->id); 1429 1430 return $DB->get_records_sql($sql, $params); 1431 } 1432 1433 /** 1434 * Get allocated assessments not graded yet by the given reviewer 1435 * 1436 * @see self::get_assessments_by_reviewer() 1437 * @param int $reviewerid the reviewer id 1438 * @param null|int|array $exclude optional assessment id (or list of them) to be excluded 1439 * @return array 1440 */ 1441 public function get_pending_assessments_by_reviewer($reviewerid, $exclude = null) { 1442 1443 $assessments = $this->get_assessments_by_reviewer($reviewerid); 1444 1445 foreach ($assessments as $id => $assessment) { 1446 if (!is_null($assessment->grade)) { 1447 unset($assessments[$id]); 1448 continue; 1449 } 1450 if (!empty($exclude)) { 1451 if (is_array($exclude) and in_array($id, $exclude)) { 1452 unset($assessments[$id]); 1453 continue; 1454 } else if ($id == $exclude) { 1455 unset($assessments[$id]); 1456 continue; 1457 } 1458 } 1459 } 1460 1461 return $assessments; 1462 } 1463 1464 /** 1465 * Allocate a submission to a user for review 1466 * 1467 * @param stdClass $submission Submission object with at least id property 1468 * @param int $reviewerid User ID 1469 * @param int $weight of the new assessment, from 0 to 16 1470 * @param bool $bulk repeated inserts into DB expected 1471 * @return int ID of the new assessment or an error code {@link self::ALLOCATION_EXISTS} if the allocation already exists 1472 */ 1473 public function add_allocation(stdclass $submission, $reviewerid, $weight=1, $bulk=false) { 1474 global $DB; 1475 1476 if ($DB->record_exists('workshop_assessments', array('submissionid' => $submission->id, 'reviewerid' => $reviewerid))) { 1477 return self::ALLOCATION_EXISTS; 1478 } 1479 1480 $weight = (int)$weight; 1481 if ($weight < 0) { 1482 $weight = 0; 1483 } 1484 if ($weight > 16) { 1485 $weight = 16; 1486 } 1487 1488 $now = time(); 1489 $assessment = new stdclass(); 1490 $assessment->submissionid = $submission->id; 1491 $assessment->reviewerid = $reviewerid; 1492 $assessment->timecreated = $now; // do not set timemodified here 1493 $assessment->weight = $weight; 1494 $assessment->feedbackauthorformat = editors_get_preferred_format(); 1495 $assessment->feedbackreviewerformat = editors_get_preferred_format(); 1496 1497 return $DB->insert_record('workshop_assessments', $assessment, true, $bulk); 1498 } 1499 1500 /** 1501 * Delete assessment record or records. 1502 * 1503 * Removes associated records from the workshop_grades table, too. 1504 * 1505 * @param int|array $id assessment id or array of assessments ids 1506 * @todo Give grading strategy plugins a chance to clean up their data, too. 1507 * @return bool true 1508 */ 1509 public function delete_assessment($id) { 1510 global $DB; 1511 1512 if (empty($id)) { 1513 return true; 1514 } 1515 1516 $fs = get_file_storage(); 1517 1518 if (is_array($id)) { 1519 $DB->delete_records_list('workshop_grades', 'assessmentid', $id); 1520 foreach ($id as $itemid) { 1521 $fs->delete_area_files($this->context->id, 'mod_workshop', 'overallfeedback_content', $itemid); 1522 $fs->delete_area_files($this->context->id, 'mod_workshop', 'overallfeedback_attachment', $itemid); 1523 } 1524 $DB->delete_records_list('workshop_assessments', 'id', $id); 1525 1526 } else { 1527 $DB->delete_records('workshop_grades', array('assessmentid' => $id)); 1528 $fs->delete_area_files($this->context->id, 'mod_workshop', 'overallfeedback_content', $id); 1529 $fs->delete_area_files($this->context->id, 'mod_workshop', 'overallfeedback_attachment', $id); 1530 $DB->delete_records('workshop_assessments', array('id' => $id)); 1531 } 1532 1533 return true; 1534 } 1535 1536 /** 1537 * Returns instance of grading strategy class 1538 * 1539 * @return stdclass Instance of a grading strategy 1540 */ 1541 public function grading_strategy_instance() { 1542 global $CFG; // because we require other libs here 1543 1544 if (is_null($this->strategyinstance)) { 1545 $strategylib = __DIR__ . '/form/' . $this->strategy . '/lib.php'; 1546 if (is_readable($strategylib)) { 1547 require_once($strategylib); 1548 } else { 1549 throw new coding_exception('the grading forms subplugin must contain library ' . $strategylib); 1550 } 1551 $classname = 'workshop_' . $this->strategy . '_strategy'; 1552 $this->strategyinstance = new $classname($this); 1553 if (!in_array('workshop_strategy', class_implements($this->strategyinstance))) { 1554 throw new coding_exception($classname . ' does not implement workshop_strategy interface'); 1555 } 1556 } 1557 return $this->strategyinstance; 1558 } 1559 1560 /** 1561 * Sets the current evaluation method to the given plugin. 1562 * 1563 * @param string $method the name of the workshopeval subplugin 1564 * @return bool true if successfully set 1565 * @throws coding_exception if attempting to set a non-installed evaluation method 1566 */ 1567 public function set_grading_evaluation_method($method) { 1568 global $DB; 1569 1570 $evaluationlib = __DIR__ . '/eval/' . $method . '/lib.php'; 1571 1572 if (is_readable($evaluationlib)) { 1573 $this->evaluationinstance = null; 1574 $this->evaluation = $method; 1575 $DB->set_field('workshop', 'evaluation', $method, array('id' => $this->id)); 1576 return true; 1577 } 1578 1579 throw new coding_exception('Attempt to set a non-existing evaluation method.'); 1580 } 1581 1582 /** 1583 * Returns instance of grading evaluation class 1584 * 1585 * @return stdclass Instance of a grading evaluation 1586 */ 1587 public function grading_evaluation_instance() { 1588 global $CFG; // because we require other libs here 1589 1590 if (is_null($this->evaluationinstance)) { 1591 if (empty($this->evaluation)) { 1592 $this->evaluation = 'best'; 1593 } 1594 $evaluationlib = __DIR__ . '/eval/' . $this->evaluation . '/lib.php'; 1595 if (is_readable($evaluationlib)) { 1596 require_once($evaluationlib); 1597 } else { 1598 // Fall back in case the subplugin is not available. 1599 $this->evaluation = 'best'; 1600 $evaluationlib = __DIR__ . '/eval/' . $this->evaluation . '/lib.php'; 1601 if (is_readable($evaluationlib)) { 1602 require_once($evaluationlib); 1603 } else { 1604 // Fall back in case the subplugin is not available any more. 1605 throw new coding_exception('Missing default grading evaluation library ' . $evaluationlib); 1606 } 1607 } 1608 $classname = 'workshop_' . $this->evaluation . '_evaluation'; 1609 $this->evaluationinstance = new $classname($this); 1610 if (!in_array('workshop_evaluation', class_parents($this->evaluationinstance))) { 1611 throw new coding_exception($classname . ' does not extend workshop_evaluation class'); 1612 } 1613 } 1614 return $this->evaluationinstance; 1615 } 1616 1617 /** 1618 * Returns instance of submissions allocator 1619 * 1620 * @param string $method The name of the allocation method, must be PARAM_ALPHA 1621 * @return stdclass Instance of submissions allocator 1622 */ 1623 public function allocator_instance($method) { 1624 global $CFG; // because we require other libs here 1625 1626 $allocationlib = __DIR__ . '/allocation/' . $method . '/lib.php'; 1627 if (is_readable($allocationlib)) { 1628 require_once($allocationlib); 1629 } else { 1630 throw new coding_exception('Unable to find the allocation library ' . $allocationlib); 1631 } 1632 $classname = 'workshop_' . $method . '_allocator'; 1633 return new $classname($this); 1634 } 1635 1636 /** 1637 * @return moodle_url of this workshop's view page 1638 */ 1639 public function view_url() { 1640 global $CFG; 1641 return new moodle_url('/mod/workshop/view.php', array('id' => $this->cm->id)); 1642 } 1643 1644 /** 1645 * @return moodle_url of the page for editing this workshop's grading form 1646 */ 1647 public function editform_url() { 1648 global $CFG; 1649 return new moodle_url('/mod/workshop/editform.php', array('cmid' => $this->cm->id)); 1650 } 1651 1652 /** 1653 * @return moodle_url of the page for previewing this workshop's grading form 1654 */ 1655 public function previewform_url() { 1656 global $CFG; 1657 return new moodle_url('/mod/workshop/editformpreview.php', array('cmid' => $this->cm->id)); 1658 } 1659 1660 /** 1661 * @param int $assessmentid The ID of assessment record 1662 * @return moodle_url of the assessment page 1663 */ 1664 public function assess_url($assessmentid) { 1665 global $CFG; 1666 $assessmentid = clean_param($assessmentid, PARAM_INT); 1667 return new moodle_url('/mod/workshop/assessment.php', array('asid' => $assessmentid)); 1668 } 1669 1670 /** 1671 * @param int $assessmentid The ID of assessment record 1672 * @return moodle_url of the example assessment page 1673 */ 1674 public function exassess_url($assessmentid) { 1675 global $CFG; 1676 $assessmentid = clean_param($assessmentid, PARAM_INT); 1677 return new moodle_url('/mod/workshop/exassessment.php', array('asid' => $assessmentid)); 1678 } 1679 1680 /** 1681 * @return moodle_url of the page to view a submission, defaults to the own one 1682 */ 1683 public function submission_url($id=null) { 1684 global $CFG; 1685 return new moodle_url('/mod/workshop/submission.php', array('cmid' => $this->cm->id, 'id' => $id)); 1686 } 1687 1688 /** 1689 * @param int $id example submission id 1690 * @return moodle_url of the page to view an example submission 1691 */ 1692 public function exsubmission_url($id) { 1693 global $CFG; 1694 return new moodle_url('/mod/workshop/exsubmission.php', array('cmid' => $this->cm->id, 'id' => $id)); 1695 } 1696 1697 /** 1698 * @param int $sid submission id 1699 * @param array $aid of int assessment ids 1700 * @return moodle_url of the page to compare assessments of the given submission 1701 */ 1702 public function compare_url($sid, array $aids) { 1703 global $CFG; 1704 1705 $url = new moodle_url('/mod/workshop/compare.php', array('cmid' => $this->cm->id, 'sid' => $sid)); 1706 $i = 0; 1707 foreach ($aids as $aid) { 1708 $url->param("aid{$i}", $aid); 1709 $i++; 1710 } 1711 return $url; 1712 } 1713 1714 /** 1715 * @param int $sid submission id 1716 * @param int $aid assessment id 1717 * @return moodle_url of the page to compare the reference assessments of the given example submission 1718 */ 1719 public function excompare_url($sid, $aid) { 1720 global $CFG; 1721 return new moodle_url('/mod/workshop/excompare.php', array('cmid' => $this->cm->id, 'sid' => $sid, 'aid' => $aid)); 1722 } 1723 1724 /** 1725 * @return moodle_url of the mod_edit form 1726 */ 1727 public function updatemod_url() { 1728 global $CFG; 1729 return new moodle_url('/course/modedit.php', array('update' => $this->cm->id, 'return' => 1)); 1730 } 1731 1732 /** 1733 * @param string $method allocation method 1734 * @return moodle_url to the allocation page 1735 */ 1736 public function allocation_url($method=null) { 1737 global $CFG; 1738 $params = array('cmid' => $this->cm->id); 1739 if (!empty($method)) { 1740 $params['method'] = $method; 1741 } 1742 return new moodle_url('/mod/workshop/allocation.php', $params); 1743 } 1744 1745 /** 1746 * @param int $phasecode The internal phase code 1747 * @return moodle_url of the script to change the current phase to $phasecode 1748 */ 1749 public function switchphase_url($phasecode) { 1750 global $CFG; 1751 $phasecode = clean_param($phasecode, PARAM_INT); 1752 return new moodle_url('/mod/workshop/switchphase.php', array('cmid' => $this->cm->id, 'phase' => $phasecode)); 1753 } 1754 1755 /** 1756 * @return moodle_url to the aggregation page 1757 */ 1758 public function aggregate_url() { 1759 global $CFG; 1760 return new moodle_url('/mod/workshop/aggregate.php', array('cmid' => $this->cm->id)); 1761 } 1762 1763 /** 1764 * @return moodle_url of this workshop's toolbox page 1765 */ 1766 public function toolbox_url($tool) { 1767 global $CFG; 1768 return new moodle_url('/mod/workshop/toolbox.php', array('id' => $this->cm->id, 'tool' => $tool)); 1769 } 1770 1771 /** 1772 * Workshop wrapper around {@see add_to_log()} 1773 * @deprecated since 2.7 Please use the provided event classes for logging actions. 1774 * 1775 * @param string $action to be logged 1776 * @param moodle_url $url absolute url as returned by {@see workshop::submission_url()} and friends 1777 * @param mixed $info additional info, usually id in a table 1778 * @param bool $return true to return the arguments for add_to_log. 1779 * @return void|array array of arguments for add_to_log if $return is true 1780 */ 1781 public function log($action, moodle_url $url = null, $info = null, $return = false) { 1782 debugging('The log method is now deprecated, please use event classes instead', DEBUG_DEVELOPER); 1783 1784 if (is_null($url)) { 1785 $url = $this->view_url(); 1786 } 1787 1788 if (is_null($info)) { 1789 $info = $this->id; 1790 } 1791 1792 $logurl = $this->log_convert_url($url); 1793 $args = array($this->course->id, 'workshop', $action, $logurl, $info, $this->cm->id); 1794 if ($return) { 1795 return $args; 1796 } 1797 call_user_func_array('add_to_log', $args); 1798 } 1799 1800 /** 1801 * Is the given user allowed to create their submission? 1802 * 1803 * @param int $userid 1804 * @return bool 1805 */ 1806 public function creating_submission_allowed($userid) { 1807 1808 $now = time(); 1809 $ignoredeadlines = has_capability('mod/workshop:ignoredeadlines', $this->context, $userid); 1810 1811 if ($this->latesubmissions) { 1812 if ($this->phase != self::PHASE_SUBMISSION and $this->phase != self::PHASE_ASSESSMENT) { 1813 // late submissions are allowed in the submission and assessment phase only 1814 return false; 1815 } 1816 if (!$ignoredeadlines and !empty($this->submissionstart) and $this->submissionstart > $now) { 1817 // late submissions are not allowed before the submission start 1818 return false; 1819 } 1820 return true; 1821 1822 } else { 1823 if ($this->phase != self::PHASE_SUBMISSION) { 1824 // submissions are allowed during the submission phase only 1825 return false; 1826 } 1827 if (!$ignoredeadlines and !empty($this->submissionstart) and $this->submissionstart > $now) { 1828 // if enabled, submitting is not allowed before the date/time defined in the mod_form 1829 return false; 1830 } 1831 if (!$ignoredeadlines and !empty($this->submissionend) and $now > $this->submissionend ) { 1832 // if enabled, submitting is not allowed after the date/time defined in the mod_form unless late submission is allowed 1833 return false; 1834 } 1835 return true; 1836 } 1837 } 1838 1839 /** 1840 * Is the given user allowed to modify their existing submission? 1841 * 1842 * @param int $userid 1843 * @return bool 1844 */ 1845 public function modifying_submission_allowed($userid) { 1846 1847 $now = time(); 1848 $ignoredeadlines = has_capability('mod/workshop:ignoredeadlines', $this->context, $userid); 1849 1850 if ($this->phase != self::PHASE_SUBMISSION) { 1851 // submissions can be edited during the submission phase only 1852 return false; 1853 } 1854 if (!$ignoredeadlines and !empty($this->submissionstart) and $this->submissionstart > $now) { 1855 // if enabled, re-submitting is not allowed before the date/time defined in the mod_form 1856 return false; 1857 } 1858 if (!$ignoredeadlines and !empty($this->submissionend) and $now > $this->submissionend) { 1859 // if enabled, re-submitting is not allowed after the date/time defined in the mod_form even if late submission is allowed 1860 return false; 1861 } 1862 return true; 1863 } 1864 1865 /** 1866 * Is the given reviewer allowed to create/edit their assessments? 1867 * 1868 * @param int $userid 1869 * @return bool 1870 */ 1871 public function assessing_allowed($userid) { 1872 1873 if ($this->phase != self::PHASE_ASSESSMENT) { 1874 // assessing is allowed in the assessment phase only, unless the user is a teacher 1875 // providing additional assessment during the evaluation phase 1876 if ($this->phase != self::PHASE_EVALUATION or !has_capability('mod/workshop:overridegrades', $this->context, $userid)) { 1877 return false; 1878 } 1879 } 1880 1881 $now = time(); 1882 $ignoredeadlines = has_capability('mod/workshop:ignoredeadlines', $this->context, $userid); 1883 1884 if (!$ignoredeadlines and !empty($this->assessmentstart) and $this->assessmentstart > $now) { 1885 // if enabled, assessing is not allowed before the date/time defined in the mod_form 1886 return false; 1887 } 1888 if (!$ignoredeadlines and !empty($this->assessmentend) and $now > $this->assessmentend) { 1889 // if enabled, assessing is not allowed after the date/time defined in the mod_form 1890 return false; 1891 } 1892 // here we go, assessing is allowed 1893 return true; 1894 } 1895 1896 /** 1897 * Are reviewers allowed to create/edit their assessments of the example submissions? 1898 * 1899 * Returns null if example submissions are not enabled in this workshop. Otherwise returns 1900 * true or false. Note this does not check other conditions like the number of already 1901 * assessed examples, examples mode etc. 1902 * 1903 * @return null|bool 1904 */ 1905 public function assessing_examples_allowed() { 1906 if (empty($this->useexamples)) { 1907 return null; 1908 } 1909 if (self::EXAMPLES_VOLUNTARY == $this->examplesmode) { 1910 return true; 1911 } 1912 if (self::EXAMPLES_BEFORE_SUBMISSION == $this->examplesmode and self::PHASE_SUBMISSION == $this->phase) { 1913 return true; 1914 } 1915 if (self::EXAMPLES_BEFORE_ASSESSMENT == $this->examplesmode and self::PHASE_ASSESSMENT == $this->phase) { 1916 return true; 1917 } 1918 return false; 1919 } 1920 1921 /** 1922 * Are the peer-reviews available to the authors? 1923 * 1924 * @return bool 1925 */ 1926 public function assessments_available() { 1927 return $this->phase == self::PHASE_CLOSED; 1928 } 1929 1930 /** 1931 * Switch to a new workshop phase 1932 * 1933 * Modifies the underlying database record. You should terminate the script shortly after calling this. 1934 * 1935 * @param int $newphase new phase code 1936 * @return bool true if success, false otherwise 1937 */ 1938 public function switch_phase($newphase) { 1939 global $DB; 1940 1941 $known = $this->available_phases_list(); 1942 if (!isset($known[$newphase])) { 1943 return false; 1944 } 1945 1946 if (self::PHASE_CLOSED == $newphase) { 1947 // push the grades into the gradebook 1948 $workshop = new stdclass(); 1949 foreach ($this as $property => $value) { 1950 $workshop->{$property} = $value; 1951 } 1952 $workshop->course = $this->course->id; 1953 $workshop->cmidnumber = $this->cm->id; 1954 $workshop->modname = 'workshop'; 1955 workshop_update_grades($workshop); 1956 } 1957 1958 $DB->set_field('workshop', 'phase', $newphase, array('id' => $this->id)); 1959 $this->phase = $newphase; 1960 $eventdata = array( 1961 'objectid' => $this->id, 1962 'context' => $this->context, 1963 'other' => array( 1964 'workshopphase' => $this->phase 1965 ) 1966 ); 1967 $event = \mod_workshop\event\phase_switched::create($eventdata); 1968 $event->trigger(); 1969 return true; 1970 } 1971 1972 /** 1973 * Saves a raw grade for submission as calculated from the assessment form fields 1974 * 1975 * @param array $assessmentid assessment record id, must exists 1976 * @param mixed $grade raw percentual grade from 0.00000 to 100.00000 1977 * @return false|float the saved grade 1978 */ 1979 public function set_peer_grade($assessmentid, $grade) { 1980 global $DB; 1981 1982 if (is_null($grade)) { 1983 return false; 1984 } 1985 $data = new stdclass(); 1986 $data->id = $assessmentid; 1987 $data->grade = $grade; 1988 $data->timemodified = time(); 1989 $DB->update_record('workshop_assessments', $data); 1990 return $grade; 1991 } 1992 1993 /** 1994 * Prepares data object with all workshop grades to be rendered 1995 * 1996 * @param int $userid the user we are preparing the report for 1997 * @param int $groupid if non-zero, prepare the report for the given group only 1998 * @param int $page the current page (for the pagination) 1999 * @param int $perpage participants per page (for the pagination) 2000 * @param string $sortby lastname|firstname|submissiontitle|submissiongrade|gradinggrade 2001 * @param string $sorthow ASC|DESC 2002 * @return stdclass data for the renderer 2003 */ 2004 public function prepare_grading_report_data($userid, $groupid, $page, $perpage, $sortby, $sorthow) { 2005 global $DB; 2006 2007 $canviewall = has_capability('mod/workshop:viewallassessments', $this->context, $userid); 2008 $isparticipant = $this->is_participant($userid); 2009 2010 if (!$canviewall and !$isparticipant) { 2011 // who the hell is this? 2012 return array(); 2013 } 2014 2015 if (!in_array($sortby, array('lastname', 'firstname', 'submissiontitle', 'submissionmodified', 2016 'submissiongrade', 'gradinggrade'))) { 2017 $sortby = 'lastname'; 2018 } 2019 2020 if (!($sorthow === 'ASC' or $sorthow === 'DESC')) { 2021 $sorthow = 'ASC'; 2022 } 2023 2024 // get the list of user ids to be displayed 2025 if ($canviewall) { 2026 $participants = $this->get_participants(false, $groupid); 2027 } else { 2028 // this is an ordinary workshop participant (aka student) - display the report just for him/her 2029 $participants = array($userid => (object)array('id' => $userid)); 2030 } 2031 2032 // we will need to know the number of all records later for the pagination purposes 2033 $numofparticipants = count($participants); 2034 2035 if ($numofparticipants > 0) { 2036 // load all fields which can be used for sorting and paginate the records 2037 list($participantids, $params) = $DB->get_in_or_equal(array_keys($participants), SQL_PARAMS_NAMED); 2038 $params['workshopid1'] = $this->id; 2039 $params['workshopid2'] = $this->id; 2040 $sqlsort = array(); 2041 $sqlsortfields = array($sortby => $sorthow) + array('lastname' => 'ASC', 'firstname' => 'ASC', 'u.id' => 'ASC'); 2042 foreach ($sqlsortfields as $sqlsortfieldname => $sqlsortfieldhow) { 2043 $sqlsort[] = $sqlsortfieldname . ' ' . $sqlsortfieldhow; 2044 } 2045 $sqlsort = implode(',', $sqlsort); 2046 $userfieldsapi = \core_user\fields::for_userpic(); 2047 $picturefields = $userfieldsapi->get_sql('u', false, '', 'userid', false)->selects; 2048 $sql = "SELECT $picturefields, s.title AS submissiontitle, s.timemodified AS submissionmodified, 2049 s.grade AS submissiongrade, ag.gradinggrade 2050 FROM {user} u 2051 LEFT JOIN {workshop_submissions} s ON (s.authorid = u.id AND s.workshopid = :workshopid1 AND s.example = 0) 2052 LEFT JOIN {workshop_aggregations} ag ON (ag.userid = u.id AND ag.workshopid = :workshopid2) 2053 WHERE u.id $participantids 2054 ORDER BY $sqlsort"; 2055 $participants = $DB->get_records_sql($sql, $params, $page * $perpage, $perpage); 2056 } else { 2057 $participants = array(); 2058 } 2059 2060 // this will hold the information needed to display user names and pictures 2061 $userinfo = array(); 2062 2063 // get the user details for all participants to display 2064 $additionalnames = \core_user\fields::get_name_fields(); 2065 foreach ($participants as $participant) { 2066 if (!isset($userinfo[$participant->userid])) { 2067 $userinfo[$participant->userid] = new stdclass(); 2068 $userinfo[$participant->userid]->id = $participant->userid; 2069 $userinfo[$participant->userid]->picture = $participant->picture; 2070 $userinfo[$participant->userid]->imagealt = $participant->imagealt; 2071 $userinfo[$participant->userid]->email = $participant->email; 2072 foreach ($additionalnames as $addname) { 2073 $userinfo[$participant->userid]->$addname = $participant->$addname; 2074 } 2075 } 2076 } 2077 2078 // load the submissions details 2079 $submissions = $this->get_submissions(array_keys($participants)); 2080 2081 // get the user details for all moderators (teachers) that have overridden a submission grade 2082 foreach ($submissions as $submission) { 2083 if (!isset($userinfo[$submission->gradeoverby])) { 2084 $userinfo[$submission->gradeoverby] = new stdclass(); 2085 $userinfo[$submission->gradeoverby]->id = $submission->gradeoverby; 2086 $userinfo[$submission->gradeoverby]->picture = $submission->overpicture; 2087 $userinfo[$submission->gradeoverby]->imagealt = $submission->overimagealt; 2088 $userinfo[$submission->gradeoverby]->email = $submission->overemail; 2089 foreach ($additionalnames as $addname) { 2090 $temp = 'over' . $addname; 2091 $userinfo[$submission->gradeoverby]->$addname = $submission->$temp; 2092 } 2093 } 2094 } 2095 2096 // get the user details for all reviewers of the displayed participants 2097 $reviewers = array(); 2098 2099 if ($submissions) { 2100 list($submissionids, $params) = $DB->get_in_or_equal(array_keys($submissions), SQL_PARAMS_NAMED); 2101 list($sort, $sortparams) = users_order_by_sql('r'); 2102 $userfieldsapi = \core_user\fields::for_userpic(); 2103 $picturefields = $userfieldsapi->get_sql('r', false, '', 'reviewerid', false)->selects; 2104 $sql = "SELECT a.id AS assessmentid, a.submissionid, a.grade, a.gradinggrade, a.gradinggradeover, a.weight, 2105 $picturefields, s.id AS submissionid, s.authorid 2106 FROM {workshop_assessments} a 2107 JOIN {user} r ON (a.reviewerid = r.id) 2108 JOIN {workshop_submissions} s ON (a.submissionid = s.id AND s.example = 0) 2109 WHERE a.submissionid $submissionids 2110 ORDER BY a.weight DESC, $sort"; 2111 $reviewers = $DB->get_records_sql($sql, array_merge($params, $sortparams)); 2112 foreach ($reviewers as $reviewer) { 2113 if (!isset($userinfo[$reviewer->reviewerid])) { 2114 $userinfo[$reviewer->reviewerid] = new stdclass(); 2115 $userinfo[$reviewer->reviewerid]->id = $reviewer->reviewerid; 2116 $userinfo[$reviewer->reviewerid]->picture = $reviewer->picture; 2117 $userinfo[$reviewer->reviewerid]->imagealt = $reviewer->imagealt; 2118 $userinfo[$reviewer->reviewerid]->email = $reviewer->email; 2119 foreach ($additionalnames as $addname) { 2120 $userinfo[$reviewer->reviewerid]->$addname = $reviewer->$addname; 2121 } 2122 } 2123 } 2124 } 2125 2126 // get the user details for all reviewees of the displayed participants 2127 $reviewees = array(); 2128 if ($participants) { 2129 list($participantids, $params) = $DB->get_in_or_equal(array_keys($participants), SQL_PARAMS_NAMED); 2130 list($sort, $sortparams) = users_order_by_sql('e'); 2131 $params['workshopid'] = $this->id; 2132 $userfieldsapi = \core_user\fields::for_userpic(); 2133 $picturefields = $userfieldsapi->get_sql('e', false, '', 'authorid', false)->selects; 2134 $sql = "SELECT a.id AS assessmentid, a.submissionid, a.grade, a.gradinggrade, a.gradinggradeover, a.reviewerid, a.weight, 2135 s.id AS submissionid, $picturefields 2136 FROM {user} u 2137 JOIN {workshop_assessments} a ON (a.reviewerid = u.id) 2138 JOIN {workshop_submissions} s ON (a.submissionid = s.id AND s.example = 0) 2139 JOIN {user} e ON (s.authorid = e.id) 2140 WHERE u.id $participantids AND s.workshopid = :workshopid 2141 ORDER BY a.weight DESC, $sort"; 2142 $reviewees = $DB->get_records_sql($sql, array_merge($params, $sortparams)); 2143 foreach ($reviewees as $reviewee) { 2144 if (!isset($userinfo[$reviewee->authorid])) { 2145 $userinfo[$reviewee->authorid] = new stdclass(); 2146 $userinfo[$reviewee->authorid]->id = $reviewee->authorid; 2147 $userinfo[$reviewee->authorid]->picture = $reviewee->picture; 2148 $userinfo[$reviewee->authorid]->imagealt = $reviewee->imagealt; 2149 $userinfo[$reviewee->authorid]->email = $reviewee->email; 2150 foreach ($additionalnames as $addname) { 2151 $userinfo[$reviewee->authorid]->$addname = $reviewee->$addname; 2152 } 2153 } 2154 } 2155 } 2156 2157 // finally populate the object to be rendered 2158 $grades = $participants; 2159 2160 foreach ($participants as $participant) { 2161 // set up default (null) values 2162 $grades[$participant->userid]->submissionid = null; 2163 $grades[$participant->userid]->submissiontitle = null; 2164 $grades[$participant->userid]->submissiongrade = null; 2165 $grades[$participant->userid]->submissiongradeover = null; 2166 $grades[$participant->userid]->submissiongradeoverby = null; 2167 $grades[$participant->userid]->submissionpublished = null; 2168 $grades[$participant->userid]->reviewedby = array(); 2169 $grades[$participant->userid]->reviewerof = array(); 2170 } 2171 unset($participants); 2172 unset($participant); 2173 2174 foreach ($submissions as $submission) { 2175 $grades[$submission->authorid]->submissionid = $submission->id; 2176 $grades[$submission->authorid]->submissiontitle = $submission->title; 2177 $grades[$submission->authorid]->submissiongrade = $this->real_grade($submission->grade); 2178 $grades[$submission->authorid]->submissiongradeover = $this->real_grade($submission->gradeover); 2179 $grades[$submission->authorid]->submissiongradeoverby = $submission->gradeoverby; 2180 $grades[$submission->authorid]->submissionpublished = $submission->published; 2181 } 2182 unset($submissions); 2183 unset($submission); 2184 2185 foreach($reviewers as $reviewer) { 2186 $info = new stdclass(); 2187 $info->userid = $reviewer->reviewerid; 2188 $info->assessmentid = $reviewer->assessmentid; 2189 $info->submissionid = $reviewer->submissionid; 2190 $info->grade = $this->real_grade($reviewer->grade); 2191 $info->gradinggrade = $this->real_grading_grade($reviewer->gradinggrade); 2192 $info->gradinggradeover = $this->real_grading_grade($reviewer->gradinggradeover); 2193 $info->weight = $reviewer->weight; 2194 $grades[$reviewer->authorid]->reviewedby[$reviewer->reviewerid] = $info; 2195 } 2196 unset($reviewers); 2197 unset($reviewer); 2198 2199 foreach($reviewees as $reviewee) { 2200 $info = new stdclass(); 2201 $info->userid = $reviewee->authorid; 2202 $info->assessmentid = $reviewee->assessmentid; 2203 $info->submissionid = $reviewee->submissionid; 2204 $info->grade = $this->real_grade($reviewee->grade); 2205 $info->gradinggrade = $this->real_grading_grade($reviewee->gradinggrade); 2206 $info->gradinggradeover = $this->real_grading_grade($reviewee->gradinggradeover); 2207 $info->weight = $reviewee->weight; 2208 $grades[$reviewee->reviewerid]->reviewerof[$reviewee->authorid] = $info; 2209 } 2210 unset($reviewees); 2211 unset($reviewee); 2212 2213 foreach ($grades as $grade) { 2214 $grade->gradinggrade = $this->real_grading_grade($grade->gradinggrade); 2215 } 2216 2217 $data = new stdclass(); 2218 $data->grades = $grades; 2219 $data->userinfo = $userinfo; 2220 $data->totalcount = $numofparticipants; 2221 $data->maxgrade = $this->real_grade(100); 2222 $data->maxgradinggrade = $this->real_grading_grade(100); 2223 return $data; 2224 } 2225 2226 /** 2227 * Calculates the real value of a grade 2228 * 2229 * @param float $value percentual value from 0 to 100 2230 * @param float $max the maximal grade 2231 * @return string 2232 */ 2233 public function real_grade_value($value, $max) { 2234 $localized = true; 2235 if (is_null($value) or $value === '') { 2236 return null; 2237 } elseif ($max == 0) { 2238 return 0; 2239 } else { 2240 return format_float($max * $value / 100, $this->gradedecimals, $localized); 2241 } 2242 } 2243 2244 /** 2245 * Calculates the raw (percentual) value from a real grade 2246 * 2247 * This is used in cases when a user wants to give a grade such as 12 of 20 and we need to save 2248 * this value in a raw percentual form into DB 2249 * @param float $value given grade 2250 * @param float $max the maximal grade 2251 * @return float suitable to be stored as numeric(10,5) 2252 */ 2253 public function raw_grade_value($value, $max) { 2254 if (is_null($value) or $value === '') { 2255 return null; 2256 } 2257 if ($max == 0 or $value < 0) { 2258 return 0; 2259 } 2260 $p = $value / $max * 100; 2261 if ($p > 100) { 2262 return $max; 2263 } 2264 return grade_floatval($p); 2265 } 2266 2267 /** 2268 * Calculates the real value of grade for submission 2269 * 2270 * @param float $value percentual value from 0 to 100 2271 * @return string 2272 */ 2273 public function real_grade($value) { 2274 return $this->real_grade_value($value, $this->grade); 2275 } 2276 2277 /** 2278 * Calculates the real value of grade for assessment 2279 * 2280 * @param float $value percentual value from 0 to 100 2281 * @return string 2282 */ 2283 public function real_grading_grade($value) { 2284 return $this->real_grade_value($value, $this->gradinggrade); 2285 } 2286 2287 /** 2288 * Sets the given grades and received grading grades to null 2289 * 2290 * This does not clear the information about how the peers filled the assessment forms, but 2291 * clears the calculated grades in workshop_assessments. Therefore reviewers have to re-assess 2292 * the allocated submissions. 2293 * 2294 * @return void 2295 */ 2296 public function clear_assessments() { 2297 global $DB; 2298 2299 $submissions = $this->get_submissions(); 2300 if (empty($submissions)) { 2301 // no money, no love 2302 return; 2303 } 2304 $submissions = array_keys($submissions); 2305 list($sql, $params) = $DB->get_in_or_equal($submissions, SQL_PARAMS_NAMED); 2306 $sql = "submissionid $sql"; 2307 $DB->set_field_select('workshop_assessments', 'grade', null, $sql, $params); 2308 $DB->set_field_select('workshop_assessments', 'gradinggrade', null, $sql, $params); 2309 } 2310 2311 /** 2312 * Sets the grades for submission to null 2313 * 2314 * @param null|int|array $restrict If null, update all authors, otherwise update just grades for the given author(s) 2315 * @return void 2316 */ 2317 public function clear_submission_grades($restrict=null) { 2318 global $DB; 2319 2320 $sql = "workshopid = :workshopid AND example = 0"; 2321 $params = array('workshopid' => $this->id); 2322 2323 if (is_null($restrict)) { 2324 // update all users - no more conditions 2325 } elseif (!empty($restrict)) { 2326 list($usql, $uparams) = $DB->get_in_or_equal($restrict, SQL_PARAMS_NAMED); 2327 $sql .= " AND authorid $usql"; 2328 $params = array_merge($params, $uparams); 2329 } else { 2330 throw new coding_exception('Empty value is not a valid parameter here'); 2331 } 2332 2333 $DB->set_field_select('workshop_submissions', 'grade', null, $sql, $params); 2334 } 2335 2336 /** 2337 * Calculates grades for submission for the given participant(s) and updates it in the database 2338 * 2339 * @param null|int|array $restrict If null, update all authors, otherwise update just grades for the given author(s) 2340 * @return void 2341 */ 2342 public function aggregate_submission_grades($restrict=null) { 2343 global $DB; 2344 2345 // fetch a recordset with all assessments to process 2346 $sql = 'SELECT s.id AS submissionid, s.grade AS submissiongrade, 2347 a.weight, a.grade 2348 FROM {workshop_submissions} s 2349 LEFT JOIN {workshop_assessments} a ON (a.submissionid = s.id) 2350 WHERE s.example=0 AND s.workshopid=:workshopid'; // to be cont. 2351 $params = array('workshopid' => $this->id); 2352 2353 if (is_null($restrict)) { 2354 // update all users - no more conditions 2355 } elseif (!empty($restrict)) { 2356 list($usql, $uparams) = $DB->get_in_or_equal($restrict, SQL_PARAMS_NAMED); 2357 $sql .= " AND s.authorid $usql"; 2358 $params = array_merge($params, $uparams); 2359 } else { 2360 throw new coding_exception('Empty value is not a valid parameter here'); 2361 } 2362 2363 $sql .= ' ORDER BY s.id'; // this is important for bulk processing 2364 2365 $rs = $DB->get_recordset_sql($sql, $params); 2366 $batch = array(); // will contain a set of all assessments of a single submission 2367 $previous = null; // a previous record in the recordset 2368 2369 foreach ($rs as $current) { 2370 if (is_null($previous)) { 2371 // we are processing the very first record in the recordset 2372 $previous = $current; 2373 } 2374 if ($current->submissionid == $previous->submissionid) { 2375 // we are still processing the current submission 2376 $batch[] = $current; 2377 } else { 2378 // process all the assessments of a sigle submission 2379 $this->aggregate_submission_grades_process($batch); 2380 // and then start to process another submission 2381 $batch = array($current); 2382 $previous = $current; 2383 } 2384 } 2385 // do not forget to process the last batch! 2386 $this->aggregate_submission_grades_process($batch); 2387 $rs->close(); 2388 } 2389 2390 /** 2391 * Sets the aggregated grades for assessment to null 2392 * 2393 * @param null|int|array $restrict If null, update all reviewers, otherwise update just grades for the given reviewer(s) 2394 * @return void 2395 */ 2396 public function clear_grading_grades($restrict=null) { 2397 global $DB; 2398 2399 $sql = "workshopid = :workshopid"; 2400 $params = array('workshopid' => $this->id); 2401 2402 if (is_null($restrict)) { 2403 // update all users - no more conditions 2404 } elseif (!empty($restrict)) { 2405 list($usql, $uparams) = $DB->get_in_or_equal($restrict, SQL_PARAMS_NAMED); 2406 $sql .= " AND userid $usql"; 2407 $params = array_merge($params, $uparams); 2408 } else { 2409 throw new coding_exception('Empty value is not a valid parameter here'); 2410 } 2411 2412 $DB->set_field_select('workshop_aggregations', 'gradinggrade', null, $sql, $params); 2413 } 2414 2415 /** 2416 * Calculates grades for assessment for the given participant(s) 2417 * 2418 * Grade for assessment is calculated as a simple mean of all grading grades calculated by the grading evaluator. 2419 * The assessment weight is not taken into account here. 2420 * 2421 * @param null|int|array $restrict If null, update all reviewers, otherwise update just grades for the given reviewer(s) 2422 * @return void 2423 */ 2424 public function aggregate_grading_grades($restrict=null) { 2425 global $DB; 2426 2427 // fetch a recordset with all assessments to process 2428 $sql = 'SELECT a.reviewerid, a.gradinggrade, a.gradinggradeover, 2429 ag.id AS aggregationid, ag.gradinggrade AS aggregatedgrade 2430 FROM {workshop_assessments} a 2431 INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id) 2432 LEFT JOIN {workshop_aggregations} ag ON (ag.userid = a.reviewerid AND ag.workshopid = s.workshopid) 2433 WHERE s.example=0 AND s.workshopid=:workshopid'; // to be cont. 2434 $params = array('workshopid' => $this->id); 2435 2436 if (is_null($restrict)) { 2437 // update all users - no more conditions 2438 } elseif (!empty($restrict)) { 2439 list($usql, $uparams) = $DB->get_in_or_equal($restrict, SQL_PARAMS_NAMED); 2440 $sql .= " AND a.reviewerid $usql"; 2441 $params = array_merge($params, $uparams); 2442 } else { 2443 throw new coding_exception('Empty value is not a valid parameter here'); 2444 } 2445 2446 $sql .= ' ORDER BY a.reviewerid'; // this is important for bulk processing 2447 2448 $rs = $DB->get_recordset_sql($sql, $params); 2449 $batch = array(); // will contain a set of all assessments of a single submission 2450 $previous = null; // a previous record in the recordset 2451 2452 foreach ($rs as $current) { 2453 if (is_null($previous)) { 2454 // we are processing the very first record in the recordset 2455 $previous = $current; 2456 } 2457 if ($current->reviewerid == $previous->reviewerid) { 2458 // we are still processing the current reviewer 2459 $batch[] = $current; 2460 } else { 2461 // process all the assessments of a sigle submission 2462 $this->aggregate_grading_grades_process($batch); 2463 // and then start to process another reviewer 2464 $batch = array($current); 2465 $previous = $current; 2466 } 2467 } 2468 // do not forget to process the last batch! 2469 $this->aggregate_grading_grades_process($batch); 2470 $rs->close(); 2471 } 2472 2473 /** 2474 * Returns the mform the teachers use to put a feedback for the reviewer 2475 * 2476 * @param mixed moodle_url|null $actionurl 2477 * @param stdClass $assessment 2478 * @param array $options editable, editableweight, overridablegradinggrade 2479 * @return workshop_feedbackreviewer_form 2480 */ 2481 public function get_feedbackreviewer_form($actionurl, stdclass $assessment, $options=array()) { 2482 global $CFG; 2483 require_once (__DIR__ . '/feedbackreviewer_form.php'); 2484 2485 $current = new stdclass(); 2486 $current->asid = $assessment->id; 2487 $current->weight = $assessment->weight; 2488 $current->gradinggrade = $this->real_grading_grade($assessment->gradinggrade); 2489 $current->gradinggradeover = $this->real_grading_grade($assessment->gradinggradeover); 2490 $current->feedbackreviewer = $assessment->feedbackreviewer; 2491 $current->feedbackreviewerformat = $assessment->feedbackreviewerformat; 2492 if (is_null($current->gradinggrade)) { 2493 $current->gradinggrade = get_string('nullgrade', 'workshop'); 2494 } 2495 if (!isset($options['editable'])) { 2496 $editable = true; // by default 2497 } else { 2498 $editable = (bool)$options['editable']; 2499 } 2500 2501 // prepare wysiwyg editor 2502 $current = file_prepare_standard_editor($current, 'feedbackreviewer', array()); 2503 2504 return new workshop_feedbackreviewer_form($actionurl, 2505 array('workshop' => $this, 'current' => $current, 'editoropts' => array(), 'options' => $options), 2506 'post', '', null, $editable); 2507 } 2508 2509 /** 2510 * Returns the mform the teachers use to put a feedback for the author on their submission 2511 * 2512 * @mixed moodle_url|null $actionurl 2513 * @param stdClass $submission 2514 * @param array $options editable 2515 * @return workshop_feedbackauthor_form 2516 */ 2517 public function get_feedbackauthor_form($actionurl, stdclass $submission, $options=array()) { 2518 global $CFG; 2519 require_once (__DIR__ . '/feedbackauthor_form.php'); 2520 2521 $current = new stdclass(); 2522 $current->submissionid = $submission->id; 2523 $current->published = $submission->published; 2524 $current->grade = $this->real_grade($submission->grade); 2525 $current->gradeover = $this->real_grade($submission->gradeover); 2526 $current->feedbackauthor = $submission->feedbackauthor; 2527 $current->feedbackauthorformat = $submission->feedbackauthorformat; 2528 if (is_null($current->grade)) { 2529 $current->grade = get_string('nullgrade', 'workshop'); 2530 } 2531 if (!isset($options['editable'])) { 2532 $editable = true; // by default 2533 } else { 2534 $editable = (bool)$options['editable']; 2535 } 2536 2537 // prepare wysiwyg editor 2538 $current = file_prepare_standard_editor($current, 'feedbackauthor', array()); 2539 2540 return new workshop_feedbackauthor_form($actionurl, 2541 array('workshop' => $this, 'current' => $current, 'editoropts' => array(), 'options' => $options), 2542 'post', '', null, $editable); 2543 } 2544 2545 /** 2546 * Returns the information about the user's grades as they are stored in the gradebook 2547 * 2548 * The submission grade is returned for users with the capability mod/workshop:submit and the 2549 * assessment grade is returned for users with the capability mod/workshop:peerassess. Unless the 2550 * user has the capability to view hidden grades, grades must be visible to be returned. Null 2551 * grades are not returned. If none grade is to be returned, this method returns false. 2552 * 2553 * @param int $userid the user's id 2554 * @return workshop_final_grades|false 2555 */ 2556 public function get_gradebook_grades($userid) { 2557 global $CFG; 2558 require_once($CFG->libdir.'/gradelib.php'); 2559 2560 if (empty($userid)) { 2561 throw new coding_exception('User id expected, empty value given.'); 2562 } 2563 2564 // Read data via the Gradebook API 2565 $gradebook = grade_get_grades($this->course->id, 'mod', 'workshop', $this->id, $userid); 2566 2567 $grades = new workshop_final_grades(); 2568 2569 if (has_capability('mod/workshop:submit', $this->context, $userid)) { 2570 if (!empty($gradebook->items[0]->grades)) { 2571 $submissiongrade = reset($gradebook->items[0]->grades); 2572 if (!is_null($submissiongrade->grade)) { 2573 if (!$submissiongrade->hidden or has_capability('moodle/grade:viewhidden', $this->context, $userid)) { 2574 $grades->submissiongrade = $submissiongrade; 2575 } 2576 } 2577 } 2578 } 2579 2580 if (has_capability('mod/workshop:peerassess', $this->context, $userid)) { 2581 if (!empty($gradebook->items[1]->grades)) { 2582 $assessmentgrade = reset($gradebook->items[1]->grades); 2583 if (!is_null($assessmentgrade->grade)) { 2584 if (!$assessmentgrade->hidden or has_capability('moodle/grade:viewhidden', $this->context, $userid)) { 2585 $grades->assessmentgrade = $assessmentgrade; 2586 } 2587 } 2588 } 2589 } 2590 2591 if (!is_null($grades->submissiongrade) or !is_null($grades->assessmentgrade)) { 2592 return $grades; 2593 } 2594 2595 return false; 2596 } 2597 2598 /** 2599 * Return the editor options for the submission content field. 2600 * 2601 * @return array 2602 */ 2603 public function submission_content_options() { 2604 global $CFG; 2605 require_once($CFG->dirroot.'/repository/lib.php'); 2606 2607 return array( 2608 'trusttext' => true, 2609 'subdirs' => false, 2610 'maxfiles' => $this->nattachments, 2611 'maxbytes' => $this->maxbytes, 2612 'context' => $this->context, 2613 'return_types' => FILE_INTERNAL | FILE_EXTERNAL, 2614 ); 2615 } 2616 2617 /** 2618 * Return the filemanager options for the submission attachments field. 2619 * 2620 * @return array 2621 */ 2622 public function submission_attachment_options() { 2623 global $CFG; 2624 require_once($CFG->dirroot.'/repository/lib.php'); 2625 2626 $options = array( 2627 'subdirs' => true, 2628 'maxfiles' => $this->nattachments, 2629 'maxbytes' => $this->maxbytes, 2630 'return_types' => FILE_INTERNAL | FILE_CONTROLLED_LINK, 2631 ); 2632 2633 $filetypesutil = new \core_form\filetypes_util(); 2634 $options['accepted_types'] = $filetypesutil->normalize_file_types($this->submissionfiletypes); 2635 2636 return $options; 2637 } 2638 2639 /** 2640 * Return the editor options for the overall feedback for the author. 2641 * 2642 * @return array 2643 */ 2644 public function overall_feedback_content_options() { 2645 global $CFG; 2646 require_once($CFG->dirroot.'/repository/lib.php'); 2647 2648 return array( 2649 'subdirs' => 0, 2650 'maxbytes' => $this->overallfeedbackmaxbytes, 2651 'maxfiles' => $this->overallfeedbackfiles, 2652 'changeformat' => 1, 2653 'context' => $this->context, 2654 'return_types' => FILE_INTERNAL, 2655 ); 2656 } 2657 2658 /** 2659 * Return the filemanager options for the overall feedback for the author. 2660 * 2661 * @return array 2662 */ 2663 public function overall_feedback_attachment_options() { 2664 global $CFG; 2665 require_once($CFG->dirroot.'/repository/lib.php'); 2666 2667 $options = array( 2668 'subdirs' => 1, 2669 'maxbytes' => $this->overallfeedbackmaxbytes, 2670 'maxfiles' => $this->overallfeedbackfiles, 2671 'return_types' => FILE_INTERNAL | FILE_CONTROLLED_LINK, 2672 ); 2673 2674 $filetypesutil = new \core_form\filetypes_util(); 2675 $options['accepted_types'] = $filetypesutil->normalize_file_types($this->overallfeedbackfiletypes); 2676 2677 return $options; 2678 } 2679 2680 /** 2681 * Performs the reset of this workshop instance. 2682 * 2683 * @param stdClass $data The actual course reset settings. 2684 * @return array List of results, each being array[(string)component, (string)item, (string)error] 2685 */ 2686 public function reset_userdata(stdClass $data) { 2687 2688 $componentstr = get_string('pluginname', 'workshop').': '.format_string($this->name); 2689 $status = array(); 2690 2691 if (!empty($data->reset_workshop_assessments) or !empty($data->reset_workshop_submissions)) { 2692 // Reset all data related to assessments, including assessments of 2693 // example submissions. 2694 $result = $this->reset_userdata_assessments($data); 2695 if ($result === true) { 2696 $status[] = array( 2697 'component' => $componentstr, 2698 'item' => get_string('resetassessments', 'mod_workshop'), 2699 'error' => false, 2700 ); 2701 } else { 2702 $status[] = array( 2703 'component' => $componentstr, 2704 'item' => get_string('resetassessments', 'mod_workshop'), 2705 'error' => $result, 2706 ); 2707 } 2708 } 2709 2710 if (!empty($data->reset_workshop_submissions)) { 2711 // Reset all remaining data related to submissions. 2712 $result = $this->reset_userdata_submissions($data); 2713 if ($result === true) { 2714 $status[] = array( 2715 'component' => $componentstr, 2716 'item' => get_string('resetsubmissions', 'mod_workshop'), 2717 'error' => false, 2718 ); 2719 } else { 2720 $status[] = array( 2721 'component' => $componentstr, 2722 'item' => get_string('resetsubmissions', 'mod_workshop'), 2723 'error' => $result, 2724 ); 2725 } 2726 } 2727 2728 if (!empty($data->reset_workshop_phase)) { 2729 // Do not use the {@link workshop::switch_phase()} here, we do not 2730 // want to trigger events. 2731 $this->reset_phase(); 2732 $status[] = array( 2733 'component' => $componentstr, 2734 'item' => get_string('resetsubmissions', 'mod_workshop'), 2735 'error' => false, 2736 ); 2737 } 2738 2739 return $status; 2740 } 2741 2742 /** 2743 * Check if the current user can access the other user's group. 2744 * 2745 * This is typically used for teacher roles that have permissions like 2746 * 'view all submissions'. Even with such a permission granted, we have to 2747 * check the workshop activity group mode. 2748 * 2749 * If the workshop is not in a group mode, or if it is in the visible group 2750 * mode, this method returns true. This is consistent with how the 2751 * {@link groups_get_activity_allowed_groups()} behaves. 2752 * 2753 * If the workshop is in a separate group mode, the current user has to 2754 * have the 'access all groups' permission, or share at least one 2755 * accessible group with the other user. 2756 * 2757 * @param int $otheruserid The ID of the other user, e.g. the author of a submission. 2758 * @return bool False if the current user cannot access the other user's group. 2759 */ 2760 public function check_group_membership($otheruserid) { 2761 global $USER; 2762 2763 if (groups_get_activity_groupmode($this->cm) != SEPARATEGROUPS) { 2764 // The workshop is not in a group mode, or it is in a visible group mode. 2765 return true; 2766 2767 } else if (has_capability('moodle/site:accessallgroups', $this->context)) { 2768 // The current user can access all groups. 2769 return true; 2770 2771 } else { 2772 $thisusersgroups = groups_get_all_groups($this->course->id, $USER->id, $this->cm->groupingid, 'g.id'); 2773 $otherusersgroups = groups_get_all_groups($this->course->id, $otheruserid, $this->cm->groupingid, 'g.id'); 2774 $commongroups = array_intersect_key($thisusersgroups, $otherusersgroups); 2775 2776 if (empty($commongroups)) { 2777 // The current user has no group common with the other user. 2778 return false; 2779 2780 } else { 2781 // The current user has a group common with the other user. 2782 return true; 2783 } 2784 } 2785 } 2786 2787 /** 2788 * Check whether the given user has assessed all his required examples before submission. 2789 * 2790 * @param int $userid the user to check 2791 * @return bool false if there are examples missing assessment, true otherwise. 2792 * @since Moodle 3.4 2793 */ 2794 public function check_examples_assessed_before_submission($userid) { 2795 2796 if ($this->useexamples and $this->examplesmode == self::EXAMPLES_BEFORE_SUBMISSION 2797 and !has_capability('mod/workshop:manageexamples', $this->context)) { 2798 2799 // Check that all required examples have been assessed by the user. 2800 $examples = $this->get_examples_for_reviewer($userid); 2801 foreach ($examples as $exampleid => $example) { 2802 if (is_null($example->assessmentid)) { 2803 $examples[$exampleid]->assessmentid = $this->add_allocation($example, $userid, 0); 2804 } 2805 if (is_null($example->grade)) { 2806 return false; 2807 } 2808 } 2809 } 2810 return true; 2811 } 2812 2813 /** 2814 * Check that all required examples have been assessed by the given user. 2815 * 2816 * @param stdClass $userid the user (reviewer) to check 2817 * @return mixed bool|state false and notice code if there are examples missing assessment, true otherwise. 2818 * @since Moodle 3.4 2819 */ 2820 public function check_examples_assessed_before_assessment($userid) { 2821 2822 if ($this->useexamples and $this->examplesmode == self::EXAMPLES_BEFORE_ASSESSMENT 2823 and !has_capability('mod/workshop:manageexamples', $this->context)) { 2824 2825 // The reviewer must have submitted their own submission. 2826 $reviewersubmission = $this->get_submission_by_author($userid); 2827 if (!$reviewersubmission) { 2828 // No money, no love. 2829 return array(false, 'exampleneedsubmission'); 2830 } else { 2831 $examples = $this->get_examples_for_reviewer($userid); 2832 foreach ($examples as $exampleid => $example) { 2833 if (is_null($example->grade)) { 2834 return array(false, 'exampleneedassessed'); 2835 } 2836 } 2837 } 2838 } 2839 return array(true, null); 2840 } 2841 2842 /** 2843 * Trigger module viewed event and set the module viewed for completion. 2844 * 2845 * @since Moodle 3.4 2846 */ 2847 public function set_module_viewed() { 2848 global $CFG; 2849 require_once($CFG->libdir . '/completionlib.php'); 2850 2851 // Mark viewed. 2852 $completion = new completion_info($this->course); 2853 $completion->set_module_viewed($this->cm); 2854 2855 $eventdata = array(); 2856 $eventdata['objectid'] = $this->id; 2857 $eventdata['context'] = $this->context; 2858 2859 // Trigger module viewed event. 2860 $event = \mod_workshop\event\course_module_viewed::create($eventdata); 2861 $event->add_record_snapshot('course', $this->course); 2862 $event->add_record_snapshot('workshop', $this->dbrecord); 2863 $event->add_record_snapshot('course_modules', $this->cm); 2864 $event->trigger(); 2865 } 2866 2867 /** 2868 * Validates the submission form or WS data. 2869 * 2870 * @param array $data the data to be validated 2871 * @return array the validation errors (if any) 2872 * @since Moodle 3.4 2873 */ 2874 public function validate_submission_data($data) { 2875 global $DB, $USER; 2876 2877 $errors = array(); 2878 if (empty($data['id']) and empty($data['example'])) { 2879 // Make sure there is no submission saved meanwhile from another browser window. 2880 $sql = "SELECT COUNT(s.id) 2881 FROM {workshop_submissions} s 2882 JOIN {workshop} w ON (s.workshopid = w.id) 2883 JOIN {course_modules} cm ON (w.id = cm.instance) 2884 JOIN {modules} m ON (m.name = 'workshop' AND m.id = cm.module) 2885 WHERE cm.id = ? AND s.authorid = ? AND s.example = 0"; 2886 2887 if ($DB->count_records_sql($sql, array($data['cmid'], $USER->id))) { 2888 $errors['title'] = get_string('err_multiplesubmissions', 'mod_workshop'); 2889 } 2890 } 2891 // Get the workshop record by id or cmid, depending on whether we're creating or editing a submission. 2892 if (empty($data['workshopid'])) { 2893 $workshop = $DB->get_record_select('workshop', 'id = (SELECT instance FROM {course_modules} WHERE id = ?)', 2894 [$data['cmid']]); 2895 } else { 2896 $workshop = $DB->get_record('workshop', ['id' => $data['workshopid']]); 2897 } 2898 2899 if (isset($data['attachment_filemanager'])) { 2900 $getfiles = file_get_drafarea_files($data['attachment_filemanager']); 2901 $attachments = $getfiles->list; 2902 } else { 2903 $attachments = array(); 2904 } 2905 2906 if ($workshop->submissiontypefile == WORKSHOP_SUBMISSION_TYPE_REQUIRED) { 2907 if (empty($attachments)) { 2908 $errors['attachment_filemanager'] = get_string('err_required', 'form'); 2909 } 2910 } else if ($workshop->submissiontypefile == WORKSHOP_SUBMISSION_TYPE_DISABLED && !empty($data['attachment_filemanager'])) { 2911 $errors['attachment_filemanager'] = get_string('submissiontypedisabled', 'mod_workshop'); 2912 } 2913 2914 if ($workshop->submissiontypetext == WORKSHOP_SUBMISSION_TYPE_REQUIRED && html_is_blank($data['content_editor']['text'])) { 2915 $errors['content_editor'] = get_string('err_required', 'form'); 2916 } else if ($workshop->submissiontypetext == WORKSHOP_SUBMISSION_TYPE_DISABLED && !empty($data['content_editor']['text'])) { 2917 $errors['content_editor'] = get_string('submissiontypedisabled', 'mod_workshop'); 2918 } 2919 2920 // If neither type is explicitly required, one or the other must be submitted. 2921 if ($workshop->submissiontypetext != WORKSHOP_SUBMISSION_TYPE_REQUIRED 2922 && $workshop->submissiontypefile != WORKSHOP_SUBMISSION_TYPE_REQUIRED 2923 && empty($attachments) && html_is_blank($data['content_editor']['text'])) { 2924 $errors['content_editor'] = get_string('submissionrequiredcontent', 'mod_workshop'); 2925 $errors['attachment_filemanager'] = get_string('submissionrequiredfile', 'mod_workshop'); 2926 } 2927 2928 return $errors; 2929 } 2930 2931 /** 2932 * Adds or updates a submission. 2933 * 2934 * @param stdClass $submission The submissin data (via form or via WS). 2935 * @return the new or updated submission id. 2936 * @since Moodle 3.4 2937 */ 2938 public function edit_submission($submission) { 2939 global $USER, $DB; 2940 2941 if ($submission->example == 0) { 2942 // This was used just for validation, it must be set to zero when dealing with normal submissions. 2943 unset($submission->example); 2944 } else { 2945 throw new coding_exception('Invalid submission form data value: example'); 2946 } 2947 $timenow = time(); 2948 if (is_null($submission->id)) { 2949 $submission->workshopid = $this->id; 2950 $submission->example = 0; 2951 $submission->authorid = $USER->id; 2952 $submission->timecreated = $timenow; 2953 $submission->feedbackauthorformat = editors_get_preferred_format(); 2954 } 2955 $submission->timemodified = $timenow; 2956 $submission->title = trim($submission->title); 2957 $submission->content = ''; // Updated later. 2958 $submission->contentformat = FORMAT_HTML; // Updated later. 2959 $submission->contenttrust = 0; // Updated later. 2960 $submission->late = 0x0; // Bit mask. 2961 if (!empty($this->submissionend) and ($this->submissionend < time())) { 2962 $submission->late = $submission->late | 0x1; 2963 } 2964 if ($this->phase == self::PHASE_ASSESSMENT) { 2965 $submission->late = $submission->late | 0x2; 2966 } 2967 2968 // Event information. 2969 $params = array( 2970 'context' => $this->context, 2971 'courseid' => $this->course->id, 2972 'other' => array( 2973 'submissiontitle' => $submission->title 2974 ) 2975 ); 2976 $logdata = null; 2977 if (is_null($submission->id)) { 2978 $submission->id = $DB->insert_record('workshop_submissions', $submission); 2979 $params['objectid'] = $submission->id; 2980 $event = \mod_workshop\event\submission_created::create($params); 2981 $event->trigger(); 2982 } else { 2983 if (empty($submission->id) or empty($submission->id) or ($submission->id != $submission->id)) { 2984 throw new moodle_exception('err_submissionid', 'workshop'); 2985 } 2986 } 2987 $params['objectid'] = $submission->id; 2988 2989 // Save and relink embedded images and save attachments. 2990 if ($this->submissiontypetext != WORKSHOP_SUBMISSION_TYPE_DISABLED) { 2991 $submission = file_postupdate_standard_editor($submission, 'content', $this->submission_content_options(), 2992 $this->context, 'mod_workshop', 'submission_content', $submission->id); 2993 } 2994 2995 $submission = file_postupdate_standard_filemanager($submission, 'attachment', $this->submission_attachment_options(), 2996 $this->context, 'mod_workshop', 'submission_attachment', $submission->id); 2997 2998 if (empty($submission->attachment)) { 2999 // Explicit cast to zero integer. 3000 $submission->attachment = 0; 3001 } 3002 // Store the updated values or re-save the new submission (re-saving needed because URLs are now rewritten). 3003 $DB->update_record('workshop_submissions', $submission); 3004 $event = \mod_workshop\event\submission_updated::create($params); 3005 $event->add_record_snapshot('workshop', $this->dbrecord); 3006 $event->trigger(); 3007 3008 // Send submitted content for plagiarism detection. 3009 $fs = get_file_storage(); 3010 $files = $fs->get_area_files($this->context->id, 'mod_workshop', 'submission_attachment', $submission->id); 3011 3012 $params['other']['content'] = $submission->content; 3013 $params['other']['pathnamehashes'] = array_keys($files); 3014 3015 $event = \mod_workshop\event\assessable_uploaded::create($params); 3016 $event->set_legacy_logdata($logdata); 3017 $event->trigger(); 3018 3019 return $submission->id; 3020 } 3021 3022 /** 3023 * Helper method for validating if the current user can view the given assessment. 3024 * 3025 * @param stdClass $assessment assessment object 3026 * @param stdClass $submission submission object 3027 * @return void 3028 * @throws moodle_exception 3029 * @since Moodle 3.4 3030 */ 3031 public function check_view_assessment($assessment, $submission) { 3032 global $USER; 3033 3034 $isauthor = $submission->authorid == $USER->id; 3035 $isreviewer = $assessment->reviewerid == $USER->id; 3036 $canviewallassessments = has_capability('mod/workshop:viewallassessments', $this->context); 3037 $canviewallsubmissions = has_capability('mod/workshop:viewallsubmissions', $this->context); 3038 3039 $canviewallsubmissions = $canviewallsubmissions && $this->check_group_membership($submission->authorid); 3040 3041 if (!$isreviewer and !$isauthor and !($canviewallassessments and $canviewallsubmissions)) { 3042 print_error('nopermissions', 'error', $this->view_url(), 'view this assessment'); 3043 } 3044 3045 if ($isauthor and !$isreviewer and !$canviewallassessments and $this->phase != self::PHASE_CLOSED) { 3046 // Authors can see assessments of their work at the end of workshop only. 3047 print_error('nopermissions', 'error', $this->view_url(), 'view assessment of own work before workshop is closed'); 3048 } 3049 } 3050 3051 /** 3052 * Helper method for validating if the current user can edit the given assessment. 3053 * 3054 * @param stdClass $assessment assessment object 3055 * @param stdClass $submission submission object 3056 * @return void 3057 * @throws moodle_exception 3058 * @since Moodle 3.4 3059 */ 3060 public function check_edit_assessment($assessment, $submission) { 3061 global $USER; 3062 3063 $this->check_view_assessment($assessment, $submission); 3064 // Further checks. 3065 $isreviewer = ($USER->id == $assessment->reviewerid); 3066 3067 $assessmenteditable = $isreviewer && $this->assessing_allowed($USER->id); 3068 if (!$assessmenteditable) { 3069 throw new moodle_exception('nopermissions', 'error', '', 'edit assessments'); 3070 } 3071 3072 list($assessed, $notice) = $this->check_examples_assessed_before_assessment($assessment->reviewerid); 3073 if (!$assessed) { 3074 throw new moodle_exception($notice, 'mod_workshop'); 3075 } 3076 } 3077 3078 /** 3079 * Adds information to an allocated assessment (function used the first time a review is done or when updating an existing one). 3080 * 3081 * @param stdClass $assessment the assessment 3082 * @param stdClass $submission the submission 3083 * @param stdClass $data the assessment data to be added or Updated 3084 * @param stdClass $strategy the strategy instance 3085 * @return float|null Raw percentual grade (0.00000 to 100.00000) for submission 3086 * @since Moodle 3.4 3087 */ 3088 public function edit_assessment($assessment, $submission, $data, $strategy) { 3089 global $DB; 3090 3091 $cansetassessmentweight = has_capability('mod/workshop:allocate', $this->context); 3092 3093 // Let the grading strategy subplugin save its data. 3094 $rawgrade = $strategy->save_assessment($assessment, $data); 3095 3096 // Store the data managed by the workshop core. 3097 $coredata = (object)array('id' => $assessment->id); 3098 if (isset($data->feedbackauthor_editor)) { 3099 $coredata->feedbackauthor_editor = $data->feedbackauthor_editor; 3100 $coredata = file_postupdate_standard_editor($coredata, 'feedbackauthor', $this->overall_feedback_content_options(), 3101 $this->context, 'mod_workshop', 'overallfeedback_content', $assessment->id); 3102 unset($coredata->feedbackauthor_editor); 3103 } 3104 if (isset($data->feedbackauthorattachment_filemanager)) { 3105 $coredata->feedbackauthorattachment_filemanager = $data->feedbackauthorattachment_filemanager; 3106 $coredata = file_postupdate_standard_filemanager($coredata, 'feedbackauthorattachment', 3107 $this->overall_feedback_attachment_options(), $this->context, 'mod_workshop', 'overallfeedback_attachment', 3108 $assessment->id); 3109 unset($coredata->feedbackauthorattachment_filemanager); 3110 if (empty($coredata->feedbackauthorattachment)) { 3111 $coredata->feedbackauthorattachment = 0; 3112 } 3113 } 3114 if (isset($data->weight) and $cansetassessmentweight) { 3115 $coredata->weight = $data->weight; 3116 } 3117 // Update the assessment data if there is something other than just the 'id'. 3118 if (count((array)$coredata) > 1 ) { 3119 $DB->update_record('workshop_assessments', $coredata); 3120 $params = array( 3121 'relateduserid' => $submission->authorid, 3122 'objectid' => $assessment->id, 3123 'context' => $this->context, 3124 'other' => array( 3125 'workshopid' => $this->id, 3126 'submissionid' => $assessment->submissionid 3127 ) 3128 ); 3129 3130 if (is_null($assessment->grade)) { 3131 // All workshop_assessments are created when allocations are made. The create event is of more use located here. 3132 $event = \mod_workshop\event\submission_assessed::create($params); 3133 $event->trigger(); 3134 } else { 3135 $params['other']['grade'] = $assessment->grade; 3136 $event = \mod_workshop\event\submission_reassessed::create($params); 3137 $event->trigger(); 3138 } 3139 } 3140 return $rawgrade; 3141 } 3142 3143 /** 3144 * Evaluates an assessment. 3145 * 3146 * @param stdClass $assessment the assessment 3147 * @param stdClass $data the assessment data to be updated 3148 * @param bool $cansetassessmentweight whether the user can change the assessment weight 3149 * @param bool $canoverridegrades whether the user can override the assessment grades 3150 * @return void 3151 * @since Moodle 3.4 3152 */ 3153 public function evaluate_assessment($assessment, $data, $cansetassessmentweight, $canoverridegrades) { 3154 global $DB, $USER; 3155 3156 $data = file_postupdate_standard_editor($data, 'feedbackreviewer', array(), $this->context); 3157 $record = new stdclass(); 3158 $record->id = $assessment->id; 3159 if ($cansetassessmentweight) { 3160 $record->weight = $data->weight; 3161 } 3162 if ($canoverridegrades) { 3163 $record->gradinggradeover = $this->raw_grade_value($data->gradinggradeover, $this->gradinggrade); 3164 $record->gradinggradeoverby = $USER->id; 3165 $record->feedbackreviewer = $data->feedbackreviewer; 3166 $record->feedbackreviewerformat = $data->feedbackreviewerformat; 3167 } 3168 $DB->update_record('workshop_assessments', $record); 3169 } 3170 3171 /** 3172 * Trigger submission viewed event. 3173 * 3174 * @param stdClass $submission submission object 3175 * @since Moodle 3.4 3176 */ 3177 public function set_submission_viewed($submission) { 3178 $params = array( 3179 'objectid' => $submission->id, 3180 'context' => $this->context, 3181 'courseid' => $this->course->id, 3182 'relateduserid' => $submission->authorid, 3183 'other' => array( 3184 'workshopid' => $this->id 3185 ) 3186 ); 3187 3188 $event = \mod_workshop\event\submission_viewed::create($params); 3189 $event->trigger(); 3190 } 3191 3192 /** 3193 * Evaluates a submission. 3194 * 3195 * @param stdClass $submission the submission 3196 * @param stdClass $data the submission data to be updated 3197 * @param bool $canpublish whether the user can publish the submission 3198 * @param bool $canoverride whether the user can override the submission grade 3199 * @return void 3200 * @since Moodle 3.4 3201 */ 3202 public function evaluate_submission($submission, $data, $canpublish, $canoverride) { 3203 global $DB, $USER; 3204 3205 $data = file_postupdate_standard_editor($data, 'feedbackauthor', array(), $this->context); 3206 $record = new stdclass(); 3207 $record->id = $submission->id; 3208 if ($canoverride) { 3209 $record->gradeover = $this->raw_grade_value($data->gradeover, $this->grade); 3210 $record->gradeoverby = $USER->id; 3211 $record->feedbackauthor = $data->feedbackauthor; 3212 $record->feedbackauthorformat = $data->feedbackauthorformat; 3213 } 3214 if ($canpublish) { 3215 $record->published = !empty($data->published); 3216 } 3217 $DB->update_record('workshop_submissions', $record); 3218 } 3219 3220 //////////////////////////////////////////////////////////////////////////////// 3221 // Internal methods (implementation details) // 3222 //////////////////////////////////////////////////////////////////////////////// 3223 3224 /** 3225 * Given an array of all assessments of a single submission, calculates the final grade for this submission 3226 * 3227 * This calculates the weighted mean of the passed assessment grades. If, however, the submission grade 3228 * was overridden by a teacher, the gradeover value is returned and the rest of grades are ignored. 3229 * 3230 * @param array $assessments of stdclass(->submissionid ->submissiongrade ->gradeover ->weight ->grade) 3231 * @return void 3232 */ 3233 protected function aggregate_submission_grades_process(array $assessments) { 3234 global $DB; 3235 3236 $submissionid = null; // the id of the submission being processed 3237 $current = null; // the grade currently saved in database 3238 $finalgrade = null; // the new grade to be calculated 3239 $sumgrades = 0; 3240 $sumweights = 0; 3241 3242 foreach ($assessments as $assessment) { 3243 if (is_null($submissionid)) { 3244 // the id is the same in all records, fetch it during the first loop cycle 3245 $submissionid = $assessment->submissionid; 3246 } 3247 if (is_null($current)) { 3248 // the currently saved grade is the same in all records, fetch it during the first loop cycle 3249 $current = $assessment->submissiongrade; 3250 } 3251 if (is_null($assessment->grade)) { 3252 // this was not assessed yet 3253 continue; 3254 } 3255 if ($assessment->weight == 0) { 3256 // this does not influence the calculation 3257 continue; 3258 } 3259 $sumgrades += $assessment->grade * $assessment->weight; 3260 $sumweights += $assessment->weight; 3261 } 3262 if ($sumweights > 0 and is_null($finalgrade)) { 3263 $finalgrade = grade_floatval($sumgrades / $sumweights); 3264 } 3265 // check if the new final grade differs from the one stored in the database 3266 if (grade_floats_different($finalgrade, $current)) { 3267 // we need to save new calculation into the database 3268 $record = new stdclass(); 3269 $record->id = $submissionid; 3270 $record->grade = $finalgrade; 3271 $record->timegraded = time(); 3272 $DB->update_record('workshop_submissions', $record); 3273 } 3274 } 3275 3276 /** 3277 * Given an array of all assessments done by a single reviewer, calculates the final grading grade 3278 * 3279 * This calculates the simple mean of the passed grading grades. If, however, the grading grade 3280 * was overridden by a teacher, the gradinggradeover value is returned and the rest of grades are ignored. 3281 * 3282 * @param array $assessments of stdclass(->reviewerid ->gradinggrade ->gradinggradeover ->aggregationid ->aggregatedgrade) 3283 * @param null|int $timegraded explicit timestamp of the aggregation, defaults to the current time 3284 * @return void 3285 */ 3286 protected function aggregate_grading_grades_process(array $assessments, $timegraded = null) { 3287 global $DB; 3288 3289 $reviewerid = null; // the id of the reviewer being processed 3290 $current = null; // the gradinggrade currently saved in database 3291 $finalgrade = null; // the new grade to be calculated 3292 $agid = null; // aggregation id 3293 $sumgrades = 0; 3294 $count = 0; 3295 3296 if (is_null($timegraded)) { 3297 $timegraded = time(); 3298 } 3299 3300 foreach ($assessments as $assessment) { 3301 if (is_null($reviewerid)) { 3302 // the id is the same in all records, fetch it during the first loop cycle 3303 $reviewerid = $assessment->reviewerid; 3304 } 3305 if (is_null($agid)) { 3306 // the id is the same in all records, fetch it during the first loop cycle 3307 $agid = $assessment->aggregationid; 3308 } 3309 if (is_null($current)) { 3310 // the currently saved grade is the same in all records, fetch it during the first loop cycle 3311 $current = $assessment->aggregatedgrade; 3312 } 3313 if (!is_null($assessment->gradinggradeover)) { 3314 // the grading grade for this assessment is overridden by a teacher 3315 $sumgrades += $assessment->gradinggradeover; 3316 $count++; 3317 } else { 3318 if (!is_null($assessment->gradinggrade)) { 3319 $sumgrades += $assessment->gradinggrade; 3320 $count++; 3321 } 3322 } 3323 } 3324 if ($count > 0) { 3325 $finalgrade = grade_floatval($sumgrades / $count); 3326 } 3327 3328 // Event information. 3329 $params = array( 3330 'context' => $this->context, 3331 'courseid' => $this->course->id, 3332 'relateduserid' => $reviewerid 3333 ); 3334 3335 // check if the new final grade differs from the one stored in the database 3336 if (grade_floats_different($finalgrade, $current)) { 3337 $params['other'] = array( 3338 'currentgrade' => $current, 3339 'finalgrade' => $finalgrade 3340 ); 3341 3342 // we need to save new calculation into the database 3343 if (is_null($agid)) { 3344 // no aggregation record yet 3345 $record = new stdclass(); 3346 $record->workshopid = $this->id; 3347 $record->userid = $reviewerid; 3348 $record->gradinggrade = $finalgrade; 3349 $record->timegraded = $timegraded; 3350 $record->id = $DB->insert_record('workshop_aggregations', $record); 3351 $params['objectid'] = $record->id; 3352 $event = \mod_workshop\event\assessment_evaluated::create($params); 3353 $event->trigger(); 3354 } else { 3355 $record = new stdclass(); 3356 $record->id = $agid; 3357 $record->gradinggrade = $finalgrade; 3358 $record->timegraded = $timegraded; 3359 $DB->update_record('workshop_aggregations', $record); 3360 $params['objectid'] = $agid; 3361 $event = \mod_workshop\event\assessment_reevaluated::create($params); 3362 $event->trigger(); 3363 } 3364 } 3365 } 3366 3367 /** 3368 * Returns SQL to fetch all enrolled users with the given capability in the current workshop 3369 * 3370 * The returned array consists of string $sql and the $params array. Note that the $sql can be 3371 * empty if a grouping is selected and it has no groups. 3372 * 3373 * The list is automatically restricted according to any availability restrictions 3374 * that apply to user lists (e.g. group, grouping restrictions). 3375 * 3376 * @param string $capability the name of the capability 3377 * @param bool $musthavesubmission ff true, return only users who have already submitted 3378 * @param int $groupid 0 means ignore groups, any other value limits the result by group id 3379 * @return array of (string)sql, (array)params 3380 */ 3381 protected function get_users_with_capability_sql($capability, $musthavesubmission, $groupid) { 3382 global $CFG; 3383 /** @var int static counter used to generate unique parameter holders */ 3384 static $inc = 0; 3385 $inc++; 3386 3387 // If the caller requests all groups and we are using a selected grouping, 3388 // recursively call this function for each group in the grouping (this is 3389 // needed because get_enrolled_sql only supports a single group). 3390 if (empty($groupid) and $this->cm->groupingid) { 3391 $groupingid = $this->cm->groupingid; 3392 $groupinggroupids = array_keys(groups_get_all_groups($this->cm->course, 0, $this->cm->groupingid, 'g.id')); 3393 $sql = array(); 3394 $params = array(); 3395 foreach ($groupinggroupids as $groupinggroupid) { 3396 if ($groupinggroupid > 0) { // just in case in order not to fall into the endless loop 3397 list($gsql, $gparams) = $this->get_users_with_capability_sql($capability, $musthavesubmission, $groupinggroupid); 3398 $sql[] = $gsql; 3399 $params = array_merge($params, $gparams); 3400 } 3401 } 3402 $sql = implode(PHP_EOL." UNION ".PHP_EOL, $sql); 3403 return array($sql, $params); 3404 } 3405 3406 list($esql, $params) = get_enrolled_sql($this->context, $capability, $groupid, true); 3407 3408 $userfieldsapi = \core_user\fields::for_userpic(); 3409 $userfields = $userfieldsapi->get_sql('u', false, '', '', false)->selects; 3410 3411 $sql = "SELECT $userfields 3412 FROM {user} u 3413 JOIN ($esql) je ON (je.id = u.id AND u.deleted = 0) "; 3414 3415 if ($musthavesubmission) { 3416 $sql .= " JOIN {workshop_submissions} ws ON (ws.authorid = u.id AND ws.example = 0 AND ws.workshopid = :workshopid{$inc}) "; 3417 $params['workshopid'.$inc] = $this->id; 3418 } 3419 3420 // If the activity is restricted so that only certain users should appear 3421 // in user lists, integrate this into the same SQL. 3422 $info = new \core_availability\info_module($this->cm); 3423 list ($listsql, $listparams) = $info->get_user_list_sql(false); 3424 if ($listsql) { 3425 $sql .= " JOIN ($listsql) restricted ON restricted.id = u.id "; 3426 $params = array_merge($params, $listparams); 3427 } 3428 3429 return array($sql, $params); 3430 } 3431 3432 /** 3433 * Returns SQL statement that can be used to fetch all actively enrolled participants in the workshop 3434 * 3435 * @param bool $musthavesubmission if true, return only users who have already submitted 3436 * @param int $groupid 0 means ignore groups, any other value limits the result by group id 3437 * @return array of (string)sql, (array)params 3438 */ 3439 protected function get_participants_sql($musthavesubmission=false, $groupid=0) { 3440 3441 list($sql1, $params1) = $this->get_users_with_capability_sql('mod/workshop:submit', $musthavesubmission, $groupid); 3442 list($sql2, $params2) = $this->get_users_with_capability_sql('mod/workshop:peerassess', $musthavesubmission, $groupid); 3443 3444 if (empty($sql1) or empty($sql2)) { 3445 if (empty($sql1) and empty($sql2)) { 3446 return array('', array()); 3447 } else if (empty($sql1)) { 3448 $sql = $sql2; 3449 $params = $params2; 3450 } else { 3451 $sql = $sql1; 3452 $params = $params1; 3453 } 3454 } else { 3455 $sql = $sql1.PHP_EOL." UNION ".PHP_EOL.$sql2; 3456 $params = array_merge($params1, $params2); 3457 } 3458 3459 return array($sql, $params); 3460 } 3461 3462 /** 3463 * @return array of available workshop phases 3464 */ 3465 protected function available_phases_list() { 3466 return array( 3467 self::PHASE_SETUP => true, 3468 self::PHASE_SUBMISSION => true, 3469 self::PHASE_ASSESSMENT => true, 3470 self::PHASE_EVALUATION => true, 3471 self::PHASE_CLOSED => true, 3472 ); 3473 } 3474 3475 /** 3476 * Converts absolute URL to relative URL needed by {@see add_to_log()} 3477 * 3478 * @param moodle_url $url absolute URL 3479 * @return string 3480 */ 3481 protected function log_convert_url(moodle_url $fullurl) { 3482 static $baseurl; 3483 3484 if (!isset($baseurl)) { 3485 $baseurl = new moodle_url('/mod/workshop/'); 3486 $baseurl = $baseurl->out(); 3487 } 3488 3489 return substr($fullurl->out(), strlen($baseurl)); 3490 } 3491 3492 /** 3493 * Removes all user data related to assessments (including allocations). 3494 * 3495 * This includes assessments of example submissions as long as they are not 3496 * referential assessments. 3497 * 3498 * @param stdClass $data The actual course reset settings. 3499 * @return bool|string True on success, error message otherwise. 3500 */ 3501 protected function reset_userdata_assessments(stdClass $data) { 3502 global $DB; 3503 3504 $sql = "SELECT a.id 3505 FROM {workshop_assessments} a 3506 JOIN {workshop_submissions} s ON (a.submissionid = s.id) 3507 WHERE s.workshopid = :workshopid 3508 AND (s.example = 0 OR (s.example = 1 AND a.weight = 0))"; 3509 3510 $assessments = $DB->get_records_sql($sql, array('workshopid' => $this->id)); 3511 $this->delete_assessment(array_keys($assessments)); 3512 3513 $DB->delete_records('workshop_aggregations', array('workshopid' => $this->id)); 3514 3515 return true; 3516 } 3517 3518 /** 3519 * Removes all user data related to participants' submissions. 3520 * 3521 * @param stdClass $data The actual course reset settings. 3522 * @return bool|string True on success, error message otherwise. 3523 */ 3524 protected function reset_userdata_submissions(stdClass $data) { 3525 global $DB; 3526 3527 $submissions = $this->get_submissions(); 3528 foreach ($submissions as $submission) { 3529 $this->delete_submission($submission); 3530 } 3531 3532 return true; 3533 } 3534 3535 /** 3536 * Hard set the workshop phase to the setup one. 3537 */ 3538 protected function reset_phase() { 3539 global $DB; 3540 3541 $DB->set_field('workshop', 'phase', self::PHASE_SETUP, array('id' => $this->id)); 3542 $this->phase = self::PHASE_SETUP; 3543 } 3544 } 3545 3546 //////////////////////////////////////////////////////////////////////////////// 3547 // Renderable components 3548 //////////////////////////////////////////////////////////////////////////////// 3549 3550 /** 3551 * Represents the user planner tool 3552 * 3553 * Planner contains list of phases. Each phase contains list of tasks. Task is a simple object with 3554 * title, link and completed (true/false/null logic). 3555 */ 3556 class workshop_user_plan implements renderable { 3557 3558 /** @var int id of the user this plan is for */ 3559 public $userid; 3560 /** @var workshop */ 3561 public $workshop; 3562 /** @var array of (stdclass)tasks */ 3563 public $phases = array(); 3564 /** @var null|array of example submissions to be assessed by the planner owner */ 3565 protected $examples = null; 3566 3567 /** 3568 * Prepare an individual workshop plan for the given user. 3569 * 3570 * @param workshop $workshop instance 3571 * @param int $userid whom the plan is prepared for 3572 */ 3573 public function __construct(workshop $workshop, $userid) { 3574 global $DB; 3575 3576 $this->workshop = $workshop; 3577 $this->userid = $userid; 3578 3579 //--------------------------------------------------------- 3580 // * SETUP | submission | assessment | evaluation | closed 3581 //--------------------------------------------------------- 3582 $phase = new stdclass(); 3583 $phase->title = get_string('phasesetup', 'workshop'); 3584 $phase->tasks = array(); 3585 if (has_capability('moodle/course:manageactivities', $workshop->context, $userid)) { 3586 $task = new stdclass(); 3587 $task->title = get_string('taskintro', 'workshop'); 3588 $task->link = $workshop->updatemod_url(); 3589 $task->completed = !(trim($workshop->intro) == ''); 3590 $phase->tasks['intro'] = $task; 3591 } 3592 if (has_capability('moodle/course:manageactivities', $workshop->context, $userid)) { 3593 $task = new stdclass(); 3594 $task->title = get_string('taskinstructauthors', 'workshop'); 3595 $task->link = $workshop->updatemod_url(); 3596 $task->completed = !(trim($workshop->instructauthors) == ''); 3597 $phase->tasks['instructauthors'] = $task; 3598 } 3599 if (has_capability('mod/workshop:editdimensions', $workshop->context, $userid)) { 3600 $task = new stdclass(); 3601 $task->title = get_string('editassessmentform', 'workshop'); 3602 $task->link = $workshop->editform_url(); 3603 if ($workshop->grading_strategy_instance()->form_ready()) { 3604 $task->completed = true; 3605 } elseif ($workshop->phase > workshop::PHASE_SETUP) { 3606 $task->completed = false; 3607 } 3608 $phase->tasks['editform'] = $task; 3609 } 3610 if ($workshop->useexamples and has_capability('mod/workshop:manageexamples', $workshop->context, $userid)) { 3611 $task = new stdclass(); 3612 $task->title = get_string('prepareexamples', 'workshop'); 3613 if ($DB->count_records('workshop_submissions', array('example' => 1, 'workshopid' => $workshop->id)) > 0) { 3614 $task->completed = true; 3615 } elseif ($workshop->phase > workshop::PHASE_SETUP) { 3616 $task->completed = false; 3617 } 3618 $phase->tasks['prepareexamples'] = $task; 3619 } 3620 if (empty($phase->tasks) and $workshop->phase == workshop::PHASE_SETUP) { 3621 // if we are in the setup phase and there is no task (typical for students), let us 3622 // display some explanation what is going on 3623 $task = new stdclass(); 3624 $task->title = get_string('undersetup', 'workshop'); 3625 $task->completed = 'info'; 3626 $phase->tasks['setupinfo'] = $task; 3627 } 3628 $this->phases[workshop::PHASE_SETUP] = $phase; 3629 3630 //--------------------------------------------------------- 3631 // setup | * SUBMISSION | assessment | evaluation | closed 3632 //--------------------------------------------------------- 3633 $phase = new stdclass(); 3634 $phase->title = get_string('phasesubmission', 'workshop'); 3635 $phase->tasks = array(); 3636 if (has_capability('moodle/course:manageactivities', $workshop->context, $userid)) { 3637 $task = new stdclass(); 3638 $task->title = get_string('taskinstructreviewers', 'workshop'); 3639 $task->link = $workshop->updatemod_url(); 3640 if (trim($workshop->instructreviewers)) { 3641 $task->completed = true; 3642 } elseif ($workshop->phase >= workshop::PHASE_ASSESSMENT) { 3643 $task->completed = false; 3644 } 3645 $phase->tasks['instructreviewers'] = $task; 3646 } 3647 if ($workshop->useexamples and $workshop->examplesmode == workshop::EXAMPLES_BEFORE_SUBMISSION 3648 and has_capability('mod/workshop:submit', $workshop->context, $userid, false) 3649 and !has_capability('mod/workshop:manageexamples', $workshop->context, $userid)) { 3650 $task = new stdclass(); 3651 $task->title = get_string('exampleassesstask', 'workshop'); 3652 $examples = $this->get_examples(); 3653 $a = new stdclass(); 3654 $a->expected = count($examples); 3655 $a->assessed = 0; 3656 foreach ($examples as $exampleid => $example) { 3657 if (!is_null($example->grade)) { 3658 $a->assessed++; 3659 } 3660 } 3661 $task->details = get_string('exampleassesstaskdetails', 'workshop', $a); 3662 if ($a->assessed == $a->expected) { 3663 $task->completed = true; 3664 } elseif ($workshop->phase >= workshop::PHASE_ASSESSMENT) { 3665 $task->completed = false; 3666 } 3667 $phase->tasks['examples'] = $task; 3668 } 3669 if (has_capability('mod/workshop:submit', $workshop->context, $userid, false)) { 3670 $task = new stdclass(); 3671 $task->title = get_string('tasksubmit', 'workshop'); 3672 $task->link = $workshop->submission_url(); 3673 if ($DB->record_exists('workshop_submissions', array('workshopid'=>$workshop->id, 'example'=>0, 'authorid'=>$userid))) { 3674 $task->completed = true; 3675 } elseif ($workshop->phase >= workshop::PHASE_ASSESSMENT) { 3676 $task->completed = false; 3677 } else { 3678 $task->completed = null; // still has a chance to submit 3679 } 3680 $phase->tasks['submit'] = $task; 3681 } 3682 if (has_capability('mod/workshop:allocate', $workshop->context, $userid)) { 3683 if ($workshop->phaseswitchassessment) { 3684 $task = new stdClass(); 3685 $allocator = $DB->get_record('workshopallocation_scheduled', array('workshopid' => $workshop->id)); 3686 if (empty($allocator)) { 3687 $task->completed = false; 3688 } else if ($allocator->enabled and is_null($allocator->resultstatus)) { 3689 $task->completed = true; 3690 } else if ($workshop->submissionend > time()) { 3691 $task->completed = null; 3692 } else { 3693 $task->completed = false; 3694 } 3695 $task->title = get_string('setup', 'workshopallocation_scheduled'); 3696 $task->link = $workshop->allocation_url('scheduled'); 3697 $phase->tasks['allocatescheduled'] = $task; 3698 } 3699 $task = new stdclass(); 3700 $task->title = get_string('allocate', 'workshop'); 3701 $task->link = $workshop->allocation_url(); 3702 $numofauthors = $workshop->count_potential_authors(false); 3703 $numofsubmissions = $DB->count_records('workshop_submissions', array('workshopid'=>$workshop->id, 'example'=>0)); 3704 $sql = 'SELECT COUNT(s.id) AS nonallocated 3705 FROM {workshop_submissions} s 3706 LEFT JOIN {workshop_assessments} a ON (a.submissionid=s.id) 3707 WHERE s.workshopid = :workshopid AND s.example=0 AND a.submissionid IS NULL'; 3708 $params['workshopid'] = $workshop->id; 3709 $numnonallocated = $DB->count_records_sql($sql, $params); 3710 if ($numofsubmissions == 0) { 3711 $task->completed = null; 3712 } elseif ($numnonallocated == 0) { 3713 $task->completed = true; 3714 } elseif ($workshop->phase > workshop::PHASE_SUBMISSION) { 3715 $task->completed = false; 3716 } else { 3717 $task->completed = null; // still has a chance to allocate 3718 } 3719 $a = new stdclass(); 3720 $a->expected = $numofauthors; 3721 $a->submitted = $numofsubmissions; 3722 $a->allocate = $numnonallocated; 3723 $task->details = get_string('allocatedetails', 'workshop', $a); 3724 unset($a); 3725 $phase->tasks['allocate'] = $task; 3726 3727 if ($numofsubmissions < $numofauthors and $workshop->phase >= workshop::PHASE_SUBMISSION) { 3728 $task = new stdclass(); 3729 $task->title = get_string('someuserswosubmission', 'workshop'); 3730 $task->completed = 'info'; 3731 $phase->tasks['allocateinfo'] = $task; 3732 } 3733 3734 } 3735 if ($workshop->submissionstart) { 3736 $task = new stdclass(); 3737 $task->title = get_string('submissionstartdatetime', 'workshop', workshop::timestamp_formats($workshop->submissionstart)); 3738 $task->completed = 'info'; 3739 $phase->tasks['submissionstartdatetime'] = $task; 3740 } 3741 if ($workshop->submissionend) { 3742 $task = new stdclass(); 3743 $task->title = get_string('submissionenddatetime', 'workshop', workshop::timestamp_formats($workshop->submissionend)); 3744 $task->completed = 'info'; 3745 $phase->tasks['submissionenddatetime'] = $task; 3746 } 3747 if (($workshop->submissionstart < time()) and $workshop->latesubmissions) { 3748 // If submission deadline has passed and late submissions are allowed, only display 'latesubmissionsallowed' text to 3749 // users (students) who have not submitted and users(teachers, admins) who can switch pahase.. 3750 if (has_capability('mod/workshop:switchphase', $workshop->context, $userid) || 3751 (!$workshop->get_submission_by_author($userid) && $workshop->submissionend < time())) { 3752 $task = new stdclass(); 3753 $task->title = get_string('latesubmissionsallowed', 'workshop'); 3754 $task->completed = 'info'; 3755 $phase->tasks['latesubmissionsallowed'] = $task; 3756 } 3757 } 3758 if (isset($phase->tasks['submissionstartdatetime']) or isset($phase->tasks['submissionenddatetime'])) { 3759 if (has_capability('mod/workshop:ignoredeadlines', $workshop->context, $userid)) { 3760 $task = new stdclass(); 3761 $task->title = get_string('deadlinesignored', 'workshop'); 3762 $task->completed = 'info'; 3763 $phase->tasks['deadlinesignored'] = $task; 3764 } 3765 } 3766 $this->phases[workshop::PHASE_SUBMISSION] = $phase; 3767 3768 //--------------------------------------------------------- 3769 // setup | submission | * ASSESSMENT | evaluation | closed 3770 //--------------------------------------------------------- 3771 $phase = new stdclass(); 3772 $phase->title = get_string('phaseassessment', 'workshop'); 3773 $phase->tasks = array(); 3774 $phase->isreviewer = has_capability('mod/workshop:peerassess', $workshop->context, $userid); 3775 if ($workshop->phase == workshop::PHASE_SUBMISSION and $workshop->phaseswitchassessment 3776 and has_capability('mod/workshop:switchphase', $workshop->context, $userid)) { 3777 $task = new stdClass(); 3778 $task->title = get_string('switchphase30auto', 'mod_workshop', workshop::timestamp_formats($workshop->submissionend)); 3779 $task->completed = 'info'; 3780 $phase->tasks['autoswitchinfo'] = $task; 3781 } 3782 if ($workshop->useexamples and $workshop->examplesmode == workshop::EXAMPLES_BEFORE_ASSESSMENT 3783 and $phase->isreviewer and !has_capability('mod/workshop:manageexamples', $workshop->context, $userid)) { 3784 $task = new stdclass(); 3785 $task->title = get_string('exampleassesstask', 'workshop'); 3786 $examples = $workshop->get_examples_for_reviewer($userid); 3787 $a = new stdclass(); 3788 $a->expected = count($examples); 3789 $a->assessed = 0; 3790 foreach ($examples as $exampleid => $example) { 3791 if (!is_null($example->grade)) { 3792 $a->assessed++; 3793 } 3794 } 3795 $task->details = get_string('exampleassesstaskdetails', 'workshop', $a); 3796 if ($a->assessed == $a->expected) { 3797 $task->completed = true; 3798 } elseif ($workshop->phase > workshop::PHASE_ASSESSMENT) { 3799 $task->completed = false; 3800 } 3801 $phase->tasks['examples'] = $task; 3802 } 3803 if (empty($phase->tasks['examples']) or !empty($phase->tasks['examples']->completed)) { 3804 $phase->assessments = $workshop->get_assessments_by_reviewer($userid); 3805 $numofpeers = 0; // number of allocated peer-assessments 3806 $numofpeerstodo = 0; // number of peer-assessments to do 3807 $numofself = 0; // number of allocated self-assessments - should be 0 or 1 3808 $numofselftodo = 0; // number of self-assessments to do - should be 0 or 1 3809 foreach ($phase->assessments as $a) { 3810 if ($a->authorid == $userid) { 3811 $numofself++; 3812 if (is_null($a->grade)) { 3813 $numofselftodo++; 3814 } 3815 } else { 3816 $numofpeers++; 3817 if (is_null($a->grade)) { 3818 $numofpeerstodo++; 3819 } 3820 } 3821 } 3822 unset($a); 3823 if ($numofpeers) { 3824 $task = new stdclass(); 3825 if ($numofpeerstodo == 0) { 3826 $task->completed = true; 3827 } elseif ($workshop->phase > workshop::PHASE_ASSESSMENT) { 3828 $task->completed = false; 3829 } 3830 $a = new stdclass(); 3831 $a->total = $numofpeers; 3832 $a->todo = $numofpeerstodo; 3833 $task->title = get_string('taskassesspeers', 'workshop'); 3834 $task->details = get_string('taskassesspeersdetails', 'workshop', $a); 3835 unset($a); 3836 $phase->tasks['assesspeers'] = $task; 3837 } 3838 if ($workshop->useselfassessment and $numofself) { 3839 $task = new stdclass(); 3840 if ($numofselftodo == 0) { 3841 $task->completed = true; 3842 } elseif ($workshop->phase > workshop::PHASE_ASSESSMENT) { 3843 $task->completed = false; 3844 } 3845 $task->title = get_string('taskassessself', 'workshop'); 3846 $phase->tasks['assessself'] = $task; 3847 } 3848 } 3849 if ($workshop->assessmentstart) { 3850 $task = new stdclass(); 3851 $task->title = get_string('assessmentstartdatetime', 'workshop', workshop::timestamp_formats($workshop->assessmentstart)); 3852 $task->completed = 'info'; 3853 $phase->tasks['assessmentstartdatetime'] = $task; 3854 } 3855 if ($workshop->assessmentend) { 3856 $task = new stdclass(); 3857 $task->title = get_string('assessmentenddatetime', 'workshop', workshop::timestamp_formats($workshop->assessmentend)); 3858 $task->completed = 'info'; 3859 $phase->tasks['assessmentenddatetime'] = $task; 3860 } 3861 if (isset($phase->tasks['assessmentstartdatetime']) or isset($phase->tasks['assessmentenddatetime'])) { 3862 if (has_capability('mod/workshop:ignoredeadlines', $workshop->context, $userid)) { 3863 $task = new stdclass(); 3864 $task->title = get_string('deadlinesignored', 'workshop'); 3865 $task->completed = 'info'; 3866 $phase->tasks['deadlinesignored'] = $task; 3867 } 3868 } 3869 $this->phases[workshop::PHASE_ASSESSMENT] = $phase; 3870 3871 //--------------------------------------------------------- 3872 // setup | submission | assessment | * EVALUATION | closed 3873 //--------------------------------------------------------- 3874 $phase = new stdclass(); 3875 $phase->title = get_string('phaseevaluation', 'workshop'); 3876 $phase->tasks = array(); 3877 if (has_capability('mod/workshop:overridegrades', $workshop->context)) { 3878 $expected = $workshop->count_potential_authors(false); 3879 $calculated = $DB->count_records_select('workshop_submissions', 3880 'workshopid = ? AND (grade IS NOT NULL OR gradeover IS NOT NULL)', array($workshop->id)); 3881 $task = new stdclass(); 3882 $task->title = get_string('calculatesubmissiongrades', 'workshop'); 3883 $a = new stdclass(); 3884 $a->expected = $expected; 3885 $a->calculated = $calculated; 3886 $task->details = get_string('calculatesubmissiongradesdetails', 'workshop', $a); 3887 if ($calculated >= $expected) { 3888 $task->completed = true; 3889 } elseif ($workshop->phase > workshop::PHASE_EVALUATION) { 3890 $task->completed = false; 3891 } 3892 $phase->tasks['calculatesubmissiongrade'] = $task; 3893 3894 $expected = $workshop->count_potential_reviewers(false); 3895 $calculated = $DB->count_records_select('workshop_aggregations', 3896 'workshopid = ? AND gradinggrade IS NOT NULL', array($workshop->id)); 3897 $task = new stdclass(); 3898 $task->title = get_string('calculategradinggrades', 'workshop'); 3899 $a = new stdclass(); 3900 $a->expected = $expected; 3901 $a->calculated = $calculated; 3902 $task->details = get_string('calculategradinggradesdetails', 'workshop', $a); 3903 if ($calculated >= $expected) { 3904 $task->completed = true; 3905 } elseif ($workshop->phase > workshop::PHASE_EVALUATION) { 3906 $task->completed = false; 3907 } 3908 $phase->tasks['calculategradinggrade'] = $task; 3909 3910 } elseif ($workshop->phase == workshop::PHASE_EVALUATION) { 3911 $task = new stdclass(); 3912 $task->title = get_string('evaluategradeswait', 'workshop'); 3913 $task->completed = 'info'; 3914 $phase->tasks['evaluateinfo'] = $task; 3915 } 3916 3917 if (has_capability('moodle/course:manageactivities', $workshop->context, $userid)) { 3918 $task = new stdclass(); 3919 $task->title = get_string('taskconclusion', 'workshop'); 3920 $task->link = $workshop->updatemod_url(); 3921 if (trim($workshop->conclusion)) { 3922 $task->completed = true; 3923 } elseif ($workshop->phase >= workshop::PHASE_EVALUATION) { 3924 $task->completed = false; 3925 } 3926 $phase->tasks['conclusion'] = $task; 3927 } 3928 3929 $this->phases[workshop::PHASE_EVALUATION] = $phase; 3930 3931 //--------------------------------------------------------- 3932 // setup | submission | assessment | evaluation | * CLOSED 3933 //--------------------------------------------------------- 3934 $phase = new stdclass(); 3935 $phase->title = get_string('phaseclosed', 'workshop'); 3936 $phase->tasks = array(); 3937 $this->phases[workshop::PHASE_CLOSED] = $phase; 3938 3939 // Polish data, set default values if not done explicitly 3940 foreach ($this->phases as $phasecode => $phase) { 3941 $phase->title = isset($phase->title) ? $phase->title : ''; 3942 $phase->tasks = isset($phase->tasks) ? $phase->tasks : array(); 3943 if ($phasecode == $workshop->phase) { 3944 $phase->active = true; 3945 } else { 3946 $phase->active = false; 3947 } 3948 if (!isset($phase->actions)) { 3949 $phase->actions = array(); 3950 } 3951 3952 foreach ($phase->tasks as $taskcode => $task) { 3953 $task->title = isset($task->title) ? $task->title : ''; 3954 $task->link = isset($task->link) ? $task->link : null; 3955 $task->details = isset($task->details) ? $task->details : ''; 3956 $task->completed = isset($task->completed) ? $task->completed : null; 3957 } 3958 } 3959 3960 // Add phase switching actions. 3961 if (has_capability('mod/workshop:switchphase', $workshop->context, $userid)) { 3962 $nextphases = array( 3963 workshop::PHASE_SETUP => workshop::PHASE_SUBMISSION, 3964 workshop::PHASE_SUBMISSION => workshop::PHASE_ASSESSMENT, 3965 workshop::PHASE_ASSESSMENT => workshop::PHASE_EVALUATION, 3966 workshop::PHASE_EVALUATION => workshop::PHASE_CLOSED, 3967 ); 3968 foreach ($this->phases as $phasecode => $phase) { 3969 if ($phase->active) { 3970 if (isset($nextphases[$workshop->phase])) { 3971 $task = new stdClass(); 3972 $task->title = get_string('switchphasenext', 'mod_workshop'); 3973 $task->link = $workshop->switchphase_url($nextphases[$workshop->phase]); 3974 $task->details = ''; 3975 $task->completed = null; 3976 $phase->tasks['switchtonextphase'] = $task; 3977 } 3978 3979 } else { 3980 $action = new stdclass(); 3981 $action->type = 'switchphase'; 3982 $action->url = $workshop->switchphase_url($phasecode); 3983 $phase->actions[] = $action; 3984 } 3985 } 3986 } 3987 } 3988 3989 /** 3990 * Returns example submissions to be assessed by the owner of the planner 3991 * 3992 * This is here to cache the DB query because the same list is needed later in view.php 3993 * 3994 * @see workshop::get_examples_for_reviewer() for the format of returned value 3995 * @return array 3996 */ 3997 public function get_examples() { 3998 if (is_null($this->examples)) { 3999 $this->examples = $this->workshop->get_examples_for_reviewer($this->userid); 4000 } 4001 return $this->examples; 4002 } 4003 } 4004 4005 /** 4006 * Common base class for submissions and example submissions rendering 4007 * 4008 * Subclasses of this class convert raw submission record from 4009 * workshop_submissions table (as returned by {@see workshop::get_submission_by_id()} 4010 * for example) into renderable objects. 4011 */ 4012 abstract class workshop_submission_base { 4013 4014 /** @var bool is the submission anonymous (i.e. contains author information) */ 4015 protected $anonymous; 4016 4017 /* @var array of columns from workshop_submissions that are assigned as properties */ 4018 protected $fields = array(); 4019 4020 /** @var workshop */ 4021 protected $workshop; 4022 4023 /** 4024 * Copies the properties of the given database record into properties of $this instance 4025 * 4026 * @param workshop $workshop 4027 * @param stdClass $submission full record 4028 * @param bool $showauthor show the author-related information 4029 * @param array $options additional properties 4030 */ 4031 public function __construct(workshop $workshop, stdClass $submission, $showauthor = false) { 4032 4033 $this->workshop = $workshop; 4034 4035 foreach ($this->fields as $field) { 4036 if (!property_exists($submission, $field)) { 4037 throw new coding_exception('Submission record must provide public property ' . $field); 4038 } 4039 if (!property_exists($this, $field)) { 4040 throw new coding_exception('Renderable component must accept public property ' . $field); 4041 } 4042 $this->{$field} = $submission->{$field}; 4043 } 4044 4045 if ($showauthor) { 4046 $this->anonymous = false; 4047 } else { 4048 $this->anonymize(); 4049 } 4050 } 4051 4052 /** 4053 * Unsets all author-related properties so that the renderer does not have access to them 4054 * 4055 * Usually this is called by the contructor but can be called explicitely, too. 4056 */ 4057 public function anonymize() { 4058 $authorfields = explode(',', implode(',', \core_user\fields::get_picture_fields())); 4059 foreach ($authorfields as $field) { 4060 $prefixedusernamefield = 'author' . $field; 4061 unset($this->{$prefixedusernamefield}); 4062 } 4063 $this->anonymous = true; 4064 } 4065 4066 /** 4067 * Does the submission object contain author-related information? 4068 * 4069 * @return null|boolean 4070 */ 4071 public function is_anonymous() { 4072 return $this->anonymous; 4073 } 4074 } 4075 4076 /** 4077 * Renderable object containing a basic set of information needed to display the submission summary 4078 * 4079 * @see workshop_renderer::render_workshop_submission_summary 4080 */ 4081 class workshop_submission_summary extends workshop_submission_base implements renderable { 4082 4083 /** @var int */ 4084 public $id; 4085 /** @var string */ 4086 public $title; 4087 /** @var string graded|notgraded */ 4088 public $status; 4089 /** @var int */ 4090 public $timecreated; 4091 /** @var int */ 4092 public $timemodified; 4093 /** @var int */ 4094 public $authorid; 4095 /** @var string */ 4096 public $authorfirstname; 4097 /** @var string */ 4098 public $authorlastname; 4099 /** @var string */ 4100 public $authorfirstnamephonetic; 4101 /** @var string */ 4102 public $authorlastnamephonetic; 4103 /** @var string */ 4104 public $authormiddlename; 4105 /** @var string */ 4106 public $authoralternatename; 4107 /** @var int */ 4108 public $authorpicture; 4109 /** @var string */ 4110 public $authorimagealt; 4111 /** @var string */ 4112 public $authoremail; 4113 /** @var moodle_url to display submission */ 4114 public $url; 4115 4116 /** 4117 * @var array of columns from workshop_submissions that are assigned as properties 4118 * of instances of this class 4119 */ 4120 protected $fields = array( 4121 'id', 'title', 'timecreated', 'timemodified', 4122 'authorid', 'authorfirstname', 'authorlastname', 'authorfirstnamephonetic', 'authorlastnamephonetic', 4123 'authormiddlename', 'authoralternatename', 'authorpicture', 4124 'authorimagealt', 'authoremail'); 4125 } 4126 4127 /** 4128 * Renderable object containing all the information needed to display the submission 4129 * 4130 * @see workshop_renderer::render_workshop_submission() 4131 */ 4132 class workshop_submission extends workshop_submission_summary implements renderable { 4133 4134 /** @var string */ 4135 public $content; 4136 /** @var int */ 4137 public $contentformat; 4138 /** @var bool */ 4139 public $contenttrust; 4140 /** @var array */ 4141 public $attachment; 4142 4143 /** 4144 * @var array of columns from workshop_submissions that are assigned as properties 4145 * of instances of this class 4146 */ 4147 protected $fields = array( 4148 'id', 'title', 'timecreated', 'timemodified', 'content', 'contentformat', 'contenttrust', 4149 'attachment', 'authorid', 'authorfirstname', 'authorlastname', 'authorfirstnamephonetic', 'authorlastnamephonetic', 4150 'authormiddlename', 'authoralternatename', 'authorpicture', 'authorimagealt', 'authoremail'); 4151 } 4152 4153 /** 4154 * Renderable object containing a basic set of information needed to display the example submission summary 4155 * 4156 * @see workshop::prepare_example_summary() 4157 * @see workshop_renderer::render_workshop_example_submission_summary() 4158 */ 4159 class workshop_example_submission_summary extends workshop_submission_base implements renderable { 4160 4161 /** @var int */ 4162 public $id; 4163 /** @var string */ 4164 public $title; 4165 /** @var string graded|notgraded */ 4166 public $status; 4167 /** @var stdClass */ 4168 public $gradeinfo; 4169 /** @var moodle_url */ 4170 public $url; 4171 /** @var moodle_url */ 4172 public $editurl; 4173 /** @var string */ 4174 public $assesslabel; 4175 /** @var moodle_url */ 4176 public $assessurl; 4177 /** @var bool must be set explicitly by the caller */ 4178 public $editable = false; 4179 4180 /** 4181 * @var array of columns from workshop_submissions that are assigned as properties 4182 * of instances of this class 4183 */ 4184 protected $fields = array('id', 'title'); 4185 4186 /** 4187 * Example submissions are always anonymous 4188 * 4189 * @return true 4190 */ 4191 public function is_anonymous() { 4192 return true; 4193 } 4194 } 4195 4196 /** 4197 * Renderable object containing all the information needed to display the example submission 4198 * 4199 * @see workshop_renderer::render_workshop_example_submission() 4200 */ 4201 class workshop_example_submission extends workshop_example_submission_summary implements renderable { 4202 4203 /** @var string */ 4204 public $content; 4205 /** @var int */ 4206 public $contentformat; 4207 /** @var bool */ 4208 public $contenttrust; 4209 /** @var array */ 4210 public $attachment; 4211 4212 /** 4213 * @var array of columns from workshop_submissions that are assigned as properties 4214 * of instances of this class 4215 */ 4216 protected $fields = array('id', 'title', 'content', 'contentformat', 'contenttrust', 'attachment'); 4217 } 4218 4219 4220 /** 4221 * Common base class for assessments rendering 4222 * 4223 * Subclasses of this class convert raw assessment record from 4224 * workshop_assessments table (as returned by {@see workshop::get_assessment_by_id()} 4225 * for example) into renderable objects. 4226 */ 4227 abstract class workshop_assessment_base { 4228 4229 /** @var string the optional title of the assessment */ 4230 public $title = ''; 4231 4232 /** @var workshop_assessment_form $form as returned by {@link workshop_strategy::get_assessment_form()} */ 4233 public $form; 4234 4235 /** @var moodle_url */ 4236 public $url; 4237 4238 /** @var float|null the real received grade */ 4239 public $realgrade = null; 4240 4241 /** @var float the real maximum grade */ 4242 public $maxgrade; 4243 4244 /** @var stdClass|null reviewer user info */ 4245 public $reviewer = null; 4246 4247 /** @var stdClass|null assessed submission's author user info */ 4248 public $author = null; 4249 4250 /** @var array of actions */ 4251 public $actions = array(); 4252 4253 /* @var array of columns that are assigned as properties */ 4254 protected $fields = array(); 4255 4256 /** @var workshop */ 4257 public $workshop; 4258 4259 /** 4260 * Copies the properties of the given database record into properties of $this instance 4261 * 4262 * The $options keys are: showreviewer, showauthor 4263 * @param workshop $workshop 4264 * @param stdClass $assessment full record 4265 * @param array $options additional properties 4266 */ 4267 public function __construct(workshop $workshop, stdClass $record, array $options = array()) { 4268 4269 $this->workshop = $workshop; 4270 $this->validate_raw_record($record); 4271 4272 foreach ($this->fields as $field) { 4273 if (!property_exists($record, $field)) { 4274 throw new coding_exception('Assessment record must provide public property ' . $field); 4275 } 4276 if (!property_exists($this, $field)) { 4277 throw new coding_exception('Renderable component must accept public property ' . $field); 4278 } 4279 $this->{$field} = $record->{$field}; 4280 } 4281 4282 if (!empty($options['showreviewer'])) { 4283 $this->reviewer = user_picture::unalias($record, null, 'revieweridx', 'reviewer'); 4284 } 4285 4286 if (!empty($options['showauthor'])) { 4287 $this->author = user_picture::unalias($record, null, 'authorid', 'author'); 4288 } 4289 } 4290 4291 /** 4292 * Adds a new action 4293 * 4294 * @param moodle_url $url action URL 4295 * @param string $label action label 4296 * @param string $method get|post 4297 */ 4298 public function add_action(moodle_url $url, $label, $method = 'get') { 4299 4300 $action = new stdClass(); 4301 $action->url = $url; 4302 $action->label = $label; 4303 $action->method = $method; 4304 4305 $this->actions[] = $action; 4306 } 4307 4308 /** 4309 * Makes sure that we can cook the renderable component from the passed raw database record 4310 * 4311 * @param stdClass $assessment full assessment record 4312 * @throws coding_exception if the caller passed unexpected data 4313 */ 4314 protected function validate_raw_record(stdClass $record) { 4315 // nothing to do here 4316 } 4317 } 4318 4319 4320 /** 4321 * Represents a rendarable full assessment 4322 */ 4323 class workshop_assessment extends workshop_assessment_base implements renderable { 4324 4325 /** @var int */ 4326 public $id; 4327 4328 /** @var int */ 4329 public $submissionid; 4330 4331 /** @var int */ 4332 public $weight; 4333 4334 /** @var int */ 4335 public $timecreated; 4336 4337 /** @var int */ 4338 public $timemodified; 4339 4340 /** @var float */ 4341 public $grade; 4342 4343 /** @var float */ 4344 public $gradinggrade; 4345 4346 /** @var float */ 4347 public $gradinggradeover; 4348 4349 /** @var string */ 4350 public $feedbackauthor; 4351 4352 /** @var int */ 4353 public $feedbackauthorformat; 4354 4355 /** @var int */ 4356 public $feedbackauthorattachment; 4357 4358 /** @var array */ 4359 protected $fields = array('id', 'submissionid', 'weight', 'timecreated', 4360 'timemodified', 'grade', 'gradinggrade', 'gradinggradeover', 'feedbackauthor', 4361 'feedbackauthorformat', 'feedbackauthorattachment'); 4362 4363 /** 4364 * Format the overall feedback text content 4365 * 4366 * False is returned if the overall feedback feature is disabled. Null is returned 4367 * if the overall feedback content has not been found. Otherwise, string with 4368 * formatted feedback text is returned. 4369 * 4370 * @return string|bool|null 4371 */ 4372 public function get_overall_feedback_content() { 4373 4374 if ($this->workshop->overallfeedbackmode == 0) { 4375 return false; 4376 } 4377 4378 if (trim($this->feedbackauthor) === '') { 4379 return null; 4380 } 4381 4382 $content = file_rewrite_pluginfile_urls($this->feedbackauthor, 'pluginfile.php', $this->workshop->context->id, 4383 'mod_workshop', 'overallfeedback_content', $this->id); 4384 $content = format_text($content, $this->feedbackauthorformat, 4385 array('overflowdiv' => true, 'context' => $this->workshop->context)); 4386 4387 return $content; 4388 } 4389 4390 /** 4391 * Prepares the list of overall feedback attachments 4392 * 4393 * Returns false if overall feedback attachments are not allowed. Otherwise returns 4394 * list of attachments (may be empty). 4395 * 4396 * @return bool|array of stdClass 4397 */ 4398 public function get_overall_feedback_attachments() { 4399 4400 if ($this->workshop->overallfeedbackmode == 0) { 4401 return false; 4402 } 4403 4404 if ($this->workshop->overallfeedbackfiles == 0) { 4405 return false; 4406 } 4407 4408 if (empty($this->feedbackauthorattachment)) { 4409 return array(); 4410 } 4411 4412 $attachments = array(); 4413 $fs = get_file_storage(); 4414 $files = $fs->get_area_files($this->workshop->context->id, 'mod_workshop', 'overallfeedback_attachment', $this->id); 4415 foreach ($files as $file) { 4416 if ($file->is_directory()) { 4417 continue; 4418 } 4419 $filepath = $file->get_filepath(); 4420 $filename = $file->get_filename(); 4421 $fileurl = moodle_url::make_pluginfile_url($this->workshop->context->id, 'mod_workshop', 4422 'overallfeedback_attachment', $this->id, $filepath, $filename, true); 4423 $previewurl = new moodle_url(moodle_url::make_pluginfile_url($this->workshop->context->id, 'mod_workshop', 4424 'overallfeedback_attachment', $this->id, $filepath, $filename, false), array('preview' => 'bigthumb')); 4425 $attachments[] = (object)array( 4426 'filepath' => $filepath, 4427 'filename' => $filename, 4428 'fileurl' => $fileurl, 4429 'previewurl' => $previewurl, 4430 'mimetype' => $file->get_mimetype(), 4431 4432 ); 4433 } 4434 4435 return $attachments; 4436 } 4437 } 4438 4439 4440 /** 4441 * Represents a renderable training assessment of an example submission 4442 */ 4443 class workshop_example_assessment extends workshop_assessment implements renderable { 4444 4445 /** 4446 * @see parent::validate_raw_record() 4447 */ 4448 protected function validate_raw_record(stdClass $record) { 4449 if ($record->weight != 0) { 4450 throw new coding_exception('Invalid weight of example submission assessment'); 4451 } 4452 parent::validate_raw_record($record); 4453 } 4454 } 4455 4456 4457 /** 4458 * Represents a renderable reference assessment of an example submission 4459 */ 4460 class workshop_example_reference_assessment extends workshop_assessment implements renderable { 4461 4462 /** 4463 * @see parent::validate_raw_record() 4464 */ 4465 protected function validate_raw_record(stdClass $record) { 4466 if ($record->weight != 1) { 4467 throw new coding_exception('Invalid weight of the reference example submission assessment'); 4468 } 4469 parent::validate_raw_record($record); 4470 } 4471 } 4472 4473 4474 /** 4475 * Renderable message to be displayed to the user 4476 * 4477 * Message can contain an optional action link with a label that is supposed to be rendered 4478 * as a button or a link. 4479 * 4480 * @see workshop::renderer::render_workshop_message() 4481 */ 4482 class workshop_message implements renderable { 4483 4484 const TYPE_INFO = 10; 4485 const TYPE_OK = 20; 4486 const TYPE_ERROR = 30; 4487 4488 /** @var string */ 4489 protected $text = ''; 4490 /** @var int */ 4491 protected $type = self::TYPE_INFO; 4492 /** @var moodle_url */ 4493 protected $actionurl = null; 4494 /** @var string */ 4495 protected $actionlabel = ''; 4496 4497 /** 4498 * @param string $text short text to be displayed 4499 * @param string $type optional message type info|ok|error 4500 */ 4501 public function __construct($text = null, $type = self::TYPE_INFO) { 4502 $this->set_text($text); 4503 $this->set_type($type); 4504 } 4505 4506 /** 4507 * Sets the message text 4508 * 4509 * @param string $text short text to be displayed 4510 */ 4511 public function set_text($text) { 4512 $this->text = $text; 4513 } 4514 4515 /** 4516 * Sets the message type 4517 * 4518 * @param int $type 4519 */ 4520 public function set_type($type = self::TYPE_INFO) { 4521 if (in_array($type, array(self::TYPE_OK, self::TYPE_ERROR, self::TYPE_INFO))) { 4522 $this->type = $type; 4523 } else { 4524 throw new coding_exception('Unknown message type.'); 4525 } 4526 } 4527 4528 /** 4529 * Sets the optional message action 4530 * 4531 * @param moodle_url $url to follow on action 4532 * @param string $label action label 4533 */ 4534 public function set_action(moodle_url $url, $label) { 4535 $this->actionurl = $url; 4536 $this->actionlabel = $label; 4537 } 4538 4539 /** 4540 * Returns message text with HTML tags quoted 4541 * 4542 * @return string 4543 */ 4544 public function get_message() { 4545 return s($this->text); 4546 } 4547 4548 /** 4549 * Returns message type 4550 * 4551 * @return int 4552 */ 4553 public function get_type() { 4554 return $this->type; 4555 } 4556 4557 /** 4558 * Returns action URL 4559 * 4560 * @return moodle_url|null 4561 */ 4562 public function get_action_url() { 4563 return $this->actionurl; 4564 } 4565 4566 /** 4567 * Returns action label 4568 * 4569 * @return string 4570 */ 4571 public function get_action_label() { 4572 return $this->actionlabel; 4573 } 4574 } 4575 4576 4577 /** 4578 * Renderable component containing all the data needed to display the grading report 4579 */ 4580 class workshop_grading_report implements renderable { 4581 4582 /** @var stdClass returned by {@see workshop::prepare_grading_report_data()} */ 4583 protected $data; 4584 /** @var stdClass rendering options */ 4585 protected $options; 4586 4587 /** 4588 * Grades in $data must be already rounded to the set number of decimals or must be null 4589 * (in which later case, the [mod_workshop,nullgrade] string shall be displayed) 4590 * 4591 * @param stdClass $data prepared by {@link workshop::prepare_grading_report_data()} 4592 * @param stdClass $options display options (showauthornames, showreviewernames, sortby, sorthow, showsubmissiongrade, showgradinggrade) 4593 */ 4594 public function __construct(stdClass $data, stdClass $options) { 4595 $this->data = $data; 4596 $this->options = $options; 4597 } 4598 4599 /** 4600 * @return stdClass grading report data 4601 */ 4602 public function get_data() { 4603 return $this->data; 4604 } 4605 4606 /** 4607 * @return stdClass rendering options 4608 */ 4609 public function get_options() { 4610 return $this->options; 4611 } 4612 4613 /** 4614 * Prepare the data to be exported to a external system via Web Services. 4615 * 4616 * This function applies extra capabilities checks. 4617 * @return stdClass the data ready for external systems 4618 */ 4619 public function export_data_for_external() { 4620 $data = $this->get_data(); 4621 $options = $this->get_options(); 4622 4623 foreach ($data->grades as $reportdata) { 4624 // If we are in submission phase ignore the following data. 4625 if ($options->workshopphase == workshop::PHASE_SUBMISSION) { 4626 unset($reportdata->submissiongrade); 4627 unset($reportdata->gradinggrade); 4628 unset($reportdata->submissiongradeover); 4629 unset($reportdata->submissiongradeoverby); 4630 unset($reportdata->submissionpublished); 4631 unset($reportdata->reviewedby); 4632 unset($reportdata->reviewerof); 4633 continue; 4634 } 4635 4636 if (!$options->showsubmissiongrade) { 4637 unset($reportdata->submissiongrade); 4638 unset($reportdata->submissiongradeover); 4639 } 4640 4641 if (!$options->showgradinggrade and $tr == 0) { 4642 unset($reportdata->gradinggrade); 4643 } 4644 4645 if (!$options->showreviewernames) { 4646 foreach ($reportdata->reviewedby as $reviewedby) { 4647 $reviewedby->userid = 0; 4648 } 4649 } 4650 4651 if (!$options->showauthornames) { 4652 foreach ($reportdata->reviewerof as $reviewerof) { 4653 $reviewerof->userid = 0; 4654 } 4655 } 4656 } 4657 4658 return $data; 4659 } 4660 } 4661 4662 4663 /** 4664 * Base class for renderable feedback for author and feedback for reviewer 4665 */ 4666 abstract class workshop_feedback { 4667 4668 /** @var stdClass the user info */ 4669 protected $provider = null; 4670 4671 /** @var string the feedback text */ 4672 protected $content = null; 4673 4674 /** @var int format of the feedback text */ 4675 protected $format = null; 4676 4677 /** 4678 * @return stdClass the user info 4679 */ 4680 public function get_provider() { 4681 4682 if (is_null($this->provider)) { 4683 throw new coding_exception('Feedback provider not set'); 4684 } 4685 4686 return $this->provider; 4687 } 4688 4689 /** 4690 * @return string the feedback text 4691 */ 4692 public function get_content() { 4693 4694 if (is_null($this->content)) { 4695 throw new coding_exception('Feedback content not set'); 4696 } 4697 4698 return $this->content; 4699 } 4700 4701 /** 4702 * @return int format of the feedback text 4703 */ 4704 public function get_format() { 4705 4706 if (is_null($this->format)) { 4707 throw new coding_exception('Feedback text format not set'); 4708 } 4709 4710 return $this->format; 4711 } 4712 } 4713 4714 4715 /** 4716 * Renderable feedback for the author of submission 4717 */ 4718 class workshop_feedback_author extends workshop_feedback implements renderable { 4719 4720 /** 4721 * Extracts feedback from the given submission record 4722 * 4723 * @param stdClass $submission record as returned by {@see self::get_submission_by_id()} 4724 */ 4725 public function __construct(stdClass $submission) { 4726 4727 $this->provider = user_picture::unalias($submission, null, 'gradeoverbyx', 'gradeoverby'); 4728 $this->content = $submission->feedbackauthor; 4729 $this->format = $submission->feedbackauthorformat; 4730 } 4731 } 4732 4733 4734 /** 4735 * Renderable feedback for the reviewer 4736 */ 4737 class workshop_feedback_reviewer extends workshop_feedback implements renderable { 4738 4739 /** 4740 * Extracts feedback from the given assessment record 4741 * 4742 * @param stdClass $assessment record as returned by eg {@see self::get_assessment_by_id()} 4743 */ 4744 public function __construct(stdClass $assessment) { 4745 4746 $this->provider = user_picture::unalias($assessment, null, 'gradinggradeoverbyx', 'overby'); 4747 $this->content = $assessment->feedbackreviewer; 4748 $this->format = $assessment->feedbackreviewerformat; 4749 } 4750 } 4751 4752 4753 /** 4754 * Holds the final grades for the activity as are stored in the gradebook 4755 */ 4756 class workshop_final_grades implements renderable { 4757 4758 /** @var object the info from the gradebook about the grade for submission */ 4759 public $submissiongrade = null; 4760 4761 /** @var object the infor from the gradebook about the grade for assessment */ 4762 public $assessmentgrade = null; 4763 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body