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