From 3abc97e45e7832bd94bc08c6a547a50d08acf679 Mon Sep 17 00:00:00 2001 From: Radek Stepan Date: Mon, 12 Aug 2013 19:23:30 +0100 Subject: [PATCH] very simple get current milestones check --- Makefile | 4 + README.md | 4 + app.coffee | 231 -------------------- package.json | 34 +++ proxy.coffee | 1 + public/build.css | 1 + public/build.js | 1 + public/config.json | 481 +++++++++++++++++++++++++++++++++++++++++ public/index.html | 23 ++ src/component.json | 14 ++ src/milestones.coffee | 11 + src/request.coffee | 4 + test/milestones.coffee | 26 +++ 13 files changed, 604 insertions(+), 231 deletions(-) create mode 100644 Makefile delete mode 100644 app.coffee create mode 100644 package.json create mode 100644 proxy.coffee create mode 120000 public/build.css create mode 120000 public/build.js create mode 100644 public/config.json create mode 100644 public/index.html create mode 100644 src/component.json create mode 100644 src/milestones.coffee create mode 100644 src/request.coffee create mode 100644 test/milestones.coffee diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6506cd0 --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ +test: + ./node_modules/.bin/mocha --compilers coffee:coffee-script --reporter spec --ui exports --bail + +.PHONY: test \ No newline at end of file diff --git a/README.md b/README.md index fd79fcb..400021e 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,10 @@ Show: * For each issue show other tags and assignee (avatar). * Number of working days left. * To whom open issues still belong. +* Projected ship date (project running late/not). +* For each user/avatar what is their % progress and number of open/closed issues. +* Heat: if we are struck/very productive for a period of time, colorize the chart line. +* For milestones with no due date, show an estimate as to when it will probably be finished. Allow: diff --git a/app.coffee b/app.coffee deleted file mode 100644 index 68ff44e..0000000 --- a/app.coffee +++ /dev/null @@ -1,231 +0,0 @@ -flatiron = require 'flatiron' -connect = require 'connect' -https = require 'https' -fs = require "fs" -yaml = require "js-yaml" -eco = require 'eco' - -# 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 - headers: - 'User-Agent': 'Scrum Burndown (1)' - - if Issues.config.api_token - options.headers.Authorization = 'token '+Issues.config.api_token - - https.request(options, (response) -> - if response.statusCode is 200 - json = "" - response.on "data", (chunk) -> json += chunk - - response.on "end", -> callback JSON.parse(json), type - else - throw response.statusCode - ).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 8601 to JS timestamp at the beginning of UTC day!' - dateToTime: (date) -> - # Some milestones do not have due dates. - return 0 unless date? - # Add miliseconds and create `Date`. - date = new Date(date[0...date.length - 1] + '.000' + date.charAt date.length-1) - # Move to the beginning of the day (at 9am BST, 8am GMT, so we do not worry about time shifts). - date = new Date date.getFullYear(), date.getMonth(), date.getDate(), 9 - # Return timestamp. - date.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 - -# Config filters. -app = flatiron.app -app.use flatiron.plugins.http, - 'before': [ - connect.favicon() - connect.static __dirname + '/public' - ] - -# Eco templating. -app.use - name: "eco-templating" - attach: (options) -> - app.eco = (path, data, cb) -> - fs.readFile "./templates/#{path}.eco", "utf8", (err, template) -> - if err then cb err, null - else - try - cb null, eco.render template, data - catch e - cb e, null - -# Show burndown chart. -getBurndown = -> - console.log 'Get burndown chart' - - 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 - # Do we actually have an open milestone? - if store.milestones.length > 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 - # JS expects more accuracy. - due = Issues.dateToTime milestone['due_on'] - # 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 ; totalNonWorkingDays = 0 - day = Issues.dateToTime current.milestone.created_at - while day < current.due - # Do we have weekends configured? - if Issues.config.weekend? and Issues.config.weekend instanceof Array - dayOfWeek = new Date(day).getDay() - # Fix stupid Abrahamic tradition. - if dayOfWeek is 0 then dayOfWeek = 7 - - # Does this day fall on a weekend? - if dayOfWeek in Issues.config.weekend - totalNonWorkingDays += 1 - # Save the day. - days[day] = { 'issues': [], 'actual': 0, 'ideal': 0, 'weekend': true } - else - # Save the day. - days[day] = { 'issues': [], 'actual': 0, 'ideal': 0, 'weekend': false } - else - # Save the day. - days[day] = { 'issues': [], 'actual': 0, 'ideal': 0, 'weekend': false } - - # 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? - # Save it. - days[Issues.dateToTime issue.closed_at]['issues'].push issue - - # Calculate the predicted daily velocity. - dailyIdeal = current['size'] / (totalDays - totalNonWorkingDays) ; ideal = current['size'] - - # Go through the days and save the number of outstanding issues size. - for day, d of days - # Does this day have any closed issues? Reduce the total for this milestone. - for issue in d['issues'] - current['size'] -= issue.size - # Save the oustanding count for that day. - days[day].actual = current['size'] - - # Save the predicted velocity for that day if it is not a non-working day. - ideal -= dailyIdeal unless days[day].weekend - days[day].ideal = ideal - - # Finally send to client. - app.eco 'burndown', - 'days': days - 'project': Issues.config.project_name - 'base_url': Issues.config.base_url - , (err, html) => - throw err if err - @res.writeHead 200, "content-type": "text/html" - @res.write html - @res.end() - - else - # No current milestone. - app.eco 'empty', - 'project': Issues.config.project_name - 'base_url': Issues.config.base_url - , (err, html) => - throw err if err - @res.writeHead 200, "content-type": "text/html" - @res.write html - @res.end() - - # Get Milestones, Opened and Closed Tickets. - Issues.getMilestones done - Issues.getOpenIssues done - Issues.getClosedIssues done - -# Show open issues. -getIssues = -> - console.log 'Get open issues' - - Issues.getOpenIssues (issues) => - - # Replace the dates in issues with nice dates. - issues = ( Issues.format(issue) for issue in issues ) - - app.eco 'issues', - 'issues': issues - 'project': Issues.config.project_name - 'base_url': Issues.config.base_url - , (err, html) => - throw err if err - @res.writeHead 200, "content-type": "text/html" - @res.write html - @res.end() - -# Routes -app.router.path '/', -> - @get getBurndown - -app.router.path '/burndown', -> - @get getBurndown - -app.router.path '/issues', -> - @get getIssues - -# Fetch config and start server. -fs.readFile "config.yml", "utf8", (err, data) -> - Issues.config = yaml.load data - - app.start process.env.PORT, (err) -> - throw err if err - console.log "Listening on port #{app.server.address().port}" diff --git a/package.json b/package.json new file mode 100644 index 0000000..bf3d380 --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "github-burndown-chart", + "version": "1.0.0-alpha", + "description": "Shows a burndown chart for GitHub Issues", + "directories": { + "test": "test" + }, + "dependencies": { + "async": "~0.2.9", + "proxyquire": "~0.4.1" + }, + "devDependencies": { + "mocha": "~1.12.0" + }, + "scripts": { + "test": "make test" + }, + "repository": { + "type": "git", + "url": "git://github.com/radekstepan/github-burndown-chart.git" + }, + "keywords": [ + "github", + "issues", + "burndown", + "chart", + "scrum" + ], + "author": "Radek ", + "license": "BSD", + "bugs": { + "url": "https://github.com/radekstepan/github-burndown-chart/issues" + } +} diff --git a/proxy.coffee b/proxy.coffee new file mode 100644 index 0000000..706739d --- /dev/null +++ b/proxy.coffee @@ -0,0 +1 @@ +#!/usr/bin/env coffee \ No newline at end of file diff --git a/public/build.css b/public/build.css new file mode 120000 index 0000000..83a1fd4 --- /dev/null +++ b/public/build.css @@ -0,0 +1 @@ +../build/build.css \ No newline at end of file diff --git a/public/build.js b/public/build.js new file mode 120000 index 0000000..c28167c --- /dev/null +++ b/public/build.js @@ -0,0 +1 @@ +../build/build.js \ No newline at end of file diff --git a/public/config.json b/public/config.json new file mode 100644 index 0000000..c6bb0fd --- /dev/null +++ b/public/config.json @@ -0,0 +1,481 @@ +{ + "1003692": { + "summary": { + "organism.name": "Drosophila melanogaster", + "primaryIdentifier": "FBgn0004110", + "symbol": "tin", + "secondaryIdentifier": "CG7895" + }, + "identifiers": { + "tinman": [ + "MATCH" + ] + }, + "type": "Gene" + }, + "1005232": { + "summary": { + "organism.name": "Drosophila melanogaster", + "primaryIdentifier": "FBgn0000099", + "symbol": "ap", + "secondaryIdentifier": "CG8376" + }, + "identifiers": { + "FBgn0000099": [ + "MATCH" + ] + }, + "type": "Gene" + }, + "1005584": { + "summary": { + "organism.name": "Drosophila melanogaster", + "primaryIdentifier": "FBgn0000166", + "symbol": "bcd", + "secondaryIdentifier": "CG1034" + }, + "identifiers": { + "CG1034": [ + "MATCH" + ] + }, + "type": "Gene" + }, + "1006039": { + "summary": { + "organism.name": "Drosophila melanogaster", + "primaryIdentifier": "FBgn0001150", + "symbol": "gt", + "secondaryIdentifier": "CG7952" + }, + "identifiers": { + "gt": [ + "DUPLICATE" + ] + }, + "type": "Gene" + }, + "1006967": { + "summary": { + "organism.name": "Drosophila melanogaster", + "primaryIdentifier": "FBgn0003300", + "symbol": "run", + "secondaryIdentifier": "CG1849" + }, + "identifiers": { + "runt": [ + "DUPLICATE" + ] + }, + "type": "Gene" + }, + "1007192": { + "summary": { + "organism.name": "Drosophila melanogaster", + "primaryIdentifier": "FBgn0000606", + "symbol": "eve", + "secondaryIdentifier": "CG2328" + }, + "identifiers": { + "CG2328": [ + "MATCH" + ] + }, + "type": "Gene" + }, + "1007568": { + "summary": { + "organism.name": "Drosophila melanogaster", + "primaryIdentifier": "FBgn0003870", + "symbol": "ttk", + "secondaryIdentifier": "CG1856" + }, + "identifiers": { + "tramtrack": [ + "DUPLICATE" + ] + }, + "type": "Gene" + }, + "1008149": { + "summary": { + "organism.name": "Drosophila melanogaster", + "primaryIdentifier": "FBgn0040765", + "symbol": "luna", + "secondaryIdentifier": "CG33473" + }, + "identifiers": { + "CG33473": [ + "MATCH" + ] + }, + "type": "Gene" + }, + "1012064": { + "summary": { + "organism.name": "Drosophila melanogaster", + "primaryIdentifier": "FBgn0003866", + "symbol": "tsh", + "secondaryIdentifier": "CG1374" + }, + "identifiers": { + "CG1374": [ + "MATCH" + ] + }, + "type": "Gene" + }, + "1013634": { + "summary": { + "organism.name": "Drosophila melanogaster", + "primaryIdentifier": "FBgn0003720", + "symbol": "tll", + "secondaryIdentifier": "CG1378" + }, + "identifiers": { + "tll": [ + "MATCH" + ] + }, + "type": "Gene" + }, + "1017495": { + "summary": { + "organism.name": "Drosophila melanogaster", + "primaryIdentifier": "FBgn0000028", + "symbol": "acj6", + "secondaryIdentifier": "CG9151" + }, + "identifiers": { + "CG9151": [ + "MATCH" + ] + }, + "type": "Gene" + }, + "1019651": { + "summary": { + "organism.name": "Drosophila melanogaster", + "primaryIdentifier": "FBgn0011655", + "symbol": "Med", + "secondaryIdentifier": "CG1775" + }, + "identifiers": { + "CG1775": [ + "MATCH" + ] + }, + "type": "Gene" + }, + "1020845": { + "summary": { + "organism.name": "Drosophila melanogaster", + "primaryIdentifier": "FBgn0024250", + "symbol": "brk", + "secondaryIdentifier": "CG9653" + }, + "identifiers": { + "FBgn0024250": [ + "MATCH" + ] + }, + "type": "Gene" + }, + "1021556": { + "summary": { + "organism.name": "Drosophila melanogaster", + "primaryIdentifier": "FBgn0041111", + "symbol": "lilli", + "secondaryIdentifier": "CG8817" + }, + "identifiers": { + "CG8817": [ + "MATCH" + ] + }, + "type": "Gene" + }, + "1022210": { + "summary": { + "organism.name": "Drosophila melanogaster", + "primaryIdentifier": "FBgn0011766", + "symbol": "E2f", + "secondaryIdentifier": "CG6376" + }, + "identifiers": { + "E2f": [ + "MATCH" + ] + }, + "type": "Gene" + }, + "1023733": { + "summary": { + "organism.name": "Drosophila melanogaster", + "primaryIdentifier": "FBgn0011648", + "symbol": "Mad", + "secondaryIdentifier": "CG12399" + }, + "identifiers": { + "Mad": [ + "MATCH" + ] + }, + "type": "Gene" + }, + "1029518": { + "summary": { + "organism.name": "Drosophila melanogaster", + "primaryIdentifier": "FBgn0003460", + "symbol": "so", + "secondaryIdentifier": "CG11121" + }, + "identifiers": { + "so": [ + "MATCH" + ] + }, + "type": "Gene" + }, + "1033140": { + "summary": { + "organism.name": "Drosophila melanogaster", + "primaryIdentifier": "FBgn0025800", + "symbol": "Smox", + "secondaryIdentifier": "CG2262" + }, + "identifiers": { + "CG2262": [ + "MATCH" + ] + }, + "type": "Gene" + }, + "1035180": { + "summary": { + "organism.name": "Drosophila melanogaster", + "primaryIdentifier": "FBgn0001325", + "symbol": "Kr", + "secondaryIdentifier": "CG3340" + }, + "identifiers": { + "FBgn0001251": [ + "OTHER" + ] + }, + "type": "Gene" + }, + "1036625": { + "summary": { + "organism.name": "Drosophila melanogaster", + "primaryIdentifier": "FBgn0003430", + "symbol": "slp1", + "secondaryIdentifier": "CG16738" + }, + "identifiers": { + "CG16738": [ + "MATCH" + ] + }, + "type": "Gene" + }, + "1042703": { + "summary": { + "organism.name": "Drosophila melanogaster", + "primaryIdentifier": "FBgn0001180", + "symbol": "hb", + "secondaryIdentifier": "CG9786" + }, + "identifiers": { + "CG9786": [ + "MATCH" + ] + }, + "type": "Gene" + }, + "1045695": { + "summary": { + "organism.name": "Drosophila melanogaster", + "primaryIdentifier": "FBgn0010433", + "symbol": "ato", + "secondaryIdentifier": "CG7508" + }, + "identifiers": { + "FBgn0010433": [ + "MATCH" + ], + "ato": [ + "MATCH" + ] + }, + "type": "Gene" + }, + "1065292": { + "summary": { + "organism.name": "Drosophila melanogaster", + "primaryIdentifier": "FBgn0004915", + "symbol": "TfIIB", + "secondaryIdentifier": "CG5193" + }, + "identifiers": { + "TfIIB": [ + "MATCH" + ] + }, + "type": "Gene" + }, + "1068009": { + "summary": { + "organism.name": "Drosophila melanogaster", + "primaryIdentifier": "FBgn0000157", + "symbol": "Dll", + "secondaryIdentifier": "CG3629" + }, + "identifiers": { + "CG3629": [ + "MATCH" + ] + }, + "type": "Gene" + }, + "1077315": { + "summary": { + "organism.name": "Drosophila melanogaster", + "primaryIdentifier": "FBgn0001077", + "symbol": "ftz", + "secondaryIdentifier": "CG2047" + }, + "identifiers": { + "ftz": [ + "MATCH" + ] + }, + "type": "Gene" + }, + "1145128": { + "summary": { + "organism.name": "Drosophila melanogaster", + "primaryIdentifier": "FBgn0003900", + "symbol": "twi", + "secondaryIdentifier": "CG2956" + }, + "identifiers": { + "TWIST_DROME": [ + "TYPE_CONVERTED" + ] + }, + "type": "Gene" + }, + "6000138": { + "summary": { + "organism.name": "Drosophila pseudoobscura", + "primaryIdentifier": "FBgn0075017", + "symbol": "Dpse\ttk", + "secondaryIdentifier": "GA14992" + }, + "identifiers": { + "tramtrack": [ + "DUPLICATE" + ] + }, + "type": "Gene" + }, + "7000252": { + "summary": { + "organism.name": "Drosophila simulans", + "primaryIdentifier": "FBgn0193021", + "symbol": "Dsim\ttk", + "secondaryIdentifier": "GD21596" + }, + "identifiers": { + "tramtrack": [ + "DUPLICATE" + ] + }, + "type": "Gene" + }, + "7000255": { + "summary": { + "organism.name": "Drosophila yakuba", + "primaryIdentifier": "FBgn0228787", + "symbol": "Dyak\ttk", + "secondaryIdentifier": "GE10957" + }, + "identifiers": { + "tramtrack": [ + "DUPLICATE" + ] + }, + "type": "Gene" + }, + "7010773": { + "summary": { + "organism.name": "Drosophila simulans", + "primaryIdentifier": "FBgn0016352", + "symbol": "Dsim\run", + "secondaryIdentifier": "GD17500" + }, + "identifiers": { + "runt": [ + "DUPLICATE" + ] + }, + "type": "Gene" + }, + "7513758": { + "summary": { + "organism.name": "Drosophila erecta", + "primaryIdentifier": "FBgn0085118", + "symbol": "Dere\run", + "secondaryIdentifier": "GG19696" + }, + "identifiers": { + "runt": [ + "DUPLICATE" + ] + }, + "type": "Gene" + }, + "7878354": { + "summary": { + "organism.name": "Drosophila virilis", + "primaryIdentifier": "FBgn0013920", + "symbol": "Dvir\run", + "secondaryIdentifier": "GJ19252" + }, + "identifiers": { + "runt": [ + "DUPLICATE" + ] + }, + "type": "Gene" + }, + "7918344": { + "summary": { + "organism.name": "Drosophila yakuba", + "primaryIdentifier": "FBgn0084618", + "symbol": "Dyak\run", + "secondaryIdentifier": "GE17894" + }, + "identifiers": { + "runt": [ + "DUPLICATE" + ] + }, + "type": "Gene" + }, + "17023535": { + "summary": { + "organism.name": "Mus musculus", + "primaryIdentifier": "MGI:95866", + "symbol": "gt", + "secondaryIdentifier": null + }, + "identifiers": { + "gt": [ + "DUPLICATE" + ] + }, + "type": "Gene" + } +} \ No newline at end of file diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..518f98d --- /dev/null +++ b/public/index.html @@ -0,0 +1,23 @@ + + + + + component-400 + + + + + + + +
+ + + \ No newline at end of file diff --git a/src/component.json b/src/component.json new file mode 100644 index 0000000..9607163 --- /dev/null +++ b/src/component.json @@ -0,0 +1,14 @@ +{ + "name": "app", + "main": "app.js", + "version": "1.0.0-alpha", + "dependencies": { + "component/map": "*", + "segmentio/extend": "*", + "component/object": "*", + "manuelstofer/foreach": "*", + "component/dom": "*" + }, + "scripts": [ ], + "styles": [ ] +} \ No newline at end of file diff --git a/src/milestones.coffee b/src/milestones.coffee new file mode 100644 index 0000000..64dc17f --- /dev/null +++ b/src/milestones.coffee @@ -0,0 +1,11 @@ +#!/usr/bin/env coffee +req = require './request' + +module.exports = + 'get_current': (user, repo, cb) -> + req.milestones user, repo, (err, data) -> + return cb err if err + # Go through the milestones looking for one that ends/ended soonest. + max = [ null, +Infinity ] + ( max = [ parseInt(i), int ] if (int = +new Date due_on) < max[1] for i, { due_on } of data ) + cb null, data[max[0]] \ No newline at end of file diff --git a/src/request.coffee b/src/request.coffee new file mode 100644 index 0000000..caf3edc --- /dev/null +++ b/src/request.coffee @@ -0,0 +1,4 @@ +#!/usr/bin/env coffee +module.exports = + 'milestones': (user, repo, cb) -> + cb null, { 'real': 'one' } \ No newline at end of file diff --git a/test/milestones.coffee b/test/milestones.coffee new file mode 100644 index 0000000..656d215 --- /dev/null +++ b/test/milestones.coffee @@ -0,0 +1,26 @@ +#!/usr/bin/env coffee +assert = require 'assert' +async = require 'async' +path = require 'path' +proxy = require 'proxyquire' + +req = {} + +milestones = proxy path.resolve(__dirname, '../src/milestones.coffee'), + './request': req + +module.exports = + 'get current': (done) -> + req.milestones = (user, repo, cb) -> + cb null, [ + { + 'number': 1 + 'created_at': '2013-01-01T00:00:00Z' + 'due_on': '2013-02-01T00:00:00Z' + } + ] + + milestones.get_current null, null, (err, milestone) -> + assert.ifError err + assert.equal milestone.number, 1 + done.call null \ No newline at end of file