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