Differences Between: [Versions 310 and 311] [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 * Unit tests for badges 19 * 20 * @package core 21 * @subpackage badges 22 * @copyright 2013 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/} 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 * @author Yuliya Bozhko <yuliya.bozhko@totaralms.com> 25 */ 26 27 defined('MOODLE_INTERNAL') || die(); 28 29 global $CFG; 30 require_once($CFG->libdir . '/badgeslib.php'); 31 require_once($CFG->dirroot . '/badges/lib.php'); 32 33 use core_badges\helper; 34 35 class badgeslib_test extends advanced_testcase { 36 protected $badgeid; 37 protected $course; 38 protected $user; 39 protected $module; 40 protected $coursebadge; 41 protected $assertion; 42 43 /** @var $assertion2 to define json format for Open badge version 2 */ 44 protected $assertion2; 45 46 protected function setUp(): void { 47 global $DB, $CFG; 48 $this->resetAfterTest(true); 49 $CFG->enablecompletion = true; 50 $user = $this->getDataGenerator()->create_user(); 51 $fordb = new stdClass(); 52 $fordb->id = null; 53 $fordb->name = "Test badge with 'apostrophe' and other friends (<>&@#)"; 54 $fordb->description = "Testing badges"; 55 $fordb->timecreated = time(); 56 $fordb->timemodified = time(); 57 $fordb->usercreated = $user->id; 58 $fordb->usermodified = $user->id; 59 $fordb->issuername = "Test issuer"; 60 $fordb->issuerurl = "http://issuer-url.domain.co.nz"; 61 $fordb->issuercontact = "issuer@example.com"; 62 $fordb->expiredate = null; 63 $fordb->expireperiod = null; 64 $fordb->type = BADGE_TYPE_SITE; 65 $fordb->version = 1; 66 $fordb->language = 'en'; 67 $fordb->courseid = null; 68 $fordb->messagesubject = "Test message subject"; 69 $fordb->message = "Test message body"; 70 $fordb->attachment = 1; 71 $fordb->notification = 0; 72 $fordb->imageauthorname = "Image Author 1"; 73 $fordb->imageauthoremail = "author@example.com"; 74 $fordb->imageauthorurl = "http://author-url.example.com"; 75 $fordb->imagecaption = "Test caption image"; 76 $fordb->status = BADGE_STATUS_INACTIVE; 77 78 $this->badgeid = $DB->insert_record('badge', $fordb, true); 79 80 // Set the default Issuer (because OBv2 needs them). 81 set_config('badges_defaultissuername', $fordb->issuername); 82 set_config('badges_defaultissuercontact', $fordb->issuercontact); 83 84 // Create a course with activity and auto completion tracking. 85 $this->course = $this->getDataGenerator()->create_course(array('enablecompletion' => true)); 86 $this->user = $this->getDataGenerator()->create_user(); 87 $studentrole = $DB->get_record('role', array('shortname' => 'student')); 88 $this->assertNotEmpty($studentrole); 89 90 // Get manual enrolment plugin and enrol user. 91 require_once($CFG->dirroot.'/enrol/manual/locallib.php'); 92 $manplugin = enrol_get_plugin('manual'); 93 $maninstance = $DB->get_record('enrol', array('courseid' => $this->course->id, 'enrol' => 'manual'), '*', MUST_EXIST); 94 $manplugin->enrol_user($maninstance, $this->user->id, $studentrole->id); 95 $this->assertEquals(1, $DB->count_records('user_enrolments')); 96 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC); 97 $this->module = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto); 98 99 // Build badge and criteria. 100 $fordb->type = BADGE_TYPE_COURSE; 101 $fordb->courseid = $this->course->id; 102 $fordb->status = BADGE_STATUS_ACTIVE; 103 $this->coursebadge = $DB->insert_record('badge', $fordb, true); 104 105 // Insert Endorsement. 106 $endorsement = new stdClass(); 107 $endorsement->badgeid = $this->coursebadge; 108 $endorsement->issuername = "Issuer 123"; 109 $endorsement->issueremail = "issuer123@email.com"; 110 $endorsement->issuerurl = "https://example.org/issuer-123"; 111 $endorsement->dateissued = 1524567747; 112 $endorsement->claimid = "https://example.org/robotics-badge.json"; 113 $endorsement->claimcomment = "Test endorser comment"; 114 $DB->insert_record('badge_endorsement', $endorsement, true); 115 116 // Insert related badges. 117 $badge = new badge($this->coursebadge); 118 $clonedid = $badge->make_clone(); 119 $badgeclone = new badge($clonedid); 120 $badgeclone->status = BADGE_STATUS_ACTIVE; 121 $badgeclone->save(); 122 123 $relatebadge = new stdClass(); 124 $relatebadge->badgeid = $this->coursebadge; 125 $relatebadge->relatedbadgeid = $clonedid; 126 $relatebadge->relatedid = $DB->insert_record('badge_related', $relatebadge, true); 127 128 // Insert a aligment. 129 $alignment = new stdClass(); 130 $alignment->badgeid = $this->coursebadge; 131 $alignment->targetname = 'CCSS.ELA-Literacy.RST.11-12.3'; 132 $alignment->targeturl = 'http://www.corestandards.org/ELA-Literacy/RST/11-12/3'; 133 $alignment->targetdescription = 'Test target description'; 134 $alignment->targetframework = 'CCSS.RST.11-12.3'; 135 $alignment->targetcode = 'CCSS.RST.11-12.3'; 136 $DB->insert_record('badge_alignment', $alignment, true); 137 138 $this->assertion = new stdClass(); 139 $this->assertion->badge = '{"uid":"%s","recipient":{"identity":"%s","type":"email","hashed":true,"salt":"%s"},"badge":"%s","verify":{"type":"hosted","url":"%s"},"issuedOn":"%d","evidence":"%s"}'; 140 $this->assertion->class = '{"name":"%s","description":"%s","image":"%s","criteria":"%s","issuer":"%s"}'; 141 $this->assertion->issuer = '{"name":"%s","url":"%s","email":"%s"}'; 142 // Format JSON-LD for Openbadge specification version 2.0. 143 $this->assertion2 = new stdClass(); 144 $this->assertion2->badge = '{"recipient":{"identity":"%s","type":"email","hashed":true,"salt":"%s"},' . 145 '"badge":{"name":"%s","description":"%s","image":"%s",' . 146 '"criteria":{"id":"%s","narrative":"%s"},"issuer":{"name":"%s","url":"%s","email":"%s",' . 147 '"@context":"https:\/\/w3id.org\/openbadges\/v2","id":"%s","type":"Issuer"},' . 148 '"@context":"https:\/\/w3id.org\/openbadges\/v2","id":"%s","type":"BadgeClass","version":"%s",' . 149 '"@language":"en","related":[{"id":"%s","version":"%s","@language":"%s"}],"endorsement":"%s",' . 150 '"alignments":[{"targetName":"%s","targetUrl":"%s","targetDescription":"%s","targetFramework":"%s",' . 151 '"targetCode":"%s"}]},"verify":{"type":"hosted","url":"%s"},"issuedOn":"%s","evidence":"%s",' . 152 '"@context":"https:\/\/w3id.org\/openbadges\/v2","type":"Assertion","id":"%s"}'; 153 154 $this->assertion2->class = '{"name":"%s","description":"%s","image":"%s",' . 155 '"criteria":{"id":"%s","narrative":"%s"},"issuer":{"name":"%s","url":"%s","email":"%s",' . 156 '"@context":"https:\/\/w3id.org\/openbadges\/v2","id":"%s","type":"Issuer"},' . 157 '"@context":"https:\/\/w3id.org\/openbadges\/v2","id":"%s","type":"BadgeClass","version":"%s",' . 158 '"@language":"%s","related":[{"id":"%s","version":"%s","@language":"%s"}],"endorsement":"%s",' . 159 '"alignments":[{"targetName":"%s","targetUrl":"%s","targetDescription":"%s","targetFramework":"%s",' . 160 '"targetCode":"%s"}]}'; 161 $this->assertion2->issuer = '{"name":"%s","url":"%s","email":"%s",' . 162 '"@context":"https:\/\/w3id.org\/openbadges\/v2","id":"%s","type":"Issuer"}'; 163 } 164 165 public function test_create_badge() { 166 $badge = new badge($this->badgeid); 167 168 $this->assertInstanceOf('badge', $badge); 169 $this->assertEquals($this->badgeid, $badge->id); 170 } 171 172 public function test_clone_badge() { 173 $badge = new badge($this->badgeid); 174 $newid = $badge->make_clone(); 175 $clonedbadge = new badge($newid); 176 177 $this->assertEquals($badge->description, $clonedbadge->description); 178 $this->assertEquals($badge->issuercontact, $clonedbadge->issuercontact); 179 $this->assertEquals($badge->issuername, $clonedbadge->issuername); 180 $this->assertEquals($badge->issuercontact, $clonedbadge->issuercontact); 181 $this->assertEquals($badge->issuerurl, $clonedbadge->issuerurl); 182 $this->assertEquals($badge->expiredate, $clonedbadge->expiredate); 183 $this->assertEquals($badge->expireperiod, $clonedbadge->expireperiod); 184 $this->assertEquals($badge->type, $clonedbadge->type); 185 $this->assertEquals($badge->courseid, $clonedbadge->courseid); 186 $this->assertEquals($badge->message, $clonedbadge->message); 187 $this->assertEquals($badge->messagesubject, $clonedbadge->messagesubject); 188 $this->assertEquals($badge->attachment, $clonedbadge->attachment); 189 $this->assertEquals($badge->notification, $clonedbadge->notification); 190 $this->assertEquals($badge->version, $clonedbadge->version); 191 $this->assertEquals($badge->language, $clonedbadge->language); 192 $this->assertEquals($badge->imagecaption, $clonedbadge->imagecaption); 193 $this->assertEquals($badge->imageauthorname, $clonedbadge->imageauthorname); 194 $this->assertEquals($badge->imageauthoremail, $clonedbadge->imageauthoremail); 195 $this->assertEquals($badge->imageauthorurl, $clonedbadge->imageauthorurl); 196 } 197 198 public function test_badge_status() { 199 $badge = new badge($this->badgeid); 200 $old_status = $badge->status; 201 $badge->set_status(BADGE_STATUS_ACTIVE); 202 $this->assertNotEquals($old_status, $badge->status); 203 $this->assertEquals(BADGE_STATUS_ACTIVE, $badge->status); 204 } 205 206 public function test_delete_badge() { 207 $badge = new badge($this->badgeid); 208 $badge->delete(); 209 // We don't actually delete badges. We archive them. 210 $this->assertEquals(BADGE_STATUS_ARCHIVED, $badge->status); 211 } 212 213 /** 214 * Really delete the badge. 215 */ 216 public function test_delete_badge_for_real() { 217 global $DB; 218 219 $badge = new badge($this->badgeid); 220 221 $newid1 = $badge->make_clone(); 222 $newid2 = $badge->make_clone(); 223 $newid3 = $badge->make_clone(); 224 225 // Insert related badges to badge 1. 226 $badge->add_related_badges([$newid1, $newid2, $newid3]); 227 228 // Another badge. 229 $badge2 = new badge($newid2); 230 // Make badge 1 related for badge 2. 231 $badge2->add_related_badges([$this->badgeid]); 232 233 // Confirm that the records about this badge about its relations have been removed as well. 234 $relatedsql = 'badgeid = :badgeid OR relatedbadgeid = :relatedbadgeid'; 235 $relatedparams = array( 236 'badgeid' => $this->badgeid, 237 'relatedbadgeid' => $this->badgeid 238 ); 239 // Badge 1 has 4 related records. 3 where it's the badgeid, 1 where it's the relatedbadgeid. 240 $this->assertEquals(4, $DB->count_records_select('badge_related', $relatedsql, $relatedparams)); 241 242 // Delete the badge for real. 243 $badge->delete(false); 244 245 // Confirm that the badge itself has been removed. 246 $this->assertFalse($DB->record_exists('badge', ['id' => $this->badgeid])); 247 248 // Confirm that the records about this badge about its relations have been removed as well. 249 $this->assertFalse($DB->record_exists_select('badge_related', $relatedsql, $relatedparams)); 250 } 251 252 public function test_create_badge_criteria() { 253 $badge = new badge($this->badgeid); 254 $criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id)); 255 $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ALL)); 256 257 $this->assertCount(1, $badge->get_criteria()); 258 259 $criteria_profile = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_PROFILE, 'badgeid' => $badge->id)); 260 $params = array('agg' => BADGE_CRITERIA_AGGREGATION_ALL, 'field_address' => 'address'); 261 $criteria_profile->save($params); 262 263 $this->assertCount(2, $badge->get_criteria()); 264 } 265 266 public function test_add_badge_criteria_description() { 267 $criteriaoverall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $this->badgeid)); 268 $criteriaoverall->save(array( 269 'agg' => BADGE_CRITERIA_AGGREGATION_ALL, 270 'description' => 'Overall description', 271 'descriptionformat' => FORMAT_HTML 272 )); 273 274 $criteriaprofile = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_PROFILE, 'badgeid' => $this->badgeid)); 275 $params = array( 276 'agg' => BADGE_CRITERIA_AGGREGATION_ALL, 277 'field_address' => 'address', 278 'description' => 'Description', 279 'descriptionformat' => FORMAT_HTML 280 ); 281 $criteriaprofile->save($params); 282 283 $badge = new badge($this->badgeid); 284 $this->assertEquals('Overall description', $badge->criteria[BADGE_CRITERIA_TYPE_OVERALL]->description); 285 $this->assertEquals('Description', $badge->criteria[BADGE_CRITERIA_TYPE_PROFILE]->description); 286 } 287 288 public function test_delete_badge_criteria() { 289 $criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $this->badgeid)); 290 $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ALL)); 291 $badge = new badge($this->badgeid); 292 293 $this->assertInstanceOf('award_criteria_overall', $badge->criteria[BADGE_CRITERIA_TYPE_OVERALL]); 294 295 $badge->criteria[BADGE_CRITERIA_TYPE_OVERALL]->delete(); 296 $this->assertEmpty($badge->get_criteria()); 297 } 298 299 public function test_badge_awards() { 300 global $DB; 301 $this->preventResetByRollback(); // Messaging is not compatible with transactions. 302 $badge = new badge($this->badgeid); 303 $user1 = $this->getDataGenerator()->create_user(); 304 305 $sink = $this->redirectMessages(); 306 307 $DB->set_field_select('message_processors', 'enabled', 0, "name <> 'email'"); 308 set_user_preference('message_provider_moodle_badgerecipientnotice_loggedoff', 'email', $user1); 309 310 $badge->issue($user1->id, false); 311 $this->assertDebuggingCalled(); // Expect debugging while baking a badge via phpunit. 312 $this->assertTrue($badge->is_issued($user1->id)); 313 314 $messages = $sink->get_messages(); 315 $sink->close(); 316 $this->assertCount(1, $messages); 317 $message = array_pop($messages); 318 // Check we have the expected data. 319 $customdata = json_decode($message->customdata); 320 $this->assertObjectHasAttribute('notificationiconurl', $customdata); 321 $this->assertObjectHasAttribute('hash', $customdata); 322 323 $user2 = $this->getDataGenerator()->create_user(); 324 $badge->issue($user2->id, true); 325 $this->assertTrue($badge->is_issued($user2->id)); 326 327 $this->assertCount(2, $badge->get_awards()); 328 } 329 330 /** 331 * Test the {@link badges_get_user_badges()} function in lib/badgeslib.php 332 */ 333 public function test_badges_get_user_badges() { 334 global $DB; 335 336 // Messaging is not compatible with transactions. 337 $this->preventResetByRollback(); 338 339 $badges = array(); 340 $user1 = $this->getDataGenerator()->create_user(); 341 $user2 = $this->getDataGenerator()->create_user(); 342 343 // Record the current time, we need to be precise about a couple of things. 344 $now = time(); 345 // Create 11 badges with which to test. 346 for ($i = 1; $i <= 11; $i++) { 347 // Mock up a badge. 348 $badge = new stdClass(); 349 $badge->id = null; 350 $badge->name = "Test badge $i"; 351 $badge->description = "Testing badges $i"; 352 $badge->timecreated = $now - 12; 353 $badge->timemodified = $now - 12; 354 $badge->usercreated = $user1->id; 355 $badge->usermodified = $user1->id; 356 $badge->issuername = "Test issuer"; 357 $badge->issuerurl = "http://issuer-url.domain.co.nz"; 358 $badge->issuercontact = "issuer@example.com"; 359 $badge->expiredate = null; 360 $badge->expireperiod = null; 361 $badge->type = BADGE_TYPE_SITE; 362 $badge->courseid = null; 363 $badge->messagesubject = "Test message subject for badge $i"; 364 $badge->message = "Test message body for badge $i"; 365 $badge->attachment = 1; 366 $badge->notification = 0; 367 $badge->status = BADGE_STATUS_INACTIVE; 368 $badge->version = "Version $i"; 369 $badge->language = "en"; 370 $badge->imagecaption = "Image caption $i"; 371 $badge->imageauthorname = "Image author's name $i"; 372 $badge->imageauthoremail = "author$i@example.com"; 373 $badge->imageauthorname = "Image author's name $i"; 374 375 $badgeid = $DB->insert_record('badge', $badge, true); 376 $badges[$badgeid] = new badge($badgeid); 377 $badges[$badgeid]->issue($user2->id, true); 378 // Check it all actually worked. 379 $this->assertCount(1, $badges[$badgeid]->get_awards()); 380 381 // Hack the database to adjust the time each badge was issued. 382 // The alternative to this is sleep which is a no-no in unit tests. 383 $DB->set_field('badge_issued', 'dateissued', $now - 11 + $i, array('userid' => $user2->id, 'badgeid' => $badgeid)); 384 } 385 386 // Make sure the first user has no badges. 387 $result = badges_get_user_badges($user1->id); 388 $this->assertIsArray($result); 389 $this->assertCount(0, $result); 390 391 // Check that the second user has the expected 11 badges. 392 $result = badges_get_user_badges($user2->id); 393 $this->assertCount(11, $result); 394 395 // Test pagination. 396 // Ordering is by time issued desc, so things will come out with the last awarded badge first. 397 $result = badges_get_user_badges($user2->id, 0, 0, 4); 398 $this->assertCount(4, $result); 399 $lastbadgeissued = reset($result); 400 $this->assertSame('Test badge 11', $lastbadgeissued->name); 401 // Page 2. Expecting 4 results again. 402 $result = badges_get_user_badges($user2->id, 0, 1, 4); 403 $this->assertCount(4, $result); 404 $lastbadgeissued = reset($result); 405 $this->assertSame('Test badge 7', $lastbadgeissued->name); 406 // Page 3. Expecting just three results here. 407 $result = badges_get_user_badges($user2->id, 0, 2, 4); 408 $this->assertCount(3, $result); 409 $lastbadgeissued = reset($result); 410 $this->assertSame('Test badge 3', $lastbadgeissued->name); 411 // Page 4.... there is no page 4. 412 $result = badges_get_user_badges($user2->id, 0, 3, 4); 413 $this->assertCount(0, $result); 414 415 // Test search. 416 $result = badges_get_user_badges($user2->id, 0, 0, 0, 'badge 1'); 417 $this->assertCount(3, $result); 418 $lastbadgeissued = reset($result); 419 $this->assertSame('Test badge 11', $lastbadgeissued->name); 420 // The term Totara doesn't appear anywhere in the badges. 421 $result = badges_get_user_badges($user2->id, 0, 0, 0, 'Totara'); 422 $this->assertCount(0, $result); 423 424 // Issue a user with a course badge and verify its returned based on if 425 // coursebadges are enabled or disabled. 426 $sitebadgeid = key($badges); 427 $badges[$sitebadgeid]->issue($this->user->id, true); 428 429 $badge = new stdClass(); 430 $badge->id = null; 431 $badge->name = "Test course badge"; 432 $badge->description = "Testing course badge"; 433 $badge->timecreated = $now; 434 $badge->timemodified = $now; 435 $badge->usercreated = $user1->id; 436 $badge->usermodified = $user1->id; 437 $badge->issuername = "Test issuer"; 438 $badge->issuerurl = "http://issuer-url.domain.co.nz"; 439 $badge->issuercontact = "issuer@example.com"; 440 $badge->expiredate = null; 441 $badge->expireperiod = null; 442 $badge->type = BADGE_TYPE_COURSE; 443 $badge->courseid = $this->course->id; 444 $badge->messagesubject = "Test message subject for course badge"; 445 $badge->message = "Test message body for course badge"; 446 $badge->attachment = 1; 447 $badge->notification = 0; 448 $badge->status = BADGE_STATUS_ACTIVE; 449 $badge->version = "Version $i"; 450 $badge->language = "en"; 451 $badge->imagecaption = "Image caption"; 452 $badge->imageauthorname = "Image author's name"; 453 $badge->imageauthoremail = "author@example.com"; 454 $badge->imageauthorname = "Image author's name"; 455 456 $badgeid = $DB->insert_record('badge', $badge, true); 457 $badges[$badgeid] = new badge($badgeid); 458 $badges[$badgeid]->issue($this->user->id, true); 459 460 // With coursebadges off, we should only get the site badge. 461 set_config('badges_allowcoursebadges', false); 462 $result = badges_get_user_badges($this->user->id); 463 $this->assertCount(1, $result); 464 465 // With it on, we should get both. 466 set_config('badges_allowcoursebadges', true); 467 $result = badges_get_user_badges($this->user->id); 468 $this->assertCount(2, $result); 469 470 } 471 472 public function data_for_message_from_template() { 473 return array( 474 array( 475 'This is a message with no variables', 476 array(), // no params 477 'This is a message with no variables' 478 ), 479 array( 480 'This is a message with %amissing% variables', 481 array(), // no params 482 'This is a message with %amissing% variables' 483 ), 484 array( 485 'This is a message with %one% variable', 486 array('one' => 'a single'), 487 'This is a message with a single variable' 488 ), 489 array( 490 'This is a message with %one% %two% %three% variables', 491 array('one' => 'more', 'two' => 'than', 'three' => 'one'), 492 'This is a message with more than one variables' 493 ), 494 array( 495 'This is a message with %three% %two% %one%', 496 array('one' => 'variables', 'two' => 'ordered', 'three' => 'randomly'), 497 'This is a message with randomly ordered variables' 498 ), 499 array( 500 'This is a message with %repeated% %one% %repeated% of variables', 501 array('one' => 'and', 'repeated' => 'lots'), 502 'This is a message with lots and lots of variables' 503 ), 504 ); 505 } 506 507 /** 508 * @dataProvider data_for_message_from_template 509 */ 510 public function test_badge_message_from_template($message, $params, $result) { 511 $this->assertEquals(badge_message_from_template($message, $params), $result); 512 } 513 514 /** 515 * Test badges observer when course module completion event id fired. 516 */ 517 public function test_badges_observer_course_module_criteria_review() { 518 $this->preventResetByRollback(); // Messaging is not compatible with transactions. 519 $badge = new badge($this->coursebadge); 520 $this->assertFalse($badge->is_issued($this->user->id)); 521 522 $criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id)); 523 $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY)); 524 $criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_ACTIVITY, 'badgeid' => $badge->id)); 525 $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY, 'module_'.$this->module->cmid => $this->module->cmid)); 526 527 // Assert the badge will not be issued to the user as is. 528 $badge = new badge($this->coursebadge); 529 $badge->review_all_criteria(); 530 $this->assertFalse($badge->is_issued($this->user->id)); 531 532 // Set completion for forum activity. 533 $c = new completion_info($this->course); 534 $activities = $c->get_activities(); 535 $this->assertEquals(1, count($activities)); 536 $this->assertTrue(isset($activities[$this->module->cmid])); 537 $this->assertEquals($activities[$this->module->cmid]->name, $this->module->name); 538 539 $current = $c->get_data($activities[$this->module->cmid], false, $this->user->id); 540 $current->completionstate = COMPLETION_COMPLETE; 541 $current->timemodified = time(); 542 $sink = $this->redirectEmails(); 543 $c->internal_set_data($activities[$this->module->cmid], $current); 544 $this->assertCount(1, $sink->get_messages()); 545 $sink->close(); 546 547 // Check if badge is awarded. 548 $this->assertDebuggingCalled('Error baking badge image!'); 549 $this->assertTrue($badge->is_issued($this->user->id)); 550 } 551 552 /** 553 * Test badges observer when course_completed event is fired. 554 */ 555 public function test_badges_observer_course_criteria_review() { 556 $this->preventResetByRollback(); // Messaging is not compatible with transactions. 557 $badge = new badge($this->coursebadge); 558 $this->assertFalse($badge->is_issued($this->user->id)); 559 560 $criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id)); 561 $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY)); 562 $criteria_overall1 = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_COURSE, 'badgeid' => $badge->id)); 563 $criteria_overall1->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY, 'course_'.$this->course->id => $this->course->id)); 564 565 $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id)); 566 567 // Assert the badge will not be issued to the user as is. 568 $badge = new badge($this->coursebadge); 569 $badge->review_all_criteria(); 570 $this->assertFalse($badge->is_issued($this->user->id)); 571 572 // Mark course as complete. 573 $sink = $this->redirectMessages(); 574 $ccompletion->mark_complete(); 575 // Two messages are generated: One for the course completed and the other one for the badge awarded. 576 $messages = $sink->get_messages(); 577 $this->assertCount(2, $messages); 578 $this->assertEquals('badgerecipientnotice', $messages[0]->eventtype); 579 $this->assertEquals('coursecompleted', $messages[1]->eventtype); 580 $sink->close(); 581 582 // Check if badge is awarded. 583 $this->assertDebuggingCalled('Error baking badge image!'); 584 $this->assertTrue($badge->is_issued($this->user->id)); 585 } 586 587 /** 588 * Test badges observer when user_updated event is fired. 589 */ 590 public function test_badges_observer_profile_criteria_review() { 591 global $CFG, $DB; 592 require_once($CFG->dirroot.'/user/profile/lib.php'); 593 594 // Add a custom field of textarea type. 595 $customprofileid = $this->getDataGenerator()->create_custom_profile_field(array( 596 'shortname' => 'newfield', 'name' => 'Description of new field', 597 'datatype' => 'textarea'))->id; 598 599 $this->preventResetByRollback(); // Messaging is not compatible with transactions. 600 $badge = new badge($this->coursebadge); 601 602 $criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id)); 603 $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY)); 604 $criteria_overall1 = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_PROFILE, 'badgeid' => $badge->id)); 605 $criteria_overall1->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ALL, 'field_address' => 'address', 606 'field_department' => 'department', 'field_' . $customprofileid => $customprofileid)); 607 608 // Assert the badge will not be issued to the user as is. 609 $badge = new badge($this->coursebadge); 610 $badge->review_all_criteria(); 611 $this->assertFalse($badge->is_issued($this->user->id)); 612 613 // Set the required fields and make sure the badge got issued. 614 $this->user->address = 'Test address'; 615 $this->user->department = 'sillywalks'; 616 $sink = $this->redirectEmails(); 617 profile_save_data((object)array('id' => $this->user->id, 'profile_field_newfield' => 'X')); 618 user_update_user($this->user, false); 619 $this->assertCount(1, $sink->get_messages()); 620 $sink->close(); 621 // Check if badge is awarded. 622 $this->assertDebuggingCalled('Error baking badge image!'); 623 $this->assertTrue($badge->is_issued($this->user->id)); 624 } 625 626 /** 627 * Test badges observer when cohort_member_added event is fired and user required to belong to any cohort. 628 * 629 * @covers \award_criteria_cohort 630 */ 631 public function test_badges_observer_any_cohort_criteria_review() { 632 global $CFG; 633 634 require_once("$CFG->dirroot/cohort/lib.php"); 635 636 $cohort1 = $this->getDataGenerator()->create_cohort(); 637 $cohort2 = $this->getDataGenerator()->create_cohort(); 638 639 $this->preventResetByRollback(); // Messaging is not compatible with transactions. 640 641 $badge = new badge($this->badgeid); 642 $this->assertFalse($badge->is_issued($this->user->id)); 643 $this->assertSame(0, $badge->review_all_criteria()); // Verify award_criteria_cohort->get_completed_criteria_sql(). 644 645 // Set up the badge criteria. 646 $criteriaoverall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id)); 647 $criteriaoverall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY)); 648 $criteriaoverall1 = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_COHORT, 'badgeid' => $badge->id)); 649 $criteriaoverall1->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY, 650 'cohort_cohorts' => array('0' => $cohort1->id, '1' => $cohort2->id))); 651 $badge->set_status(BADGE_STATUS_ACTIVE); 652 653 // Reload it to contain criteria. 654 $badge = new badge($this->badgeid); 655 $this->assertFalse($badge->is_issued($this->user->id)); 656 $this->assertSame(0, $badge->review_all_criteria()); // Verify award_criteria_cohort->get_completed_criteria_sql(). 657 658 // Add the user to the cohort. 659 cohort_add_member($cohort2->id, $this->user->id); 660 $this->assertDebuggingCalled(); 661 662 // Verify that the badge was awarded. 663 $this->assertTrue($badge->is_issued($this->user->id)); 664 // As the badge has been awarded to user because core_badges_observer been called when the member has been added to the 665 // cohort, there are no other users that can award this badge. 666 $this->assertSame(0, $badge->review_all_criteria()); // Verify award_criteria_cohort->get_completed_criteria_sql(). 667 } 668 669 /** 670 * Test badges observer when cohort_member_added event is fired and user required to belong to multiple (all) cohorts. 671 * 672 * @covers \award_criteria_cohort 673 */ 674 public function test_badges_observer_all_cohort_criteria_review() { 675 global $CFG; 676 677 require_once("$CFG->dirroot/cohort/lib.php"); 678 679 $cohort1 = $this->getDataGenerator()->create_cohort(); 680 $cohort2 = $this->getDataGenerator()->create_cohort(); 681 $cohort3 = $this->getDataGenerator()->create_cohort(); 682 683 // Add user2 to cohort1 and cohort3. 684 $user2 = $this->getDataGenerator()->create_user(); 685 cohort_add_member($cohort3->id, $user2->id); 686 cohort_add_member($cohort1->id, $user2->id); 687 688 // Add user3 to cohort1, cohort2 and cohort3. 689 $user3 = $this->getDataGenerator()->create_user(); 690 cohort_add_member($cohort1->id, $user3->id); 691 cohort_add_member($cohort2->id, $user3->id); 692 cohort_add_member($cohort3->id, $user3->id); 693 694 $this->preventResetByRollback(); // Messaging is not compatible with transactions. 695 696 // Cohort criteria are used in site badges. 697 $badge = new badge($this->badgeid); 698 699 $this->assertFalse($badge->is_issued($this->user->id)); 700 $this->assertSame(0, $badge->review_all_criteria()); // Verify award_criteria_cohort->get_completed_criteria_sql(). 701 702 // Set up the badge criteria. 703 $criteriaoverall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id)); 704 $criteriaoverall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY)); 705 $criteriaoverall1 = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_COHORT, 'badgeid' => $badge->id)); 706 $criteriaoverall1->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ALL, 707 'cohort_cohorts' => array('0' => $cohort1->id, '1' => $cohort2->id, '2' => $cohort3->id))); 708 $badge->set_status(BADGE_STATUS_ACTIVE); 709 710 // Reload it to contain criteria. 711 $badge = new badge($this->badgeid); 712 713 // Verify that the badge was not awarded yet (ALL cohorts are needed and review_all_criteria has to be called). 714 $this->assertFalse($badge->is_issued($this->user->id)); 715 $this->assertFalse($badge->is_issued($user2->id)); 716 $this->assertFalse($badge->is_issued($user3->id)); 717 718 // Verify that after calling review_all_criteria, users with the criteria (user3) award the badge instantly. 719 $this->assertSame(1, $badge->review_all_criteria()); // Verify award_criteria_cohort->get_completed_criteria_sql(). 720 $this->assertFalse($badge->is_issued($this->user->id)); 721 $this->assertFalse($badge->is_issued($user2->id)); 722 $this->assertTrue($badge->is_issued($user3->id)); 723 $this->assertDebuggingCalled(); 724 725 // Add the user to the cohort1. 726 cohort_add_member($cohort1->id, $this->user->id); 727 728 // Verify that the badge was not awarded yet (ALL cohorts are needed). 729 $this->assertFalse($badge->is_issued($this->user->id)); 730 $this->assertSame(0, $badge->review_all_criteria()); // Verify award_criteria_cohort->get_completed_criteria_sql(). 731 732 // Add the user to the cohort3. 733 cohort_add_member($cohort3->id, $this->user->id); 734 735 // Verify that the badge was not awarded yet (ALL cohorts are needed). 736 $this->assertFalse($badge->is_issued($this->user->id)); 737 $this->assertSame(0, $badge->review_all_criteria()); // Verify award_criteria_cohort->get_completed_criteria_sql(). 738 739 // Add user to cohort2. 740 cohort_add_member($cohort2->id, $this->user->id); 741 $this->assertDebuggingCalled(); 742 743 // Verify that the badge was awarded (ALL cohorts). 744 $this->assertTrue($badge->is_issued($this->user->id)); 745 // As the badge has been awarded to user because core_badges_observer been called when the member has been added to the 746 // cohort, there are no other users that can award this badge. 747 $this->assertSame(0, $badge->review_all_criteria()); // Verify award_criteria_cohort->get_completed_criteria_sql(). 748 } 749 750 /** 751 * Test badges assertion generated when a badge is issued. 752 */ 753 public function test_badges_assertion() { 754 $this->preventResetByRollback(); // Messaging is not compatible with transactions. 755 $badge = new badge($this->coursebadge); 756 $this->assertFalse($badge->is_issued($this->user->id)); 757 758 $criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id)); 759 $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY)); 760 $criteria_overall1 = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_PROFILE, 'badgeid' => $badge->id)); 761 $criteria_overall1->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ALL, 'field_address' => 'address')); 762 763 $this->user->address = 'Test address'; 764 $sink = $this->redirectEmails(); 765 user_update_user($this->user, false); 766 $this->assertCount(1, $sink->get_messages()); 767 $sink->close(); 768 // Check if badge is awarded. 769 $this->assertDebuggingCalled('Error baking badge image!'); 770 $awards = $badge->get_awards(); 771 $this->assertCount(1, $awards); 772 773 // Get assertion. 774 $award = reset($awards); 775 $assertion = new core_badges_assertion($award->uniquehash, OPEN_BADGES_V1); 776 $testassertion = $this->assertion; 777 778 // Make sure JSON strings have the same structure. 779 $this->assertStringMatchesFormat($testassertion->badge, json_encode($assertion->get_badge_assertion())); 780 $this->assertStringMatchesFormat($testassertion->class, json_encode($assertion->get_badge_class())); 781 $this->assertStringMatchesFormat($testassertion->issuer, json_encode($assertion->get_issuer())); 782 783 // Test Openbadge specification version 2. 784 // Get assertion version 2. 785 $award = reset($awards); 786 $assertion2 = new core_badges_assertion($award->uniquehash, OPEN_BADGES_V2); 787 $testassertion2 = $this->assertion2; 788 789 // Make sure JSON strings have the same structure. 790 $this->assertStringMatchesFormat($testassertion2->badge, json_encode($assertion2->get_badge_assertion())); 791 $this->assertStringMatchesFormat($testassertion2->class, json_encode($assertion2->get_badge_class())); 792 $this->assertStringMatchesFormat($testassertion2->issuer, json_encode($assertion2->get_issuer())); 793 794 // Test Openbadge specification version 2.1. It has the same format as OBv2.0. 795 // Get assertion version 2.1. 796 $award = reset($awards); 797 $assertion2 = new core_badges_assertion($award->uniquehash, OPEN_BADGES_V2P1); 798 799 // Make sure JSON strings have the same structure. 800 $this->assertStringMatchesFormat($testassertion2->badge, json_encode($assertion2->get_badge_assertion())); 801 $this->assertStringMatchesFormat($testassertion2->class, json_encode($assertion2->get_badge_class())); 802 $this->assertStringMatchesFormat($testassertion2->issuer, json_encode($assertion2->get_issuer())); 803 } 804 805 /** 806 * Tests the core_badges_myprofile_navigation() function. 807 */ 808 public function test_core_badges_myprofile_navigation() { 809 // Set up the test. 810 $tree = new \core_user\output\myprofile\tree(); 811 $this->setAdminUser(); 812 $badge = new badge($this->badgeid); 813 $badge->issue($this->user->id, true); 814 $iscurrentuser = true; 815 $course = null; 816 817 // Enable badges. 818 set_config('enablebadges', true); 819 820 // Check the node tree is correct. 821 core_badges_myprofile_navigation($tree, $this->user, $iscurrentuser, $course); 822 $reflector = new ReflectionObject($tree); 823 $nodes = $reflector->getProperty('nodes'); 824 $nodes->setAccessible(true); 825 $this->assertArrayHasKey('localbadges', $nodes->getValue($tree)); 826 } 827 828 /** 829 * Tests the core_badges_myprofile_navigation() function with badges disabled.. 830 */ 831 public function test_core_badges_myprofile_navigation_badges_disabled() { 832 // Set up the test. 833 $tree = new \core_user\output\myprofile\tree(); 834 $this->setAdminUser(); 835 $badge = new badge($this->badgeid); 836 $badge->issue($this->user->id, true); 837 $iscurrentuser = false; 838 $course = null; 839 840 // Disable badges. 841 set_config('enablebadges', false); 842 843 // Check the node tree is correct. 844 core_badges_myprofile_navigation($tree, $this->user, $iscurrentuser, $course); 845 $reflector = new ReflectionObject($tree); 846 $nodes = $reflector->getProperty('nodes'); 847 $nodes->setAccessible(true); 848 $this->assertArrayNotHasKey('localbadges', $nodes->getValue($tree)); 849 } 850 851 /** 852 * Tests the core_badges_myprofile_navigation() function with a course badge. 853 */ 854 public function test_core_badges_myprofile_navigation_with_course_badge() { 855 // Set up the test. 856 $tree = new \core_user\output\myprofile\tree(); 857 $this->setAdminUser(); 858 $badge = new badge($this->coursebadge); 859 $badge->issue($this->user->id, true); 860 $iscurrentuser = false; 861 862 // Check the node tree is correct. 863 core_badges_myprofile_navigation($tree, $this->user, $iscurrentuser, $this->course); 864 $reflector = new ReflectionObject($tree); 865 $nodes = $reflector->getProperty('nodes'); 866 $nodes->setAccessible(true); 867 $this->assertArrayHasKey('localbadges', $nodes->getValue($tree)); 868 } 869 870 /** 871 * Test insert and update endorsement with a site badge. 872 */ 873 public function test_badge_endorsement() { 874 $badge = new badge($this->badgeid); 875 876 // Insert Endorsement. 877 $endorsement = new stdClass(); 878 $endorsement->badgeid = $this->badgeid; 879 $endorsement->issuername = "Issuer 123"; 880 $endorsement->issueremail = "issuer123@email.com"; 881 $endorsement->issuerurl = "https://example.org/issuer-123"; 882 $endorsement->dateissued = 1524567747; 883 $endorsement->claimid = "https://example.org/robotics-badge.json"; 884 $endorsement->claimcomment = "Test endorser comment"; 885 886 $badge->save_endorsement($endorsement); 887 $endorsement1 = $badge->get_endorsement(); 888 $this->assertEquals($endorsement->badgeid, $endorsement1->badgeid); 889 $this->assertEquals($endorsement->issuername, $endorsement1->issuername); 890 $this->assertEquals($endorsement->issueremail, $endorsement1->issueremail); 891 $this->assertEquals($endorsement->issuerurl, $endorsement1->issuerurl); 892 $this->assertEquals($endorsement->dateissued, $endorsement1->dateissued); 893 $this->assertEquals($endorsement->claimid, $endorsement1->claimid); 894 $this->assertEquals($endorsement->claimcomment, $endorsement1->claimcomment); 895 896 // Update Endorsement. 897 $endorsement1->issuername = "Issuer update"; 898 $badge->save_endorsement($endorsement1); 899 $endorsement2 = $badge->get_endorsement(); 900 $this->assertEquals($endorsement1->id, $endorsement2->id); 901 $this->assertEquals($endorsement1->issuername, $endorsement2->issuername); 902 } 903 904 /** 905 * Test insert and delete related badge with a site badge. 906 */ 907 public function test_badge_related() { 908 $badge = new badge($this->badgeid); 909 $newid1 = $badge->make_clone(); 910 $newid2 = $badge->make_clone(); 911 $newid3 = $badge->make_clone(); 912 913 // Insert an related badge. 914 $badge->add_related_badges([$newid1, $newid2, $newid3]); 915 $this->assertCount(3, $badge->get_related_badges()); 916 917 // Only get related is active. 918 $clonedbage1 = new badge($newid1); 919 $clonedbage1->status = BADGE_STATUS_ACTIVE; 920 $clonedbage1->save(); 921 $this->assertCount(1, $badge->get_related_badges(true)); 922 923 // Delete an related badge. 924 $badge->delete_related_badge($newid2); 925 $this->assertCount(2, $badge->get_related_badges()); 926 } 927 928 /** 929 * Test insert, update, delete alignment with a site badge. 930 */ 931 public function test_alignments() { 932 $badge = new badge($this->badgeid); 933 934 // Insert a alignment. 935 $alignment1 = new stdClass(); 936 $alignment1->badgeid = $this->badgeid; 937 $alignment1->targetname = 'CCSS.ELA-Literacy.RST.11-12.3'; 938 $alignment1->targeturl = 'http://www.corestandards.org/ELA-Literacy/RST/11-12/3'; 939 $alignment1->targetdescription = 'Test target description'; 940 $alignment1->targetframework = 'CCSS.RST.11-12.3'; 941 $alignment1->targetcode = 'CCSS.RST.11-12.3'; 942 $alignment2 = clone $alignment1; 943 $newid1 = $badge->save_alignment($alignment1); 944 $newid2 = $badge->save_alignment($alignment2); 945 $alignments1 = $badge->get_alignments(); 946 $this->assertCount(2, $alignments1); 947 948 $this->assertEquals($alignment1->badgeid, $alignments1[$newid1]->badgeid); 949 $this->assertEquals($alignment1->targetname, $alignments1[$newid1]->targetname); 950 $this->assertEquals($alignment1->targeturl, $alignments1[$newid1]->targeturl); 951 $this->assertEquals($alignment1->targetdescription, $alignments1[$newid1]->targetdescription); 952 $this->assertEquals($alignment1->targetframework, $alignments1[$newid1]->targetframework); 953 $this->assertEquals($alignment1->targetcode, $alignments1[$newid1]->targetcode); 954 955 // Update aligment. 956 $alignments1[$newid1]->targetname = 'CCSS.ELA-Literacy.RST.11-12.3 update'; 957 $badge->save_alignment($alignments1[$newid1], $alignments1[$newid1]->id); 958 $alignments2 = $badge->get_alignments(); 959 $this->assertEquals($alignments1[$newid1]->id, $alignments2[$newid1]->id); 960 $this->assertEquals($alignments1[$newid1]->targetname, $alignments2[$newid1]->targetname); 961 962 // Delete alignment. 963 $badge->delete_alignment($alignments1[$newid2]->id); 964 $this->assertCount(1, $badge->get_alignments()); 965 } 966 967 /** 968 * Test badges_delete_site_backpack(). 969 * 970 */ 971 public function test_badges_delete_site_backpack(): void { 972 global $DB; 973 974 $this->setAdminUser(); 975 976 // Create one backpack. 977 $total = $DB->count_records('badge_external_backpack'); 978 $this->assertEquals(1, $total); 979 980 $data = new \stdClass(); 981 $data->apiversion = OPEN_BADGES_V2P1; 982 $data->backpackapiurl = 'https://dc.imsglobal.org/obchost/ims/ob/v2p1'; 983 $data->backpackweburl = 'https://dc.imsglobal.org'; 984 badges_create_site_backpack($data); 985 $backpack = $DB->get_record('badge_external_backpack', ['backpackweburl' => $data->backpackweburl]); 986 $user1 = $this->getDataGenerator()->create_user(); 987 $user2 = $this->getDataGenerator()->create_user(); 988 // User1 is connected to the backpack to be removed and has 2 collections. 989 $backpackuser1 = helper::create_fake_backpack(['userid' => $user1->id, 'externalbackpackid' => $backpack->id]); 990 helper::create_fake_backpack_collection(['backpackid' => $backpackuser1->id]); 991 helper::create_fake_backpack_collection(['backpackid' => $backpackuser1->id]); 992 // User2 is connected to a different backpack and has 1 collection. 993 $backpackuser2 = helper::create_fake_backpack(['userid' => $user2->id]); 994 helper::create_fake_backpack_collection(['backpackid' => $backpackuser2->id]); 995 996 $total = $DB->count_records('badge_external_backpack'); 997 $this->assertEquals(2, $total); 998 $total = $DB->count_records('badge_backpack'); 999 $this->assertEquals(2, $total); 1000 $total = $DB->count_records('badge_external'); 1001 $this->assertEquals(3, $total); 1002 1003 // Remove the backpack created previously. 1004 $result = badges_delete_site_backpack($backpack->id); 1005 $this->assertTrue($result); 1006 1007 $total = $DB->count_records('badge_external_backpack'); 1008 $this->assertEquals(1, $total); 1009 1010 $total = $DB->count_records('badge_backpack'); 1011 $this->assertEquals(1, $total); 1012 1013 $total = $DB->count_records('badge_external'); 1014 $this->assertEquals(1, $total); 1015 1016 // Try to remove an non-existent backpack. 1017 $result = badges_delete_site_backpack($backpack->id); 1018 $this->assertFalse($result); 1019 } 1020 1021 /** 1022 * Test to validate badges_save_backpack_credentials. 1023 * 1024 * @dataProvider save_backpack_credentials_provider 1025 * @param bool $addbackpack True if backpack data has to be created; false otherwise (empty data will be used then). 1026 * @param string|null $mail Backpack mail address. 1027 * @param string|null $password Backpack password. 1028 */ 1029 public function test_save_backpack_credentials(bool $addbackpack = true, ?string $mail = null, ?string $password = null) { 1030 global $DB; 1031 1032 $this->resetAfterTest(); 1033 $this->setAdminUser(); 1034 1035 $data = []; 1036 if ($addbackpack) { 1037 $data = new \stdClass(); 1038 $data->apiversion = OPEN_BADGES_V2P1; 1039 $data->backpackapiurl = 'https://dc.imsglobal.org/obchost/ims/ob/v2p1'; 1040 $data->backpackweburl = 'https://dc.imsglobal.org'; 1041 badges_create_site_backpack($data); 1042 $backpack = $DB->get_record('badge_external_backpack', ['backpackweburl' => $data->backpackweburl]); 1043 $user = $this->getDataGenerator()->create_user(); 1044 1045 $data = [ 1046 'externalbackpackid' => $backpack->id, 1047 'userid' => $user->id, 1048 ]; 1049 1050 if (!empty($mail)) { 1051 $data['backpackemail'] = $mail; 1052 } 1053 if (!empty($password)) { 1054 $data['password'] = $password; 1055 } 1056 } 1057 1058 $return = badges_save_backpack_credentials((object) $data); 1059 if (array_key_exists('userid', $data)) { 1060 $record = $DB->get_record('badge_backpack', ['userid' => $user->id]); 1061 } else { 1062 $record = $DB->get_records('badge_backpack'); 1063 } 1064 1065 if (!empty($mail) && !empty($password)) { 1066 // The backpack credentials are created if the given information is right. 1067 $this->assertNotEmpty($record); 1068 $this->assertEquals($data['externalbackpackid'], $return); 1069 } else if ($addbackpack) { 1070 // If no email and password are given, no backpack is created/modified. 1071 $this->assertEmpty($record); 1072 $this->assertEquals($data['externalbackpackid'], $return); 1073 } else { 1074 // There weren't fields to add to the backpack so no DB change is expected. 1075 $this->assertEmpty($record); 1076 $this->assertEquals(0, $return); 1077 } 1078 1079 // Confirm the existing backpack credential can be updated (if it has been created). 1080 if (!empty($record)) { 1081 $data['backpackemail'] = 'modified_' . $mail; 1082 $data['id'] = $record->id; 1083 $return = badges_save_backpack_credentials((object) $data); 1084 $record = $DB->get_record('badge_backpack', ['userid' => $user->id]); 1085 1086 $this->assertNotEmpty($record); 1087 $this->assertEquals($data['backpackemail'], $record->email); 1088 $this->assertEquals($data['externalbackpackid'], $return); 1089 } 1090 } 1091 1092 /** 1093 * Data provider for test_create_backpack_credentials(). 1094 * 1095 * @return array 1096 */ 1097 public function save_backpack_credentials_provider(): array { 1098 return [ 1099 'Empty fields' => [ 1100 false, 1101 ], 1102 'No backpack mail or password are defined' => [ 1103 true, 1104 ], 1105 'Both backpack mail and password are defined' => [ 1106 true, 'test@test.com', '1234', 1107 ], 1108 'Only backpack mail is defined (no password is given)' => [ 1109 true, 'test@test.com', null, 1110 ], 1111 'Only backpack password is defined (no mail is given)' => [ 1112 true, null, '1234' 1113 ], 1114 ]; 1115 } 1116 1117 /** 1118 * Test badges_save_external_backpack. 1119 * 1120 * @dataProvider badges_save_external_backpack_provider 1121 * @param array $data Backpack data to save. 1122 * @param bool $adduser True if a real user has to be used for creating the backpack; false otherwise. 1123 * @param bool $duplicates True if duplicates has to be tested too; false otherwise. 1124 */ 1125 public function test_badges_save_external_backpack(array $data, bool $adduser, bool $duplicates) { 1126 global $DB; 1127 1128 $this->resetAfterTest(); 1129 1130 $userid = 0; 1131 if ($adduser) { 1132 $user = $this->getDataGenerator()->create_user(); 1133 $userid = $user->id; 1134 $data['userid'] = $user->id; 1135 } 1136 1137 $result = badges_save_external_backpack((object) $data); 1138 $this->assertNotEquals(0, $result); 1139 $record = $DB->get_record('badge_external_backpack', ['id' => $result]); 1140 $this->assertEquals($record->backpackweburl, $data['backpackweburl']); 1141 $this->assertEquals($record->backpackapiurl, $data['backpackapiurl']); 1142 1143 $record = $DB->get_record('badge_backpack', ['externalbackpackid' => $result]); 1144 if (!array_key_exists('backpackemail', $data) && !array_key_exists('password', $data)) { 1145 $this->assertEmpty($record); 1146 $total = $DB->count_records('badge_backpack'); 1147 $this->assertEquals(0, $total); 1148 } else { 1149 $this->assertNotEmpty($record); 1150 $this->assertEquals($record->userid, $userid); 1151 } 1152 1153 if ($duplicates) { 1154 // We shouldn't be able to insert multiple external_backpacks with the same values. 1155 $this->expectException('dml_write_exception'); 1156 $result = badges_save_external_backpack((object)$data); 1157 } 1158 } 1159 1160 /** 1161 * Provider for test_badges_save_external_backpack 1162 * 1163 * @return array 1164 */ 1165 public function badges_save_external_backpack_provider() { 1166 $data = [ 1167 'apiversion' => 2, 1168 'backpackapiurl' => 'https://api.ca.badgr.io/v2', 1169 'backpackweburl' => 'https://ca.badgr.io', 1170 ]; 1171 return [ 1172 'Test without user and auth details. Check duplicates too' => [ 1173 'data' => $data, 1174 'adduser' => false, 1175 'duplicates' => true, 1176 ], 1177 'Test without user and auth details. No duplicates' => [ 1178 'data' => $data, 1179 'adduser' => false, 1180 'duplicates' => false, 1181 ], 1182 'Test with user and without auth details' => [ 1183 'data' => $data, 1184 'adduser' => true, 1185 'duplicates' => false, 1186 ], 1187 'Test with user and without auth details. Check duplicates too' => [ 1188 'data' => $data, 1189 'adduser' => true, 1190 'duplicates' => true, 1191 ], 1192 'Test with empty backpackemail, password and id' => [ 1193 'data' => array_merge($data, [ 1194 'backpackemail' => '', 1195 'password' => '', 1196 'id' => 0, 1197 ]), 1198 'adduser' => false, 1199 'duplicates' => false, 1200 ], 1201 'Test with empty backpackemail, password and id but with user' => [ 1202 'data' => array_merge($data, [ 1203 'backpackemail' => '', 1204 'password' => '', 1205 'id' => 0, 1206 ]), 1207 'adduser' => true, 1208 'duplicates' => false, 1209 ], 1210 'Test with auth details but without user' => [ 1211 'data' => array_merge($data, [ 1212 'backpackemail' => 'test@test.com', 1213 'password' => 'test', 1214 ]), 1215 'adduser' => false, 1216 'duplicates' => false, 1217 ], 1218 'Test with auth details and user' => [ 1219 'data' => array_merge($data, [ 1220 'backpackemail' => 'test@test.com', 1221 'password' => 'test', 1222 ]), 1223 'adduser' => true, 1224 'duplicates' => false, 1225 ], 1226 ]; 1227 } 1228 1229 /** 1230 * Test backpack creation/update with auth details provided 1231 * 1232 * @param boolean $isadmin 1233 * @param boolean $updatetest 1234 * @dataProvider badges_create_site_backpack_provider 1235 */ 1236 public function test_badges_create_site_backpack($isadmin, $updatetest) { 1237 global $DB; 1238 $this->resetAfterTest(); 1239 1240 $data = [ 1241 'apiversion' => 2, 1242 'backpackapiurl' => 'https://api.ca.badgr.io/v2', 1243 'backpackweburl' => 'https://ca.badgr.io', 1244 ]; 1245 1246 $data['backpackemail'] = 'test@test.com'; 1247 $data['password'] = 'test'; 1248 if ($isadmin || $updatetest) { 1249 $this->setAdminUser(); 1250 $lastmax = $DB->get_field_sql('SELECT MAX(sortorder) FROM {badge_external_backpack}'); 1251 $backpack = badges_create_site_backpack((object) $data); 1252 } 1253 1254 if ($isadmin) { 1255 if ($updatetest) { 1256 $record = $DB->get_record('badge_backpack', ['userid' => 0]); 1257 $data['badgebackpack'] = $record->id; 1258 $data['backpackapiurl'] = 'https://api.ca.badgr.io/v3'; 1259 badges_update_site_backpack($backpack, (object)$data); 1260 } 1261 $record = $DB->get_record('badge_external_backpack', ['id' => $backpack]); 1262 $this->assertEquals($data['backpackweburl'], $record->backpackweburl); 1263 $this->assertEquals($data['backpackapiurl'], $record->backpackapiurl); 1264 $this->assertEquals($lastmax + 1, $record->sortorder); 1265 $record = $DB->get_record('badge_backpack', ['userid' => 0]); 1266 $this->assertNotEmpty($record); 1267 } else { 1268 $user = $this->getDataGenerator()->create_user(); 1269 $this->setUser($user); 1270 $this->expectException('required_capability_exception'); 1271 if ($updatetest) { 1272 $result = badges_update_site_backpack($backpack, (object) $data); 1273 } else { 1274 $result = badges_create_site_backpack((object)$data); 1275 } 1276 } 1277 } 1278 1279 /** 1280 * Provider for test_badges_(create/update)_site_backpack 1281 */ 1282 public function badges_create_site_backpack_provider() { 1283 return [ 1284 "Test as admin user - creation test" => [true, true], 1285 "Test as admin user - update test" => [true, false], 1286 "Test as normal user - creation test" => [false, true], 1287 "Test as normal user - update test" => [false, false], 1288 ]; 1289 } 1290 1291 /** 1292 * Test the badges_open_badges_backpack_api with different backpacks 1293 */ 1294 public function test_badges_open_badges_backpack_api() { 1295 $this->resetAfterTest(); 1296 1297 $data = [ 1298 'apiversion' => 2, 1299 'backpackapiurl' => 'https://api.ca.badgr.io/v2', 1300 'backpackweburl' => 'https://ca.badgr.io', 1301 'sortorder' => 2, 1302 ]; 1303 1304 // Given a complete set of unique data, a new backpack and auth records should exist in the tables. 1305 $data['backpackemail'] = 'test@test.com'; 1306 $data['password'] = 'test'; 1307 $backpack1 = badges_save_external_backpack((object) $data); 1308 $data['backpackweburl'] = 'https://eu.badgr.io'; 1309 $data['backpackapiurl'] = 'https://api.eu.badgr.io/v2'; 1310 $data['apiversion'] = '2.1'; 1311 $data['sortorder'] = 3; 1312 $backpack2 = badges_save_external_backpack((object) $data); 1313 1314 // Move backpack2 to the first position to set it as primary site backpack. 1315 $this->move_backpack_to_first_position($backpack2); 1316 1317 // The default response should check the default site backpack api version. 1318 $this->assertEquals(2.1, badges_open_badges_backpack_api()); 1319 // Check the api version for the other backpack created. 1320 $this->assertEquals(2, badges_open_badges_backpack_api($backpack1)); 1321 $this->assertEquals(2.1, badges_open_badges_backpack_api($backpack2)); 1322 } 1323 1324 /** 1325 * Test the badges_get_site_backpack function 1326 */ 1327 public function test_badges_get_site_backpack() { 1328 $this->resetAfterTest(); 1329 $user = $this->getDataGenerator()->create_user(); 1330 $data = [ 1331 'apiversion' => '2', 1332 'backpackapiurl' => 'https://api.ca.badgr.io/v2', 1333 'backpackweburl' => 'https://ca.badgr.io', 1334 ]; 1335 $backpack1 = badges_save_external_backpack((object) $data); 1336 $data2 = array_merge($data, [ 1337 'backpackapiurl' => 'https://api.eu.badgr.io/v2', 1338 'backpackweburl' => 'https://eu.badgr.io', 1339 'backpackemail' => 'test@test.com', 1340 'password' => 'test', 1341 ]); 1342 $backpack2 = badges_save_external_backpack((object) $data2); 1343 $data3 = array_merge($data2, [ 1344 'userid' => $user->id, 1345 'externalbackpackid' => $backpack2, 1346 'backpackemail' => 'test2@test.com' 1347 ]); 1348 // In the following case, the id returned below equals backpack2. So we aren't storing it. 1349 badges_save_backpack_credentials((object) $data3); 1350 unset($data3['userid']); 1351 1352 // Get a site back based on the id returned from creation and no user id provided. 1353 $this->assertEquals($data, array_intersect($data, (array) badges_get_site_backpack($backpack1))); 1354 $this->assertEquals($data2, array_intersect($data2, (array) badges_get_site_backpack($backpack2))); 1355 $this->assertEquals($data2, array_intersect($data2, (array) badges_get_site_backpack($backpack2, 0))); 1356 $this->assertEquals($data3, array_intersect($data3, (array) badges_get_site_backpack($backpack2, $user->id))); 1357 1358 // Non-existent user backpack should return only configuration details and not auth details. 1359 $userbackpack = badges_get_site_backpack($backpack1, $user->id); 1360 $this->assertNull($userbackpack->badgebackpack); 1361 $this->assertNull($userbackpack->password); 1362 $this->assertNull($userbackpack->backpackemail); 1363 } 1364 1365 /** 1366 * Test the badges_get_user_backpack function 1367 */ 1368 public function test_badges_get_user_backpack() { 1369 $this->resetAfterTest(); 1370 $user = $this->getDataGenerator()->create_user(); 1371 $data = [ 1372 'apiversion' => '2', 1373 'backpackapiurl' => 'https://api.ca.badgr.io/v2', 1374 'backpackweburl' => 'https://ca.badgr.io', 1375 ]; 1376 $backpack1 = badges_save_external_backpack((object) $data); 1377 $data2 = array_merge($data, [ 1378 'backpackapiurl' => 'https://api.eu.badgr.io/v2', 1379 'backpackweburl' => 'https://eu.badgr.io', 1380 'backpackemail' => 'test@test.com', 1381 'password' => 'test', 1382 ]); 1383 $backpack2 = badges_save_external_backpack((object) $data2); 1384 $data3 = array_merge($data2, [ 1385 'userid' => $user->id, 1386 'externalbackpackid' => $backpack2, 1387 'backpackemail' => 'test2@test.com' 1388 ]); 1389 // In the following case, the id returned below equals backpack2. So we aren't storing it. 1390 badges_save_backpack_credentials((object) $data3); 1391 unset($data3['userid']); 1392 1393 // Currently logged in as admin. 1394 $this->assertEquals($data2, array_intersect($data2, (array) badges_get_user_backpack())); 1395 $this->assertEquals($data2, array_intersect($data2, (array) badges_get_user_backpack(0))); 1396 $this->assertEquals($data3, array_intersect($data3, (array) badges_get_user_backpack($user->id))); 1397 1398 // Non-existent user backpack should return nothing. 1399 $this->assertFalse(badges_get_user_backpack($backpack1, $user->id)); 1400 1401 // Login as user. 1402 $this->setUser($user); 1403 $this->assertEquals($data3, array_intersect($data3, (array) badges_get_user_backpack())); 1404 } 1405 1406 /** 1407 * Test the badges_get_site_primary_backpack function 1408 * 1409 * @param boolean $withauth Testing with authentication or not. 1410 * @dataProvider badges_get_site_primary_backpack_provider 1411 */ 1412 public function test_badges_get_site_primary_backpack($withauth) { 1413 $data = [ 1414 'apiversion' => '2', 1415 'backpackapiurl' => 'https://api.ca.badgr.io/v2', 1416 'backpackweburl' => 'https://ca.badgr.io', 1417 'sortorder' => '2', 1418 ]; 1419 if ($withauth) { 1420 $data = array_merge($data, [ 1421 'backpackemail' => 'test@test.com', 1422 'password' => 'test', 1423 ]); 1424 } 1425 $backpack = badges_save_external_backpack((object) $data); 1426 1427 // Check the backpack created is not the primary one. 1428 $sitebackpack = badges_get_site_primary_backpack(); 1429 $this->assertNotEquals($backpack, $sitebackpack->id); 1430 1431 // Move backpack to the first position to set it as primary site backpack. 1432 $this->move_backpack_to_first_position($backpack); 1433 1434 $sitebackpack = badges_get_site_primary_backpack(); 1435 $this->assertEquals($backpack, $sitebackpack->id); 1436 1437 if ($withauth) { 1438 $this->assertEquals($data, array_intersect($data, (array) $sitebackpack)); 1439 $this->assertEquals($data['password'], $sitebackpack->password); 1440 $this->assertEquals($data['backpackemail'], $sitebackpack->backpackemail); 1441 } else { 1442 $this->assertNull($sitebackpack->badgebackpack); 1443 $this->assertNull($sitebackpack->password); 1444 $this->assertNull($sitebackpack->backpackemail); 1445 } 1446 } 1447 1448 /** 1449 * Test the test_badges_get_site_primary_backpack function. 1450 * 1451 * @return array 1452 */ 1453 public function badges_get_site_primary_backpack_provider() { 1454 return [ 1455 "Test with auth details" => [true], 1456 "Test without auth details" => [false], 1457 ]; 1458 } 1459 1460 /** 1461 * Test badges_change_sortorder_backpacks(). 1462 * 1463 * @dataProvider badges_change_sortorder_backpacks_provider 1464 * @covers ::badges_change_sortorder_backpacks 1465 * 1466 * @param int $backpacktomove Backpack index to move (from 0 to 5). 1467 * @param int $direction Direction to move the backpack. 1468 * @param int|null $expectedsortorder Expected sortorder or null if an exception is expected. 1469 */ 1470 public function test_badges_change_sortorder_backpacks(int $backpacktomove, int $direction, ?int $expectedsortorder): void { 1471 global $DB; 1472 1473 $this->resetAfterTest(); 1474 $this->setAdminUser(); 1475 1476 // Create 5 more backpacks. 1477 for ($i = 0; $i < 5; $i++) { 1478 $data = new \stdClass(); 1479 $data->apiversion = OPEN_BADGES_V2P1; 1480 $data->backpackapiurl = "https://myurl$i.cat/ob/v2p1"; 1481 $data->backpackweburl = "https://myurl$i.cat"; 1482 badges_create_site_backpack($data); 1483 } 1484 1485 // Check there are 6 backpacks (1 pre-existing + 5 news). 1486 $total = $DB->count_records('badge_external_backpack'); 1487 $this->assertEquals(6, $total); 1488 $backpacks = array_values(badges_get_site_backpacks()); 1489 1490 if (is_null($expectedsortorder)) { 1491 $this->expectException('moodle_exception'); 1492 } 1493 1494 // Move the backpack. 1495 badges_change_sortorder_backpacks($backpacks[$backpacktomove]->id, $direction); 1496 1497 if (!is_null($expectedsortorder)) { 1498 $backpack = badges_get_site_backpack($backpacks[$backpacktomove]->id); 1499 $this->assertEquals($expectedsortorder, $backpack->sortorder); 1500 } 1501 } 1502 1503 /** 1504 * Provider for test_badges_change_sortorder_backpacks. 1505 * 1506 * @return array 1507 */ 1508 public function badges_change_sortorder_backpacks_provider(): array { 1509 return [ 1510 "Test up" => [ 1511 'backpacktomove' => 1, 1512 'direction' => BACKPACK_MOVE_UP, 1513 'expectedsortorder' => 1, 1514 ], 1515 "Test down" => [ 1516 'backpacktomove' => 1, 1517 'direction' => BACKPACK_MOVE_DOWN, 1518 'expectedsortorder' => 3, 1519 ], 1520 "Test up the very first element" => [ 1521 'backpacktomove' => 0, 1522 'direction' => BACKPACK_MOVE_UP, 1523 'expectedsortorder' => 1, 1524 ], 1525 "Test down the very last element" => [ 1526 'backpacktomove' => 5, 1527 'direction' => BACKPACK_MOVE_DOWN, 1528 'expectedsortorder' => 6, 1529 ], 1530 "Test with an invalid direction value" => [ 1531 'backpacktomove' => 1, 1532 'direction' => 10, 1533 'expectedsortorder' => null, 1534 ], 1535 ]; 1536 } 1537 1538 /** 1539 * Test the Badgr URL generator function 1540 * 1541 * @param mixed $type Type corresponding to the badge entites 1542 * @param string $expected Expected string result 1543 * @dataProvider badgr_open_url_generator 1544 */ 1545 public function test_badges_generate_badgr_open_url($type, $expected) { 1546 $data = [ 1547 'apiversion' => '2', 1548 'backpackapiurl' => 'https://api.ca.badgr.io/v2', 1549 'backpackweburl' => 'https://ca.badgr.io', 1550 'backpackemail' => 'test@test.com', 1551 'password' => 'test', 1552 ]; 1553 $backpack2 = badges_save_external_backpack((object) $data); 1554 $backpack = badges_get_site_backpack($backpack2); 1555 $this->assertEquals($expected, badges_generate_badgr_open_url($backpack, $type, 123455)); 1556 } 1557 1558 /** 1559 * Data provider for test_badges_generate_badgr_open_url 1560 * @return array 1561 */ 1562 public function badgr_open_url_generator() { 1563 return [ 1564 'Badgr Assertion URL test' => [ 1565 OPEN_BADGES_V2_TYPE_ASSERTION, "https://api.ca.badgr.io/public/assertions/123455" 1566 ], 1567 'Badgr Issuer URL test' => [ 1568 OPEN_BADGES_V2_TYPE_ISSUER, "https://api.ca.badgr.io/public/issuers/123455" 1569 ], 1570 'Badgr Badge URL test' => [ 1571 OPEN_BADGES_V2_TYPE_BADGE, "https://api.ca.badgr.io/public/badges/123455" 1572 ] 1573 ]; 1574 } 1575 1576 /** 1577 * Test badges_external_get_mapping function 1578 * 1579 * @param int $internalid The internal id of the mapping 1580 * @param int $externalid The external / remote ref to the mapping 1581 * @param mixed $expected The expected result from the function 1582 * @param string|null $field The field we are passing to the function. Null if we don't want to pass anything.ss 1583 * 1584 * @dataProvider badges_external_get_mapping_provider 1585 */ 1586 public function test_badges_external_get_mapping($internalid, $externalid, $expected, $field = null) { 1587 $data = [ 1588 'apiversion' => '2', 1589 'backpackapiurl' => 'https://api.ca.badgr.io/v2', 1590 'backpackweburl' => 'https://ca.badgr.io', 1591 'backpackemail' => 'test@test.com', 1592 'password' => 'test', 1593 ]; 1594 $backpack2 = badges_save_external_backpack((object) $data); 1595 badges_external_create_mapping($backpack2, OPEN_BADGES_V2_TYPE_BADGE, $internalid, $externalid); 1596 $expected = $expected == "id" ? $backpack2 : $expected; 1597 if ($field) { 1598 $this->assertEquals($expected, badges_external_get_mapping($backpack2, OPEN_BADGES_V2_TYPE_BADGE, $internalid, $field)); 1599 } else { 1600 $this->assertEquals($expected, badges_external_get_mapping($backpack2, OPEN_BADGES_V2_TYPE_BADGE, $internalid)); 1601 } 1602 } 1603 1604 /** 1605 * Data provider for badges_external_get_mapping_provider 1606 * 1607 * @return array 1608 */ 1609 public function badges_external_get_mapping_provider() { 1610 return [ 1611 "Get the site backpack value" => [ 1612 1234, 4321, 'id', 'sitebackpackid' 1613 ], 1614 "Get the type of the mapping" => [ 1615 1234, 4321, OPEN_BADGES_V2_TYPE_BADGE, 'type' 1616 ], 1617 "Get the externalid of the mapping" => [ 1618 1234, 4321, 4321, 'externalid' 1619 ], 1620 "Get the externalid of the mapping without providing a param" => [ 1621 1234, 4321, 4321, null 1622 ], 1623 "Get the internalid of the mapping" => [ 1624 1234, 4321, 1234, 'internalid' 1625 ] 1626 ]; 1627 } 1628 1629 /** 1630 * Move the backpack to the first position, to set it as primary site backpack. 1631 * 1632 * @param int $backpackid The backpack identifier. 1633 */ 1634 private function move_backpack_to_first_position(int $backpackid): void { 1635 $backpack = badges_get_site_backpack($backpackid); 1636 while ($backpack->sortorder > 1) { 1637 badges_change_sortorder_backpacks($backpackid, BACKPACK_MOVE_UP); 1638 $backpack = badges_get_site_backpack($backpackid); 1639 } 1640 } 1641 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body