From 35b1f1ed606dabdce95561d582890a9d80fe8c25 Mon Sep 17 00:00:00 2001 From: Radek Stepan Date: Thu, 30 Oct 2014 18:36:40 -0600 Subject: [PATCH] coverage for lines, reorg todo tasks --- Makefile | 3 +- README.md | 85 +++++++++++++++++++- docs/COVERAGE.html | 2 +- docs/TODO.md | 140 +++++++++++++-------------------- src/models/config.coffee | 2 - src/modules/chart/lines.coffee | 8 +- test/lines.coffee | 41 +++++++++- 7 files changed, 182 insertions(+), 99 deletions(-) diff --git a/Makefile b/Makefile index 94e7e55..bb957e1 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,8 @@ watch-js: # Watch the styles. watch-css: - $(GRUNT) watch + $(GRUNT) css # first build + $(GRUNT) watch # then watch # Serve locally. serve: diff --git a/README.md b/README.md index af097f7..963d368 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +*Existing users: The url mapping has been preserved from the original app, we are just using a different domain. If you'd like to use the previous version(s), grab the tags `v1`, `v2`.* + #burnchart GitHub Burndown Chart as a service. Answers the question "are my projects on track"? @@ -7,4 +9,85 @@ GitHub Burndown Chart as a service. Answers the question "are my projects on tra [![Dependencies](http://img.shields.io/david/radekstepan/burnchart.svg?style=flat)](https://david-dm.org/radekstepan/burnchart) [![License](http://img.shields.io/badge/license-AGPL--3.0-red.svg?style=flat)](LICENSE) -![image](https://raw.githubusercontent.com/radekstepan/burnchart/assembly/public/screenshots.jpg) \ No newline at end of file +![image](https://raw.githubusercontent.com/radekstepan/burnchart/assembly/public/screenshots.jpg) + +##Features + +1. Running from the browser, apart from GitHub account sign in. +1. Private repos; sign in with your GitHub account. +1. Store projects in browser's `localStorage`. +1. Off days; specify which days of the week to leave out from ideal burndown progression line. +1. Trend line; to see if you can make it to the deadline at this pace. +1. Different point counting strategies; select from 1 issues = 1 point or read size from issue label. + +##Configuration + +At the moment, there is no ui exposed to change the app settings. You have to edit the `src/models/config.coffee` file. + +An array of days when we are not working where Monday = 1. The ideal progression line won't *drop* on these days. + +```coffeescript +"off_days": [ ] +``` + +Choose from `ONE_SIZE` which means each issue is worth 1 point or `LABELS` where issue labels determine its size. + +```coffeescript +"points": "ONE_SIZE" +``` + +If you specify `LABELS` above, here is the place set the regex used to parse the number out of a label. When multiple matching size labels exist, their sum is taken. + +```coffeescript +"size_label": /^size (\d+)$/ +``` + +##Build + +The app is built using [Node](http://nodejs.org/). To install dev dependencies: + +```bash +$ make install +``` + +###Development + +To create an unminified package with source maps for debugging: + +```bash +$ make watch +``` + +You can then start a local http server with: + +```bash +$ make serve +``` + +To test your changes run: + +```bash +$ make test +``` + +And finally for code coverage: + +```bash +$ make coverage +``` + +There is currently a bug that incorrectly shows code coverage (using [blanket.js](http://blanketjs.org/)) for modules that are loaded using [proxyquire](https://github.com/thlorenz/proxyquire). + +###Production + +To make a minified package for production: + +```bash +$ make build +``` + +You can then publish the contents of the `public` folder to `gh-pages` branch with: + +```bash +$ make publish +``` \ No newline at end of file diff --git a/docs/COVERAGE.html b/docs/COVERAGE.html index 3e87d1c..168ff7e 100644 --- a/docs/COVERAGE.html +++ b/docs/COVERAGE.html @@ -352,4 +352,4 @@ code .string { color: #5890AD } code .keyword { color: #8A6343 } code .number { color: #2F6FAD } -

Coverage

67%
478
323
155

/home/radek/dev/burnchart/src/models/config.coffee

100%
4
4
0
LineHitsSource
11(function() {
21 var Model;
3
41 Model = require('../utils/ractive/model.coffee');
5
61 module.exports = new Model({
7 'name': 'models/config',
8 "data": {
9 "firebase": "burnchart",
10 "provider": "github",
11 "fields": {
12 "milestone": ["closed_issues", "created_at", "description", "due_on", "number", "open_issues", "title", "updated_at"]
13 },
14 "chart": {
15 "off_days": [],
16 "datetime": /^(\d{4}-\d{2}-\d{2})T(.*)/,
17 "size_label": /^size (\d+)$/,
18 "location": /^#!((\/[^\/]+){2,3})$/,
19 "points": 'ONE_SIZE'
20 },
21 "request": {
22 "timeout": 5e3
23 }
24 }
25 });
26
27}).call(this);
28

/home/radek/dev/burnchart/src/models/projects.coffee

80%
122
98
24
LineHitsSource
11(function() {
21 var Model, config, lscache, semver, sortedIndex, stats, user, _,
3 __slice = [].slice;
4
51 _ = require('lodash');
6
71 lscache = require('lscache');
8
91 sortedIndex = require('sortedindex-compare');
10
111 semver = require('semver');
12
131 Model = require('../utils/ractive/model.coffee');
14
151 config = require('../models/config.coffee');
16
171 stats = require('../modules/stats.coffee');
18
191 user = require('./user.coffee');
20
211 module.exports = new Model({
22 'name': 'models/projects',
23 'data': {
24 'sortBy': 'priority',
25 'sortFns': ['progress', 'priority', 'name']
26 },
27 comparator: function() {
2814 var deIdx, defaults, list, sortBy, _ref;
2914 _ref = this.data, list = _ref.list, sortBy = _ref.sortBy;
3014 deIdx = (function(_this) {
3114 return function(fn) {
3214 return function() {
3311 var i, j, rest, _arg;
3411 _arg = arguments[0], rest = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
3511 i = _arg[0], j = _arg[1];
3611 return fn.apply(_this, [[list[i], list[i].milestones[j]]].concat(rest));
37 };
38 };
39 })(this);
4014 defaults = function(arr, hash) {
417 var i, item, k, keys, p, ref, v, _i, _len, _results;
427 _results = [];
437 for (_i = 0, _len = arr.length; _i < _len; _i++) {
4414 item = arr[_i];
4514 _results.push((function() {
4614 var _results1;
4714 _results1 = [];
4814 for (k in hash) {
4926 v = hash[k];
5026 ref = item;
5126 _results1.push((function() {
5226 var _j, _len1, _ref1, _results2;
5326 _ref1 = keys = k.split('.');
5426 _results2 = [];
5526 for (i = _j = 0, _len1 = _ref1.length; _j < _len1; i = ++_j) {
5666 p = _ref1[i];
5766 if (i === keys.length - 1) {
5826 _results2.push(ref[p] != null ? ref[p] : ref[p] = v);
59 } else {
6040 _results2.push(ref = ref[p] != null ? ref[p] : ref[p] = {});
61 }
62 }
6326 return _results2;
64 })());
65 }
6614 return _results1;
67 })());
68 }
697 return _results;
70 };
7114 switch (sortBy) {
72 case 'progress':
732 return deIdx(function(_arg, _arg1) {
741 var aM, aP, bM, bP;
751 aP = _arg[0], aM = _arg[1];
761 bP = _arg1[0], bM = _arg1[1];
771 defaults([aM, bM], {
78 'stats.progress.points': 0
79 });
801 return aM.stats.progress.points - bM.stats.progress.points;
81 });
82 case 'priority':
837 return deIdx(function(_arg, _arg1) {
846 var $a, $b, aM, aP, bM, bP, _ref1;
856 aP = _arg[0], aM = _arg[1];
866 bP = _arg1[0], bM = _arg1[1];
876 defaults([aM, bM], {
88 'stats.progress.time': 0,
89 'stats.days': 1e3
90 });
916 _ref1 = _.map([aM, bM], function(_arg2) {
9212 var stats;
9312 stats = _arg2.stats;
9412 return (stats.progress.points - stats.progress.time) * stats.days;
95 }), $a = _ref1[0], $b = _ref1[1];
966 return $b - $a;
97 });
98 case 'name':
995 return deIdx(function(_arg, _arg1) {
1004 var aM, aP, bM, bP, name, owner;
1014 aP = _arg[0], aM = _arg[1];
1024 bP = _arg1[0], bM = _arg1[1];
1034 if (owner = bP.owner.localeCompare(aP.owner)) {
1040 return owner;
105 }
1064 if (name = bP.name.localeCompare(aP.name)) {
1070 return name;
108 }
1094 if (semver.valid(bM.title) && semver.valid(aM.title)) {
1101 return semver.gt(bM.title, aM.title);
111 } else {
1123 return bM.title.localeCompare(aM.title);
113 }
114 });
115 default:
1160 return function() {
1170 return 0;
118 };
119 }
120 },
121 find: function(project) {
1220 return _.find(this.data.list, project);
123 },
124 exists: function() {
1250 return !!this.find.apply(this, arguments);
126 },
127 add: function(project) {
1280 if (!this.exists(project)) {
1290 return this.push('list', project);
130 }
131 },
132 findIndex: function(_arg) {
13314 var name, owner;
13414 owner = _arg.owner, name = _arg.name;
13514 return _.findIndex(this.data.list, {
136 owner: owner,
137 name: name
138 });
139 },
140 addMilestone: function(project, milestone) {
14114 var i, j;
14214 _.extend(milestone, {
143 'stats': stats(milestone)
144 });
14514 if ((i = this.findIndex(project)) < 0) {
1460 throw 500;
147 }
14814 if (project.milestones != null) {
1498 this.push("list." + i + ".milestones", milestone);
1508 j = this.data.list[i].milestones.length - 1;
151 } else {
1526 this.set("list." + i + ".milestones", [milestone]);
1536 j = 0;
154 }
15514 return this.sort([i, j], [project, milestone]);
156 },
157 saveError: function(project, err) {
1580 var idx;
1590 if ((idx = this.findIndex(project)) > -1) {
1600 if (project.errors != null) {
1610 return this.push("list." + idx + ".errors", err);
162 } else {
1630 return this.set("list." + idx + ".errors", [err]);
164 }
165 } else {
1660 throw 500;
167 }
168 },
169 demo: function() {
1700 return this.set({
171 'list': [
172 {
173 'owner': 'mbostock',
174 'name': 'd3'
175 }, {
176 'owner': 'medic',
177 'name': 'medic-webapp'
178 }, {
179 'owner': 'ractivejs',
180 'name': 'ractive'
181 }, {
182 'owner': 'radekstepan',
183 'name': 'disposable'
184 }, {
185 'owner': 'rails',
186 'name': 'rails'
187 }, {
188 'owner': 'rails',
189 'name': 'spring'
190 }
191 ],
192 'index': []
193 });
194 },
195 clear: function() {
1966 return this.set({
197 'list': [],
198 'index': []
199 });
200 },
201 sort: function(ref, data) {
20217 var i, idx, index, j, m, p, _i, _j, _len, _len1, _ref, _ref1;
20317 index = this.data.index || [];
20417 if (ref) {
20514 idx = sortedIndex(index, data, this.comparator());
20614 index.splice(idx, 0, ref);
207 } else {
2083 _ref = this.data.list;
2093 for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) {
2100 p = _ref[i];
2110 if (p.milestones == null) {
2120 continue;
213 }
2140 _ref1 = p.milestones;
2150 for (j = _j = 0, _len1 = _ref1.length; _j < _len1; j = ++_j) {
2160 m = _ref1[j];
2170 idx = sortedIndex(index, [p, m], this.comparator());
2180 index.splice(idx, 0, [i, j]);
219 }
220 }
221 }
22217 return this.set('index', index);
223 },
224 onconstruct: function() {
2251 this.subscribe('!projects/add', this.add, this);
2261 return this.subscribe('!projects/demo', this.demo, this);
227 },
228 onrender: function() {
2291 this.set('list', lscache.get('projects') || []);
2301 this.observe('list', function(projects) {
23126 return lscache.set('projects', _.pluckMany(projects, ['owner', 'name']));
232 }, {
233 'init': false
234 });
2351 return this.observe('sortBy', function() {
2363 this.set('index', null);
2373 return this.sort();
238 }, {
239 'init': false
240 });
241 }
242 });
243
244}).call(this);
245

