Differences Between: [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]
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 * Activity completion condition. 19 * 20 * @package availability_completion 21 * @copyright 2014 The Open University 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 namespace availability_completion; 26 27 use cache; 28 use core_availability\info; 29 use core_availability\info_module; 30 use core_availability\info_section; 31 use stdClass; 32 33 defined('MOODLE_INTERNAL') || die(); 34 35 require_once($CFG->libdir . '/completionlib.php'); 36 37 /** 38 * Activity completion condition. 39 * 40 * @package availability_completion 41 * @copyright 2014 The Open University 42 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 43 */ 44 class condition extends \core_availability\condition { 45 46 /** @var int previous module cm value used to calculate relative completions */ 47 public const OPTION_PREVIOUS = -1; 48 49 /** @var int ID of module that this depends on */ 50 protected $cmid; 51 52 /** @var array IDs of the current module and section */ 53 protected $selfids; 54 55 /** @var int Expected completion type (one of the COMPLETE_xx constants) */ 56 protected $expectedcompletion; 57 58 /** @var array Array of previous cmids used to calculate relative completions */ 59 protected $modfastprevious = []; 60 61 /** @var array Array of cmids previous to each course section */ 62 protected $sectionfastprevious = []; 63 64 /** @var array Array of modules used in these conditions for course */ 65 protected static $modsusedincondition = []; 66 67 /** 68 * Constructor. 69 * 70 * @param \stdClass $structure Data structure from JSON decode 71 * @throws \coding_exception If invalid data structure. 72 */ 73 public function __construct($structure) { 74 // Get cmid. 75 if (isset($structure->cm) && is_number($structure->cm)) { 76 $this->cmid = (int)$structure->cm; 77 } else { 78 throw new \coding_exception('Missing or invalid ->cm for completion condition'); 79 } 80 // Get expected completion. 81 if (isset($structure->e) && in_array($structure->e, 82 [COMPLETION_COMPLETE, COMPLETION_INCOMPLETE, 83 COMPLETION_COMPLETE_PASS, COMPLETION_COMPLETE_FAIL])) { 84 $this->expectedcompletion = $structure->e; 85 } else { 86 throw new \coding_exception('Missing or invalid ->e for completion condition'); 87 } 88 } 89 90 /** 91 * Saves tree data back to a structure object. 92 * 93 * @return stdClass Structure object (ready to be made into JSON format) 94 */ 95 public function save(): stdClass { 96 return (object) [ 97 'type' => 'completion', 98 'cm' => $this->cmid, 99 'e' => $this->expectedcompletion, 100 ]; 101 } 102 103 /** 104 * Returns a JSON object which corresponds to a condition of this type. 105 * 106 * Intended for unit testing, as normally the JSON values are constructed 107 * by JavaScript code. 108 * 109 * @param int $cmid Course-module id of other activity 110 * @param int $expectedcompletion Expected completion value (COMPLETION_xx) 111 * @return stdClass Object representing condition 112 */ 113 public static function get_json(int $cmid, int $expectedcompletion): stdClass { 114 return (object) [ 115 'type' => 'completion', 116 'cm' => (int)$cmid, 117 'e' => (int)$expectedcompletion, 118 ]; 119 } 120 121 /** 122 * Determines whether a particular item is currently available 123 * according to this availability condition. 124 * 125 * @see \core_availability\tree_node\update_after_restore 126 * 127 * @param bool $not Set true if we are inverting the condition 128 * @param info $info Item we're checking 129 * @param bool $grabthelot Performance hint: if true, caches information 130 * required for all course-modules, to make the front page and similar 131 * pages work more quickly (works only for current user) 132 * @param int $userid User ID to check availability for 133 * @return bool True if available 134 */ 135 public function is_available($not, info $info, $grabthelot, $userid): bool { 136 list($selfcmid, $selfsectionid) = $this->get_selfids($info); 137 $cmid = $this->get_cmid($info->get_course(), $selfcmid, $selfsectionid); 138 $modinfo = $info->get_modinfo(); 139 $completion = new \completion_info($modinfo->get_course()); 140 if (!array_key_exists($cmid, $modinfo->cms) || $modinfo->cms[$cmid]->deletioninprogress) { 141 // If the cmid cannot be found, always return false regardless 142 // of the condition or $not state. (Will be displayed in the 143 // information message.) 144 $allow = false; 145 } else { 146 // The completion system caches its own data so no caching needed here. 147 $completiondata = $completion->get_data((object)['id' => $cmid], 148 $grabthelot, $userid, $modinfo); 149 150 $allow = true; 151 if ($this->expectedcompletion == COMPLETION_COMPLETE) { 152 // Complete also allows the pass, fail states. 153 switch ($completiondata->completionstate) { 154 case COMPLETION_COMPLETE: 155 case COMPLETION_COMPLETE_FAIL: 156 case COMPLETION_COMPLETE_PASS: 157 break; 158 default: 159 $allow = false; 160 } 161 } else { 162 // Other values require exact match. 163 if ($completiondata->completionstate != $this->expectedcompletion) { 164 $allow = false; 165 } 166 } 167 168 if ($not) { 169 $allow = !$allow; 170 } 171 } 172 173 return $allow; 174 } 175 176 /** 177 * Return current item IDs (cmid and sectionid). 178 * 179 * @param info $info 180 * @return int[] with [0] => cmid/null, [1] => sectionid/null 181 */ 182 public function get_selfids(info $info): array { 183 if (isset($this->selfids)) { 184 return $this->selfids; 185 } 186 if ($info instanceof info_module) { 187 $cminfo = $info->get_course_module(); 188 if (!empty($cminfo->id)) { 189 $this->selfids = [$cminfo->id, null]; 190 return $this->selfids; 191 } 192 } 193 if ($info instanceof info_section) { 194 $section = $info->get_section(); 195 if (!empty($section->id)) { 196 $this->selfids = [null, $section->id]; 197 return $this->selfids; 198 } 199 200 } 201 return [null, null]; 202 } 203 204 /** 205 * Get the cmid referenced in the access restriction. 206 * 207 * @param stdClass $course course object 208 * @param int|null $selfcmid current course-module ID or null 209 * @param int|null $selfsectionid current course-section ID or null 210 * @return int|null cmid or null if no referenced cm is found 211 */ 212 public function get_cmid(stdClass $course, ?int $selfcmid, ?int $selfsectionid): ?int { 213 if ($this->cmid > 0) { 214 return $this->cmid; 215 } 216 // If it's a relative completion, load fast browsing. 217 if ($this->cmid == self::OPTION_PREVIOUS) { 218 $prevcmid = $this->get_previous_cmid($course, $selfcmid, $selfsectionid); 219 if ($prevcmid) { 220 return $prevcmid; 221 } 222 } 223 return null; 224 } 225 226 /** 227 * Return the previous CM ID of an specific course-module or course-section. 228 * 229 * @param stdClass $course course object 230 * @param int|null $selfcmid course-module ID or null 231 * @param int|null $selfsectionid course-section ID or null 232 * @return int|null 233 */ 234 private function get_previous_cmid(stdClass $course, ?int $selfcmid, ?int $selfsectionid): ?int { 235 $this->load_course_structure($course); 236 if (isset($this->modfastprevious[$selfcmid])) { 237 return $this->modfastprevious[$selfcmid]; 238 } 239 if (isset($this->sectionfastprevious[$selfsectionid])) { 240 return $this->sectionfastprevious[$selfsectionid]; 241 } 242 return null; 243 } 244 245 /** 246 * Loads static information about a course elements previous activities. 247 * 248 * Populates two variables: 249 * - $this->sectionprevious[] course-module previous to a cmid 250 * - $this->sectionfastprevious[] course-section previous to a cmid 251 * 252 * @param stdClass $course course object 253 */ 254 private function load_course_structure(stdClass $course): void { 255 // If already loaded we don't need to do anything. 256 if (empty($this->modfastprevious)) { 257 $previouscache = cache::make('availability_completion', 'previous_cache'); 258 $this->modfastprevious = $previouscache->get("mod_{$course->id}"); 259 $this->sectionfastprevious = $previouscache->get("sec_{$course->id}"); 260 } 261 262 if (!empty($this->modfastprevious)) { 263 return; 264 } 265 266 if (empty($this->modfastprevious)) { 267 $this->modfastprevious = []; 268 $sectionprevious = []; 269 270 $modinfo = get_fast_modinfo($course); 271 $lastcmid = 0; 272 foreach ($modinfo->cms as $othercm) { 273 if ($othercm->deletioninprogress) { 274 continue; 275 } 276 // Save first cm of every section. 277 if (!isset($sectionprevious[$othercm->section])) { 278 $sectionprevious[$othercm->section] = $lastcmid; 279 } 280 // Load previous to all cms with completion. 281 if ($othercm->completion == COMPLETION_TRACKING_NONE) { 282 continue; 283 } 284 if ($lastcmid) { 285 $this->modfastprevious[$othercm->id] = $lastcmid; 286 } 287 $lastcmid = $othercm->id; 288 } 289 // Fill empty sections index. 290 $isections = array_reverse($modinfo->get_section_info_all()); 291 foreach ($isections as $section) { 292 if (isset($sectionprevious[$section->id])) { 293 $lastcmid = $sectionprevious[$section->id]; 294 } else { 295 $sectionprevious[$section->id] = $lastcmid; 296 } 297 } 298 $this->sectionfastprevious = $sectionprevious; 299 $previouscache->set("mod_{$course->id}", $this->modfastprevious); 300 $previouscache->set("sec_{$course->id}", $this->sectionfastprevious); 301 } 302 } 303 304 /** 305 * Returns a more readable keyword corresponding to a completion state. 306 * 307 * Used to make lang strings easier to read. 308 * 309 * @param int $completionstate COMPLETION_xx constant 310 * @return string Readable keyword 311 */ 312 protected static function get_lang_string_keyword(int $completionstate): string { 313 switch($completionstate) { 314 case COMPLETION_INCOMPLETE: 315 return 'incomplete'; 316 case COMPLETION_COMPLETE: 317 return 'complete'; 318 case COMPLETION_COMPLETE_PASS: 319 return 'complete_pass'; 320 case COMPLETION_COMPLETE_FAIL: 321 return 'complete_fail'; 322 default: 323 throw new \coding_exception('Unexpected completion state: ' . $completionstate); 324 } 325 } 326 327 /** 328 * Obtains a string describing this restriction (whether or not 329 * it actually applies). 330 * 331 * @param bool $full Set true if this is the 'full information' view 332 * @param bool $not Set true if we are inverting the condition 333 * @param info $info Item we're checking 334 * @return string Information string (for admin) about all restrictions on 335 * this item 336 */ 337 public function get_description($full, $not, info $info): string { 338 global $USER; 339 $str = 'requires_'; 340 $course = $info->get_course(); 341 list($selfcmid, $selfsectionid) = $this->get_selfids($info); 342 $modname = ''; 343 // On ajax duplicate get_fast_modinfo is called before $PAGE->set_context 344 // so we cannot use $PAGE->user_is_editing(). 345 $coursecontext = \context_course::instance($course->id); 346 $editing = !empty($USER->editing) && has_capability('moodle/course:manageactivities', $coursecontext); 347 if ($this->cmid == self::OPTION_PREVIOUS && $editing) { 348 // Previous activity name could be inconsistent when editing due to partial page loadings. 349 $str .= 'previous_'; 350 } else { 351 // Get name for module. 352 $cmid = $this->get_cmid($course, $selfcmid, $selfsectionid); 353 $modinfo = $info->get_modinfo(); 354 if (!array_key_exists($cmid, $modinfo->cms) || $modinfo->cms[$cmid]->deletioninprogress) { 355 $modname = get_string('missing', 'availability_completion'); 356 } else { 357 $modname = self::description_cm_name($modinfo->cms[$cmid]->id); 358 } 359 } 360 361 // Work out which lang string to use depending on required completion status. 362 if ($not) { 363 // Convert NOT strings to use the equivalent where possible. 364 switch ($this->expectedcompletion) { 365 case COMPLETION_INCOMPLETE: 366 $str .= self::get_lang_string_keyword(COMPLETION_COMPLETE); 367 break; 368 case COMPLETION_COMPLETE: 369 $str .= self::get_lang_string_keyword(COMPLETION_INCOMPLETE); 370 break; 371 default: 372 // The other two cases do not have direct opposites. 373 $str .= 'not_' . self::get_lang_string_keyword($this->expectedcompletion); 374 break; 375 } 376 } else { 377 $str .= self::get_lang_string_keyword($this->expectedcompletion); 378 } 379 380 return get_string($str, 'availability_completion', $modname); 381 } 382 383 /** 384 * Obtains a representation of the options of this condition as a string, 385 * for debugging. 386 * 387 * @return string Text representation of parameters 388 */ 389 protected function get_debug_string(): string { 390 switch ($this->expectedcompletion) { 391 case COMPLETION_COMPLETE : 392 $type = 'COMPLETE'; 393 break; 394 case COMPLETION_INCOMPLETE : 395 $type = 'INCOMPLETE'; 396 break; 397 case COMPLETION_COMPLETE_PASS: 398 $type = 'COMPLETE_PASS'; 399 break; 400 case COMPLETION_COMPLETE_FAIL: 401 $type = 'COMPLETE_FAIL'; 402 break; 403 default: 404 throw new \coding_exception('Unexpected expected completion'); 405 } 406 $cm = $this->cmid; 407 if ($this->cmid == self::OPTION_PREVIOUS) { 408 $cm = 'opprevious'; 409 } 410 return 'cm' . $cm . ' ' . $type; 411 } 412 413 /** 414 * Updates this node after restore, returning true if anything changed. 415 * 416 * @see \core_availability\tree_node\update_after_restore 417 * 418 * @param string $restoreid Restore ID 419 * @param int $courseid ID of target course 420 * @param \base_logger $logger Logger for any warnings 421 * @param string $name Name of this item (for use in warning messages) 422 * @return bool True if there was any change 423 */ 424 public function update_after_restore($restoreid, $courseid, \base_logger $logger, $name): bool { 425 global $DB; 426 $res = false; 427 // If we depend on the previous activity, no translation is needed. 428 if ($this->cmid == self::OPTION_PREVIOUS) { 429 return $res; 430 } 431 $rec = \restore_dbops::get_backup_ids_record($restoreid, 'course_module', $this->cmid); 432 if (!$rec || !$rec->newitemid) { 433 // If we are on the same course (e.g. duplicate) then we can just 434 // use the existing one. 435 if ($DB->record_exists('course_modules', 436 ['id' => $this->cmid, 'course' => $courseid])) { 437 return $res; 438 } 439 // Otherwise it's a warning. 440 $this->cmid = 0; 441 $logger->process('Restored item (' . $name . 442 ') has availability condition on module that was not restored', 443 \backup::LOG_WARNING); 444 } else { 445 $this->cmid = (int)$rec->newitemid; 446 } 447 return true; 448 } 449 450 /** 451 * Used in course/lib.php because we need to disable the completion JS if 452 * a completion value affects a conditional activity. 453 * 454 * @param \stdClass $course Moodle course object 455 * @param int $cmid Course-module id 456 * @return bool True if this is used in a condition, false otherwise 457 */ 458 public static function completion_value_used($course, $cmid): bool { 459 // Have we already worked out a list of required completion values 460 // for this course? If so just use that. 461 if (!array_key_exists($course->id, self::$modsusedincondition)) { 462 // We don't have data for this course, build it. 463 $modinfo = get_fast_modinfo($course); 464 self::$modsusedincondition[$course->id] = []; 465 466 // Activities. 467 foreach ($modinfo->cms as $othercm) { 468 if (is_null($othercm->availability)) { 469 continue; 470 } 471 $ci = new \core_availability\info_module($othercm); 472 $tree = $ci->get_availability_tree(); 473 foreach ($tree->get_all_children('availability_completion\condition') as $cond) { 474 $condcmid = $cond->get_cmid($course, $othercm->id, null); 475 if (!empty($condcmid)) { 476 self::$modsusedincondition[$course->id][$condcmid] = true; 477 } 478 } 479 } 480 481 // Sections. 482 foreach ($modinfo->get_section_info_all() as $section) { 483 if (is_null($section->availability)) { 484 continue; 485 } 486 $ci = new \core_availability\info_section($section); 487 $tree = $ci->get_availability_tree(); 488 foreach ($tree->get_all_children('availability_completion\condition') as $cond) { 489 $condcmid = $cond->get_cmid($course, null, $section->id); 490 if (!empty($condcmid)) { 491 self::$modsusedincondition[$course->id][$condcmid] = true; 492 } 493 } 494 } 495 } 496 return array_key_exists($cmid, self::$modsusedincondition[$course->id]); 497 } 498 499 /** 500 * Wipes the static cache of modules used in a condition (for unit testing). 501 */ 502 public static function wipe_static_cache() { 503 self::$modsusedincondition = []; 504 } 505 506 public function update_dependency_id($table, $oldid, $newid) { 507 if ($table === 'course_modules' && (int)$this->cmid === (int)$oldid) { 508 $this->cmid = $newid; 509 return true; 510 } else { 511 return false; 512 } 513 } 514 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body