milestones page working
This commit is contained in:
parent
a3a8c38a6b
commit
79e3253e5a
40
TODO.md
40
TODO.md
|
@ -2,45 +2,6 @@
|
|||
|
||||
##Release: MVP
|
||||
|
||||
###Main
|
||||
|
||||
- [ ] reorg code into its own little modules; have a consistent naming strategy, get an inspiration somewhere and stick to t
|
||||
|
||||
For `LABELS` strategy:
|
||||
|
||||
- [ ] the page says whether we need to know progress for a particular milestone, all of them, all projects.
|
||||
- [ ] based on previous fetch all issues
|
||||
- [ ] determine the total issues size of a milestone and the closed issues size
|
||||
- [ ] save this information on the milestone
|
||||
|
||||
For `ONE_SIZE` stŕategy:
|
||||
|
||||
- [ ] we need not fetch all issues, progress is determined based on `open_issues` and `closed_issues` keys
|
||||
|
||||
For both strategies:
|
||||
|
||||
- [ ] based on the page we are at (chart), we may need to fetch issues for a milestone, do that
|
||||
|
||||
```coffeescript
|
||||
{
|
||||
'name': 'radekstepan'
|
||||
'owner': 'disposable'
|
||||
'milestones': [
|
||||
{
|
||||
'issues':
|
||||
'open':
|
||||
'github': obj
|
||||
'size': 15 # in points be it LABELS or ONE_SIZE
|
||||
'closed':
|
||||
'size': 7
|
||||
'github': obj
|
||||
'progress': 0.66
|
||||
'github': obj
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
###GitHub
|
||||
|
||||
- [ ] progress needs to be calculated based on strategy even on homepage, then sort the milestones based on priority
|
||||
|
@ -60,6 +21,7 @@ For both strategies:
|
|||
- [ ] check that we have not run out of requests to make
|
||||
- [ ] can we get more than 1 notification at a time?
|
||||
- [ ] save in memory only if no `localStorage`, warn about that
|
||||
- [ ] what if milestone does not match our strategy?
|
||||
|
||||
###Bugs
|
||||
|
||||
|
|
|
@ -39104,13 +39104,58 @@ Router.prototype.mount = function(routes, path) {
|
|||
"datetime": /^(\d{4}-\d{2}-\d{2})T(.*)/,
|
||||
"size_label": /^size (\d+)$/,
|
||||
"location": /^#!((\/[^\/]+){2,3})$/,
|
||||
"points": 'LABELS'
|
||||
"points": 'ONE_SIZE'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// firebase.coffee
|
||||
root.require.register('burnchart/src/models/firebase.js', function(exports, require, module) {
|
||||
|
||||
var Model, config, user;
|
||||
|
||||
Model = require('../utils/model');
|
||||
|
||||
user = require('./user');
|
||||
|
||||
config = require('./config');
|
||||
|
||||
module.exports = new Model({
|
||||
'name': 'models/firebase',
|
||||
auth: function() {
|
||||
throw 'Not overriden';
|
||||
},
|
||||
login: function(cb) {
|
||||
return this.auth.login(config.data.provider, {
|
||||
'rememberMe': true,
|
||||
'scope': 'public_repo'
|
||||
});
|
||||
},
|
||||
logout: function() {
|
||||
var _ref;
|
||||
if ((_ref = this.auth) != null) {
|
||||
_ref.logout;
|
||||
}
|
||||
return user.reset();
|
||||
},
|
||||
onrender: function() {
|
||||
var client,
|
||||
_this = this;
|
||||
this.set('client', client = new Firebase("https://" + config.data.firebase + ".firebaseio.com"));
|
||||
return this.auth = new FirebaseSimpleLogin(client, function(err, obj) {
|
||||
user.set('loaded', true);
|
||||
if (err || !obj) {
|
||||
throw err;
|
||||
}
|
||||
return user.set(obj);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// projects.coffee
|
||||
root.require.register('burnchart/src/models/projects.js', function(exports, require, module) {
|
||||
|
||||
|
@ -39216,133 +39261,14 @@ Router.prototype.mount = function(routes, path) {
|
|||
});
|
||||
|
||||
// chart.coffee
|
||||
root.require.register('burnchart/src/modules/chart.js', function(exports, require, module) {
|
||||
root.require.register('burnchart/src/modules/draw/chart.js', function(exports, require, module) {
|
||||
|
||||
var config,
|
||||
__indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
|
||||
var config;
|
||||
|
||||
config = require('../models/config');
|
||||
|
||||
module.exports = {
|
||||
'actual': function(collection, created_at, total, cb) {
|
||||
var head, max, min, range, rest;
|
||||
head = [
|
||||
{
|
||||
'date': new Date(created_at),
|
||||
'points': total
|
||||
}
|
||||
];
|
||||
min = +Infinity;
|
||||
max = -Infinity;
|
||||
rest = _.map(collection, function(issue) {
|
||||
var closed_at, size;
|
||||
size = issue.size, closed_at = issue.closed_at;
|
||||
if (size < min) {
|
||||
min = size;
|
||||
}
|
||||
if (size > max) {
|
||||
max = size;
|
||||
}
|
||||
issue.date = new Date(closed_at);
|
||||
issue.points = total -= size;
|
||||
return issue;
|
||||
});
|
||||
range = d3.scale.linear().domain([min, max]).range([5, 8]);
|
||||
rest = _.map(rest, function(issue) {
|
||||
issue.radius = range(issue.size);
|
||||
return issue;
|
||||
});
|
||||
return cb(null, [].concat(head, rest));
|
||||
},
|
||||
'ideal': function(a, b, total, cb) {
|
||||
var cutoff, d, days, length, m, now, once, velocity, y, _ref, _ref1;
|
||||
if (b < a) {
|
||||
_ref = [a, b], b = _ref[0], a = _ref[1];
|
||||
}
|
||||
_ref1 = _.map(a.match(config.get('chart.datetime'))[1].split('-'), function(v) {
|
||||
return parseInt(v);
|
||||
}), y = _ref1[0], m = _ref1[1], d = _ref1[2];
|
||||
cutoff = new Date(b);
|
||||
days = [];
|
||||
length = 0;
|
||||
(once = function(inc) {
|
||||
var day, day_of;
|
||||
day = new Date(y, m - 1, d + inc);
|
||||
if (!(day_of = day.getDay())) {
|
||||
day_of = 7;
|
||||
}
|
||||
if (__indexOf.call(config.get('chart.off_days'), day_of) >= 0) {
|
||||
days.push({
|
||||
date: day,
|
||||
off_day: true
|
||||
});
|
||||
} else {
|
||||
length += 1;
|
||||
days.push({
|
||||
date: day
|
||||
});
|
||||
}
|
||||
if (!(day > cutoff)) {
|
||||
return once(inc + 1);
|
||||
}
|
||||
})(0);
|
||||
velocity = total / (length - 1);
|
||||
days = _.map(days, function(day, i) {
|
||||
day.points = total;
|
||||
if (days[i] && !days[i].off_day) {
|
||||
total -= velocity;
|
||||
}
|
||||
return day;
|
||||
});
|
||||
if ((now = new Date()) > cutoff) {
|
||||
days.push({
|
||||
date: now,
|
||||
points: 0
|
||||
});
|
||||
}
|
||||
return cb(null, days);
|
||||
},
|
||||
'trendline': function(actual, created_at, due_on) {
|
||||
var a, b, b1, c1, e, fn, intercept, l, last, slope, start, values;
|
||||
start = +actual[0].date;
|
||||
values = _.map(actual, function(_arg) {
|
||||
var date, points;
|
||||
date = _arg.date, points = _arg.points;
|
||||
return [+date - start, points];
|
||||
});
|
||||
last = actual[actual.length - 1];
|
||||
values.push([+new Date() - start, last.points]);
|
||||
b1 = 0;
|
||||
e = 0;
|
||||
c1 = 0;
|
||||
a = (l = values.length) * _.reduce(values, function(sum, _arg) {
|
||||
var a, b;
|
||||
a = _arg[0], b = _arg[1];
|
||||
b1 += a;
|
||||
e += b;
|
||||
c1 += Math.pow(a, 2);
|
||||
return sum + (a * b);
|
||||
}, 0);
|
||||
slope = (a - (b1 * e)) / ((l * c1) - (Math.pow(b1, 2)));
|
||||
intercept = (e - (slope * b1)) / l;
|
||||
fn = function(x) {
|
||||
return slope * x + intercept;
|
||||
};
|
||||
created_at = new Date(created_at);
|
||||
due_on = due_on ? new Date(due_on) : new Date();
|
||||
a = created_at - start;
|
||||
b = due_on - start;
|
||||
return [
|
||||
{
|
||||
date: created_at,
|
||||
points: fn(a)
|
||||
}, {
|
||||
date: due_on,
|
||||
points: fn(b)
|
||||
}
|
||||
];
|
||||
},
|
||||
'render': function(_arg, cb) {
|
||||
render: function(_arg, cb) {
|
||||
var actual, height, ideal, line, m, mAxis, margin, svg, tooltip, trendline, width, x, xAxis, y, yAxis, _ref;
|
||||
actual = _arg[0], ideal = _arg[1], trendline = _arg[2];
|
||||
document.querySelector('#svg').innerHTML = '';
|
||||
|
@ -39411,75 +39337,183 @@ Router.prototype.mount = function(routes, path) {
|
|||
|
||||
});
|
||||
|
||||
// firebase.coffee
|
||||
root.require.register('burnchart/src/modules/firebase.js', function(exports, require, module) {
|
||||
// line.coffee
|
||||
root.require.register('burnchart/src/modules/draw/line.js', function(exports, require, module) {
|
||||
|
||||
var Class, config, user;
|
||||
var __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
|
||||
|
||||
user = require('../models/user');
|
||||
|
||||
config = require('../models/config');
|
||||
|
||||
Class = (function() {
|
||||
function Class() {
|
||||
var _this = this;
|
||||
this.client = new Firebase("https://" + (config.get('firebase')) + ".firebaseio.com");
|
||||
this.auth = new FirebaseSimpleLogin(this.client, function(err, obj) {
|
||||
user.set('loaded', true);
|
||||
if (err || !obj) {
|
||||
return _this.authCb(err);
|
||||
module.exports = {
|
||||
actual: function(collection, created_at, total, cb) {
|
||||
var head, max, min, range, rest;
|
||||
head = [
|
||||
{
|
||||
'date': new Date(created_at),
|
||||
'points': total
|
||||
}
|
||||
return user.set(obj);
|
||||
];
|
||||
min = +Infinity;
|
||||
max = -Infinity;
|
||||
rest = _.map(collection, function(issue) {
|
||||
var closed_at, size;
|
||||
size = issue.size, closed_at = issue.closed_at;
|
||||
if (size < min) {
|
||||
min = size;
|
||||
}
|
||||
if (size > max) {
|
||||
max = size;
|
||||
}
|
||||
issue.date = new Date(closed_at);
|
||||
issue.points = total -= size;
|
||||
return issue;
|
||||
});
|
||||
range = d3.scale.linear().domain([min, max]).range([5, 8]);
|
||||
rest = _.map(rest, function(issue) {
|
||||
issue.radius = range(issue.size);
|
||||
return issue;
|
||||
});
|
||||
return cb(null, [].concat(head, rest));
|
||||
},
|
||||
ideal: function(a, b, total, cb) {
|
||||
var cutoff, d, days, length, m, now, once, velocity, y, _ref, _ref1;
|
||||
if (b < a) {
|
||||
_ref = [a, b], b = _ref[0], a = _ref[1];
|
||||
}
|
||||
_ref1 = _.map(a.match(config.get('chart.datetime'))[1].split('-'), function(v) {
|
||||
return parseInt(v);
|
||||
}), y = _ref1[0], m = _ref1[1], d = _ref1[2];
|
||||
cutoff = new Date(b);
|
||||
days = [];
|
||||
length = 0;
|
||||
(once = function(inc) {
|
||||
var day, day_of;
|
||||
day = new Date(y, m - 1, d + inc);
|
||||
if (!(day_of = day.getDay())) {
|
||||
day_of = 7;
|
||||
}
|
||||
if (__indexOf.call(config.get('chart.off_days'), day_of) >= 0) {
|
||||
days.push({
|
||||
date: day,
|
||||
off_day: true
|
||||
});
|
||||
} else {
|
||||
length += 1;
|
||||
days.push({
|
||||
date: day
|
||||
});
|
||||
}
|
||||
if (!(day > cutoff)) {
|
||||
return once(inc + 1);
|
||||
}
|
||||
})(0);
|
||||
velocity = total / (length - 1);
|
||||
days = _.map(days, function(day, i) {
|
||||
day.points = total;
|
||||
if (days[i] && !days[i].off_day) {
|
||||
total -= velocity;
|
||||
}
|
||||
return day;
|
||||
});
|
||||
if ((now = new Date()) > cutoff) {
|
||||
days.push({
|
||||
date: now,
|
||||
points: 0
|
||||
});
|
||||
}
|
||||
return cb(null, days);
|
||||
},
|
||||
trend: function(actual, created_at, due_on) {
|
||||
var a, b, b1, c1, e, fn, intercept, l, last, slope, start, values;
|
||||
start = +actual[0].date;
|
||||
values = _.map(actual, function(_arg) {
|
||||
var date, points;
|
||||
date = _arg.date, points = _arg.points;
|
||||
return [+date - start, points];
|
||||
});
|
||||
last = actual[actual.length - 1];
|
||||
values.push([+new Date() - start, last.points]);
|
||||
b1 = 0;
|
||||
e = 0;
|
||||
c1 = 0;
|
||||
a = (l = values.length) * _.reduce(values, function(sum, _arg) {
|
||||
var a, b;
|
||||
a = _arg[0], b = _arg[1];
|
||||
b1 += a;
|
||||
e += b;
|
||||
c1 += Math.pow(a, 2);
|
||||
return sum + (a * b);
|
||||
}, 0);
|
||||
slope = (a - (b1 * e)) / ((l * c1) - (Math.pow(b1, 2)));
|
||||
intercept = (e - (slope * b1)) / l;
|
||||
fn = function(x) {
|
||||
return slope * x + intercept;
|
||||
};
|
||||
created_at = new Date(created_at);
|
||||
due_on = due_on ? new Date(due_on) : new Date();
|
||||
a = created_at - start;
|
||||
b = due_on - start;
|
||||
return [
|
||||
{
|
||||
date: created_at,
|
||||
points: fn(a)
|
||||
}, {
|
||||
date: due_on,
|
||||
points: fn(b)
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
Class.prototype.authCb = function() {};
|
||||
|
||||
Class.prototype.login = function(cb) {
|
||||
if (!this.client) {
|
||||
return cb('Client is not setup');
|
||||
}
|
||||
this.authCb = cb;
|
||||
return this.auth.login(config.get('provider'), {
|
||||
'rememberMe': true,
|
||||
'scope': 'public_repo'
|
||||
});
|
||||
};
|
||||
|
||||
Class.prototype.logout = function() {
|
||||
var _ref;
|
||||
if ((_ref = this.auth) != null) {
|
||||
_ref.logout;
|
||||
}
|
||||
return user.reset();
|
||||
};
|
||||
|
||||
return Class;
|
||||
|
||||
})();
|
||||
|
||||
module.exports = new Class();
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
// issues.coffee
|
||||
root.require.register('burnchart/src/modules/issues.js', function(exports, require, module) {
|
||||
root.require.register('burnchart/src/modules/github/issues.js', function(exports, require, module) {
|
||||
|
||||
var config, request;
|
||||
|
||||
config = require('../models/config');
|
||||
config = require('../../models/config');
|
||||
|
||||
request = require('./request');
|
||||
request = require('../request');
|
||||
|
||||
module.exports = {
|
||||
'get_all': function(opts, cb) {
|
||||
var one_status;
|
||||
one_status = function(state, cb) {
|
||||
var fetch_page, results;
|
||||
fetchAll: function(repo, cb) {
|
||||
var calcSize, oneStatus;
|
||||
calcSize = function(list, cb) {
|
||||
var size;
|
||||
switch (config.data.chart.points) {
|
||||
case 'ONE_SIZE':
|
||||
size = list.length;
|
||||
return cb(null, {
|
||||
list: list,
|
||||
size: size
|
||||
});
|
||||
case 'LABELS':
|
||||
size = 0;
|
||||
list = _.filter(list, function(issue) {
|
||||
var labels;
|
||||
if (!(labels = issue.labels)) {
|
||||
return false;
|
||||
}
|
||||
issue.size = _.reduce(labels, function(sum, label) {
|
||||
var matches;
|
||||
if (!(matches = label.name.match(config.data.chart.size_label))) {
|
||||
return sum;
|
||||
}
|
||||
return sum += parseInt(matches[1]);
|
||||
}, 0);
|
||||
size += issue.size;
|
||||
return !!issue.size;
|
||||
});
|
||||
return cb(null, {
|
||||
list: list,
|
||||
size: size
|
||||
});
|
||||
}
|
||||
};
|
||||
oneStatus = function(state, cb) {
|
||||
var fetchPage, results;
|
||||
results = [];
|
||||
return (fetch_page = function(page) {
|
||||
return request.allIssues(opts, {
|
||||
'milestone': opts.milestone.number,
|
||||
return (fetchPage = function(page) {
|
||||
return request.allIssues(repo, {
|
||||
state: state,
|
||||
page: page
|
||||
}, function(err, data) {
|
||||
|
@ -39493,46 +39527,37 @@ Router.prototype.mount = function(routes, path) {
|
|||
if (data.length < 100) {
|
||||
return cb(null, results);
|
||||
}
|
||||
return fetch_page(page + 1);
|
||||
return fetchPage(page + 1);
|
||||
});
|
||||
})(1);
|
||||
};
|
||||
return async.parallel([_.partial(one_status, 'open'), _.partial(one_status, 'closed')], cb);
|
||||
},
|
||||
'filter': function(collection, cb) {
|
||||
var filtered, total;
|
||||
total = 0;
|
||||
switch (config.get('chart.points')) {
|
||||
case 'ONE_SIZE':
|
||||
total = collection.length;
|
||||
filtered = _.map(collection, function(issue) {
|
||||
issue.size = 1;
|
||||
return issue;
|
||||
});
|
||||
break;
|
||||
case 'LABELS':
|
||||
filtered = _.filter(collection, function(issue) {
|
||||
var labels;
|
||||
if (!(labels = issue.labels)) {
|
||||
return false;
|
||||
}
|
||||
issue.size = _.reduce(labels, function(sum, label) {
|
||||
var matches;
|
||||
if (!(matches = label.name.match(config.get('chart.size_label')))) {
|
||||
return sum;
|
||||
}
|
||||
return sum += parseInt(matches[1]);
|
||||
}, 0);
|
||||
total += issue.size;
|
||||
return !!issue.size;
|
||||
});
|
||||
}
|
||||
return cb(null, filtered, total);
|
||||
return async.parallel([_.partial(async.waterfall, [_.partial(oneStatus, 'open'), calcSize]), _.partial(async.waterfall, [_.partial(oneStatus, 'closed'), calcSize])], function(err, _arg) {
|
||||
var closed, open;
|
||||
open = _arg[0], closed = _arg[1];
|
||||
return cb(err, {
|
||||
open: open,
|
||||
closed: closed
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
// milestone.coffee
|
||||
root.require.register('burnchart/src/modules/github/milestone.js', function(exports, require, module) {
|
||||
|
||||
var request;
|
||||
|
||||
request = require('../request');
|
||||
|
||||
module.exports = {
|
||||
'fetch': request.oneMilestone,
|
||||
'fetchAll': request.allMilestones
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
// mediator.coffee
|
||||
root.require.register('burnchart/src/modules/mediator.js', function(exports, require, module) {
|
||||
|
||||
|
@ -39544,90 +39569,10 @@ Router.prototype.mount = function(routes, path) {
|
|||
|
||||
});
|
||||
|
||||
// milestone.coffee
|
||||
root.require.register('burnchart/src/modules/milestone.js', function(exports, require, module) {
|
||||
|
||||
var request;
|
||||
|
||||
request = require('./request');
|
||||
|
||||
module.exports = {
|
||||
get: function(repo, cb) {
|
||||
if (repo.milestone) {
|
||||
return request.oneMilestone(repo, repo.milestone, function(err, m) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
if (m.open_issues + m.closed_issues === 0) {
|
||||
return cb(null, "No issues for milestone `" + m.title + "`");
|
||||
}
|
||||
return cb(null, null, m);
|
||||
});
|
||||
}
|
||||
},
|
||||
getAll: function(repo, cb) {
|
||||
return request.allMilestones(repo, function(err, data) {
|
||||
return cb(err, null, data);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
// project.coffee
|
||||
root.require.register('burnchart/src/modules/project.js', function(exports, require, module) {
|
||||
|
||||
var chart, issues;
|
||||
|
||||
issues = require('./issues');
|
||||
|
||||
chart = require('./chart');
|
||||
|
||||
module.exports = function(opts, cb) {
|
||||
return async.waterfall([
|
||||
function(cb) {
|
||||
return issues.get_all(opts, cb);
|
||||
}, function(all, cb) {
|
||||
return async.map(all, function(array, cb) {
|
||||
return issues.filter(array, function(err, filtered, total) {
|
||||
return cb(err, [filtered, total]);
|
||||
});
|
||||
}, function(err, _arg) {
|
||||
var closed, open, start;
|
||||
open = _arg[0], closed = _arg[1];
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
if (open[1] + closed[1] === 0) {
|
||||
return cb('No matching issues found');
|
||||
}
|
||||
opts.issues = {
|
||||
'closed': {
|
||||
'points': closed[1],
|
||||
'data': closed[0]
|
||||
},
|
||||
'open': {
|
||||
'points': open[1],
|
||||
'data': open[0]
|
||||
}
|
||||
};
|
||||
if ((start = closed[0][0].closed_at) < opts.milestone.created_at) {
|
||||
opts.milestone.created_at = start;
|
||||
}
|
||||
return cb(null);
|
||||
});
|
||||
}, function(cb) {
|
||||
var total;
|
||||
total = opts.issues.open.points + opts.issues.closed.points;
|
||||
return async.parallel([_.partial(chart.actual, opts.issues.closed.data, opts.milestone.created_at, total), _.partial(chart.ideal, opts.milestone.created_at, opts.milestone.due_on, total)], function(err, values) {
|
||||
if (values[0].length) {
|
||||
values.push(chart.trendline(values[0], opts.milestone.created_at, opts.milestone.due_on));
|
||||
}
|
||||
return chart.render(values, cb);
|
||||
});
|
||||
}
|
||||
], cb);
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
|
@ -39658,48 +39603,53 @@ Router.prototype.mount = function(routes, path) {
|
|||
};
|
||||
|
||||
module.exports = {
|
||||
'repo': function(repo, cb) {
|
||||
var data;
|
||||
repo: function(_arg, cb) {
|
||||
var data, name, owner;
|
||||
owner = _arg.owner, name = _arg.name;
|
||||
data = _.defaults({
|
||||
'path': "/repos/" + repo.owner + "/" + repo.name,
|
||||
'headers': headers(user.get('token'))
|
||||
'path': "/repos/" + owner + "/" + name,
|
||||
'headers': headers(user.data.token)
|
||||
}, defaults.github);
|
||||
return request(data, cb);
|
||||
},
|
||||
'allMilestones': function(repo, cb) {
|
||||
var data;
|
||||
allMilestones: function(_arg, cb) {
|
||||
var data, name, owner;
|
||||
owner = _arg.owner, name = _arg.name;
|
||||
data = _.defaults({
|
||||
'path': "/repos/" + repo.owner + "/" + repo.name + "/milestones",
|
||||
'path': "/repos/" + owner + "/" + name + "/milestones",
|
||||
'query': {
|
||||
'state': 'open',
|
||||
'sort': 'due_date',
|
||||
'direction': 'asc'
|
||||
},
|
||||
'headers': headers(user.get('token'))
|
||||
'headers': headers(user.data.token)
|
||||
}, defaults.github);
|
||||
return request(data, cb);
|
||||
},
|
||||
'oneMilestone': function(repo, number, cb) {
|
||||
var data;
|
||||
oneMilestone: function(_arg, cb) {
|
||||
var data, milestone, name, owner;
|
||||
owner = _arg.owner, name = _arg.name, milestone = _arg.milestone;
|
||||
data = _.defaults({
|
||||
'path': "/repos/" + repo.owner + "/" + repo.name + "/milestones/" + number,
|
||||
'path': "/repos/" + owner + "/" + name + "/milestones/" + milestone,
|
||||
'query': {
|
||||
'state': 'open',
|
||||
'sort': 'due_date',
|
||||
'direction': 'asc'
|
||||
},
|
||||
'headers': headers(user.get('token'))
|
||||
'headers': headers(user.data.token)
|
||||
}, defaults.github);
|
||||
return request(data, cb);
|
||||
},
|
||||
'allIssues': function(repo, query, cb) {
|
||||
var data;
|
||||
allIssues: function(_arg, query, cb) {
|
||||
var data, milestone, name, owner;
|
||||
owner = _arg.owner, name = _arg.name, milestone = _arg.milestone;
|
||||
data = _.defaults({
|
||||
'path': "/repos/" + repo.owner + "/" + repo.name + "/issues",
|
||||
'path': "/repos/" + owner + "/" + name + "/issues",
|
||||
'query': _.extend(query, {
|
||||
milestone: milestone,
|
||||
'per_page': '100'
|
||||
}),
|
||||
'headers': headers(user.get('token'))
|
||||
'headers': headers(user.data.token)
|
||||
}, defaults.github);
|
||||
return request(data, cb);
|
||||
}
|
||||
|
@ -39894,7 +39844,7 @@ Router.prototype.mount = function(routes, path) {
|
|||
// milestones.mustache
|
||||
root.require.register('burnchart/src/templates/milestones.js', function(exports, require, module) {
|
||||
|
||||
module.exports = ["<div id=\"projects\">"," <div class=\"header\">"," <a href=\"#\" class=\"sort\"><Icons icon=\"sort-alphabet\"/> Sorted by priority</a>"," <h2>Milestones</h2>"," </div>",""," <table>"," {{#project.milestones}}"," <tr>"," <td>"," <a class=\"milestone\" href=\"#{{project.owner}}/{{project.name}}/{{number}}\">{{ title }}</a>"," </td>"," <td style=\"width:1%\">"," <div class=\"progress\">"," <span class=\"percent\">{{Math.floor(progress)}}%</span>"," <span class=\"due\">{{{ due }}}</span>"," <div class=\"outer bar\">"," <div class=\"inner bar {{on_time}}\" style=\"width:{{progress}}%\"></div>"," </div>"," </div>"," </td>"," </tr>"," {{/project.milestones}}"," </table>",""," <div class=\"footer\">"," <a href=\"#\"><Icons icon=\"cog\"/> Edit</a>"," </div>","</div>"].join("\n");
|
||||
module.exports = ["<div id=\"projects\">"," <div class=\"header\">"," <a href=\"#\" class=\"sort\"><Icons icon=\"sort-alphabet\"/> Sorted by priority</a>"," <h2>Milestones</h2>"," </div>",""," <table>"," {{#project.milestones}}"," <tr>"," <td>"," <a class=\"milestone\" href=\"#{{project.owner}}/{{project.name}}/{{number}}\">{{ title }}</a>"," </td>"," <td style=\"width:1%\">"," <div class=\"progress\">"," <span class=\"percent\">{{Math.floor(format.progress(issues.closed.size, issues.open.size))}}%</span>"," <span class=\"due\">{{{ format.due(due_on) }}}</span>"," <div class=\"outer bar\">"," <div class=\"inner bar {{format.onTime(number, due_on, created_at, issues.closed.size, issues.open.size)}}\" style=\"width:{{format.progress(issues.closed.size, issues.open.size)}}%\"></div>"," </div>"," </div>"," </td>"," </tr>"," {{/project.milestones}}"," </table>",""," <div class=\"footer\">"," <a href=\"#\"><Icons icon=\"cog\"/> Edit</a>"," </div>","</div>"].join("\n");
|
||||
});
|
||||
|
||||
// notify.mustache
|
||||
|
@ -39947,26 +39897,26 @@ Router.prototype.mount = function(routes, path) {
|
|||
// format.coffee
|
||||
root.require.register('burnchart/src/utils/format.js', function(exports, require, module) {
|
||||
|
||||
var config;
|
||||
|
||||
config = require('../models/config');
|
||||
var __slice = [].slice;
|
||||
|
||||
module.exports = {
|
||||
'progress': _.memoize(function(a, b) {
|
||||
return 100 * (a / (b + a));
|
||||
}),
|
||||
'onTime': _.memoize(function(milestone) {
|
||||
'onTime': _.memoize(function(number, due_on, created_at, closed_size, open_size) {
|
||||
var a, b, c, time;
|
||||
if (!milestone.due_on) {
|
||||
if (!due_on) {
|
||||
return 'green';
|
||||
}
|
||||
a = +new Date(milestone.created_at);
|
||||
a = +new Date(created_at);
|
||||
b = +(new Date);
|
||||
c = +new Date(milestone.due_on);
|
||||
c = +new Date(due_on);
|
||||
time = this.progress(b - a, c - b);
|
||||
return ['red', 'green'][+(milestone.progress > time)];
|
||||
}, function(m) {
|
||||
return [m.created_at, m.number].join('/');
|
||||
return ['red', 'green'][+(this.progress(closed_size, open_size) > time)];
|
||||
}, function() {
|
||||
var args;
|
||||
args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
|
||||
return args.join('/');
|
||||
}),
|
||||
'fromNow': _.memoize(function(jsonDate) {
|
||||
return moment(new Date(jsonDate)).fromNow();
|
||||
|
@ -40050,7 +40000,7 @@ Router.prototype.mount = function(routes, path) {
|
|||
|
||||
system = require('../models/system').system;
|
||||
|
||||
firebase = require('../modules/firebase');
|
||||
firebase = require('../models/firebase');
|
||||
|
||||
user = require('../models/user');
|
||||
|
||||
|
@ -40154,17 +40104,22 @@ Router.prototype.mount = function(routes, path) {
|
|||
// milestones.coffee
|
||||
root.require.register('burnchart/src/views/milestones.js', function(exports, require, module) {
|
||||
|
||||
var Icons, mediator, projects;
|
||||
var Icons, format, mediator, projects;
|
||||
|
||||
mediator = require('../modules/mediator');
|
||||
|
||||
projects = require('../models/projects');
|
||||
|
||||
format = require('../utils/format');
|
||||
|
||||
Icons = require('./icons');
|
||||
|
||||
module.exports = Ractive.extend({
|
||||
'name': 'views/milestones',
|
||||
'template': require('../templates/milestones'),
|
||||
'data': {
|
||||
format: format
|
||||
},
|
||||
'components': {
|
||||
Icons: Icons
|
||||
},
|
||||
|
@ -40384,9 +40339,9 @@ Router.prototype.mount = function(routes, path) {
|
|||
|
||||
config = require('../../models/config');
|
||||
|
||||
milestones = require('../../modules/milestone');
|
||||
milestones = require('../../modules/github/milestone');
|
||||
|
||||
issues = require('../../modules/issues');
|
||||
issues = require('../../modules/github/issues');
|
||||
|
||||
mediator = require('../../modules/mediator');
|
||||
|
||||
|
@ -40402,7 +40357,7 @@ Router.prototype.mount = function(routes, path) {
|
|||
'ready': false
|
||||
},
|
||||
onrender: function() {
|
||||
var done, name, owner, project, _ref,
|
||||
var done, fetchIssues, fetchMilestones, name, owner, project, _ref,
|
||||
_this = this;
|
||||
_ref = this.get('route'), owner = _ref[0], name = _ref[1];
|
||||
document.title = "" + owner + "/" + name;
|
||||
|
@ -40417,67 +40372,36 @@ Router.prototype.mount = function(routes, path) {
|
|||
return this.set('ready', true);
|
||||
}
|
||||
done = system.async();
|
||||
return milestones.getAll(project, function(err, warn, list) {
|
||||
if (err || warn) {
|
||||
done();
|
||||
if (err) {
|
||||
return mediator.fire('!app/notify', {
|
||||
'text': err.toString(),
|
||||
'type': 'alert',
|
||||
'system': true,
|
||||
'ttl': null
|
||||
});
|
||||
}
|
||||
if (warn) {
|
||||
return mediator.fire('!app/notify', {
|
||||
'text': warn.toString(),
|
||||
'type': 'warn',
|
||||
'system': true,
|
||||
'ttl': null
|
||||
});
|
||||
}
|
||||
}
|
||||
switch (config.data.chart.points) {
|
||||
case 'ONE_SIZE':
|
||||
list = _.map(list, function(m) {
|
||||
m.progress = format.progress(m.closed_issues, m.open_issues);
|
||||
m.on_time = format.onTime(m);
|
||||
m.due = format.due(m.due_on);
|
||||
return m;
|
||||
});
|
||||
done();
|
||||
return _this.set({
|
||||
'project.milestones': list,
|
||||
'ready': true
|
||||
});
|
||||
case 'LABELS':
|
||||
return async.map(list, function(m, cb) {
|
||||
return issues.get_all(_.extend(project, {
|
||||
'milestone': m
|
||||
}), function(err, arr) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
return issues.filter(arr, function(err, filtered, total) {
|
||||
return console.log(filtered, total);
|
||||
});
|
||||
});
|
||||
}, function(err, list) {
|
||||
done();
|
||||
if (err) {
|
||||
return mediator.fire('!app/notify', {
|
||||
'text': err.toString(),
|
||||
'type': 'alert',
|
||||
'system': true,
|
||||
'ttl': null
|
||||
});
|
||||
}
|
||||
return this.set({
|
||||
'project.milestones': list,
|
||||
'ready': true
|
||||
});
|
||||
});
|
||||
fetchMilestones = function(cb) {
|
||||
return milestones.fetchAll(project, cb);
|
||||
};
|
||||
fetchIssues = function(allMilestones, cb) {
|
||||
return async.map(allMilestones, function(milestone, cb) {
|
||||
return issues.fetchAll({
|
||||
owner: owner,
|
||||
name: name,
|
||||
'milestone': milestone.number
|
||||
}, function(err, obj) {
|
||||
return cb(err, _.extend(milestone, {
|
||||
'issues': obj
|
||||
}));
|
||||
});
|
||||
}, cb);
|
||||
};
|
||||
return async.waterfall([fetchMilestones, fetchIssues], function(err, data) {
|
||||
done();
|
||||
if (err) {
|
||||
return mediator.fire('!app/notify', {
|
||||
'text': err.toString(),
|
||||
'type': 'alert',
|
||||
'system': true,
|
||||
'ttl': null
|
||||
});
|
||||
}
|
||||
return _this.set({
|
||||
'project.milestones': data,
|
||||
'ready': true
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
684
public/js/app.js
684
public/js/app.js
|
@ -56,13 +56,58 @@
|
|||
"datetime": /^(\d{4}-\d{2}-\d{2})T(.*)/,
|
||||
"size_label": /^size (\d+)$/,
|
||||
"location": /^#!((\/[^\/]+){2,3})$/,
|
||||
"points": 'LABELS'
|
||||
"points": 'ONE_SIZE'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// firebase.coffee
|
||||
root.require.register('burnchart/src/models/firebase.js', function(exports, require, module) {
|
||||
|
||||
var Model, config, user;
|
||||
|
||||
Model = require('../utils/model');
|
||||
|
||||
user = require('./user');
|
||||
|
||||
config = require('./config');
|
||||
|
||||
module.exports = new Model({
|
||||
'name': 'models/firebase',
|
||||
auth: function() {
|
||||
throw 'Not overriden';
|
||||
},
|
||||
login: function(cb) {
|
||||
return this.auth.login(config.data.provider, {
|
||||
'rememberMe': true,
|
||||
'scope': 'public_repo'
|
||||
});
|
||||
},
|
||||
logout: function() {
|
||||
var _ref;
|
||||
if ((_ref = this.auth) != null) {
|
||||
_ref.logout;
|
||||
}
|
||||
return user.reset();
|
||||
},
|
||||
onrender: function() {
|
||||
var client,
|
||||
_this = this;
|
||||
this.set('client', client = new Firebase("https://" + config.data.firebase + ".firebaseio.com"));
|
||||
return this.auth = new FirebaseSimpleLogin(client, function(err, obj) {
|
||||
user.set('loaded', true);
|
||||
if (err || !obj) {
|
||||
throw err;
|
||||
}
|
||||
return user.set(obj);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// projects.coffee
|
||||
root.require.register('burnchart/src/models/projects.js', function(exports, require, module) {
|
||||
|
||||
|
@ -168,133 +213,14 @@
|
|||
});
|
||||
|
||||
// chart.coffee
|
||||
root.require.register('burnchart/src/modules/chart.js', function(exports, require, module) {
|
||||
root.require.register('burnchart/src/modules/draw/chart.js', function(exports, require, module) {
|
||||
|
||||
var config,
|
||||
__indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
|
||||
var config;
|
||||
|
||||
config = require('../models/config');
|
||||
|
||||
module.exports = {
|
||||
'actual': function(collection, created_at, total, cb) {
|
||||
var head, max, min, range, rest;
|
||||
head = [
|
||||
{
|
||||
'date': new Date(created_at),
|
||||
'points': total
|
||||
}
|
||||
];
|
||||
min = +Infinity;
|
||||
max = -Infinity;
|
||||
rest = _.map(collection, function(issue) {
|
||||
var closed_at, size;
|
||||
size = issue.size, closed_at = issue.closed_at;
|
||||
if (size < min) {
|
||||
min = size;
|
||||
}
|
||||
if (size > max) {
|
||||
max = size;
|
||||
}
|
||||
issue.date = new Date(closed_at);
|
||||
issue.points = total -= size;
|
||||
return issue;
|
||||
});
|
||||
range = d3.scale.linear().domain([min, max]).range([5, 8]);
|
||||
rest = _.map(rest, function(issue) {
|
||||
issue.radius = range(issue.size);
|
||||
return issue;
|
||||
});
|
||||
return cb(null, [].concat(head, rest));
|
||||
},
|
||||
'ideal': function(a, b, total, cb) {
|
||||
var cutoff, d, days, length, m, now, once, velocity, y, _ref, _ref1;
|
||||
if (b < a) {
|
||||
_ref = [a, b], b = _ref[0], a = _ref[1];
|
||||
}
|
||||
_ref1 = _.map(a.match(config.get('chart.datetime'))[1].split('-'), function(v) {
|
||||
return parseInt(v);
|
||||
}), y = _ref1[0], m = _ref1[1], d = _ref1[2];
|
||||
cutoff = new Date(b);
|
||||
days = [];
|
||||
length = 0;
|
||||
(once = function(inc) {
|
||||
var day, day_of;
|
||||
day = new Date(y, m - 1, d + inc);
|
||||
if (!(day_of = day.getDay())) {
|
||||
day_of = 7;
|
||||
}
|
||||
if (__indexOf.call(config.get('chart.off_days'), day_of) >= 0) {
|
||||
days.push({
|
||||
date: day,
|
||||
off_day: true
|
||||
});
|
||||
} else {
|
||||
length += 1;
|
||||
days.push({
|
||||
date: day
|
||||
});
|
||||
}
|
||||
if (!(day > cutoff)) {
|
||||
return once(inc + 1);
|
||||
}
|
||||
})(0);
|
||||
velocity = total / (length - 1);
|
||||
days = _.map(days, function(day, i) {
|
||||
day.points = total;
|
||||
if (days[i] && !days[i].off_day) {
|
||||
total -= velocity;
|
||||
}
|
||||
return day;
|
||||
});
|
||||
if ((now = new Date()) > cutoff) {
|
||||
days.push({
|
||||
date: now,
|
||||
points: 0
|
||||
});
|
||||
}
|
||||
return cb(null, days);
|
||||
},
|
||||
'trendline': function(actual, created_at, due_on) {
|
||||
var a, b, b1, c1, e, fn, intercept, l, last, slope, start, values;
|
||||
start = +actual[0].date;
|
||||
values = _.map(actual, function(_arg) {
|
||||
var date, points;
|
||||
date = _arg.date, points = _arg.points;
|
||||
return [+date - start, points];
|
||||
});
|
||||
last = actual[actual.length - 1];
|
||||
values.push([+new Date() - start, last.points]);
|
||||
b1 = 0;
|
||||
e = 0;
|
||||
c1 = 0;
|
||||
a = (l = values.length) * _.reduce(values, function(sum, _arg) {
|
||||
var a, b;
|
||||
a = _arg[0], b = _arg[1];
|
||||
b1 += a;
|
||||
e += b;
|
||||
c1 += Math.pow(a, 2);
|
||||
return sum + (a * b);
|
||||
}, 0);
|
||||
slope = (a - (b1 * e)) / ((l * c1) - (Math.pow(b1, 2)));
|
||||
intercept = (e - (slope * b1)) / l;
|
||||
fn = function(x) {
|
||||
return slope * x + intercept;
|
||||
};
|
||||
created_at = new Date(created_at);
|
||||
due_on = due_on ? new Date(due_on) : new Date();
|
||||
a = created_at - start;
|
||||
b = due_on - start;
|
||||
return [
|
||||
{
|
||||
date: created_at,
|
||||
points: fn(a)
|
||||
}, {
|
||||
date: due_on,
|
||||
points: fn(b)
|
||||
}
|
||||
];
|
||||
},
|
||||
'render': function(_arg, cb) {
|
||||
render: function(_arg, cb) {
|
||||
var actual, height, ideal, line, m, mAxis, margin, svg, tooltip, trendline, width, x, xAxis, y, yAxis, _ref;
|
||||
actual = _arg[0], ideal = _arg[1], trendline = _arg[2];
|
||||
document.querySelector('#svg').innerHTML = '';
|
||||
|
@ -363,75 +289,183 @@
|
|||
|
||||
});
|
||||
|
||||
// firebase.coffee
|
||||
root.require.register('burnchart/src/modules/firebase.js', function(exports, require, module) {
|
||||
// line.coffee
|
||||
root.require.register('burnchart/src/modules/draw/line.js', function(exports, require, module) {
|
||||
|
||||
var Class, config, user;
|
||||
var __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
|
||||
|
||||
user = require('../models/user');
|
||||
|
||||
config = require('../models/config');
|
||||
|
||||
Class = (function() {
|
||||
function Class() {
|
||||
var _this = this;
|
||||
this.client = new Firebase("https://" + (config.get('firebase')) + ".firebaseio.com");
|
||||
this.auth = new FirebaseSimpleLogin(this.client, function(err, obj) {
|
||||
user.set('loaded', true);
|
||||
if (err || !obj) {
|
||||
return _this.authCb(err);
|
||||
module.exports = {
|
||||
actual: function(collection, created_at, total, cb) {
|
||||
var head, max, min, range, rest;
|
||||
head = [
|
||||
{
|
||||
'date': new Date(created_at),
|
||||
'points': total
|
||||
}
|
||||
return user.set(obj);
|
||||
];
|
||||
min = +Infinity;
|
||||
max = -Infinity;
|
||||
rest = _.map(collection, function(issue) {
|
||||
var closed_at, size;
|
||||
size = issue.size, closed_at = issue.closed_at;
|
||||
if (size < min) {
|
||||
min = size;
|
||||
}
|
||||
if (size > max) {
|
||||
max = size;
|
||||
}
|
||||
issue.date = new Date(closed_at);
|
||||
issue.points = total -= size;
|
||||
return issue;
|
||||
});
|
||||
range = d3.scale.linear().domain([min, max]).range([5, 8]);
|
||||
rest = _.map(rest, function(issue) {
|
||||
issue.radius = range(issue.size);
|
||||
return issue;
|
||||
});
|
||||
return cb(null, [].concat(head, rest));
|
||||
},
|
||||
ideal: function(a, b, total, cb) {
|
||||
var cutoff, d, days, length, m, now, once, velocity, y, _ref, _ref1;
|
||||
if (b < a) {
|
||||
_ref = [a, b], b = _ref[0], a = _ref[1];
|
||||
}
|
||||
_ref1 = _.map(a.match(config.get('chart.datetime'))[1].split('-'), function(v) {
|
||||
return parseInt(v);
|
||||
}), y = _ref1[0], m = _ref1[1], d = _ref1[2];
|
||||
cutoff = new Date(b);
|
||||
days = [];
|
||||
length = 0;
|
||||
(once = function(inc) {
|
||||
var day, day_of;
|
||||
day = new Date(y, m - 1, d + inc);
|
||||
if (!(day_of = day.getDay())) {
|
||||
day_of = 7;
|
||||
}
|
||||
if (__indexOf.call(config.get('chart.off_days'), day_of) >= 0) {
|
||||
days.push({
|
||||
date: day,
|
||||
off_day: true
|
||||
});
|
||||
} else {
|
||||
length += 1;
|
||||
days.push({
|
||||
date: day
|
||||
});
|
||||
}
|
||||
if (!(day > cutoff)) {
|
||||
return once(inc + 1);
|
||||
}
|
||||
})(0);
|
||||
velocity = total / (length - 1);
|
||||
days = _.map(days, function(day, i) {
|
||||
day.points = total;
|
||||
if (days[i] && !days[i].off_day) {
|
||||
total -= velocity;
|
||||
}
|
||||
return day;
|
||||
});
|
||||
if ((now = new Date()) > cutoff) {
|
||||
days.push({
|
||||
date: now,
|
||||
points: 0
|
||||
});
|
||||
}
|
||||
return cb(null, days);
|
||||
},
|
||||
trend: function(actual, created_at, due_on) {
|
||||
var a, b, b1, c1, e, fn, intercept, l, last, slope, start, values;
|
||||
start = +actual[0].date;
|
||||
values = _.map(actual, function(_arg) {
|
||||
var date, points;
|
||||
date = _arg.date, points = _arg.points;
|
||||
return [+date - start, points];
|
||||
});
|
||||
last = actual[actual.length - 1];
|
||||
values.push([+new Date() - start, last.points]);
|
||||
b1 = 0;
|
||||
e = 0;
|
||||
c1 = 0;
|
||||
a = (l = values.length) * _.reduce(values, function(sum, _arg) {
|
||||
var a, b;
|
||||
a = _arg[0], b = _arg[1];
|
||||
b1 += a;
|
||||
e += b;
|
||||
c1 += Math.pow(a, 2);
|
||||
return sum + (a * b);
|
||||
}, 0);
|
||||
slope = (a - (b1 * e)) / ((l * c1) - (Math.pow(b1, 2)));
|
||||
intercept = (e - (slope * b1)) / l;
|
||||
fn = function(x) {
|
||||
return slope * x + intercept;
|
||||
};
|
||||
created_at = new Date(created_at);
|
||||
due_on = due_on ? new Date(due_on) : new Date();
|
||||
a = created_at - start;
|
||||
b = due_on - start;
|
||||
return [
|
||||
{
|
||||
date: created_at,
|
||||
points: fn(a)
|
||||
}, {
|
||||
date: due_on,
|
||||
points: fn(b)
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
Class.prototype.authCb = function() {};
|
||||
|
||||
Class.prototype.login = function(cb) {
|
||||
if (!this.client) {
|
||||
return cb('Client is not setup');
|
||||
}
|
||||
this.authCb = cb;
|
||||
return this.auth.login(config.get('provider'), {
|
||||
'rememberMe': true,
|
||||
'scope': 'public_repo'
|
||||
});
|
||||
};
|
||||
|
||||
Class.prototype.logout = function() {
|
||||
var _ref;
|
||||
if ((_ref = this.auth) != null) {
|
||||
_ref.logout;
|
||||
}
|
||||
return user.reset();
|
||||
};
|
||||
|
||||
return Class;
|
||||
|
||||
})();
|
||||
|
||||
module.exports = new Class();
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
// issues.coffee
|
||||
root.require.register('burnchart/src/modules/issues.js', function(exports, require, module) {
|
||||
root.require.register('burnchart/src/modules/github/issues.js', function(exports, require, module) {
|
||||
|
||||
var config, request;
|
||||
|
||||
config = require('../models/config');
|
||||
config = require('../../models/config');
|
||||
|
||||
request = require('./request');
|
||||
request = require('../request');
|
||||
|
||||
module.exports = {
|
||||
'get_all': function(opts, cb) {
|
||||
var one_status;
|
||||
one_status = function(state, cb) {
|
||||
var fetch_page, results;
|
||||
fetchAll: function(repo, cb) {
|
||||
var calcSize, oneStatus;
|
||||
calcSize = function(list, cb) {
|
||||
var size;
|
||||
switch (config.data.chart.points) {
|
||||
case 'ONE_SIZE':
|
||||
size = list.length;
|
||||
return cb(null, {
|
||||
list: list,
|
||||
size: size
|
||||
});
|
||||
case 'LABELS':
|
||||
size = 0;
|
||||
list = _.filter(list, function(issue) {
|
||||
var labels;
|
||||
if (!(labels = issue.labels)) {
|
||||
return false;
|
||||
}
|
||||
issue.size = _.reduce(labels, function(sum, label) {
|
||||
var matches;
|
||||
if (!(matches = label.name.match(config.data.chart.size_label))) {
|
||||
return sum;
|
||||
}
|
||||
return sum += parseInt(matches[1]);
|
||||
}, 0);
|
||||
size += issue.size;
|
||||
return !!issue.size;
|
||||
});
|
||||
return cb(null, {
|
||||
list: list,
|
||||
size: size
|
||||
});
|
||||
}
|
||||
};
|
||||
oneStatus = function(state, cb) {
|
||||
var fetchPage, results;
|
||||
results = [];
|
||||
return (fetch_page = function(page) {
|
||||
return request.allIssues(opts, {
|
||||
'milestone': opts.milestone.number,
|
||||
return (fetchPage = function(page) {
|
||||
return request.allIssues(repo, {
|
||||
state: state,
|
||||
page: page
|
||||
}, function(err, data) {
|
||||
|
@ -445,46 +479,37 @@
|
|||
if (data.length < 100) {
|
||||
return cb(null, results);
|
||||
}
|
||||
return fetch_page(page + 1);
|
||||
return fetchPage(page + 1);
|
||||
});
|
||||
})(1);
|
||||
};
|
||||
return async.parallel([_.partial(one_status, 'open'), _.partial(one_status, 'closed')], cb);
|
||||
},
|
||||
'filter': function(collection, cb) {
|
||||
var filtered, total;
|
||||
total = 0;
|
||||
switch (config.get('chart.points')) {
|
||||
case 'ONE_SIZE':
|
||||
total = collection.length;
|
||||
filtered = _.map(collection, function(issue) {
|
||||
issue.size = 1;
|
||||
return issue;
|
||||
});
|
||||
break;
|
||||
case 'LABELS':
|
||||
filtered = _.filter(collection, function(issue) {
|
||||
var labels;
|
||||
if (!(labels = issue.labels)) {
|
||||
return false;
|
||||
}
|
||||
issue.size = _.reduce(labels, function(sum, label) {
|
||||
var matches;
|
||||
if (!(matches = label.name.match(config.get('chart.size_label')))) {
|
||||
return sum;
|
||||
}
|
||||
return sum += parseInt(matches[1]);
|
||||
}, 0);
|
||||
total += issue.size;
|
||||
return !!issue.size;
|
||||
});
|
||||
}
|
||||
return cb(null, filtered, total);
|
||||
return async.parallel([_.partial(async.waterfall, [_.partial(oneStatus, 'open'), calcSize]), _.partial(async.waterfall, [_.partial(oneStatus, 'closed'), calcSize])], function(err, _arg) {
|
||||
var closed, open;
|
||||
open = _arg[0], closed = _arg[1];
|
||||
return cb(err, {
|
||||
open: open,
|
||||
closed: closed
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
// milestone.coffee
|
||||
root.require.register('burnchart/src/modules/github/milestone.js', function(exports, require, module) {
|
||||
|
||||
var request;
|
||||
|
||||
request = require('../request');
|
||||
|
||||
module.exports = {
|
||||
'fetch': request.oneMilestone,
|
||||
'fetchAll': request.allMilestones
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
// mediator.coffee
|
||||
root.require.register('burnchart/src/modules/mediator.js', function(exports, require, module) {
|
||||
|
||||
|
@ -496,90 +521,10 @@
|
|||
|
||||
});
|
||||
|
||||
// milestone.coffee
|
||||
root.require.register('burnchart/src/modules/milestone.js', function(exports, require, module) {
|
||||
|
||||
var request;
|
||||
|
||||
request = require('./request');
|
||||
|
||||
module.exports = {
|
||||
get: function(repo, cb) {
|
||||
if (repo.milestone) {
|
||||
return request.oneMilestone(repo, repo.milestone, function(err, m) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
if (m.open_issues + m.closed_issues === 0) {
|
||||
return cb(null, "No issues for milestone `" + m.title + "`");
|
||||
}
|
||||
return cb(null, null, m);
|
||||
});
|
||||
}
|
||||
},
|
||||
getAll: function(repo, cb) {
|
||||
return request.allMilestones(repo, function(err, data) {
|
||||
return cb(err, null, data);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
// project.coffee
|
||||
root.require.register('burnchart/src/modules/project.js', function(exports, require, module) {
|
||||
|
||||
var chart, issues;
|
||||
|
||||
issues = require('./issues');
|
||||
|
||||
chart = require('./chart');
|
||||
|
||||
module.exports = function(opts, cb) {
|
||||
return async.waterfall([
|
||||
function(cb) {
|
||||
return issues.get_all(opts, cb);
|
||||
}, function(all, cb) {
|
||||
return async.map(all, function(array, cb) {
|
||||
return issues.filter(array, function(err, filtered, total) {
|
||||
return cb(err, [filtered, total]);
|
||||
});
|
||||
}, function(err, _arg) {
|
||||
var closed, open, start;
|
||||
open = _arg[0], closed = _arg[1];
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
if (open[1] + closed[1] === 0) {
|
||||
return cb('No matching issues found');
|
||||
}
|
||||
opts.issues = {
|
||||
'closed': {
|
||||
'points': closed[1],
|
||||
'data': closed[0]
|
||||
},
|
||||
'open': {
|
||||
'points': open[1],
|
||||
'data': open[0]
|
||||
}
|
||||
};
|
||||
if ((start = closed[0][0].closed_at) < opts.milestone.created_at) {
|
||||
opts.milestone.created_at = start;
|
||||
}
|
||||
return cb(null);
|
||||
});
|
||||
}, function(cb) {
|
||||
var total;
|
||||
total = opts.issues.open.points + opts.issues.closed.points;
|
||||
return async.parallel([_.partial(chart.actual, opts.issues.closed.data, opts.milestone.created_at, total), _.partial(chart.ideal, opts.milestone.created_at, opts.milestone.due_on, total)], function(err, values) {
|
||||
if (values[0].length) {
|
||||
values.push(chart.trendline(values[0], opts.milestone.created_at, opts.milestone.due_on));
|
||||
}
|
||||
return chart.render(values, cb);
|
||||
});
|
||||
}
|
||||
], cb);
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
|
@ -610,48 +555,53 @@
|
|||
};
|
||||
|
||||
module.exports = {
|
||||
'repo': function(repo, cb) {
|
||||
var data;
|
||||
repo: function(_arg, cb) {
|
||||
var data, name, owner;
|
||||
owner = _arg.owner, name = _arg.name;
|
||||
data = _.defaults({
|
||||
'path': "/repos/" + repo.owner + "/" + repo.name,
|
||||
'headers': headers(user.get('token'))
|
||||
'path': "/repos/" + owner + "/" + name,
|
||||
'headers': headers(user.data.token)
|
||||
}, defaults.github);
|
||||
return request(data, cb);
|
||||
},
|
||||
'allMilestones': function(repo, cb) {
|
||||
var data;
|
||||
allMilestones: function(_arg, cb) {
|
||||
var data, name, owner;
|
||||
owner = _arg.owner, name = _arg.name;
|
||||
data = _.defaults({
|
||||
'path': "/repos/" + repo.owner + "/" + repo.name + "/milestones",
|
||||
'path': "/repos/" + owner + "/" + name + "/milestones",
|
||||
'query': {
|
||||
'state': 'open',
|
||||
'sort': 'due_date',
|
||||
'direction': 'asc'
|
||||
},
|
||||
'headers': headers(user.get('token'))
|
||||
'headers': headers(user.data.token)
|
||||
}, defaults.github);
|
||||
return request(data, cb);
|
||||
},
|
||||
'oneMilestone': function(repo, number, cb) {
|
||||
var data;
|
||||
oneMilestone: function(_arg, cb) {
|
||||
var data, milestone, name, owner;
|
||||
owner = _arg.owner, name = _arg.name, milestone = _arg.milestone;
|
||||
data = _.defaults({
|
||||
'path': "/repos/" + repo.owner + "/" + repo.name + "/milestones/" + number,
|
||||
'path': "/repos/" + owner + "/" + name + "/milestones/" + milestone,
|
||||
'query': {
|
||||
'state': 'open',
|
||||
'sort': 'due_date',
|
||||
'direction': 'asc'
|
||||
},
|
||||
'headers': headers(user.get('token'))
|
||||
'headers': headers(user.data.token)
|
||||
}, defaults.github);
|
||||
return request(data, cb);
|
||||
},
|
||||
'allIssues': function(repo, query, cb) {
|
||||
var data;
|
||||
allIssues: function(_arg, query, cb) {
|
||||
var data, milestone, name, owner;
|
||||
owner = _arg.owner, name = _arg.name, milestone = _arg.milestone;
|
||||
data = _.defaults({
|
||||
'path': "/repos/" + repo.owner + "/" + repo.name + "/issues",
|
||||
'path': "/repos/" + owner + "/" + name + "/issues",
|
||||
'query': _.extend(query, {
|
||||
milestone: milestone,
|
||||
'per_page': '100'
|
||||
}),
|
||||
'headers': headers(user.get('token'))
|
||||
'headers': headers(user.data.token)
|
||||
}, defaults.github);
|
||||
return request(data, cb);
|
||||
}
|
||||
|
@ -846,7 +796,7 @@
|
|||
// milestones.mustache
|
||||
root.require.register('burnchart/src/templates/milestones.js', function(exports, require, module) {
|
||||
|
||||
module.exports = ["<div id=\"projects\">"," <div class=\"header\">"," <a href=\"#\" class=\"sort\"><Icons icon=\"sort-alphabet\"/> Sorted by priority</a>"," <h2>Milestones</h2>"," </div>",""," <table>"," {{#project.milestones}}"," <tr>"," <td>"," <a class=\"milestone\" href=\"#{{project.owner}}/{{project.name}}/{{number}}\">{{ title }}</a>"," </td>"," <td style=\"width:1%\">"," <div class=\"progress\">"," <span class=\"percent\">{{Math.floor(progress)}}%</span>"," <span class=\"due\">{{{ due }}}</span>"," <div class=\"outer bar\">"," <div class=\"inner bar {{on_time}}\" style=\"width:{{progress}}%\"></div>"," </div>"," </div>"," </td>"," </tr>"," {{/project.milestones}}"," </table>",""," <div class=\"footer\">"," <a href=\"#\"><Icons icon=\"cog\"/> Edit</a>"," </div>","</div>"].join("\n");
|
||||
module.exports = ["<div id=\"projects\">"," <div class=\"header\">"," <a href=\"#\" class=\"sort\"><Icons icon=\"sort-alphabet\"/> Sorted by priority</a>"," <h2>Milestones</h2>"," </div>",""," <table>"," {{#project.milestones}}"," <tr>"," <td>"," <a class=\"milestone\" href=\"#{{project.owner}}/{{project.name}}/{{number}}\">{{ title }}</a>"," </td>"," <td style=\"width:1%\">"," <div class=\"progress\">"," <span class=\"percent\">{{Math.floor(format.progress(issues.closed.size, issues.open.size))}}%</span>"," <span class=\"due\">{{{ format.due(due_on) }}}</span>"," <div class=\"outer bar\">"," <div class=\"inner bar {{format.onTime(number, due_on, created_at, issues.closed.size, issues.open.size)}}\" style=\"width:{{format.progress(issues.closed.size, issues.open.size)}}%\"></div>"," </div>"," </div>"," </td>"," </tr>"," {{/project.milestones}}"," </table>",""," <div class=\"footer\">"," <a href=\"#\"><Icons icon=\"cog\"/> Edit</a>"," </div>","</div>"].join("\n");
|
||||
});
|
||||
|
||||
// notify.mustache
|
||||
|
@ -899,26 +849,26 @@
|
|||
// format.coffee
|
||||
root.require.register('burnchart/src/utils/format.js', function(exports, require, module) {
|
||||
|
||||
var config;
|
||||
|
||||
config = require('../models/config');
|
||||
var __slice = [].slice;
|
||||
|
||||
module.exports = {
|
||||
'progress': _.memoize(function(a, b) {
|
||||
return 100 * (a / (b + a));
|
||||
}),
|
||||
'onTime': _.memoize(function(milestone) {
|
||||
'onTime': _.memoize(function(number, due_on, created_at, closed_size, open_size) {
|
||||
var a, b, c, time;
|
||||
if (!milestone.due_on) {
|
||||
if (!due_on) {
|
||||
return 'green';
|
||||
}
|
||||
a = +new Date(milestone.created_at);
|
||||
a = +new Date(created_at);
|
||||
b = +(new Date);
|
||||
c = +new Date(milestone.due_on);
|
||||
c = +new Date(due_on);
|
||||
time = this.progress(b - a, c - b);
|
||||
return ['red', 'green'][+(milestone.progress > time)];
|
||||
}, function(m) {
|
||||
return [m.created_at, m.number].join('/');
|
||||
return ['red', 'green'][+(this.progress(closed_size, open_size) > time)];
|
||||
}, function() {
|
||||
var args;
|
||||
args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
|
||||
return args.join('/');
|
||||
}),
|
||||
'fromNow': _.memoize(function(jsonDate) {
|
||||
return moment(new Date(jsonDate)).fromNow();
|
||||
|
@ -1002,7 +952,7 @@
|
|||
|
||||
system = require('../models/system').system;
|
||||
|
||||
firebase = require('../modules/firebase');
|
||||
firebase = require('../models/firebase');
|
||||
|
||||
user = require('../models/user');
|
||||
|
||||
|
@ -1106,17 +1056,22 @@
|
|||
// milestones.coffee
|
||||
root.require.register('burnchart/src/views/milestones.js', function(exports, require, module) {
|
||||
|
||||
var Icons, mediator, projects;
|
||||
var Icons, format, mediator, projects;
|
||||
|
||||
mediator = require('../modules/mediator');
|
||||
|
||||
projects = require('../models/projects');
|
||||
|
||||
format = require('../utils/format');
|
||||
|
||||
Icons = require('./icons');
|
||||
|
||||
module.exports = Ractive.extend({
|
||||
'name': 'views/milestones',
|
||||
'template': require('../templates/milestones'),
|
||||
'data': {
|
||||
format: format
|
||||
},
|
||||
'components': {
|
||||
Icons: Icons
|
||||
},
|
||||
|
@ -1336,9 +1291,9 @@
|
|||
|
||||
config = require('../../models/config');
|
||||
|
||||
milestones = require('../../modules/milestone');
|
||||
milestones = require('../../modules/github/milestone');
|
||||
|
||||
issues = require('../../modules/issues');
|
||||
issues = require('../../modules/github/issues');
|
||||
|
||||
mediator = require('../../modules/mediator');
|
||||
|
||||
|
@ -1354,7 +1309,7 @@
|
|||
'ready': false
|
||||
},
|
||||
onrender: function() {
|
||||
var done, name, owner, project, _ref,
|
||||
var done, fetchIssues, fetchMilestones, name, owner, project, _ref,
|
||||
_this = this;
|
||||
_ref = this.get('route'), owner = _ref[0], name = _ref[1];
|
||||
document.title = "" + owner + "/" + name;
|
||||
|
@ -1369,67 +1324,36 @@
|
|||
return this.set('ready', true);
|
||||
}
|
||||
done = system.async();
|
||||
return milestones.getAll(project, function(err, warn, list) {
|
||||
if (err || warn) {
|
||||
done();
|
||||
if (err) {
|
||||
return mediator.fire('!app/notify', {
|
||||
'text': err.toString(),
|
||||
'type': 'alert',
|
||||
'system': true,
|
||||
'ttl': null
|
||||
});
|
||||
}
|
||||
if (warn) {
|
||||
return mediator.fire('!app/notify', {
|
||||
'text': warn.toString(),
|
||||
'type': 'warn',
|
||||
'system': true,
|
||||
'ttl': null
|
||||
});
|
||||
}
|
||||
}
|
||||
switch (config.data.chart.points) {
|
||||
case 'ONE_SIZE':
|
||||
list = _.map(list, function(m) {
|
||||
m.progress = format.progress(m.closed_issues, m.open_issues);
|
||||
m.on_time = format.onTime(m);
|
||||
m.due = format.due(m.due_on);
|
||||
return m;
|
||||
});
|
||||
done();
|
||||
return _this.set({
|
||||
'project.milestones': list,
|
||||
'ready': true
|
||||
});
|
||||
case 'LABELS':
|
||||
return async.map(list, function(m, cb) {
|
||||
return issues.get_all(_.extend(project, {
|
||||
'milestone': m
|
||||
}), function(err, arr) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
return issues.filter(arr, function(err, filtered, total) {
|
||||
return console.log(filtered, total);
|
||||
});
|
||||
});
|
||||
}, function(err, list) {
|
||||
done();
|
||||
if (err) {
|
||||
return mediator.fire('!app/notify', {
|
||||
'text': err.toString(),
|
||||
'type': 'alert',
|
||||
'system': true,
|
||||
'ttl': null
|
||||
});
|
||||
}
|
||||
return this.set({
|
||||
'project.milestones': list,
|
||||
'ready': true
|
||||
});
|
||||
});
|
||||
fetchMilestones = function(cb) {
|
||||
return milestones.fetchAll(project, cb);
|
||||
};
|
||||
fetchIssues = function(allMilestones, cb) {
|
||||
return async.map(allMilestones, function(milestone, cb) {
|
||||
return issues.fetchAll({
|
||||
owner: owner,
|
||||
name: name,
|
||||
'milestone': milestone.number
|
||||
}, function(err, obj) {
|
||||
return cb(err, _.extend(milestone, {
|
||||
'issues': obj
|
||||
}));
|
||||
});
|
||||
}, cb);
|
||||
};
|
||||
return async.waterfall([fetchMilestones, fetchIssues], function(err, data) {
|
||||
done();
|
||||
if (err) {
|
||||
return mediator.fire('!app/notify', {
|
||||
'text': err.toString(),
|
||||
'type': 'alert',
|
||||
'system': true,
|
||||
'ttl': null
|
||||
});
|
||||
}
|
||||
return _this.set({
|
||||
'project.milestones': data,
|
||||
'ready': true
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -32,4 +32,4 @@ module.exports = new Model
|
|||
# How do we specify which user/repo/(milestone) we want?
|
||||
"location": /^#!((\/[^\/]+){2,3})$/
|
||||
# Process all issues as one size (ONE_SIZE) or use labels (LABELS).
|
||||
"points": 'LABELS'
|
||||
"points": 'ONE_SIZE'
|
|
@ -0,0 +1,35 @@
|
|||
Model = require '../utils/model'
|
||||
user = require './user'
|
||||
config = require './config'
|
||||
|
||||
module.exports = new Model
|
||||
|
||||
'name': 'models/firebase'
|
||||
|
||||
auth: ->
|
||||
throw 'Not overriden'
|
||||
|
||||
# Login a user.
|
||||
login: (cb) ->
|
||||
# Login.
|
||||
@auth.login config.data.provider,
|
||||
'rememberMe': yes
|
||||
'scope': 'public_repo'
|
||||
|
||||
# Logout a user.
|
||||
logout: ->
|
||||
@auth?.logout
|
||||
do user.reset
|
||||
|
||||
onrender: ->
|
||||
# Setup a new client.
|
||||
@set 'client', client = new Firebase "https://#{config.data.firebase}.firebaseio.com"
|
||||
|
||||
# Check if we have a user in session.
|
||||
@auth = new FirebaseSimpleLogin client, (err, obj) =>
|
||||
user.set 'loaded', yes
|
||||
|
||||
throw err if err or not obj
|
||||
|
||||
# Save user.
|
||||
user.set obj
|
|
@ -3,126 +3,8 @@ config = require '../models/config'
|
|||
|
||||
module.exports =
|
||||
|
||||
# A graph of closed issues.
|
||||
# `collection`: issues
|
||||
# `created_at`: milestone start date
|
||||
# `total`: total number of points (open & closed issues)
|
||||
'actual': (collection, created_at, total, cb) ->
|
||||
head = [ {
|
||||
'date': new Date created_at
|
||||
'points': total
|
||||
} ]
|
||||
|
||||
min = +Infinity ; max = -Infinity
|
||||
|
||||
# Generate the actual closes.
|
||||
rest = _.map collection, (issue) ->
|
||||
{ size, closed_at } = issue
|
||||
# Determine the range.
|
||||
min = size if size < min
|
||||
max = size if size > max
|
||||
|
||||
# Dropping points remaining.
|
||||
issue.date = new Date closed_at
|
||||
issue.points = total -= size
|
||||
issue
|
||||
|
||||
# Now add a radius in a range (will be used for a circle).
|
||||
range = d3.scale.linear().domain([ min, max ]).range([ 5, 8 ])
|
||||
|
||||
rest = _.map rest, (issue) ->
|
||||
issue.radius = range issue.size
|
||||
issue
|
||||
|
||||
cb null, [].concat head, rest
|
||||
|
||||
# A graph of an ideal progression..
|
||||
# `a`: milestone start date
|
||||
# `b`: milestone end date
|
||||
# `total`: total number of points (open & closed issues)
|
||||
'ideal': (a, b, total, cb) ->
|
||||
# Swap?
|
||||
[ b, a ] = [ a, b ] if b < a
|
||||
|
||||
# We start here adding days to `d`.
|
||||
[ y, m, d ] = _.map a.match(config.get('chart.datetime'))[1].split('-'), (v) -> parseInt v
|
||||
# We want to end here.
|
||||
cutoff = new Date(b)
|
||||
|
||||
# Go through the beginning to the end skipping off days.
|
||||
days = [] ; length = 0
|
||||
do once = (inc = 0) ->
|
||||
# A new day.
|
||||
day = new Date y, m - 1, d + inc
|
||||
|
||||
# Does this day count?
|
||||
day_of = 7 if !day_of = day.getDay()
|
||||
if day_of in config.get('chart.off_days')
|
||||
days.push { date: day, off_day: yes }
|
||||
else
|
||||
length += 1
|
||||
days.push { date: day }
|
||||
|
||||
# Go again?
|
||||
once(inc + 1) unless day > cutoff
|
||||
|
||||
# Map points on the array of days now.
|
||||
velocity = total / (length - 1)
|
||||
|
||||
days = _.map days, (day, i) ->
|
||||
day.points = total
|
||||
total -= velocity if days[i] and not days[i].off_day
|
||||
day
|
||||
|
||||
# Do we need to make a link to right now?
|
||||
days.push { date: now, points: 0 } if (now = new Date()) > cutoff
|
||||
|
||||
cb null, days
|
||||
|
||||
# Graph representing a trendling of actual issues.
|
||||
'trendline': (actual, created_at, due_on) ->
|
||||
start = +actual[0].date
|
||||
|
||||
# Values is a list of time from the start and points remaining.
|
||||
values = _.map actual, ({ date, points }) ->
|
||||
[ +date - start, points ]
|
||||
|
||||
# Now is an actual point too.
|
||||
last = actual[actual.length - 1]
|
||||
values.push [ + new Date() - start, last.points ]
|
||||
|
||||
# http://classroom.synonym.com/calculate-trendline-2709.html
|
||||
b1 = 0 ; e = 0 ; c1 = 0
|
||||
a = (l = values.length) * _.reduce(values, (sum, [ a, b ]) ->
|
||||
b1 += a ; e += b
|
||||
c1 += Math.pow(a, 2)
|
||||
sum + (a * b)
|
||||
, 0)
|
||||
|
||||
slope = (a - (b1 * e)) / ((l * c1) - (Math.pow(b1, 2)))
|
||||
intercept = (e - (slope * b1)) / l
|
||||
fn = (x) -> slope * x + intercept
|
||||
|
||||
# Milestone always has a creation date.
|
||||
created_at = new Date created_at
|
||||
# Due date can be empty.
|
||||
due_on = if due_on then new Date(due_on) else new Date()
|
||||
|
||||
a = created_at - start
|
||||
b = due_on - start
|
||||
|
||||
[
|
||||
{
|
||||
date: created_at
|
||||
points: fn(a)
|
||||
}, {
|
||||
date: due_on
|
||||
points: fn(b)
|
||||
}
|
||||
]
|
||||
|
||||
# The graph as a whole.
|
||||
'render': ([ actual, ideal, trendline ], cb) ->
|
||||
# Render the chart.
|
||||
render: ([ actual, ideal, trendline ], cb) ->
|
||||
document.querySelector('#svg').innerHTML = ''
|
||||
|
||||
# Get available space.
|
|
@ -0,0 +1,119 @@
|
|||
module.exports =
|
||||
|
||||
# A graph of closed issues.
|
||||
# `collection`: issues
|
||||
# `created_at`: milestone start date
|
||||
# `total`: total number of points (open & closed issues)
|
||||
actual: (collection, created_at, total, cb) ->
|
||||
head = [ {
|
||||
'date': new Date created_at
|
||||
'points': total
|
||||
} ]
|
||||
|
||||
min = +Infinity ; max = -Infinity
|
||||
|
||||
# Generate the actual closes.
|
||||
rest = _.map collection, (issue) ->
|
||||
{ size, closed_at } = issue
|
||||
# Determine the range.
|
||||
min = size if size < min
|
||||
max = size if size > max
|
||||
|
||||
# Dropping points remaining.
|
||||
issue.date = new Date closed_at
|
||||
issue.points = total -= size
|
||||
issue
|
||||
|
||||
# Now add a radius in a range (will be used for a circle).
|
||||
range = d3.scale.linear().domain([ min, max ]).range([ 5, 8 ])
|
||||
|
||||
rest = _.map rest, (issue) ->
|
||||
issue.radius = range issue.size
|
||||
issue
|
||||
|
||||
cb null, [].concat head, rest
|
||||
|
||||
# A graph of an ideal progression..
|
||||
# `a`: milestone start date
|
||||
# `b`: milestone end date
|
||||
# `total`: total number of points (open & closed issues)
|
||||
ideal: (a, b, total, cb) ->
|
||||
# Swap?
|
||||
[ b, a ] = [ a, b ] if b < a
|
||||
|
||||
# We start here adding days to `d`.
|
||||
[ y, m, d ] = _.map a.match(config.get('chart.datetime'))[1].split('-'), (v) -> parseInt v
|
||||
# We want to end here.
|
||||
cutoff = new Date(b)
|
||||
|
||||
# Go through the beginning to the end skipping off days.
|
||||
days = [] ; length = 0
|
||||
do once = (inc = 0) ->
|
||||
# A new day.
|
||||
day = new Date y, m - 1, d + inc
|
||||
|
||||
# Does this day count?
|
||||
day_of = 7 if !day_of = day.getDay()
|
||||
if day_of in config.get('chart.off_days')
|
||||
days.push { date: day, off_day: yes }
|
||||
else
|
||||
length += 1
|
||||
days.push { date: day }
|
||||
|
||||
# Go again?
|
||||
once(inc + 1) unless day > cutoff
|
||||
|
||||
# Map points on the array of days now.
|
||||
velocity = total / (length - 1)
|
||||
|
||||
days = _.map days, (day, i) ->
|
||||
day.points = total
|
||||
total -= velocity if days[i] and not days[i].off_day
|
||||
day
|
||||
|
||||
# Do we need to make a link to right now?
|
||||
days.push { date: now, points: 0 } if (now = new Date()) > cutoff
|
||||
|
||||
cb null, days
|
||||
|
||||
# Graph representing a trendling of actual issues.
|
||||
trend: (actual, created_at, due_on) ->
|
||||
start = +actual[0].date
|
||||
|
||||
# Values is a list of time from the start and points remaining.
|
||||
values = _.map actual, ({ date, points }) ->
|
||||
[ +date - start, points ]
|
||||
|
||||
# Now is an actual point too.
|
||||
last = actual[actual.length - 1]
|
||||
values.push [ + new Date() - start, last.points ]
|
||||
|
||||
# http://classroom.synonym.com/calculate-trendline-2709.html
|
||||
b1 = 0 ; e = 0 ; c1 = 0
|
||||
a = (l = values.length) * _.reduce(values, (sum, [ a, b ]) ->
|
||||
b1 += a ; e += b
|
||||
c1 += Math.pow(a, 2)
|
||||
sum + (a * b)
|
||||
, 0)
|
||||
|
||||
slope = (a - (b1 * e)) / ((l * c1) - (Math.pow(b1, 2)))
|
||||
intercept = (e - (slope * b1)) / l
|
||||
fn = (x) -> slope * x + intercept
|
||||
|
||||
# Milestone always has a creation date.
|
||||
created_at = new Date created_at
|
||||
# Due date can be empty.
|
||||
due_on = if due_on then new Date(due_on) else new Date()
|
||||
|
||||
a = created_at - start
|
||||
b = due_on - start
|
||||
|
||||
[
|
||||
{
|
||||
date: created_at
|
||||
points: fn(a)
|
||||
}, {
|
||||
date: due_on
|
||||
points: fn(b)
|
||||
}
|
||||
]
|
|
@ -1,40 +0,0 @@
|
|||
user = require '../models/user'
|
||||
config = require '../models/config'
|
||||
|
||||
# Default "silent" callback for auth.
|
||||
class Class
|
||||
|
||||
constructor: ->
|
||||
# Setup a new client.
|
||||
@client = new Firebase "https://#{config.get('firebase')}.firebaseio.com"
|
||||
|
||||
# Check if we have a user in session.
|
||||
@auth = new FirebaseSimpleLogin @client, (err, obj) =>
|
||||
user.set 'loaded', yes
|
||||
|
||||
return @authCb err if err or not obj
|
||||
|
||||
# Save user.
|
||||
user.set obj
|
||||
|
||||
# Default "blank" callback.
|
||||
authCb: ->
|
||||
|
||||
# Login a user.
|
||||
login: (cb) ->
|
||||
return cb 'Client is not setup' unless @client
|
||||
|
||||
# Override the default auth callback.
|
||||
@authCb = cb
|
||||
|
||||
# Login.
|
||||
@auth.login config.get('provider'),
|
||||
'rememberMe': yes
|
||||
'scope': 'public_repo'
|
||||
|
||||
# Logout a user.
|
||||
logout: ->
|
||||
@auth?.logout
|
||||
do user.reset
|
||||
|
||||
module.exports = new Class()
|
|
@ -0,0 +1,64 @@
|
|||
#!/usr/bin/env coffee
|
||||
config = require '../../models/config'
|
||||
request = require '../request'
|
||||
|
||||
module.exports =
|
||||
|
||||
# Fetch issues for a milestone.
|
||||
fetchAll: (repo, cb) ->
|
||||
# Calculate size of either open or closed issues.
|
||||
# Modifies issues by ref.
|
||||
calcSize = (list, cb) ->
|
||||
switch config.data.chart.points
|
||||
when 'ONE_SIZE'
|
||||
size = list.length
|
||||
cb null, { list, size }
|
||||
|
||||
when 'LABELS'
|
||||
size = 0
|
||||
|
||||
list = _.filter list, (issue) ->
|
||||
# Skip if no labels exist.
|
||||
return no unless labels = issue.labels
|
||||
|
||||
# Determine the total issue size from all labels.
|
||||
issue.size = _.reduce labels, (sum, label) ->
|
||||
# Not matching.
|
||||
return sum unless matches = label.name.match config.data.chart.size_label
|
||||
# Increase sum.
|
||||
sum += parseInt matches[1]
|
||||
, 0
|
||||
|
||||
# Increase the total.
|
||||
size += issue.size
|
||||
|
||||
# Are we saving it?
|
||||
!!issue.size
|
||||
|
||||
cb null, { list, size }
|
||||
|
||||
# For each state...
|
||||
oneStatus = (state, cb) ->
|
||||
# Concat them here.
|
||||
results = []
|
||||
|
||||
# One pageful fetch (next pages in series).
|
||||
do fetchPage = (page=1) ->
|
||||
request.allIssues repo, { state, page }, (err, data) ->
|
||||
# Errors?
|
||||
return cb err if err
|
||||
# Empty?
|
||||
return cb null, results unless data.length
|
||||
# Concat sorted (api does not sort on closed_at!).
|
||||
results = results.concat _.sortBy data, 'closed_at'
|
||||
# < 100 results?
|
||||
return cb null, results if data.length < 100
|
||||
# Fetch the next page then.
|
||||
fetchPage page + 1
|
||||
|
||||
# For each `open` and `closed` issues in parallel.
|
||||
async.parallel [
|
||||
_.partial async.waterfall, [ _.partial(oneStatus, 'open'), calcSize ]
|
||||
_.partial async.waterfall, [ _.partial(oneStatus, 'closed'), calcSize ]
|
||||
], (err, [ open, closed ]) ->
|
||||
cb err, { open, closed }
|
|
@ -1,25 +1,13 @@
|
|||
#!/usr/bin/env coffee
|
||||
request = require './request'
|
||||
request = require '../request'
|
||||
|
||||
module.exports =
|
||||
|
||||
# Get a specific milestone for a repo.
|
||||
get: (repo, cb) ->
|
||||
# Get a specific milestone.
|
||||
if repo.milestone
|
||||
request.oneMilestone repo, repo.milestone, (err, m) ->
|
||||
# Errors?
|
||||
return cb err if err
|
||||
# Empty milestone?
|
||||
if m.open_issues + m.closed_issues is 0
|
||||
return cb null, "No issues for milestone `#{m.title}`"
|
||||
# Fetch a milestone.
|
||||
'fetch': request.oneMilestone
|
||||
|
||||
cb null, null, m
|
||||
|
||||
# Get all milestones.
|
||||
getAll: (repo, cb) ->
|
||||
request.allMilestones repo, (err, data) ->
|
||||
cb err, null, data
|
||||
# Fetch all milestones.
|
||||
'fetchAll': request.allMilestones
|
||||
|
||||
# # Get the current milestone out of many.
|
||||
# else
|
|
@ -1,70 +0,0 @@
|
|||
#!/usr/bin/env coffee
|
||||
config = require '../models/config'
|
||||
request = require './request'
|
||||
|
||||
module.exports =
|
||||
|
||||
# Used on an initial fetch of issues for a repo.
|
||||
'get_all': (opts, cb) ->
|
||||
# For each state...
|
||||
one_status = (state, cb) ->
|
||||
# Concat them here.
|
||||
results = []
|
||||
# One pageful fetch (next pages in series).
|
||||
do fetch_page = (page=1) ->
|
||||
request.allIssues opts, {
|
||||
'milestone': opts.milestone.number,
|
||||
state, page
|
||||
}, (err, data) ->
|
||||
# Errors?
|
||||
return cb err if err
|
||||
# Empty?
|
||||
return cb null, results unless data.length
|
||||
# Concat sorted (API does not sort on closed_at!).
|
||||
results = results.concat _.sortBy data, 'closed_at'
|
||||
# < 100 results?
|
||||
return cb null, results if data.length < 100
|
||||
# Fetch the next page then.
|
||||
fetch_page page + 1
|
||||
|
||||
# For each `open` and `closed` issues in parallel.
|
||||
async.parallel [
|
||||
_.partial one_status, 'open'
|
||||
_.partial one_status, 'closed'
|
||||
], cb
|
||||
|
||||
# Filter an array of incoming issues based on a regex & save size on them.
|
||||
'filter': (collection, cb) ->
|
||||
# The total size of all issues.
|
||||
total = 0
|
||||
|
||||
# Which point counting mode are we in?
|
||||
switch config.get 'chart.points'
|
||||
# All issues are the same size
|
||||
when 'ONE_SIZE'
|
||||
total = collection.length
|
||||
filtered = _.map collection, (issue) ->
|
||||
issue.size = 1
|
||||
issue
|
||||
|
||||
# Take the points size from issue label.
|
||||
when 'LABELS'
|
||||
filtered = _.filter collection, (issue) ->
|
||||
# Skip if no labels exist.
|
||||
return no unless labels = issue.labels
|
||||
|
||||
# Determine the total issue size from all labels.
|
||||
issue.size = _.reduce labels, (sum, label) ->
|
||||
# Not matching.
|
||||
return sum unless matches = label.name.match config.get 'chart.size_label'
|
||||
# Increase sum.
|
||||
sum += parseInt matches[1]
|
||||
, 0
|
||||
|
||||
# Increase the total.
|
||||
total += issue.size
|
||||
|
||||
# Are we saving it?
|
||||
!!issue.size
|
||||
|
||||
cb null, filtered, total
|
|
@ -1,62 +1,62 @@
|
|||
#!/usr/bin/env coffee
|
||||
issues = require './issues'
|
||||
chart = require './chart'
|
||||
# #!/usr/bin/env coffee
|
||||
# issues = require './issues'
|
||||
# chart = require './chart'
|
||||
|
||||
# Setup a project and render it.
|
||||
module.exports = (opts, cb) ->
|
||||
# # Setup a project and render it.
|
||||
# module.exports = (opts, cb) ->
|
||||
|
||||
# Get all issues.
|
||||
async.waterfall [ (cb) ->
|
||||
issues.get_all opts, cb
|
||||
# # Get all issues.
|
||||
# async.waterfall [ (cb) ->
|
||||
# issues.get_all opts, cb
|
||||
|
||||
# Filter them to labeled ones.
|
||||
(all, cb) ->
|
||||
async.map all, (array, cb) ->
|
||||
issues.filter array, (err, filtered, total) ->
|
||||
cb err, [ filtered, total ]
|
||||
, (err, [ open, closed ]) ->
|
||||
return cb err if err
|
||||
# Empty?
|
||||
return cb 'No matching issues found' if open[1] + closed[1] is 0
|
||||
# Save the open/closed on us first.
|
||||
opts.issues =
|
||||
'closed': { 'points': closed[1], 'data': closed[0] }
|
||||
'open': { 'points': open[1], 'data': open[0] }
|
||||
# Do we need to move the milestone start date?
|
||||
if (start = closed[0][0].closed_at) < opts.milestone.created_at
|
||||
opts.milestone.created_at = start
|
||||
# # Filter them to labeled ones.
|
||||
# (all, cb) ->
|
||||
# async.map all, (array, cb) ->
|
||||
# issues.filter array, (err, filtered, total) ->
|
||||
# cb err, [ filtered, total ]
|
||||
# , (err, [ open, closed ]) ->
|
||||
# return cb err if err
|
||||
# # Empty?
|
||||
# return cb 'No matching issues found' if open[1] + closed[1] is 0
|
||||
# # Save the open/closed on us first.
|
||||
# opts.issues =
|
||||
# 'closed': { 'points': closed[1], 'data': closed[0] }
|
||||
# 'open': { 'points': open[1], 'data': open[0] }
|
||||
# # Do we need to move the milestone start date?
|
||||
# if (start = closed[0][0].closed_at) < opts.milestone.created_at
|
||||
# opts.milestone.created_at = start
|
||||
|
||||
cb null
|
||||
# cb null
|
||||
|
||||
# Create actual and ideal lines & render.
|
||||
(cb) ->
|
||||
total = opts.issues.open.points + opts.issues.closed.points
|
||||
# # Create actual and ideal lines & render.
|
||||
# (cb) ->
|
||||
# total = opts.issues.open.points + opts.issues.closed.points
|
||||
|
||||
async.parallel [
|
||||
_.partial(
|
||||
chart.actual, # actual line
|
||||
opts.issues.closed.data,
|
||||
opts.milestone.created_at,
|
||||
total
|
||||
)
|
||||
_.partial(
|
||||
chart.ideal, # ideal line
|
||||
opts.milestone.created_at,
|
||||
opts.milestone.due_on,
|
||||
total
|
||||
)
|
||||
], (err, values) ->
|
||||
# Generate a trendline?
|
||||
values.push(chart.trendline(
|
||||
values[0],
|
||||
opts.milestone.created_at,
|
||||
opts.milestone.due_on
|
||||
)) if values[0].length
|
||||
# async.parallel [
|
||||
# _.partial(
|
||||
# chart.actual, # actual line
|
||||
# opts.issues.closed.data,
|
||||
# opts.milestone.created_at,
|
||||
# total
|
||||
# )
|
||||
# _.partial(
|
||||
# chart.ideal, # ideal line
|
||||
# opts.milestone.created_at,
|
||||
# opts.milestone.due_on,
|
||||
# total
|
||||
# )
|
||||
# ], (err, values) ->
|
||||
# # Generate a trendline?
|
||||
# values.push(chart.trendline(
|
||||
# values[0],
|
||||
# opts.milestone.created_at,
|
||||
# opts.milestone.due_on
|
||||
# )) if values[0].length
|
||||
|
||||
# Render the chart.
|
||||
chart.render values, cb
|
||||
# # Render the chart.
|
||||
# chart.render values, cb
|
||||
|
||||
# Watch window resize from now on?
|
||||
# window.onresize = doit if 'onresize' of window
|
||||
# # Watch window resize from now on?
|
||||
# # window.onresize = doit if 'onresize' of window
|
||||
|
||||
], cb
|
||||
# ], cb
|
|
@ -18,40 +18,40 @@ defaults =
|
|||
module.exports =
|
||||
|
||||
# Get a repo.
|
||||
'repo': (repo, cb) ->
|
||||
repo: ({ owner, name }, cb) ->
|
||||
data = _.defaults
|
||||
'path': "/repos/#{repo.owner}/#{repo.name}"
|
||||
'headers': headers user.get('token')
|
||||
'path': "/repos/#{owner}/#{name}"
|
||||
'headers': headers user.data.token
|
||||
, defaults.github
|
||||
|
||||
request data, cb
|
||||
|
||||
# Get all open milestones.
|
||||
'allMilestones': (repo, cb) ->
|
||||
allMilestones: ({ owner, name }, cb) ->
|
||||
data = _.defaults
|
||||
'path': "/repos/#{repo.owner}/#{repo.name}/milestones"
|
||||
'path': "/repos/#{owner}/#{name}/milestones"
|
||||
'query': { 'state': 'open', 'sort': 'due_date', 'direction': 'asc' }
|
||||
'headers': headers user.get('token')
|
||||
'headers': headers user.data.token
|
||||
, defaults.github
|
||||
|
||||
request data, cb
|
||||
|
||||
# Get one open milestone.
|
||||
'oneMilestone': (repo, number, cb) ->
|
||||
oneMilestone: ({ owner, name, milestone }, cb) ->
|
||||
data = _.defaults
|
||||
'path': "/repos/#{repo.owner}/#{repo.name}/milestones/#{number}"
|
||||
'path': "/repos/#{owner}/#{name}/milestones/#{milestone}"
|
||||
'query': { 'state': 'open', 'sort': 'due_date', 'direction': 'asc' }
|
||||
'headers': headers user.get('token')
|
||||
'headers': headers user.data.token
|
||||
, defaults.github
|
||||
|
||||
request data, cb
|
||||
|
||||
# Get all issues for a state.
|
||||
'allIssues': (repo, query, cb) ->
|
||||
allIssues: ({ owner, name, milestone }, query, cb) ->
|
||||
data = _.defaults
|
||||
'path': "/repos/#{repo.owner}/#{repo.name}/issues"
|
||||
'query': _.extend query, { 'per_page': '100' }
|
||||
'headers': headers user.get('token')
|
||||
'path': "/repos/#{owner}/#{name}/issues"
|
||||
'query': _.extend query, { milestone, 'per_page': '100' }
|
||||
'headers': headers user.data.token
|
||||
, defaults.github
|
||||
|
||||
request data, cb
|
||||
|
|
|
@ -12,10 +12,10 @@
|
|||
</td>
|
||||
<td style="width:1%">
|
||||
<div class="progress">
|
||||
<span class="percent">{{Math.floor(progress)}}%</span>
|
||||
<span class="due">{{{ due }}}</span>
|
||||
<span class="percent">{{Math.floor(format.progress(issues.closed.size, issues.open.size))}}%</span>
|
||||
<span class="due">{{{ format.due(due_on) }}}</span>
|
||||
<div class="outer bar">
|
||||
<div class="inner bar {{on_time}}" style="width:{{progress}}%"></div>
|
||||
<div class="inner bar {{format.onTime(number, due_on, created_at, issues.closed.size, issues.open.size)}}" style="width:{{format.progress(issues.closed.size, issues.open.size)}}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
config = require '../models/config'
|
||||
|
||||
module.exports =
|
||||
|
||||
# Progress in percentages.
|
||||
|
@ -7,21 +5,22 @@ module.exports =
|
|||
100 * (a / (b + a))
|
||||
|
||||
# Is a milestone on time?
|
||||
'onTime': _.memoize (milestone) ->
|
||||
'onTime': _.memoize (number, due_on, created_at, closed_size, open_size) ->
|
||||
# Milestones with no due date are always on track.
|
||||
return 'green' unless milestone.due_on
|
||||
return 'green' unless due_on
|
||||
|
||||
# Calculate the progress in days.
|
||||
a = +new Date milestone.created_at
|
||||
a = +new Date created_at
|
||||
b = +new Date
|
||||
c = +new Date milestone.due_on
|
||||
c = +new Date due_on
|
||||
|
||||
# Progress in time.
|
||||
time = @progress b - a, c - b
|
||||
|
||||
[ 'red', 'green' ][ +(milestone.progress > time) ]
|
||||
, (m) -> # resolver
|
||||
[ m.created_at, m.number ].join '/'
|
||||
# Progress in size.
|
||||
[ 'red', 'green' ][ +(@progress(closed_size, open_size) > time) ]
|
||||
, (args...) -> # resolver
|
||||
args.join '/'
|
||||
|
||||
# Time from now.
|
||||
'fromNow': _.memoize (jsonDate) ->
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{ system } = require '../models/system'
|
||||
firebase = require '../modules/firebase'
|
||||
firebase = require '../models/firebase'
|
||||
user = require '../models/user'
|
||||
Icons = require './icons'
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
mediator = require '../modules/mediator'
|
||||
projects = require '../models/projects'
|
||||
format = require '../utils/format'
|
||||
Icons = require './icons'
|
||||
|
||||
module.exports = Ractive.extend
|
||||
|
@ -8,6 +9,8 @@ module.exports = Ractive.extend
|
|||
|
||||
'template': require '../templates/milestones'
|
||||
|
||||
'data': { format }
|
||||
|
||||
'components': { Icons }
|
||||
|
||||
'adapt': [ Ractive.adaptors.Ractive ]
|
|
@ -3,8 +3,8 @@ Milestones = require '../milestones'
|
|||
projects = require '../../models/projects'
|
||||
system = require '../../models/system'
|
||||
config = require '../../models/config'
|
||||
milestones = require '../../modules/milestone'
|
||||
issues = require '../../modules/issues'
|
||||
milestones = require '../../modules/github/milestone'
|
||||
issues = require '../../modules/github/issues'
|
||||
mediator = require '../../modules/mediator'
|
||||
format = require '../../utils/format'
|
||||
|
||||
|
@ -36,61 +36,30 @@ module.exports = Ractive.extend
|
|||
# We are loading the milestones then.
|
||||
done = do system.async
|
||||
|
||||
milestones.getAll project, (err, warn, list) =>
|
||||
if err or warn
|
||||
do done
|
||||
fetchMilestones = (cb) ->
|
||||
milestones.fetchAll project, cb
|
||||
|
||||
return mediator.fire '!app/notify', {
|
||||
'text': do err.toString
|
||||
'type': 'alert'
|
||||
'system': yes
|
||||
'ttl': null
|
||||
} if err
|
||||
fetchIssues = (allMilestones, cb) ->
|
||||
async.map allMilestones, (milestone, cb) ->
|
||||
issues.fetchAll { owner, name, 'milestone': milestone.number }, (err, obj) ->
|
||||
cb err, _.extend milestone, { 'issues': obj }
|
||||
, cb
|
||||
|
||||
return mediator.fire '!app/notify', {
|
||||
'text': do warn.toString
|
||||
'type': 'warn'
|
||||
'system': yes
|
||||
'ttl': null
|
||||
} if warn
|
||||
async.waterfall [
|
||||
# First get all the milestones.
|
||||
fetchMilestones,
|
||||
# Then all the issues per milestone.
|
||||
fetchIssues
|
||||
], (err, data) =>
|
||||
do done
|
||||
return mediator.fire '!app/notify', {
|
||||
'text': do err.toString
|
||||
'type': 'alert'
|
||||
'system': yes
|
||||
'ttl': null
|
||||
} if err
|
||||
|
||||
# Calculate progress in all milestones.
|
||||
switch config.data.chart.points
|
||||
when 'ONE_SIZE'
|
||||
list = _.map list, (m) ->
|
||||
m.progress = format.progress m.closed_issues, m.open_issues
|
||||
m.on_time = format.onTime m
|
||||
m.due = format.due m.due_on
|
||||
m
|
||||
|
||||
# Now we are done.
|
||||
do done
|
||||
|
||||
# Save the milestones.
|
||||
@set
|
||||
'project.milestones': list
|
||||
'ready': yes
|
||||
|
||||
when 'LABELS'
|
||||
# We need to fetch all issues per milestone.
|
||||
async.map list, (m, cb) ->
|
||||
issues.get_all _.extend(project, { 'milestone': m }), (err, arr) ->
|
||||
return cb err if err
|
||||
issues.filter arr, (err, filtered, total) ->
|
||||
console.log filtered, total
|
||||
|
||||
, (err, list) ->
|
||||
# Now we are done.
|
||||
do done
|
||||
|
||||
return mediator.fire '!app/notify', {
|
||||
'text': do err.toString
|
||||
'type': 'alert'
|
||||
'system': yes
|
||||
'ttl': null
|
||||
} if err
|
||||
|
||||
# Save the milestones.
|
||||
@set
|
||||
'project.milestones': list
|
||||
'ready': yes
|
||||
# Save the milestones.
|
||||
@set
|
||||
'project.milestones': data
|
||||
'ready': yes
|
Loading…
Reference in New Issue