/home/radek/dev/burnchart/src/models/user.coffee

100%
4
4
0
LineHitsSource
11(function() {
21 var Model;
3
41 Model = require('../utils/ractive/model.coffee');
5
61 module.exports = new Model({
7 'name': 'models/user'
8 });
9
10}).call(this);
11

/home/radek/dev/burnchart/src/modules/chart/lines.coffee

6%
88
6
82
LineHitsSource
11(function() {
21 var config, d3, _,
30 __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; };
4
51 _ = require('lodash');
6
71 d3 = require('d3');
8
91 config = require('../../models/config.coffee');
10
111 module.exports = {
12 actual: function(issues, created_at, total) {
130 var head, max, min, range, rest;
140 head = [
15 {
16 'date': new Date(created_at),
17 'points': total
18 }
19 ];
200 min = +Infinity;
210 max = -Infinity;
220 rest = _.map(issues, function(issue) {
230 var closed_at, size;
240 size = issue.size, closed_at = issue.closed_at;
250 if (size < min) {
260 min = size;
27 }
280 if (size > max) {
290 max = size;
30 }
310 issue.date = new Date(closed_at);
320 issue.points = total -= size;
330 return issue;
34 });
350 range = d3.scale.linear().domain([min, max]).range([5, 8]);
360 rest = _.map(rest, function(issue) {
370 issue.radius = range(issue.size);
380 return issue;
39 });
400 return [].concat(head, rest);
41 },
42 ideal: function(a, b, total) {
430 var cutoff, d, days, length, m, now, once, velocity, y, _ref, _ref1;
440 if (b < a) {
450 _ref = [a, b], b = _ref[0], a = _ref[1];
46 }
470 _ref1 = _.map(a.match(config.data.chart.datetime)[1].split('-'), function(v) {
480 return parseInt(v);
49 }), y = _ref1[0], m = _ref1[1], d = _ref1[2];
500 cutoff = new Date(b);
510 days = [];
520 length = 0;
530 (once = function(inc) {
540 var day, day_of;
550 day = new Date(y, m - 1, d + inc);
560 if (!(day_of = day.getDay())) {
570 day_of = 7;
58 }
590 if (__indexOf.call(config.data.chart.off_days, day_of) >= 0) {
600 days.push({
61 date: day,
62 off_day: true
63 });
64 } else {
650 length += 1;
660 days.push({
67 date: day
68 });
69 }
700 if (!(day > cutoff)) {
710 return once(inc + 1);
72 }
73 })(0);
740 velocity = total / (length - 1);
750 days = _.map(days, function(day, i) {
760 day.points = total;
770 if (days[i] && !days[i].off_day) {
780 total -= velocity;
79 }
800 return day;
81 });
820 if ((now = new Date) > cutoff) {
830 days.push({
84 date: now,
85 points: 0
86 });
87 }
880 return days;
89 },
90 trend: function(actual, created_at, due_on) {
910 var a, b, b1, c1, e, fn, intercept, l, last, now, slope, start, values;
920 if (!actual.length) {
930 return [];
94 }
950 start = +actual[0].date;
960 values = _.map(actual, function(_arg) {
970 var date, points;
980 date = _arg.date, points = _arg.points;
990 return [+date - start, points];
100 });
1010 last = actual[actual.length - 1];
1020 values.push([+(new Date) - start, last.points]);
1030 b1 = 0;
1040 e = 0;
1050 c1 = 0;
1060 a = (l = values.length) * _.reduce(values, function(sum, _arg) {
1070 var a, b;
1080 a = _arg[0], b = _arg[1];
1090 b1 += a;
1100 e += b;
1110 c1 += Math.pow(a, 2);
1120 return sum + (a * b);
113 }, 0);
1140 slope = (a - (b1 * e)) / ((l * c1) - (Math.pow(b1, 2)));
1150 intercept = (e - (slope * b1)) / l;
1160 fn = function(x) {
1170 return slope * x + intercept;
118 };
1190 created_at = new Date(created_at);
1200 now = new Date;
1210 if (due_on) {
1220 due_on = new Date(due_on);
1230 if (now > due_on) {
1240 due_on = now;
125 }
126 } else {
1270 due_on = now;
128 }
1290 a = created_at - start;
1300 b = due_on - start;
1310 return [
132 {
133 'date': created_at,
134 'points': fn(a)
135 }, {
136 'date': due_on,
137 'points': fn(b)
138 }
139 ];
140 }
141 };
142
143}).call(this);
144

/home/radek/dev/burnchart/src/modules/github/issues.coffee

