Differences Between: [Versions 400 and 401] [Versions 400 and 402] [Versions 400 and 403]
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 namespace core_courseformat; 18 19 use core\event\course_module_updated; 20 use core_courseformat\base as course_format; 21 use core_courseformat\stateupdates; 22 use cm_info; 23 use section_info; 24 use stdClass; 25 use course_modinfo; 26 use moodle_exception; 27 use context_module; 28 use context_course; 29 use cache; 30 31 /** 32 * Contains the core course state actions. 33 * 34 * The methods from this class should be executed via "core_courseformat_edit" web service. 35 * 36 * Each format plugin could extend this class to provide new actions to the editor. 37 * Extended classes should be locate in "format_XXX\course" namespace and 38 * extends core_courseformat\stateactions. 39 * 40 * @package core_courseformat 41 * @copyright 2021 Ferran Recio <ferran@moodle.com> 42 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 43 */ 44 class stateactions { 45 46 /** 47 * Move course modules to another location in the same course. 48 * 49 * @param stateupdates $updates the affected course elements track 50 * @param stdClass $course the course object 51 * @param int[] $ids the list of affected course module ids 52 * @param int $targetsectionid optional target section id 53 * @param int $targetcmid optional target cm id 54 */ 55 public function cm_move( 56 stateupdates $updates, 57 stdClass $course, 58 array $ids, 59 ?int $targetsectionid = null, 60 ?int $targetcmid = null 61 ): void { 62 // Validate target elements. 63 if (!$targetsectionid && !$targetcmid) { 64 throw new moodle_exception("Action cm_move requires targetsectionid or targetcmid"); 65 } 66 67 $this->validate_cms($course, $ids, __FUNCTION__); 68 69 // Check capabilities on every activity context. 70 foreach ($ids as $cmid) { 71 $modcontext = context_module::instance($cmid); 72 require_capability('moodle/course:manageactivities', $modcontext); 73 } 74 75 $modinfo = get_fast_modinfo($course); 76 77 // Target cm has more priority than target section. 78 if (!empty($targetcmid)) { 79 $this->validate_cms($course, [$targetcmid], __FUNCTION__); 80 $targetcm = $modinfo->get_cm($targetcmid); 81 $targetsection = $modinfo->get_section_info_by_id($targetcm->section, MUST_EXIST); 82 } else { 83 $this->validate_sections($course, [$targetsectionid], __FUNCTION__); 84 $targetcm = null; 85 $targetsection = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST); 86 } 87 88 // The origin sections must be updated as well. 89 $originalsections = []; 90 91 $cms = $this->get_cm_info($modinfo, $ids); 92 foreach ($cms as $cm) { 93 $currentsection = $modinfo->get_section_info_by_id($cm->section, MUST_EXIST); 94 moveto_module($cm, $targetsection, $targetcm); 95 $updates->add_cm_put($cm->id); 96 if ($currentsection->id != $targetsection->id) { 97 $originalsections[$currentsection->id] = true; 98 } 99 // If some of the original sections are also target sections, we don't need to update them. 100 if (array_key_exists($targetsection->id, $originalsections)) { 101 unset($originalsections[$targetsection->id]); 102 } 103 } 104 105 // Use section_state to return the full affected section and activities updated state. 106 $this->cm_state($updates, $course, $ids, $targetsectionid, $targetcmid); 107 108 foreach (array_keys($originalsections) as $sectionid) { 109 $updates->add_section_put($sectionid); 110 } 111 } 112 113 /** 114 * Move course sections to another location in the same course. 115 * 116 * @param stateupdates $updates the affected course elements track 117 * @param stdClass $course the course object 118 * @param int[] $ids the list of affected course module ids 119 * @param int $targetsectionid optional target section id 120 * @param int $targetcmid optional target cm id 121 */ 122 public function section_move( 123 stateupdates $updates, 124 stdClass $course, 125 array $ids, 126 ?int $targetsectionid = null, 127 ?int $targetcmid = null 128 ): void { 129 // Validate target elements. 130 if (!$targetsectionid) { 131 throw new moodle_exception("Action cm_move requires targetsectionid"); 132 } 133 134 $this->validate_sections($course, $ids, __FUNCTION__); 135 136 $coursecontext = context_course::instance($course->id); 137 require_capability('moodle/course:movesections', $coursecontext); 138 139 $modinfo = get_fast_modinfo($course); 140 141 // Target section. 142 $this->validate_sections($course, [$targetsectionid], __FUNCTION__); 143 $targetsection = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST); 144 145 $affectedsections = [$targetsection->section => true]; 146 147 $sections = $this->get_section_info($modinfo, $ids); 148 foreach ($sections as $section) { 149 $affectedsections[$section->section] = true; 150 move_section_to($course, $section->section, $targetsection->section); 151 } 152 153 // Use section_state to return the section and activities updated state. 154 $this->section_state($updates, $course, $ids, $targetsectionid); 155 156 // All course sections can be renamed because of the resort. 157 $allsections = $modinfo->get_section_info_all(); 158 foreach ($allsections as $section) { 159 // Ignore the affected sections because they are already in the updates. 160 if (isset($affectedsections[$section->section])) { 161 continue; 162 } 163 $updates->add_section_put($section->id); 164 } 165 // The section order is at a course level. 166 $updates->add_course_put(); 167 } 168 169 /** 170 * Create a course section. 171 * 172 * This method follows the same logic as changenumsections.php. 173 * 174 * @param stateupdates $updates the affected course elements track 175 * @param stdClass $course the course object 176 * @param int[] $ids not used 177 * @param int $targetsectionid optional target section id (if not passed section will be appended) 178 * @param int $targetcmid not used 179 */ 180 public function section_add( 181 stateupdates $updates, 182 stdClass $course, 183 array $ids = [], 184 ?int $targetsectionid = null, 185 ?int $targetcmid = null 186 ): void { 187 188 $coursecontext = context_course::instance($course->id); 189 require_capability('moodle/course:update', $coursecontext); 190 191 // Get course format settings. 192 $format = course_get_format($course->id); 193 $lastsectionnumber = $format->get_last_section_number(); 194 $maxsections = $format->get_max_sections(); 195 196 if ($lastsectionnumber >= $maxsections) { 197 throw new moodle_exception('maxsectionslimit', 'moodle', $maxsections); 198 } 199 200 $modinfo = get_fast_modinfo($course); 201 202 // Get target section. 203 if ($targetsectionid) { 204 $this->validate_sections($course, [$targetsectionid], __FUNCTION__); 205 $targetsection = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST); 206 // Inserting sections at any position except in the very end requires capability to move sections. 207 require_capability('moodle/course:movesections', $coursecontext); 208 $insertposition = $targetsection->section + 1; 209 } else { 210 // Get last section. 211 $insertposition = 0; 212 } 213 214 course_create_section($course, $insertposition); 215 216 // Adding a section affects the full course structure. 217 $this->course_state($updates, $course); 218 } 219 220 /** 221 * Delete course sections. 222 * 223 * This method follows the same logic as editsection.php. 224 * 225 * @param stateupdates $updates the affected course elements track 226 * @param stdClass $course the course object 227 * @param int[] $ids section ids 228 * @param int $targetsectionid not used 229 * @param int $targetcmid not used 230 */ 231 public function section_delete( 232 stateupdates $updates, 233 stdClass $course, 234 array $ids = [], 235 ?int $targetsectionid = null, 236 ?int $targetcmid = null 237 ): void { 238 239 $coursecontext = context_course::instance($course->id); 240 require_capability('moodle/course:update', $coursecontext); 241 require_capability('moodle/course:movesections', $coursecontext); 242 243 $modinfo = get_fast_modinfo($course); 244 245 foreach ($ids as $sectionid) { 246 $section = $modinfo->get_section_info_by_id($sectionid, MUST_EXIST); 247 // Send all activity deletions. 248 if (!empty($modinfo->sections[$section->section])) { 249 foreach ($modinfo->sections[$section->section] as $modnumber) { 250 $cm = $modinfo->cms[$modnumber]; 251 $updates->add_cm_remove($cm->id); 252 } 253 } 254 course_delete_section($course, $section, true, true); 255 $updates->add_section_remove($sectionid); 256 } 257 258 // Removing a section affects the full course structure. 259 $this->course_state($updates, $course); 260 } 261 262 /** 263 * Move course cms to the right. Indent = 1. 264 * 265 * @param stateupdates $updates the affected course elements track 266 * @param stdClass $course the course object 267 * @param int[] $ids cm ids 268 * @param int $targetsectionid not used 269 * @param int $targetcmid not used 270 */ 271 public function cm_moveright( 272 stateupdates $updates, 273 stdClass $course, 274 array $ids = [], 275 ?int $targetsectionid = null, 276 ?int $targetcmid = null 277 ): void { 278 $this->set_cm_indentation($updates, $course, $ids, 1); 279 } 280 281 /** 282 * Move course cms to the left. Indent = 0. 283 * 284 * @param stateupdates $updates the affected course elements track 285 * @param stdClass $course the course object 286 * @param int[] $ids cm ids 287 * @param int $targetsectionid not used 288 * @param int $targetcmid not used 289 */ 290 public function cm_moveleft( 291 stateupdates $updates, 292 stdClass $course, 293 array $ids = [], 294 ?int $targetsectionid = null, 295 ?int $targetcmid = null 296 ): void { 297 $this->set_cm_indentation($updates, $course, $ids, 0); 298 } 299 300 /** 301 * Internal method to define the cm indentation level. 302 * 303 * @param stateupdates $updates the affected course elements track 304 * @param stdClass $course the course object 305 * @param int[] $ids cm ids 306 * @param int $indent new value for indentation 307 */ 308 protected function set_cm_indentation( 309 stateupdates $updates, 310 stdClass $course, 311 array $ids, 312 int $indent 313 ): void { 314 global $DB; 315 316 $this->validate_cms($course, $ids, __FUNCTION__); 317 318 // Check capabilities on every activity context. 319 foreach ($ids as $cmid) { 320 $modcontext = context_module::instance($cmid); 321 require_capability('moodle/course:manageactivities', $modcontext); 322 } 323 $modinfo = get_fast_modinfo($course); 324 $cms = $this->get_cm_info($modinfo, $ids); 325 list($insql, $inparams) = $DB->get_in_or_equal(array_keys($cms), SQL_PARAMS_NAMED); 326 $DB->set_field_select('course_modules', 'indent', $indent, "id $insql", $inparams); 327 rebuild_course_cache($course->id, false, true); 328 foreach ($cms as $cm) { 329 $modcontext = context_module::instance($cm->id); 330 course_module_updated::create_from_cm($cm, $modcontext)->trigger(); 331 $updates->add_cm_put($cm->id); 332 } 333 } 334 335 /** 336 * Extract several cm_info from the course_modinfo. 337 * 338 * @param course_modinfo $modinfo the course modinfo. 339 * @param int[] $ids the course modules $ids 340 * @return cm_info[] the extracted cm_info objects 341 */ 342 protected function get_cm_info (course_modinfo $modinfo, array $ids): array { 343 $cms = []; 344 foreach ($ids as $cmid) { 345 $cms[$cmid] = $modinfo->get_cm($cmid); 346 } 347 return $cms; 348 } 349 350 /** 351 * Extract several section_info from the course_modinfo. 352 * 353 * @param course_modinfo $modinfo the course modinfo. 354 * @param int[] $ids the course modules $ids 355 * @return section_info[] the extracted section_info objects 356 */ 357 protected function get_section_info(course_modinfo $modinfo, array $ids): array { 358 $sections = []; 359 foreach ($ids as $sectionid) { 360 $sections[$sectionid] = $modinfo->get_section_info_by_id($sectionid); 361 } 362 return $sections; 363 } 364 365 /** 366 * Update the course content section collapsed value. 367 * 368 * @param stateupdates $updates the affected course elements track 369 * @param stdClass $course the course object 370 * @param int[] $ids the collapsed section ids 371 * @param int $targetsectionid not used 372 * @param int $targetcmid not used 373 */ 374 public function section_content_collapsed( 375 stateupdates $updates, 376 stdClass $course, 377 array $ids = [], 378 ?int $targetsectionid = null, 379 ?int $targetcmid = null 380 ): void { 381 if (!empty($ids)) { 382 $this->validate_sections($course, $ids, __FUNCTION__); 383 } 384 $format = course_get_format($course->id); 385 $format->set_sections_preference('contentcollapsed', $ids); 386 } 387 388 /** 389 * Update the course index section collapsed value. 390 * 391 * @param stateupdates $updates the affected course elements track 392 * @param stdClass $course the course object 393 * @param int[] $ids the collapsed section ids 394 * @param int $targetsectionid not used 395 * @param int $targetcmid not used 396 */ 397 public function section_index_collapsed( 398 stateupdates $updates, 399 stdClass $course, 400 array $ids = [], 401 ?int $targetsectionid = null, 402 ?int $targetcmid = null 403 ): void { 404 if (!empty($ids)) { 405 $this->validate_sections($course, $ids, __FUNCTION__); 406 } 407 $format = course_get_format($course->id); 408 $format->set_sections_preference('indexcollapsed', $ids); 409 } 410 411 /** 412 * Add the update messages of the updated version of any cm and section related to the cm ids. 413 * 414 * This action is mainly used by legacy actions to partially update the course state when the 415 * result of core_course_edit_module is not enough to generate the correct state data. 416 * 417 * @param stateupdates $updates the affected course elements track 418 * @param stdClass $course the course object 419 * @param int[] $ids the list of affected course module ids 420 * @param int $targetsectionid optional target section id 421 * @param int $targetcmid optional target cm id 422 */ 423 public function cm_state( 424 stateupdates $updates, 425 stdClass $course, 426 array $ids, 427 ?int $targetsectionid = null, 428 ?int $targetcmid = null 429 ): void { 430 431 // Collect all section and cm to return. 432 $cmids = []; 433 foreach ($ids as $cmid) { 434 $cmids[$cmid] = true; 435 } 436 if ($targetcmid) { 437 $cmids[$targetcmid] = true; 438 } 439 440 $sectionids = []; 441 if ($targetsectionid) { 442 $this->validate_sections($course, [$targetsectionid], __FUNCTION__); 443 $sectionids[$targetsectionid] = true; 444 } 445 446 $this->validate_cms($course, array_keys($cmids), __FUNCTION__); 447 448 $modinfo = course_modinfo::instance($course); 449 450 foreach (array_keys($cmids) as $cmid) { 451 452 // Add this action to updates array. 453 $updates->add_cm_put($cmid); 454 455 $cm = $modinfo->get_cm($cmid); 456 $sectionids[$cm->section] = true; 457 } 458 459 foreach (array_keys($sectionids) as $sectionid) { 460 $updates->add_section_put($sectionid); 461 } 462 } 463 464 /** 465 * Add the update messages of the updated version of any cm and section related to the section ids. 466 * 467 * This action is mainly used by legacy actions to partially update the course state when the 468 * result of core_course_edit_module is not enough to generate the correct state data. 469 * 470 * @param stateupdates $updates the affected course elements track 471 * @param stdClass $course the course object 472 * @param int[] $ids the list of affected course section ids 473 * @param int $targetsectionid optional target section id 474 * @param int $targetcmid optional target cm id 475 */ 476 public function section_state( 477 stateupdates $updates, 478 stdClass $course, 479 array $ids, 480 ?int $targetsectionid = null, 481 ?int $targetcmid = null 482 ): void { 483 484 $cmids = []; 485 if ($targetcmid) { 486 $this->validate_cms($course, [$targetcmid], __FUNCTION__); 487 $cmids[$targetcmid] = true; 488 } 489 490 $sectionids = []; 491 foreach ($ids as $sectionid) { 492 $sectionids[$sectionid] = true; 493 } 494 if ($targetsectionid) { 495 $sectionids[$targetsectionid] = true; 496 } 497 498 $this->validate_sections($course, array_keys($sectionids), __FUNCTION__); 499 500 $modinfo = course_modinfo::instance($course); 501 502 foreach (array_keys($sectionids) as $sectionid) { 503 $sectioninfo = $modinfo->get_section_info_by_id($sectionid); 504 $updates->add_section_put($sectionid); 505 // Add cms. 506 if (empty($modinfo->sections[$sectioninfo->section])) { 507 continue; 508 } 509 510 foreach ($modinfo->sections[$sectioninfo->section] as $modnumber) { 511 $mod = $modinfo->cms[$modnumber]; 512 if ($mod->is_visible_on_course_page()) { 513 $cmids[$mod->id] = true; 514 } 515 } 516 } 517 518 foreach (array_keys($cmids) as $cmid) { 519 // Add this action to updates array. 520 $updates->add_cm_put($cmid); 521 } 522 } 523 524 /** 525 * Add all the update messages from the complete course state. 526 * 527 * This action is mainly used by legacy actions to partially update the course state when the 528 * result of core_course_edit_module is not enough to generate the correct state data. 529 * 530 * @param stateupdates $updates the affected course elements track 531 * @param stdClass $course the course object 532 * @param int[] $ids the list of affected course module ids (not used) 533 * @param int $targetsectionid optional target section id (not used) 534 * @param int $targetcmid optional target cm id (not used) 535 */ 536 public function course_state( 537 stateupdates $updates, 538 stdClass $course, 539 array $ids = [], 540 ?int $targetsectionid = null, 541 ?int $targetcmid = null 542 ): void { 543 544 $modinfo = course_modinfo::instance($course); 545 546 $updates->add_course_put(); 547 548 // Add sections updates. 549 $sections = $modinfo->get_section_info_all(); 550 $sectionids = []; 551 foreach ($sections as $sectioninfo) { 552 $sectionids[] = $sectioninfo->id; 553 } 554 if (!empty($sectionids)) { 555 $this->section_state($updates, $course, $sectionids); 556 } 557 } 558 559 /** 560 * Checks related to sections: course format support them, all given sections exist and topic 0 is not included. 561 * 562 * @param stdClass $course The course where given $sectionids belong. 563 * @param array $sectionids List of sections to validate. 564 * @param string|null $info additional information in case of error (default null). 565 * @throws moodle_exception if any id is not valid 566 */ 567 protected function validate_sections(stdClass $course, array $sectionids, ?string $info = null): void { 568 global $DB; 569 570 if (empty($sectionids)) { 571 throw new moodle_exception('emptysectionids', 'core', null, $info); 572 } 573 574 // No section actions are allowed if course format does not support sections. 575 $courseformat = course_get_format($course->id); 576 if (!$courseformat->uses_sections()) { 577 throw new moodle_exception('sectionactionnotsupported', 'core', null, $info); 578 } 579 580 list($insql, $inparams) = $DB->get_in_or_equal($sectionids, SQL_PARAMS_NAMED); 581 582 // Check if all the given sections exist. 583 $couintsections = $DB->count_records_select('course_sections', "id $insql", $inparams); 584 if ($couintsections != count($sectionids)) { 585 throw new moodle_exception('unexistingsectionid', 'core', null, $info); 586 } 587 } 588 589 /** 590 * Checks related to course modules: all given cm exist. 591 * 592 * @param stdClass $course The course where given $cmids belong. 593 * @param array $cmids List of course module ids to validate. 594 * @param string $info additional information in case of error. 595 * @throws moodle_exception if any id is not valid 596 */ 597 protected function validate_cms(stdClass $course, array $cmids, ?string $info = null): void { 598 599 if (empty($cmids)) { 600 throw new moodle_exception('emptycmids', 'core', null, $info); 601 } 602 603 $moduleinfo = get_fast_modinfo($course->id); 604 $intersect = array_intersect($cmids, array_keys($moduleinfo->get_cms())); 605 if (count($cmids) != count($intersect)) { 606 throw new moodle_exception('unexistingcmid', 'core', null, $info); 607 } 608 } 609 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body