See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * H5P activity manager class 19 * 20 * @package mod_h5pactivity 21 * @since Moodle 3.9 22 * @copyright 2020 Ferran Recio <ferran@moodle.com> 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 namespace mod_h5pactivity\local; 27 28 use mod_h5pactivity\local\report\participants; 29 use mod_h5pactivity\local\report\attempts; 30 use mod_h5pactivity\local\report\results; 31 use context_module; 32 use cm_info; 33 use moodle_recordset; 34 use core_user; 35 use stdClass; 36 use core\dml\sql_join; 37 use mod_h5pactivity\event\course_module_viewed; 38 39 /** 40 * Class manager for H5P activity 41 * 42 * @package mod_h5pactivity 43 * @since Moodle 3.9 44 * @copyright 2020 Ferran Recio <ferran@moodle.com> 45 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 46 */ 47 class manager { 48 49 /** No automathic grading using attempt results. */ 50 const GRADEMANUAL = 0; 51 52 /** Use highest attempt results for grading. */ 53 const GRADEHIGHESTATTEMPT = 1; 54 55 /** Use average attempt results for grading. */ 56 const GRADEAVERAGEATTEMPT = 2; 57 58 /** Use last attempt results for grading. */ 59 const GRADELASTATTEMPT = 3; 60 61 /** Use first attempt results for grading. */ 62 const GRADEFIRSTATTEMPT = 4; 63 64 /** Participants cannot review their own attempts. */ 65 const REVIEWNONE = 0; 66 67 /** Participants can review their own attempts when have one attempt completed. */ 68 const REVIEWCOMPLETION = 1; 69 70 /** @var stdClass course_module record. */ 71 private $instance; 72 73 /** @var context_module the current context. */ 74 private $context; 75 76 /** @var cm_info course_modules record. */ 77 private $coursemodule; 78 79 /** 80 * Class contructor. 81 * 82 * @param cm_info $coursemodule course module info object 83 * @param stdClass $instance H5Pactivity instance object. 84 */ 85 public function __construct(cm_info $coursemodule, stdClass $instance) { 86 $this->coursemodule = $coursemodule; 87 $this->instance = $instance; 88 $this->context = context_module::instance($coursemodule->id); 89 $this->instance->cmidnumber = $coursemodule->idnumber; 90 } 91 92 /** 93 * Create a manager instance from an instance record. 94 * 95 * @param stdClass $instance a h5pactivity record 96 * @return manager 97 */ 98 public static function create_from_instance(stdClass $instance): self { 99 $coursemodule = get_coursemodule_from_instance('h5pactivity', $instance->id); 100 // Ensure that $this->coursemodule is a cm_info object. 101 $coursemodule = cm_info::create($coursemodule); 102 return new self($coursemodule, $instance); 103 } 104 105 /** 106 * Create a manager instance from an course_modules record. 107 * 108 * @param stdClass|cm_info $coursemodule a h5pactivity record 109 * @return manager 110 */ 111 public static function create_from_coursemodule($coursemodule): self { 112 global $DB; 113 // Ensure that $this->coursemodule is a cm_info object. 114 $coursemodule = cm_info::create($coursemodule); 115 $instance = $DB->get_record('h5pactivity', ['id' => $coursemodule->instance], '*', MUST_EXIST); 116 return new self($coursemodule, $instance); 117 } 118 119 /** 120 * Return the available grading methods. 121 * @return string[] an array "option value" => "option description" 122 */ 123 public static function get_grading_methods(): array { 124 return [ 125 self::GRADEHIGHESTATTEMPT => get_string('grade_highest_attempt', 'mod_h5pactivity'), 126 self::GRADEAVERAGEATTEMPT => get_string('grade_average_attempt', 'mod_h5pactivity'), 127 self::GRADELASTATTEMPT => get_string('grade_last_attempt', 'mod_h5pactivity'), 128 self::GRADEFIRSTATTEMPT => get_string('grade_first_attempt', 'mod_h5pactivity'), 129 self::GRADEMANUAL => get_string('grade_manual', 'mod_h5pactivity'), 130 ]; 131 } 132 133 /** 134 * Return the selected attempt criteria. 135 * @return string[] an array "grademethod value", "attempt description" 136 */ 137 public function get_selected_attempt(): array { 138 $types = [ 139 self::GRADEHIGHESTATTEMPT => get_string('attempt_highest', 'mod_h5pactivity'), 140 self::GRADEAVERAGEATTEMPT => get_string('attempt_average', 'mod_h5pactivity'), 141 self::GRADELASTATTEMPT => get_string('attempt_last', 'mod_h5pactivity'), 142 self::GRADEFIRSTATTEMPT => get_string('attempt_first', 'mod_h5pactivity'), 143 self::GRADEMANUAL => get_string('attempt_none', 'mod_h5pactivity'), 144 ]; 145 if ($this->instance->enabletracking) { 146 $key = $this->instance->grademethod; 147 } else { 148 $key = self::GRADEMANUAL; 149 } 150 return [$key, $types[$key]]; 151 } 152 153 /** 154 * Return the available review modes. 155 * 156 * @return string[] an array "option value" => "option description" 157 */ 158 public static function get_review_modes(): array { 159 return [ 160 self::REVIEWCOMPLETION => get_string('review_on_completion', 'mod_h5pactivity'), 161 self::REVIEWNONE => get_string('review_none', 'mod_h5pactivity'), 162 ]; 163 } 164 165 /** 166 * Check if tracking is enabled in a particular h5pactivity for a specific user. 167 * 168 * @param stdClass|null $user user record (default $USER) 169 * @return bool if tracking is enabled in this activity 170 */ 171 public function is_tracking_enabled(stdClass $user = null): bool { 172 global $USER; 173 if (!$this->instance->enabletracking) { 174 return false; 175 } 176 if (empty($user)) { 177 $user = $USER; 178 } 179 return has_capability('mod/h5pactivity:submit', $this->context, $user, false); 180 } 181 182 /** 183 * Check if a user can see the activity attempts list. 184 * 185 * @param stdClass|null $user user record (default $USER) 186 * @return bool if the user can see the attempts link 187 */ 188 public function can_view_all_attempts(stdClass $user = null): bool { 189 global $USER; 190 if (!$this->instance->enabletracking) { 191 return false; 192 } 193 if (empty($user)) { 194 $user = $USER; 195 } 196 return has_capability('mod/h5pactivity:reviewattempts', $this->context, $user); 197 } 198 199 /** 200 * Check if a user can see own attempts. 201 * 202 * @param stdClass|null $user user record (default $USER) 203 * @return bool if the user can see the own attempts link 204 */ 205 public function can_view_own_attempts(stdClass $user = null): bool { 206 global $USER; 207 if (!$this->instance->enabletracking) { 208 return false; 209 } 210 if (empty($user)) { 211 $user = $USER; 212 } 213 if (has_capability('mod/h5pactivity:reviewattempts', $this->context, $user, false)) { 214 return true; 215 } 216 if ($this->instance->reviewmode == self::REVIEWNONE) { 217 return false; 218 } 219 if ($this->instance->reviewmode == self::REVIEWCOMPLETION) { 220 return true; 221 } 222 return false; 223 224 } 225 226 /** 227 * Return a relation of userid and the valid attempt's scaled score. 228 * 229 * The returned elements contain a record 230 * of userid, scaled value, attemptid and timemodified. In case the grading method is "GRADEAVERAGEATTEMPT" 231 * the attemptid will be zero. In case that tracking is disabled or grading method is "GRADEMANUAL" 232 * the method will return null. 233 * 234 * @param int $userid a specific userid or 0 for all user attempts. 235 * @return array|null of userid, scaled value and, if exists, the attempt id 236 */ 237 public function get_users_scaled_score(int $userid = 0): ?array { 238 global $DB; 239 240 $scaled = []; 241 if (!$this->instance->enabletracking) { 242 return null; 243 } 244 245 if ($this->instance->grademethod == self::GRADEMANUAL) { 246 return null; 247 } 248 249 $sql = ''; 250 251 // General filter. 252 $where = 'a.h5pactivityid = :h5pactivityid'; 253 $params['h5pactivityid'] = $this->instance->id; 254 255 if ($userid) { 256 $where .= ' AND a.userid = :userid'; 257 $params['userid'] = $userid; 258 } 259 260 // Average grading needs aggregation query. 261 if ($this->instance->grademethod == self::GRADEAVERAGEATTEMPT) { 262 $sql = "SELECT a.userid, AVG(a.scaled) AS scaled, 0 AS attemptid, MAX(timemodified) AS timemodified 263 FROM {h5pactivity_attempts} a 264 WHERE $where AND a.completion = 1 265 GROUP BY a.userid"; 266 } 267 268 if (empty($sql)) { 269 // Decide which attempt is used for the calculation. 270 $condition = [ 271 self::GRADEHIGHESTATTEMPT => "a.scaled < b.scaled", 272 self::GRADELASTATTEMPT => "a.attempt < b.attempt", 273 self::GRADEFIRSTATTEMPT => "a.attempt > b.attempt", 274 ]; 275 $join = $condition[$this->instance->grademethod] ?? $condition[self::GRADEHIGHESTATTEMPT]; 276 277 $sql = "SELECT a.userid, a.scaled, MAX(a.id) AS attemptid, MAX(a.timemodified) AS timemodified 278 FROM {h5pactivity_attempts} a 279 LEFT JOIN {h5pactivity_attempts} b ON a.h5pactivityid = b.h5pactivityid 280 AND a.userid = b.userid AND b.completion = 1 281 AND $join 282 WHERE $where AND b.id IS NULL AND a.completion = 1 283 GROUP BY a.userid, a.scaled"; 284 } 285 286 return $DB->get_records_sql($sql, $params); 287 } 288 289 /** 290 * Count the activity completed attempts. 291 * 292 * If no user is provided the method will count all active users attempts. 293 * Check get_active_users_join PHPdoc to a more detailed description of "active users". 294 * 295 * @param int|null $userid optional user id (default null) 296 * @return int the total amount of attempts 297 */ 298 public function count_attempts(int $userid = null): int { 299 global $DB; 300 301 // Counting records is enough for one user. 302 if ($userid) { 303 $params['userid'] = $userid; 304 $params = [ 305 'h5pactivityid' => $this->instance->id, 306 'userid' => $userid, 307 'completion' => 1, 308 ]; 309 return $DB->count_records('h5pactivity_attempts', $params); 310 } 311 312 $usersjoin = $this->get_active_users_join(); 313 314 // Final SQL. 315 return $DB->count_records_sql( 316 "SELECT COUNT(*) 317 FROM {user} u $usersjoin->joins 318 WHERE $usersjoin->wheres", 319 array_merge($usersjoin->params) 320 ); 321 } 322 323 /** 324 * Return the join to collect all activity active users. 325 * 326 * The concept of active user is relative to the activity permissions. All users with 327 * "mod/h5pactivity:view" are potential users but those with "mod/h5pactivity:reviewattempts" 328 * are evaluators and they don't count as valid submitters. 329 * 330 * Note that, in general, the active list has the same effect as checking for "mod/h5pactivity:submit" 331 * but submit capability cannot be used because is a write capability and does not apply to frozen contexts. 332 * 333 * @since Moodle 3.11 334 * @param bool $allpotentialusers if true, the join will return all active users, not only the ones with attempts. 335 * @param int|bool $currentgroup False if groups not used, 0 for all groups, group id (int) to filter by specific group 336 * @return sql_join the active users attempts join 337 */ 338 public function get_active_users_join(bool $allpotentialusers = false, $currentgroup = false): sql_join { 339 340 // Only valid users counts. By default, all users with submit capability are considered potential ones. 341 $context = $this->get_context(); 342 $coursemodule = $this->get_coursemodule(); 343 344 // Ensure user can view users from all groups. 345 if ($currentgroup === 0 && $coursemodule->effectivegroupmode == SEPARATEGROUPS 346 && !has_capability('moodle/site:accessallgroups', $context)) { 347 348 return new sql_join('', '1=2', [], true); 349 } 350 351 // We want to present all potential users. 352 $capjoin = get_enrolled_with_capabilities_join($context, '', 'mod/h5pactivity:view', $currentgroup); 353 354 if ($capjoin->cannotmatchanyrows) { 355 return $capjoin; 356 } 357 358 // But excluding all reviewattempts users converting a capabilities join into left join. 359 $reviewersjoin = get_with_capability_join($context, 'mod/h5pactivity:reviewattempts', 'u.id'); 360 if ($reviewersjoin->cannotmatchanyrows) { 361 return $capjoin; 362 } 363 364 $capjoin = new sql_join( 365 $capjoin->joins . "\n LEFT " . str_replace('ra', 'reviewer', $reviewersjoin->joins), 366 $capjoin->wheres . " AND reviewer.userid IS NULL", 367 $capjoin->params 368 ); 369 370 if ($allpotentialusers) { 371 return $capjoin; 372 } 373 374 // Add attempts join. 375 $where = "ha.h5pactivityid = :h5pactivityid AND ha.completion = :completion"; 376 $params = [ 377 'h5pactivityid' => $this->instance->id, 378 'completion' => 1, 379 ]; 380 381 return new sql_join( 382 $capjoin->joins . "\n JOIN {h5pactivity_attempts} ha ON ha.userid = u.id", 383 $capjoin->wheres . " AND $where", 384 array_merge($capjoin->params, $params) 385 ); 386 } 387 388 /** 389 * Return an array of all users and it's total attempts. 390 * 391 * Note: this funciton only returns the list of users with attempts, 392 * it does not check all participants. 393 * 394 * @return array indexed count userid => total number of attempts 395 */ 396 public function count_users_attempts(): array { 397 global $DB; 398 $params = [ 399 'h5pactivityid' => $this->instance->id, 400 ]; 401 $sql = "SELECT userid, count(*) 402 FROM {h5pactivity_attempts} 403 WHERE h5pactivityid = :h5pactivityid 404 GROUP BY userid"; 405 return $DB->get_records_sql_menu($sql, $params); 406 } 407 408 /** 409 * Return the current context. 410 * 411 * @return context_module 412 */ 413 public function get_context(): context_module { 414 return $this->context; 415 } 416 417 /** 418 * Return the current instance. 419 * 420 * @return stdClass the instance record 421 */ 422 public function get_instance(): stdClass { 423 return $this->instance; 424 } 425 426 /** 427 * Return the current cm_info. 428 * 429 * @return cm_info the course module 430 */ 431 public function get_coursemodule(): cm_info { 432 return $this->coursemodule; 433 } 434 435 /** 436 * Return the specific grader object for this activity. 437 * 438 * @return grader 439 */ 440 public function get_grader(): grader { 441 $idnumber = $this->coursemodule->idnumber ?? ''; 442 return new grader($this->instance, $idnumber); 443 } 444 445 /** 446 * Return the suitable report to show the attempts. 447 * 448 * This method controls the access to the different reports 449 * the activity have. 450 * 451 * @param int $userid an opional userid to show 452 * @param int $attemptid an optional $attemptid to show 453 * @param int|bool $currentgroup False if groups not used, 0 for all groups, group id (int) to filter by specific group 454 * @return report|null available report (or null if no report available) 455 */ 456 public function get_report(int $userid = null, int $attemptid = null, $currentgroup = false): ?report { 457 global $USER, $CFG; 458 459 require_once("{$CFG->dirroot}/user/lib.php"); 460 461 // If tracking is disabled, no reports are available. 462 if (!$this->instance->enabletracking) { 463 return null; 464 } 465 466 $attempt = null; 467 if ($attemptid) { 468 $attempt = $this->get_attempt($attemptid); 469 if (!$attempt) { 470 return null; 471 } 472 // If we have and attempt we can ignore the provided $userid. 473 $userid = $attempt->get_userid(); 474 } 475 476 if ($this->can_view_all_attempts()) { 477 $user = core_user::get_user($userid); 478 479 // Ensure user can view the attempt of specific userid, respecting access checks. 480 if ($user && $user->id != $USER->id) { 481 $course = get_course($this->coursemodule->course); 482 if ($this->coursemodule->effectivegroupmode == SEPARATEGROUPS && !user_can_view_profile($user, $course)) { 483 return null; 484 } 485 } 486 } else if ($this->can_view_own_attempts()) { 487 $user = core_user::get_user($USER->id); 488 if ($userid && $user->id != $userid) { 489 return null; 490 } 491 } else { 492 return null; 493 } 494 495 // Only enrolled users has reports. 496 if ($user && !is_enrolled($this->context, $user, 'mod/h5pactivity:view')) { 497 return null; 498 } 499 500 // Create the proper report. 501 if ($user && $attempt) { 502 return new results($this, $user, $attempt); 503 } else if ($user) { 504 return new attempts($this, $user); 505 } 506 return new participants($this, $currentgroup); 507 } 508 509 /** 510 * Return a single attempt. 511 * 512 * @param int $attemptid the attempt id 513 * @return attempt 514 */ 515 public function get_attempt(int $attemptid): ?attempt { 516 global $DB; 517 $record = $DB->get_record('h5pactivity_attempts', [ 518 'id' => $attemptid, 519 'h5pactivityid' => $this->instance->id, 520 ]); 521 if (!$record) { 522 return null; 523 } 524 return new attempt($record); 525 } 526 527 /** 528 * Return an array of all user attempts (including incompleted) 529 * 530 * @param int $userid the user id 531 * @return attempt[] 532 */ 533 public function get_user_attempts(int $userid): array { 534 global $DB; 535 $records = $DB->get_records( 536 'h5pactivity_attempts', 537 ['userid' => $userid, 'h5pactivityid' => $this->instance->id], 538 'id ASC' 539 ); 540 if (!$records) { 541 return []; 542 } 543 $result = []; 544 foreach ($records as $record) { 545 $result[] = new attempt($record); 546 } 547 return $result; 548 } 549 550 /** 551 * Trigger module viewed event and set the module viewed for completion. 552 * 553 * @param stdClass $course course object 554 * @return void 555 */ 556 public function set_module_viewed(stdClass $course): void { 557 global $CFG; 558 require_once($CFG->libdir . '/completionlib.php'); 559 560 // Trigger module viewed event. 561 $event = course_module_viewed::create([ 562 'objectid' => $this->instance->id, 563 'context' => $this->context 564 ]); 565 $event->add_record_snapshot('course', $course); 566 $event->add_record_snapshot('course_modules', $this->coursemodule); 567 $event->add_record_snapshot('h5pactivity', $this->instance); 568 $event->trigger(); 569 570 // Completion. 571 $completion = new \completion_info($course); 572 $completion->set_module_viewed($this->coursemodule); 573 } 574 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body