See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 39 and 401] [Versions 401 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; 18 19 /** 20 * Test for various bits of datalib.php. 21 * 22 * @package core 23 * @category test 24 * @copyright 2012 The Open University 25 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 26 */ 27 class datalib_test extends \advanced_testcase { 28 protected function normalise_sql($sort) { 29 return preg_replace('~\s+~', ' ', $sort); 30 } 31 32 protected function assert_same_sql($expected, $actual) { 33 $this->assertSame($this->normalise_sql($expected), $this->normalise_sql($actual)); 34 } 35 36 /** 37 * Do a test of the user search SQL with database users. 38 */ 39 public function test_users_search_sql() { 40 global $DB; 41 $this->resetAfterTest(); 42 43 // Set up test users. 44 $user1 = array( 45 'username' => 'usernametest1', 46 'idnumber' => 'idnumbertest1', 47 'firstname' => 'First Name User Test 1', 48 'lastname' => 'Last Name User Test 1', 49 'email' => 'usertest1@example.com', 50 'address' => '2 Test Street Perth 6000 WA', 51 'phone1' => '01010101010', 52 'phone2' => '02020203', 53 'department' => 'Department of user 1', 54 'institution' => 'Institution of user 1', 55 'description' => 'This is a description for user 1', 56 'descriptionformat' => FORMAT_MOODLE, 57 'city' => 'Perth', 58 'country' => 'AU' 59 ); 60 $user1 = self::getDataGenerator()->create_user($user1); 61 $user2 = array( 62 'username' => 'usernametest2', 63 'idnumber' => 'idnumbertest2', 64 'firstname' => 'First Name User Test 2', 65 'lastname' => 'Last Name User Test 2', 66 'email' => 'usertest2@example.com', 67 'address' => '222 Test Street Perth 6000 WA', 68 'phone1' => '01010101010', 69 'phone2' => '02020203', 70 'department' => 'Department of user 2', 71 'institution' => 'Institution of user 2', 72 'description' => 'This is a description for user 2', 73 'descriptionformat' => FORMAT_MOODLE, 74 'city' => 'Perth', 75 'country' => 'AU' 76 ); 77 $user2 = self::getDataGenerator()->create_user($user2); 78 79 // Search by name (anywhere in text). 80 list($sql, $params) = users_search_sql('User Test 2', ''); 81 $results = $DB->get_records_sql("SELECT id FROM {user} WHERE $sql ORDER BY username", $params); 82 $this->assertFalse(array_key_exists($user1->id, $results)); 83 $this->assertTrue(array_key_exists($user2->id, $results)); 84 85 // Search by (most of) full name. 86 list($sql, $params) = users_search_sql('First Name User Test 2 Last Name User', ''); 87 $results = $DB->get_records_sql("SELECT id FROM {user} WHERE $sql ORDER BY username", $params); 88 $this->assertFalse(array_key_exists($user1->id, $results)); 89 $this->assertTrue(array_key_exists($user2->id, $results)); 90 91 // Search by name (start of text) valid or not. 92 list($sql, $params) = users_search_sql('User Test 2', '', false); 93 $results = $DB->get_records_sql("SELECT id FROM {user} WHERE $sql ORDER BY username", $params); 94 $this->assertEquals(0, count($results)); 95 list($sql, $params) = users_search_sql('First Name User Test 2', '', false); 96 $results = $DB->get_records_sql("SELECT id FROM {user} WHERE $sql ORDER BY username", $params); 97 $this->assertFalse(array_key_exists($user1->id, $results)); 98 $this->assertTrue(array_key_exists($user2->id, $results)); 99 100 // Search by extra fields included or not (address). 101 list($sql, $params) = users_search_sql('Test Street', '', true); 102 $results = $DB->get_records_sql("SELECT id FROM {user} WHERE $sql ORDER BY username", $params); 103 $this->assertCount(0, $results); 104 list($sql, $params) = users_search_sql('Test Street', '', true, array('address')); 105 $results = $DB->get_records_sql("SELECT id FROM {user} WHERE $sql ORDER BY username", $params); 106 $this->assertCount(2, $results); 107 108 // Exclude user. 109 list($sql, $params) = users_search_sql('User Test', '', true, array(), array($user1->id)); 110 $results = $DB->get_records_sql("SELECT id FROM {user} WHERE $sql ORDER BY username", $params); 111 $this->assertFalse(array_key_exists($user1->id, $results)); 112 $this->assertTrue(array_key_exists($user2->id, $results)); 113 114 // Include only user. 115 list($sql, $params) = users_search_sql('User Test', '', true, array(), array(), array($user1->id)); 116 $results = $DB->get_records_sql("SELECT id FROM {user} WHERE $sql ORDER BY username", $params); 117 $this->assertTrue(array_key_exists($user1->id, $results)); 118 $this->assertFalse(array_key_exists($user2->id, $results)); 119 120 // Join with another table and use different prefix. 121 set_user_preference('amphibian', 'frog', $user1); 122 set_user_preference('amphibian', 'salamander', $user2); 123 list($sql, $params) = users_search_sql('User Test 1', 'qq'); 124 $results = $DB->get_records_sql(" 125 SELECT up.id, up.value 126 FROM {user} qq 127 JOIN {user_preferences} up ON up.userid = qq.id 128 WHERE up.name = :prefname 129 AND $sql", array_merge(array('prefname' => 'amphibian'), $params)); 130 $this->assertEquals(1, count($results)); 131 foreach ($results as $record) { 132 $this->assertSame('frog', $record->value); 133 } 134 135 // Join with another table and include other table fields in search. 136 set_user_preference('reptile', 'snake', $user1); 137 set_user_preference('reptile', 'lizard', $user2); 138 list($sql, $params) = users_search_sql('snake', 'qq', true, ['up.value']); 139 $results = $DB->get_records_sql(" 140 SELECT up.id, up.value 141 FROM {user} qq 142 JOIN {user_preferences} up ON up.userid = qq.id 143 WHERE up.name = :prefname 144 AND $sql", array_merge(array('prefname' => 'reptile'), $params)); 145 $this->assertEquals(1, count($results)); 146 foreach ($results as $record) { 147 $this->assertSame('snake', $record->value); 148 } 149 } 150 151 public function test_users_order_by_sql_simple() { 152 list($sort, $params) = users_order_by_sql(); 153 $this->assert_same_sql('lastname, firstname, id', $sort); 154 $this->assertEquals(array(), $params); 155 } 156 157 public function test_users_order_by_sql_table_prefix() { 158 list($sort, $params) = users_order_by_sql('u'); 159 $this->assert_same_sql('u.lastname, u.firstname, u.id', $sort); 160 $this->assertEquals(array(), $params); 161 } 162 163 public function test_users_order_by_sql_search_no_extra_fields() { 164 global $CFG, $DB; 165 $this->resetAfterTest(true); 166 167 $CFG->showuseridentity = ''; 168 169 list($sort, $params) = users_order_by_sql('', 'search', \context_system::instance()); 170 $this->assert_same_sql('CASE WHEN 171 ' . $DB->sql_fullname() . ' = :usersortexact1 OR 172 LOWER(firstname) = LOWER(:usersortexact2) OR 173 LOWER(lastname) = LOWER(:usersortexact3) 174 THEN 0 ELSE 1 END, lastname, firstname, id', $sort); 175 $this->assertEquals(array('usersortexact1' => 'search', 'usersortexact2' => 'search', 176 'usersortexact3' => 'search'), $params); 177 } 178 179 public function test_users_order_by_sql_search_with_extra_fields_and_prefix() { 180 global $CFG, $DB; 181 $this->resetAfterTest(); 182 183 $CFG->showuseridentity = 'email,idnumber'; 184 $this->setAdminUser(); 185 186 list($sort, $params) = users_order_by_sql('u', 'search', \context_system::instance()); 187 $this->assert_same_sql('CASE WHEN 188 ' . $DB->sql_fullname('u.firstname', 'u.lastname') . ' = :usersortexact1 OR 189 LOWER(u.firstname) = LOWER(:usersortexact2) OR 190 LOWER(u.lastname) = LOWER(:usersortexact3) OR 191 LOWER(u.email) = LOWER(:usersortexact4) OR 192 LOWER(u.idnumber) = LOWER(:usersortexact5) 193 THEN 0 ELSE 1 END, u.lastname, u.firstname, u.id', $sort); 194 $this->assertEquals(array('usersortexact1' => 'search', 'usersortexact2' => 'search', 195 'usersortexact3' => 'search', 'usersortexact4' => 'search', 'usersortexact5' => 'search'), $params); 196 } 197 198 public function test_users_order_by_sql_search_with_custom_fields(): void { 199 global $CFG, $DB; 200 $this->resetAfterTest(); 201 202 $CFG->showuseridentity = 'email,idnumber'; 203 $this->setAdminUser(); 204 205 list($sort, $params) = 206 users_order_by_sql('u', 'search', \context_system::instance(), ['profile_field_customfield' => 'x.customfield']); 207 $this->assert_same_sql('CASE WHEN 208 ' . $DB->sql_fullname('u.firstname', 'u.lastname') . ' = :usersortexact1 OR 209 LOWER(u.firstname) = LOWER(:usersortexact2) OR 210 LOWER(u.lastname) = LOWER(:usersortexact3) OR 211 LOWER(x.customfield) = LOWER(:usersortexact4) 212 THEN 0 ELSE 1 END, u.lastname, u.firstname, u.id', $sort); 213 $this->assertEquals(array('usersortexact1' => 'search', 'usersortexact2' => 'search', 214 'usersortexact3' => 'search', 'usersortexact4' => 'search'), $params); 215 } 216 217 public function test_get_admin() { 218 global $CFG, $DB; 219 $this->resetAfterTest(); 220 221 $this->assertSame('2', $CFG->siteadmins); // Admin always has id 2 in new installs. 222 $defaultadmin = get_admin(); 223 $this->assertEquals($defaultadmin->id, 2); 224 225 unset_config('siteadmins'); 226 $this->assertFalse(get_admin()); 227 228 set_config('siteadmins', -1); 229 $this->assertFalse(get_admin()); 230 231 $user1 = $this->getDataGenerator()->create_user(); 232 $user2 = $this->getDataGenerator()->create_user(); 233 234 set_config('siteadmins', $user1->id.','.$user2->id); 235 $admin = get_admin(); 236 $this->assertEquals($user1->id, $admin->id); 237 238 set_config('siteadmins', '-1,'.$user2->id.','.$user1->id); 239 $admin = get_admin(); 240 $this->assertEquals($user2->id, $admin->id); 241 242 $odlread = $DB->perf_get_reads(); 243 get_admin(); // No DB queries on repeated call expected. 244 get_admin(); 245 get_admin(); 246 $this->assertEquals($odlread, $DB->perf_get_reads()); 247 } 248 249 public function test_get_admins() { 250 global $CFG, $DB; 251 $this->resetAfterTest(); 252 253 $this->assertSame('2', $CFG->siteadmins); // Admin always has id 2 in new installs. 254 255 $user1 = $this->getDataGenerator()->create_user(); 256 $user2 = $this->getDataGenerator()->create_user(); 257 $user3 = $this->getDataGenerator()->create_user(); 258 $user4 = $this->getDataGenerator()->create_user(); 259 260 $admins = get_admins(); 261 $this->assertCount(1, $admins); 262 $admin = reset($admins); 263 $this->assertTrue(isset($admins[$admin->id])); 264 $this->assertEquals(2, $admin->id); 265 266 unset_config('siteadmins'); 267 $this->assertSame(array(), get_admins()); 268 269 set_config('siteadmins', -1); 270 $this->assertSame(array(), get_admins()); 271 272 set_config('siteadmins', '-1,'.$user2->id.','.$user1->id.','.$user3->id); 273 $this->assertEquals(array($user2->id=>$user2, $user1->id=>$user1, $user3->id=>$user3), get_admins()); 274 275 $odlread = $DB->perf_get_reads(); 276 get_admins(); // This should make just one query. 277 $this->assertEquals($odlread+1, $DB->perf_get_reads()); 278 } 279 280 public function test_get_course() { 281 global $DB, $PAGE, $SITE; 282 $this->resetAfterTest(); 283 284 // First test course will be current course ($COURSE). 285 $course1obj = $this->getDataGenerator()->create_course(array('shortname' => 'FROGS')); 286 $PAGE->set_course($course1obj); 287 288 // Second test course is not current course. 289 $course2obj = $this->getDataGenerator()->create_course(array('shortname' => 'ZOMBIES')); 290 291 // Check it does not make any queries when requesting the $COURSE/$SITE. 292 $before = $DB->perf_get_queries(); 293 $result = get_course($course1obj->id); 294 $this->assertEquals($before, $DB->perf_get_queries()); 295 $this->assertSame('FROGS', $result->shortname); 296 $result = get_course($SITE->id); 297 $this->assertEquals($before, $DB->perf_get_queries()); 298 299 // Check it makes 1 query to request other courses. 300 $result = get_course($course2obj->id); 301 $this->assertSame('ZOMBIES', $result->shortname); 302 $this->assertEquals($before + 1, $DB->perf_get_queries()); 303 } 304 305 /** 306 * Test that specifying fields when calling get_courses always returns required fields "id, category, visible" 307 */ 308 public function test_get_courses_with_fields(): void { 309 $this->resetAfterTest(); 310 311 $category = $this->getDataGenerator()->create_category(); 312 $course = $this->getDataGenerator()->create_course(['category' => $category->id]); 313 314 // Specify "id" only. 315 $courses = get_courses($category->id, 'c.sortorder', 'c.id'); 316 $this->assertCount(1, $courses); 317 $this->assertEquals((object) [ 318 'id' => $course->id, 319 'category' => $course->category, 320 'visible' => $course->visible, 321 ], reset($courses)); 322 323 // Specify some optional fields. 324 $courses = get_courses($category->id, 'c.sortorder', 'c.id, c.shortname, c.fullname'); 325 $this->assertCount(1, $courses); 326 $this->assertEquals((object) [ 327 'id' => $course->id, 328 'category' => $course->category, 329 'visible' => $course->visible, 330 'shortname' => $course->shortname, 331 'fullname' => $course->fullname, 332 ], reset($courses)); 333 } 334 335 public function test_increment_revision_number() { 336 global $DB; 337 $this->resetAfterTest(); 338 339 // Use one of the fields that are used with increment_revision_number(). 340 $course1 = $this->getDataGenerator()->create_course(); 341 $course2 = $this->getDataGenerator()->create_course(); 342 $DB->set_field('course', 'cacherev', 1, array()); 343 344 $record1 = $DB->get_record('course', array('id'=>$course1->id)); 345 $record2 = $DB->get_record('course', array('id'=>$course2->id)); 346 $this->assertEquals(1, $record1->cacherev); 347 $this->assertEquals(1, $record2->cacherev); 348 349 // Incrementing some lower value. 350 $this->setCurrentTimeStart(); 351 increment_revision_number('course', 'cacherev', 'id = :id', array('id'=>$course1->id)); 352 $record1 = $DB->get_record('course', array('id'=>$course1->id)); 353 $record2 = $DB->get_record('course', array('id'=>$course2->id)); 354 $this->assertTimeCurrent($record1->cacherev); 355 $this->assertEquals(1, $record2->cacherev); 356 357 // Incrementing in the same second. 358 $rev1 = $DB->get_field('course', 'cacherev', array('id'=>$course1->id)); 359 $now = time(); 360 $DB->set_field('course', 'cacherev', $now, array('id'=>$course1->id)); 361 increment_revision_number('course', 'cacherev', 'id = :id', array('id'=>$course1->id)); 362 $rev2 = $DB->get_field('course', 'cacherev', array('id'=>$course1->id)); 363 $this->assertGreaterThan($rev1, $rev2); 364 increment_revision_number('course', 'cacherev', 'id = :id', array('id'=>$course1->id)); 365 $rev3 = $DB->get_field('course', 'cacherev', array('id'=>$course1->id)); 366 $this->assertGreaterThan($rev2, $rev3); 367 $this->assertGreaterThan($now+1, $rev3); 368 increment_revision_number('course', 'cacherev', 'id = :id', array('id'=>$course1->id)); 369 $rev4 = $DB->get_field('course', 'cacherev', array('id'=>$course1->id)); 370 $this->assertGreaterThan($rev3, $rev4); 371 $this->assertGreaterThan($now+2, $rev4); 372 373 // Recovering from runaway revision. 374 $DB->set_field('course', 'cacherev', time()+60*60*60, array('id'=>$course2->id)); 375 $record2 = $DB->get_record('course', array('id'=>$course2->id)); 376 $this->assertGreaterThan(time(), $record2->cacherev); 377 $this->setCurrentTimeStart(); 378 increment_revision_number('course', 'cacherev', 'id = :id', array('id'=>$course2->id)); 379 $record2b = $DB->get_record('course', array('id'=>$course2->id)); 380 $this->assertTimeCurrent($record2b->cacherev); 381 382 // Update all revisions. 383 $DB->set_field('course', 'cacherev', 1, array()); 384 $this->setCurrentTimeStart(); 385 increment_revision_number('course', 'cacherev', ''); 386 $record1 = $DB->get_record('course', array('id'=>$course1->id)); 387 $record2 = $DB->get_record('course', array('id'=>$course2->id)); 388 $this->assertTimeCurrent($record1->cacherev); 389 $this->assertEquals($record1->cacherev, $record2->cacherev); 390 } 391 392 public function test_get_coursemodule_from_id() { 393 global $CFG; 394 395 $this->resetAfterTest(); 396 $this->setAdminUser(); // Some generators have bogus access control. 397 398 $this->assertFileExists("$CFG->dirroot/mod/folder/lib.php"); 399 $this->assertFileExists("$CFG->dirroot/mod/glossary/lib.php"); 400 401 $course1 = $this->getDataGenerator()->create_course(); 402 $course2 = $this->getDataGenerator()->create_course(); 403 404 $folder1a = $this->getDataGenerator()->create_module('folder', array('course' => $course1, 'section' => 3)); 405 $folder1b = $this->getDataGenerator()->create_module('folder', array('course' => $course1)); 406 $glossary1 = $this->getDataGenerator()->create_module('glossary', array('course' => $course1)); 407 408 $folder2 = $this->getDataGenerator()->create_module('folder', array('course' => $course2)); 409 410 $cm = get_coursemodule_from_id('folder', $folder1a->cmid); 411 $this->assertInstanceOf('stdClass', $cm); 412 $this->assertSame('folder', $cm->modname); 413 $this->assertSame($folder1a->id, $cm->instance); 414 $this->assertSame($folder1a->course, $cm->course); 415 $this->assertObjectNotHasAttribute('sectionnum', $cm); 416 417 $this->assertEquals($cm, get_coursemodule_from_id('', $folder1a->cmid)); 418 $this->assertEquals($cm, get_coursemodule_from_id('folder', $folder1a->cmid, $course1->id)); 419 $this->assertEquals($cm, get_coursemodule_from_id('folder', $folder1a->cmid, 0)); 420 $this->assertFalse(get_coursemodule_from_id('folder', $folder1a->cmid, -10)); 421 422 $cm2 = get_coursemodule_from_id('folder', $folder1a->cmid, 0, true); 423 $this->assertEquals(3, $cm2->sectionnum); 424 unset($cm2->sectionnum); 425 $this->assertEquals($cm, $cm2); 426 427 $this->assertFalse(get_coursemodule_from_id('folder', -11)); 428 429 try { 430 get_coursemodule_from_id('folder', -11, 0, false, MUST_EXIST); 431 $this->fail('dml_missing_record_exception expected'); 432 } catch (\moodle_exception $e) { 433 $this->assertInstanceOf('dml_missing_record_exception', $e); 434 } 435 436 try { 437 get_coursemodule_from_id('', -11, 0, false, MUST_EXIST); 438 $this->fail('dml_missing_record_exception expected'); 439 } catch (\moodle_exception $e) { 440 $this->assertInstanceOf('dml_missing_record_exception', $e); 441 } 442 443 try { 444 get_coursemodule_from_id('a b', $folder1a->cmid, 0, false, MUST_EXIST); 445 $this->fail('coding_exception expected'); 446 } catch (\moodle_exception $e) { 447 $this->assertInstanceOf('coding_exception', $e); 448 } 449 450 try { 451 get_coursemodule_from_id('abc', $folder1a->cmid, 0, false, MUST_EXIST); 452 $this->fail('dml_read_exception expected'); 453 } catch (\moodle_exception $e) { 454 $this->assertInstanceOf('dml_read_exception', $e); 455 } 456 } 457 458 public function test_get_coursemodule_from_instance() { 459 global $CFG; 460 461 $this->resetAfterTest(); 462 $this->setAdminUser(); // Some generators have bogus access control. 463 464 $this->assertFileExists("$CFG->dirroot/mod/folder/lib.php"); 465 $this->assertFileExists("$CFG->dirroot/mod/glossary/lib.php"); 466 467 $course1 = $this->getDataGenerator()->create_course(); 468 $course2 = $this->getDataGenerator()->create_course(); 469 470 $folder1a = $this->getDataGenerator()->create_module('folder', array('course' => $course1, 'section' => 3)); 471 $folder1b = $this->getDataGenerator()->create_module('folder', array('course' => $course1)); 472 473 $folder2 = $this->getDataGenerator()->create_module('folder', array('course' => $course2)); 474 475 $cm = get_coursemodule_from_instance('folder', $folder1a->id); 476 $this->assertInstanceOf('stdClass', $cm); 477 $this->assertSame('folder', $cm->modname); 478 $this->assertSame($folder1a->id, $cm->instance); 479 $this->assertSame($folder1a->course, $cm->course); 480 $this->assertObjectNotHasAttribute('sectionnum', $cm); 481 482 $this->assertEquals($cm, get_coursemodule_from_instance('folder', $folder1a->id, $course1->id)); 483 $this->assertEquals($cm, get_coursemodule_from_instance('folder', $folder1a->id, 0)); 484 $this->assertFalse(get_coursemodule_from_instance('folder', $folder1a->id, -10)); 485 486 $cm2 = get_coursemodule_from_instance('folder', $folder1a->id, 0, true); 487 $this->assertEquals(3, $cm2->sectionnum); 488 unset($cm2->sectionnum); 489 $this->assertEquals($cm, $cm2); 490 491 $this->assertFalse(get_coursemodule_from_instance('folder', -11)); 492 493 try { 494 get_coursemodule_from_instance('folder', -11, 0, false, MUST_EXIST); 495 $this->fail('dml_missing_record_exception expected'); 496 } catch (\moodle_exception $e) { 497 $this->assertInstanceOf('dml_missing_record_exception', $e); 498 } 499 500 try { 501 get_coursemodule_from_instance('a b', $folder1a->cmid, 0, false, MUST_EXIST); 502 $this->fail('coding_exception expected'); 503 } catch (\moodle_exception $e) { 504 $this->assertInstanceOf('coding_exception', $e); 505 } 506 507 try { 508 get_coursemodule_from_instance('', $folder1a->cmid, 0, false, MUST_EXIST); 509 $this->fail('coding_exception expected'); 510 } catch (\moodle_exception $e) { 511 $this->assertInstanceOf('coding_exception', $e); 512 } 513 514 try { 515 get_coursemodule_from_instance('abc', $folder1a->cmid, 0, false, MUST_EXIST); 516 $this->fail('dml_read_exception expected'); 517 } catch (\moodle_exception $e) { 518 $this->assertInstanceOf('dml_read_exception', $e); 519 } 520 } 521 522 public function test_get_coursemodules_in_course() { 523 global $CFG; 524 525 $this->resetAfterTest(); 526 $this->setAdminUser(); // Some generators have bogus access control. 527 528 $this->assertFileExists("$CFG->dirroot/mod/folder/lib.php"); 529 $this->assertFileExists("$CFG->dirroot/mod/glossary/lib.php"); 530 $this->assertFileExists("$CFG->dirroot/mod/label/lib.php"); 531 532 $course1 = $this->getDataGenerator()->create_course(); 533 $course2 = $this->getDataGenerator()->create_course(); 534 535 $folder1a = $this->getDataGenerator()->create_module('folder', array('course' => $course1, 'section' => 3)); 536 $folder1b = $this->getDataGenerator()->create_module('folder', array('course' => $course1)); 537 $glossary1 = $this->getDataGenerator()->create_module('glossary', array('course' => $course1)); 538 539 $folder2 = $this->getDataGenerator()->create_module('folder', array('course' => $course2)); 540 $glossary2a = $this->getDataGenerator()->create_module('glossary', array('course' => $course2)); 541 $glossary2b = $this->getDataGenerator()->create_module('glossary', array('course' => $course2)); 542 543 $modules = get_coursemodules_in_course('folder', $course1->id); 544 $this->assertCount(2, $modules); 545 546 $cm = $modules[$folder1a->cmid]; 547 $this->assertSame('folder', $cm->modname); 548 $this->assertSame($folder1a->id, $cm->instance); 549 $this->assertSame($folder1a->course, $cm->course); 550 $this->assertObjectNotHasAttribute('sectionnum', $cm); 551 $this->assertObjectNotHasAttribute('revision', $cm); 552 $this->assertObjectNotHasAttribute('display', $cm); 553 554 $cm = $modules[$folder1b->cmid]; 555 $this->assertSame('folder', $cm->modname); 556 $this->assertSame($folder1b->id, $cm->instance); 557 $this->assertSame($folder1b->course, $cm->course); 558 $this->assertObjectNotHasAttribute('sectionnum', $cm); 559 $this->assertObjectNotHasAttribute('revision', $cm); 560 $this->assertObjectNotHasAttribute('display', $cm); 561 562 $modules = get_coursemodules_in_course('folder', $course1->id, 'revision, display'); 563 $this->assertCount(2, $modules); 564 565 $cm = $modules[$folder1a->cmid]; 566 $this->assertSame('folder', $cm->modname); 567 $this->assertSame($folder1a->id, $cm->instance); 568 $this->assertSame($folder1a->course, $cm->course); 569 $this->assertObjectNotHasAttribute('sectionnum', $cm); 570 $this->assertObjectHasAttribute('revision', $cm); 571 $this->assertObjectHasAttribute('display', $cm); 572 573 $modules = get_coursemodules_in_course('label', $course1->id); 574 $this->assertCount(0, $modules); 575 576 try { 577 get_coursemodules_in_course('a b', $course1->id); 578 $this->fail('coding_exception expected'); 579 } catch (\moodle_exception $e) { 580 $this->assertInstanceOf('coding_exception', $e); 581 } 582 583 try { 584 get_coursemodules_in_course('abc', $course1->id); 585 $this->fail('dml_read_exception expected'); 586 } catch (\moodle_exception $e) { 587 $this->assertInstanceOf('dml_read_exception', $e); 588 } 589 } 590 591 public function test_get_all_instances_in_courses() { 592 global $CFG; 593 594 $this->resetAfterTest(); 595 $this->setAdminUser(); // Some generators have bogus access control. 596 597 $this->assertFileExists("$CFG->dirroot/mod/folder/lib.php"); 598 $this->assertFileExists("$CFG->dirroot/mod/glossary/lib.php"); 599 600 $course1 = $this->getDataGenerator()->create_course(); 601 $course2 = $this->getDataGenerator()->create_course(); 602 $course3 = $this->getDataGenerator()->create_course(); 603 604 $folder1a = $this->getDataGenerator()->create_module('folder', array('course' => $course1, 'section' => 3)); 605 $folder1b = $this->getDataGenerator()->create_module('folder', array('course' => $course1)); 606 $glossary1 = $this->getDataGenerator()->create_module('glossary', array('course' => $course1)); 607 608 $folder2 = $this->getDataGenerator()->create_module('folder', array('course' => $course2)); 609 $glossary2a = $this->getDataGenerator()->create_module('glossary', array('course' => $course2)); 610 $glossary2b = $this->getDataGenerator()->create_module('glossary', array('course' => $course2)); 611 612 $folder3 = $this->getDataGenerator()->create_module('folder', array('course' => $course3)); 613 614 $modules = get_all_instances_in_courses('folder', array($course1->id => $course1, $course2->id => $course2)); 615 $this->assertCount(3, $modules); 616 617 foreach ($modules as $cm) { 618 if ($folder1a->cmid == $cm->coursemodule) { 619 $folder = $folder1a; 620 } else if ($folder1b->cmid == $cm->coursemodule) { 621 $folder = $folder1b; 622 } else if ($folder2->cmid == $cm->coursemodule) { 623 $folder = $folder2; 624 } else { 625 $this->fail('Unexpected cm'. $cm->coursemodule); 626 } 627 $this->assertSame($folder->name, $cm->name); 628 $this->assertSame($folder->course, $cm->course); 629 } 630 631 try { 632 get_all_instances_in_courses('a b', array($course1->id => $course1, $course2->id => $course2)); 633 $this->fail('coding_exception expected'); 634 } catch (\moodle_exception $e) { 635 $this->assertInstanceOf('coding_exception', $e); 636 } 637 638 try { 639 get_all_instances_in_courses('', array($course1->id => $course1, $course2->id => $course2)); 640 $this->fail('coding_exception expected'); 641 } catch (\moodle_exception $e) { 642 $this->assertInstanceOf('coding_exception', $e); 643 } 644 } 645 646 public function test_get_all_instances_in_course() { 647 global $CFG; 648 649 $this->resetAfterTest(); 650 $this->setAdminUser(); // Some generators have bogus access control. 651 652 $this->assertFileExists("$CFG->dirroot/mod/folder/lib.php"); 653 $this->assertFileExists("$CFG->dirroot/mod/glossary/lib.php"); 654 655 $course1 = $this->getDataGenerator()->create_course(); 656 $course2 = $this->getDataGenerator()->create_course(); 657 $course3 = $this->getDataGenerator()->create_course(); 658 659 $folder1a = $this->getDataGenerator()->create_module('folder', array('course' => $course1, 'section' => 3)); 660 $folder1b = $this->getDataGenerator()->create_module('folder', array('course' => $course1)); 661 $glossary1 = $this->getDataGenerator()->create_module('glossary', array('course' => $course1)); 662 663 $folder2 = $this->getDataGenerator()->create_module('folder', array('course' => $course2)); 664 $glossary2a = $this->getDataGenerator()->create_module('glossary', array('course' => $course2)); 665 $glossary2b = $this->getDataGenerator()->create_module('glossary', array('course' => $course2)); 666 667 $folder3 = $this->getDataGenerator()->create_module('folder', array('course' => $course3)); 668 669 $modules = get_all_instances_in_course('folder', $course1); 670 $this->assertCount(2, $modules); 671 672 foreach ($modules as $cm) { 673 if ($folder1a->cmid == $cm->coursemodule) { 674 $folder = $folder1a; 675 } else if ($folder1b->cmid == $cm->coursemodule) { 676 $folder = $folder1b; 677 } else { 678 $this->fail('Unexpected cm'. $cm->coursemodule); 679 } 680 $this->assertSame($folder->name, $cm->name); 681 $this->assertSame($folder->course, $cm->course); 682 } 683 684 try { 685 get_all_instances_in_course('a b', $course1); 686 $this->fail('coding_exception expected'); 687 } catch (\moodle_exception $e) { 688 $this->assertInstanceOf('coding_exception', $e); 689 } 690 691 try { 692 get_all_instances_in_course('', $course1); 693 $this->fail('coding_exception expected'); 694 } catch (\moodle_exception $e) { 695 $this->assertInstanceOf('coding_exception', $e); 696 } 697 } 698 699 /** 700 * Test max courses in category 701 */ 702 public function test_max_courses_in_category() { 703 global $CFG; 704 $this->resetAfterTest(); 705 706 // Default settings. 707 $this->assertEquals(MAX_COURSES_IN_CATEGORY, get_max_courses_in_category()); 708 709 // Misc category. 710 $misc = \core_course_category::get_default(); 711 $this->assertEquals(MAX_COURSES_IN_CATEGORY, $misc->sortorder); 712 713 $category1 = $this->getDataGenerator()->create_category(); 714 $category2 = $this->getDataGenerator()->create_category(); 715 716 // Check category sort orders. 717 $this->assertEquals(MAX_COURSES_IN_CATEGORY, \core_course_category::get($misc->id)->sortorder); 718 $this->assertEquals(MAX_COURSES_IN_CATEGORY * 2, \core_course_category::get($category1->id)->sortorder); 719 $this->assertEquals(MAX_COURSES_IN_CATEGORY * 3, \core_course_category::get($category2->id)->sortorder); 720 721 // Create courses. 722 $course1 = $this->getDataGenerator()->create_course(['category' => $category1->id]); 723 $course2 = $this->getDataGenerator()->create_course(['category' => $category2->id]); 724 $course3 = $this->getDataGenerator()->create_course(['category' => $category1->id]); 725 $course4 = $this->getDataGenerator()->create_course(['category' => $category2->id]); 726 727 // Check course sort orders. 728 $this->assertEquals(MAX_COURSES_IN_CATEGORY * 2 + 2, get_course($course1->id)->sortorder); 729 $this->assertEquals(MAX_COURSES_IN_CATEGORY * 3 + 2, get_course($course2->id)->sortorder); 730 $this->assertEquals(MAX_COURSES_IN_CATEGORY * 2 + 1, get_course($course3->id)->sortorder); 731 $this->assertEquals(MAX_COURSES_IN_CATEGORY * 3 + 1, get_course($course4->id)->sortorder); 732 733 // Increase max course in category. 734 $CFG->maxcoursesincategory = 20000; 735 $this->assertEquals(20000, get_max_courses_in_category()); 736 737 // The sort order has not yet fixed, these sort orders should be the same as before. 738 // Categories. 739 $this->assertEquals(MAX_COURSES_IN_CATEGORY, \core_course_category::get($misc->id)->sortorder); 740 $this->assertEquals(MAX_COURSES_IN_CATEGORY * 2, \core_course_category::get($category1->id)->sortorder); 741 $this->assertEquals(MAX_COURSES_IN_CATEGORY * 3, \core_course_category::get($category2->id)->sortorder); 742 // Courses in category 1. 743 $this->assertEquals(MAX_COURSES_IN_CATEGORY * 2 + 2, get_course($course1->id)->sortorder); 744 $this->assertEquals(MAX_COURSES_IN_CATEGORY * 2 + 1, get_course($course3->id)->sortorder); 745 // Courses in category 2. 746 $this->assertEquals(MAX_COURSES_IN_CATEGORY * 3 + 2, get_course($course2->id)->sortorder); 747 $this->assertEquals(MAX_COURSES_IN_CATEGORY * 3 + 1, get_course($course4->id)->sortorder); 748 749 // Create new category so that the sort orders are applied. 750 $category3 = $this->getDataGenerator()->create_category(); 751 // Categories. 752 $this->assertEquals(20000, \core_course_category::get($misc->id)->sortorder); 753 $this->assertEquals(20000 * 2, \core_course_category::get($category1->id)->sortorder); 754 $this->assertEquals(20000 * 3, \core_course_category::get($category2->id)->sortorder); 755 $this->assertEquals(20000 * 4, \core_course_category::get($category3->id)->sortorder); 756 // Courses in category 1. 757 $this->assertEquals(20000 * 2 + 2, get_course($course1->id)->sortorder); 758 $this->assertEquals(20000 * 2 + 1, get_course($course3->id)->sortorder); 759 // Courses in category 2. 760 $this->assertEquals(20000 * 3 + 2, get_course($course2->id)->sortorder); 761 $this->assertEquals(20000 * 3 + 1, get_course($course4->id)->sortorder); 762 } 763 764 /** 765 * Test debug message for max courses in category 766 */ 767 public function test_debug_max_courses_in_category() { 768 global $CFG; 769 $this->resetAfterTest(); 770 771 // Set to small value so that we can check the debug message. 772 $CFG->maxcoursesincategory = 3; 773 $this->assertEquals(3, get_max_courses_in_category()); 774 775 $category1 = $this->getDataGenerator()->create_category(); 776 777 // There is only one course, no debug message. 778 $this->getDataGenerator()->create_course(['category' => $category1->id]); 779 $this->assertDebuggingNotCalled(); 780 // There are two courses, no debug message. 781 $this->getDataGenerator()->create_course(['category' => $category1->id]); 782 $this->assertDebuggingNotCalled(); 783 // There is debug message when number of courses reaches the maximum number. 784 $this->getDataGenerator()->create_course(['category' => $category1->id]); 785 $this->assertDebuggingCalled("The number of courses (category id: $category1->id) has reached max number of courses " . 786 "in a category (" . get_max_courses_in_category() . "). It will cause a sorting performance issue. " . 787 "Please set higher value for \$CFG->maxcoursesincategory in config.php. " . 788 "Please also make sure \$CFG->maxcoursesincategory * MAX_COURSE_CATEGORIES less than max integer. " . 789 "See tracker issues: MDL-25669 and MDL-69573"); 790 } 791 792 /** 793 * Tests the get_users_listing function. 794 */ 795 public function test_get_users_listing(): void { 796 global $DB; 797 798 $this->resetAfterTest(); 799 800 $generator = $this->getDataGenerator(); 801 802 // Set up profile field. 803 $generator->create_custom_profile_field(['datatype' => 'text', 804 'shortname' => 'specialid', 'name' => 'Special user id']); 805 806 // Set up the show user identity option. 807 set_config('showuseridentity', 'department,profile_field_specialid'); 808 809 // Get all the existing user ids (we're going to remove these from test results). 810 $existingids = array_fill_keys($DB->get_fieldset_select('user', 'id', '1 = 1'), true); 811 812 // Create some test user accounts. 813 $userids = []; 814 foreach (['a', 'b', 'c', 'd'] as $key) { 815 $record = [ 816 'username' => 'user_' . $key, 817 'firstname' => $key . '_first', 818 'lastname' => 'last_' . $key, 819 'department' => 'department_' . $key, 820 'profile_field_specialid' => 'special_' . $key, 821 'lastaccess' => ord($key) 822 ]; 823 $user = $generator->create_user($record); 824 $userids[] = $user->id; 825 } 826 827 // Check default result with no parameters. 828 $results = get_users_listing(); 829 $results = array_diff_key($results, $existingids); 830 831 // It should return all the results in order. 832 $this->assertEquals($userids, array_keys($results)); 833 834 // Results should have some general fields and name fields, check some samples. 835 $this->assertEquals('user_a', $results[$userids[0]]->username); 836 $this->assertEquals('user_a@example.com', $results[$userids[0]]->email); 837 $this->assertEquals(1, $results[$userids[0]]->confirmed); 838 $this->assertEquals('a_first', $results[$userids[0]]->firstname); 839 $this->assertObjectHasAttribute('firstnamephonetic', $results[$userids[0]]); 840 841 // Should not have the custom field or department because no context specified. 842 $this->assertObjectNotHasAttribute('department', $results[$userids[0]]); 843 $this->assertObjectNotHasAttribute('profile_field_specialid', $results[$userids[0]]); 844 845 // Check sorting. 846 $results = get_users_listing('username', 'DESC'); 847 $results = array_diff_key($results, $existingids); 848 $this->assertEquals([$userids[3], $userids[2], $userids[1], $userids[0]], array_keys($results)); 849 850 // Check default fallback sort field works as expected. 851 $results = get_users_listing('blah2', 'ASC'); 852 $results = array_diff_key($results, $existingids); 853 $this->assertEquals([$userids[0], $userids[1], $userids[2], $userids[3]], array_keys($results)); 854 855 // Check default fallback sort direction works as expected. 856 $results = get_users_listing('lastaccess', 'blah2'); 857 $results = array_diff_key($results, $existingids); 858 $this->assertEquals([$userids[0], $userids[1], $userids[2], $userids[3]], array_keys($results)); 859 860 // Add the options to showuseridentity and check it returns those fields but only if you 861 // specify a context AND have permissions. 862 $results = get_users_listing('lastaccess', 'asc', 0, 0, '', '', '', '', null, 863 \context_system::instance()); 864 $this->assertObjectNotHasAttribute('department', $results[$userids[0]]); 865 $this->assertObjectNotHasAttribute('profile_field_specialid', $results[$userids[0]]); 866 $this->setAdminUser(); 867 $results = get_users_listing('lastaccess', 'asc', 0, 0, '', '', '', '', null, 868 \context_system::instance()); 869 $this->assertEquals('department_a', $results[$userids[0]]->department); 870 $this->assertEquals('special_a', $results[$userids[0]]->profile_field_specialid); 871 872 // Check search (full name, email, username). 873 $results = get_users_listing('lastaccess', 'asc', 0, 0, 'b_first last_b'); 874 $this->assertEquals([$userids[1]], array_keys($results)); 875 $results = get_users_listing('lastaccess', 'asc', 0, 0, 'c@example'); 876 $this->assertEquals([$userids[2]], array_keys($results)); 877 $results = get_users_listing('lastaccess', 'asc', 0, 0, 'user_d'); 878 $this->assertEquals([$userids[3]], array_keys($results)); 879 880 // Check first and last initial restriction (all the test ones have same last initial). 881 $results = get_users_listing('lastaccess', 'asc', 0, 0, '', 'C'); 882 $this->assertEquals([$userids[2]], array_keys($results)); 883 $results = get_users_listing('lastaccess', 'asc', 0, 0, '', '', 'L'); 884 $results = array_diff_key($results, $existingids); 885 $this->assertEquals($userids, array_keys($results)); 886 887 // Check the extra where clause, either with the 'u.' prefix or not. 888 $results = get_users_listing('lastaccess', 'asc', 0, 0, '', '', '', 'id IN (:x,:y)', 889 ['x' => $userids[1], 'y' => $userids[3]]); 890 $results = array_diff_key($results, $existingids); 891 $this->assertEquals([$userids[1], $userids[3]], array_keys($results)); 892 $results = get_users_listing('lastaccess', 'asc', 0, 0, '', '', '', 'u.id IN (:x,:y)', 893 ['x' => $userids[1], 'y' => $userids[3]]); 894 $results = array_diff_key($results, $existingids); 895 $this->assertEquals([$userids[1], $userids[3]], array_keys($results)); 896 } 897 898 /** 899 * Data provider for test_get_safe_orderby(). 900 * 901 * @return array 902 */ 903 public function get_safe_orderby_provider(): array { 904 $orderbymap = [ 905 'courseid' => 'c.id', 906 'somecustomvalue' => 'c.startdate, c.shortname', 907 'default' => 'c.fullname', 908 ]; 909 $orderbymapnodefault = [ 910 'courseid' => 'c.id', 911 'somecustomvalue' => 'c.startdate, c.shortname', 912 ]; 913 914 return [ 915 'Valid option, no direction specified' => [ 916 $orderbymap, 917 'somecustomvalue', 918 '', 919 ' ORDER BY c.startdate, c.shortname', 920 ], 921 'Valid option, valid direction specified' => [ 922 $orderbymap, 923 'courseid', 924 'DESC', 925 ' ORDER BY c.id DESC', 926 ], 927 'Valid option, valid lowercase direction specified' => [ 928 $orderbymap, 929 'courseid', 930 'asc', 931 ' ORDER BY c.id ASC', 932 ], 933 'Valid option, invalid direction specified' => [ 934 $orderbymap, 935 'courseid', 936 'BOOP', 937 ' ORDER BY c.id', 938 ], 939 'Valid option, invalid lowercase direction specified' => [ 940 $orderbymap, 941 'courseid', 942 'boop', 943 ' ORDER BY c.id', 944 ], 945 'Invalid option default fallback, with valid direction' => [ 946 $orderbymap, 947 'thisdoesnotexist', 948 'ASC', 949 ' ORDER BY c.fullname ASC', 950 ], 951 'Invalid option default fallback, with invalid direction' => [ 952 $orderbymap, 953 'thisdoesnotexist', 954 'BOOP', 955 ' ORDER BY c.fullname', 956 ], 957 'Invalid option without default, with valid direction' => [ 958 $orderbymapnodefault, 959 'thisdoesnotexist', 960 'ASC', 961 '', 962 ], 963 'Invalid option without default, with invalid direction' => [ 964 $orderbymapnodefault, 965 'thisdoesnotexist', 966 'NOPE', 967 '', 968 ], 969 ]; 970 } 971 972 /** 973 * Tests the get_safe_orderby function. 974 * 975 * @dataProvider get_safe_orderby_provider 976 * @param array $orderbymap The ORDER BY parameter mapping array. 977 * @param string $orderbykey The string key being provided, to check against the map. 978 * @param string $direction The optional direction to order by. 979 * @param string $expected The expected string output of the method. 980 */ 981 public function test_get_safe_orderby(array $orderbymap, string $orderbykey, string $direction, string $expected): void { 982 $actual = get_safe_orderby($orderbymap, $orderbykey, $direction); 983 $this->assertEquals($expected, $actual); 984 } 985 986 /** 987 * Data provider for test_get_safe_orderby_multiple(). 988 * 989 * @return array 990 */ 991 public function get_safe_orderby_multiple_provider(): array { 992 $orderbymap = [ 993 'courseid' => 'c.id', 994 'firstname' => 'u.firstname', 995 'default' => 'c.startdate', 996 ]; 997 $orderbymapnodefault = [ 998 'courseid' => 'c.id', 999 'firstname' => 'u.firstname', 1000 ]; 1001 1002 return [ 1003 'Valid options, no directions specified' => [ 1004 $orderbymap, 1005 ['courseid', 'firstname'], 1006 [], 1007 ' ORDER BY c.id, u.firstname', 1008 ], 1009 'Valid options, some direction specified' => [ 1010 $orderbymap, 1011 ['courseid', 'firstname'], 1012 ['DESC'], 1013 ' ORDER BY c.id DESC, u.firstname', 1014 ], 1015 'Valid options, all directions specified' => [ 1016 $orderbymap, 1017 ['courseid', 'firstname'], 1018 ['ASC', 'desc'], 1019 ' ORDER BY c.id ASC, u.firstname DESC', 1020 ], 1021 'Valid options, valid and invalid directions specified' => [ 1022 $orderbymap, 1023 ['courseid', 'firstname'], 1024 ['BOOP', 'DESC'], 1025 ' ORDER BY c.id, u.firstname DESC', 1026 ], 1027 'Valid options, all invalid directions specified' => [ 1028 $orderbymap, 1029 ['courseid', 'firstname'], 1030 ['BOOP', 'SNOOT'], 1031 ' ORDER BY c.id, u.firstname', 1032 ], 1033 'Valid and invalid option default fallback, with valid directions' => [ 1034 $orderbymap, 1035 ['thisdoesnotexist', 'courseid'], 1036 ['asc', 'DESC'], 1037 ' ORDER BY c.startdate ASC, c.id DESC', 1038 ], 1039 'Valid and invalid option default fallback, with invalid direction' => [ 1040 $orderbymap, 1041 ['courseid', 'thisdoesnotexist'], 1042 ['BOOP', 'SNOOT'], 1043 ' ORDER BY c.id, c.startdate', 1044 ], 1045 'Valid and invalid option without default, with valid direction' => [ 1046 $orderbymapnodefault, 1047 ['thisdoesnotexist', 'courseid'], 1048 ['ASC', 'DESC'], 1049 ' ORDER BY c.id DESC', 1050 ], 1051 'Valid and invalid option without default, with invalid direction' => [ 1052 $orderbymapnodefault, 1053 ['thisdoesnotexist', 'courseid'], 1054 ['BOOP', 'SNOOT'], 1055 ' ORDER BY c.id', 1056 ], 1057 'Invalid option only without default, with valid direction' => [ 1058 $orderbymapnodefault, 1059 ['thisdoesnotexist'], 1060 ['ASC'], 1061 '', 1062 ], 1063 'Invalid option only without default, with invalid direction' => [ 1064 $orderbymapnodefault, 1065 ['thisdoesnotexist'], 1066 ['BOOP'], 1067 '', 1068 ], 1069 'Single valid option, direction specified' => [ 1070 $orderbymap, 1071 ['firstname'], 1072 ['ASC'], 1073 ' ORDER BY u.firstname ASC', 1074 ], 1075 'Single valid option, direction not specified' => [ 1076 $orderbymap, 1077 ['firstname'], 1078 [], 1079 ' ORDER BY u.firstname', 1080 ], 1081 ]; 1082 } 1083 1084 /** 1085 * Tests the get_safe_orderby_multiple function. 1086 * 1087 * @dataProvider get_safe_orderby_multiple_provider 1088 * @param array $orderbymap The ORDER BY parameter mapping array. 1089 * @param array $orderbykeys The array of string keys being provided, to check against the map. 1090 * @param array $directions The optional directions to order by. 1091 * @param string $expected The expected string output of the method. 1092 */ 1093 public function test_get_safe_orderby_multiple(array $orderbymap, array $orderbykeys, array $directions, 1094 string $expected): void { 1095 $actual = get_safe_orderby_multiple($orderbymap, $orderbykeys, $directions); 1096 $this->assertEquals($expected, $actual); 1097 } 1098 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body