98%
51
50
1
LineHitsSource
11(function() {
21 var async, calcSize, config, oneStatus, request, _;
3
41 _ = require('lodash');
5
61 async = require('async');
7
81 config = require('../../models/config.coffee');
9
101 request = require('./request.coffee');
11
121 module.exports = {
13 fetchAll: function(repo, cb) {
1414 return async.parallel([_.partial(oneStatus, repo, 'open'), _.partial(oneStatus, repo, 'closed')], function(err, _arg) {
1514 var closed, open;
1614 open = _arg[0], closed = _arg[1];
1714 return cb(err, {
18 open: open,
19 closed: closed
20 });
21 });
22 }
23 };
24
251 calcSize = function(list) {
2626 var issue, size, _i, _len;
2726 switch (config.data.chart.points) {
28 case 'ONE_SIZE':
2916 size = list.length;
3016 for (_i = 0, _len = list.length; _i < _len; _i++) {
311006 issue = list[_i];
321006 issue.size = 1;
33 }
3416 break;
35 case 'LABELS':
3610 size = 0;
3710 list = _.filter(list, function(issue) {
3816 var labels;
3916 if (!(labels = issue.labels)) {
402 return false;
41 }
4214 issue.size = _.reduce(labels, function(sum, label) {
4316 var matches;
4416 if (!(matches = label.name.match(config.data.chart.size_label))) {
456 return sum;
46 }
4710 return sum += parseInt(matches[1]);
48 }, 0);
4914 size += issue.size;
5014 return !!issue.size;
51 });
5210 break;
53 default:
540 throw 500;
55 }
5626 return {
57 list: list,
58 size: size
59 };
60 };
61
621 oneStatus = function(repo, state, cb) {
6328 var done, fetchPage, results;
6428 results = [];
6528 done = function(err) {
6628 if (err) {
672 return cb(err);
68 }
6926 return cb(null, calcSize(results));
70 };
7128 return (fetchPage = function(page) {
7236 return request.allIssues(repo, {
73 state: state,
74 page: page
75 }, function(err, data) {
7636 if (err) {
772 return done(err);
78 }
7934 if (!data.length) {
806 return done(null, results);
81 }
8228 results = results.concat(_.sortBy(data, 'closed_at'));
8328 if (data.length < 100) {
8420 return done(null, results);
85 }
868 return fetchPage(page + 1);
87 });
88 })(1);
89 };
90
91}).call(this);
92

/home/radek/dev/burnchart/src/modules/github/request.coffee

73%
124
91
33
LineHitsSource
12(function() {
22 var config, defaults, error, headers, isReady, isValid, ready, request, response, stack, superagent, user, _;
3
42 _ = require('lodash');
5
62 superagent = require('superagent');
7
82 require('../../utils/mixins.coffee');
9
102 config = require('../../models/config.coffee');
11
122 user = require('../../models/user.coffee');
13
142 superagent.parse = {
15 'application/json': function(res) {
160 var e;
170 try {
180 return JSON.parse(res);
19 } catch (_error) {
200 e = _error;
210 return {};
22 }
23 }
24 };
25
262 defaults = {
27 'github': {
28 'host': 'api.github.com',
29 'protocol': 'https'
30 }
31 };
32
332 module.exports = {
34 repo: function(_arg, cb) {
350 var name, owner;
360 owner = _arg.owner, name = _arg.name;
370 if (!isValid({
38 owner: owner,
39 name: name
40 })) {
410 return cb('Request is malformed');
42 }
430 return ready(function() {
440 var data;
450 data = _.defaults({
46 'path': "/repos/" + owner + "/" + name,
47 'headers': headers(user.data.accessToken)
48 }, defaults.github);
490 return request(data, cb);
50 });
51 },
52 allMilestones: function(_arg, cb) {
531 var name, owner;
541 owner = _arg.owner, name = _arg.name;
551 if (!isValid({
56 owner: owner,
57 name: name
58 })) {
590 return cb('Request is malformed');
60 }
611 return ready(function() {
621 var data;
631 data = _.defaults({
64 'path': "/repos/" + owner + "/" + name + "/milestones",
65 'query': {
66 'state': 'open',
67 'sort': 'due_date',
68 'direction': 'asc'
69 },
70 'headers': headers(user.data.accessToken)
71 }, defaults.github);
721 return request(data, cb);
73 });
74 },
75 oneMilestone: function(_arg, cb) {
763 var milestone, name, owner;
773 owner = _arg.owner, name = _arg.name, milestone = _arg.milestone;
783 if (!isValid({
79 owner: owner,
80 name: name,
81 milestone: milestone
82 })) {
830 return cb('Request is malformed');
84 }
853 return ready(function() {
863 var data;
873 data = _.defaults({
88 'path': "/repos/" + owner + "/" + name + "/milestones/" + milestone,
89 'query': {
90 'state': 'open',
91 'sort': 'due_date',
92 'direction': 'asc'
93 },
94 'headers': headers(user.data.accessToken)
95 }, defaults.github);
963 return request(data, cb);
97 });
98 },
99 allIssues: function(_arg, query, cb) {
1002 var milestone, name, owner;
1012 owner = _arg.owner, name = _arg.name, milestone = _arg.milestone;
1022 if (!isValid({
103 owner: owner,
104 name: name,
105 milestone: milestone
106 })) {
1070 return cb('Request is malformed');
108 }
1092 return ready(function() {
1102 var data;
1112 data = _.defaults({
112 'path': "/repos/" + owner + "/" + name + "/issues",
113 'query': _.extend(query, {
114 milestone: milestone,
115 'per_page': '100'
116 }),
117 'headers': headers(user.data.accessToken)
118 }, defaults.github);
1192 return request(data, cb);
120 });
121 }
122 };
123
1242 request = function(_arg, cb) {
1256 var exited, headers, host, k, path, protocol, q, query, req, timeout, v;
1266 protocol = _arg.protocol, host = _arg.host, path = _arg.path, query = _arg.query, headers = _arg.headers;
1276 exited = false;
1286 q = query ? '?' + ((function() {
1296 var _results;
1306 _results = [];
1316 for (k in query) {
13216 v = query[k];
13316 _results.push("" + k + "=" + v);
134 }
1356 return _results;
136 })()).join('&') : '';
1376 req = superagent.get("" + protocol + "://" + host + path + q);
1386 for (k in headers) {
13912 v = headers[k];
14012 req.set(k, v);
141 }
1426 timeout = setTimeout(function() {
1431 exited = true;
1441 return cb('Request has timed out');
145 }, config.data.request.timeout);
1466 return req.end(function(err, data) {
1476 if (exited) {
1481 return;
149 }
1505 exited = true;
1515 clearTimeout(timeout);
1525 return response(err, data, cb);
153 });
154 };
155
1562 response = function(err, data, cb) {
1575 var _ref;
1585 if (err) {
1590 return cb(error(err));
160 }
1615 if (data.statusType !== 2) {
1622 if ((data != null ? (_ref = data.body) != null ? _ref.message : void 0 : void 0) != null) {
1631 return cb(data.body.message);
164 }
1651 return cb(data.error.message);
166 }
1673 return cb(null, data.body);
168 };
169
1702 headers = function(token) {
1716 var h;
1726 h = {
173 'Content-Type': 'application/json',
174 'Accept': 'application/vnd.github.v3'
175 };
1766 if (token != null) {
1770 h.Authorization = "token " + token;
178 }
1796 return h;
180 };
181
1822 isValid = function(obj) {
1836 var key, rules, val;
1846 rules = {
185 'owner': function(val) {
1866 return val != null;
187 },
188 'name': function(val) {
1896 return val != null;
190 },
191 'milestone': function(val) {
1925 return _.isInt(val);
193 }
194 };
1956 for (key in obj) {
19617 val = obj[key];
19717 if (key in rules && !rules[key](val)) {
1980 return false;
199 }
200 }
2016 return true;
202 };
203
2042 isReady = user.data.ready;
205
2062 stack = [];
207
2082 ready = function(cb) {
2096 if (isReady) {
2106 return cb();
211 } else {
2120 return stack.push(cb);
213 }
214 };
215
2162 user.observe('ready', function(val) {
2174 var _results;
2184 isReady = val;
2194 if (val) {
2202 _results = [];
2212 while (stack.length) {
2220 _results.push(stack.shift()());
223 }
2242 return _results;
225 }
226 });
227
2282 error = function(err) {
2290 var message;
2300 switch (false) {
231 case !_.isString(err):
2320 message = err;
2330 break;
234 case !_.isArray(err):
2350 message = err[1];
2360 break;
237 case !(_.isObject(err) && _.isString(err.message)):
2380 message = err.message;
239 }
2400 if (!message) {
2410 try {
2420 message = JSON.stringify(err);
243 } catch (_error) {
2440 message = err.toString();
245 }
246 }
2470 return message;
248 };
249
250}).call(this);
251

/home/radek/dev/burnchart/src/modules/mediator.coffee

100%
5
5
0
LineHitsSource
11(function() {
21 var Mediator, Ractive;
3
41 Ractive = require('ractive');
5
61 Mediator = Ractive.extend({});
7
81 module.exports = new Mediator();
9
10}).call(this);
11

/home/radek/dev/burnchart/src/modules/stats.coffee

