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 * Badge assertion library. 19 * 20 * @package core 21 * @subpackage badges 22 * @copyright 2012 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 27namespace core_badges; 28 29defined('MOODLE_INTERNAL') || die(); 30 31require_once($CFG->dirroot.'/lib/badgeslib.php'); 32 33use context_system; 34use context_course; 35use context_user; 36use moodle_exception; 37use moodle_url; 38use core_text; 39use award_criteria; 40use core_php_time_limit; 41use html_writer; 42use stdClass; 43 44/** 45 * Class that represents badge. 46 * 47 * @copyright 2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/} 48 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 49 */ 50class badge { 51 /** @var int Badge id */ 52 public $id; 53 54 /** @var string Badge name */ 55 public $name; 56 57 /** @var string Badge description */ 58 public $description; 59 60 /** @var integer Timestamp this badge was created */ 61 public $timecreated; 62 63 /** @var integer Timestamp this badge was modified */ 64 public $timemodified; 65 66 /** @var int The user who created this badge */ 67 public $usercreated; 68 69 /** @var int The user who modified this badge */ 70 public $usermodified; 71 72 /** @var string The name of the issuer of this badge */ 73 public $issuername; 74 75 /** @var string The url of the issuer of this badge */ 76 public $issuerurl; 77 78 /** @var string The email of the issuer of this badge */ 79 public $issuercontact; 80 81 /** @var integer Timestamp this badge will expire */ 82 public $expiredate; 83 84 /** @var integer Duration this badge is valid for */ 85 public $expireperiod; 86 87 /** @var integer Site or course badge */ 88 public $type; 89 90 /** @var integer The course this badge belongs to */ 91 public $courseid; 92 93 /** @var string The message this badge includes. */ 94 public $message; 95 96 /** @var string The subject of the message for this badge */ 97 public $messagesubject; 98 99 /** @var int Is this badge image baked. */ 100 public $attachment; 101 102 /** @var int Send a message when this badge is awarded. */ 103 public $notification; 104 105 /** @var int Lifecycle status for this badge. */ 106 public $status = 0; 107 108 /** @var int Timestamp to next run cron for this badge. */ 109 public $nextcron; 110 111 /** @var int What backpack api version to use for this badge. */ 112 public $version; 113 114 /** @var string What language is this badge written in. */ 115 public $language; 116 117 /** @var string The author of the image for this badge. */ 118 public $imageauthorname; 119 120 /** @var string The email of the author of the image for this badge. */ 121 public $imageauthoremail; 122 123 /** @var string The url of the author of the image for this badge. */ 124 public $imageauthorurl; 125 126 /** @var string The caption of the image for this badge. */ 127 public $imagecaption; 128 129 /** @var array Badge criteria */ 130 public $criteria = array(); 131 132 /** 133 * Constructs with badge details. 134 * 135 * @param int $badgeid badge ID. 136 */ 137 public function __construct($badgeid) { 138 global $DB; 139 $this->id = $badgeid; 140 141 $data = $DB->get_record('badge', array('id' => $badgeid)); 142 143 if (empty($data)) { 144 print_error('error:nosuchbadge', 'badges', $badgeid); 145 } 146 147 foreach ((array)$data as $field => $value) { 148 if (property_exists($this, $field)) { 149 $this->{$field} = $value; 150 } 151 } 152 153 if (badges_open_badges_backpack_api() != OPEN_BADGES_V1) { 154 // For Open Badges 2 we need to use a single site issuer with no exceptions. 155 $issuer = badges_get_default_issuer(); 156 $this->issuername = $issuer['name']; 157 $this->issuercontact = $issuer['email']; 158 $this->issuerurl = $issuer['url']; 159 } 160 161 $this->criteria = self::get_criteria(); 162 } 163 164 /** 165 * Use to get context instance of a badge. 166 * 167 * @return context instance. 168 */ 169 public function get_context() { 170 if ($this->type == BADGE_TYPE_SITE) { 171 return context_system::instance(); 172 } else if ($this->type == BADGE_TYPE_COURSE) { 173 return context_course::instance($this->courseid); 174 } else { 175 debugging('Something is wrong...'); 176 } 177 } 178 179 /** 180 * Return array of aggregation methods 181 * 182 * @return array 183 */ 184 public static function get_aggregation_methods() { 185 return array( 186 BADGE_CRITERIA_AGGREGATION_ALL => get_string('all', 'badges'), 187 BADGE_CRITERIA_AGGREGATION_ANY => get_string('any', 'badges'), 188 ); 189 } 190 191 /** 192 * Return array of accepted criteria types for this badge 193 * 194 * @return array 195 */ 196 public function get_accepted_criteria() { 197 global $CFG; 198 $criteriatypes = array(); 199 200 if ($this->type == BADGE_TYPE_COURSE) { 201 $criteriatypes = array( 202 BADGE_CRITERIA_TYPE_OVERALL, 203 BADGE_CRITERIA_TYPE_MANUAL, 204 BADGE_CRITERIA_TYPE_COURSE, 205 BADGE_CRITERIA_TYPE_BADGE, 206 BADGE_CRITERIA_TYPE_ACTIVITY, 207 BADGE_CRITERIA_TYPE_COMPETENCY 208 ); 209 } else if ($this->type == BADGE_TYPE_SITE) { 210 $criteriatypes = array( 211 BADGE_CRITERIA_TYPE_OVERALL, 212 BADGE_CRITERIA_TYPE_MANUAL, 213 BADGE_CRITERIA_TYPE_COURSESET, 214 BADGE_CRITERIA_TYPE_BADGE, 215 BADGE_CRITERIA_TYPE_PROFILE, 216 BADGE_CRITERIA_TYPE_COHORT, 217 BADGE_CRITERIA_TYPE_COMPETENCY 218 ); 219 } 220 $alltypes = badges_list_criteria(); 221 foreach ($criteriatypes as $index => $type) { 222 if (!isset($alltypes[$type])) { 223 unset($criteriatypes[$index]); 224 } 225 } 226 227 return $criteriatypes; 228 } 229 230 /** 231 * Save/update badge information in 'badge' table only. 232 * Cannot be used for updating awards and criteria settings. 233 * 234 * @return boolean Returns true on success. 235 */ 236 public function save() { 237 global $DB; 238 239 $fordb = new stdClass(); 240 foreach (get_object_vars($this) as $k => $v) { 241 $fordb->{$k} = $v; 242 } 243 unset($fordb->criteria); 244 245 $fordb->timemodified = time(); 246 if ($DB->update_record_raw('badge', $fordb)) { 247 // Trigger event, badge updated. 248 $eventparams = array('objectid' => $this->id, 'context' => $this->get_context()); 249 $event = \core\event\badge_updated::create($eventparams); 250 $event->trigger(); 251 return true; 252 } else { 253 throw new moodle_exception('error:save', 'badges'); 254 return false; 255 } 256 } 257 258 /** 259 * Creates and saves a clone of badge with all its properties. 260 * Clone is not active by default and has 'Copy of' attached to its name. 261 * 262 * @return int ID of new badge. 263 */ 264 public function make_clone() { 265 global $DB, $USER, $PAGE; 266 267 $fordb = new stdClass(); 268 foreach (get_object_vars($this) as $k => $v) { 269 $fordb->{$k} = $v; 270 } 271 272 $fordb->name = get_string('copyof', 'badges', $this->name); 273 $fordb->status = BADGE_STATUS_INACTIVE; 274 $fordb->usercreated = $USER->id; 275 $fordb->usermodified = $USER->id; 276 $fordb->timecreated = time(); 277 $fordb->timemodified = time(); 278 unset($fordb->id); 279 280 if ($fordb->notification > 1) { 281 $fordb->nextcron = badges_calculate_message_schedule($fordb->notification); 282 } 283 284 $criteria = $fordb->criteria; 285 unset($fordb->criteria); 286 287 if ($new = $DB->insert_record('badge', $fordb, true)) { 288 $newbadge = new badge($new); 289 290 // Copy badge image. 291 $fs = get_file_storage(); 292 if ($file = $fs->get_file($this->get_context()->id, 'badges', 'badgeimage', $this->id, '/', 'f3.png')) { 293 if ($imagefile = $file->copy_content_to_temp()) { 294 badges_process_badge_image($newbadge, $imagefile); 295 } 296 } 297 298 // Copy badge criteria. 299 foreach ($this->criteria as $crit) { 300 $crit->make_clone($new); 301 } 302 303 // Trigger event, badge duplicated. 304 $eventparams = array('objectid' => $new, 'context' => $PAGE->context); 305 $event = \core\event\badge_duplicated::create($eventparams); 306 $event->trigger(); 307 308 return $new; 309 } else { 310 throw new moodle_exception('error:clone', 'badges'); 311 return false; 312 } 313 } 314 315 /** 316 * Checks if badges is active. 317 * Used in badge award. 318 * 319 * @return boolean A status indicating badge is active 320 */ 321 public function is_active() { 322 if (($this->status == BADGE_STATUS_ACTIVE) || 323 ($this->status == BADGE_STATUS_ACTIVE_LOCKED)) { 324 return true; 325 } 326 return false; 327 } 328 329 /** 330 * Use to get the name of badge status. 331 * 332 * @return string 333 */ 334 public function get_status_name() { 335 return get_string('badgestatus_' . $this->status, 'badges'); 336 } 337 338 /** 339 * Use to set badge status. 340 * Only active badges can be earned/awarded/issued. 341 * 342 * @param int $status Status from BADGE_STATUS constants 343 */ 344 public function set_status($status = 0) { 345 $this->status = $status; 346 $this->save(); 347 if ($status == BADGE_STATUS_ACTIVE) { 348 // Trigger event, badge enabled. 349 $eventparams = array('objectid' => $this->id, 'context' => $this->get_context()); 350 $event = \core\event\badge_enabled::create($eventparams); 351 $event->trigger(); 352 } else if ($status == BADGE_STATUS_INACTIVE) { 353 // Trigger event, badge disabled. 354 $eventparams = array('objectid' => $this->id, 'context' => $this->get_context()); 355 $event = \core\event\badge_disabled::create($eventparams); 356 $event->trigger(); 357 } 358 } 359 360 /** 361 * Checks if badges is locked. 362 * Used in badge award and editing. 363 * 364 * @return boolean A status indicating badge is locked 365 */ 366 public function is_locked() { 367 if (($this->status == BADGE_STATUS_ACTIVE_LOCKED) || 368 ($this->status == BADGE_STATUS_INACTIVE_LOCKED)) { 369 return true; 370 } 371 return false; 372 } 373 374 /** 375 * Checks if badge has been awarded to users. 376 * Used in badge editing. 377 * 378 * @return boolean A status indicating badge has been awarded at least once 379 */ 380 public function has_awards() { 381 global $DB; 382 $awarded = $DB->record_exists_sql('SELECT b.uniquehash 383 FROM {badge_issued} b INNER JOIN {user} u ON b.userid = u.id 384 WHERE b.badgeid = :badgeid AND u.deleted = 0', array('badgeid' => $this->id)); 385 386 return $awarded; 387 } 388 389 /** 390 * Gets list of users who have earned an instance of this badge. 391 * 392 * @return array An array of objects with information about badge awards. 393 */ 394 public function get_awards() { 395 global $DB; 396 397 $awards = $DB->get_records_sql( 398 'SELECT b.userid, b.dateissued, b.uniquehash, u.firstname, u.lastname 399 FROM {badge_issued} b INNER JOIN {user} u 400 ON b.userid = u.id 401 WHERE b.badgeid = :badgeid AND u.deleted = 0', array('badgeid' => $this->id)); 402 403 return $awards; 404 } 405 406 /** 407 * Indicates whether badge has already been issued to a user. 408 * 409 * @param int $userid User to check 410 * @return boolean 411 */ 412 public function is_issued($userid) { 413 global $DB; 414 return $DB->record_exists('badge_issued', array('badgeid' => $this->id, 'userid' => $userid)); 415 } 416 417 /** 418 * Issue a badge to user. 419 * 420 * @param int $userid User who earned the badge 421 * @param boolean $nobake Not baking actual badges (for testing purposes) 422 */ 423 public function issue($userid, $nobake = false) { 424 global $DB, $CFG; 425 426 $now = time(); 427 $issued = new stdClass(); 428 $issued->badgeid = $this->id; 429 $issued->userid = $userid; 430 $issued->uniquehash = sha1(rand() . $userid . $this->id . $now); 431 $issued->dateissued = $now; 432 433 if ($this->can_expire()) { 434 $issued->dateexpire = $this->calculate_expiry($now); 435 } else { 436 $issued->dateexpire = null; 437 } 438 439 // Take into account user badges privacy settings. 440 // If none set, badges default visibility is set to public. 441 $issued->visible = get_user_preferences('badgeprivacysetting', 1, $userid); 442 443 $result = $DB->insert_record('badge_issued', $issued, true); 444 445 if ($result) { 446 // Trigger badge awarded event. 447 $eventdata = array ( 448 'context' => $this->get_context(), 449 'objectid' => $this->id, 450 'relateduserid' => $userid, 451 'other' => array('dateexpire' => $issued->dateexpire, 'badgeissuedid' => $result) 452 ); 453 \core\event\badge_awarded::create($eventdata)->trigger(); 454 455 // Lock the badge, so that its criteria could not be changed any more. 456 if ($this->status == BADGE_STATUS_ACTIVE) { 457 $this->set_status(BADGE_STATUS_ACTIVE_LOCKED); 458 } 459 460 // Update details in criteria_met table. 461 $compl = $this->get_criteria_completions($userid); 462 foreach ($compl as $c) { 463 $obj = new stdClass(); 464 $obj->id = $c->id; 465 $obj->issuedid = $result; 466 $DB->update_record('badge_criteria_met', $obj, true); 467 } 468 469 if (!$nobake) { 470 // Bake a badge image. 471 $pathhash = badges_bake($issued->uniquehash, $this->id, $userid, true); 472 473 // Notify recipients and badge creators. 474 badges_notify_badge_award($this, $userid, $issued->uniquehash, $pathhash); 475 } 476 } 477 } 478 479 /** 480 * Reviews all badge criteria and checks if badge can be instantly awarded. 481 * 482 * @return int Number of awards 483 */ 484 public function review_all_criteria() { 485 global $DB, $CFG; 486 $awards = 0; 487 488 // Raise timelimit as this could take a while for big web sites. 489 core_php_time_limit::raise(); 490 raise_memory_limit(MEMORY_HUGE); 491 492 foreach ($this->criteria as $crit) { 493 // Overall criterion is decided when other criteria are reviewed. 494 if ($crit->criteriatype == BADGE_CRITERIA_TYPE_OVERALL) { 495 continue; 496 } 497 498 list($extrajoin, $extrawhere, $extraparams) = $crit->get_completed_criteria_sql(); 499 // For site level badges, get all active site users who can earn this badge and haven't got it yet. 500 if ($this->type == BADGE_TYPE_SITE) { 501 $sql = "SELECT DISTINCT u.id, bi.badgeid 502 FROM {user} u 503 {$extrajoin} 504 LEFT JOIN {badge_issued} bi 505 ON u.id = bi.userid AND bi.badgeid = :badgeid 506 WHERE bi.badgeid IS NULL AND u.id != :guestid AND u.deleted = 0 " . $extrawhere; 507 $params = array_merge(array('badgeid' => $this->id, 'guestid' => $CFG->siteguest), $extraparams); 508 $toearn = $DB->get_fieldset_sql($sql, $params); 509 } else { 510 // For course level badges, get all users who already earned the badge in this course. 511 // Then find the ones who are enrolled in the course and don't have a badge yet. 512 $earned = $DB->get_fieldset_select( 513 'badge_issued', 514 'userid AS id', 515 'badgeid = :badgeid', 516 array('badgeid' => $this->id) 517 ); 518 519 $wheresql = ''; 520 $earnedparams = array(); 521 if (!empty($earned)) { 522 list($earnedsql, $earnedparams) = $DB->get_in_or_equal($earned, SQL_PARAMS_NAMED, 'u', false); 523 $wheresql = ' WHERE u.id ' . $earnedsql; 524 } 525 list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->get_context(), 'moodle/badges:earnbadge', 0, true); 526 $sql = "SELECT DISTINCT u.id 527 FROM {user} u 528 {$extrajoin} 529 JOIN ({$enrolledsql}) je ON je.id = u.id " . $wheresql . $extrawhere; 530 $params = array_merge($enrolledparams, $earnedparams, $extraparams); 531 $toearn = $DB->get_fieldset_sql($sql, $params); 532 } 533 534 foreach ($toearn as $uid) { 535 $reviewoverall = false; 536 if ($crit->review($uid, true)) { 537 $crit->mark_complete($uid); 538 if ($this->criteria[BADGE_CRITERIA_TYPE_OVERALL]->method == BADGE_CRITERIA_AGGREGATION_ANY) { 539 $this->criteria[BADGE_CRITERIA_TYPE_OVERALL]->mark_complete($uid); 540 $this->issue($uid); 541 $awards++; 542 } else { 543 $reviewoverall = true; 544 } 545 } else { 546 // Will be reviewed some other time. 547 $reviewoverall = false; 548 } 549 // Review overall if it is required. 550 if ($reviewoverall && $this->criteria[BADGE_CRITERIA_TYPE_OVERALL]->review($uid)) { 551 $this->criteria[BADGE_CRITERIA_TYPE_OVERALL]->mark_complete($uid); 552 $this->issue($uid); 553 $awards++; 554 } 555 } 556 } 557 558 return $awards; 559 } 560 561 /** 562 * Gets an array of completed criteria from 'badge_criteria_met' table. 563 * 564 * @param int $userid Completions for a user 565 * @return array Records of criteria completions 566 */ 567 public function get_criteria_completions($userid) { 568 global $DB; 569 $completions = array(); 570 $sql = "SELECT bcm.id, bcm.critid 571 FROM {badge_criteria_met} bcm 572 INNER JOIN {badge_criteria} bc ON bcm.critid = bc.id 573 WHERE bc.badgeid = :badgeid AND bcm.userid = :userid "; 574 $completions = $DB->get_records_sql($sql, array('badgeid' => $this->id, 'userid' => $userid)); 575 576 return $completions; 577 } 578 579 /** 580 * Checks if badges has award criteria set up. 581 * 582 * @return boolean A status indicating badge has at least one criterion 583 */ 584 public function has_criteria() { 585 if (count($this->criteria) > 0) { 586 return true; 587 } 588 return false; 589 } 590 591 /** 592 * Returns badge award criteria 593 * 594 * @return array An array of badge criteria 595 */ 596 public function get_criteria() { 597 global $DB; 598 $criteria = array(); 599 600 if ($records = (array)$DB->get_records('badge_criteria', array('badgeid' => $this->id))) { 601 foreach ($records as $record) { 602 $criteria[$record->criteriatype] = award_criteria::build((array)$record); 603 } 604 } 605 606 return $criteria; 607 } 608 609 /** 610 * Get aggregation method for badge criteria 611 * 612 * @param int $criteriatype If none supplied, get overall aggregation method (optional) 613 * @return int One of BADGE_CRITERIA_AGGREGATION_ALL or BADGE_CRITERIA_AGGREGATION_ANY 614 */ 615 public function get_aggregation_method($criteriatype = 0) { 616 global $DB; 617 $params = array('badgeid' => $this->id, 'criteriatype' => $criteriatype); 618 $aggregation = $DB->get_field('badge_criteria', 'method', $params, IGNORE_MULTIPLE); 619 620 if (!$aggregation) { 621 return BADGE_CRITERIA_AGGREGATION_ALL; 622 } 623 624 return $aggregation; 625 } 626 627 /** 628 * Checks if badge has expiry period or date set up. 629 * 630 * @return boolean A status indicating badge can expire 631 */ 632 public function can_expire() { 633 if ($this->expireperiod || $this->expiredate) { 634 return true; 635 } 636 return false; 637 } 638 639 /** 640 * Calculates badge expiry date based on either expirydate or expiryperiod. 641 * 642 * @param int $timestamp Time of badge issue 643 * @return int A timestamp 644 */ 645 public function calculate_expiry($timestamp) { 646 $expiry = null; 647 648 if (isset($this->expiredate)) { 649 $expiry = $this->expiredate; 650 } else if (isset($this->expireperiod)) { 651 $expiry = $timestamp + $this->expireperiod; 652 } 653 654 return $expiry; 655 } 656 657 /** 658 * Checks if badge has manual award criteria set. 659 * 660 * @return boolean A status indicating badge can be awarded manually 661 */ 662 public function has_manual_award_criteria() { 663 foreach ($this->criteria as $criterion) { 664 if ($criterion->criteriatype == BADGE_CRITERIA_TYPE_MANUAL) { 665 return true; 666 } 667 } 668 return false; 669 } 670 671 /** 672 * Fully deletes the badge or marks it as archived. 673 * 674 * @param boolean $archive Achive a badge without actual deleting of any data. 675 */ 676 public function delete($archive = true) { 677 global $DB; 678 679 if ($archive) { 680 $this->status = BADGE_STATUS_ARCHIVED; 681 $this->save(); 682 683 // Trigger event, badge archived. 684 $eventparams = array('objectid' => $this->id, 'context' => $this->get_context()); 685 $event = \core\event\badge_archived::create($eventparams); 686 $event->trigger(); 687 return; 688 } 689 690 $fs = get_file_storage(); 691 692 // Remove all issued badge image files and badge awards. 693 // Cannot bulk remove area files here because they are issued in user context. 694 $awards = $this->get_awards(); 695 foreach ($awards as $award) { 696 $usercontext = context_user::instance($award->userid); 697 $fs->delete_area_files($usercontext->id, 'badges', 'userbadge', $this->id); 698 } 699 $DB->delete_records('badge_issued', array('badgeid' => $this->id)); 700 701 // Remove all badge criteria. 702 $criteria = $this->get_criteria(); 703 foreach ($criteria as $criterion) { 704 $criterion->delete(); 705 } 706 707 // Delete badge images. 708 $badgecontext = $this->get_context(); 709 $fs->delete_area_files($badgecontext->id, 'badges', 'badgeimage', $this->id); 710 711 // Delete endorsements, competencies and related badges. 712 $DB->delete_records('badge_endorsement', array('badgeid' => $this->id)); 713 $relatedsql = 'badgeid = :badgeid OR relatedbadgeid = :relatedbadgeid'; 714 $relatedparams = array( 715 'badgeid' => $this->id, 716 'relatedbadgeid' => $this->id 717 ); 718 $DB->delete_records_select('badge_related', $relatedsql, $relatedparams); 719 $DB->delete_records('badge_alignment', array('badgeid' => $this->id)); 720 721 // Finally, remove badge itself. 722 $DB->delete_records('badge', array('id' => $this->id)); 723 724 // Trigger event, badge deleted. 725 $eventparams = array('objectid' => $this->id, 726 'context' => $this->get_context(), 727 'other' => array('badgetype' => $this->type, 'courseid' => $this->courseid) 728 ); 729 $event = \core\event\badge_deleted::create($eventparams); 730 $event->trigger(); 731 } 732 733 /** 734 * Add multiple related badges. 735 * 736 * @param array $relatedids Id of badges. 737 */ 738 public function add_related_badges($relatedids) { 739 global $DB; 740 $relatedbadges = array(); 741 foreach ($relatedids as $relatedid) { 742 $relatedbadge = new stdClass(); 743 $relatedbadge->badgeid = $this->id; 744 $relatedbadge->relatedbadgeid = $relatedid; 745 $relatedbadges[] = $relatedbadge; 746 } 747 $DB->insert_records('badge_related', $relatedbadges); 748 } 749 750 /** 751 * Delete an related badge. 752 * 753 * @param int $relatedid Id related badge. 754 * @return boolean A status for delete an related badge. 755 */ 756 public function delete_related_badge($relatedid) { 757 global $DB; 758 $sql = "(badgeid = :badgeid AND relatedbadgeid = :relatedid) OR " . 759 "(badgeid = :relatedid2 AND relatedbadgeid = :badgeid2)"; 760 $params = ['badgeid' => $this->id, 'badgeid2' => $this->id, 'relatedid' => $relatedid, 'relatedid2' => $relatedid]; 761 return $DB->delete_records_select('badge_related', $sql, $params); 762 } 763 764 /** 765 * Checks if badge has related badges. 766 * 767 * @return boolean A status related badge. 768 */ 769 public function has_related() { 770 global $DB; 771 $sql = "SELECT DISTINCT b.id 772 FROM {badge_related} br 773 JOIN {badge} b ON (br.relatedbadgeid = b.id OR br.badgeid = b.id) 774 WHERE (br.badgeid = :badgeid OR br.relatedbadgeid = :badgeid2) AND b.id != :badgeid3"; 775 return $DB->record_exists_sql($sql, ['badgeid' => $this->id, 'badgeid2' => $this->id, 'badgeid3' => $this->id]); 776 } 777 778 /** 779 * Get related badges of badge. 780 * 781 * @param boolean $activeonly Do not get the inactive badges when is true. 782 * @return array Related badges information. 783 */ 784 public function get_related_badges($activeonly = false) { 785 global $DB; 786 787 $params = array('badgeid' => $this->id, 'badgeid2' => $this->id, 'badgeid3' => $this->id); 788 $query = "SELECT DISTINCT b.id, b.name, b.version, b.language, b.type 789 FROM {badge_related} br 790 JOIN {badge} b ON (br.relatedbadgeid = b.id OR br.badgeid = b.id) 791 WHERE (br.badgeid = :badgeid OR br.relatedbadgeid = :badgeid2) AND b.id != :badgeid3"; 792 if ($activeonly) { 793 $query .= " AND b.status <> :status"; 794 $params['status'] = BADGE_STATUS_INACTIVE; 795 } 796 $relatedbadges = $DB->get_records_sql($query, $params); 797 return $relatedbadges; 798 } 799 800 /** 801 * Insert/update alignment information of badge. 802 * 803 * @param stdClass $alignment Data of a alignment. 804 * @param int $alignmentid ID alignment. 805 * @return bool|int A status/ID when insert or update data. 806 */ 807 public function save_alignment($alignment, $alignmentid = 0) { 808 global $DB; 809 810 $record = $DB->record_exists('badge_alignment', array('id' => $alignmentid)); 811 if ($record) { 812 $alignment->id = $alignmentid; 813 return $DB->update_record('badge_alignment', $alignment); 814 } else { 815 return $DB->insert_record('badge_alignment', $alignment, true); 816 } 817 } 818 819 /** 820 * Delete a alignment of badge. 821 * 822 * @param int $alignmentid ID alignment. 823 * @return boolean A status for delete a alignment. 824 */ 825 public function delete_alignment($alignmentid) { 826 global $DB; 827 return $DB->delete_records('badge_alignment', array('badgeid' => $this->id, 'id' => $alignmentid)); 828 } 829 830 /** 831 * Get alignments of badge. 832 * 833 * @return array List content alignments. 834 */ 835 public function get_alignments() { 836 global $DB; 837 return $DB->get_records('badge_alignment', array('badgeid' => $this->id)); 838 } 839 840 /** 841 * Insert/update Endorsement information of badge. 842 * 843 * @param stdClass $endorsement Data of an endorsement. 844 * @return bool|int A status/ID when insert or update data. 845 */ 846 public function save_endorsement($endorsement) { 847 global $DB; 848 $record = $DB->get_record('badge_endorsement', array('badgeid' => $this->id)); 849 if ($record) { 850 $endorsement->id = $record->id; 851 return $DB->update_record('badge_endorsement', $endorsement); 852 } else { 853 return $DB->insert_record('badge_endorsement', $endorsement, true); 854 } 855 } 856 857 /** 858 * Get endorsement of badge. 859 * 860 * @return array|stdClass Endorsement information. 861 */ 862 public function get_endorsement() { 863 global $DB; 864 return $DB->get_record('badge_endorsement', array('badgeid' => $this->id)); 865 } 866 867 /** 868 * Markdown language support for criteria. 869 * 870 * @return string $output Markdown content to output. 871 */ 872 public function markdown_badge_criteria() { 873 $agg = $this->get_aggregation_methods(); 874 if (empty($this->criteria)) { 875 return get_string('nocriteria', 'badges'); 876 } 877 $overalldescr = ''; 878 $overall = $this->criteria[BADGE_CRITERIA_TYPE_OVERALL]; 879 if (!empty($overall->description)) { 880 $overalldescr = format_text($overall->description, $overall->descriptionformat, 881 array('context' => $this->get_context())) . '\n'; 882 } 883 // Get the condition string. 884 if (count($this->criteria) == 2) { 885 $condition = get_string('criteria_descr', 'badges'); 886 } else { 887 $condition = get_string('criteria_descr_' . BADGE_CRITERIA_TYPE_OVERALL, 'badges', 888 core_text::strtoupper($agg[$this->get_aggregation_method()])); 889 } 890 unset($this->criteria[BADGE_CRITERIA_TYPE_OVERALL]); 891 $items = array(); 892 // If only one criterion left, make sure its description goe to the top. 893 if (count($this->criteria) == 1) { 894 $c = reset($this->criteria); 895 if (!empty($c->description)) { 896 $overalldescr = $c->description . '\n'; 897 } 898 if (count($c->params) == 1) { 899 $items[] = ' * ' . get_string('criteria_descr_single_' . $c->criteriatype, 'badges') . 900 $c->get_details(); 901 } else { 902 $items[] = '* ' . get_string('criteria_descr_' . $c->criteriatype, 'badges', 903 core_text::strtoupper($agg[$this->get_aggregation_method($c->criteriatype)])) . 904 $c->get_details(); 905 } 906 } else { 907 foreach ($this->criteria as $type => $c) { 908 $criteriadescr = ''; 909 if (!empty($c->description)) { 910 $criteriadescr = $c->description; 911 } 912 if (count($c->params) == 1) { 913 $items[] = ' * ' . get_string('criteria_descr_single_' . $type, 'badges') . 914 $c->get_details() . $criteriadescr; 915 } else { 916 $items[] = '* ' . get_string('criteria_descr_' . $type, 'badges', 917 core_text::strtoupper($agg[$this->get_aggregation_method($type)])) . 918 $c->get_details() . $criteriadescr; 919 } 920 } 921 } 922 return strip_tags($overalldescr . $condition . html_writer::alist($items, array(), 'ul')); 923 } 924 925 /** 926 * Define issuer information by format Open Badges specification version 2. 927 * 928 * @param int $obversion OB version to use. 929 * @return array Issuer informations of the badge. 930 */ 931 public function get_badge_issuer(?int $obversion = null) { 932 global $DB; 933 934 $issuer = []; 935 if ($obversion == OPEN_BADGES_V1) { 936 $data = $DB->get_record('badge', ['id' => $this->id]); 937 $issuer['name'] = $data->issuername; 938 $issuer['url'] = $data->issuerurl; 939 $issuer['email'] = $data->issuercontact; 940 } else { 941 $issuer['name'] = $this->issuername; 942 $issuer['url'] = $this->issuerurl; 943 $issuer['email'] = $this->issuercontact; 944 $issuer['@context'] = OPEN_BADGES_V2_CONTEXT; 945 $issueridurl = new moodle_url('/badges/issuer_json.php', array('id' => $this->id)); 946 $issuer['id'] = $issueridurl->out(false); 947 $issuer['type'] = OPEN_BADGES_V2_TYPE_ISSUER; 948 } 949 950 return $issuer; 951 } 952} 953