Commit 42c171f9 authored by Son Nguyen's avatar Son Nguyen Committed by Robert Lyon
Browse files

Enhance the openbadgedisplayer plugin. Bug 1536393

Allow loading openbadgedisplayer block via ajax.
Dynamically load badge groups from sources.
Cache badge details in database for one day if $fromcache is true.

behatnotneeded

Change-Id: I36c8054fd6daf7ca1fcf1fe3a22672c9eb009c6e
parent 9a6a59e9
<?php
/**
*
* @package mahara
* @subpackage blocktype-openbadgedisplayer
* @author Catalyst IT Ltd
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL version 3 or later
* @copyright For copyright information on Mahara, please see the README file distributed with this software.
*
*/
define('INTERNAL', 1);
define('JSON', 1);
require(dirname(dirname(dirname(__FILE__))) . '/init.php');
safe_require('blocktype', 'openbadgedisplayer');
$host = param_variable('host', null);
$email = param_variable('email', null);
if (!isset($host) || !isset($email)) {
json_reply('local', get_string('parameterexception', 'error'));
}
// Make sure the email belongs to the current user
$emails = get_column('artefact_internal_profile_email', 'email', 'owner', $USER->id, 'verified', 1);
if (!isset($emails) || !in_array($email, $emails)) {
json_reply('local', get_string('accessdeniedbadge', 'error'));
}
$uid = PluginBlocktypeOpenbadgedisplayer::get_backpack_id($host, $email);
json_reply(false, array(
'host' => $host,
'hosttitle' => get_string('title_' . $host, 'blocktype.openbadgedisplayer'),
'uid' => $uid,
'badgegroups' => PluginBlocktypeOpenbadgedisplayer::get_badgegroupnames($host, $uid),
));
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="lib/db" VERSION="20060926" COMMENT="XMLDB file for core Mahara tables"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
>
<TABLES>
<TABLE NAME="blocktype_openbadgedisplayer_data">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" SEQUENCE="true" NOTNULL="true" />
<FIELD NAME="host" TYPE="char" LENGTH="255" NOTNULL="true" />
<FIELD NAME="uid" TYPE="int" LENGTH="10" NOTNULL="true" />
<FIELD NAME="badgegroupid" TYPE="int" LENGTH="10" NOTNULL="true" />
<FIELD NAME="name" TYPE="text" NOTNULL="false" />
<FIELD NAME="html" TYPE="text" NOTNULL="false" />
<FIELD NAME="lastupdate" TYPE="datetime" NOTNULL="false" />
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id" />
</KEYS>
<INDEXES>
<INDEX NAME="hostuidix" UNIQUE="false" FIELDS="host,uid"/>
<INDEX NAME="hostuidbadgegroupidix" UNIQUE="true" FIELDS="host,uid,badgegroupid"/>
</INDEXES>
</TABLE>
</TABLES>
</XMLDB>
......@@ -31,5 +31,24 @@ function xmldb_blocktype_openbadgedisplayer_upgrade($oldversion = 0) {
}
}
if ($oldversion < 2016030200) {
// Add a new table blocktype_openbadgedisplayer_data for storing prefetch badges
$table = new XMLDBTable('blocktype_openbadgedisplayer_data');
$table->addFieldInfo('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE);
$table->addFieldInfo('host', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL);
$table->addFieldInfo('uid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL);
$table->addFieldInfo('badgegroupid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL);
$table->addFieldInfo('name', XMLDB_TYPE_TEXT);
$table->addFieldInfo('html', XMLDB_TYPE_TEXT);
$table->addFieldInfo('lastupdate', XMLDB_TYPE_DATETIME);
$table->addKeyInfo('primary', XMLDB_KEY_PRIMARY, array('id'));
$table->addIndexInfo('hostuidix', XMLDB_INDEX_NOTUNIQUE, array('host', 'uid'));
$table->addIndexInfo('hostuidbadgegroupidix', XMLDB_INDEX_UNIQUE, array('host', 'uid', 'badgegroupid'));
create_table($table);
}
return true;
}
\ No newline at end of file
/**
* Asynchronous loading badges
*
* @package mahara
* @subpackage blocktype-openbadgedisplayer
* @author Discendum Oy
* @author Catalyst IT Ltd
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL version 3 or later
* @copyright For copyright information on Mahara, please see the README file distributed with this software.
*/
/* pieform_element_checkboxes_get_headdata() includes the javascript
needed by the "Select all/none" -links. That function isn't called
when the config form is rendered, so let's just copy the code here
and add it to window scope.*/
if (typeof pieform_element_checkboxes_update === 'undefined') {
window.pieform_element_checkboxes_update = function (p, v) {
forEach(getElementsByTagAndClassName('input', 'checkboxes', p), function(e) {
if (!e.disabled) {
e.checked = v;
}
});
if (typeof formchangemanager !== 'undefined') {
var form = jQuery('div#' + p).closest('form')[0];
formchangemanager.setFormState(form, FORM_CHANGED);
}
};
}
var badgegroups_hosts = JSON.parse(jQuery("input#instconf_hosts").val());
var badgegroups_emails = JSON.parse(jQuery("input#instconf_emails").val());
var selectedbadgegroups = JSON.parse(jQuery("input#instconf_selectedbadgegroups").val());
if ((badgegroups_hosts instanceof Array && badgegroups_hosts.length >= 1)
&& (badgegroups_emails instanceof Array && badgegroups_emails.length >= 1)) {
var count=0;
jQuery("div#instconf_loadinginfo_container > p.alert").removeClass('hidden');
for (var i=0; i < badgegroups_hosts.length; i++) {
var h = badgegroups_hosts[i];
for (var j=0; j < badgegroups_emails.length; j++) {
var e = badgegroups_emails[j];
var params = {'host': h, 'email': e};
count++;
/* Fetching the badge info via ajax and render the pieform checkbox element */
sendjsonrequest(config['wwwroot'] + '/blocktype/openbadgedisplayer/badgegroupnames.json.php', params, 'POST', function(data) {
if (!jQuery.isEmptyObject(data.badgegroups)) {
var htmlstr =
'<div id="instconf_' + data.host + '_container" class="checkboxes form-group">' +
'<span class="pseudolabel">' + data["hosttitle"] + '</span>' +
'<div class="btn-group">' +
'<a href="" class="btn btn-default btn-xs" onclick="pieform_element_checkboxes_update(\'instconf_' + data["host"] + '_container\', true); return false;">Select all</a>' +
'<a href="" class="btn btn-default btn-xs" onclick="pieform_element_checkboxes_update(\'instconf_' + data["host"] + '_container\', false); return false;">Select none</a>&nbsp;' +
'</div>';
for (var badgegroupid in data.badgegroups) {
var badgegroupname = data.badgegroups[badgegroupid];
var checkboxvalue = data["host"] + ':' + data["uid"] + ':' + badgegroupid;
var checkboxid = data["host"] + '_' + data["uid"] + '_' + badgegroupid;
var selected = '';
if (jQuery.inArray(checkboxvalue, selectedbadgegroups) != -1) {
selected = 'checked';
}
htmlstr +=
'<div class="checkboxes-option checkbox">' +
'<input type="checkbox" id="instconf_' + checkboxid + '"name="' + data["host"] + '[]" value="' + checkboxvalue + '" ' + selected + ' class="checkboxes">' +
'<label class="checkbox" for="instconf_' + checkboxid + '">' +
'<span class="accessible-hidden sr-only">' + data["hosttitle"] + ': </span>' +
badgegroupname +
'</label>' +
'</div>';
}
htmlstr +=
'<div class="cl"></div>' +
'</div>';
jQuery("div#instconf_loadinginfo_container > div").append(htmlstr);
count--;
if (count == 0) {
jQuery("div#instconf_loadinginfo_container > p.alert").addClass('hidden');
}
}
});
}
}
}
......@@ -64,3 +64,4 @@ $string['title_backpack'] = 'Mozilla Backpack';
$string['title_passport'] = 'Open Badge Passport';
$string['fetchingbadges'] = 'Fetching entries. This may take a while.';
......@@ -102,17 +102,14 @@ class PluginBlocktypeOpenbadgedisplayer extends SystemBlocktype {
if ($editing) {
$items = array();
foreach ($badgegroups as $badgegroup) {
list($host, $bid, $group) = explode(':', $badgegroup);
$backpack_url = self::get_backpack_url($host);
$res = mahara_http_request(array(CURLOPT_URL => $backpack_url . "displayer/{$bid}/groups.json"));
$json = json_decode($res->data);
if (!empty($json->groups)) {
foreach ($json->groups as $g) {
if ((int) $group === (int) $g->groupId) {
$items[] = hsc($g->name) . ' (' . get_string('nbadges', 'blocktype.openbadgedisplayer', $g->badges) . ')';
foreach ($badgegroups as $selectedbadgegroup) {
list($host, $uid, $selectedgroupid) = explode(':', $selectedbadgegroup);
$allbadgegroups = self::get_badgegroupnames($host, $uid);
if (!empty($allbadgegroups)) {
foreach ($allbadgegroups as $badgegroupid => $name) {
if ((int) $selectedgroupid === (int) $badgegroupid) {
$items[] = $name;
}
}
}
......@@ -127,7 +124,7 @@ class PluginBlocktypeOpenbadgedisplayer extends SystemBlocktype {
else {
$smarty = smarty_core();
$smarty->assign('id', $instance->get('id'));
$smarty->assign('badgehtml', self::get_badge_html($badgegroups));
$smarty->assign('badgehtml', self::get_badges_html($badgegroups));
$has_pagemodal = true;
......@@ -150,50 +147,100 @@ class PluginBlocktypeOpenbadgedisplayer extends SystemBlocktype {
return $html;
}
private static function get_badge_html($groups) {
$html = '';
$existing = array();
foreach ($groups as $group) {
$parts = explode(':', $group);
/**
* Returns html code for badge in a group
* @param string $group in format <host>:<uid>:<badgegroupid>
* @param bool $fromcache if true the info will be fetched from database first
* @return string HTML code
*/
private static function get_badge_html($group, $fromcache=false) {
if (!isset($group) && !is_string($group)) {
return '';
}
if (count($parts) < 3) {
continue;
}
$parts = explode(':', $group);
$backpack_url = self::get_backpack_url($parts[0]);
$url = $backpack_url . 'displayer/' . $parts[1] . '/group/' . $parts[2] . '.json';
$res = mahara_http_request(array(CURLOPT_URL => $url));
if (count($parts) < 3) {
return '';
}
if ($res->info['http_code'] != 200) {
continue;
$host = $parts[0];
$uid = $parts[1];
$badgegroupid = $parts[2];
// Try to get the badge html from database first
// Get badge group html using uid (backpackid)
if ($fromcache && $badgegroup = get_record_select('blocktype_openbadgedisplayer_data',
'host = ? AND uid = ? AND badgegroupid = ? AND lastupdate > ?',
array($host, $uid, $badgegroupid, db_format_timestamp(strtotime('-1 day'))),
'html')) {
if (isset($badgegroup->html)) {
return $badgegroup->html;
}
}
$json = json_decode($res->data);
$html = '';
$existing = array();
if (isset($json->badges) && is_array($json->badges)) {
$backpack_url = self::get_backpack_url($host);
$url = $backpack_url . 'displayer/' . $uid . '/group/' . $badgegroupid . '.json';
$res = mahara_http_request(array(CURLOPT_URL => $url));
foreach ($json->badges as $badge) {
$b = $badge->assertion->badge;
if ($res->info['http_code'] != 200) {
return '';
}
// TODO: Simple check for unique badges, improve me!
if (array_key_exists($b->name, $existing) && strcmp($existing[$b->name], $b->description) === 0) {
continue;
}
$json = json_decode($res->data);
if (self::assertion_has_expired($badge->assertion)) {
continue;
}
if (isset($json->badges) && is_array($json->badges)) {
foreach ($json->badges as $badge) {
$b = $badge->assertion->badge;
$html .= '<img '
. 'src="' . $b->image . '" '
. 'title="' . $b->name . '" '
. 'data-assertion="' . htmlentities(json_encode($badge->assertion)) . '" />';
// TODO: Simple check for unique badges, improve me!
if (array_key_exists($b->name, $existing) && strcmp($existing[$b->name], $b->description) === 0) {
continue;
}
$existing[$b->name] = $b->description;
if (self::assertion_has_expired($badge->assertion)) {
continue;
}
$html .= '<img '
. 'src="' . $b->image . '" '
. 'title="' . $b->name . '" '
. 'data-assertion="' . htmlentities(json_encode($badge->assertion)) . '" />';
$existing[$b->name] = $b->description;
}
}
// Caching badge info into database for better performance
if ($fromcache) {
ensure_record_exists('blocktype_openbadgedisplayer_data',
(object) array(
'host' => $host,
'uid' => $uid,
'badgegroupid' => $badgegroupid,
),
(object) array(
'host' => $host,
'uid' => $uid,
'badgegroupid' => $badgegroupid,
'html' => $html,
'lastupdate' => db_format_timestamp(time())
)
);
}
return $html;
}
private static function get_badges_html($groups) {
$html = '';
foreach ($groups as $group) {
$html .= self::get_badge_html($group);
}
return $html;
......@@ -259,66 +306,36 @@ class PluginBlocktypeOpenbadgedisplayer extends SystemBlocktype {
'badgegroups' => array(
'type' => 'container',
'class' => '',
'elements' => array()
'elements' => array(
'loadinginfo' => array(
'type' => 'html',
'class' => '',
'value' => '<p class="loading-box alert alert-info">'. get_string('fetchingbadges', 'blocktype.openbadgedisplayer') .'</p>' .
'<div></div>',
),
'hosts' => array(
'type' => 'hidden',
'value' => json_encode(array_keys($sources)),
),
'emails' => array(
'type' => 'hidden',
'value' => json_encode($addresses),
),
'selectedbadgegroups' => array(
'type' => 'hidden',
'value' => json_encode($current_values),
),
)
)
);
$groupcount = 0;
foreach (array_keys($sources) as $source) {
$groups = self::get_form_fields($source, $addresses);
$fields['badgegroups']['elements'][$source] = array(
'title' => get_string('title_' . $source, 'blocktype.openbadgedisplayer'),
'type' => 'checkboxes',
'class' => '',
'labelwidth' => false,
'elements' => $groups
);
// Set checked states for elements.
if (is_array($groups)) {
$groupcount += count($groups);
foreach ($fields['badgegroups']['elements'][$source]['elements'] as &$element) {
$element['defaultvalue'] = in_array($element['value'], $current_values);
}
}
}
if ($groupcount === 0) {
return array(
'colorcode' => array('type' => 'hidden', 'value' => ''),
'title' => array('type' => 'hidden', 'value' => ''),
'message' => array(
'type' => 'html',
'value' => '<p class="message">'. get_string('nogroups', 'blocktype.openbadgedisplayer', $sources['backpack']) .'</p>'
)
);
}
return $fields;
}
public static function get_instance_config_javascript(\BlockInstance $instance) {
// pieform_element_checkboxes_get_headdata() includes the javascript
// needed by the "Select all/none" -links. That function isn't called
// when the config form is rendered, so let's just copy the code here
// and add it to window scope.
return <<<JS
if (typeof pieform_element_checkboxes_update === 'undefined') {
window.pieform_element_checkboxes_update = function (p, v) {
forEach(getElementsByTagAndClassName('input', 'checkboxes', p), function(e) {
if (!e.disabled) {
e.checked = v;
}
});
if (typeof formchangemanager !== 'undefined') {
var form = jQuery('div#' + p).closest('form')[0];
formchangemanager.setFormState(form, FORM_CHANGED);
}
};
}
JS;
public static function get_instance_config_javascript(BlockInstance $instance) {
return array(
'js/configform.js',
);
}
private static function get_form_fields($host, $addresses) {
......@@ -342,9 +359,14 @@ JS;
}
private static function get_backpack_id($host, $email) {
public static function get_backpack_id($host, $email) {
static $backpackids = array();
$backpack_url = self::get_backpack_url($host);
if (isset($backpackids[$host][$email])) {
return $backpackids[$host][$email];
}
if ($backpack_url !== false) {
$res = mahara_http_request(
array(
......@@ -355,12 +377,112 @@ JS;
);
$res = json_decode($res->data);
if (isset($res->userId)) {
$backpackids[$host][$email] = $res->userId;
return $res->userId;
}
}
return null;
}
/**
* Returns all backpack IDs of current logged-in user
*
* @return array of backpack IDs:
* array(
* <host> => array (
* <email> => <backpackid>
* )
* )
*/
public static function get_user_backpack_ids() {
global $USER;
if (!$USER->is_logged_in()) {
return array();
}
$sources = self::get_backpack_source();
$addresses = get_column('artefact_internal_profile_email', 'email', 'owner', $USER->get('id'), 'verified', 1);
$userbackpackids = array();
if (!empty($sources) && !empty($addresses)) {
foreach ($sources as $h => $url) {
$userbackpackids[$h] = array();
foreach ($addresses as $e) {
$userbackpackids[$h][$e] = self::get_backpack_id($h, $e);
}
}
}
return $userbackpackids;
}
/**
* Return name of badge groups for a given host and backpackid
* @param $host
* @param $uid backpack ID attached to an email on the host
* @param $usedbcache if true, the badge groups will be fetched from database first
* @return array
* <badgegroupid> => <badgegroupname>
* )
*/
public static function get_badgegroupnames($host, $uid, $usedbcache=false) {
static $badgegroupnames = array();
if (!isset($host) || !isset($uid)) {
return array();
}
if (isset($badgegroupnames[$host][$uid])) {
return $badgegroupnames[$host][$uid];
}
// Get badge group names using uid (backpackid) from database
if ($usedbcache && $badgegroups = get_records_select_array('blocktype_openbadgedisplayer_data',
'host = ? AND uid = ? AND lastupdate > ?', array($host, $uid, db_format_timestamp(strtotime('-1 day'))),
'', 'badgegroupid, name')) {
foreach ($badgegroups as $badgegroup) {
$badgegroupnames[$host][$uid][$badgegroup->badgegroupid] = $badgegroup->name;
}
return $badgegroupnames[$host][$uid];
}
$badgegroupnames[$host][$uid] = array();
$backpack_url = self::get_backpack_url($host);
$res = mahara_http_request(array(CURLOPT_URL => $backpack_url . "displayer/{$uid}/groups.json"));
$res = json_decode($res->data);
if (!empty($res->groups)) {
foreach ($res->groups AS $g) {
if ($g->name == 'Public badges' && $g->groupId == 0) {
$name = get_string('obppublicbadges', 'blocktype.openbadgedisplayer');
}
else {
$name = hsc($g->name);
}
$name .= ' (' . get_string('nbadges', 'blocktype.openbadgedisplayer', $g->badges) . ')';
$badgegroupnames[$host][$uid][$g->groupId] = $name;
// Caching badge info into database for better performance
ensure_record_exists('blocktype_openbadgedisplayer_data',
(object) array(
'host' => $host,
'uid' => $uid,
'badgegroupid' => $g->groupId,
),
(object) array(
'host' => $host,
'uid' => $uid,
'badgegroupid' => $g->groupId,
'name' => $name,
'lastupdate' => db_format_timestamp(time())
)
);
}
}
return $badgegroupnames[$host][$uid];
}
private static function get_group_opt($host, $uid) {
$opt = array();
......@@ -396,7 +518,7 @@ JS;
return isset($sources[$host]) ? $sources[$host] : false;
}
private static function _sanitize_name($name) {
public static function _sanitize_name($name) {
return preg_replace('/[^a-zA-Z0-9_]/', '_', $name);
}
......@@ -405,12 +527,25 @@ JS;
// Support old save format.
$sources = array_keys(self::get_backpack_source());
$values['badgegroup'] = array();
$validbackpackids = self::get_user_backpack_ids();
foreach ($sources as $source) {
if (isset($values[$source])) {
$values['badgegroup'] = array_merge($values['badgegroup'], $values[$source]);
unset($values[$source]);
}