97%
34
33
1
LineHitsSource
11(function() {
21 var moment, progress;
3
41 moment = require('moment');
5
61 progress = function(a, b) {
76 if (a + b === 0) {
80 return 0;
9 } else {
106 return 100 * (a / (b + a));
11 }
12 };
13
141 module.exports = function(milestone) {
1519 var a, b, c, days, isDone, isEmpty, isOnTime, isOverdue, points, time;
1619 if (milestone.stats != null) {
1714 return milestone.stats;
18 }
195 isDone = false;
205 isOnTime = true;
215 isOverdue = false;
225 isEmpty = true;
235 points = 0;
245 a = milestone.issues.closed.size;
255 b = milestone.issues.open.size;
265 if (a + b > 0) {
273 isEmpty = false;
283 points = progress(a, b);
293 if (points === 100) {
301 isDone = true;
31 }
32 }
335 if (milestone.due_on == null) {
342 return {
35 isOverdue: isOverdue,
36 isOnTime: isOnTime,
37 isDone: isDone,
38 isEmpty: isEmpty,
39 'progress': {
40 points: points
41 }
42 };
43 }
443 a = +new Date(milestone.created_at);
453 b = +(new Date);
463 c = +new Date(milestone.due_on);
473 if (b > c) {
481 isOverdue = true;
49 }
503 time = progress(b - a, c - b);
513 days = (moment(b).diff(moment(a), 'days')) / 100;
523 isOnTime = points > time;
533 return {
54 isDone: isDone,
55 days: days,
56 isOnTime: isOnTime,
57 isOverdue: isOverdue,
58 'progress': {
59 points: points,
60 time: time
61 }
62 };
63 };
64
65}).call(this);
66

/home/radek/dev/burnchart/src/utils/mixins.coffee

92%
13
12
1
LineHitsSource
11(function() {
21 var _;
3
41 _ = require('lodash');
5
61 _.mixin({
7 'pluckMany': function(source, keys) {
826 if (!_.isArray(keys)) {
90 throw '`keys` needs to be an Array';
10 }
1126 return _.map(source, function(item) {
1220 var obj;
1320 obj = {};
1420 _.each(keys, function(key) {
1540 return obj[key] = item[key];
16 });
1720 return obj;
18 });
19 },
20 'isInt': function(val) {
215 return !isNaN(val) && parseInt(Number(val)) === val && !isNaN(parseInt(val, 10));
22 }
23 });
24
25}).call(this);
26

/home/radek/dev/burnchart/src/utils/ractive/eventful.coffee

45%
24
11
13
LineHitsSource
11(function() {
21 var Ractive, mediator, _;
3
41 _ = require('lodash');
5
61 Ractive = require('ractive');
7
81 mediator = require('../../modules/mediator.coffee');
9
101 module.exports = Ractive.extend({
11 subscribe: function(name, cb, ctx) {
122 if (ctx == null) {
130 ctx = this;
14 }
152 if (!_.isArray(this._subs)) {
161 this._subs = [];
17 }
182 if (_.isFunction(cb)) {
192 return this._subs.push(mediator.on(name, _.bind(cb, ctx)));
20 } else {
210 return console.log("Warning: `cb` is not a function");
22 }
23 },
24 publish: function() {
250 return mediator.fire.apply(mediator, arguments);
26 },
27 onteardown: function() {
280 var sub, _i, _len, _ref, _results;
290 if (_.isArray(this._subs)) {
300 _ref = this._subs;
310 _results = [];
320 for (_i = 0, _len = _ref.length; _i < _len; _i++) {
330 sub = _ref[_i];
340 if (_.isFunction(sub.cancel)) {
350 _results.push(sub.cancel());
36 } else {
370 _results.push(console.log("Warning: `sub.cancel` is not a function"));
38 }
39 }
400 return _results;
41 }
42 }
43 });
44
45}).call(this);
46

/home/radek/dev/burnchart/src/utils/ractive/model.coffee

100%
9
9
0
LineHitsSource
11(function() {
21 var Eventful;
3
41 Eventful = require('./eventful.coffee');
5
61 module.exports = function(opts) {
73 var Model, model;
83 Model = Eventful.extend(opts);
93 model = new Model();
103 model.render();
113 return model;
12 };
13
14}).call(this);
15
\ No newline at end of file +

Coverage

83%
478
399
79

/home/radek/dev/burnchart/src/models/config.coffee

100%
4
4
0
LineHitsSource
11(function() {
21 var Model;
3
41 Model = require('../utils/ractive/model.coffee');
5
61 module.exports = new Model({
7 'name': 'models/config',
8 "data": {
9 "firebase": "burnchart",
10 "provider": "github",
11 "fields": {
12 "milestone": ["closed_issues", "created_at", "description", "due_on", "number", "open_issues", "title", "updated_at"]
13 },
14 "chart": {
15 "off_days": [],
16 "datetime": /^(\d{4}-\d{2}-\d{2})T(.*)/,
17 "size_label": /^size (\d+)$/,
18 "points": 'ONE_SIZE'
19 },
20 "request": {
21 "timeout": 5e3
22 }
23 }
24 });
25
26}).call(this);
27

/home/radek/dev/burnchart/src/models/projects.coffee

80%
122
98
24
LineHitsSource
11(function() {
21 var Model, config, lscache, semver, sortedIndex, stats, user, _,
3 __slice = [].slice;
4
51 _ = require('lodash');
6
71 lscache = require('lscache');
8
91 sortedIndex = require('sortedindex-compare');
10
111 semver = require('semver');
12
131 Model = require('../utils/ractive/model.coffee');
14
151 config = require('../models/config.coffee');
16
171 stats = require('../modules/stats.coffee');
18
191 user = require('./user.coffee');
20
211 module.exports = new Model({
22 'name': 'models/projects',
23 'data': {
24 'sortBy': 'priority',
25 'sortFns': ['progress', 'priority', 'name']
26 },
27 comparator: function() {
2814 var deIdx, defaults, list, sortBy, _ref;
2914 _ref = this.data, list = _ref.list, sortBy = _ref.sortBy;
3014 deIdx = (function(_this) {
3114 return function(fn) {
3214 return function() {
3311 var i, j, rest, _arg;
3411 _arg = arguments[0], rest = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
3511 i = _arg[0], j = _arg[1];
3611 return fn.apply(_this, [[list[i], list[i].milestones[j]]].concat(rest));
37 };
38 };
39 })(this);
4014 defaults = function(arr, hash) {
417 var i, item, k, keys, p, ref, v, _i, _len, _results;
427 _results = [];
437 for (_i = 0, _len = arr.length; _i < _len; _i++) {
4414 item = arr[_i];
4514 _results.push((function() {
4614 var _results1;
4714 _results1 = [];
4814 for (k in hash) {
4926 v = hash[k];
5026 ref = item;
5126 _results1.push((function() {
5226 var _j, _len1, _ref1, _results2;
5326 _ref1 = keys = k.split('.');
5426 _results2 = [];
5526 for (i = _j = 0, _len1 = _ref1.length; _j < _len1; i = ++_j) {
5666 p = _ref1[i];
5766 if (i === keys.length - 1) {
5826 _results2.push(ref[p] != null ? ref[p] : ref[p] = v);
59 } else {
6040 _results2.push(ref = ref[p] != null ? ref[p] : ref[p] = {});
61 }
62 }
6326 return _results2;
64 })());
65 }
6614 return _results1;
67 })());
68 }
697 return _results;
70 };
7114 switch (sortBy) {
72 case 'progress':
732 return deIdx(function(_arg, _arg1) {
741 var aM, aP, bM, bP;
751 aP = _arg[0], aM = _arg[1];
761 bP = _arg1[0], bM = _arg1[1];
771 defaults([aM, bM], {
78 'stats.progress.points': 0
79 });
801 return aM.stats.progress.points - bM.stats.progress.points;
81 });
82 case 'priority':
837 return deIdx(function(_arg, _arg1) {
846 var $a, $b, aM, aP, bM, bP, _ref1;
856 aP = _arg[0], aM = _arg[1];
866 bP = _arg1[0], bM = _arg1[1];
876 defaults([aM, bM], {
88 'stats.progress.time': 0,
89 'stats.days': 1e3
90 });
916 _ref1 = _.map([aM, bM], function(_arg2) {
9212 var stats;
9312 stats = _arg2.stats;
9412 return (stats.progress.points - stats.progress.time) * stats.days;
95 }), $a = _ref1[0], $b = _ref1[1];
966 return $b - $a;
97 });
98 case 'name':
995 return deIdx(function(_arg, _arg1) {
1004 var aM, aP, bM, bP, name, owner;
1014 aP = _arg[0], aM = _arg[1];
1024 bP = _arg1[0], bM = _arg1[1];
1034 if (owner = bP.owner.localeCompare(aP.owner)) {
1040 return owner;
105 }
1064 if (name = bP.name.localeCompare(aP.name)) {
1070 return name;
108 }
1094 if (semver.valid(bM.title) && semver.valid(aM.title)) {
1101 return semver.gt(bM.title, aM.title);
111 } else {
1123 return bM.title.localeCompare(aM.title);
113 }
114 });
115 default:
1160 return function() {
1170 return 0;
118 };
119 }
120 },
121 find: function(project) {
1220 return _.find(this.data.list, project);
123 },
124 exists: function() {
1250 return !!this.find.apply(this, arguments);
126 },
127 add: function(project) {
1280 if (!this.exists(project)) {
1290 return this.push('list', project);
130 }
131 },
132 findIndex: function(_arg) {
13314 var name, owner;
13414 owner = _arg.owner, name = _arg.name;
13514 return _.findIndex(this.data.list, {
136 owner: owner,
137 name: name
138 });
139 },
140 addMilestone: function(project, milestone) {
14114 var i, j;
14214 _.extend(milestone, {
143 'stats': stats(milestone)
144 });
14514 if ((i = this.findIndex(project)) < 0) {
1460 throw 500;
147 }
14814 if (project.milestones != null) {
1498 this.push("list." + i + ".milestones", milestone);
1508 j = this.data.list[i].milestones.length - 1;
151 } else {
1526 this.set("list." + i + ".milestones", [milestone]);
1536 j = 0;
154 }
15514 return this.sort([i, j], [project, milestone]);
156 },
157 saveError: function(project, err) {
1580 var idx;
1590 if ((idx = this.findIndex(project)) > -1) {
1600 if (project.errors != null) {
1610 return this.push("list." + idx + ".errors", err);
162 } else {
1630 return this.set("list." + idx + ".errors", [err]);
164 }
165 } else {
1660 throw 500;
167 }
168 },
169 demo: function() {
1700 return this.set({
171 'list': [
172 {
173 'owner': 'mbostock',
174 'name': 'd3'
175 }, {
176 'owner': 'medic',
177 'name': 'medic-webapp'
178 }, {
179 'owner': 'ractivejs',
180 'name': 'ractive'
181 }, {
182 'owner': 'radekstepan',
183 'name': 'disposable'
184 }, {
185 'owner': 'rails',
186 'name': 'rails'
187 }, {
188 'owner': 'rails',
189 'name': 'spring'
190 }
191 ],
192 'index': []
193 });
194 },
195 clear: function() {
1966 return this.set({
197 'list': [],
198 'index': []
199 });
200 },
201 sort: function(ref, data) {
20217 var i, idx, index, j, m, p, _i, _j, _len, _len1, _ref, _ref1;
20317 index = this.data.index || [];
20417 if (ref) {
20514 idx = sortedIndex(index, data, this.comparator());
20614 index.splice(idx, 0, ref);
207 } else {
2083 _ref = this.data.list;
2093 for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) {
2100 p = _ref[i];
2110 if (p.milestones == null) {
2120 continue;
213 }
2140 _ref1 = p.milestones;
2150 for (j = _j = 0, _len1 = _ref1.length; _j < _len1; j = ++_j) {
2160 m = _ref1[j];
2170 idx = sortedIndex(index, [p, m], this.comparator());
2180 index.splice(idx, 0, [i, j]);
219 }
220 }
221 }
22217 return this.set('index', index);
223 },
224 onconstruct: function() {
2251 this.subscribe('!projects/add', this.add, this);
2261 return this.subscribe('!projects/demo', this.demo, this);
227 },
228 onrender: function() {
2291 this.set('list', lscache.get('projects') || []);
2301 this.observe('list', function(projects) {
23126 return lscache.set('projects', _.pluckMany(projects, ['owner', 'name']));
232 }, {
233 'init': false
234 });
2351 return this.observe('sortBy', function() {
2363 this.set('index', null);
2373 return this.sort();
238 }, {
239 'init': false
240 });
241 }
242 });
243
244}).call(this);
245

