burnchart/app.coffee

171 lines
6.5 KiB
CoffeeScript

express = require 'express'
eco = require 'eco'
https = require 'https'
fs = require "fs"
yaml = require "js-yaml"
# Helper object for GitHub Issues.
Issues =
# Make HTTPS GET to GitHub API v3.
get: (path, type, callback) ->
options =
host: "api.github.com"
method: "GET"
path: path
https.request(options, (response) ->
if response.statusCode is 200
json = ""
response.on "data", (chunk) -> json += chunk
response.on "end", -> callback JSON.parse(json), type
).end()
# URLs to API.
getOpenIssues: (callback) -> Issues.get "/repos/#{Issues.config.github_user}/#{Issues.config.github_project}/issues?state=open", 'issues', callback
getClosedIssues: (callback) -> Issues.get "/repos/#{Issues.config.github_user}/#{Issues.config.github_project}/issues?state=closed", 'issues', callback
getMilestones: (callback) -> Issues.get "/repos/#{Issues.config.github_user}/#{Issues.config.github_project}/milestones", 'milestones', callback
# Convert GitHub ISO to JS ISO date and then time
dateToTime: (date) -> new Date(date[0...date.length - 1] + '.000' + date.charAt date.length-1).getTime()
# Format issues for display in a listing.
format: (issue) ->
# Format the timestamps.
if issue.created_at? then issue.created_at = new Date(Issues.dateToTime(issue.created_at)).toUTCString()
if issue.updated_at? then issue.updated_at = new Date(Issues.dateToTime(issue.updated_at)).toUTCString()
issue
# Express.
app = express.createServer()
app.configure ->
app.use express.logger()
app.use express.bodyParser()
app.set 'view engine', 'eco'
app.set 'views', './templates'
# Register a custom .eco compiler.
app.engine 'eco', (path, options, callback) ->
fs.readFile "./#{path}", "utf8", (err, str) ->
callback eco.render str, options
app.use express.static('./public')
app.configure 'development', ->
app.use express.errorHandler
dumpExceptions: true
showStack: true
app.configure 'production', ->
app.use express.errorHandler()
# Show burndown chart.
app.get '/burndown', (req, res) ->
resources = 3 ; store = { 'issues': [], 'milestones': [] }
done = (data, type) ->
# One less to do.
resources--
switch type
when 'issues' then store.issues = store.issues.concat data
when 'milestones' then store.milestones = store.milestones.concat data
# Are we done?
if resources is 0
# Store the current milestone and its size.
current = { 'milestone': {}, 'diff': +Infinity, 'size': 0 }
# Determine the 'current' milestone
now = new Date().getTime()
for milestone in store.milestones
due = milestone['due_on']
# JS expects more accuracy.
due = Issues.dateToTime due
# Is this the 'current' one?
diff = due - now
if diff > 0 and diff < current.diff
current.milestone = milestone ; current.diff = diff ; current.due = due
# Create n dict with all dates in the milestone span.
days = {} ; totalDays = 0
day = Issues.dateToTime current.milestone.created_at # TODO: shift this to the start of the day and deal with time shifts.
while day < current.due
# Save the day.
days[day] = { 'issue': {}, 'actual': 0, 'ideal': 0 }
# Shift by a day.
day += 1000 * 60 * 60 * 24
# Increase the total count.
totalDays += 1
# Now go through the issues and place them to the appropriate days.
for issue in store.issues
# This milestone?
if issue.milestone?.number is current.milestone.number
# Has a size label?
if issue.labels?
issue.size = do (issue) ->
for label in issue.labels
if label.name.indexOf("size ") is 0
return parseInt label.name[5...]
if issue.size?
# Increase the total size of the milestone.
current.size += issue.size
# Is it closed?
if issue.closed_at?
closed = Issues.dateToTime issue.closed_at
# Find when was it closed (will be made faster)
day = do () ->
for day, x of days
if closed < day then return day
# Save it.
if day? then days[day]['issue'] = issue
# Calculate the predicted daily velocity.
dailyIdeal = current['size'] / totalDays ; ideal = current['size']
# Go through the days and save the number of outstanding issues size.
for day, d of days
# Does this day have an issue closed? Reduce the total for this milestone.
if d['issue'].size? then current['size'] -= d['issue'].size
# Save the oustanding count for that day.
days[day].actual = current['size']
# Save the predicted velocity for that day.
ideal -= dailyIdeal
days[day].ideal = ideal
# Finally send to client.
res.render 'burndown',
'days': days
'project': Issues.config.project_name
, (html) -> res.send html, 'Content-Type': 'text/html', 200
# Get Milestones, Opened and Closed Tickets.
Issues.getMilestones done
Issues.getOpenIssues done
Issues.getClosedIssues done
# Show open issues.
app.get '/issues', (req, res) ->
Issues.getOpenIssues (issues) ->
# Replace the dates in issues with nice dates.
issues = ( Issues.format(issue) for issue in issues )
res.render 'issues',
'issues': issues
'project': Issues.config.project_name
, (html) -> res.send html, 'Content-Type': 'text/html', 200
# Fetch config and start server.
fs.readFile "config.yml", "utf8", (err, data) ->
Issues.config = yaml.load data
app.listen 3000
console.log "Express server listening to port 3000"