Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402]
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->trigger(); 3031 3032 return $submission->id; 3033 } 3034 3035 /** 3036 * Helper method for validating if the current user can view the given assessment. 3037 * 3038 * @param stdClass $assessment assessment object 3039 * @param stdClass $submission submission object 3040 * @return void 3041 * @throws moodle_exception 3042 * @since Moodle 3.4 3043 */ 3044 public function check_view_assessment($assessment, $submission) { 3045 global $USER; 3046 3047 $isauthor = $submission->authorid == $USER->id; 3048 $isreviewer = $assessment->reviewerid == $USER->id; 3049 $canviewallassessments = has_capability('mod/workshop:viewallassessments', $this->context); 3050 $canviewallsubmissions = has_capability('mod/workshop:viewallsubmissions', $this->context); 3051 3052 $canviewallsubmissions = $canviewallsubmissions && $this->check_group_membership($submission->authorid); 3053 3054 if (!$isreviewer and !$isauthor and !($canviewallassessments and $canviewallsubmissions)) { 3055 throw new \moodle_exception('nopermissions', 'error', $this->view_url(), 'view this assessment'); 3056 } 3057 3058 if ($isauthor and !$isreviewer and !$canviewallassessments and $this->phase != self::PHASE_CLOSED) { 3059 // Authors can see assessments of their work at the end of workshop only. 3060 throw new \moodle_exception('nopermissions', 'error', $this->view_url(), 3061 'view assessment of own work before workshop is closed'); 3062 } 3063 } 3064 3065 /** 3066 * Helper method for validating if the current user can edit the given assessment. 3067 * 3068 * @param stdClass $assessment assessment object 3069 * @param stdClass $submission submission object 3070 * @return void 3071 * @throws moodle_exception 3072 * @since Moodle 3.4 3073 */ 3074 public function check_edit_assessment($assessment, $submission) { 3075 global $USER; 3076 3077 $this->check_view_assessment($assessment, $submission); 3078 // Further checks. 3079 $isreviewer = ($USER->id == $assessment->reviewerid); 3080 3081 $assessmenteditable = $isreviewer && $this->assessing_allowed($USER->id); 3082 if (!$assessmenteditable) { 3083 throw new moodle_exception('nopermissions', 'error', '', 'edit assessments'); 3084 } 3085 3086 list($assessed, $notice) = $this->check_examples_assessed_before_assessment($assessment->reviewerid); 3087 if (!$assessed) { 3088 throw new moodle_exception($notice, 'mod_workshop'); 3089 } 3090 } 3091 3092 /** 3093 * Adds information to an allocated assessment (function used the first time a review is done or when updating an existing one). 3094 * 3095 * @param stdClass $assessment the assessment 3096 * @param stdClass $submission the submission 3097 * @param stdClass $data the assessment data to be added or Updated 3098 * @param stdClass $strategy the strategy instance 3099 * @return float|null Raw percentual grade (0.00000 to 100.00000) for submission 3100 * @since Moodle 3.4 3101 */ 3102 public function edit_assessment($assessment, $submission, $data, $strategy) { 3103 global $DB; 3104 3105 $cansetassessmentweight = has_capability('mod/workshop:allocate', $this->context); 3106 3107 // Let the grading strategy subplugin save its data. 3108 $rawgrade = $strategy->save_assessment($assessment, $data); 3109 3110 // Store the data managed by the workshop core. 3111 $coredata = (object)array('id' => $assessment->id); 3112 if (isset($data->feedbackauthor_editor)) { 3113 $coredata->feedbackauthor_editor = $data->feedbackauthor_editor; 3114 $coredata = file_postupdate_standard_editor($coredata, 'feedbackauthor', $this->overall_feedback_content_options(), 3115 $this->context, 'mod_workshop', 'overallfeedback_content', $assessment->id); 3116 unset($coredata->feedbackauthor_editor); 3117 } 3118 if (isset($data->feedbackauthorattachment_filemanager)) { 3119 $coredata->feedbackauthorattachment_filemanager = $data->feedbackauthorattachment_filemanager; 3120 $coredata = file_postupdate_standard_filemanager($coredata, 'feedbackauthorattachment', 3121 $this->overall_feedback_attachment_options(), $this->context, 'mod_workshop', 'overallfeedback_attachment', 3122 $assessment->id); 3123 unset($coredata->feedbackauthorattachment_filemanager); 3124 if (empty($coredata->feedbackauthorattachment)) { 3125 $coredata->feedbackauthorattachment = 0; 3126 } 3127 } 3128 if (isset($data->weight) and $cansetassessmentweight) { 3129 $coredata->weight = $data->weight; 3130 } 3131 // Update the assessment data if there is something other than just the 'id'. 3132 if (count((array)$coredata) > 1 ) { 3133 $DB->update_record('workshop_assessments', $coredata); 3134 $params = array( 3135 'relateduserid' => $submission->authorid, 3136 'objectid' => $assessment->id, 3137 'context' => $this->context, 3138 'other' => array( 3139 'workshopid' => $this->id, 3140 'submissionid' => $assessment->submissionid 3141 ) 3142 ); 3143 3144 if (is_null($assessment->grade)) { 3145 // All workshop_assessments are created when allocations are made. The create event is of more use located here. 3146 $event = \mod_workshop\event\submission_assessed::create($params); 3147 $event->trigger(); 3148 } else { 3149 $params['other']['grade'] = $assessment->grade; 3150 $event = \mod_workshop\event\submission_reassessed::create($params); 3151 $event->trigger(); 3152 } 3153 } 3154 return $rawgrade; 3155 } 3156 3157 /** 3158 * Evaluates an assessment. 3159 * 3160 * @param stdClass $assessment the assessment 3161 * @param stdClass $data the assessment data to be updated 3162 * @param bool $cansetassessmentweight whether the user can change the assessment weight 3163 * @param bool $canoverridegrades whether the user can override the assessment grades 3164 * @return void 3165 * @since Moodle 3.4 3166 */ 3167 public function evaluate_assessment($assessment, $data, $cansetassessmentweight, $canoverridegrades) { 3168 global $DB, $USER; 3169 3170 $data = file_postupdate_standard_editor($data, 'feedbackreviewer', array(), $this->context); 3171 $record = new stdclass(); 3172 $record->id = $assessment->id; 3173 if ($cansetassessmentweight) { 3174 $record->weight = $data->weight; 3175 } 3176 if ($canoverridegrades) { 3177 $record->gradinggradeover = $this->raw_grade_value($data->gradinggradeover, $this->gradinggrade); 3178 $record->gradinggradeoverby = $USER->id; 3179 $record->feedbackreviewer = $data->feedbackreviewer; 3180 $record->feedbackreviewerformat = $data->feedbackreviewerformat; 3181 } 3182 $DB->update_record('workshop_assessments', $record); 3183 } 3184 3185 /** 3186 * Trigger submission viewed event. 3187 * 3188 * @param stdClass $submission submission object 3189 * @since Moodle 3.4 3190 */ 3191 public function set_submission_viewed($submission) { 3192 $params = array( 3193 'objectid' => $submission->id, 3194 'context' => $this->context, 3195 'courseid' => $this->course->id, 3196 'relateduserid' => $submission->authorid, 3197 'other' => array( 3198 'workshopid' => $this->id 3199 ) 3200 ); 3201 3202 $event = \mod_workshop\event\submission_viewed::create($params); 3203 $event->trigger(); 3204 } 3205 3206 /** 3207 * Evaluates a submission. 3208 * 3209 * @param stdClass $submission the submission 3210 * @param stdClass $data the submission data to be updated 3211 * @param bool $canpublish whether the user can publish the submission 3212 * @param bool $canoverride whether the user can override the submission grade 3213 * @return void 3214 * @since Moodle 3.4 3215 */ 3216 public function evaluate_submission($submission, $data, $canpublish, $canoverride) { 3217 global $DB, $USER; 3218 3219 $data = file_postupdate_standard_editor($data, 'feedbackauthor', array(), $this->context); 3220 $record = new stdclass(); 3221 $record->id = $submission->id; 3222 if ($canoverride) { 3223 $record->gradeover = $this->raw_grade_value($data->gradeover, $this->grade); 3224 $record->gradeoverby = $USER->id; 3225 $record->feedbackauthor = $data->feedbackauthor; 3226 $record->feedbackauthorformat = $data->feedbackauthorformat; 3227 } 3228 if ($canpublish) { 3229 $record->published = !empty($data->published); 3230 } 3231 $DB->update_record('workshop_submissions', $record); 3232 } 3233 3234 /** 3235 * Get the initial first name. 3236 * 3237 * @return string|null initial of first name we are currently filtering by. 3238 */ 3239 public function get_initial_first(): ?string { 3240 if (empty($this->initialbarprefs['i_first'])) { 3241 return null; 3242 } 3243 3244 return $this->initialbarprefs['i_first']; 3245 } 3246 3247 /** 3248 * Get the initial last name. 3249 * 3250 * @return string|null initial of last name we are currently filtering by. 3251 */ 3252 public function get_initial_last(): ?string { 3253 if (empty($this->initialbarprefs['i_last'])) { 3254 return null; 3255 } 3256 3257 return $this->initialbarprefs['i_last']; 3258 } 3259 3260 /** 3261 * Init method for initial bars. 3262 * @return void 3263 */ 3264 public function init_initial_bar(): void { 3265 global $SESSION; 3266 if ($this->phase === self::PHASE_SETUP) { 3267 return; 3268 } 3269 3270 $ifirst = optional_param('ifirst', null, PARAM_NOTAGS); 3271 $ilast = optional_param('ilast', null, PARAM_NOTAGS); 3272 3273 if (empty($SESSION->mod_workshop->initialbarprefs['id-'.$this->context->id])) { 3274 $SESSION->mod_workshop = new stdClass(); 3275 $SESSION->mod_workshop->initialbarprefs['id-'.$this->context->id] = []; 3276 } 3277 if (!empty($SESSION->mod_workshop->initialbarprefs['id-'.$this->context->id]['i_first'])) { 3278 $this->initialbarprefs['i_first'] = $SESSION->mod_workshop->initialbarprefs['id-'.$this->context->id]['i_first']; 3279 } 3280 if (!empty($SESSION->mod_workshop->initialbarprefs['id-'.$this->context->id]['i_last'])) { 3281 $this->initialbarprefs['i_last'] = $SESSION->mod_workshop->initialbarprefs['id-'.$this->context->id]['i_last']; 3282 } 3283 if (!is_null($ifirst)) { 3284 $this->initialbarprefs['i_first'] = $ifirst; 3285 $SESSION->mod_workshop->initialbarprefs['id-'.$this->context->id]['i_first'] = $ifirst; 3286 } 3287 3288 if (!is_null($ilast)) { 3289 $this->initialbarprefs['i_last'] = $ilast; 3290 $SESSION->mod_workshop->initialbarprefs['id-'.$this->context->id]['i_last'] = $ilast; 3291 } 3292 } 3293 3294 //////////////////////////////////////////////////////////////////////////////// 3295 // Internal methods (implementation details) // 3296 //////////////////////////////////////////////////////////////////////////////// 3297 3298 /** 3299 * Given an array of all assessments of a single submission, calculates the final grade for this submission 3300 * 3301 * This calculates the weighted mean of the passed assessment grades. If, however, the submission grade 3302 * was overridden by a teacher, the gradeover value is returned and the rest of grades are ignored. 3303 * 3304 * @param array $assessments of stdclass(->submissionid ->submissiongrade ->gradeover ->weight ->grade) 3305 * @return void 3306 */ 3307 protected function aggregate_submission_grades_process(array $assessments) { 3308 global $DB; 3309 3310 $submissionid = null; // the id of the submission being processed 3311 $current = null; // the grade currently saved in database 3312 $finalgrade = null; // the new grade to be calculated 3313 $sumgrades = 0; 3314 $sumweights = 0; 3315 3316 foreach ($assessments as $assessment) { 3317 if (is_null($submissionid)) { 3318 // the id is the same in all records, fetch it during the first loop cycle 3319 $submissionid = $assessment->submissionid; 3320 } 3321 if (is_null($current)) { 3322 // the currently saved grade is the same in all records, fetch it during the first loop cycle 3323 $current = $assessment->submissiongrade; 3324 } 3325 if (is_null($assessment->grade)) { 3326 // this was not assessed yet 3327 continue; 3328 } 3329 if ($assessment->weight == 0) { 3330 // this does not influence the calculation 3331 continue; 3332 } 3333 $sumgrades += $assessment->grade * $assessment->weight; 3334 $sumweights += $assessment->weight; 3335 } 3336 if ($sumweights > 0 and is_null($finalgrade)) { 3337 $finalgrade = grade_floatval($sumgrades / $sumweights); 3338 } 3339 // check if the new final grade differs from the one stored in the database 3340 if (grade_floats_different($finalgrade, $current)) { 3341 // we need to save new calculation into the database 3342 $record = new stdclass(); 3343 $record->id = $submissionid; 3344 $record->grade = $finalgrade; 3345 $record->timegraded = time(); 3346 $DB->update_record('workshop_submissions', $record); 3347 } 3348 } 3349 3350 /** 3351 * Given an array of all assessments done by a single reviewer, calculates the final grading grade 3352 * 3353 * This calculates the simple mean of the passed grading grades. If, however, the grading grade 3354 * was overridden by a teacher, the gradinggradeover value is returned and the rest of grades are ignored. 3355 * 3356 * @param array $assessments of stdclass(->reviewerid ->gradinggrade ->gradinggradeover ->aggregationid ->aggregatedgrade) 3357 * @param null|int $timegraded explicit timestamp of the aggregation, defaults to the current time 3358 * @return void 3359 */ 3360 protected function aggregate_grading_grades_process(array $assessments, $timegraded = null) { 3361 global $DB; 3362 3363 $reviewerid = null; // the id of the reviewer being processed 3364 $current = null; // the gradinggrade currently saved in database 3365 $finalgrade = null; // the new grade to be calculated 3366 $agid = null; // aggregation id 3367 $sumgrades = 0; 3368 $count = 0; 3369 3370 if (is_null($timegraded)) { 3371 $timegraded = time(); 3372 } 3373 3374 foreach ($assessments as $assessment) { 3375 if (is_null($reviewerid)) { 3376 // the id is the same in all records, fetch it during the first loop cycle 3377 $reviewerid = $assessment->reviewerid; 3378 } 3379 if (is_null($agid)) { 3380 // the id is the same in all records, fetch it during the first loop cycle 3381 $agid = $assessment->aggregationid; 3382 } 3383 if (is_null($current)) { 3384 // the currently saved grade is the same in all records, fetch it during the first loop cycle 3385 $current = $assessment->aggregatedgrade; 3386 } 3387 if (!is_null($assessment->gradinggradeover)) { 3388 // the grading grade for this assessment is overridden by a teacher 3389 $sumgrades += $assessment->gradinggradeover; 3390 $count++; 3391 } else { 3392 if (!is_null($assessment->gradinggrade)) { 3393 $sumgrades += $assessment->gradinggrade; 3394 $count++; 3395 } 3396 } 3397 } 3398 if ($count > 0) { 3399 $finalgrade = grade_floatval($sumgrades / $count); 3400 } 3401 3402 // Event information. 3403 $params = array( 3404 'context' => $this->context, 3405 'courseid' => $this->course->id, 3406 'relateduserid' => $reviewerid 3407 ); 3408 3409 // check if the new final grade differs from the one stored in the database 3410 if (grade_floats_different($finalgrade, $current)) { 3411 $params['other'] = array( 3412 'currentgrade' => $current, 3413 'finalgrade' => $finalgrade 3414 ); 3415 3416 // we need to save new calculation into the database 3417 if (is_null($agid)) { 3418 // no aggregation record yet 3419 $record = new stdclass(); 3420 $record->workshopid = $this->id; 3421 $record->userid = $reviewerid; 3422 $record->gradinggrade = $finalgrade; 3423 $record->timegraded = $timegraded; 3424 $record->id = $DB->insert_record('workshop_aggregations', $record); 3425 $params['objectid'] = $record->id; 3426 $event = \mod_workshop\event\assessment_evaluated::create($params); 3427 $event->trigger(); 3428 } else { 3429 $record = new stdclass(); 3430 $record->id = $agid; 3431 $record->gradinggrade = $finalgrade; 3432 $record->timegraded = $timegraded; 3433 $DB->update_record('workshop_aggregations', $record); 3434 $params['objectid'] = $agid; 3435 $event = \mod_workshop\event\assessment_reevaluated::create($params); 3436 $event->trigger(); 3437 } 3438 } 3439 } 3440 3441 /** 3442 * Returns SQL to fetch all enrolled users with the given capability in the current workshop 3443 * 3444 * The returned array consists of string $sql and the $params array. Note that the $sql can be 3445 * empty if a grouping is selected and it has no groups. 3446 * 3447 * The list is automatically restricted according to any availability restrictions 3448 * that apply to user lists (e.g. group, grouping restrictions). 3449 * 3450 * @param string $capability the name of the capability 3451 * @param bool $musthavesubmission ff true, return only users who have already submitted 3452 * @param int $groupid 0 means ignore groups, any other value limits the result by group id 3453 * @return array of (string)sql, (array)params 3454 */ 3455 protected function get_users_with_capability_sql($capability, $musthavesubmission, $groupid) { 3456 global $CFG; 3457 /** @var int static counter used to generate unique parameter holders */ 3458 static $inc = 0; 3459 $inc++; 3460 3461 // If the caller requests all groups and we are using a selected grouping, 3462 // recursively call this function for each group in the grouping (this is 3463 // needed because get_enrolled_sql only supports a single group). 3464 if (empty($groupid) and $this->cm->groupingid) { 3465 $groupingid = $this->cm->groupingid; 3466 $groupinggroupids = array_keys(groups_get_all_groups($this->cm->course, 0, $this->cm->groupingid, 'g.id')); 3467 $sql = array(); 3468 $params = array(); 3469 foreach ($groupinggroupids as $groupinggroupid) { 3470 if ($groupinggroupid > 0) { // just in case in order not to fall into the endless loop 3471 list($gsql, $gparams) = $this->get_users_with_capability_sql($capability, $musthavesubmission, $groupinggroupid); 3472 $sql[] = $gsql; 3473 $params = array_merge($params, $gparams); 3474 } 3475 } 3476 $sql = implode(PHP_EOL." UNION ".PHP_EOL, $sql); 3477 return array($sql, $params); 3478 } 3479 3480 list($esql, $params) = get_enrolled_sql($this->context, $capability, $groupid, true); 3481 3482 $userfieldsapi = \core_user\fields::for_userpic(); 3483 $userfields = $userfieldsapi->get_sql('u', false, '', '', false)->selects; 3484 3485 $sql = "SELECT $userfields 3486 FROM {user} u 3487 JOIN ($esql) je ON (je.id = u.id AND u.deleted = 0) "; 3488 3489 if ($musthavesubmission) { 3490 $sql .= " JOIN {workshop_submissions} ws ON (ws.authorid = u.id AND ws.example = 0 AND ws.workshopid = :workshopid{$inc}) "; 3491 $params['workshopid'.$inc] = $this->id; 3492 } 3493 3494 // If the activity is restricted so that only certain users should appear 3495 // in user lists, integrate this into the same SQL. 3496 $info = new \core_availability\info_module($this->cm); 3497 list ($listsql, $listparams) = $info->get_user_list_sql(false); 3498 if ($listsql) { 3499 $sql .= " JOIN ($listsql) restricted ON restricted.id = u.id "; 3500 $params = array_merge($params, $listparams); 3501 } 3502 3503 return array($sql, $params); 3504 } 3505 3506 /** 3507 * Returns SQL to fetch all enrolled users with the first name or last name. 3508 * 3509 * @return array 3510 */ 3511 protected function get_users_with_initial_filtering_sql_where(): array { 3512 global $DB; 3513 $conditions = []; 3514 $params = []; 3515 $ifirst = $this->get_initial_first(); 3516 $ilast = $this->get_initial_last(); 3517 if ($ifirst) { 3518 $conditions[] = $DB->sql_like('LOWER(tmp.firstname)', ':i_first' , false, false); 3519 $params['i_first'] = $DB->sql_like_escape($ifirst) . '%'; 3520 } 3521 if ($ilast) { 3522 $conditions[] = $DB->sql_like('LOWER(tmp.lastname)', ':i_last' , false, false); 3523 $params['i_last'] = $DB->sql_like_escape($ilast) . '%'; 3524 } 3525 return [implode(" AND ", $conditions), $params]; 3526 } 3527 3528 /** 3529 * Returns SQL statement that can be used to fetch all actively enrolled participants in the workshop 3530 * 3531 * @param bool $musthavesubmission if true, return only users who have already submitted 3532 * @param int $groupid 0 means ignore groups, any other value limits the result by group id 3533 * @return array of (string)sql, (array)params 3534 */ 3535 protected function get_participants_sql($musthavesubmission=false, $groupid=0) { 3536 3537 list($sql1, $params1) = $this->get_users_with_capability_sql('mod/workshop:submit', $musthavesubmission, $groupid); 3538 list($sql2, $params2) = $this->get_users_with_capability_sql('mod/workshop:peerassess', $musthavesubmission, $groupid); 3539 3540 if (empty($sql1) or empty($sql2)) { 3541 if (empty($sql1) and empty($sql2)) { 3542 return array('', array()); 3543 } else if (empty($sql1)) { 3544 $sql = $sql2; 3545 $params = $params2; 3546 } else { 3547 $sql = $sql1; 3548 $params = $params1; 3549 } 3550 } else { 3551 $sql = $sql1.PHP_EOL." UNION ".PHP_EOL.$sql2; 3552 $params = array_merge($params1, $params2); 3553 } 3554 3555 return array($sql, $params); 3556 } 3557 3558 /** 3559 * @return array of available workshop phases 3560 */ 3561 protected function available_phases_list() { 3562 return array( 3563 self::PHASE_SETUP => true, 3564 self::PHASE_SUBMISSION => true, 3565 self::PHASE_ASSESSMENT => true, 3566 self::PHASE_EVALUATION => true, 3567 self::PHASE_CLOSED => true, 3568 ); 3569 } 3570 3571 /** 3572 * Converts absolute URL to relative URL needed by {@see add_to_log()} 3573 * 3574 * @param moodle_url $url absolute URL 3575 * @return string 3576 */ 3577 protected function log_convert_url(moodle_url $fullurl) { 3578 static $baseurl; 3579 3580 if (!isset($baseurl)) { 3581 $baseurl = new moodle_url('/mod/workshop/'); 3582 $baseurl = $baseurl->out(); 3583 } 3584 3585 return substr($fullurl->out(), strlen($baseurl)); 3586 } 3587 3588 /** 3589 * Removes all user data related to assessments (including allocations). 3590 * 3591 * This includes assessments of example submissions as long as they are not 3592 * referential assessments. 3593 * 3594 * @param stdClass $data The actual course reset settings. 3595 * @return bool|string True on success, error message otherwise. 3596 */ 3597 protected function reset_userdata_assessments(stdClass $data) { 3598 global $DB; 3599 3600 $sql = "SELECT a.id 3601 FROM {workshop_assessments} a 3602 JOIN {workshop_submissions} s ON (a.submissionid = s.id) 3603 WHERE s.workshopid = :workshopid 3604 AND (s.example = 0 OR (s.example = 1 AND a.weight = 0))"; 3605 3606 $assessments = $DB->get_records_sql($sql, array('workshopid' => $this->id)); 3607 $this->delete_assessment(array_keys($assessments)); 3608 3609 $DB->delete_records('workshop_aggregations', array('workshopid' => $this->id)); 3610 3611 return true; 3612 } 3613 3614 /** 3615 * Removes all user data related to participants' submissions. 3616 * 3617 * @param stdClass $data The actual course reset settings. 3618 * @return bool|string True on success, error message otherwise. 3619 */ 3620 protected function reset_userdata_submissions(stdClass $data) { 3621 global $DB; 3622 3623 $submissions = $this->get_submissions(); 3624 foreach ($submissions as $submission) { 3625 $this->delete_submission($submission); 3626 } 3627 3628 return true; 3629 } 3630 3631 /** 3632 * Hard set the workshop phase to the setup one. 3633 */ 3634 protected function reset_phase() { 3635 global $DB; 3636 3637 $DB->set_field('workshop', 'phase', self::PHASE_SETUP, array('id' => $this->id)); 3638 $this->phase = self::PHASE_SETUP; 3639 } 3640 } 3641 3642 //////////////////////////////////////////////////////////////////////////////// 3643 // Renderable components 3644 //////////////////////////////////////////////////////////////////////////////// 3645 3646 /** 3647 * Represents the user planner tool 3648 * 3649 * Planner contains list of phases. Each phase contains list of tasks. Task is a simple object with 3650 * title, link and completed (true/false/null logic). 3651 */ 3652 class workshop_user_plan implements renderable { 3653 3654 /** @var int id of the user this plan is for */ 3655 public $userid; 3656 /** @var workshop */ 3657 public $workshop; 3658 /** @var array of (stdclass)tasks */ 3659 public $phases = array(); 3660 /** @var null|array of example submissions to be assessed by the planner owner */ 3661 protected $examples = null; 3662 3663 /** 3664 * Prepare an individual workshop plan for the given user. 3665 * 3666 * @param workshop $workshop instance 3667 * @param int $userid whom the plan is prepared for 3668 */ 3669 public function __construct(workshop $workshop, $userid) { 3670 global $DB; 3671 3672 $this->workshop = $workshop; 3673 $this->userid = $userid; 3674 3675 //--------------------------------------------------------- 3676 // * SETUP | submission | assessment | evaluation | closed 3677 //--------------------------------------------------------- 3678 $phase = new stdclass(); 3679 $phase->title = get_string('phasesetup', 'workshop'); 3680 $phase->tasks = array(); 3681 if (has_capability('moodle/course:manageactivities', $workshop->context, $userid)) { 3682 $task = new stdclass(); 3683 $task->title = get_string('taskintro', 'workshop'); 3684 $task->link = $workshop->updatemod_url(); 3685 $task->completed = !(trim($workshop->intro) == ''); 3686 $phase->tasks['intro'] = $task; 3687 } 3688 if (has_capability('moodle/course:manageactivities', $workshop->context, $userid)) { 3689 $task = new stdclass(); 3690 $task->title = get_string('taskinstructauthors', 'workshop'); 3691 $task->link = $workshop->updatemod_url(); 3692 $task->completed = !(trim($workshop->instructauthors) == ''); 3693 $phase->tasks['instructauthors'] = $task; 3694 } 3695 if (has_capability('mod/workshop:editdimensions', $workshop->context, $userid)) { 3696 $task = new stdclass(); 3697 $task->title = get_string('editassessmentform', 'workshop'); 3698 $task->link = $workshop->editform_url(); 3699 if ($workshop->grading_strategy_instance()->form_ready()) { 3700 $task->completed = true; 3701 } elseif ($workshop->phase > workshop::PHASE_SETUP) { 3702 $task->completed = false; 3703 } 3704 $phase->tasks['editform'] = $task; 3705 } 3706 if ($workshop->useexamples and has_capability('mod/workshop:manageexamples', $workshop->context, $userid)) { 3707 $task = new stdclass(); 3708 $task->title = get_string('prepareexamples', 'workshop'); 3709 if ($DB->count_records('workshop_submissions', array('example' => 1, 'workshopid' => $workshop->id)) > 0) { 3710 $task->completed = true; 3711 } elseif ($workshop->phase > workshop::PHASE_SETUP) { 3712 $task->completed = false; 3713 } 3714 $phase->tasks['prepareexamples'] = $task; 3715 } 3716 if (empty($phase->tasks) and $workshop->phase == workshop::PHASE_SETUP) { 3717 // if we are in the setup phase and there is no task (typical for students), let us 3718 // display some explanation what is going on 3719 $task = new stdclass(); 3720 $task->title = get_string('undersetup', 'workshop'); 3721 $task->completed = 'info'; 3722 $phase->tasks['setupinfo'] = $task; 3723 } 3724 $this->phases[workshop::PHASE_SETUP] = $phase; 3725 3726 //--------------------------------------------------------- 3727 // setup | * SUBMISSION | assessment | evaluation | closed 3728 //--------------------------------------------------------- 3729 $phase = new stdclass(); 3730 $phase->title = get_string('phasesubmission', 'workshop'); 3731 $phase->tasks = array(); 3732 if (has_capability('moodle/course:manageactivities', $workshop->context, $userid)) { 3733 $task = new stdclass(); 3734 $task->title = get_string('taskinstructreviewers', 'workshop'); 3735 $task->link = $workshop->updatemod_url(); 3736 if (trim($workshop->instructreviewers)) { 3737 $task->completed = true; 3738 } elseif ($workshop->phase >= workshop::PHASE_ASSESSMENT) { 3739 $task->completed = false; 3740 } 3741 $phase->tasks['instructreviewers'] = $task; 3742 } 3743 if ($workshop->useexamples and $workshop->examplesmode == workshop::EXAMPLES_BEFORE_SUBMISSION 3744 and has_capability('mod/workshop:submit', $workshop->context, $userid, false) 3745 and !has_capability('mod/workshop:manageexamples', $workshop->context, $userid)) { 3746 $task = new stdclass(); 3747 $task->title = get_string('exampleassesstask', 'workshop'); 3748 $examples = $this->get_examples(); 3749 $a = new stdclass(); 3750 $a->expected = count($examples); 3751 $a->assessed = 0; 3752 foreach ($examples as $exampleid => $example) { 3753 if (!is_null($example->grade)) { 3754 $a->assessed++; 3755 } 3756 } 3757 $task->details = get_string('exampleassesstaskdetails', 'workshop', $a); 3758 if ($a->assessed == $a->expected) { 3759 $task->completed = true; 3760 } elseif ($workshop->phase >= workshop::PHASE_ASSESSMENT) { 3761 $task->completed = false; 3762 } 3763 $phase->tasks['examples'] = $task; 3764 } 3765 if (has_capability('mod/workshop:submit', $workshop->context, $userid, false)) { 3766 $task = new stdclass(); 3767 $task->title = get_string('tasksubmit', 'workshop'); 3768 $task->link = $workshop->submission_url(); 3769 if ($DB->record_exists('workshop_submissions', array('workshopid'=>$workshop->id, 'example'=>0, 'authorid'=>$userid))) { 3770 $task->completed = true; 3771 } elseif ($workshop->phase >= workshop::PHASE_ASSESSMENT) { 3772 $task->completed = false; 3773 } else { 3774 $task->completed = null; // still has a chance to submit 3775 } 3776 $phase->tasks['submit'] = $task; 3777 } 3778 if (has_capability('mod/workshop:allocate', $workshop->context, $userid)) { 3779 if ($workshop->phaseswitchassessment) { 3780 $task = new stdClass(); 3781 $allocator = $DB->get_record('workshopallocation_scheduled', array('workshopid' => $workshop->id)); 3782 if (empty($allocator)) { 3783 $task->completed = false; 3784 } else if ($allocator->enabled and is_null($allocator->resultstatus)) { 3785 $task->completed = true; 3786 } else if ($workshop->submissionend > time()) { 3787 $task->completed = null; 3788 } else { 3789 $task->completed = false; 3790 } 3791 $task->title = get_string('setup', 'workshopallocation_scheduled'); 3792 $task->link = $workshop->allocation_url('scheduled'); 3793 $phase->tasks['allocatescheduled'] = $task; 3794 } 3795 $task = new stdclass(); 3796 $task->title = get_string('allocate', 'workshop'); 3797 $task->link = $workshop->allocation_url(); 3798 $numofauthors = $workshop->count_potential_authors(false); 3799 $numofsubmissions = $DB->count_records('workshop_submissions', array('workshopid'=>$workshop->id, 'example'=>0)); 3800 $sql = 'SELECT COUNT(s.id) AS nonallocated 3801 FROM {workshop_submissions} s 3802 LEFT JOIN {workshop_assessments} a ON (a.submissionid=s.id) 3803 WHERE s.workshopid = :workshopid AND s.example=0 AND a.submissionid IS NULL'; 3804 $params['workshopid'] = $workshop->id; 3805 $numnonallocated = $DB->count_records_sql($sql, $params); 3806 if ($numofsubmissions == 0) { 3807 $task->completed = null; 3808 } elseif ($numnonallocated == 0) { 3809 $task->completed = true; 3810 } elseif ($workshop->phase > workshop::PHASE_SUBMISSION) { 3811 $task->completed = false; 3812 } else { 3813 $task->completed = null; // still has a chance to allocate 3814 } 3815 $a = new stdclass(); 3816 $a->expected = $numofauthors; 3817 $a->submitted = $numofsubmissions; 3818 $a->allocate = $numnonallocated; 3819 $task->details = get_string('allocatedetails', 'workshop', $a); 3820 unset($a); 3821 $phase->tasks['allocate'] = $task; 3822 3823 if ($numofsubmissions < $numofauthors and $workshop->phase >= workshop::PHASE_SUBMISSION) { 3824 $task = new stdclass(); 3825 $task->title = get_string('someuserswosubmission', 'workshop'); 3826 $task->completed = 'info'; 3827 $phase->tasks['allocateinfo'] = $task; 3828 } 3829 3830 } 3831 if ($workshop->submissionstart) { 3832 $task = new stdclass(); 3833 $task->title = get_string('submissionstartdatetime', 'workshop', workshop::timestamp_formats($workshop->submissionstart)); 3834 $task->completed = 'info'; 3835 $phase->tasks['submissionstartdatetime'] = $task; 3836 } 3837 if ($workshop->submissionend) { 3838 $task = new stdclass(); 3839 $task->title = get_string('submissionenddatetime', 'workshop', workshop::timestamp_formats($workshop->submissionend)); 3840 $task->completed = 'info'; 3841 $phase->tasks['submissionenddatetime'] = $task; 3842 } 3843 if (($workshop->submissionstart < time()) and $workshop->latesubmissions) { 3844 // If submission deadline has passed and late submissions are allowed, only display 'latesubmissionsallowed' text to 3845 // users (students) who have not submitted and users(teachers, admins) who can switch pahase.. 3846 if (has_capability('mod/workshop:switchphase', $workshop->context, $userid) || 3847 (!$workshop->get_submission_by_author($userid) && $workshop->submissionend < time())) { 3848 $task = new stdclass(); 3849 $task->title = get_string('latesubmissionsallowed', 'workshop'); 3850 $task->completed = 'info'; 3851 $phase->tasks['latesubmissionsallowed'] = $task; 3852 } 3853 } 3854 if (isset($phase->tasks['submissionstartdatetime']) or isset($phase->tasks['submissionenddatetime'])) { 3855 if (has_capability('mod/workshop:ignoredeadlines', $workshop->context, $userid)) { 3856 $task = new stdclass(); 3857 $task->title = get_string('deadlinesignored', 'workshop'); 3858 $task->completed = 'info'; 3859 $phase->tasks['deadlinesignored'] = $task; 3860 } 3861 } 3862 $this->phases[workshop::PHASE_SUBMISSION] = $phase; 3863 3864 //--------------------------------------------------------- 3865 // setup | submission | * ASSESSMENT | evaluation | closed 3866 //--------------------------------------------------------- 3867 $phase = new stdclass(); 3868 $phase->title = get_string('phaseassessment', 'workshop'); 3869 $phase->tasks = array(); 3870 $phase->isreviewer = has_capability('mod/workshop:peerassess', $workshop->context, $userid); 3871 if ($workshop->phase == workshop::PHASE_SUBMISSION and $workshop->phaseswitchassessment 3872 and has_capability('mod/workshop:switchphase', $workshop->context, $userid)) { 3873 $task = new stdClass(); 3874 $task->title = get_string('switchphase30auto', 'mod_workshop', workshop::timestamp_formats($workshop->submissionend)); 3875 $task->completed = 'info'; 3876 $phase->tasks['autoswitchinfo'] = $task; 3877 } 3878 if ($workshop->useexamples and $workshop->examplesmode == workshop::EXAMPLES_BEFORE_ASSESSMENT 3879 and $phase->isreviewer and !has_capability('mod/workshop:manageexamples', $workshop->context, $userid)) { 3880 $task = new stdclass(); 3881 $task->title = get_string('exampleassesstask', 'workshop'); 3882 $examples = $workshop->get_examples_for_reviewer($userid); 3883 $a = new stdclass(); 3884 $a->expected = count($examples); 3885 $a->assessed = 0; 3886 foreach ($examples as $exampleid => $example) { 3887 if (!is_null($example->grade)) { 3888 $a->assessed++; 3889 } 3890 } 3891 $task->details = get_string('exampleassesstaskdetails', 'workshop', $a); 3892 if ($a->assessed == $a->expected) { 3893 $task->completed = true; 3894 } elseif ($workshop->phase > workshop::PHASE_ASSESSMENT) { 3895 $task->completed = false; 3896 } 3897 $phase->tasks['examples'] = $task; 3898 } 3899 if (empty($phase->tasks['examples']) or !empty($phase->tasks['examples']->completed)) { 3900 $phase->assessments = $workshop->get_assessments_by_reviewer($userid); 3901 $numofpeers = 0; // number of allocated peer-assessments 3902 $numofpeerstodo = 0; // number of peer-assessments to do 3903 $numofself = 0; // number of allocated self-assessments - should be 0 or 1 3904 $numofselftodo = 0; // number of self-assessments to do - should be 0 or 1 3905 foreach ($phase->assessments as $a) { 3906 if ($a->authorid == $userid) { 3907 $numofself++; 3908 if (is_null($a->grade)) { 3909 $numofselftodo++; 3910 } 3911 } else { 3912 $numofpeers++; 3913 if (is_null($a->grade)) { 3914 $numofpeerstodo++; 3915 } 3916 } 3917 } 3918 unset($a); 3919 if ($numofpeers) { 3920 $task = new stdclass(); 3921 if ($numofpeerstodo == 0) { 3922 $task->completed = true; 3923 } elseif ($workshop->phase > workshop::PHASE_ASSESSMENT) { 3924 $task->completed = false; 3925 } 3926 $a = new stdclass(); 3927 $a->total = $numofpeers; 3928 $a->todo = $numofpeerstodo; 3929 $task->title = get_string('taskassesspeers', 'workshop'); 3930 $task->details = get_string('taskassesspeersdetails', 'workshop', $a); 3931 unset($a); 3932 $phase->tasks['assesspeers'] = $task; 3933 } 3934 if ($workshop->useselfassessment and $numofself) { 3935 $task = new stdclass(); 3936 if ($numofselftodo == 0) { 3937 $task->completed = true; 3938 } elseif ($workshop->phase > workshop::PHASE_ASSESSMENT) { 3939 $task->completed = false; 3940 } 3941 $task->title = get_string('taskassessself', 'workshop'); 3942 $phase->tasks['assessself'] = $task; 3943 } 3944 } 3945 if ($workshop->assessmentstart) { 3946 $task = new stdclass(); 3947 $task->title = get_string('assessmentstartdatetime', 'workshop', workshop::timestamp_formats($workshop->assessmentstart)); 3948 $task->completed = 'info'; 3949 $phase->tasks['assessmentstartdatetime'] = $task; 3950 } 3951 if ($workshop->assessmentend) { 3952 $task = new stdclass(); 3953 $task->title = get_string('assessmentenddatetime', 'workshop', workshop::timestamp_formats($workshop->assessmentend)); 3954 $task->completed = 'info'; 3955 $phase->tasks['assessmentenddatetime'] = $task; 3956 } 3957 if (isset($phase->tasks['assessmentstartdatetime']) or isset($phase->tasks['assessmentenddatetime'])) { 3958 if (has_capability('mod/workshop:ignoredeadlines', $workshop->context, $userid)) { 3959 $task = new stdclass(); 3960 $task->title = get_string('deadlinesignored', 'workshop'); 3961 $task->completed = 'info'; 3962 $phase->tasks['deadlinesignored'] = $task; 3963 } 3964 } 3965 $this->phases[workshop::PHASE_ASSESSMENT] = $phase; 3966 3967 //--------------------------------------------------------- 3968 // setup | submission | assessment | * EVALUATION | closed 3969 //--------------------------------------------------------- 3970 $phase = new stdclass(); 3971 $phase->title = get_string('phaseevaluation', 'workshop'); 3972 $phase->tasks = array(); 3973 if (has_capability('mod/workshop:overridegrades', $workshop->context)) { 3974 $expected = $workshop->count_potential_authors(false); 3975 $calculated = $DB->count_records_select('workshop_submissions', 3976 'workshopid = ? AND (grade IS NOT NULL OR gradeover IS NOT NULL)', array($workshop->id)); 3977 $task = new stdclass(); 3978 $task->title = get_string('calculatesubmissiongrades', 'workshop'); 3979 $a = new stdclass(); 3980 $a->expected = $expected; 3981 $a->calculated = $calculated; 3982 $task->details = get_string('calculatesubmissiongradesdetails', 'workshop', $a); 3983 if ($calculated >= $expected) { 3984 $task->completed = true; 3985 } elseif ($workshop->phase > workshop::PHASE_EVALUATION) { 3986 $task->completed = false; 3987 } 3988 $phase->tasks['calculatesubmissiongrade'] = $task; 3989 3990 $expected = $workshop->count_potential_reviewers(false); 3991 $calculated = $DB->count_records_select('workshop_aggregations', 3992 'workshopid = ? AND gradinggrade IS NOT NULL', array($workshop->id)); 3993 $task = new stdclass(); 3994 $task->title = get_string('calculategradinggrades', 'workshop'); 3995 $a = new stdclass(); 3996 $a->expected = $expected; 3997 $a->calculated = $calculated; 3998 $task->details = get_string('calculategradinggradesdetails', 'workshop', $a); 3999 if ($calculated >= $expected) { 4000 $task->completed = true; 4001 } elseif ($workshop->phase > workshop::PHASE_EVALUATION) { 4002 $task->completed = false; 4003 } 4004 $phase->tasks['calculategradinggrade'] = $task; 4005 4006 } elseif ($workshop->phase == workshop::PHASE_EVALUATION) { 4007 $task = new stdclass(); 4008 $task->title = get_string('evaluategradeswait', 'workshop'); 4009 $task->completed = 'info'; 4010 $phase->tasks['evaluateinfo'] = $task; 4011 } 4012 4013 if (has_capability('moodle/course:manageactivities', $workshop->context, $userid)) { 4014 $task = new stdclass(); 4015 $task->title = get_string('taskconclusion', 'workshop'); 4016 $task->link = $workshop->updatemod_url(); 4017 if (trim($workshop->conclusion)) { 4018 $task->completed = true; 4019 } elseif ($workshop->phase >= workshop::PHASE_EVALUATION) { 4020 $task->completed = false; 4021 } 4022 $phase->tasks['conclusion'] = $task; 4023 } 4024 4025 $this->phases[workshop::PHASE_EVALUATION] = $phase; 4026 4027 //--------------------------------------------------------- 4028 // setup | submission | assessment | evaluation | * CLOSED 4029 //--------------------------------------------------------- 4030 $phase = new stdclass(); 4031 $phase->title = get_string('phaseclosed', 'workshop'); 4032 $phase->tasks = array(); 4033 $this->phases[workshop::PHASE_CLOSED] = $phase; 4034 4035 // Polish data, set default values if not done explicitly 4036 foreach ($this->phases as $phasecode => $phase) { 4037 $phase->title = isset($phase->title) ? $phase->title : ''; 4038 $phase->tasks = isset($phase->tasks) ? $phase->tasks : array(); 4039 if ($phasecode == $workshop->phase) { 4040 $phase->active = true; 4041 } else { 4042 $phase->active = false; 4043 } 4044 if (!isset($phase->actions)) { 4045 $phase->actions = array(); 4046 } 4047 4048 foreach ($phase->tasks as $taskcode => $task) { 4049 $task->title = isset($task->title) ? $task->title : ''; 4050 $task->link = isset($task->link) ? $task->link : null; 4051 $task->details = isset($task->details) ? $task->details : ''; 4052 $task->completed = isset($task->completed) ? $task->completed : null; 4053 } 4054 } 4055 4056 // Add phase switching actions. 4057 if (has_capability('mod/workshop:switchphase', $workshop->context, $userid)) { 4058 $nextphases = array( 4059 workshop::PHASE_SETUP => workshop::PHASE_SUBMISSION, 4060 workshop::PHASE_SUBMISSION => workshop::PHASE_ASSESSMENT, 4061 workshop::PHASE_ASSESSMENT => workshop::PHASE_EVALUATION, 4062 workshop::PHASE_EVALUATION => workshop::PHASE_CLOSED, 4063 ); 4064 foreach ($this->phases as $phasecode => $phase) { 4065 if ($phase->active) { 4066 if (isset($nextphases[$workshop->phase])) { 4067 $task = new stdClass(); 4068 $task->title = get_string('switchphasenext', 'mod_workshop'); 4069 $task->link = $workshop->switchphase_url($nextphases[$workshop->phase]); 4070 $task->details = ''; 4071 $task->completed = null; 4072 $phase->tasks['switchtonextphase'] = $task; 4073 } 4074 4075 } else { 4076 $action = new stdclass(); 4077 $action->type = 'switchphase'; 4078 $action->url = $workshop->switchphase_url($phasecode); 4079 $phase->actions[] = $action; 4080 } 4081 } 4082 } 4083 } 4084 4085 /** 4086 * Returns example submissions to be assessed by the owner of the planner 4087 * 4088 * This is here to cache the DB query because the same list is needed later in view.php 4089 * 4090 * @see workshop::get_examples_for_reviewer() for the format of returned value 4091 * @return array 4092 */ 4093 public function get_examples() { 4094 if (is_null($this->examples)) { 4095 $this->examples = $this->workshop->get_examples_for_reviewer($this->userid); 4096 } 4097 return $this->examples; 4098 } 4099 } 4100 4101 /** 4102 * Common base class for submissions and example submissions rendering 4103 * 4104 * Subclasses of this class convert raw submission record from 4105 * workshop_submissions table (as returned by {@see workshop::get_submission_by_id()} 4106 * for example) into renderable objects. 4107 */ 4108 abstract class workshop_submission_base { 4109 4110 /** @var bool is the submission anonymous (i.e. contains author information) */ 4111 protected $anonymous; 4112 4113 /* @var array of columns from workshop_submissions that are assigned as properties */ 4114 protected $fields = array(); 4115 4116 /** @var workshop */ 4117 protected $workshop; 4118 4119 /** 4120 * Copies the properties of the given database record into properties of $this instance 4121 * 4122 * @param workshop $workshop 4123 * @param stdClass $submission full record 4124 * @param bool $showauthor show the author-related information 4125 * @param array $options additional properties 4126 */ 4127 public function __construct(workshop $workshop, stdClass $submission, $showauthor = false) { 4128 4129 $this->workshop = $workshop; 4130 4131 foreach ($this->fields as $field) { 4132 if (!property_exists($submission, $field)) { 4133 throw new coding_exception('Submission record must provide public property ' . $field); 4134 } 4135 if (!property_exists($this, $field)) { 4136 throw new coding_exception('Renderable component must accept public property ' . $field); 4137 } 4138 $this->{$field} = $submission->{$field}; 4139 } 4140 4141 if ($showauthor) { 4142 $this->anonymous = false; 4143 } else { 4144 $this->anonymize(); 4145 } 4146 } 4147 4148 /** 4149 * Unsets all author-related properties so that the renderer does not have access to them 4150 * 4151 * Usually this is called by the contructor but can be called explicitely, too. 4152 */ 4153 public function anonymize() { 4154 $authorfields = explode(',', implode(',', \core_user\fields::get_picture_fields())); 4155 foreach ($authorfields as $field) { 4156 $prefixedusernamefield = 'author' . $field; 4157 unset($this->{$prefixedusernamefield}); 4158 } 4159 $this->anonymous = true; 4160 } 4161 4162 /** 4163 * Does the submission object contain author-related information? 4164 * 4165 * @return null|boolean 4166 */ 4167 public function is_anonymous() { 4168 return $this->anonymous; 4169 } 4170 } 4171 4172 /** 4173 * Renderable object containing a basic set of information needed to display the submission summary 4174 * 4175 * @see workshop_renderer::render_workshop_submission_summary 4176 */ 4177 class workshop_submission_summary extends workshop_submission_base implements renderable { 4178 4179 /** @var int */ 4180 public $id; 4181 /** @var string */ 4182 public $title; 4183 /** @var string graded|notgraded */ 4184 public $status; 4185 /** @var int */ 4186 public $timecreated; 4187 /** @var int */ 4188 public $timemodified; 4189 /** @var int */ 4190 public $authorid; 4191 /** @var string */ 4192 public $authorfirstname; 4193 /** @var string */ 4194 public $authorlastname; 4195 /** @var string */ 4196 public $authorfirstnamephonetic; 4197 /** @var string */ 4198 public $authorlastnamephonetic; 4199 /** @var string */ 4200 public $authormiddlename; 4201 /** @var string */ 4202 public $authoralternatename; 4203 /** @var int */ 4204 public $authorpicture; 4205 /** @var string */ 4206 public $authorimagealt; 4207 /** @var string */ 4208 public $authoremail; 4209 /** @var moodle_url to display submission */ 4210 public $url; 4211 4212 /** 4213 * @var array of columns from workshop_submissions that are assigned as properties 4214 * of instances of this class 4215 */ 4216 protected $fields = array( 4217 'id', 'title', 'timecreated', 'timemodified', 4218 'authorid', 'authorfirstname', 'authorlastname', 'authorfirstnamephonetic', 'authorlastnamephonetic', 4219 'authormiddlename', 'authoralternatename', 'authorpicture', 4220 'authorimagealt', 'authoremail'); 4221 } 4222 4223 /** 4224 * Renderable object containing all the information needed to display the submission 4225 * 4226 * @see workshop_renderer::render_workshop_submission() 4227 */ 4228 class workshop_submission extends workshop_submission_summary implements renderable { 4229 4230 /** @var string */ 4231 public $content; 4232 /** @var int */ 4233 public $contentformat; 4234 /** @var bool */ 4235 public $contenttrust; 4236 /** @var array */ 4237 public $attachment; 4238 4239 /** 4240 * @var array of columns from workshop_submissions that are assigned as properties 4241 * of instances of this class 4242 */ 4243 protected $fields = array( 4244 'id', 'title', 'timecreated', 'timemodified', 'content', 'contentformat', 'contenttrust', 4245 'attachment', 'authorid', 'authorfirstname', 'authorlastname', 'authorfirstnamephonetic', 'authorlastnamephonetic', 4246 'authormiddlename', 'authoralternatename', 'authorpicture', 'authorimagealt', 'authoremail'); 4247 } 4248 4249 /** 4250 * Renderable object containing a basic set of information needed to display the example submission summary 4251 * 4252 * @see workshop::prepare_example_summary() 4253 * @see workshop_renderer::render_workshop_example_submission_summary() 4254 */ 4255 class workshop_example_submission_summary extends workshop_submission_base implements renderable { 4256 4257 /** @var int */ 4258 public $id; 4259 /** @var string */ 4260 public $title; 4261 /** @var string graded|notgraded */ 4262 public $status; 4263 /** @var stdClass */ 4264 public $gradeinfo; 4265 /** @var moodle_url */ 4266 public $url; 4267 /** @var moodle_url */ 4268 public $editurl; 4269 /** @var string */ 4270 public $assesslabel; 4271 /** @var moodle_url */ 4272 public $assessurl; 4273 /** @var bool must be set explicitly by the caller */ 4274 public $editable = false; 4275 4276 /** 4277 * @var array of columns from workshop_submissions that are assigned as properties 4278 * of instances of this class 4279 */ 4280 protected $fields = array('id', 'title'); 4281 4282 /** 4283 * Example submissions are always anonymous 4284 * 4285 * @return true 4286 */ 4287 public function is_anonymous() { 4288 return true; 4289 } 4290 } 4291 4292 /** 4293 * Renderable object containing all the information needed to display the example submission 4294 * 4295 * @see workshop_renderer::render_workshop_example_submission() 4296 */ 4297 class workshop_example_submission extends workshop_example_submission_summary implements renderable { 4298 4299 /** @var string */ 4300 public $content; 4301 /** @var int */ 4302 public $contentformat; 4303 /** @var bool */ 4304 public $contenttrust; 4305 /** @var array */ 4306 public $attachment; 4307 4308 /** 4309 * @var array of columns from workshop_submissions that are assigned as properties 4310 * of instances of this class 4311 */ 4312 protected $fields = array('id', 'title', 'content', 'contentformat', 'contenttrust', 'attachment'); 4313 } 4314 4315 4316 /** 4317 * Common base class for assessments rendering 4318 * 4319 * Subclasses of this class convert raw assessment record from 4320 * workshop_assessments table (as returned by {@see workshop::get_assessment_by_id()} 4321 * for example) into renderable objects. 4322 */ 4323 abstract class workshop_assessment_base { 4324 4325 /** @var string the optional title of the assessment */ 4326 public $title = ''; 4327 4328 /** @var workshop_assessment_form $form as returned by {@link workshop_strategy::get_assessment_form()} */ 4329 public $form; 4330 4331 /** @var moodle_url */ 4332 public $url; 4333 4334 /** @var float|null the real received grade */ 4335 public $realgrade = null; 4336 4337 /** @var float the real maximum grade */ 4338 public $maxgrade; 4339 4340 /** @var stdClass|null reviewer user info */ 4341 public $reviewer = null; 4342 4343 /** @var stdClass|null assessed submission's author user info */ 4344 public $author = null; 4345 4346 /** @var array of actions */ 4347 public $actions = array(); 4348 4349 /* @var array of columns that are assigned as properties */ 4350 protected $fields = array(); 4351 4352 /** @var workshop */ 4353 public $workshop; 4354 4355 /** 4356 * Copies the properties of the given database record into properties of $this instance 4357 * 4358 * The $options keys are: showreviewer, showauthor 4359 * @param workshop $workshop 4360 * @param stdClass $assessment full record 4361 * @param array $options additional properties 4362 */ 4363 public function __construct(workshop $workshop, stdClass $record, array $options = array()) { 4364 4365 $this->workshop = $workshop; 4366 $this->validate_raw_record($record); 4367 4368 foreach ($this->fields as $field) { 4369 if (!property_exists($record, $field)) { 4370 throw new coding_exception('Assessment record must provide public property ' . $field); 4371 } 4372 if (!property_exists($this, $field)) { 4373 throw new coding_exception('Renderable component must accept public property ' . $field); 4374 } 4375 $this->{$field} = $record->{$field}; 4376 } 4377 4378 if (!empty($options['showreviewer'])) { 4379 $this->reviewer = user_picture::unalias($record, null, 'revieweridx', 'reviewer'); 4380 } 4381 4382 if (!empty($options['showauthor'])) { 4383 $this->author = user_picture::unalias($record, null, 'authorid', 'author'); 4384 } 4385 } 4386 4387 /** 4388 * Adds a new action 4389 * 4390 * @param moodle_url $url action URL 4391 * @param string $label action label 4392 * @param string $method get|post 4393 */ 4394 public function add_action(moodle_url $url, $label, $method = 'get') { 4395 4396 $action = new stdClass(); 4397 $action->url = $url; 4398 $action->label = $label; 4399 $action->method = $method; 4400 4401 $this->actions[] = $action; 4402 } 4403 4404 /** 4405 * Makes sure that we can cook the renderable component from the passed raw database record 4406 * 4407 * @param stdClass $assessment full assessment record 4408 * @throws coding_exception if the caller passed unexpected data 4409 */ 4410 protected function validate_raw_record(stdClass $record) { 4411 // nothing to do here 4412 } 4413 } 4414 4415 4416 /** 4417 * Represents a rendarable full assessment 4418 */ 4419 class workshop_assessment extends workshop_assessment_base implements renderable { 4420 4421 /** @var int */ 4422 public $id; 4423 4424 /** @var int */ 4425 public $submissionid; 4426 4427 /** @var int */ 4428 public $weight; 4429 4430 /** @var int */ 4431 public $timecreated; 4432 4433 /** @var int */ 4434 public $timemodified; 4435 4436 /** @var float */ 4437 public $grade; 4438 4439 /** @var float */ 4440 public $gradinggrade; 4441 4442 /** @var float */ 4443 public $gradinggradeover; 4444 4445 /** @var string */ 4446 public $feedbackauthor; 4447 4448 /** @var int */ 4449 public $feedbackauthorformat; 4450 4451 /** @var int */ 4452 public $feedbackauthorattachment; 4453 4454 /** @var array */ 4455 protected $fields = array('id', 'submissionid', 'weight', 'timecreated', 4456 'timemodified', 'grade', 'gradinggrade', 'gradinggradeover', 'feedbackauthor', 4457 'feedbackauthorformat', 'feedbackauthorattachment'); 4458 4459 /** 4460 * Format the overall feedback text content 4461 * 4462 * False is returned if the overall feedback feature is disabled. Null is returned 4463 * if the overall feedback content has not been found. Otherwise, string with 4464 * formatted feedback text is returned. 4465 * 4466 * @return string|bool|null 4467 */ 4468 public function get_overall_feedback_content() { 4469 4470 if ($this->workshop->overallfeedbackmode == 0) { 4471 return false; 4472 } 4473 4474 if (trim($this->feedbackauthor) === '') { 4475 return null; 4476 } 4477 4478 $content = file_rewrite_pluginfile_urls($this->feedbackauthor, 'pluginfile.php', $this->workshop->context->id, 4479 'mod_workshop', 'overallfeedback_content', $this->id); 4480 $content = format_text($content, $this->feedbackauthorformat, 4481 array('overflowdiv' => true, 'context' => $this->workshop->context)); 4482 4483 return $content; 4484 } 4485 4486 /** 4487 * Prepares the list of overall feedback attachments 4488 * 4489 * Returns false if overall feedback attachments are not allowed. Otherwise returns 4490 * list of attachments (may be empty). 4491 * 4492 * @return bool|array of stdClass 4493 */ 4494 public function get_overall_feedback_attachments() { 4495 4496 if ($this->workshop->overallfeedbackmode == 0) { 4497 return false; 4498 } 4499 4500 if ($this->workshop->overallfeedbackfiles == 0) { 4501 return false; 4502 } 4503 4504 if (empty($this->feedbackauthorattachment)) { 4505 return array(); 4506 } 4507 4508 $attachments = array(); 4509 $fs = get_file_storage(); 4510 $files = $fs->get_area_files($this->workshop->context->id, 'mod_workshop', 'overallfeedback_attachment', $this->id); 4511 foreach ($files as $file) { 4512 if ($file->is_directory()) { 4513 continue; 4514 } 4515 $filepath = $file->get_filepath(); 4516 $filename = $file->get_filename(); 4517 $fileurl = moodle_url::make_pluginfile_url($this->workshop->context->id, 'mod_workshop', 4518 'overallfeedback_attachment', $this->id, $filepath, $filename, true); 4519 $previewurl = new moodle_url(moodle_url::make_pluginfile_url($this->workshop->context->id, 'mod_workshop', 4520 'overallfeedback_attachment', $this->id, $filepath, $filename, false), array('preview' => 'bigthumb')); 4521 $attachments[] = (object)array( 4522 'filepath' => $filepath, 4523 'filename' => $filename, 4524 'fileurl' => $fileurl, 4525 'previewurl' => $previewurl, 4526 'mimetype' => $file->get_mimetype(), 4527 4528 ); 4529 } 4530 4531 return $attachments; 4532 } 4533 } 4534 4535 4536 /** 4537 * Represents a renderable training assessment of an example submission 4538 */ 4539 class workshop_example_assessment extends workshop_assessment implements renderable { 4540 4541 /** 4542 * @see parent::validate_raw_record() 4543 */ 4544 protected function validate_raw_record(stdClass $record) { 4545 if ($record->weight != 0) { 4546 throw new coding_exception('Invalid weight of example submission assessment'); 4547 } 4548 parent::validate_raw_record($record); 4549 } 4550 } 4551 4552 4553 /** 4554 * Represents a renderable reference assessment of an example submission 4555 */ 4556 class workshop_example_reference_assessment extends workshop_assessment implements renderable { 4557 4558 /** 4559 * @see parent::validate_raw_record() 4560 */ 4561 protected function validate_raw_record(stdClass $record) { 4562 if ($record->weight != 1) { 4563 throw new coding_exception('Invalid weight of the reference example submission assessment'); 4564 } 4565 parent::validate_raw_record($record); 4566 } 4567 } 4568 4569 4570 /** 4571 * Renderable message to be displayed to the user 4572 * 4573 * Message can contain an optional action link with a label that is supposed to be rendered 4574 * as a button or a link. 4575 * 4576 * @see workshop::renderer::render_workshop_message() 4577 */ 4578 class workshop_message implements renderable { 4579 4580 const TYPE_INFO = 10; 4581 const TYPE_OK = 20; 4582 const TYPE_ERROR = 30; 4583 4584 /** @var string */ 4585 protected $text = ''; 4586 /** @var int */ 4587 protected $type = self::TYPE_INFO; 4588 /** @var moodle_url */ 4589 protected $actionurl = null; 4590 /** @var string */ 4591 protected $actionlabel = ''; 4592 4593 /** 4594 * @param string $text short text to be displayed 4595 * @param string $type optional message type info|ok|error 4596 */ 4597 public function __construct($text = null, $type = self::TYPE_INFO) { 4598 $this->set_text($text); 4599 $this->set_type($type); 4600 } 4601 4602 /** 4603 * Sets the message text 4604 * 4605 * @param string $text short text to be displayed 4606 */ 4607 public function set_text($text) { 4608 $this->text = $text; 4609 } 4610 4611 /** 4612 * Sets the message type 4613 * 4614 * @param int $type 4615 */ 4616 public function set_type($type = self::TYPE_INFO) { 4617 if (in_array($type, array(self::TYPE_OK, self::TYPE_ERROR, self::TYPE_INFO))) { 4618 $this->type = $type; 4619 } else { 4620 throw new coding_exception('Unknown message type.'); 4621 } 4622 } 4623 4624 /** 4625 * Sets the optional message action 4626 * 4627 * @param moodle_url $url to follow on action 4628 * @param string $label action label 4629 */ 4630 public function set_action(moodle_url $url, $label) { 4631 $this->actionurl = $url; 4632 $this->actionlabel = $label; 4633 } 4634 4635 /** 4636 * Returns message text with HTML tags quoted 4637 * 4638 * @return string 4639 */ 4640 public function get_message() { 4641 return s($this->text); 4642 } 4643 4644 /** 4645 * Returns message type 4646 * 4647 * @return int 4648 */ 4649 public function get_type() { 4650 return $this->type; 4651 } 4652 4653 /** 4654 * Returns action URL 4655 * 4656 * @return moodle_url|null 4657 */ 4658 public function get_action_url() { 4659 return $this->actionurl; 4660 } 4661 4662 /** 4663 * Returns action label 4664 * 4665 * @return string 4666 */ 4667 public function get_action_label() { 4668 return $this->actionlabel; 4669 } 4670 } 4671 4672 4673 /** 4674 * Renderable component containing all the data needed to display the grading report 4675 */ 4676 class workshop_grading_report implements renderable { 4677 4678 /** @var stdClass returned by {@see workshop::prepare_grading_report_data()} */ 4679 protected $data; 4680 /** @var stdClass rendering options */ 4681 protected $options; 4682 4683 /** 4684 * Grades in $data must be already rounded to the set number of decimals or must be null 4685 * (in which later case, the [mod_workshop,nullgrade] string shall be displayed) 4686 * 4687 * @param stdClass $data prepared by {@link workshop::prepare_grading_report_data()} 4688 * @param stdClass $options display options (showauthornames, showreviewernames, sortby, sorthow, showsubmissiongrade, showgradinggrade) 4689 */ 4690 public function __construct(stdClass $data, stdClass $options) { 4691 $this->data = $data; 4692 $this->options = $options; 4693 } 4694 4695 /** 4696 * @return stdClass grading report data 4697 */ 4698 public function get_data() { 4699 return $this->data; 4700 } 4701 4702 /** 4703 * @return stdClass rendering options 4704 */ 4705 public function get_options() { 4706 return $this->options; 4707 } 4708 4709 /** 4710 * Prepare the data to be exported to a external system via Web Services. 4711 * 4712 * This function applies extra capabilities checks. 4713 * @return stdClass the data ready for external systems 4714 */ 4715 public function export_data_for_external() { 4716 $data = $this->get_data(); 4717 $options = $this->get_options(); 4718 4719 foreach ($data->grades as $reportdata) { 4720 // If we are in submission phase ignore the following data. 4721 if ($options->workshopphase == workshop::PHASE_SUBMISSION) { 4722 unset($reportdata->submissiongrade); 4723 unset($reportdata->gradinggrade); 4724 unset($reportdata->submissiongradeover); 4725 unset($reportdata->submissiongradeoverby); 4726 unset($reportdata->submissionpublished); 4727 unset($reportdata->reviewedby); 4728 unset($reportdata->reviewerof); 4729 continue; 4730 } 4731 4732 if (!$options->showsubmissiongrade) { 4733 unset($reportdata->submissiongrade); 4734 unset($reportdata->submissiongradeover); 4735 } 4736 4737 if (!$options->showgradinggrade and $tr == 0) { 4738 unset($reportdata->gradinggrade); 4739 } 4740 4741 if (!$options->showreviewernames) { 4742 foreach ($reportdata->reviewedby as $reviewedby) { 4743 $reviewedby->userid = 0; 4744 } 4745 } 4746 4747 if (!$options->showauthornames) { 4748 foreach ($reportdata->reviewerof as $reviewerof) { 4749 $reviewerof->userid = 0; 4750 } 4751 } 4752 } 4753 4754 return $data; 4755 } 4756 } 4757 4758 4759 /** 4760 * Base class for renderable feedback for author and feedback for reviewer 4761 */ 4762 abstract class workshop_feedback { 4763 4764 /** @var stdClass the user info */ 4765 protected $provider = null; 4766 4767 /** @var string the feedback text */ 4768 protected $content = null; 4769 4770 /** @var int format of the feedback text */ 4771 protected $format = null; 4772 4773 /** 4774 * @return stdClass the user info 4775 */ 4776 public function get_provider() { 4777 4778 if (is_null($this->provider)) { 4779 throw new coding_exception('Feedback provider not set'); 4780 } 4781 4782 return $this->provider; 4783 } 4784 4785 /** 4786 * @return string the feedback text 4787 */ 4788 public function get_content() { 4789 4790 if (is_null($this->content)) { 4791 throw new coding_exception('Feedback content not set'); 4792 } 4793 4794 return $this->content; 4795 } 4796 4797 /** 4798 * @return int format of the feedback text 4799 */ 4800 public function get_format() { 4801 4802 if (is_null($this->format)) { 4803 throw new coding_exception('Feedback text format not set'); 4804 } 4805 4806 return $this->format; 4807 } 4808 } 4809 4810 4811 /** 4812 * Renderable feedback for the author of submission 4813 */ 4814 class workshop_feedback_author extends workshop_feedback implements renderable { 4815 4816 /** 4817 * Extracts feedback from the given submission record 4818 * 4819 * @param stdClass $submission record as returned by {@see self::get_submission_by_id()} 4820 */ 4821 public function __construct(stdClass $submission) { 4822 4823 $this->provider = user_picture::unalias($submission, null, 'gradeoverbyx', 'gradeoverby'); 4824 $this->content = $submission->feedbackauthor; 4825 $this->format = $submission->feedbackauthorformat; 4826 } 4827 } 4828 4829 4830 /** 4831 * Renderable feedback for the reviewer 4832 */ 4833 class workshop_feedback_reviewer extends workshop_feedback implements renderable { 4834 4835 /** 4836 * Extracts feedback from the given assessment record 4837 * 4838 * @param stdClass $assessment record as returned by eg {@see self::get_assessment_by_id()} 4839 */ 4840 public function __construct(stdClass $assessment) { 4841 4842 $this->provider = user_picture::unalias($assessment, null, 'gradinggradeoverbyx', 'overby'); 4843 $this->content = $assessment->feedbackreviewer; 4844 $this->format = $assessment->feedbackreviewerformat; 4845 } 4846 } 4847 4848 4849 /** 4850 * Holds the final grades for the activity as are stored in the gradebook 4851 */ 4852 class workshop_final_grades implements renderable { 4853 4854 /** @var object the info from the gradebook about the grade for submission */ 4855 public $submissiongrade = null; 4856 4857 /** @var object the infor from the gradebook about the grade for assessment */ 4858 public $assessmentgrade = null; 4859 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body