/home/radek/dev/burnchart/src/models/user.coffee

100%
4
4
0
LineHitsSource
11(function() {
21 var Model;
3
41 Model = require('../utils/ractive/model.coffee');
5
61 module.exports = new Model({
7 'name': 'models/user'
8 });
9
10}).call(this);
11

/home/radek/dev/burnchart/src/modules/chart/lines.coffee

94%
88
83
5
LineHitsSource
11(function() {
21 var config, d3, _,
30 __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; };
4
51 _ = require('lodash');
6
71 d3 = require('d3');
8
91 config = require('../../models/config.coffee');
10
111 module.exports = {
12 actual: function(issues, created_at, total) {
131 var head, max, min, range, rest;
141 head = [
15 {
16 'date': new Date(created_at),
17 'points': total
18 }
19 ];
201 min = +Infinity;
211 max = -Infinity;
221 rest = _.map(issues, function(issue) {
233 var closed_at, size;
243 size = issue.size, closed_at = issue.closed_at;
253 if (size < min) {
263 min = size;
27 }
283 if (size > max) {
291 max = size;
30 }
313 issue.date = new Date(closed_at);
323 issue.points = total -= size;
333 return issue;
34 });
351 range = d3.scale.linear().domain([min, max]).range([5, 8]);
361 rest = _.map(rest, function(issue) {
373 issue.radius = range(issue.size);
383 return issue;
39 });
401 return [].concat(head, rest);
41 },
42 ideal: function(a, b, total) {
431 var cutoff, d, days, length, m, now, once, velocity, y, _ref, _ref1;
441 if (b < a) {
450 _ref = [a, b], b = _ref[0], a = _ref[1];
46 }
471 _ref1 = _.map(a.match(config.data.chart.datetime)[1].split('-'), function(v) {
483 return parseInt(v);
49 }), y = _ref1[0], m = _ref1[1], d = _ref1[2];
501 cutoff = new Date(b);
511 days = [];
521 length = 0;
531 (once = function(inc) {
543 var day, day_of;
553 day = new Date(y, m - 1, d + inc);
563 if (!(day_of = day.getDay())) {
571 day_of = 7;
58 }
593 if (__indexOf.call(config.data.chart.off_days, day_of) >= 0) {
600 days.push({
61 date: day,
62 off_day: true
63 });
64 } else {
653 length += 1;
663 days.push({
67 date: day
68 });
69 }
703 if (!(day > cutoff)) {
712 return once(inc + 1);
72 }
73 })(0);
741 velocity = total / (length - 1);
751 days = _.map(days, function(day, i) {
763 day.points = total;
773 if (days[i] && !days[i].off_day) {
783 total -= velocity;
79 }
803 return day;
81 });
821 if ((now = new Date) > cutoff) {
831 days.push({
84 date: now,
85 points: 0
86 });
87 }
881 return days;
89 },
90 trend: function(actual, created_at, due_on) {
911 var a, b, b1, c1, e, fn, intercept, l, last, now, slope, start, values;
921 if (!actual.length) {
930 return [];
94 }
951 start = +actual[0].date;
961 values = _.map(actual, function(_arg) {
973 var date, points;
983 date = _arg.date, points = _arg.points;
993 return [+date - start, points];
100 });
1011 last = actual[actual.length - 1];
1021 values.push([+(new Date) - start, last.points]);
1031 b1 = 0;
1041 e = 0;
1051 c1 = 0;
1061 a = (l = values.length) * _.reduce(values, function(sum, _arg) {
1074 var a, b;
1084 a = _arg[0], b = _arg[1];
1094 b1 += a;
1104 e += b;
1114 c1 += Math.pow(a, 2);
1124 return sum + (a * b);
113 }, 0);
1141 slope = (a - (b1 * e)) / ((l * c1) - (Math.pow(b1, 2)));
1151 intercept = (e - (slope * b1)) / l;
1161 fn = function(x) {
1172 return slope * x + intercept;
118 };
1191 created_at = new Date(created_at);
1201 now = new Date;
1211 if (due_on) {
1221 due_on = new Date(due_on);
1231 if (now > due_on) {
1241 due_on = now;
125 }
126 } else {
1270 due_on = now;
128 }
1291 a = created_at - start;
1301 b = due_on - start;
1311 return [
132 {
133 'date': created_at,
134 'points': fn(a)
135 }, {
136 'date': due_on,
137 'points': fn(b)
138 }
139 ];
140 }
141 };
142
143}).call(this);
144

/home/radek/dev/burnchart/src/modules/github/issues.coffee

