ready for assembly

This commit is contained in:
Radek Stepan 2014-10-28 08:02:19 -06:00
parent 9703da48b2
commit aaf7fc2a18
45 changed files with 0 additions and 25404 deletions

View File

@ -1,3 +0,0 @@
{
"directory": "vendor"
}

7
.gitignore vendored
View File

@ -1,7 +0,0 @@
node_modules/
.idea/
*.log
src/components/
config.json
vendor/
.build_cache~

View File

@ -1,74 +0,0 @@
module.exports = (grunt) ->
grunt.initConfig
pkg: grunt.file.readJSON("package.json")
apps_c:
commonjs:
src: [ 'src/**/*.{coffee,js,eco}' ]
dest: 'build/app.js'
options:
main: 'src/app.coffee'
name: [ 'ghbc', 'ghb', 'github-burndown-chart' ]
stylus:
compile:
options:
paths: [ 'src/styles/app.styl' ]
files:
'build/app.css': 'src/styles/app.styl'
concat:
scripts:
src: [
# Vendor dependencies.
'vendor/async/lib/async.js'
'vendor/d3/d3.js'
'vendor/d3-tip/index.js'
'vendor/lodash/dist/lodash.js'
'vendor/marked/lib/marked.js'
'vendor/superagent/superagent.js'
# Our app.
'build/app.js'
]
dest: 'build/app.bundle.js'
options:
separator: ';' # for minification purposes
styles:
src: [
# Vendor dependencies.
'vendor/normalize-css/normalize.css'
# Our styles.
'src/styles/fonts.css'
'build/app.css'
]
dest: 'build/app.bundle.css'
uglify:
scripts:
files:
'build/app.min.js': 'build/app.js'
'build/app.bundle.min.js': 'build/app.bundle.js'
cssmin:
combine:
files:
'build/app.min.css': 'build/app.css'
'build/app.bundle.min.css': 'build/app.bundle.css'
grunt.loadNpmTasks('grunt-apps-c')
grunt.loadNpmTasks('grunt-contrib-stylus')
grunt.loadNpmTasks('grunt-contrib-concat')
grunt.loadNpmTasks('grunt-contrib-uglify')
grunt.loadNpmTasks('grunt-contrib-cssmin')
grunt.registerTask('default', [
'apps_c'
'stylus'
'concat'
])
grunt.registerTask('minify', [
'uglify'
'cssmin'
])

View File

@ -1,32 +0,0 @@
install:
npm install
bower install
build:
grunt
minify:
grunt minify
watch:
watch --color -n 1 grunt
publish: build minify
git checkout gh-pages
git show master:build/app.bundle.min.js > app.bundle.min.js
git show master:build/app.bundle.min.css > app.bundle.min.css
git add .
@status=$$(git status --porcelain); \
if ! test "x$${status}" = x; then \
git commit -m 'publish latest build to gh-pages'; \
git push -u origin gh-pages; \
fi
git checkout master
test:
./node_modules/.bin/mocha --compilers coffee:coffee-script --reporter spec --ui exports --timeout 20000 --slow 15000 --bail
serve:
cd public; python -m SimpleHTTPServer 1892
.PHONY: build test

145
README.md
View File

