Conflicts:
	static/media/less/core.less
This commit is contained in:
Michael Ballard 2013-06-03 17:19:00 -04:00
Родитель b2901df59b 2065c8908f
Коммит be84537d57
11 изменённых файлов: 442 добавлений и 213 удалений

1
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 || {},

1
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);

Просмотреть файл

@ -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)
});
});

Просмотреть файл

@ -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

Просмотреть файл

@ -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 = '<span>' + display + '</span>';
} else {
content = '<a href="' + href + '">' + (display || pageNum) + '</a>';
}
return '<' + el + (className ? ' class="' + className + '"' : '') + '>' + content + '</' + el + '>';
}
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:'&laquo;', className:'disabled', el:el}));
else
pages.push(page({path:path, pageNum:current - 1, display:'&laquo', 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:'&raquo;', className:'disabled', el:el}));
else
pages.push(page({path:path, pageNum:current + 1, display:'&raquo;', el:el}));
return pages.join('');
}
next();
}
function extractMessageData (req) {
var messages = {};
var fields = {};

Просмотреть файл

@ -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;

Просмотреть файл

@ -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('<span><strong>Drop photos and videos here</strong> <em>or</em></span>')
.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();

Просмотреть файл

@ -5,26 +5,25 @@
{% block filter %}
{% if filters %}
<div class="navbar filter">
<form class="navbar-inner navbar-form form-inline" method="get">
<form class="navbar-inner navbar-form form-inline text-center" method="get">
{% for filter in filters %}
<label for="filter-{{ filter.name }}" class="filter-icon filter-{{ filter.name }}">{{ filter.label }}</label>
{% if filter.options %}
<select id="filter-{{ filter.name }}" name="{{ filter.name }}" class="input-medium{% if filter.class %} {{ filter.class }}{% endif %}">
<option></option>
{% if filter.is_grouped %}
{% for label, options in filter.options %}
<optgroup label="{{ label }}">
{% for value, label in options %}
<option value="{{ value }}"{% if filter.selected == value %} selected="selected"{% endif %}>{{ label }}</option>
{% endfor %}
</optgroup>
{% endfor %}
{% else %}
<option selected="selected" value="{{ value }}">{{ filter.name }}</option>
{% for value, label in filter.options %}
<option value="{{ value }}"{% if filter.selected == value %} selected="selected"{% endif %}>{{ label }}</option>
{% endfor %}
{% endif %}
{% if filter.is_grouped %}
{% for label, options in filter.options %}
<optgroup label="{{ label }}">
{% for value, label in options %}
<option value="{{ value }}"{% if filter.selected == value %} selected="selected"{% endif %}>{{ label }}</option>
{% endfor %}
</optgroup>
{% endfor %}
{% else %}
{% for item in filter.options %}
<option value="{{ item.value }}"{% if filter.selected == item.value %} selected="selected"{% endif %}>{{ item.label }}</option>
{% endfor %}
{% endif %}
</select>
{% else %}
<input type="{{ filter.type | default('text') }}" id="filter-{{ filter.name }}" name="{{ filter.name }}" class="input-medium{% if filter.class %} {{ filter.class }}{% endif %}"{% if filter.value %} value="{{ filter.value }}"{% endif %}>
@ -36,62 +35,33 @@
{% endif %}
{% endblock %}
{% block list_wrapper %}
{% if items.length %}
<ul class="thumbnails">
{% block list %}
{% for item in items %}
{% block item %}
<li class="span3">
{% if item.template %}
{% include item.template %}
{% else %}
<figure class="thumbnail">
<a href="{{ item.url }}"><img src="{{ item.image }}"></a>
<figcaption class="caption">
<p>{{ item.name }}</p>
<p class="description">{{ item.description | default('description tbd') | truncate(20) }}</p>
{% block item_actions_wrapper %}
<p class="text-right">
{% block item_actions %}
<a href="{{ item.url }}" class="btn">Details</a>
{% endblock %}
</p>
{% endblock %}
</figcaption>
</figure>
{% endif %}
{% endblock %}
</li>
{% endfor %}
{% endblock %}
</ul>
{% else %}
{% block no_data %}
No data found.
{% endblock %}
{% endif %}
{% if items.length %}
<ul class="thumbnails">
{% block list %}
{% for item in items %}
{% block item %}
<li class="span3">
{% if item.template %}
{% include item.template %}
{% else %}
{% include "includes/badge-thumbnail.html" %}
{% endif %}
{% endblock %}
</li>
{% endfor %}
{% endblock %}
</ul>
{% else %}
{% block no_data %}
No data found.
{% endblock %}
{% endif %}
{% endblock %}
{% block pagination %}
{% if pages > 1 %}
<nav class="pagination pagination-centered">
<ul>
{% if page == 1 %}
<li class="disabled"><span>&laquo;</span></li>
{% else %}
<li><a href="?page={{ page - 1 }}">&laquo;</a></li>
{% endif %}
{% for pageNum in range(1, (pages + 1)) %}
{% if page == pageNum %}
<li class="disabled"><span>{{ pageNum }}</span></li>
{% else %}
<li><a href="?page={{ pageNum }}">{{ pageNum }}</a></li>
{% endif %}
{% endfor %}
{% if page == pages %}
<li class="disabled"><span>&raquo;</span></li>
{% else %}
<li><a href="?page={{ page + 1 }}">&raquo;</a></li>
{% endif %}
{{ paginate(pages, page) }}
</ul>
</nav>
{% endif %}

Просмотреть файл

@ -0,0 +1,17 @@
{# A badge in a list or grid context #}
<figure class="thumbnail">
<a href="{{ item.url }}"><img src="{{ item.image }}"></a>
<figcaption class="caption">
<p>{{ item.name }}</p>
{% if item.description %}
<p class="description">{{ item.description | truncate(20) }}</p>
{% endif %}
{% block item_actions_wrapper %}
<p class="text-right">
{% block item_actions %}
<a href="{{ item.url }}" class="btn">Details</a>
{% endblock %}
</p>
{% endblock %}
</figcaption>
</figure>

Просмотреть файл

@ -3,45 +3,46 @@
{% set navItem = 'backpack' %}
{% block list %}
{{ super() }}
<li class="span3">
<figure class="thumbnail">
<a href="/claim"><img src="/media/img/add-badge.png"></a>
<figcaption class="caption">
<p>Claim another badge!</p>
<p class="text-right"><a href="/claim" class="btn">+1</a></p>
</figcaption>
</figure>
</li>
{{ super() }}
<li class="span3">
<figure class="thumbnail">
<a href="/claim"><img src="/media/img/add-badge.png"></a>
<figcaption class="caption">
<p>Claim another badge!</p>
<p class="text-right"><a href="/claim" class="btn">+1</a></p>
</figcaption>
</figure>
</li>
{% endblock %}
{% block item_actions %}
{{ super() }}
<a href="#" title="Delete this badge"><i class="icon-trash"></i></a>
<a class="btn show-tooltip" title="Add to your favorites" href="{{ item.url }}/favorite"><i class="icon-heart"></i></a>
{#
{{ super() }}
<a class="btn show-tooltip" title="Add to your favorites" href="{{ item.url }}/favorite"><i class="icon-heart"></i></a>
#}
{% endblock %}
{% block modal %}
<!-- Modal -->
<div id="shareModal" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h3 id="myModalLabel">Share This Badge</h3>
</div>
<div class="modal-body">
<a href="#" class="btn btn-block"><i class="icon-twitter"></i> Twitter</a>
<a href="#" class="btn btn-block"><i class="icon-facebook"></i> Facebook</a>
<a href="#" class="btn btn-block">Mozilla Backpack</a>
</div>
</div>
<div id="trashModal" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h3 id="myModalLabel">Are you sure you want to delete this badge?</h3>
</div>
<div class="modal-body">
<a href="#" class="btn btn-block">Cancel</a>
<a href="#" class="btn btn-block">Yes</a>
</div>
</div>
<!-- Modal -->
<div id="shareModal" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h3 id="myModalLabel">Share This Badge</h3>
</div>
<div class="modal-body">
<a href="#" class="btn btn-block"><i class="icon-twitter"></i> Twitter</a>
<a href="#" class="btn btn-block"><i class="icon-facebook"></i> Facebook</a>
<a href="#" class="btn btn-block">Mozilla Backpack</a>
</div>
</div>
<div id="trashModal" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h3 id="myModalLabel">Are you sure you want to delete this badge?</h3>
</div>
<div class="modal-body">
<a href="#" class="btn btn-block">Cancel</a>
<a href="#" class="btn btn-block">Yes</a>
</div>
</div>
{% endblock %}

Просмотреть файл

@ -8,8 +8,36 @@
<img src="{{ badge.image }}">
</div>
<div class="span8">
<h3>What is this badge about?</h3>
<p>{{ badge.description }}</p>
<p>
<b>Issued by:</b>
{% if badge.program.issuer.url %}
<a href="{{ badge.program.issuer.url }}">
{% endif %}
{{ badge.program.issuer.name }}
{% if badge.program.issuer.url %}
</a>
{% endif %}
</p>
<p>
<b>Issued to:</b> {{ user.email }}
</p>
<h3>What is this badge about?</h3>
<p>{{ badge.description }}</p>
</div>
</div>
<div class="row">
<h3>Similar Badges</h3>
<p>Cupcake <a href="#">link to Badges page</a> something about badges
should go here.</p>
{% for item in similar %}
<div class="span3">
{% include "includes/badge-thumbnail.html" %}
</div>
{% endfor %}
</div>
{% endblock %}