98%
51
50
1
LineHitsSource
11(function() {
21 var async, calcSize, config, oneStatus, request, _;
3
41 _ = require('lodash');
5
61 async = require('async');
7
81 config = require('../../models/config.coffee');
9
101 request = require('./request.coffee');
11
121 module.exports = {
13 fetchAll: function(repo, cb) {
1414 return async.parallel([_.partial(oneStatus, repo, 'open'), _.partial(oneStatus, repo, 'closed')], function(err, _arg) {
1514 var closed, open;
1614 open = _arg[0], closed = _arg[1];
1714 return cb(err, {
18 open: open,
19 closed: closed
20 });
21 });
22 }
23 };
24
251 calcSize = function(list) {
2626 var issue, size, _i, _len;
2726 switch (config.data.chart.points) {
28 case 'ONE_SIZE':
2916 size = list.length;
3016 for (_i = 0, _len = list.length; _i < _len; _i++) {
311006 issue = list[_i];
321006 issue.size = 1;
33 }
3416 break;
35 case 'LABELS':
3610 size = 0;
3710 list = _.filter(list, function(issue) {
3816 var labels;
3916 if (!(labels = issue.labels)) {
402 return false;
41 }
4214 issue.size = _.reduce(labels, function(sum, label) {
4316 var matches;
4416 if (!(matches = label.name.match(config.data.chart.size_label))) {
456 return sum;
46 }
4710 return sum += parseInt(matches[1]);
48 }, 0);
4914 size += issue.size;
5014 return !!issue.size;
51 });
5210 break;
53 default:
540 throw 500;
55 }
5626 return {
57 list: list,
58 size: size
59 };
60 };
61
621 oneStatus = function(repo, state, cb) {
6328 var done, fetchPage, results;
6428 results = [];
6528 done = function(err) {
6628 if (err) {
672 return cb(err);
68 }
6926 return cb(null, calcSize(results));
70 };
7128 return (fetchPage = function(page) {
7236 return request.allIssues(repo, {
73 state: state,
74 page: page
75 }, function(err, data) {
7636 if (err) {
772 return done(err);
78 }
7934 if (!data.length) {
806 return done(null, results);
81 }
8228 results = results.concat(_.sortBy(data, 'closed_at'));
8328 if (data.length < 100) {
8420 return done(null, results);
85 }
868 return fetchPage(page + 1);
87 });
88 })(1);
89 };
90
91}).call(this);
92

/home/radek/dev/burnchart/src/modules/github/request.coffee

72%
124
90
34
LineHitsSource
12(function() {
22 var config, defaults, error, headers, isReady, isValid, ready, request, response, stack, superagent, user, _;
3
42 _ = require('lodash');
5
62 superagent = require('superagent');
7
82 require('../../utils/mixins.coffee');
9
102 config = require('../../models/config.coffee');
11
122 user = require('../../models/user.coffee');
13
142 superagent.parse = {
15 'application/json': function(res) {
160 var e;
170 try {
180 return JSON.parse(res);
19 } catch (_error) {
200 e = _error;
210 return {};
22 }
23 }
24 };
25
262 defaults = {
27 'github': {
28 'host': 'api.github.com',
29 'protocol': 'https'
30 }
31 };
32
332 module.exports = {
34 repo: function(_arg, cb) {
350 var name, owner;
360 owner = _arg.owner, name = _arg.name;
370 if (!isValid({
38 owner: owner,
39 name: name
40 })) {
410 return cb('Request is malformed');
42 }
430 return ready(function() {
440 var data;
450 data = _.defaults({
46 'path': "/repos/" + owner + "/" + name,
47 'headers': headers(user.data.accessToken)
48 }, defaults.github);
490 return request(data, cb);
50 });
51 },
52 allMilestones: function(_arg, cb) {
531 var name, owner;
541 owner = _arg.owner, name = _arg.name;
551 if (!isValid({
56 owner: owner,
57 name: name
58 })) {
590 return cb('Request is malformed');
60 }
611 return ready(function() {
621 var data;
631 data = _.defaults({
64 'path': "/repos/" + owner + "/" + name + "/milestones",
65 'query': {
66 'state': 'open',
67 'sort': 'due_date',
68 'direction': 'asc'
69 },
70 'headers': headers(user.data.accessToken)
71 }, defaults.github);
721 return request(data, cb);
73 });
74 },
75 oneMilestone: function(_arg, cb) {
763 var milestone, name, owner;
773 owner = _arg.owner, name = _arg.name, milestone = _arg.milestone;
783 if (!isValid({
79 owner: owner,
80 name: name,
81 milestone: milestone
82 })) {
830 return cb('Request is malformed');
84 }
853 return ready(function() {
863 var data;
873 data = _.defaults({
88 'path': "/repos/" + owner + "/" + name + "/milestones/" + milestone,
89 'query': {
90 'state': 'open',
91 'sort': 'due_date',
92 'direction': 'asc'
93 },
94 'headers': headers(user.data.accessToken)
95 }, defaults.github);
963 return request(data, cb);
97 });
98 },
99 allIssues: function(_arg, query, cb) {
1002 var milestone, name, owner;
1012 owner = _arg.owner, name = _arg.name, milestone = _arg.milestone;
1022 if (!isValid({
103 owner: owner,
104 name: name,
105 milestone: milestone
106 })) {
1070 return cb('Request is malformed');
108 }
1092 return ready(function() {
1102 var data;
1112 data = _.defaults({
112 'path': "/repos/" + owner + "/" + name + "/issues",
113 'query': _.extend(query, {
114 milestone: milestone,
115 'per_page': '100'
116 }),
117 'headers': headers(user.data.accessToken)
118 }, defaults.github);
1192 return request(data, cb);
120 });
121 }
122 };
123
1242 request = function(_arg, cb) {
1256 var exited, headers, host, k, path, protocol, q, query, req, timeout, v;
1266 protocol = _arg.protocol, host = _arg.host, path = _arg.path, query = _arg.query, headers = _arg.headers;
1276 exited = false;
1286 q = query ? '?' + ((function() {
1296 var _results;
1306 _results = [];
1316 for (k in query) {
13216 v = query[k];
13316 _results.push("" + k + "=" + v);
134 }
1356 return _results;
136 })()).join('&') : '';
1376 req = superagent.get("" + protocol + "://" + host + path + q);
1386 for (k in headers) {
13912 v = headers[k];
14012 req.set(k, v);
141 }
1426 timeout = setTimeout(function() {
1431 exited = true;
1441 return cb('Request has timed out');
145 }, config.data.request.timeout);
1466 return req.end(function(err, data) {
1475 if (exited) {
1480 return;
149 }
1505 exited = true;
1515 clearTimeout(timeout);
1525 return response(err, data, cb);
153 });
154 };
155
1562 response = function(err, data, cb) {
1575 var _ref;
1585 if (err) {
1590 return cb(error(err));
160 }
1615 if (data.statusType !== 2) {
1622 if ((data != null ? (_ref = data.body) != null ? _ref.message : void 0 : void 0) != null) {
1631 return cb(data.body.message);
164 }
1651 return cb(data.error.message);
166 }
1673 return cb(null, data.body);
168 };
169
1702 headers = function(token) {
1716 var h;
1726 h = {
173 'Content-Type': 'application/json',
174 'Accept': 'application/vnd.github.v3'
175 };
1766 if (token != null) {
1770 h.Authorization = "token " + token;
178 }
1796 return h;
180 };
181
1822 isValid = function(obj) {
1836 var key, rules, val;
1846 rules = {
185 'owner': function(val) {
1866 return val != null;
187 },
188 'name': function(val) {
1896 return val != null;
190 },
191 'milestone': function(val) {
1925 return _.isInt(val);
193 }
194 };
1956 for (key in obj) {
19617 val = obj[key];
19717 if (key in rules && !rules[key](val)) {
1980 return false;
199 }
200 }
2016 return true;
202 };
203
2042 isReady = user.data.ready;
205
2062 stack = [];
207
2082 ready = function(cb) {
2096 if (isReady) {
2106 return cb();
211 } else {
2120 return stack.push(cb);
213 }
214 };
215
2162 user.observe('ready', function(val) {
2174 var _results;
2184 isReady = val;
2194 if (val) {
2202 _results = [];
2212 while (stack.length) {
2220 _results.push(stack.shift()());
223 }
2242 return _results;
225 }
226 });
227
2282 error = function(err) {
2290 var message;
2300 switch (false) {
231 case !_.isString(err):
2320 message = err;
2330 break;
234 case !_.isArray(err):
2350 message = err[1];
2360 break;
237 case !(_.isObject(err) && _.isString(err.message)):
2380 message = err.message;
239 }
2400 if (!message) {
2410 try {
2420 message = JSON.stringify(err);
243 } catch (_error) {
2440 message = err.toString();
245 }
246 }
2470 return message;
248 };
249
250}).call(this);
251

/home/radek/dev/burnchart/src/modules/mediator.coffee

100%
5
5
0
LineHitsSource
11(function() {
21 var Mediator, Ractive;
3
41 Ractive = require('ractive');
5
61 Mediator = Ractive.extend({});
7
81 module.exports = new Mediator();
9
10}).call(this);
11

/home/radek/dev/burnchart/src/modules/stats.coffee

97%
34
33
1
LineHitsSource
11(function() {
21 var moment, progress;
3
41 moment = require('moment');
5
61 progress = function(a, b) {
76 if (a + b === 0) {
80 return 0;
9 } else {
106 return 100 * (a / (b + a));
11 }
12 };
13
141 module.exports = function(milestone) {
1519 var a, b, c, days, isDone, isEmpty, isOnTime, isOverdue, points, time;
1619 if (milestone.stats != null) {
1714 return milestone.stats;
18 }
195 isDone = false;
205 isOnTime = true;
215 isOverdue = false;
225 isEmpty = true;
235 points = 0;
245 a = milestone.issues.closed.size;
255 b = milestone.issues.open.size;
265 if (a + b > 0) {
273 isEmpty = false;
283 points = progress(a, b);
293 if (points === 100) {
301 isDone = true;
31 }
32 }
335 if (milestone.due_on == null) {
342 return {
35 isOverdue: isOverdue,
36 isOnTime: isOnTime,
37 isDone: isDone,
38 isEmpty: isEmpty,
39 'progress': {
40 points: points
41 }
42 };
43 }
443 a = +new Date(milestone.created_at);
453 b = +(new Date);
463 c = +new Date(milestone.due_on);
473 if (b > c) {
481 isOverdue = true;
49 }
503 time = progress(b - a, c - b);
513 days = (moment(b).diff(moment(a), 'days')) / 100;
523 isOnTime = points > time;
533 return {
54 isDone: isDone,
55 days: days,
56 isOnTime: isOnTime,
57 isOverdue: isOverdue,
58 'progress': {
59 points: points,
60 time: time
61 }
62 };
63 };
64
65}).call(this);
66

