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