@ -1,145 +0,0 @@
#GitHub Burndown Chart [![Built with Grunt](https://cdn.gruntjs.com/builtwith.png)](http://gruntjs.com/)
Displays a burndown chart from a set of GitHub issues in a milestone.
[ ![Codeship Status for radekstepan/github-burndown-chart](https://www.codeship.io/projects/d69f4420-e5b0-0130-bbae-1632ddfb80f8/status)](https://www.codeship.io/projects/5855)
##Features
1. Client side; easily hosted on GitHub Pages.
1. Private repos; use your GitHub API Token hiding it from public view if need be.
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. Multiple milestones; watch multiple milestones per repo, e.g. when using them for tracking epics.
![image](https://raw.github.com/radekstepan/github-burndown-chart/master/example.png)
##Quickstart
1. Choose a **repo** that you want to display burndown chart for.
1. Make sure this repo has some **issues** assigned to a **milestone**.
1. Put some **labels** on the issues looking like this: `size 1`, `size 3` etc.
1. **Close** some of them labeled issues.
1. Visit [http://radekstepan.com/github-burndown-chart/](http://radekstepan.com/github-burndown-chart/) following the instructions there.
##Configuration
There are three modes of operation balancing between usability & security:
1. **Static Mode**: you can just serve the `public` directory using a static file server or GitHub Pages. No config needed, just serve the app and point to your repo in the browser, e.g.: `http://127.0.0.1:8000/#!/radekstepan/disposable`. You are rate limited to the tune of [60 requests per hour](http://developer.github.com/v3/#rate-limiting).
1. **Static Mode (Public Token)**: as before but now you want to use your [GitHub Personal Access Token](http://developer.github.com/v3/#authentication) in the config. This will require you to specify the token in the `config.json` file as outlined below.
1. **Proxy Mode (Private Token)**: you find it preposterous to share your token with the world. In this case you will need to serve the app using the [Proxy Mode](#proxy-mode). Your token will be scrubbed from the config file and all requests be routed through a proxy.
All of the following fields are defined in `config.json` and none of them, including the file itself, are required. Just make sure that if the file exists, it is served with a correct MIME media type which is [application/json](http://stackoverflow.com/questions/477816/what-is-the-correct-json-content-type).
###Size Label
The way we are getting a size of an issue from GitHub is by putting a label on it. The following regex (string) specifies which part of the label represents the number.
```json
{
"size_label": "^size (\\d+)$"
}
```
This is also the default label if no other is specified.
When multiple matching size labels are present on an issue, their **sum** is taken as the size of the whole issue. This allows you to mix & match (if you want) without creating too many labels in the GitHub interface.
###Token
Your **personal access token** token from GitHub. Get it [here](https://github.com/settings/applications). Bear in mind that if you just statically serve the app, everybody will be able to see the token in transmission. If you would like to avoid that, use the [Proxy Mode](#proxy-mode).
Using the token increases your limit of requests per hour from [60 to 5000](http://developer.github.com/v3/#rate-limiting).
```json
{
"token": "API_TOKEN"
}
```
###Off Days/Weekends
An array of day integers (Monday = 1) representing days of the week when you are not working. This will make the expected burndown line be more accurate.
```json
{
"off_days": [ 6, 7 ]
}
```
###Style
Edit the `src/styles/app.styl` file to change the look & feel of the app. The head of the file contains variables for easy editing.
##Proxy Mode
Use this strategy if you do not wish for your token to be publicly visible. Proxy mode routes all requests from the client side app through it, scrubbing the token from the `config.json` file. It is *slightly* slower than requesting data straight from GitHub of course.
Make sure you have [CoffeeScript](http://coffeescript.org/) installed:
```bash
$ npm install coffee-script -g
```
Then start the proxy passing port number as an argument:
```bash
$ PORT=1234 coffee proxy.coffee
```
Visit the port in question in the browser and continue as before.
##Build It
If you would like to build a custom version of your app, edit the `Gruntfile.coffee` and run the following:
```bash
$ make install
$ make build
```
When you are done developing, you may want to create a minified build:
```bash
$ make minify
```
We are using the [Bower](http://bower.io/) package manager and [Grunt](http://gruntjs.com/) task runner. To [install Bower](http://bower.io/#install-bower) and [install Grunt](http://gruntjs.com/installing-grunt) run the following commands:
```bash
$ sudo npm install bower -g
$ sudo npm install grunt -g
$ sudo npm install grunt-cli -g
```
##Publish It
If you would like to track changes to build files in `gh-pages` branch, execute the following command:
```bash
$ make publish
```
It will checkout the `gh-pages` branch, copy the two build files from master and provided we have changed them locally, make a commit a push them to remote.
##Test It
```bash
$ npm install -d
$ make test
```
Each bugfix receives an accompanying test case.
##Rewrite
The original app got rewritten from a clunky server side to a (better) client side app. Some tests are also provided and more will be going into the future.
If you are upgrading from the previous app, then please bear in mind that `config.yaml` is replaced with `config.json`.
If you would like to use the original app, please refer to the `original` branch.
##Thanks
Thank you for using the app and your feedback/comments are very much welcome. Radek

View File

@ -1,13 +0,0 @@
{
"name": "github-burndown-chart",
"version": "1.0.0-alpha",
"dependencies": {
"lodash": "2.4.1",
"async": "0.9.0",
"d3": "3.4.8",
"d3-tip": "0.6.4",
"superagent": "0.18.0",
"normalize-css": "3.0.1",
"marked": "0.3.2"
}
}

View File

@ -1,483 +0,0 @@
/*! normalize.css v3.0.1 | MIT License | git.io/normalize */
/**
* 1. Set default font family to sans-serif.
* 2. Prevent iOS text size adjust after orientation change, without disabling
* user zoom.
*/
html {
font-family: sans-serif; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/**
* Remove default margin.
*/
body {
margin: 0;
}
/* HTML5 display definitions
========================================================================== */
/**
* Correct `block` display not defined for any HTML5 element in IE 8/9.
* Correct `block` display not defined for `details` or `summary` in IE 10/11 and Firefox.
* Correct `block` display not defined for `main` in IE 11.
*/
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
nav,
section,
summary {
display: block;
}
/**
* 1. Correct `inline-block` display not defined in IE 8/9.
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
*/
audio,
canvas,
progress,
video {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* Prevent modern browsers from displaying `audio` without controls.
* Remove excess height in iOS 5 devices.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Address `[hidden]` styling not present in IE 8/9/10.
* Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
*/
[hidden],
template {
display: none;
}
/* Links
========================================================================== */
/**
* Remove the gray background color from active links in IE 10.
*/
a {
background: transparent;
}
/**
* Improve readability when focused and also mouse hovered in all browsers.
*/
a:active,
a:hover {
outline: 0;
}
/* Text-level semantics
========================================================================== */
/**
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
*/
abbr[title] {
border-bottom: 1px dotted;
}
/**
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
*/
b,
strong {
font-weight: bold;
}
/**
* Address styling not present in Safari and Chrome.
*/
dfn {
font-style: italic;
}
/**
* Address variable `h1` font-size and margin within `section` and `article`
* contexts in Firefox 4+, Safari, and Chrome.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/**
* Address styling not present in IE 8/9.
*/
mark {
background: #ff0;
color: #000;
}
/**
* Address inconsistent and variable font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
/* Embedded content
========================================================================== */
/**
* Remove border when inside `a` element in IE 8/9/10.
*/
img {
border: 0;
}
/**
* Correct overflow not hidden in IE 9/10/11.
*/
svg:not(:root) {
overflow: hidden;
}
/* Grouping content
========================================================================== */
/**
* Address margin not present in IE 8/9 and Safari.
*/
figure {
margin: 1em 40px;
}
/**
* Address differences between Firefox and other browsers.
*/
hr {
-moz-box-sizing: content-box;
box-sizing: content-box;
height: 0;
}
/**
* Contain overflow in all browsers.
*/
pre {
overflow: auto;
}
/**
* Address odd `em`-unit font size rendering in all browsers.
*/
code,
kbd,
pre,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
/* Forms
========================================================================== */
/**
* Known limitation: by default, Chrome and Safari on OS X allow very limited
* styling of `select`, unless a `border` property is set.
*/
/**
* 1. Correct color not being inherited.
* Known issue: affects color of disabled elements.
* 2. Correct font properties not being inherited.
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
*/
button,
input,
optgroup,
select,
textarea {
color: inherit; /* 1 */
font: inherit; /* 2 */
margin: 0; /* 3 */
}
/**
* Address `overflow` set to `hidden` in IE 8/9/10/11.
*/
button {
overflow: visible;
}
/**
* Address inconsistent `text-transform` inheritance for `button` and `select`.
* All other form control elements do not inherit `text-transform` values.
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
* Correct `select` style inheritance in Firefox.
*/
button,
select {
text-transform: none;
}
/**
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
* and `video` controls.
* 2. Correct inability to style clickable `input` types in iOS.
* 3. Improve usability and consistency of cursor style between image-type
* `input` and others.
*/
button,
html input[type="button"], /* 1 */
input[type="reset"],
input[type="submit"] {
-webkit-appearance: button; /* 2 */
cursor: pointer; /* 3 */
}
/**
* Re-set default cursor for disabled elements.
*/
button[disabled],
html input[disabled] {
cursor: default;
}
/**
* Remove inner padding and border in Firefox 4+.
*/
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0;
}
/**
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
* the UA stylesheet.
*/
input {
line-height: normal;
}
/**
* It's recommended that you don't attempt to style these elements.
* Firefox's implementation doesn't respect box-sizing, padding, or width.
*
* 1. Address box sizing set to `content-box` in IE 8/9/10.
* 2. Remove excess padding in IE 8/9/10.
*/
input[type="checkbox"],
input[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
* `font-size` values of the `input`, it causes the cursor style of the
* decrement button to change from `default` to `text`.
*/
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome
* (include `-moz` to future-proof).
*/
input[type="search"] {
-webkit-appearance: textfield; /* 1 */
-moz-box-sizing: content-box;
-webkit-box-sizing: content-box; /* 2 */
box-sizing: content-box;
}
/**
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
* Safari (but not Chrome) clips the cancel button when the search input has
* padding (and `textfield` appearance).
*/
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* Define consistent border, margin, and padding.
*/
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
/**
* 1. Correct `color` not being inherited in IE 8/9/10/11.
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
*/
legend {
border: 0; /* 1 */
padding: 0; /* 2 */
}
/**
* Remove default vertical scrollbar in IE 8/9/10/11.
*/
textarea {
overflow: auto;
}
/**
* Don't inherit the `font-weight` (applied by a rule above).
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
*/
optgroup {
font-weight: bold;
}
/* Tables
========================================================================== */
/**
* Remove most spacing between table cells.
*/
table {
border-collapse: collapse;
border-spacing: 0;
}
td,
th {
padding: 0;
}
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 400;
src: local('Source Sans Pro'), local('SourceSansPro-Regular'), url(http://themes.googleusercontent.com/static/fonts/sourcesanspro/v6/ODelI1aHBYDBqgeIAH2zlBM0YzuT7MdOe03otPbuUS0.woff) format('woff');
}
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 600;
src: local('Source Sans Pro Semibold'), local('SourceSansPro-Semibold'), url(http://themes.googleusercontent.com/static/fonts/sourcesanspro/v6/toadOcfmlt9b38dHJxOBGJ6-ys_j0H4QL65VLqzI3wI.woff) format('woff');
}
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 700;
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url(http://themes.googleusercontent.com/static/fonts/sourcesanspro/v6/toadOcfmlt9b38dHJxOBGFkQc6VGVFSmCnC_l7QZG60.woff) format('woff');
}
body{height:100%;background:#d7bcab;background:-moz--webkit-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-moz--moz-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-moz--o-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-moz--ms-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-moz-linear-gradient(-45deg, #d7bcab 0%, #cc9485 100%);background:-webkit-gradient(linear, left top, right bottom, color-stop(0%, $background), color-stop(100%, #cc9485));background:-webkit-gradient(linear, left top, right bottom, color-stop(0%, $background), color-stop(100%, #cc9485));background:-webkit-gradient(linear, left top, right bottom, color-stop(0%, $background), color-stop(100%, #cc9485));background:-webkit-gradient(linear, left top, right bottom, color-stop(0%, $background), color-stop(100%, #cc9485));background:-webkit-gradient(linear, left top, right bottom, color-stop(0%, $background), color-stop(100%, #cc9485));background:-webkit--webkit-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-webkit--moz-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-webkit--o-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-webkit--ms-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-webkit-linear-gradient(-45deg, #d7bcab 0%, #cc9485 100%);background:-o--webkit-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-o--moz-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-o--o-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-o--ms-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-o-linear-gradient(-45deg, #d7bcab 0%, #cc9485 100%);background:-ms--webkit-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-ms--moz-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-ms--o-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-ms--ms-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-ms-linear-gradient(-45deg, #d7bcab 0%, #cc9485 100%);background:-webkit-linear-gradient(315deg, #d7bcab 0%, #cc9485 100%);background:-moz-linear-gradient(315deg, #d7bcab 0%, #cc9485 100%);background:-o-linear-gradient(315deg, #d7bcab 0%, #cc9485 100%);background:-ms-linear-gradient(315deg, #d7bcab 0%, #cc9485 100%);background:linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background-repeat:no-repeat;background-attachment:fixed;font-family:'Source Sans Pro',sans-serif;padding:100px;color:#64584c}
ul{list-style-type:none;padding:0;}
ul li{padding:0}
h2{font-size:16px;text-transform:uppercase}
.box{background:#fff;-webkit-box-shadow:2px 4px 6px rgba(0,0,0,0.2);box-shadow:2px 4px 6px rgba(0,0,0,0.2);}
.box.generic,.box.info,.box.error,.box.success{padding:20px 0;border-top:4px solid #eac85d;width:50%;margin:0 auto}
.box.info{border-top-color:#5f90b0}
.box.error{border-top-color:#e45e39}
.box.success{border-top-color:#4db07a}
.box a{color:#64584c}
.box h1{margin:0;padding:20px;color:#64584c;font-size:20px;text-transform:uppercase}
.box h2{margin:0;padding:0 20px 20px}
.box p{margin:5px 0;padding:0 20px;}
.box p.description{margin:-10px 0 0 0}
#graph{height:200px;position:relative;}
#graph #tooltip{position:absolute;top:0;left:0}
#graph svg path.line{fill:none;stroke-width:1px;clip-path:url("#clip");}
#graph svg path.line.actual{stroke:#64584c;stroke-width:3px}
#graph svg path.line.ideal{stroke:#cacaca;stroke-width:3px}
#graph svg path.line.trendline{stroke:#64584c;stroke-width:1.5px;stroke-dasharray:5,5}
#graph svg line.today{stroke:#cacaca;stroke-width:1px;shape-rendering:crispEdges;stroke-dasharray:5,5}
#graph svg circle{fill:#64584c;stroke:transparent;stroke-width:15px;cursor:pointer}
#graph svg .axis{shape-rendering:crispEdges;}
#graph svg .axis line{stroke:rgba(202,202,202,0.25);shape-rendering:crispEdges}
#graph svg .axis text{font-weight:bold;fill:#cacaca}
#graph svg .axis path{display:none}
.d3-tip{margin-top:-10px;font-size:11px;padding:8px 10px 7px 10px;text-align:center;background:rgba(0,0,0,0.75);color:#fff;-webkit-border-radius:3px;border-radius:3px;}
.d3-tip:after{width:100%;color:rgba(0,0,0,0.8);content:"\25BC";position:absolute}
.d3-tip.n:after{margin:-3px 0 0 0;top:100%;left:0}
#progress{padding:20px;-webkit-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;}
#progress:after{clear:both;display:block;content:""}
#progress .bars{position:relative;}
#progress .bars div{-webkit-border-radius:6px;border-radius:6px;height:12px;}
#progress .bars div.closed{position:absolute;top:0;left:0;background:#4daf7c;}
#progress .bars div.closed:not(.done){-webkit-border-radius:6px 0 0 6px;border-radius:6px 0 0 6px}
#progress .bars div.opened{width:100%;background:#e55f3a}
#progress h2{margin:10px 0 0 0;padding:0;}
#progress h2.closed{float:left;color:#4daf7c}
#progress h2.opened{float:right;color:#e55f3a}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,39 +0,0 @@
body{height:100%;background:#d7bcab;background:-moz--webkit-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-moz--moz-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-moz--o-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-moz--ms-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-moz-linear-gradient(-45deg, #d7bcab 0%, #cc9485 100%);background:-webkit-gradient(linear, left top, right bottom, color-stop(0%, $background), color-stop(100%, #cc9485));background:-webkit-gradient(linear, left top, right bottom, color-stop(0%, $background), color-stop(100%, #cc9485));background:-webkit-gradient(linear, left top, right bottom, color-stop(0%, $background), color-stop(100%, #cc9485));background:-webkit-gradient(linear, left top, right bottom, color-stop(0%, $background), color-stop(100%, #cc9485));background:-webkit-gradient(linear, left top, right bottom, color-stop(0%, $background), color-stop(100%, #cc9485));background:-webkit--webkit-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-webkit--moz-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-webkit--o-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-webkit--ms-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-webkit-linear-gradient(-45deg, #d7bcab 0%, #cc9485 100%);background:-o--webkit-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-o--moz-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-o--o-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-o--ms-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-o-linear-gradient(-45deg, #d7bcab 0%, #cc9485 100%);background:-ms--webkit-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-ms--moz-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-ms--o-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-ms--ms-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-ms-linear-gradient(-45deg, #d7bcab 0%, #cc9485 100%);background:-webkit-linear-gradient(315deg, #d7bcab 0%, #cc9485 100%);background:-moz-linear-gradient(315deg, #d7bcab 0%, #cc9485 100%);background:-o-linear-gradient(315deg, #d7bcab 0%, #cc9485 100%);background:-ms-linear-gradient(315deg, #d7bcab 0%, #cc9485 100%);background:linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background-repeat:no-repeat;background-attachment:fixed;font-family:'Source Sans Pro',sans-serif;padding:100px;color:#64584c}
ul{list-style-type:none;padding:0;}
ul li{padding:0}
h2{font-size:16px;text-transform:uppercase}
.box{background:#fff;-webkit-box-shadow:2px 4px 6px rgba(0,0,0,0.2);box-shadow:2px 4px 6px rgba(0,0,0,0.2);}
.box.generic,.box.info,.box.error,.box.success{padding:20px 0;border-top:4px solid #eac85d;width:50%;margin:0 auto}
.box.info{border-top-color:#5f90b0}
.box.error{border-top-color:#e45e39}
.box.success{border-top-color:#4db07a}
.box a{color:#64584c}
.box h1{margin:0;padding:20px;color:#64584c;font-size:20px;text-transform:uppercase}
.box h2{margin:0;padding:0 20px 20px}
.box p{margin:5px 0;padding:0 20px;}
.box p.description{margin:-10px 0 0 0}
#graph{height:200px;position:relative;}
#graph #tooltip{position:absolute;top:0;left:0}
#graph svg path.line{fill:none;stroke-width:1px;clip-path:url("#clip");}
#graph svg path.line.actual{stroke:#64584c;stroke-width:3px}
#graph svg path.line.ideal{stroke:#cacaca;stroke-width:3px}
#graph svg path.line.trendline{stroke:#64584c;stroke-width:1.5px;stroke-dasharray:5,5}
#graph svg line.today{stroke:#cacaca;stroke-width:1px;shape-rendering:crispEdges;stroke-dasharray:5,5}
#graph svg circle{fill:#64584c;stroke:transparent;stroke-width:15px;cursor:pointer}
#graph svg .axis{shape-rendering:crispEdges;}
#graph svg .axis line{stroke:rgba(202,202,202,0.25);shape-rendering:crispEdges}
#graph svg .axis text{font-weight:bold;fill:#cacaca}
#graph svg .axis path{display:none}
.d3-tip{margin-top:-10px;font-size:11px;padding:8px 10px 7px 10px;text-align:center;background:rgba(0,0,0,0.75);color:#fff;-webkit-border-radius:3px;border-radius:3px;}
.d3-tip:after{width:100%;color:rgba(0,0,0,0.8);content:"\25BC";position:absolute}
.d3-tip.n:after{margin:-3px 0 0 0;top:100%;left:0}
#progress{padding:20px;-webkit-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;}
#progress:after{clear:both;display:block;content:""}
#progress .bars{position:relative;}
#progress .bars div{-webkit-border-radius:6px;border-radius:6px;height:12px;}
#progress .bars div.closed{position:absolute;top:0;left:0;background:#4daf7c;}
#progress .bars div.closed:not(.done){-webkit-border-radius:6px 0 0 6px;border-radius:6px 0 0 6px}
#progress .bars div.opened{width:100%;background:#e55f3a}
#progress h2{margin:10px 0 0 0;padding:0;}
#progress h2.closed{float:left;color:#4daf7c}
#progress h2.opened{float:right;color:#e55f3a}

File diff suppressed because it is too large Load Diff

1
build/app.min.css vendored
View File

@ -1 +0,0 @@
body{height:100%;background:#d7bcab;background:-moz--webkit-linear-gradient(135deg,#d7bcab 0,#cc9485 100%);background:-moz--moz-linear-gradient(135deg,#d7bcab 0,#cc9485 100%);background:-moz--o-linear-gradient(135deg,#d7bcab 0,#cc9485 100%);background:-moz--ms-linear-gradient(135deg,#d7bcab 0,#cc9485 100%);background:-moz-linear-gradient(-45deg,#d7bcab 0,#cc9485 100%);background:-webkit-gradient(linear,left top,right bottom,color-stop(0%,$background),color-stop(100%,#cc9485));background:-webkit-gradient(linear,left top,right bottom,color-stop(0%,$background),color-stop(100%,#cc9485));background:-webkit-gradient(linear,left top,right bottom,color-stop(0%,$background),color-stop(100%,#cc9485));background:-webkit-gradient(linear,left top,right bottom,color-stop(0%,$background),color-stop(100%,#cc9485));background:-webkit-gradient(linear,left top,right bottom,color-stop(0%,$background),color-stop(100%,#cc9485));background:-webkit--webkit-linear-gradient(135deg,#d7bcab 0,#cc9485 100%);background:-webkit--moz-linear-gradient(135deg,#d7bcab 0,#cc9485 100%);background:-webkit--o-linear-gradient(135deg,#d7bcab 0,#cc9485 100%);background:-webkit--ms-linear-gradient(135deg,#d7bcab 0,#cc9485 100%);background:-webkit-linear-gradient(-45deg,#d7bcab 0,#cc9485 100%);background:-o--webkit-linear-gradient(135deg,#d7bcab 0,#cc9485 100%);background:-o--moz-linear-gradient(135deg,#d7bcab 0,#cc9485 100%);background:-o--o-linear-gradient(135deg,#d7bcab 0,#cc9485 100%);background:-o--ms-linear-gradient(135deg,#d7bcab 0,#cc9485 100%);background:-o-linear-gradient(-45deg,#d7bcab 0,#cc9485 100%);background:-ms--webkit-linear-gradient(135deg,#d7bcab 0,#cc9485 100%);background:-ms--moz-linear-gradient(135deg,#d7bcab 0,#cc9485 100%);background:-ms--o-linear-gradient(135deg,#d7bcab 0,#cc9485 100%);background:-ms--ms-linear-gradient(135deg,#d7bcab 0,#cc9485 100%);background:-ms-linear-gradient(-45deg,#d7bcab 0,#cc9485 100%);background:-webkit-linear-gradient(315deg,#d7bcab 0,#cc9485 100%);background:-moz-linear-gradient(315deg,#d7bcab 0,#cc9485 100%);background:-o-linear-gradient(315deg,#d7bcab 0,#cc9485 100%);background:-ms-linear-gradient(315deg,#d7bcab 0,#cc9485 100%);background:linear-gradient(135deg,#d7bcab 0,#cc9485 100%);background-repeat:no-repeat;background-attachment:fixed;font-family:'Source Sans Pro',sans-serif;padding:100px;color:#64584c}ul{list-style-type:none;padding:0}ul li{padding:0}h2{font-size:16px;text-transform:uppercase}.box{background:#fff;-webkit-box-shadow:2px 4px 6px rgba(0,0,0,.2);box-shadow:2px 4px 6px rgba(0,0,0,.2)}.box.generic,.box.info,.box.error,.box.success{padding:20px 0;border-top:4px solid #eac85d;width:50%;margin:0 auto}.box.info{border-top-color:#5f90b0}.box.error{border-top-color:#e45e39}.box.success{border-top-color:#4db07a}.box a{color:#64584c}.box h1{margin:0;padding:20px;color:#64584c;font-size:20px;text-transform:uppercase}.box h2{margin:0;padding:0 20px 20px}.box p{margin:5px 0;padding:0 20px}.box p.description{margin:-10px 0 0 0}#graph{height:200px;position:relative}#graph #tooltip{position:absolute;top:0;left:0}#graph svg path.line{fill:none;stroke-width:1px;clip-path:url(#clip)}#graph svg path.line.actual{stroke:#64584c;stroke-width:3px}#graph svg path.line.ideal{stroke:#cacaca;stroke-width:3px}#graph svg path.line.trendline{stroke:#64584c;stroke-width:1.5px;stroke-dasharray:5,5}#graph svg line.today{stroke:#cacaca;stroke-width:1px;shape-rendering:crispEdges;stroke-dasharray:5,5}#graph svg circle{fill:#64584c;stroke:transparent;stroke-width:15px;cursor:pointer}#graph svg .axis{shape-rendering:crispEdges}#graph svg .axis line{stroke:rgba(202,202,202,.25);shape-rendering:crispEdges}#graph svg .axis text{font-weight:700;fill:#cacaca}#graph svg .axis path{display:none}.d3-tip{margin-top:-10px;font-size:11px;padding:8px 10px 7px;text-align:center;background:rgba(0,0,0,.75);color:#fff;-webkit-border-radius:3px;border-radius:3px}.d3-tip:after{width:100%;color:rgba(0,0,0,.8);content:"\25BC";position:absolute}.d3-tip.n:after{margin:-3px 0 0 0;top:100%;left:0}#progress{padding:20px;-webkit-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px}#progress:after{clear:both;display:block;content:""}#progress .bars{position:relative}#progress .bars div{-webkit-border-radius:6px;border-radius:6px;height:12px}#progress .bars div.closed{position:absolute;top:0;left:0;background:#4daf7c}#progress .bars div.closed:not(.done){-webkit-border-radius:6px 0 0 6px;border-radius:6px 0 0 6px}#progress .bars div.opened{width:100%;background:#e55f3a}#progress h2{margin:10px 0 0;padding:0}#progress h2.closed{float:left;color:#4daf7c}#progress h2.opened{float:right;color:#e55f3a}

1
build/app.min.js vendored

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

View File

@ -1,46 +0,0 @@
{
"name": "github-burndown-chart",
"version": "1.0.0",
"description": "Shows a burndown chart for GitHub Issues",
"directories": {
"test": "test"
},
"dependencies": {
"coffee-script": "~1.6.3",
"async": "~0.2.9",
"proxyquire": "~0.5.1",
"lodash": "~1.3.1",
"connect": "~2.12.0",
"request": "~2.27.0"
},
"devDependencies": {
"mocha": "~1.12.0",
"marked": "~0.3.2",
"grunt": "~0.4.1",
"grunt-apps-c": "0.1.10",
"grunt-contrib-stylus": "~0.9.0",
"grunt-contrib-concat": "~0.3.0",
"grunt-contrib-uglify": "~0.2.5",
"grunt-contrib-cssmin": "~0.6.2"
},
"scripts": {
"test": "make test"
},
"repository": {
"type": "git",
"url": "git://github.com/radekstepan/github-burndown-chart.git"
},
"keywords": [
"github",
"issues",
"burndown",
"chart",
"scrum"
],
"author": "Radek <dev@radekstepan.com>",
"license": "BSD",
"bugs": {
"url": "https://github.com/radekstepan/github-burndown-chart/issues"
}
}

View File

@ -1,59 +0,0 @@
#!/usr/bin/env coffee
_ = require 'lodash'
http = require 'http'
fs = require 'fs'
connect = require 'connect'
request = require 'request'
# Read the original config.
config = JSON.parse fs.readFileSync './config.json', 'utf-8'
# Some defaults.
config.host ?= 'api.github.com'
# This is the scrubbed version.
_.extend scrubbed = {}, config, { 'protocol': 'http', 'token': null }
proxy = (req, res, next) ->
end = (code, body) ->
res.writeHead code, {'Content-Type': 'application/json; charset=utf-8'}
res.end body
# Log it.
console.log new Date(), req.url
# Config?
if req.url is '/config.json'
# Refer to us like so.
# Prefer custom header x-forwarded-host if defined.
scrubbed.host = req.headers['x-forwarded-host'] or req.headers.host
return end 200, JSON.stringify scrubbed, null, 4
# GitHub API request?
if req.url.match /^\/repos/
# The default headers.
headers =
# See http://developer.github.com/v3/media/#beta-v3-and-the-future
'Accept': 'application/vnd.github.v3'
# See http://developer.github.com/v3/#user-agent-required
'User-Agent': 'GitHub-Burndown-Chart'
# Add a token?
headers.Authorization = "token #{config.token}" if config.token?
# Make the HTTPS request.
return request {
'uri': "https://#{config.host}#{req.url}"
headers
# Handle the response.
}, (err, _res, body) ->
return end(500) if err
end _res.statusCode, body
# Get handled by Connect.
do next
app = connect()
.use(proxy)
# Serve the public directory with the app, no need to launch another service.
.use(connect.static(__dirname + '/public'))
# Connect on an env port or go random.
.listen process.env.PORT, ->
console.log 'Proxy listening on port', app.address().port

1
public/.gitignore vendored
View File

@ -1 +0,0 @@
!config.json

View File

@ -1 +0,0 @@
../build/app.bundle.js

View File

@ -1 +0,0 @@
../build/app.bundle.min.css

View File

@ -1 +0,0 @@
../build/app.bundle.min.js

View File

@ -1 +0,0 @@
../config.json

View File

@ -1,19 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>GitHub Burndown Chart</title>
<link href="app.bundle.min.css" media="all" rel="stylesheet" type="text/css" />
<script src="app.bundle.min.js"></script>
<script>
document.onreadystatechange = function() {
if (document.readyState == "complete") {
require('ghbc').call(null);
}
};
</script>
</head>
<body>
</body>
</html>

View File

@ -1,40 +0,0 @@
#!/usr/bin/env coffee
config = require './modules/config'
regex = require './modules/regex'
render = require './modules/render'
repo = require './modules/repo'
# Check for a route.
route = ->
# Do we have a location match?
if match = window.location.hash.match regex.location
# User/repo/(milestone) path
path = match[1][1...]
# Say we are loading this repo then.
render 'body', 'loading', { path }
# Did we specify a milestone?
[ u, r, m ] = path.split('/')
opts = if m then { 'path': "#{u}/#{r}", 'milestone': m } else { path }
# Get config/cache.
return async.waterfall [ config
# Render this repo.
, (conf, cb) ->
repo _.extend(opts, conf), cb
], (err) ->
render 'body', 'error', { 'text': do err.toString } if err
# Info notice for you.
render 'body', 'info'
module.exports = ->
# Do we have browser support?
if 'onhashchange' of window and 'hash' of window.location
# Detect route changes.
window.addEventListener 'hashchange', route, no
# And route now.
return do route
render 'body', 'error', { 'text': 'URL fragment identifier not supported' }

View File

@ -1,66 +0,0 @@
#!/usr/bin/env coffee
{ _ } = require './require'
request = require './request'
regex = require './regex'
# Have it?
config = null
# We are cold.
wait = no
# Callbacks go here.
queue = []
# Defaults.
defaults =
# You do know we work with GitHub right?
'host': 'api.github.com'
# Making NSA (err taxpayer) work for it.
'protocol': 'https'
# Validators of config fields.
validators =
'host': (value) ->
_.isString value
'protocol': (value) ->
_.isString(value) and value.match /^http(s?)$/
'token': (value) ->
_.isString value
'off_days': (value) ->
return no unless _.isArray value
( return no for day in value when day not in [ 1..7 ] )
yes
# Get (& cache) configuration from the server.
module.exports = (cb) ->
# Skip cache in node.
config = null if typeof window is 'undefined'
# Have config?
return cb null, config if config
# Enqueue.
queue.push cb
# Load it?
unless wait
# Everyone else wait now.
wait = yes
# Make the request.
request.config (err, result) ->
# The wait is over.
wait = no
# We do not strictly require config files.
config = _.defaults result or {}, defaults
# RegExpify the size label?
if config.size_label
config.size_label = new RegExp config.size_label
else
config.size_label = regex.size_label
# Validate it.
for field, validator of validators when config[field]
unless validator config[field]
return cb "Config field `#{field}` misconfigured"
# Call back for each enqueued.
( queue.pop() null, config while queue.length )

View File

@ -1,241 +0,0 @@
#!/usr/bin/env coffee
{ _, d3 } = require './require'
reg = require './regex'
module.exports =
# A graph of 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.
_.extend {}, issue,
date: new Date(closed_at)
points: total -= size
# 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..
'ideal': (a, b, off_days, total, cb) ->
# Swap?
[ b, a ] = [ a, b ] if b < a
# We start here adding days to `d`.
[ y, m, d ] = _.map a.match(reg.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 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) ->
document.querySelector('#svg').innerHTML = ''
# Get available space.
{ height, width } = document.querySelector('#graph').getBoundingClientRect()
margin = { top: 30, right: 30, bottom: 40, left: 50 }
width -= margin.left + margin.right
height -= margin.top + margin.bottom
# Scales.
x = d3.time.scale().range([ 0, width ])
y = d3.scale.linear().range([ height, 0 ])
# Axes.
xAxis = d3.svg.axis().scale(x)
.orient("bottom")
# Show vertical lines...
.tickSize(-height)
# ...with day of the month...
.tickFormat( (d) -> d.getDate() )
# ...and give us a spacer.
.tickPadding(10)
yAxis = d3.svg.axis().scale(y)
.orient("left")
.tickSize(-width)
.ticks(5)
.tickPadding(10)
# Line generator.
line = d3.svg.line()
.interpolate("linear")
.x( (d) -> x(d.date) )
.y( (d) -> y(d.points) )
# Get the minimum and maximum date, and initial points.
x.domain([ ideal[0].date, ideal[ideal.length - 1].date ])
y.domain([ 0, ideal[0].points ]).nice()
# Add an SVG element with the desired dimensions and margin.
svg = d3.select("#svg").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
# Add the days x-axis.
svg.append("g")
.attr("class", "x axis day")
.attr("transform", "translate(0,#{height})")
.call(xAxis)
# Add the months x-axis.
m = [
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
]
mAxis = xAxis
.orient("top")
.tickSize(height)
.tickFormat( (d) -> m[d.getMonth()] )
.ticks(2)
svg.append("g")
.attr("class", "x axis month")
.attr("transform", "translate(0,#{height})")
.call(mAxis)
# Add the y-axis.
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
# Add a line showing where we are now.
svg.append("svg:line")
.attr("class", "today")
.attr("x1", x(new Date()))
.attr("y1", 0)
.attr("x2", x(new Date()))
.attr("y2", height)
# Add the ideal line path.
svg.append("path")
.attr("class", "ideal line")
.attr("d", line.interpolate("basis")(ideal))
# Add the trendline path.
svg.append("path")
.attr("class", "trendline line")
.attr("d", line.interpolate("linear")(trendline))
# Add the actual line path.
svg.append("path")
.attr("class", "actual line")
.attr("d", line.interpolate("linear").y( (d) -> y(d.points) )(actual))
# Collect the tooltip here.
tooltip = d3.tip().attr('class', 'd3-tip').html ({ number, title }) ->
"##{number}: #{title}"
svg.call(tooltip)
# Show when we closed an issue.
svg.selectAll("a.issue")
.data(actual[1...]) # skip the starting point
.enter()
# A wrapping link.
.append('svg:a')
.attr("xlink:href", ({ html_url }) -> html_url )
.attr("xlink:show", 'new')
.append('svg:circle')
.attr("cx", ({ date }) -> x date )
.attr("cy", ({ points }) -> y points )
.attr("r", ({ radius }) -> 5 ) # fixed for now
.on('mouseover', tooltip.show)
.on('mouseout', tooltip.hide)
cb null

View File

@ -1,62 +0,0 @@
#!/usr/bin/env coffee
{ _, async } = require './require'
req = require './request'
reg = require './regex'
module.exports =
# Used on an initial fetch of issues for a repo.
'get_all': (repo, cb) ->
# For each state...
one_status = (state, cb) ->
# Concat them here.
results = []
# One pageful fetch (next pages in series).
do fetch_page = (page = 1) ->
req.all_issues repo, {
milestone: repo.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, regex, cb) ->
# The total size of all issues.
total = 0
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(regex)
# Increase sum.
sum += parseInt matches[1]
, 0
# Increase the total.
total += issue.size
# Are we saving it?
!!issue.size
cb null, filtered, total

View File

@ -1,45 +0,0 @@
#!/usr/bin/env coffee
{ _, marked } = require './require'
request = require './request'
# Get current/specified milestone for a repo.
module.exports = (repo, cb) ->
# Has description? Parse GFM.
parse = (data) ->
data.description = marked(data.description)[3...-5] if data.description
data
# Get a specific milestone.
if repo.milestone
request.one_milestone 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}`"
# Parse GFM.
m = parse m
cb null, null, m
# Get the current milestone out of many.
else
request.all_milestones repo, (err, data) ->
# Errors?
return cb err if err
# Empty warning?
return cb null, "No open milestones for repo #{repo.path}" unless data.length
# The first milestone should be ending soonest.
m = data[0]
# Filter milestones without due date.
m = _.rest data, { 'due_on' : null }
# The first milestone should be ending soonest. Prefer milestones with due dates.
m = if m[0] then m[0] else data[0]
# Empty milestone?
if m.open_issues + m.closed_issues is 0
return cb null, "No issues for milestone `#{m.title}`"
# Parse GFM.
m = parse m
cb null, null, m

View File

@ -1,8 +0,0 @@
#!/usr/bin/env coffee
module.exports =
# How do we parse GitHub dates?
'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})$/

View File

@ -1,6 +0,0 @@
#!/usr/bin/env coffee
# Render an eco template into a selector (innerHTML).
module.exports = (selector, template, context = {}) ->
tml = require "../templates/#{template}"
document.querySelector(selector).innerHTML = tml context

View File

@ -1,79 +0,0 @@
#!/usr/bin/env coffee
{ _, async } = require './require'
milestones = require './milestones'
issues = require './issues'
graph = require './graph'
regex = require './regex'
render = require './render'
# Setup a repo and render it.
module.exports = (opts, cb) ->
# Get the current/specified milestone.
async.waterfall [ (cb) ->
milestones opts, (err, warn, milestone) ->
return cb err if err
return cb warn if warn
opts.milestone = milestone
cb null
# Get all issues.
(cb) ->
issues.get_all opts, cb
# Filter them to labeled ones.
(all, cb) ->
async.map all, (array, cb) ->
issues.filter array, opts.size_label, (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] }
cb null
# Create actual and ideal lines & render.
(cb) ->
progress = 100 * opts.issues.closed.points /
(total = opts.issues.open.points + opts.issues.closed.points)
async.parallel [
_.partial(
graph.actual,
opts.issues.closed.data,
opts.milestone.created_at,
total
)
_.partial(
graph.ideal,
opts.milestone.created_at,
opts.milestone.due_on,
opts.off_days or [],
total
)
], (err, values) ->
# Render the body.
render 'body', 'graph', { 'repo': opts.path, 'milestone': opts.milestone }
# Render the progress.
render '#progress', 'progress', { progress }
# Generate a trendline?
values.push(graph.trendline(
values[0],
opts.milestone.created_at,
opts.milestone.due_on
)) if values[0].length
# Render the chart.
do doit = -> graph.render values, cb
# Watch window resize from now on?
window.onresize = doit if 'onresize' of window
], cb

View File

@ -1,119 +0,0 @@
#!/usr/bin/env coffee
{ superagent, _ } = require './require'
# Custom JSON parser.
superagent.parse =
'application/json': (res) ->
try
JSON.parse res
catch e
{} # it was not to be...
module.exports =
# Get all milestones.
'all_milestones': (repo, cb) ->
request
'protocol': repo.protocol
'host': repo.host
'path': "/repos/#{repo.path}/milestones"
'query': { 'state': 'open', 'sort': 'due_date', 'direction': 'asc' }
'headers': headers repo.token
, cb
# Get one milestone.
'one_milestone': (repo, number, cb) ->
request
'protocol': repo.protocol
'host': repo.host
'path': "/repos/#{repo.path}/milestones/#{number}"
'query': { 'state': 'open', 'sort': 'due_date', 'direction': 'asc' }
'headers': headers repo.token
, cb
# Get all issues for a state.
'all_issues': (repo, query, cb) ->
request
'protocol': repo.protocol
'host': repo.host
'path': "/repos/#{repo.path}/issues"
'query': _.extend query, { 'per_page': '100' }
'headers': headers repo.token
, cb
# Get config from our host always.
'config': (cb) ->
request
'protocol': 'http'
'host': window.location.host
'path': "#{window.location.pathname}config.json"
'headers': _.extend headers(), { 'Accept': 'application/json' }
, cb
# Make a request using SuperAgent.
request = ({ protocol, host, path, query, headers }, cb) ->
exited = no
# Make the query params.
q = if query then '?' + ( "#{k}=#{v}" for k, v of query ).join('&') else ''
# The URI.
req = superagent.get("#{protocol}://#{host}#{path}#{q}")
# Add headers.
( req.set(k, v) for k, v of headers )
# Timeout for requests that do not finish... see #32.
timeout = setTimeout ->
exited = yes
cb 'Request has timed out'
, 1e4 # give us 10s
# Send.
req.end (err, data) ->
# Arrived too late.
return if exited
# All fine.
exited = yes
clearTimeout timeout
# Actually process the response.
response err, data, cb
# How do we respond to a response?
response = (err, data, cb) ->
return cb error err if err
# 2xx?
if data.statusType isnt 2
# Do we have a message from GitHub?
return cb data.body.message if data?.body?.message?
# Use SA one.
return cb data.error.message
# All good.
cb null, data.body
# Give us headers.
headers = (token) ->
# The defaults.
h = _.extend {},
'Content-Type': 'application/json'
'Accept': 'application/vnd.github.v3'
# Add token?
h.Authorization = "token #{token}" if token?
h
# Parse an error.
error = (err) ->
switch
when _.isString err
message = err
when _.isArray err
message = err[1]
when _.isObject(err) and _.isString(err.message)
message = err.message
unless message
try
message = JSON.stringify err
catch
message = do err.toString
message

View File

@ -1,5 +0,0 @@
#!/usr/bin/env coffee
# So we can easily stub these.
module.exports = {
_, superagent, d3, async, marked
}

View File

@ -1,209 +0,0 @@
@import 'nib'
// color definitions
$closed = #4DAF7C
$opened = #E55F3A
$grey = #CACACA
$brown = #64584C
$background1 = #D7BCAB
$background2 = #CC9485
// font and gradient bg
body
height: 100%
background: $background1
background: -moz-linear-gradient(-45deg, $background1 0%, $background2 100%)
background: -webkit-gradient(linear, left top, right bottom, color-stop(0%,$background), color-stop(100%,$background2))
background: -webkit-linear-gradient(-45deg, $background1 0%,$background2 100%)
background: -o-linear-gradient(-45deg, $background1 0%,$background2 100%)
background: -ms-linear-gradient(-45deg, $background1 0%,$background2 100%)
background: linear-gradient(135deg, $background1 0%,$background2 100%)
background-repeat: no-repeat
background-attachment: fixed
font-family: 'Source Sans Pro', sans-serif
padding: 100px
color: $brown
ul
list-style-type: none
padding: 0
li
padding: 0
h2
font-size: 16px
text-transform: uppercase
// the white content box
.box
background: #FFF
box-shadow: 2px 4px 6px rgba(0,0,0,0.2)
// different classes thereof
&.generic, &.info, &.error, &.success
padding: 20px 0
border-top: 4px solid #EAC85D
width: 50%
margin: 0 auto
&.info
border-top-color: #5F90B0
&.error
border-top-color: #E45E39
&.success
border-top-color: #4DB07A
a
color: $brown
h1
margin: 0
padding: 20px
color: $brown
font-size: 20px
text-transform: uppercase
h2
margin: 0
padding: 0 20px 20px
p
margin: 5px 0
padding: 0 20px
&.description
margin: -10px 0 0 0
// where D3 renders to
#graph
height: 200px
position: relative
// position will be adjusted dynamically
#tooltip
position: absolute
top: 0
left: 0
svg
path
&.line
fill: none
stroke-width: 1px
clip-path: url(#clip)
// actual progress
&.actual
stroke: $brown
stroke-width: 3px
// ideal velocity throughout the sprint
&.ideal
stroke: $grey
stroke-width: 3px
// trend of actual issue closures
&.trendline
stroke: $brown
stroke-width: 1.5px
stroke-dasharray: 5,5
// right now
line
&.today
stroke: $grey
stroke-width: 1px
shape-rendering: crispEdges
stroke-dasharray: 5,5
// represents one issue closed
circle
fill: $brown
// make it easier to click
stroke: transparent
stroke-width: 15px
cursor: pointer
// axes...
.axis
shape-rendering: crispEdges
line
stroke: rgba($grey, 0.25)
shape-rendering: crispEdges
text
font-weight: bold
fill: $grey
path
display: none
// tooltips
.d3-tip
margin-top: -10px
font-size: 11px
padding: 8px 10px 7px 10px
text-align: center
background: rgba(0,0,0,0.75)
color: #fff
border-radius: 3px
&:after
width: 100%
color: rgba(0,0,0,0.8)
content: "\25BC"
position: absolute
&.n:after
margin: -3px 0 0 0
top: 100%
left: 0
// progression graph
#progress
padding: 20px
border-radius: 0 0 6px 6px
// clear
&:after
clear: both
display: block
content: ""
.bars
position: relative
// the two bars
div
border-radius: 6px
height: 12px
&.closed
position: absolute
top: 0
left: 0
background: $closed
// when we have issues left
&:not(.done)
border-radius: 6px 0 0 6px
&.opened
width: 100%
background: $opened
h2
margin: 10px 0 0 0
padding: 0
&.closed
float: left
color: $closed
&.opened
float: right
color: $opened

View File

@ -1,18 +0,0 @@
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 400;
src: local('Source Sans Pro'), local('SourceSansPro-Regular'), url(http://themes.googleusercontent.com/static/fonts/sourcesanspro/v6/ODelI1aHBYDBqgeIAH2zlBM0YzuT7MdOe03otPbuUS0.woff) format('woff');
}
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 600;
src: local('Source Sans Pro Semibold'), local('SourceSansPro-Semibold'), url(http://themes.googleusercontent.com/static/fonts/sourcesanspro/v6/toadOcfmlt9b38dHJxOBGJ6-ys_j0H4QL65VLqzI3wI.woff) format('woff');
}
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 700;
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url(http://themes.googleusercontent.com/static/fonts/sourcesanspro/v6/toadOcfmlt9b38dHJxOBGFkQc6VGVFSmCnC_l7QZG60.woff) format('woff');
}

View File

@ -1,4 +0,0 @@
<div class="box error">
<h2>Trouble</h2>
<p><%- @text %></p>
</div>

View File

@ -1,11 +0,0 @@
<div class="box">
<h1><%- @milestone.title %>@<%- @repo %></h1>
<% if @milestone.description: %>
<p class="description"><%- @milestone.description %></p>
<% end %>
<div id="graph">
<div id="tooltip"></div>
<div id="svg"></div>
</div>
<div id="progress"></div>
</div>

View File

@ -1,9 +0,0 @@
<div class="box info">
<h2>GitHub Burndown Chart</h2>
<p>Use your browser's location hash to specify a <strong>repo</strong>: <a href="#!/radekstepan/disposable">#!/radekstepan/disposable</a>.</p>
<p>You can choose a specific <strong>milestone</strong> by its <em>number</em>: <a href="#!/radekstepan/disposable/1">#!/radekstepan/disposable/1</a>.</p>
<p>To get the milestone <em>number</em>, fetch all your milestones using <a href="https://developer.github.com/v3/issues/milestones/#list-milestones-for-a-repository" title="GitHub API docs">GitHub API</a>.</p>
</div>

View File

@ -1,10 +0,0 @@
<% points = Math.ceil @points %>
<% if points > 1: %>
<%- points %> points left
<% else: %>
<% if points is 1: %>
1 point left
<% else: %>
Done
<% end %>
<% end %>

View File

@ -1,4 +0,0 @@
<div class="box generic">
<h2>GitHub Burndown Chart</h2>
<p>Loading <a href="#!/<%- @path %>">#!/<%- @path %></a>.</p>
</div>

View File

@ -1,10 +0,0 @@
<div class="bars">
<% if @progress is 100: %>
<div class="closed done" style="width:100%"></div>
<% else: %>
<div class="closed" style="width:<%= @progress %>%"></div>
<% end %>
<div class="opened"></div>
</div>
<h2 class="closed">Closed / <%= Math.floor @progress %>%</h2>
<h2 class="opened">Open / <%= 100 - Math.floor @progress %>%</h2>

View File

@ -1,87 +0,0 @@
#!/usr/bin/env coffee
proxy = do require('proxyquire').noCallThru
assert = require 'assert'
path = require 'path'
req = {}
config = proxy path.resolve(__dirname, '../src/modules/config.coffee'),
'./request': req
'./require':
'_': require 'lodash'
'superagent': null
'd3': null
'async': null
'marked': null
{ size_label } = require path.resolve __dirname, '../src/modules/regex.coffee'
module.exports =
'config - is null': (done) ->
req.config = (cb) ->
cb null, null
config (err, cfg) ->
assert.ifError err
assert.deepEqual cfg,
'host': 'api.github.com'
'protocol': 'https'
'size_label': new RegExp size_label
do done
'config - is empty': (done) ->
req.config = (cb) ->
cb null, {}
config (err, cfg) ->
assert.ifError err
assert.deepEqual cfg,
'host': 'api.github.com'
'protocol': 'https'
'size_label': new RegExp size_label
do done
'config - custom size label': (done) ->
size = '/^taille (\d+)$/'
req.config = (cb) ->
cb null, { 'size_label': size }
config (err, cfg) ->
assert.ifError err
assert.deepEqual cfg,
'host': 'api.github.com'
'protocol': 'https'
'size_label': new RegExp size
do done
'config - custom valid protocol': (done) ->
req.config = (cb) ->
cb null, { 'protocol': 'http' }
config (err, cfg) ->
assert.ifError err
assert.deepEqual cfg,
'host': 'api.github.com'
'protocol': 'http'
'size_label': new RegExp size_label
do done
'config - custom invalid protocol': (done) ->
req.config = (cb) ->
cb null, { 'protocol': 'nntp' }
config (err, cfg) ->
assert.equal err, 'Config field `protocol` misconfigured'
assert.equal cfg, null
do done
'config - custom invalid off days': (done) ->
req.config = (cb) ->
cb null, { 'off_days': [ 0 ] }
config (err, cfg) ->
assert.equal err, 'Config field `off_days` misconfigured'
assert.equal cfg, null
do done

View File

@ -1,196 +0,0 @@
#!/usr/bin/env coffee
proxy = do require('proxyquire').noCallThru
assert = require 'assert'
path = require 'path'
req = {}
regex = require path.resolve(__dirname, '../src/modules/regex.coffee')
issues = proxy path.resolve(__dirname, '../src/modules/issues.coffee'),
'./request': req
'./require':
'_': require 'lodash'
'superagent': null
'd3': null
'async': require 'async'
'marked': null
repo = { 'milestone': { 'number': no } }
module.exports =
'issues - all empty': (done) ->
called = 0
req.all_issues = (repo, opts, cb) ->
called += 1
cb null, []
issues.get_all repo, (err, [ open, closed ]) ->
assert.ifError err
assert.equal called, 2
assert.equal open.length, 0
assert.equal closed.length, 0
do done
'issues - open empty': (done) ->
called = 0
req.all_issues = (repo, opts, cb) ->
called += 1
cb null, if called is 1 then [] else [
{ number: 1 }
]
issues.get_all repo, (err, [ open, closed ]) ->
assert.ifError err
assert.equal called, 2
assert.equal open.length, 0
assert.equal closed.length, 1
do done
'issues - closed empty': (done) ->
called = 0
req.all_issues = (repo, opts, cb) ->
called += 1
cb null, if called is 2 then [] else [
{ number: 1 }
]
issues.get_all repo, (err, [ open, closed ]) ->
assert.ifError err
assert.equal called, 2
assert.equal open.length, 1
assert.equal closed.length, 0
do done
'issues - both not empty': (done) ->
called = 0
req.all_issues = (repo, opts, cb) ->
called += 1
cb null, [ { number: 1 } ]
issues.get_all repo, (err, [ open, closed ]) ->
assert.ifError err
assert.equal called, 2
assert.equal open.length, 1
assert.equal closed.length, 1
do done
'issues - 99 results on a page': (done) ->
called = 0
req.all_issues = (repo, opts, cb) ->
called += 1
cb null, ( { number: i } for i in [ 0...99 ] )
issues.get_all repo, (err, [ open, closed ]) ->
assert.ifError err
assert.equal called, 2
assert.equal open.length, 99
assert.equal closed.length, 99
do done
'issues - 100 results on a page': (done) ->
called = 0
req.all_issues = (repo, opts, cb) ->
called += 1
assert opts.page in [ 1, 2 ]
cb null, if opts.page is 1 then ( { number: i } for i in [ 0...100 ] ) else []
issues.get_all repo, (err, [ open, closed ]) ->
assert.ifError err
assert.equal called, 4
assert.equal open.length, 100
assert.equal closed.length, 100
do done
'issues - 101 total results': (done) ->
called = 0
req.all_issues = (repo, opts, cb) ->
called += 1
assert opts.page in [ 1, 2 ]
cb null, if opts.page is 1
( { number: i } for i in [ 0...100 ] )
else
[ { number: 100 } ]
issues.get_all repo, (err, [ open, closed ]) ->
assert.ifError err
assert.equal called, 4
assert.equal open.length, 101
assert.equal closed.length, 101
assert.deepEqual open[100], { number: 100 }
assert.deepEqual closed[100], { number: 100 }
do done
'issues - 201 total results': (done) ->
called = 0
req.all_issues = (repo, opts, cb) ->
called += 1
assert opts.page in [ 1, 2, 3 ]
cb null, if opts.page in [ 1, 2 ]
( { number: i } for i in [ (h = 100 * (opts.page - 1))...h + 100 ] )
else
[ { number: 200 } ]
issues.get_all repo, (err, [ open, closed ]) ->
assert.ifError err
assert.equal called, 6
assert.equal open.length, 201
assert.equal closed.length, 201
for i in [ open, closed ]
for j in [ 100, 200 ]
assert.deepEqual i[j], { number: j }
do done
'issues - get all when not found': (done) ->
called = 0
req.all_issues = (repo, opts, cb) ->
called += 1
cb 'Not Found'
issues.get_all repo, (err, [ open, closed ]) ->
assert.equal err, 'Not Found'
assert.equal called, 1
do done
'issues - filter on existing label regex': (done) ->
issues.filter [ { labels: [ { name: 'size 15' } ] } ]
, regex.size_label, (err, data) ->
assert.ifError err
assert.equal data.length, 1
assert.equal data[0].size, 15
do done
'issues - filter when no labels': (done) ->
issues.filter [ { } ]
, regex.size_label, (err, data) ->
assert.ifError err
assert.equal data.length, 0
do done
'issues - filter when empty labels': (done) ->
issues.filter [ { labels: [] } ]
, regex.size_label, (err, data) ->
assert.ifError err
assert.equal data.length, 0
do done
'issues - filter when not matching regex': (done) ->
issues.filter [ { labels: [ { name: 'size 1A' } ] } ]
, regex.size_label, (err, data) ->
assert.ifError err
assert.equal data.length, 0
do done
'issues - filter when multiple match the regex': (done) ->
issues.filter [
{ labels: [ { name: 'size 1' }, { name: 'size 6' } ] }
{ labels: [ { name: 'size really big' }, { name: 'size 4' } ] }
]
, regex.size_label, (err, data) ->
assert.ifError err
assert.equal data.length, 2
[ a, b ] = data
assert.equal a.size, 7
assert.equal b.size, 4
do done

View File

@ -1,188 +0,0 @@
#!/usr/bin/env coffee
proxy = do require('proxyquire').noCallThru
assert = require 'assert'
path = require 'path'
req = {}
milestones = proxy path.resolve(__dirname, '../src/modules/milestones.coffee'),
'./request': req
'./require':
'_': require 'lodash'
'superagent': null
'd3': null
'async': null
'marked': require 'marked'
module.exports =
'milestones - get current from 1': (done) ->
req.all_milestones = (opts, cb) ->
cb null, [
{
'number': 1
'created_at': '2013-01-01T00:00:00Z'
'due_on': '2013-02-01T00:00:00Z'
}
]
milestones {}, (err, warn, milestone) ->
assert.ifError err
assert.equal milestone.number, 1
do done
'milestones - get current from 1 when milestone has no due date': (done) ->
req.all_milestones = (opts, cb) ->
cb null, [
{
'number': 1
'created_at': '2013-01-01T00:00:00Z'
'due_on': null
}
]
milestones {}, (err, warn, milestone) ->
assert.ifError err
assert.equal milestone.number, 1
do done
# We always take from head because of request params.
'milestones - get current from > 1': (done) ->
req.all_milestones = (opts, cb) ->
cb null, [
{
'number': 2
'created_at': '2013-01-01T00:00:00Z'
'due_on': '2013-01-15T00:00:00Z'
}
{
'number': 1
'created_at': '2013-01-01T00:00:00Z'
'due_on': '2013-02-01T00:00:00Z'
}
{
'number': 3
'created_at': '2013-01-01T00:00:00Z'
'due_on': '2013-02-15T00:00:00Z'
}
]
milestones {}, (err, warn, milestone) ->
assert.ifError err
assert.equal milestone.number, 2
do done
'milestones - get current from > 1 when there are milestones without due date': (done) ->
req.all_milestones = (opts, cb) ->
cb null, [
{
'number': 2
'created_at': '2013-01-01T00:00:00Z'
'due_on': null
}
{
'number': 1
'created_at': '2013-01-01T00:00:00Z'
'due_on': '2013-02-01T00:00:00Z'
}
{
'number': 3
'created_at': '2013-01-01T00:00:00Z'
'due_on': '2013-02-15T00:00:00Z'
}
]
milestones {}, (err, warn, milestone) ->
assert.ifError err
assert.equal milestone.number, 1
do done
'milestones - get current when empty': (done) ->
req.all_milestones = (opts, cb) ->
cb null, []
milestones { 'path': 'some/repo' }, (err, warn, milestone) ->
assert.ifError err
assert.equal warn, 'No open milestones for repo some/repo'
do done
'milestones - get current when not found': (done) ->
req.all_milestones = (opts, cb) ->
cb 'Not Found'
milestones {}, (err, warn, milestone) ->
assert.equal err, 'Not Found'
do done
'milestones - get current when no issues': (done) ->
req.all_milestones = (opts, cb) ->
cb null, [
{
'title': 'No issues'
'number': 1
'created_at': '2013-01-01T00:00:00Z'
'due_on': '2013-02-01T00:00:00Z',
'open_issues': 0,
'closed_issues': 0
}
]
milestones {}, (err, warn, milestone) ->
assert.ifError err
assert.equal warn, 'No issues for milestone `No issues`'
do done
'milestones - get one': (done) ->
m =
'number': 1
'created_at': '2013-01-01T00:00:00Z'
'due_on': '2013-02-01T00:00:00Z'
req.one_milestone = (opts, number, cb) ->
cb null, m
milestones { 'milestone': 1 }, (err, warn, milestone) ->
assert.ifError err
assert.equal warn, null
assert.deepEqual milestone, m
do done
'milestones - get one (404)': (done) ->
req.one_milestone = (opts, number, cb) ->
cb 'Not Found'
milestones { 'milestone': 9 }, (err, warn, milestone) ->
assert.equal err, 'Not Found'
do done
'milestones - get one when no issues': (done) ->
req.one_milestone = (opts, number, cb) ->
cb null, {
'title': 'No issues'
'number': 1
'created_at': '2013-01-01T00:00:00Z'
'due_on': '2013-02-01T00:00:00Z',
'open_issues': 0,
'closed_issues': 0
}
milestones { 'milestone': 9 }, (err, warn, milestone) ->
assert.ifError err
assert.equal warn, 'No issues for milestone `No issues`'
do done
'milestones - has description': (done) ->
req.all_milestones = (opts, cb) ->
cb null, [
{
'number': 1
'created_at': '2013-01-01T00:00:00Z'
'due_on': '2013-02-01T00:00:00Z'
'description': 'A description of this <strong>milestone</strong> goes *here*'
}
]
milestones {}, (err, warn, milestone) ->
assert.ifError err
assert.equal milestone.description, 'A description of this <strong>milestone</strong> goes <em>here</em>'
do done

View File

@ -1,114 +0,0 @@
#!/usr/bin/env coffee
proxy = do require('proxyquire').noCallThru
assert = require 'assert'
path = require 'path'
_ = require 'lodash'
class Superagent
# How soon do we call back?
timeout: 1
# Save the uri.
get: (uri) ->
@params = { uri }
@
# Save the key-value pair.
set: (key, value) ->
@params[key] = value
@
# Call back with the response.
end: (cb) ->
setTimeout =>
cb null, @response
, @timeout
request = proxy path.resolve(__dirname, '../src/modules/request.coffee'),
'./require':
'_': require 'lodash'
'superagent': sa = new Superagent()
'd3': null
'async': null
'marked': null
module.exports =
'request - all milestones (ok)': (done) ->
sa.response =
'statusType': 2
'error': no
'body': [ null ]
request.all_milestones {}, (err, data) ->
assert.ifError err
assert.deepEqual sa.params,
'uri': 'undefined://undefined/repos/undefined/milestones?state=open&sort=due_date&direction=asc'
'Content-Type': 'application/json',
'Accept': 'application/vnd.github.v3'
assert.deepEqual data, [ null ]
do done
'request - one milestone (ok)': (done) ->
sa.response =
'statusType': 2
'error': no
'body': [ null ]
request.one_milestone {}, 1, (err, data) ->
assert.ifError err
assert.deepEqual sa.params,
'uri': 'undefined://undefined/repos/undefined/milestones/1?state=open&sort=due_date&direction=asc'
'Content-Type': 'application/json',
'Accept': 'application/vnd.github.v3'
assert.deepEqual data, [ null ]
do done
'request - one milestone (404)': (done) ->
sa.response =
'statusType': 4
'error': Error "cannot GET undefined (404)"
'body':
'documentation_url': "http://developer.github.com/v3"
'message': "Not Found"
request.one_milestone {}, 9, (err) ->
assert.equal err, 'Not Found'
do done
'request - one milestone (500)': (done) ->
sa.response =
'statusType': 5
'error': Error "Error"
'body': null
request.one_milestone {}, 9, (err) ->
assert.equal err, 'Error'
do done
'request - all issues (ok)': (done) ->
sa.response =
'statusType': 2
'error': no
'body': [ null ]
request.all_issues {}, {}, (err, data) ->
assert.ifError err
assert.deepEqual sa.params,
'uri': 'undefined://undefined/repos/undefined/issues?per_page=100'
'Content-Type': 'application/json',
'Accept': 'application/vnd.github.v3'
assert.deepEqual data, [ null ]
do done
'request - timeout': (done) ->
sa.timeout = 10001
sa.response =
'statusType': 2
'error': no
'body': [ null ]
request.all_issues {}, {}, (err) ->
assert.equal err, 'Request has timed out'
do done