/home/radek/dev/burnchart/src/utils/mixins.coffee

92%
13
12
1
LineHitsSource
11(function() {
21 var _;
3
41 _ = require('lodash');
5
61 _.mixin({
7 'pluckMany': function(source, keys) {
826 if (!_.isArray(keys)) {
90 throw '`keys` needs to be an Array';
10 }
1126 return _.map(source, function(item) {
1220 var obj;
1320 obj = {};
1420 _.each(keys, function(key) {
1540 return obj[key] = item[key];
16 });
1720 return obj;
18 });
19 },
20 'isInt': function(val) {
215 return !isNaN(val) && parseInt(Number(val)) === val && !isNaN(parseInt(val, 10));
22 }
23 });
24
25}).call(this);
26

/home/radek/dev/burnchart/src/utils/ractive/eventful.coffee

45%
24
11
13
LineHitsSource
11(function() {
21 var Ractive, mediator, _;
3
41 _ = require('lodash');
5
61 Ractive = require('ractive');
7
81 mediator = require('../../modules/mediator.coffee');
9
101 module.exports = Ractive.extend({
11 subscribe: function(name, cb, ctx) {
122 if (ctx == null) {
130 ctx = this;
14 }
152 if (!_.isArray(this._subs)) {
161 this._subs = [];
17 }
182 if (_.isFunction(cb)) {
192 return this._subs.push(mediator.on(name, _.bind(cb, ctx)));
20 } else {
210 return console.log("Warning: `cb` is not a function");
22 }
23 },
24 publish: function() {
250 return mediator.fire.apply(mediator, arguments);
26 },
27 onteardown: function() {
280 var sub, _i, _len, _ref, _results;
290 if (_.isArray(this._subs)) {
300 _ref = this._subs;
310 _results = [];
320 for (_i = 0, _len = _ref.length; _i < _len; _i++) {
330 sub = _ref[_i];
340 if (_.isFunction(sub.cancel)) {
350 _results.push(sub.cancel());
36 } else {
370 _results.push(console.log("Warning: `sub.cancel` is not a function"));
38 }
39 }
400 return _results;
41 }
42 }
43 });
44
45}).call(this);
46

/home/radek/dev/burnchart/src/utils/ractive/model.coffee

