diff --git a/api.js b/api.js index 3dadbad..1be419b 100644 --- a/api.js +++ b/api.js @@ -23,6 +23,7 @@ function middleware (method, default_query) { // Build query from various inputs var query = _.extend( + {}, DEFAULT_QUERY, default_query || {}, req.query || {}, diff --git a/app.js b/app.js index 20cd9c3..9ed9e21 100644 --- a/app.js +++ b/app.js @@ -33,6 +33,7 @@ app.use(flash()); app.use(helpers.addCsrfToken); app.use(helpers.addRangeMethod); +app.use(helpers.addPaginateMethod); app.use(helpers.addMessages); require('./controllers/auth')(app); diff --git a/controllers/backpack.js b/controllers/backpack.js index dc73c40..1964def 100644 --- a/controllers/backpack.js +++ b/controllers/backpack.js @@ -86,9 +86,42 @@ module.exports = function (app) { ], function (req, res, next) { var data = req.remote; + // XXX: replace with API call to openbadger + var similar = [ + { + url: "/mybadges/this-badge", + image: "http://openbadger-csol.mofostaging.net/badge/image/this-badge.png", + name: "Test Badge CLM", + description: "This is a test badge!" + }, + { + url: "/mybadges/this-badge", + image: "http://openbadger-csol.mofostaging.net/badge/image/this-badge.png", + name: "Test Badge CLM", + description: "This is a test badge!" + }, + { + url: "/mybadges/this-badge", + image: "http://openbadger-csol.mofostaging.net/badge/image/this-badge.png", + name: "Test Badge CLM", + description: "This is a test badge!" + }, + { + url: "/mybadges/this-badge", + image: "http://openbadger-csol.mofostaging.net/badge/image/this-badge.png", + name: "Test Badge CLM", + description: "This is a test badge!" + } + ]; + + const NSIMILAR = 4; + console.log(data.badge); + res.render('user/badge.html', { - badge: data.badge + badge: data.badge, + user: req.session.user, + similar: similar.slice(0, NSIMILAR) }); }); diff --git a/controllers/program.js b/controllers/program.js index 904d734..c2d68e4 100644 --- a/controllers/program.js +++ b/controllers/program.js @@ -11,96 +11,22 @@ var evidence = db.model('Evidence'); module.exports = function (app) { - function getFilters() { - var filters = [], - requested; + function getFilters(query, subset) { + var all = badger.getFilters(), + filters = []; - if (arguments.length) { - requested = Array.prototype.splice.call(arguments, 0); - } else { - requested = ['categories', 'grouped_programs', 'ages']; - } + query = query || {}; - requested.forEach(function(filter) { - switch (filter) { - case 'categories': - case 'category': - filters.push({ - name: 'category', - label: 'Category', - options: { - science: 'Science', - technology: 'Technology', - engineering: 'Engineering', - art: 'Art', - math: 'Math' - } - }); - break; - case 'orgs': - case 'org': - filters.push({ - name: 'org', - label: 'Organization', - options: { - 'org1': 'Org 1', - 'org2': 'Org 2', - 'org3': 'Org 3', - } - }); - break; - case 'programs': - case 'program': - filters.push({ - name: 'program', - label: 'Program', - options: { - 'p1': 'Program 1', - 'p2': 'Program 2', - 'p3': 'Program 3', - 'p4': 'Program 4', - 'p5': 'Program 5', - 'p6': 'Program 6' - } - }); - break; - case 'grouped_programs': - case 'grouped_program': - filters.push({ - name: 'program', - label: 'Program', - options: { - 'Org 1': { - 'p1': 'Program 1', - 'p2': 'Program 2' - }, - 'Org 2': { - 'p3': 'Program 3', - 'p4': 'Program 4' - }, - 'Org 3': { - 'p5': 'Program 5', - 'p6': 'Program 6' - } - }, - is_grouped: true - }); - break; - case 'ages': - case 'age': - filters.push({ - name: 'age', - label: 'Age Group', - options: { - 'lt-13': 'Under 13', - '13-14': '13 to 14', - '15-16': '15 to 16', - '17-18': '17 to 18', - 'gt-18': 'Over 18' - } - }); - break; - } + if (!subset || !subset.length) + subset = _.keys(all); + + if (subset && !_.isArray(subset)) + subset = [subset]; + + _.each(subset, function (item) { + var filter = all[item] || {name:item, label: item, options: []}; + filter.selected = query[filter.name]; + filters.push(filter); }); return filters; @@ -120,7 +46,7 @@ module.exports = function (app) { var data = req.remote; res.render('programs/list.html', { - filters: getFilters('categories', 'orgs', 'ages'), + filters: getFilters(req.query, ['categories', 'orgs', 'ageRanges', 'activityTypes']), items: data.programs, page: data.page, pages: data.pages @@ -285,7 +211,7 @@ module.exports = function (app) { var data = req.remote; res.render('badges/list.html', { - filters: getFilters(), + filters: getFilters(req.query, ['categories', 'ageRanges', 'badgeTypes', 'activityTypes']), items: data.badges, page: data.page, pages: data.pages diff --git a/helpers.js b/helpers.js index a3f9578..6628b82 100644 --- a/helpers.js +++ b/helpers.js @@ -1,3 +1,5 @@ +const querystring = require('querystring'); +const url = require('url'); const _ = require('underscore'); @@ -13,6 +15,116 @@ exports.addRangeMethod = function addRangeMethod (req, res, next) { next(); }; +exports.addPaginateMethod = function addPaginateMethod (req, res, next) { + function page (options) { + var path = options.path, + pageNum = options.pageNum, + display = options.display, + className = options.className, + el = options.el; + + var query = querystring.parse(path.query), + content, + href; + + if (pageNum === 1) + delete query.page + else + query.page = pageNum + + query = querystring.stringify(query); + href = path.href.replace(/\?.*$/, '') + (query ? '?' + query : ''); + + if (!pageNum) { + content = '' + display + ''; + } else { + content = '' + (display || pageNum) + ''; + } + + return '<' + el + (className ? ' class="' + className + '"' : '') + '>' + content + ''; + } + + function generatePageNumbers (total, current, maxItems) { + maxItems = maxItems || 12; + + if (total <= maxItems) + return Array(total).join(',').split(',').map(function(e,i){return i+1;}); + + var paged = {} + pages = [], + count = 1, + extraItems = Math.min(3, Math.floor((maxItems - 3) / 4)); + + paged[current] = current; + + for (var i = 1, max = Math.min(total, extraItems + 1); i <= max; ++i) + (paged[i] = i) && count++; + + for (var i = Math.max(1, total - extraItems); i <= total; ++i) + (paged[i] = i) && count++; + + var i = 1; + + while (count < maxItems - 1) { + if (!paged[current - i] && (current - i) >= 1) + (paged[current - i] = current - i) && count++; + if (!paged[current + i] && (current + i) <= total) + (paged[current + i] = current + i) && count++; + i++; + } + + var previous, + current; + + for (var i in paged) { + current = paged[i]; + if (previous === current - 2) + pages.push(current - 1); + else if (previous <= current - 3) + pages.push('...'); + pages.push(current); + previous = current; + } + + return pages; + } + + res.locals.paginate = function (count, current, path, extraItems, el) { + current = current || 1; + path = url.parse(path || req.url); + el = el || 'li'; + + var pages = [], + pageNums = generatePageNumbers(count, current, extraItems), + pageNum; + + if (current === 1) + pages.push(page({path:path, display:'«', className:'disabled', el:el})); + else + pages.push(page({path:path, pageNum:current - 1, display:'«', el:el})); + + for (var i = 0, l = pageNums.length; i < l; ++i) { + pageNum = pageNums[i]; + + if (pageNum === current) + pages.push(page({path:path, display:pageNum, className:'active', el:el})); + else if (!parseInt(pageNum,10)) + pages.push(page({path:path, display:pageNum, el:el})); + else + pages.push(page({path:path, pageNum:pageNum, el:el})); + } + + if (current === count) + pages.push(page({path:path, display:'»', className:'disabled', el:el})); + else + pages.push(page({path:path, pageNum:current + 1, display:'»', el:el})); + + return pages.join(''); + } + + next(); +} + function extractMessageData (req) { var messages = {}; var fields = {}; diff --git a/openbadger.js b/openbadger.js index ec0c8cd..6d12928 100644 --- a/openbadger.js +++ b/openbadger.js @@ -49,30 +49,135 @@ function normalizeProgram(program, id) { return program; } -function filterBadges(data, query) { - // TO DO - We should probably be a little less naive about this, and make sure - // that these values are from an allowed list +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: 'Activity', value: 'activity'} +]; +var orgs = []; - var category = query.category, - ageGroup = query.age, - program = query.program; +function updateOrgs (callback) { + if (typeof callback !== 'function') + callback = function () {}; - data = _.filter(data, function(item) { - if (category && !_.contains(item.categories, category)) - return false; + openbadger.getOrgs(function (err, data) { + if (err) + return callback(err); - if (ageGroup && !_.contains(item.ageRange, ageGroup)) - return false; + orgs = []; - if (program && item.program !== program) - return false; + (data.orgs || data.issuers).forEach(function (org) { + orgs.push({ + label: org.name, + value: org.shortname + }); + }); - return true; + 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 filterBadges (data, query) { + var category = confirmFilterValue(query.category, categories), + ageGroup = confirmFilterValue(query.age, ageRanges), + badgeType = confirmFilterValue(query.type, badgeTypes), + activityType = confirmFilterValue(query.activity, activityTypes); + + if (!category && !ageGroup && !badgeType && !activityType) + return data; + + return applyFilter(data, { + 'categories': category, + 'ageRange': ageGroup, + 'badgeType': badgeType, + 'activityType': activityType }); return data; } +function filterPrograms (data, query) { + var category = confirmFilterValue(query.category, categories), + org = confirmFilterValue(query.org, orgs), + ageGroup = confirmFilterValue(query.age, ageRanges), + activityType = confirmFilterValue(query.activity, activityTypes); + + if (!category && !org && !ageGroup && !activityType) + return data; + + return applyFilter(data, { + 'categories': category, + 'issuer.shortname': org, + 'ageRange': ageGroup, + 'activityType': activityType + }); +} + function getJWTToken(email) { var claims = { prn: email, @@ -126,6 +231,7 @@ var openbadger = new Api(ENDPOINT, { }); }); }, + filters: filterPrograms, paginate: true, key: 'programs' }, @@ -168,8 +274,6 @@ var openbadger = new Api(ENDPOINT, { if (err) return callback(err, data); - - console.log(data); badges = _.map(data.badges, normalizeBadgeInstance) return callback(null, { @@ -229,4 +333,36 @@ var openbadger = new Api(ENDPOINT, { }, }); +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 + } + }; +} +module.exports.updateOrgs = updateOrgs; \ No newline at end of file diff --git a/static/media/js/jquery.uploader.js b/static/media/js/jquery.uploader.js index b500232..ebf7c8c 100644 --- a/static/media/js/jquery.uploader.js +++ b/static/media/js/jquery.uploader.js @@ -72,7 +72,7 @@ $template = $(options.template), $buttons = $(options.buttonContainer), $description = $(document.createElement('div')), - $label = $(document.createElement('label')), + $btn = $(document.createElement('span')), itemSelector = '.' + $template[0].className.replace(/\s+/g, '.'), itemCount = 0, xhr = (window.XMLHttpRequest && new XMLHttpRequest()) || {}; @@ -81,16 +81,20 @@ $template.remove(); + $template.find('input[type="file"]').attr('tabIndex', -1); + $description .addClass('description') .html('Drop photos and videos here or') .prependTo($template); - $label + $btn .addClass('btn') .text('Choose photos and videos to upload') .appendTo($description) - .click(function() { $(this).parents('.item').find('input').click(); }); + .attr('tabIndex', 0) + .click(function() { $(this).parents('.item').find('input').click(); }) + .keypress(function() { $(this).click(); }); if (xhr.upload && window.FormData) { goAsync(); diff --git a/views/filter.html b/views/filter.html index 92c0834..322d9d1 100644 --- a/views/filter.html +++ b/views/filter.html @@ -5,26 +5,25 @@ {% block filter %} {% if filters %}
-

What is this badge about?

-

{{ badge.description }}

+

+ Issued by: + {% if badge.program.issuer.url %} + + {% endif %} + {{ badge.program.issuer.name }} + {% if badge.program.issuer.url %} + + {% endif %} +

+ +

+ Issued to: {{ user.email }} +

+ +

What is this badge about?

+

{{ badge.description }}

+ +
+

Similar Badges

+

Cupcake link to Badges page something about badges + should go here.

+ + {% for item in similar %} +
+ {% include "includes/badge-thumbnail.html" %} +
+ {% endfor %} + +
{% endblock %}