moved openbadger out of csol-site, doesn't actually work, but tests pass. needs more tests. :fistbump:
This commit is contained in:
Коммит
8458bcd3e6
|
@ -0,0 +1,275 @@
|
|||
var request = require('request');
|
||||
var errors = require('./errors');
|
||||
var _ = require('underscore');
|
||||
var url = require('url');
|
||||
|
||||
|
||||
var DEFAULT_ERROR = 'There was a problem accessing this data.';
|
||||
var DEFAULT_QUERY = {
|
||||
page: 1,
|
||||
pageSize: 12
|
||||
};
|
||||
|
||||
function middleware (method, default_query) {
|
||||
if (!_.isFunction(method))
|
||||
method = this[method];
|
||||
|
||||
if (!_.isFunction(method)) {
|
||||
throw new Error('Supplied method ' + method + ' not valid');
|
||||
}
|
||||
|
||||
return function (req, res, next) {
|
||||
|
||||
// Build query from various inputs
|
||||
var query = _.extend(
|
||||
{},
|
||||
DEFAULT_QUERY,
|
||||
default_query || {},
|
||||
req.query || {},
|
||||
req.body || {},
|
||||
req.params || {},
|
||||
{ session: req.session || {} } // TODO: move session to separate arg
|
||||
);
|
||||
|
||||
method(query, function(err, data) {
|
||||
if (!data || _.isString(data))
|
||||
data = {message: data || DEFAULT_ERROR};
|
||||
|
||||
if (!_.isObject(data))
|
||||
data = {data: data};
|
||||
|
||||
if (req.xhr) {
|
||||
data.status = err ? err.status.toLowerCase() : 'ok';
|
||||
return res.json(data);
|
||||
}
|
||||
|
||||
// Mutiple API calls are supported: to prevent clobbering existing data,
|
||||
// use different keys for each call in the 'data' object.
|
||||
req.remote = _.extend(req.remote || {}, data);
|
||||
|
||||
if (err)
|
||||
return next(err);
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper for API methods
|
||||
// Normalises input and output
|
||||
function apiMethod (method) {
|
||||
return function (query, callback) {
|
||||
if (_.isFunction(query) && !callback) {
|
||||
callback = query;
|
||||
query = {};
|
||||
}
|
||||
|
||||
if (!_.isFunction(callback))
|
||||
callback = function() {};
|
||||
|
||||
// Assume any non-object query is being passed in as an ID
|
||||
if (!_.isObject(query))
|
||||
query = {id: query};
|
||||
|
||||
query = _.defaults(query, DEFAULT_QUERY);
|
||||
|
||||
method(query, function(err, data) {
|
||||
if (err)
|
||||
return callback(err, data);
|
||||
|
||||
callback(null, data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getFullUrl(origin, path) {
|
||||
if (!_.isObject(origin))
|
||||
origin = url.parse(origin);
|
||||
|
||||
path = path || '';
|
||||
path = path.replace(/^\/?/, '');
|
||||
|
||||
return url.format(_.extend(
|
||||
origin,
|
||||
{ pathname: origin.path + path }));
|
||||
}
|
||||
|
||||
// Load data from remote endpoint
|
||||
// TODO - need to add ability to pass data through
|
||||
// TODO - might want to cache this at some point
|
||||
function remote (method, path, options, callback) {
|
||||
if (!request[method])
|
||||
return callback(new errors.NotImplemented('Unknown method ' + method));
|
||||
|
||||
if (_.isFunction(options)) {
|
||||
callback = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
if (this.defaultOptions && _.isObject(this.defaultOptions)) {
|
||||
options = _.defaults(options, this.defaultOptions);
|
||||
}
|
||||
|
||||
var endpointUrl = getFullUrl(this.origin, path);
|
||||
request[method](endpointUrl, options, function(err, response, body) {
|
||||
|
||||
console.log('info', 'API request: "%s %s" %s',
|
||||
method.toUpperCase(), endpointUrl, response ? response.statusCode : "Error", err);
|
||||
|
||||
if (err)
|
||||
return callback(new errors.Unknown(err));
|
||||
|
||||
if (response.statusCode >= 300) {
|
||||
var msg;
|
||||
|
||||
if (!_.isObject(body)) {
|
||||
try {
|
||||
body = JSON.parse(body) || {};
|
||||
} catch (e) {
|
||||
body = {};
|
||||
};
|
||||
}
|
||||
|
||||
body.code = response.statusCode;
|
||||
msg = body.message || body.reason || errors.lookup(response.statusCode).status;
|
||||
|
||||
return callback(new errors.BadGateway(msg, body));
|
||||
}
|
||||
|
||||
try {
|
||||
var data = body;
|
||||
if (!_.isObject(body))
|
||||
data = JSON.parse(data);
|
||||
} catch (e) {
|
||||
return callback(new errors.Unknown(e.message));
|
||||
}
|
||||
|
||||
if ('status' in data && _.isString(data.status) && data.status.slice(0, 2) !== 'ok')
|
||||
return callback(new errors.Unknown(data.reason || body.message), data);
|
||||
|
||||
callback(null, data);
|
||||
});
|
||||
}
|
||||
|
||||
function paginate(key, dataFn) {
|
||||
if (!dataFn && _.isFunction(key)) {
|
||||
dataFn = key;
|
||||
key = 'data';
|
||||
}
|
||||
|
||||
return function(query, callback) {
|
||||
var pageSize = parseInt(query.pageSize, 10),
|
||||
page = parseInt(query.page, 10);
|
||||
|
||||
if (isNaN(pageSize) || pageSize < 1)
|
||||
return callback(new errors.BadRequest('Invalid pageSize number'));
|
||||
|
||||
if (isNaN(page) || page < 1)
|
||||
return callback(new errors.BadRequest('Invalid page number'));
|
||||
|
||||
var start = (page - 1) * pageSize,
|
||||
end = start + pageSize;
|
||||
|
||||
dataFn(query, function(err, data) {
|
||||
if (err)
|
||||
return callback(err, data);
|
||||
|
||||
if (typeof data[key].length !== 'number')
|
||||
return callback(new errors.BadGateway('Unpageable data returned from upstream'), data);
|
||||
|
||||
var pages = Math.ceil(data[key].length / pageSize);
|
||||
|
||||
if (pages > 0 && page > pages)
|
||||
return callback(new errors.NotFound('Page not found'), {
|
||||
page: page,
|
||||
pages: pages
|
||||
});
|
||||
|
||||
data[key] = data[key].slice(start, end);
|
||||
callback(null, _.extend(data, {
|
||||
page: page,
|
||||
pages: pages,
|
||||
}));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function addFilters(filters, key, dataFn) {
|
||||
if (!dataFn && _.isFunction(key)) {
|
||||
dataFn = key;
|
||||
key = 'data';
|
||||
}
|
||||
|
||||
return function(query, callback) {
|
||||
dataFn(query, function(err, data) {
|
||||
if (err)
|
||||
return callback(err, data);
|
||||
|
||||
if (typeof data[key].length === 'number') {
|
||||
_.each(filters, function (filter) {
|
||||
data[key] = filter(data[key], query);
|
||||
});
|
||||
}
|
||||
|
||||
callback(null, data);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function Api(origin, globalFilters, config) {
|
||||
if (!config) {
|
||||
config = globalFilters;
|
||||
globalFilters = [];
|
||||
}
|
||||
|
||||
config = config || {};
|
||||
globalFilters = globalFilters || [];
|
||||
|
||||
if (_.isFunction(globalFilters))
|
||||
globalFilters = [globalFilters];
|
||||
|
||||
origin = url.parse(origin);
|
||||
this.origin = origin;
|
||||
|
||||
_.each(['get', 'post', 'put', 'patch', 'head', 'del'], function(method) {
|
||||
Object.defineProperty(this, method, {
|
||||
enumerable: true,
|
||||
value: function(path, opts, callback) {
|
||||
this.remote(method, path, opts, callback);
|
||||
},
|
||||
/* TODO: writable is set to true for mocking, but it would
|
||||
be nice to revisit and try to remove that line. */
|
||||
writable: true
|
||||
});
|
||||
}, this);
|
||||
|
||||
_.each(config, function(item, name) {
|
||||
var methodConfig = _.isObject(item) ? item : {};
|
||||
var method = _.isFunction(item) ? item : methodConfig.func;
|
||||
var key = methodConfig.key || 'data';
|
||||
var filters = methodConfig.filters || [];
|
||||
|
||||
if (_.isFunction(filters))
|
||||
filters = [filters];
|
||||
|
||||
method = method.bind(this);
|
||||
|
||||
if (filters.length)
|
||||
method = addFilters(filters, key, method);
|
||||
|
||||
if (globalFilters.length)
|
||||
method = addFilters(globalFilters, key, method);
|
||||
|
||||
if (methodConfig.paginate)
|
||||
method = paginate(key, method);
|
||||
|
||||
this[name] = apiMethod(method);
|
||||
}, this);
|
||||
|
||||
_.extend(this, {
|
||||
middleware: middleware,
|
||||
remote: remote,
|
||||
getFullUrl: getFullUrl
|
||||
});
|
||||
|
||||
};
|
|
@ -0,0 +1,121 @@
|
|||
var _ = require('underscore');
|
||||
|
||||
function middleware (env, config) {
|
||||
return function(err, req, res, next) {
|
||||
if (req.xhr)
|
||||
return res.json(err);
|
||||
|
||||
var templates = [
|
||||
err.status,
|
||||
err.code,
|
||||
Unknown.status,
|
||||
Unknown.code
|
||||
];
|
||||
|
||||
templates.forEach(function(item, index, list) {
|
||||
var templateName = (''+item)
|
||||
.replace(/\s+/g, '-')
|
||||
.toLowerCase();
|
||||
list[index] = './errors/' + templateName + '.html';
|
||||
});
|
||||
|
||||
function render (template) {
|
||||
if (!template)
|
||||
return res.send(err);
|
||||
|
||||
try {
|
||||
template = env.getTemplate(template);
|
||||
res.status(err.code || 500);
|
||||
res.send(template.render(_.defaults({
|
||||
error: err
|
||||
}, res.locals)));
|
||||
} catch (e) {
|
||||
if (e.name === 'Template render error')
|
||||
console.log('Error rendering template:', template.path);
|
||||
render(templates.shift());
|
||||
}
|
||||
}
|
||||
|
||||
render(templates.shift());
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function (app, env, config) {
|
||||
// Generate a 404 - if we've got this far, the page doesn't exist
|
||||
app.use(function(req, res, next) {
|
||||
next(new NotFound(req.url));
|
||||
})
|
||||
|
||||
// It would be nice not to have to pass env around, but it doesn't
|
||||
// seem to be available through `app`, and there doesn't seem to be
|
||||
// a nice way of catching a failed template rendering attempt.
|
||||
app.use(middleware(env, config));
|
||||
}
|
||||
|
||||
var register = {};
|
||||
|
||||
function createExceptionType (name, code) {
|
||||
if (!code) code = 500;
|
||||
|
||||
var status = name
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/(^\s+|\s+$)/g, '')
|
||||
.replace(/\w[A-Z]/g, function(m) { return m[0] + ' ' + m[1]; });
|
||||
|
||||
function Exception (msg, meta) {
|
||||
this.name = name;
|
||||
this.status = status;
|
||||
this.code = code;
|
||||
this.message = msg || status;
|
||||
this.meta = meta;
|
||||
Error.call(this, this.message);
|
||||
Error.captureStackTrace(this, arguments.callee);
|
||||
}
|
||||
|
||||
Exception.prototype = new Error();
|
||||
Exception.prototype.constructor = Exception;
|
||||
Exception.prototype.toString = function () {
|
||||
var msg = '[' + this.name + ' Exception: ' + this.message + ']';
|
||||
|
||||
if ('DEBUG' in process.env)
|
||||
return this.stack ? msg + this.stack : '';
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
Exception.status = status;
|
||||
Exception.code = code;
|
||||
|
||||
register[code] = Exception;
|
||||
|
||||
return Exception;
|
||||
}
|
||||
|
||||
var BadRequest = createExceptionType('BadRequest', 400);
|
||||
var Unauthorized = createExceptionType('Unauthorized', 401);
|
||||
var Forbidden = createExceptionType('Forbidden', 403);
|
||||
var NotFound = createExceptionType('NotFound', 404);
|
||||
var NotAllowed = createExceptionType('MethodNotAllowed', 405);
|
||||
var Conflict = createExceptionType('Conflict', 409);
|
||||
var Unsupported = createExceptionType('UnsupportedMediaType', 415);
|
||||
var Unknown = createExceptionType('Internal', 500);
|
||||
var NotImplemented = createExceptionType('NotImplemented', 501);
|
||||
var BadGateway = createExceptionType('BadGateway', 502);
|
||||
|
||||
module.exports.BadRequest = BadRequest;
|
||||
module.exports.Unauthorized = Unauthorized;
|
||||
module.exports.Forbidden = Forbidden;
|
||||
module.exports.NotFound = NotFound;
|
||||
module.exports.NotAllowed = NotAllowed;
|
||||
module.exports.Conflict = Conflict;
|
||||
module.exports.Unsupported = Unsupported;
|
||||
module.exports.Unknown = Unknown;
|
||||
module.exports.NotImplemented = NotImplemented;
|
||||
module.exports.BadGateway = BadGateway;
|
||||
|
||||
module.exports.lookup = function lookup (code) {
|
||||
if (!register[code])
|
||||
code = 500;
|
||||
|
||||
return register[code];
|
||||
};
|
|
@ -0,0 +1,440 @@
|
|||
const Api = require('./api');
|
||||
const errors = require('./errors');
|
||||
const _ = require('underscore');
|
||||
const jwt = require('jwt-simple');
|
||||
const async = require('async');
|
||||
|
||||
const ENDPOINT = process.env['CSOL_OPENBADGER_URL'];
|
||||
const JWT_SECRET = process.env['CSOL_OPENBADGER_SECRET'];
|
||||
const TOKEN_LIFETIME = process.env['CSOL_OPENBADGER_TOKEN_LIFETIME'] || 10000;
|
||||
|
||||
if (!ENDPOINT)
|
||||
throw new Error('Must specify CSOL_OPENBADGER_URL in the environment');
|
||||
if (!JWT_SECRET)
|
||||
throw new Error('Must specify CSOL_OPENBADGER_SECRET in the environment');
|
||||
|
||||
function normalizeBadge (badge, id) {
|
||||
if (badge.shortname)
|
||||
badge.id = badge.shortname;
|
||||
|
||||
if (!id)
|
||||
id = badge.shortname;
|
||||
|
||||
if (!badge.id)
|
||||
badge.id = id;
|
||||
|
||||
if (!badge.url)
|
||||
badge.url = '/earn/' + badge.id;
|
||||
|
||||
return badge;
|
||||
}
|
||||
|
||||
function normalizeBadgeInstance (badge, id) {
|
||||
/* This is dumb, but let's us reuse current templates to
|
||||
build out a single-level object. */
|
||||
_.extend(badge, badge.badgeClass);
|
||||
|
||||
if (!badge.id)
|
||||
badge.id = id;
|
||||
|
||||
if (!badge.url)
|
||||
badge.url = '/mybadges/' + id;
|
||||
|
||||
badge.id = id;
|
||||
|
||||
return badge;
|
||||
}
|
||||
|
||||
function normalizeProgram(program, id) {
|
||||
if (!id)
|
||||
id = program.shortname;
|
||||
|
||||
if (!program.id)
|
||||
program.id = id;
|
||||
|
||||
program.localUrl = '/explore/' + program.shortname;
|
||||
|
||||
return program;
|
||||
}
|
||||
|
||||
var categories = [
|
||||
{label: 'Science', value: 'science'},
|
||||
{label: 'Technology', value: 'technology'},
|
||||
{label: 'Engineering', value: 'engineering'},
|
||||
{label: 'Art', value: 'art'},
|
||||
{label: 'Math', value: 'math'}
|
||||
];
|
||||
var ageRanges = [
|
||||
{label: 'Under 13', value: '0-13'},
|
||||
{label: '13-18', value: '13-18'},
|
||||
{label: '19-24', value: '19-24'}
|
||||
];
|
||||
var activityTypes = [
|
||||
{label: 'Online', value: 'online'},
|
||||
{label: 'Offline', value: 'offline'}
|
||||
];
|
||||
var badgeTypes = [
|
||||
{label: 'Participation', value: 'participation'},
|
||||
{label: 'Skill', value: 'skill'},
|
||||
{label: 'Achievement', value: 'achievement'}
|
||||
];
|
||||
var orgs = [];
|
||||
|
||||
function updateOrgs (callback) {
|
||||
if (typeof callback !== 'function')
|
||||
callback = function () {};
|
||||
|
||||
openbadger.getOrgs(function (err, data) {
|
||||
if (err)
|
||||
return callback(err);
|
||||
|
||||
orgs = [];
|
||||
|
||||
(data.orgs || data.issuers).forEach(function (org) {
|
||||
orgs.push({
|
||||
label: org.name,
|
||||
value: org.shortname
|
||||
});
|
||||
});
|
||||
|
||||
orgs.sort(function(a, b) {
|
||||
var aVal = (a && a.label || '').toLowerCase().replace(/^\s*the\s+/, ''),
|
||||
bVal = (b && b.label || '').toLowerCase().replace(/^\s*the\s+/, '');
|
||||
|
||||
return aVal.localeCompare(bVal);
|
||||
});
|
||||
|
||||
callback(null, orgs);
|
||||
});
|
||||
}
|
||||
|
||||
function confirmFilterValue (value, list) {
|
||||
if (!value && value !== 0)
|
||||
return null;
|
||||
|
||||
for (var i = 0, l = list.length; i < l; ++i)
|
||||
if (list[i].value === value)
|
||||
return value;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function applyFilter (data, query) {
|
||||
return _.filter(data, function(item) {
|
||||
return _.reduce(query, function(memo, value, field) {
|
||||
if (!memo) // We've already failed a test - no point in continuing
|
||||
return memo;
|
||||
|
||||
if (!value && value !== 0)
|
||||
return memo;
|
||||
|
||||
var data = item;
|
||||
|
||||
if (field.indexOf('.') > -1) {
|
||||
var fieldParts = field.split('.').reverse();
|
||||
|
||||
while (data && fieldParts.length > 1) {
|
||||
data = data[fieldParts.pop()];
|
||||
}
|
||||
|
||||
field = fieldParts.reverse().join('.');
|
||||
}
|
||||
|
||||
var itemValue = data ? data[field] : null;
|
||||
|
||||
if (_.isArray(itemValue))
|
||||
return memo && _.contains(itemValue, value);
|
||||
|
||||
return memo && (itemValue === value);
|
||||
}, true);
|
||||
})
|
||||
}
|
||||
|
||||
function getJWTToken(email) {
|
||||
var claims = {
|
||||
prn: email,
|
||||
exp: Date.now() + TOKEN_LIFETIME
|
||||
};
|
||||
return jwt.encode(claims, JWT_SECRET);
|
||||
}
|
||||
|
||||
function handleAutoAwards(email, learner, autoAwardedBadges) {
|
||||
if (autoAwardedBadges && autoAwardedBadges.length > 0) {
|
||||
async.map(autoAwardedBadges, function(shortname, cb) {
|
||||
openbadger.getUserBadge({ id: shortname, email: email }, cb);
|
||||
}, function(err, results) {
|
||||
if (err) {
|
||||
console.error('info', 'Failed to get user badges from openbadger for email %s', email);
|
||||
return;
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var openbadger = new Api(ENDPOINT, {
|
||||
|
||||
getBadges: {
|
||||
func: function getBadges (query, callback) {
|
||||
this.getAllBadges(query, callback);
|
||||
},
|
||||
paginate: true,
|
||||
key: 'badges'
|
||||
},
|
||||
|
||||
getAllBadges: function getAllBadges (query, callback) {
|
||||
var category = confirmFilterValue(query.category, categories),
|
||||
ageGroup = confirmFilterValue(query.age, ageRanges),
|
||||
badgeType = confirmFilterValue(query.type, badgeTypes),
|
||||
activityType = confirmFilterValue(query.activity, activityTypes);
|
||||
|
||||
this.get('/badges', {qs: {search: query.search, category: category, ageGroup: ageGroup, badgeType: badgeType, activityType: activityType }}, function(err, data) {
|
||||
if (err)
|
||||
return callback(err, data);
|
||||
|
||||
return callback(null, {
|
||||
badges: _.map(data.badges, normalizeBadge)
|
||||
});
|
||||
})
|
||||
},
|
||||
|
||||
getBadge: function getBadge (query, callback) {
|
||||
var id = query.id;
|
||||
|
||||
if (!id)
|
||||
return callback(new errors.BadRequest('Invalid badge key'));
|
||||
|
||||
this.get('/badge/' + id, function(err, data) {
|
||||
if (err)
|
||||
return callback(err, data);
|
||||
|
||||
return callback(null, {
|
||||
badge: normalizeBadge(data.badge, id)
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
getPrograms: {
|
||||
func: function getPrograms (query, callback) {
|
||||
var qs = {
|
||||
category: query.category,
|
||||
org: query.org,
|
||||
age: query.age,
|
||||
activity: query.activity,
|
||||
search: query.search,
|
||||
};
|
||||
this.get('/programs', {qs: qs}, function(err, data) {
|
||||
if (err)
|
||||
return callback(err, data);
|
||||
|
||||
return callback(null, {
|
||||
programs: _.map(data.programs, normalizeProgram)
|
||||
});
|
||||
});
|
||||
},
|
||||
paginate: true,
|
||||
key: 'programs'
|
||||
},
|
||||
|
||||
getProgram: function getProgram (query, callback) {
|
||||
var id = query.id;
|
||||
|
||||
if (!id)
|
||||
return callback(new errors.BadRequest('Invalid program key'));
|
||||
|
||||
this.get('/program/' + id, function(err, data) {
|
||||
if (err)
|
||||
return callback(err, data);
|
||||
|
||||
return callback(null, {
|
||||
program: normalizeProgram(data.program, id)
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
getOrgs: function getOrgs (query, callback) {
|
||||
this.get('/issuers/', function(err, data) {
|
||||
if (err)
|
||||
return callback(err, data);
|
||||
|
||||
return callback(null, {
|
||||
orgs: _.values(data.issuers)
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
getUserBadges: {
|
||||
func: function getUserBadges (query, callback) {
|
||||
var email = query.email || query.session.user.email;
|
||||
var params = {
|
||||
auth: getJWTToken(email),
|
||||
email: email
|
||||
};
|
||||
this.get('/user', { qs: params }, function(err, data) {
|
||||
if (err)
|
||||
return callback(err, data);
|
||||
|
||||
badges = _.map(data.badges, normalizeBadgeInstance)
|
||||
|
||||
return callback(null, {
|
||||
badges: badges.sort(function(a, b) {
|
||||
return b.issuedOn - a.issuedOn;
|
||||
})
|
||||
});
|
||||
});
|
||||
},
|
||||
paginate: true,
|
||||
key: 'badges'
|
||||
},
|
||||
|
||||
getUserBadge: function getUserBadge (query, callback) {
|
||||
var id = query.id;
|
||||
|
||||
var email = query.email || query.session.user.email;
|
||||
var params = {
|
||||
auth: getJWTToken(email),
|
||||
email: email
|
||||
};
|
||||
|
||||
this.get('/user/badge/' + id, { qs: params }, function(err, data) {
|
||||
if (err)
|
||||
return callback(err, data);
|
||||
|
||||
return callback(null, {
|
||||
badge: normalizeBadgeInstance(data.badge, id)
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
awardBadge: function awardBadge (query, callback) {
|
||||
var email = query.learner ? query.learner.email : query.session.user.email;
|
||||
var shortname = query.badge;
|
||||
|
||||
var params = {
|
||||
auth: getJWTToken(email),
|
||||
email: email
|
||||
}
|
||||
|
||||
this.post('/user/badge/' + shortname, { form: params }, function(err, data) {
|
||||
if (err)
|
||||
return callback(err, data);
|
||||
|
||||
handleAutoAwards(email, query.learner, data.autoAwardedBadges);
|
||||
|
||||
return callback(null, {
|
||||
assertionUrl: data.url
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
getBadgeFromCode: function getBadgeFromCode (query, callback) {
|
||||
var email = query.email;
|
||||
var code = query.code;
|
||||
var params = {
|
||||
auth: getJWTToken(email),
|
||||
email: email,
|
||||
code: code,
|
||||
};
|
||||
this.get('/unclaimed', { qs: params }, function(err, data) {
|
||||
return callback(err, data);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
claim: function claim (query, callback) {
|
||||
var email = query.learner ? query.learner.email : null;
|
||||
var code = query.code;
|
||||
var params = {
|
||||
auth: getJWTToken(email),
|
||||
email: email,
|
||||
code: code,
|
||||
};
|
||||
this.post('/claim', { json: params }, function(err, data) {
|
||||
if (err)
|
||||
return callback(err);
|
||||
|
||||
handleAutoAwards(email, query.learner, data.autoAwardedBadges);
|
||||
|
||||
return callback(null, data);
|
||||
});
|
||||
},
|
||||
|
||||
getBadgeRecommendations: function getBadgeRecommendations (query, callback) {
|
||||
var badgename = query.badgeName;
|
||||
var id = query.id;
|
||||
var limit = query.limit;
|
||||
var params = {
|
||||
limit: limit
|
||||
};
|
||||
|
||||
if (badgename)
|
||||
id = badgename
|
||||
|
||||
if (!id)
|
||||
return callback(new errors.BadRequest('Invalid badge key'));
|
||||
|
||||
this.get('/badge/' + id + '/recommendations', { qs: params }, function(err, data) {
|
||||
if (err)
|
||||
return callback(err, data);
|
||||
|
||||
return callback(null, {
|
||||
badges: _.map(data.badges, normalizeBadge)
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
getUserRecommendations: function getUserRecommendations (query, callback) {
|
||||
var user = query.session.user;
|
||||
var email = user.email;
|
||||
var params = {
|
||||
auth: getJWTToken(email),
|
||||
email: email
|
||||
};
|
||||
this.get('/user/recommendations', {qs: params}, function(err, data) {
|
||||
if (err)
|
||||
return callback(err, null);
|
||||
|
||||
return callback(null, {
|
||||
recommendations: _.map(data.badges, normalizeBadge)
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
updateOrgs();
|
||||
|
||||
module.exports = openbadger;
|
||||
module.exports.getFilters = function getFilters () {
|
||||
return {
|
||||
categories: {
|
||||
name: 'category',
|
||||
label: 'Category',
|
||||
options: categories
|
||||
},
|
||||
ageRanges: {
|
||||
name: 'age',
|
||||
label: 'Age',
|
||||
options: ageRanges
|
||||
},
|
||||
orgs: {
|
||||
name: 'org',
|
||||
label: 'Organization',
|
||||
options: orgs
|
||||
},
|
||||
activityTypes: {
|
||||
name: 'activity',
|
||||
label: 'Activity',
|
||||
options: activityTypes
|
||||
},
|
||||
badgeTypes: {
|
||||
name: 'type',
|
||||
label: 'Type',
|
||||
options: badgeTypes
|
||||
},
|
||||
search: {
|
||||
name: 'search',
|
||||
label: 'Search'
|
||||
}
|
||||
};
|
||||
}
|
||||
module.exports.updateOrgs = updateOrgs;
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "openbadger-client",
|
||||
"version": "0.0.1",
|
||||
"description": "A client for the OpenBadger API",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "NODE_ENV=test ./node_modules/.bin/tap-prettify test/*.test.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MPLv2",
|
||||
"dependencies": {
|
||||
"tap-prettify": "0.0.2",
|
||||
"tap": "~0.4.4",
|
||||
"sinon": "~1.7.3"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,290 @@
|
|||
['CSOL_OPENBADGER_URL',
|
||||
'CSOL_OPENBADGER_SECRET',
|
||||
'CSOL_IREMIX_URL',
|
||||
'CSOL_IREMIX_USER',
|
||||
'CSOL_IREMIX_PASS'].forEach(function(name) {
|
||||
if (!process.env[name])
|
||||
process.env[name] = 'FAKEVALUE';
|
||||
});
|
||||
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const sinon = require('sinon');
|
||||
var openbadger = require('..');
|
||||
var mock = sinon.mock(openbadger);
|
||||
|
||||
const DATA = {
|
||||
'badges': {
|
||||
status: 'ok',
|
||||
badges: {
|
||||
'link-basic': {
|
||||
name: 'Link Badge, basic',
|
||||
description: 'For doing links.',
|
||||
prerequisites: [],
|
||||
image: 'http://openbadger-csol.mofostaging.net/badge/image/link-basic.png',
|
||||
behaviors: [ { name: 'link', score: 5 } ]
|
||||
},
|
||||
'link-advanced': {
|
||||
name: 'Link Badge, advanced',
|
||||
description: 'For doing links, but like, a lot of them',
|
||||
prerequisites: [],
|
||||
image: 'http://openbadger-csol.mofostaging.net/badge/image/link-advanced.png',
|
||||
behaviors: [ { name: 'link', score: 5 } ]
|
||||
},
|
||||
comment: {
|
||||
name: 'Commenting badge',
|
||||
description: 'For doing lots of comments.',
|
||||
prerequisites: [],
|
||||
image: 'http://openbadger-csol.mofostaging.net/badge/image/comment.png',
|
||||
behaviors: [ { name: 'link', score: 5 } ]
|
||||
}
|
||||
}
|
||||
},
|
||||
'badge': {
|
||||
status: 'ok',
|
||||
badge: {
|
||||
name: 'Link Badge, basic',
|
||||
description: 'For doing links.',
|
||||
prerequisites: [],
|
||||
image: 'http://openbadger-csol.mofostaging.net/badge/image/link-basic.png',
|
||||
behaviors: [ { name: 'link', score: 5 } ]
|
||||
}
|
||||
},
|
||||
'programs': {
|
||||
status: 'ok',
|
||||
programs: {
|
||||
"prog-a": {
|
||||
image: "http://some.org/prog-a/img.png",
|
||||
name: "Program A",
|
||||
shortname: "program-a"
|
||||
},
|
||||
"prog-b": {
|
||||
image: "http://some.org/prog-b/img.png",
|
||||
name: "Program B",
|
||||
shortname: "program-b"
|
||||
},
|
||||
"prog-c": {
|
||||
image: "http://some.org/prog-c/img.png",
|
||||
name: "Program C",
|
||||
shortname: "program-c"
|
||||
}
|
||||
}
|
||||
},
|
||||
'program': {
|
||||
status: 'ok',
|
||||
program: {
|
||||
image: "http://some.org/prog-a/img.png",
|
||||
name: "Program A",
|
||||
shortname: "program-a"
|
||||
}
|
||||
},
|
||||
'issuers': {
|
||||
status: 'ok',
|
||||
issuers: {
|
||||
'issuer-a': {
|
||||
name: "Issuer A",
|
||||
url: "http://issuer-a.org"
|
||||
},
|
||||
'issuer-b': {
|
||||
name: "Issuer B",
|
||||
url: "http://issuer-b.org"
|
||||
}
|
||||
}
|
||||
},
|
||||
'claim': {
|
||||
status: 'ok',
|
||||
url: 'http://some-url.org/assertion'
|
||||
}
|
||||
};
|
||||
|
||||
const DEFAULT_QUERY = {
|
||||
pageSize: 5,
|
||||
page: 1
|
||||
};
|
||||
|
||||
test('getBadge', function(t) {
|
||||
|
||||
t.test('called without id', function(t) {
|
||||
var getStub = mock.expects('get').never();
|
||||
openbadger.getBadge(function(err, data) {
|
||||
t.notOk(getStub.called, 'no call');
|
||||
t.similar(err, { code: 400, message: 'Invalid badge key' });
|
||||
t.notOk(data);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
t.test('on error', function(t) {
|
||||
var getStub = mock.expects('get');
|
||||
getStub.callsArgWith(1, 404, 'barf');
|
||||
openbadger.getBadge({ id: 'whatever' }, function(err, data) {
|
||||
t.ok(getStub.calledOnce, "called");
|
||||
t.similar(err, 404);
|
||||
t.same(data, "barf", "error message");
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
t.test('on success', function(t) {
|
||||
var getStub = mock.expects('get');
|
||||
getStub.callsArgWith(1, null, DATA['badge']);
|
||||
openbadger.getBadge({ id: 'some-id' }, function(err, data) {
|
||||
t.notOk(err, "no error");
|
||||
t.ok(getStub.calledWithMatch('/badge/some-id'), 'remote endpoint');
|
||||
t.similar(data.badge, { name: "Link Badge, basic"}, 'badge');
|
||||
t.similar(data.badge, { id: 'some-id', url: '/earn/some-id' }, 'normalized');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
test('getBadges', function(t){
|
||||
const CALLBACK_INDEX = 2;
|
||||
|
||||
t.test('on error', function(t) {
|
||||
var getStub = mock.expects('get');
|
||||
getStub.callsArgWith(CALLBACK_INDEX, 500, 'error of some sort');
|
||||
openbadger.getBadges(DEFAULT_QUERY, function(err, data) {
|
||||
t.same(err, 500, 'error');
|
||||
t.same(data, 'error of some sort', 'data');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
t.test('with data', function(t) {
|
||||
var getStub = mock.expects('get');
|
||||
getStub.callsArgWith(CALLBACK_INDEX, null, DATA['badges']);
|
||||
openbadger.getBadges(DEFAULT_QUERY, function(err, data) {
|
||||
t.notOk(err, 'no error');
|
||||
t.same(data.badges.length, 3, 'data length');
|
||||
var badge = data.badges[0];
|
||||
t.ok(badge.id && badge.url && badge.name && badge.behaviors, 'looks like normalized badge');
|
||||
t.ok(getStub.calledWithMatch('/badges'), 'endpoint');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
t.test('paginates', function(t) {
|
||||
var getStub = mock.expects('get');
|
||||
getStub.callsArgWith(CALLBACK_INDEX, null, DATA['badges']);
|
||||
openbadger.getBadges({ pageSize: 2, page: 1 }, function(err, data) {
|
||||
t.notOk(err, 'no error');
|
||||
t.same(data.badges.length, 2, 'paginated');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
test('getProgram', function(t) {
|
||||
|
||||
t.test('called without id', function(t) {
|
||||
var getStub = mock.expects('get').never();
|
||||
openbadger.getProgram(function(err, data) {
|
||||
t.notOk(getStub.called, 'no call');
|
||||
t.similar(err, { code: 400, message: "Invalid program key" });
|
||||
t.notOk(data);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
t.test('on error', function(t) {
|
||||
var getStub = mock.expects('get');
|
||||
getStub.callsArgWith(1, 404, 'barf');
|
||||
openbadger.getProgram({ id: 'whatever' }, function(err, data) {
|
||||
t.ok(getStub.calledOnce, "called");
|
||||
t.same(err, 404);
|
||||
t.same(data, "barf", "error message");
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
t.test('on success', function(t) {
|
||||
var getStub = mock.expects('get');
|
||||
getStub.callsArgWith(1, null, DATA['program']);
|
||||
openbadger.getProgram({ id: 'some-id' }, function(err, data) {
|
||||
t.notOk(err, "no error");
|
||||
t.ok(getStub.calledWithMatch('/program/some-id'), 'endpoint');
|
||||
t.similar(data.program, { name: "Program A" }, 'program');
|
||||
t.similar(data.program, { id: 'some-id', localUrl: '/explore/program-a' }, 'normalized');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
test('getPrograms', function(t) {
|
||||
const CALLBACK_INDEX = 2;
|
||||
|
||||
t.test('on error', function(t) {
|
||||
var getStub = mock.expects('get');
|
||||
getStub.callsArgWith(CALLBACK_INDEX, 500, 'error of some sort');
|
||||
openbadger.getPrograms(DEFAULT_QUERY, function(err, data) {
|
||||
t.same(err, 500, 'error');
|
||||
t.similar(data, 'error of some sort', 'data');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
t.test('with data', function(t) {
|
||||
var getStub = mock.expects('get');
|
||||
getStub.callsArgWith(CALLBACK_INDEX, null, DATA['programs']);
|
||||
openbadger.getPrograms(DEFAULT_QUERY, function(err, data) {
|
||||
t.notOk(err, 'no error');
|
||||
t.same(data.programs.length, 3, 'data length');
|
||||
var program = data.programs[0];
|
||||
t.ok(program.id && program.localUrl && program.name, 'looks like normalized program');
|
||||
t.ok(getStub.calledWithMatch('/programs'), 'endpoint');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
t.test('paginates', function(t) {
|
||||
var getStub = mock.expects('get');
|
||||
getStub.callsArgWith(CALLBACK_INDEX, null, DATA['programs']);
|
||||
openbadger.getPrograms({ pageSize: 2, page: 1 }, function(err, data) {
|
||||
t.notOk(err, 'no error');
|
||||
t.same(data.programs.length, 2, 'paginated');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
test('getIssuers', function(t) {
|
||||
|
||||
t.test('with data', function(t) {
|
||||
var getStub = mock.expects('get');
|
||||
getStub.callsArgWith(1, null, DATA['issuers']);
|
||||
openbadger.getOrgs(DEFAULT_QUERY, function(err, data) {
|
||||
t.notOk(err, 'no error');
|
||||
t.same(data.orgs.length, 2, 'data length');
|
||||
var org = data.orgs[0];
|
||||
t.ok(org.url && org.name, 'needed data');
|
||||
t.ok(getStub.calledWithMatch('/issuers'), 'endpoint');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
test('claim', function(t) {
|
||||
|
||||
t.test('with data', function(t) {
|
||||
var postStub = mock.expects('post');
|
||||
postStub.callsArgWith(2, null, DATA['claim']);
|
||||
openbadger.claim({
|
||||
code: 'CLAIMCODE',
|
||||
learner: { email: 'EMAIL' }
|
||||
}, function(err, data) {
|
||||
t.notOk(err, 'no error');
|
||||
var opts = postStub.args[0][1];
|
||||
t.ok(opts.json, 'post with json data');
|
||||
t.ok(opts.json.auth, 'contains auth');
|
||||
t.similar(opts.json, { email: 'EMAIL', code: 'CLAIMCODE' }, 'params');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
Загрузка…
Ссылка в новой задаче