100%
9
9
0
LineHitsSource
11(function() {
21 var Eventful;
3
41 Eventful = require('./eventful.coffee');
5
61 module.exports = function(opts) {
73 var Model, model;
83 Model = Eventful.extend(opts);
93 model = new Model();
103 model.render();
113 return model;
12 };
13
14}).call(this);
15
\ No newline at end of file diff --git a/docs/TODO.md b/docs/TODO.md index 48ae735..2720597 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1,89 +1,55 @@ -#Tasks to do - -- [ ] create notes about how original people can upgrade to burnchart -- [ ] clean up docs, track them on git or using Assembly system? -- [ ] check with austin@assembly.com if my repo looks good to be forked to Assembly -- [ ] fork it to Assembly -- [ ] landing page for the project and put message on `github-burndown-chart` repo -- [ ] provide a documentation site (because we ref it from hero) -- [ ] track users/make it easy for people to leave feedback - -##Next Release - -- [ ] http://burnchart.io#rails I would expect it to list all the projects for that owner so I can select one of them (Ryan); we could show a list of available project names with their: `description`, `private` flag and `has_issues` making the project greyed out if no issues found - ##Backlog -- [ ] coverage using `blanket` does not work in tests that `proxyquire` -- [ ] highlight today in the chart better -- [ ] one click to go from a project or milestone view to github -- [ ] be able to specify milestone by name (will nicely show in title) -- [ ] focus on form fields style (blue outline etc) -- [ ] switch off `user-select` on buttons -- [ ] make async pages transition so that there is no "jumping" on the page -- [ ] index page alert tooltip (like on chart page) -- [ ] app icon like http://thenounproject.com/term/fire/50966/ -- [ ] tell people if they have no due date -- [ ] calculate left margin based on the total number of points text width -- [ ] responsive layout -- [ ] show project name on the milestone page, in the title -- [ ] conctact the people that have starred the original burndown chart telling them about the repo; keep track of connects via a tiny crm/spreadsheet and use a custom email address like radek@burnchart.io -- [ ] html entities (like & at the bottom of the page) are not being rendered correctly; {{{}}} ? -- [ ] `rails/rails/24` has issues in two clusters as if merged from two milestones -- [ ] trendline cutting into axes -- [ ] topbar messages set position from `top`, does not work when we have scrolled on the page; show sticky to the top and move with our scroll; at the very least make them show up at the top and not be hidden -- [ ] GitHub Pages 404 file -- [ ] deal with Firebase timing out, are we still logged-in? -- [ ] check that we have not run out of requests to make -- [ ] what if milestone does not match our strategy? -- [ ] web storage and location hash supported by 93% of browsers; good enough? -- [ ] create a 500/400/loading system messages -- [ ] mediator `!app/notify/edit` will edit the current notification -- [ ] handle multiple notifications, for example success on closed milestone and then show a different chart or add a project -- [ ] be able to logout -- [ ] be able to delete added projects; see the cog at the bottom of tables -- [ ] how GitHub show commit activity in weekly slots, can we have something like this in the chart? Basically show commits in that week and their users -- [ ] add a chart straight from the hero banner -- [ ] on chart page show a little progress bar in the title -- [ ] use tap plugin for `Ractive` -- [ ] the app bundle (albeit uncompressed) clocks in at 1.5MB, reduce the size (`d3` is huge (use [grunt-smash](https://github.com/cvisco/grunt-smash), [docs here](https://github.com/mbostock/smash/wiki)), `localForage` not nedded) -- [ ] make the names consistent, reuse code, template etc. -- [ ] implement search box that quickly takes you to a chart (and may hide "pro actions") -- [ ] make an extensible architecture; for example I might want to enable another trendline in the chart which shows estimated end date if one keeps up the pace of last 5 days. -- [ ] desktop app via `node-gyp` -- [ ] when watching, only build changed files and then concat them to make builds much faster -- [ ] smooth animation when transitioning between icons and notifications -- [ ] show animated lines when drawing the chart -- [ ] highlight changes from past fetch -- [ ] In add a project form autocomplete on my username, orgs I am member of and repos I have access to -- [ ] Make sure the padding fits throughout the interface; we have user-select on elements. -- [ ] Have an app wide of triggering a URL and have named routes too -- [ ] rotate between percentage progress and points left -- [ ] be able to config options through UI that currently have to be hardcoded in config -- [ ] choose your own theme -- [ ] show burndown chart for all milestones -- [ ] handle Enterprise editions of GH (signed up in gh dev program) -- [ ] auto-update the chart (with delay when no activity) when logged-in -- [ ] add weekly velocity across all projects and a bar chart to that effect -- [ ] show a little lightning and a number for today's velocity -- [ ] show burnchart only for your tasks; this would be a second category of projects & tasks in the dashboard -- [ ] show an overall text-based status like: all projects on time etc. -- [ ] until GH fix milestone start date then provide an option to specify it (either do that on GH server or locally); for example a text like this: `starts: 09-10-2014` in the description which we provide regex for -- [ ] work on mobile devices -- [ ] show velocity number for each member of the team in the corner of the layout (the point is to get better at planning how many tasks can people take on, thus how fast can we work) -- [ ] if we have the above, we could get a suggestion as to how many points we are able to go through in the next iteration while keeping everyone at their max capacity. One could almost drag & drop tasks to people and see a live progress of how the ideal trendline will fare based on a known speed of people; or we could be somehow notified that people are maxed out -- [ ] show velocity for all team members and how it progresses through time -- [ ] points collector - give medals for 1st 3 spots in terms of velocity -- [ ] show past commits or due dates like in [this calendar](https://dribbble.com/shots/1736128-Meetups-Page?list=shots&sort=popular&timeframe=now&offset=5) -- [ ] support Jira & Gitlab -- [ ] when fetching subsequent updates, fetch only the last page of issues since some repos are large (2.5MB & 19 pages for `mbostock/d3`); actually that is for all issues, not milestone constrained. So only an issue if we want to see a burnchart for all the issues for a repo -- [ ] if all issue circles are close to each other, make a "master circle" that amalgamates all the issues into one large circle, makes for a prettier view -- [ ] make better x-axis date display, otherwise we see all 1s. -- [ ] some [fun loading messages](http://www.gamefaqs.com/pc/561176-simcity-4/faqs/22135) from Sim City. -- [ ] show number of tasks, points, days left just like in Assembly on chart page -- [ ] receive reminders when a due date is nearing and our project is behind schedule; receive a daily digest saying how the progress went in that day/week; these are all ways we can help people answer the question: is my project on track? -- [ ] if we save user's tokens we could check data on their behalf, then messaging would work; API could be provided so that others could plug into the data -- [ ] derive insights; one part is to see if we are on track, the other is to get better at estimating. If we know when an issue is worked on and when closed, with its accompanying size, we can say which issues went well, and which fared poorly. Then we can visualize a weekly/monthly/per-milestone list of loosers and winners. Perhaps the user can glean a pattern from that. -- [ ] create fake Firebase endpoint for GitHub auth, or change the endpoint in settings (easier) -- [ ] have an icon that shows a progress for a milestone that can be shown on GitHub README page -- [ ] try appending '.0' to milestone titles to pass `semver` validation and compare 4.0, 5.x etc. \ No newline at end of file +###Important + +- [ ] 3 `rails/rails/24` has issues in two clusters as if merged from two milestones, does it mean that sort by date is not working? +- [ ] 4 http://burnchart.io#rails I would expect it to list all the projects for that owner so I can select one of them (Ryan); we could show a list of available project names with their: `description`, `private` flag and `has_issues` making the project greyed out if no issues found, cache these projects in local storage +- [ ] 4 if all issue circles in the chart are close to each other, make a "master circle" that amalgamates all the issues into one large circle, makes for a prettier view +- [ ] 4 until GH fix milestone start date then provide an option to specify it; for example a text like this: `starts: 09-10-2014` in the description which we provide regex for in the config +- [ ] 5 be able to config options through ui that currently have to be hardcoded in the config +- [ ] 5 be able to delete added projects; on the project page listing all milestone, enable the cog at the bottom of the table, clicking it slides a link with a dustbin next to it which deletes the project +- [ ] 3 dates not parsing well with timezones, see ideal lines test file + +###Normal + +- [ ] 1 highlight today in the chart better, perhaps just a red line and a text next to it saying what date it is +- [ ] 1 one click to go from a project or milestone view to github; have an icon in the header +- [ ] 1 use tap plugin for `Ractive` so we work on mobile +- [ ] 2 show project name on the milestone page, in the title so that we immediately know where we are +- [ ] 2 focus on form fields style (blue outline etc) and switch off `user-select` on buttons +- [ ] 2 be able to logout, add an icon next to the name with arrow leading out of the square +- [ ] 4 make better x-axis date display, otherwise we see all 1s, basically show better bands, choose per week or per month where appropriate +- [ ] 3 In add a project form autocomplete on my username, orgs I am member of and repos I have access to, use code from [elastic-med](https://github.com/intermine/intermine-apps-c/blob/master/elastic-med/src/components/search.coffee#L24-L46) to show the first option with Tab doing the autocomplete +- [ ] 4 show number of tasks, points, days left, progress bar in the header of a chart page, just like in Assembly +- [ ] 3 be able to specify milestone by name (will nicely show in title), so when we type in `owner/name/name` it should resolve the number +- [ ] 3 trendline is sometimes cutting into axes, but maybe it was just an interpolation +- [ ] 3 deal with Firebase timing out, are we still logged-in? Show a warning page telling the people to refresh the browser (adding a button to do the same) +- [ ] 3 use issue title to determine size +- [ ] 3 the app bundle (albeit uncompressed) clocks in at 1.5MB, reduce the size (`d3` is huge (use [grunt-smash](https://github.com/cvisco/grunt-smash), [docs here](https://github.com/mbostock/smash/wiki))) +- [ ] 5 responsive layout hiding header links into a button +- [ ] 5 show burndown chart for all milestones + +###Nice to Have + +- [ ] 1 tell people if they have no due date +- [ ] 2 try appending '.0' to milestone titles to pass `semver` validation and compare 4.0, 5.x etc. +- [ ] 2 show an overall text-based status like: all projects on time etc. +- [ ] 2 web storage and location hash supported by 93% of browsers; good enough? check for support and throw an error +- [ ] 2 check that we have not run out of requests to make, write a test for this, it should throw an error when we are making a request +- [ ] 2 index page alert tooltip (like on chart page) +- [ ] 3 when fetching subsequent updates, fetch only the last page of issues since some repos are large (2.5MB & 19 pages for `mbostock/d3`); actually that is for all issues, not milestone constrained. So only an issue if we want to see a burnchart for all the issues for a repo +- [ ] 3 choose your own theme in config +- [ ] 3 rotate between percentage progress and points left, fade them in/out +- [ ] 3 make async pages transition so that there is no "jumping" on the page +- [ ] 3 calculate left margin based on the total number of points text width +- [ ] 3 GitHub Pages 404 file +- [ ] 3 show burnchart only for your tasks; this would be a second category of projects & tasks in the dashboard +- [ ] 3 smooth animation when transitioning between icons and notifications, sort of there, but not really +- [ ] 4 implement search box that quickly takes you to a chart (and may hide "pro actions") +- [ ] 4 handle Enterprise editions of GH (signed up in gh dev program) +- [ ] 4 auto-update the chart (with delay when no activity) when logged-in +- [ ] 4 show animated lines when drawing the chart +- [ ] 5 how GitHub show commit activity in weekly slots, can we have something like this in the chart? Basically show commits in that week and their users +- [ ] 5 create fake Firebase endpoint for GitHub auth, or change the endpoint in settings (easier) if people don't trust Google Firebase +- [ ] 5 show past commits or due dates like in [this calendar](https://dribbble.com/shots/1736128-Meetups-Page?list=shots&sort=popular&timeframe=now&offset=5) +- [ ] 7 support Jira, Gitlab, Assembly \ No newline at end of file diff --git a/src/models/config.coffee b/src/models/config.coffee index a81839d..87072d9 100644 --- a/src/models/config.coffee +++ b/src/models/config.coffee @@ -29,8 +29,6 @@ module.exports = new Model "datetime": /^(\d{4}-\d{2}-\d{2})T(.*)/ # How does a size label look like? "size_label": /^size (\d+)$/ - # 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": 'ONE_SIZE' # Request pertaining. diff --git a/src/modules/chart/lines.coffee b/src/modules/chart/lines.coffee index ec41b5e..6c435b4 100644 --- a/src/modules/chart/lines.coffee +++ b/src/modules/chart/lines.coffee @@ -6,9 +6,9 @@ config = require '../../models/config.coffee' module.exports = # A graph of closed issues. - # `issues`: issues list + # `issues`: closed issues list # `created_at`: milestone start date - # `total`: total number of points (open & closed issues) + # `total`: total number of points (open & closed issues) actual: (issues, created_at, total) -> head = [ { 'date': new Date created_at @@ -39,8 +39,8 @@ module.exports = [].concat head, rest # A graph of an ideal progression.. - # `a`: milestone start date - # `b`: milestone end date + # `a`: milestone start date + # `b`: milestone end date # `total`: total number of points (open & closed issues) ideal: (a, b, total) -> # Swap? diff --git a/test/lines.coffee b/test/lines.coffee index 8fd2896..2fe3888 100644 --- a/test/lines.coffee +++ b/test/lines.coffee @@ -4,7 +4,42 @@ lines = require '../src/modules/chart/lines.coffee' module.exports = - # TODO: add moar tests. - 'lines - na': (done) -> - assert.equal 1, 1 + 'lines - actual': (done) -> + issues = [ + { 'size': 3, 'date': 2 } + { 'size': 2, 'date': 3 } + { 'size': 1, 'date': 4 } + ] + + points = ( points for { points } in lines.actual issues, 1, 6 ) + + assert.deepEqual points, [ 6, 3, 1, 0 ] + + do done + + 'lines - ideal': (done) -> + a = '2011-04-01T00:00:00Z' + b = '2011-04-03T00:00:00Z' + + line = lines.ideal(a, b, 4)[ 0...3 ] + + assert.deepEqual line, [ + { 'date': new Date('2011-04-01T06:00:00Z'), 'points': 4 } + { 'date': new Date('2011-04-02T06:00:00Z'), 'points': 2 } + { 'date': new Date('2011-04-03T06:00:00Z'), 'points': 0 } + ] + + do done + + 'lines - trend': (done) -> + issues = [ + { 'date': 1, 'points': 4 } + { 'date': 2, 'points': 1 } + { 'date': 3, 'points': 1 } + ] + + line = (Math.round(points) for { points } in lines.trend(issues, 1, new Date)) + + assert.deepEqual line, [ 2, 1 ] + do done \ No newline at end of file