asanban 0.0.2 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ MWQxYzY4OTJkMzJlOTdiMWQwMDE4MzM0NWFhODQxOWMwZWJkZmY1Yg==
5
+ data.tar.gz: !binary |-
6
+ MmY0MTYzM2RmYzNiNGQ1MzlmNWQwMjViOGUxYTg2YWQyYmRhNDZlNg==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ NzVkZWNiMjIzYzQ5YzRiYjI2NjA5ZWU0YzhmMjJmYjk2MWMzZTliOGZmMTIx
10
+ ZjYxYzY2MzVjYTk3YzgyNmNiYzEzNWI2NDA3NGY5NzdmNWFmYzZiMGE5YWY5
11
+ ODIzMTRjNzkyYjIxNDBlMTgyMTlkMzdiM2NhZmUzNzJhZjk1ZTg=
12
+ data.tar.gz: !binary |-
13
+ OWU5NTlmOWRkZjI4Y2I1NWI1MTczYTg3NWI5NGUzN2JlMzgwMjMwYjMyNDM0
14
+ ZjY5MWI5NTM4YTRkY2M2ZGQzYjRjMDBhYTRiZDdjZjlkNzVjOTVmYjViZDhk
15
+ N2JmMmYwNTliZTI0MjM4YzY2ZGM3NDlkZjgzYjgyOWEzYmE3YjM=
data/README.md CHANGED
@@ -1,24 +1,35 @@
1
1
  # Asanban
2
2
 
3
- TODO: Write a gem description
4
-
5
- ## Installation
6
-
7
- Add this line to your application's Gemfile:
8
-
9
- gem 'asanban'
10
-
11
- And then execute:
12
-
13
- $ bundle
14
-
15
- Or install it yourself as:
16
-
17
- $ gem install asanban
18
-
19
- ## Usage
20
-
21
- TODO: Write usage instructions here
3
+ Tooling for using Asana with a Kanban system.
4
+
5
+ See: http://dev.bizo.com/2013/01/asanban-lean-development-with-asana-and.html
6
+
7
+ ## Installation and Usage
8
+
9
+ 0. Make sure you have ruby 1.9 and rubygems installed and available on your system path.
10
+ 1. Create a directory which will contain a configuration file with your API and DB settings. You can call it "asana" or whatever you like.
11
+ 2. Run: "gem install asanban" (or better yet, use bundler)
12
+ 3. Install MongoDB on your local machine, if you haven't already.
13
+ 4. Provision a production installation of MongoDB. A free account at mongolab should get you started if needed.
14
+ 5. Create a YAML file called asana.yml in the directory you created in step 1. The file should have the following structure:
15
+ - mongodb_uri: mongodb://[user]:[password]@[server]:[port]/[db-name] (Note that this is for the production MongoDB installation)
16
+ - mongodb_dbname: [db-name] (for both environments)
17
+ - asana_api_key: [your Asana API key - available in the Asana UI]
18
+ - asana_workspace_id: [your Asana workspace ID]
19
+ - asana_project_id: [the ID of the Asana project for which you would like to track metrics]
20
+ - asana_beginning_milestone: [the name of the first stage in your Kanban system, eg. Dev Ready]
21
+ - asana_ending_milestone: [the name of the last stage in your Kanban system, eg. Production]
22
+
23
+ 6. Run: "asanban-load local" (the bulk loader). This will create task, milestone and lead time data in your local MongoDB, in the DB with the name given in the configuration file specified above.
24
+ - Note: You must follow the Asana conventions outlined above for the bulk loader to work, particularly naming your priority headers as follows: "{STAGE NAME} ({WIP})" (parens must be specified around WIP limit).
25
+ 7. If the process succeeded and the data created in your local MongoDB looks reasonable, you can go ahead and create the data in your production MongoDB instance by running: "asana-load prod"
26
+ 8. Run: "asanban-service". This starts the web service that will serve up your aggregated lead time data.
27
+ 9. You can hit http://localhost:4567/metrics?aggregate_by=month to see your data in JSON format. (You can also pass in "year" or "day" instead of "month".) There is also a very rough prototype of a page with a raphy-charts graph showing the data from the web service that you can hit by visiting http://localhost:4567/metrics.html . This is just an example of how to consume the data; it needs a lot of work.
28
+ 10. Since the web service is implemented with Sinatra, you will want to create a create a rack-up file if you're going to deploy it to production (or Heroku). Do that by creating a file called config.ru in the directory you created in step 1, which contains the following:
29
+
30
+ - load Gem.bin_path('asanban', 'asanban-service')
31
+
32
+ 11. Schedule the bulk loader in cron to run every night.
22
33
 
23
34
  ## Contributing
24
35
 
data/asanban.gemspec CHANGED
@@ -20,4 +20,6 @@ Gem::Specification.new do |gem|
20
20
  gem.add_dependency "json"
21
21
  gem.add_dependency "json_pure"
22
22
  gem.add_dependency "sinatra"
23
+ gem.add_development_dependency "rspec"
24
+ gem.add_development_dependency "rack-test"
23
25
  end
data/bin/asanban-load CHANGED
@@ -1,153 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
- require "rubygems"
3
- require "json"
4
- require "net/https"
5
- require "mongo"
6
- require "time"
7
- require "yaml"
2
+ require "asanban/bulk_load"
8
3
 
9
- def record_time(task_id, start_story, start_milestone, end_story, end_milestone, collection)
10
- end_story_id = end_story["id"]
11
- start_story_id = start_story["id"]
12
- start_timestamp = Time.parse(start_story["created_at"])
13
- end_timestamp = Time.parse(end_story["created_at"])
14
- elapsed_time_seconds = end_timestamp - start_timestamp
15
- elapsed_days = elapsed_time_seconds / (60.0 * 60.0 * 24.0)
16
- day = "#{end_timestamp.year}-#{end_timestamp.month}-#{end_timestamp.day}"
17
- month = "#{end_timestamp.year}-#{end_timestamp.month}"
18
- year = end_timestamp.year.to_s
19
-
20
- record = {"day" => day, "month" => month, "year" => year, "task_id" => task_id,
21
- "start_milestone" => start_milestone, "end_milestone" => end_milestone,
22
- "start_story_id" => start_story_id, "end_story_id" => end_story_id,
23
- "elapsed_days" => elapsed_days}
24
- collection.remove("end_story_id" => end_story_id)
25
- collection.insert(record)
26
- record
27
- end
28
-
29
- if !File.exists?('asana.yml')
30
- puts "Must specify configuration in asana.yml"
31
- exit(1)
32
- end
33
- config = YAML::load(File.open('asana.yml'))
34
- api_key = config['asana_api_key']
35
- workspace_id = config['asana_workspace_id']
36
- project_id = config['asana_project_id']
37
- mongodb_uri = config['mongodb_uri']
38
- mongodb_dbname = config['mongodb_dbname']
39
-
40
- if (ARGV.count < 1 || ARGV.count > 2)
41
- puts "Syntax is: bulkload {stage} [mode]"
42
- puts "{stage} is 'LOCAL' or 'PROD'"
43
- puts "[mode] can be 'tasks' (to create task data) or 'times' to create milestone and lead time data. Script will do both if not specified."
44
- exit(1)
45
- else
46
- if (ARGV[0].downcase == "local")
47
- conn = Mongo::Connection.new
48
- elsif (ARGV[0].downcase == "prod")
49
- conn = Mongo::Connection.from_uri(mongodb_uri)
50
- else
51
- puts "Invalid stage: #{ARGV[0]}"
52
- exit(1)
53
- end
54
- if mode = ARGV[1] && !['tasks', 'times'].include?(mode)
55
- puts "Invalid mode: #{mode}"
56
- exit(1)
57
- end
58
- end
59
-
60
- db = conn.db(mongodb_dbname)
61
- tasks_collection = db["tasks"]
62
-
63
- if (mode == 'tasks' || !mode)
64
- puts "Creating times..."
65
- uri = URI.parse("https://app.asana.com/api/1.0/projects/#{project_id}/tasks?opt_fields=id,name,assignee,assignee_status,created_at,completed,completed_at,due_on,followers,modified_at,name,notes,projects,parent")
66
- http = Net::HTTP.new(uri.host, uri.port)
67
- http.use_ssl = true
68
- http.verify_mode = OpenSSL::SSL::VERIFY_PEER
69
- header = { "Content-Type" => "application/json" }
70
- path = "#{uri.path}?#{uri.query}"
71
- req = Net::HTTP::Get.new(path, header)
72
- req.basic_auth(api_key, '')
73
- res = http.start { |http| http.request(req) }
74
- tasks = JSON.parse(res.body)
75
-
76
- if tasks['errors']
77
- puts "Server returned an error retrieving tasks: #{tasks['errors'][0]['message']}"
78
- else
79
- #old_tasks = tasks_collection.find({"projects.id" => project_id, "completed" => false}).to_a
80
- tasks['data'].each_with_index do |task, i|
81
- uri = URI.parse("https://app.asana.com/api/1.0/tasks/#{task['id']}/stories")
82
- req = Net::HTTP::Get.new(uri.path, header)
83
- req.basic_auth(api_key, '')
84
- res = http.start { |http| http.request(req) }
85
- stories = JSON.parse(res.body)
86
-
87
- if task['errors']
88
- puts "Server returned an error retrieving stories: #{task['errors'][0]['message']}"
89
- else
90
- task["stories"] = stories['data']
91
- end
92
-
93
- tasks_collection.update({"id" => task['id']}, task, {:upsert => true})
94
- puts "Created task: #{task['id']}"
95
-
96
- if (((i + 1) % 100) == 0)
97
- puts "Sleeping for one minute to avoid Asana's rate limit of 100 requests per minute"
98
- sleep 60
99
- end
100
- end
101
-
102
- puts "Done creating times."
103
- end
104
- end
105
-
106
- if (mode == 'times' || !mode)
107
- puts "Creating milestone and lead time data..."
108
- milestone_times_collection = db["milestone_times"]
109
- lead_times_collection = db["lead_times"]
110
- # TODO: filter - complete_time = null or completed_time - now <= 24hours
111
- tasks_collection.find().each do |task|
112
- stories = task["stories"]
113
- task_id = task["_id"]
114
- stories.each do |story|
115
- if (story['text'] =~ /Moved from (.*)\(\d+\) to (.*)\(\d+\)/)
116
- start_milestone = $1.strip
117
- end_milestone = $2.strip
118
- timestamp = Time.parse(story["created_at"])
119
- day = "#{timestamp.year}-#{timestamp.month}-#{timestamp.day}"
120
- month = "#{timestamp.year}-#{timestamp.month}"
121
- year = timestamp.year.to_s
122
- end_story_id = story["id"]
123
-
124
- if (start_story = stories.find {|s| s['text'] =~ /Moved .*to #{start_milestone}/})
125
- #TODO: Refactor to use record_time
126
- start_story_id = start_story["id"]
127
- start_timestamp = Time.parse(start_story["created_at"])
128
- elapsed_time_seconds = timestamp - start_timestamp
129
- elapsed_days = elapsed_time_seconds / (60.0 * 60.0 * 24.0)
130
-
131
- milestone = {"day" => day, "month" => month, "year" => year, "task_id" => task_id,
132
- "start_milestone" => start_milestone, "end_milestone" => end_milestone,
133
- "start_story_id" => start_story_id, "end_story_id" => end_story_id,
134
- "elapsed_days" => elapsed_days}
135
- milestone_times_collection.remove("end_story_id" => end_story_id)
136
- milestone_times_collection.insert(milestone)
137
- puts "Inserted milestone: #{milestone}"
138
- else
139
- puts "Could not find time task entered #{start_milestone}"
140
- end
141
- end
142
- end
143
-
144
- if ((end_story = stories.find_all {|s| s['text'] =~ /Moved .*to #{config['asana_ending_milestone']}/}[-1]) &&
145
- (start_story = stories.find_all {|s| s['text'] =~ /Moved .*to #{config['asana_beginning_milestone']}/}[0]))
146
- lead_times_collection.remove("end_story_id" => end_story["id"])
147
- lead_time = record_time(task_id, start_story, config['asana_beginning_milestone'], end_story, config['asana_ending_milestone'], lead_times_collection)
148
- puts "Inserted lead time: #{lead_time}"
149
- end
150
- end
151
- puts "Finished creating milestone and lead time data."
152
- end
153
- puts "Done!"
4
+ Asanban::BulkLoad.run()
data/bin/asanban-service CHANGED
@@ -1,3 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  #
3
- require 'asanban/service.rb'
3
+ require 'asanban/service'
4
+
5
+ puts "Running asanban web service"
6
+ Asanban::Service.run!
@@ -0,0 +1,161 @@
1
+ require "rubygems"
2
+ require "json"
3
+ require "net/https"
4
+ require "mongo"
5
+ require "time"
6
+ require "yaml"
7
+
8
+ module Asanban
9
+ class BulkLoad
10
+ def self.run(config = nil)
11
+ unless config
12
+ if !File.exists?('asana.yml')
13
+ puts "Must specify configuration in asana.yml"
14
+ exit(1)
15
+ end
16
+ config = YAML::load(File.open('asana.yml'))
17
+ end
18
+ api_key = config['asana_api_key']
19
+ workspace_id = config['asana_workspace_id']
20
+ project_id = config['asana_project_id']
21
+ mongodb_uri = config['mongodb_uri']
22
+ mongodb_dbname = config['mongodb_dbname']
23
+
24
+ if (ARGV.count < 1 || ARGV.count > 2)
25
+ puts "Syntax is: bulkload {stage} [mode]"
26
+ puts "{stage} is 'LOCAL' or 'PROD'"
27
+ puts "[mode] can be 'tasks' (to create task data) or 'times' to create milestone and lead time data. Script will do both if not specified."
28
+ exit(1)
29
+ else
30
+ if (ARGV[0].downcase == "local")
31
+ conn = Mongo::Connection.new
32
+ elsif (ARGV[0].downcase == "prod")
33
+ conn = Mongo::Connection.from_uri(mongodb_uri)
34
+ else
35
+ puts "Invalid stage: #{ARGV[0]}"
36
+ exit(1)
37
+ end
38
+ if mode = ARGV[1] && !['tasks', 'times'].include?(mode)
39
+ puts "Invalid mode: #{mode}"
40
+ exit(1)
41
+ end
42
+ end
43
+
44
+ db = conn.db(mongodb_dbname)
45
+ tasks_collection = db["tasks"]
46
+
47
+ if (mode == 'tasks' || !mode)
48
+ puts "Creating times..."
49
+ uri = URI.parse("https://app.asana.com/api/1.0/projects/#{project_id}/tasks?opt_fields=id,name,assignee,assignee_status,created_at,completed,completed_at,due_on,followers,modified_at,name,notes,projects,parent")
50
+ http = Net::HTTP.new(uri.host, uri.port)
51
+ http.use_ssl = true
52
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
53
+ header = { "Content-Type" => "application/json" }
54
+ path = "#{uri.path}?#{uri.query}"
55
+ req = Net::HTTP::Get.new(path, header)
56
+ req.basic_auth(api_key, '')
57
+ res = http.start { |http| http.request(req) }
58
+ tasks = JSON.parse(res.body)
59
+
60
+ if tasks['errors']
61
+ puts "Server returned an error retrieving tasks: #{tasks['errors'][0]['message']}"
62
+ else
63
+ #old_tasks = tasks_collection.find({"projects.id" => project_id, "completed" => false}).to_a
64
+ tasks['data'].each_with_index do |task, i|
65
+ uri = URI.parse("https://app.asana.com/api/1.0/tasks/#{task['id']}/stories")
66
+ req = Net::HTTP::Get.new(uri.path, header)
67
+ req.basic_auth(api_key, '')
68
+ res = http.start { |http| http.request(req) }
69
+ stories = JSON.parse(res.body)
70
+
71
+ if task['errors']
72
+ puts "Server returned an error retrieving stories: #{task['errors'][0]['message']}"
73
+ else
74
+ task["stories"] = stories['data']
75
+ end
76
+
77
+ tasks_collection.update({"id" => task['id']}, task, {:upsert => true})
78
+ puts "Created task: #{task['id']}"
79
+
80
+ if (((i + 1) % 100) == 0)
81
+ puts "Sleeping for one minute to avoid Asana's rate limit of 100 requests per minute"
82
+ sleep 60
83
+ end
84
+ end
85
+
86
+ puts "Done creating times."
87
+ end
88
+ end
89
+
90
+ if (mode == 'times' || !mode)
91
+ puts "Creating milestone and lead time data..."
92
+ milestone_times_collection = db["milestone_times"]
93
+ lead_times_collection = db["lead_times"]
94
+ # TODO: filter - complete_time = null or completed_time - now <= 24hours
95
+ tasks_collection.find().each do |task|
96
+ stories = task["stories"]
97
+ task_id = task["_id"]
98
+ stories.each do |story|
99
+ if (story['text'] =~ /Moved from (.*)\(\d+\) to (.*)\(\d+\)/)
100
+ start_milestone = $1.strip
101
+ end_milestone = $2.strip
102
+ timestamp = Time.parse(story["created_at"])
103
+ day = "#{timestamp.year}-#{timestamp.month}-#{timestamp.day}"
104
+ month = "#{timestamp.year}-#{timestamp.month}"
105
+ year = timestamp.year.to_s
106
+ end_story_id = story["id"]
107
+
108
+ if (start_story = stories.find {|s| s['text'] =~ /Moved .*to #{start_milestone}/})
109
+ #TODO: Refactor to use record_time
110
+ start_story_id = start_story["id"]
111
+ start_timestamp = Time.parse(start_story["created_at"])
112
+ elapsed_time_seconds = timestamp - start_timestamp
113
+ elapsed_days = elapsed_time_seconds / (60.0 * 60.0 * 24.0)
114
+
115
+ milestone = {"day" => day, "month" => month, "year" => year, "task_id" => task_id,
116
+ "start_milestone" => start_milestone, "end_milestone" => end_milestone,
117
+ "start_story_id" => start_story_id, "end_story_id" => end_story_id,
118
+ "elapsed_days" => elapsed_days}
119
+ milestone_times_collection.remove("end_story_id" => end_story_id)
120
+ milestone_times_collection.insert(milestone)
121
+ puts "Inserted milestone: #{milestone}"
122
+ else
123
+ puts "Could not find time task entered #{start_milestone}"
124
+ end
125
+ end
126
+ end
127
+
128
+ if ((end_story = stories.find_all {|s| s['text'] =~ /Moved .*to #{config['asana_ending_milestone']}/}[-1]) &&
129
+ (start_story = stories.find_all {|s| s['text'] =~ /Moved .*to #{config['asana_beginning_milestone']}/}[0]))
130
+ lead_times_collection.remove("end_story_id" => end_story["id"])
131
+ lead_time = record_time(task_id, start_story, config['asana_beginning_milestone'], end_story, config['asana_ending_milestone'], lead_times_collection)
132
+ puts "Inserted lead time: #{lead_time}"
133
+ end
134
+ end
135
+ puts "Finished creating milestone and lead time data."
136
+ end
137
+ puts "Done!"
138
+ end
139
+
140
+ #Visible For Testing
141
+ def self.record_time(task_id, start_story, start_milestone, end_story, end_milestone, collection)
142
+ end_story_id = end_story["id"]
143
+ start_story_id = start_story["id"]
144
+ start_timestamp = Time.parse(start_story["created_at"])
145
+ end_timestamp = Time.parse(end_story["created_at"])
146
+ elapsed_time_seconds = end_timestamp - start_timestamp
147
+ elapsed_days = elapsed_time_seconds / (60.0 * 60.0 * 24.0)
148
+ day = "#{end_timestamp.year}-#{end_timestamp.month}-#{end_timestamp.day}"
149
+ month = "#{end_timestamp.year}-#{end_timestamp.month}"
150
+ year = end_timestamp.year.to_s
151
+
152
+ record = {"day" => day, "month" => month, "year" => year, "task_id" => task_id,
153
+ "start_milestone" => start_milestone, "end_milestone" => end_milestone,
154
+ "start_story_id" => start_story_id, "end_story_id" => end_story_id,
155
+ "elapsed_days" => elapsed_days}
156
+ collection.remove("end_story_id" => end_story_id)
157
+ collection.insert(record)
158
+ record
159
+ end
160
+ end
161
+ end
@@ -9,13 +9,16 @@ module Asanban
9
9
  set :public_folder, File.expand_path(File.join('..', '..', '..', 'static'), __FILE__)
10
10
  set :port, ENV['PORT'] if ENV['PORT'] #set by Heroku
11
11
 
12
- if !File.exists?('asana.yml')
13
- puts "Must specify configuration in asana.yml"
14
- exit(1)
12
+ configure :production, :development do
13
+ if !File.exists?('asana.yml')
14
+ puts "Must specify configuration in asana.yml"
15
+ exit(1)
16
+ end
17
+ set :config, YAML::load(File.open('asana.yml'))
15
18
  end
16
- config = YAML::load(File.open('asana.yml'))
17
19
 
18
20
  get '/metrics' do
21
+ config = settings.config
19
22
  conn = Mongo::Connection.from_uri(config['mongodb_uri'])
20
23
  db = conn.db(config["mongodb_dbname"])
21
24
  aggregate_by = params[:aggregate_by]
@@ -49,13 +52,12 @@ module Asanban
49
52
 
50
53
  content_type :json
51
54
  results.find().map do |result|
52
- date_str = result['_id']
53
- date_str[0,2] = "" if aggregate_by == "month"
54
- [date_str.sub("-","").to_i, result["value"]["avg"]]
55
- end.sort {|r| r[0]}.to_json
55
+ [result['_id'], result["value"]["avg"]]
56
+ end.sort {|a, b| datestring_to_int(a[0]) <=> datestring_to_int(b[0])}.to_json
56
57
  end
57
- end
58
58
 
59
- puts "Running asanban web service"
60
- Service.run!
59
+ def datestring_to_int(datestring)
60
+ datestring.split("-").map {|part| part.length == 1 ? "0" + part : part}.join("").to_i
61
+ end
62
+ end
61
63
  end
@@ -1,3 +1,3 @@
1
1
  module Asanban
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.4"
3
3
  end
@@ -0,0 +1,90 @@
1
+ ENV['RACK_ENV'] = 'test'
2
+ require 'asanban/service'
3
+ require 'asanban/bulk_load'
4
+ require 'rspec'
5
+ require 'rack/test'
6
+
7
+ describe Asanban::Service do
8
+ include Rack::Test::Methods
9
+
10
+ before do
11
+ config = { "mongodb_uri" => "mongodb://localhost/", "mongodb_dbname" => "asana_test" }
12
+ app.settings.set(:config, config)
13
+ conn = Mongo::Connection.from_uri(config['mongodb_uri'])
14
+ db = conn.db(config["mongodb_dbname"])
15
+ @milestone_times_collection = db["milestone_times"]
16
+ @milestone_times_collection.drop
17
+ @lead_times_collection = db["lead_times"]
18
+ @lead_times_collection.drop
19
+ end
20
+
21
+ it "returns average lead times by year" do
22
+ record_lead_time('Foo', "October 2, 2012", "October 14, 2012") #12 days
23
+ record_lead_time('Bar', "May 12, 2013", "May 14, 2013") #2 days
24
+ record_lead_time('Baz', "October 12, 2013", "October 16, 2013") #4 days
25
+
26
+ get '/metrics', :aggregate_by => 'year'
27
+ #Average: 12 days 2012, 3 days 2013
28
+ expect(last_response.body).to eq('[["2012",12.0],["2013",3.0]]')
29
+ end
30
+
31
+ it "returns average lead times by month" do
32
+ record_lead_time('Foo', "September 20, 2013", "September 30, 2013") #10 days
33
+ record_lead_time('Bar', "October 2, 2013", "October 14, 2013") #12 days
34
+ record_lead_time('Baz', "October 12, 2013", "October 16, 2013") #4 days
35
+
36
+ get '/metrics', :aggregate_by => 'month'
37
+ #Average: 10 days September, 8 days in October
38
+ expect(last_response.body).to eq('[["2013-9",10.0],["2013-10",8.0]]')
39
+ end
40
+
41
+ it "returns average lead times by day" do
42
+ record_lead_time('Foo', "October 2, 2013", "October 14, 2013") #12 days
43
+ record_lead_time('Bar', "October 12, 2013", "October 14, 2013") #2 days
44
+ record_lead_time('Baz', "October 12, 2013", "October 16, 2013") #4 days
45
+
46
+ get '/metrics', :aggregate_by => 'day'
47
+ #Average: 7 days 10/14, 4 days 10/16
48
+ expect(last_response.body).to eq('[["2013-10-14",7.0],["2013-10-16",4.0]]')
49
+ end
50
+
51
+ describe "milestone times recorded" do
52
+ before do
53
+ record_time('Foo', "September 1, 2013", "September 3, 2013", 'Dev Ready', 'Dev In Progress', @milestone_times_collection) #2
54
+ record_time('Foo', "September 3, 2013", "September 10, 2013", 'Dev In Progress', 'Dev Done', @milestone_times_collection) #7
55
+ record_time('Foo', "September 10, 2013", "September 15, 2013", 'Dev Done', 'PM Test, in Dev', @milestone_times_collection) #5
56
+ record_time('Bar', "September 1, 2013", "September 7, 2013", 'Dev Ready', 'Dev In Progress', @milestone_times_collection) #6
57
+ record_time('Bar', "September 7, 2013", "September 12, 2013", 'Dev In Progress', 'Dev Done', @milestone_times_collection) #5
58
+ record_time('Bar', "September 12, 2013", "September 15, 2013", 'Dev Done', 'PM Test, in Dev', @milestone_times_collection) #3
59
+ end
60
+
61
+ it "returns average time in 'Dev Ready' by month" do
62
+ get '/metrics', :aggregate_by => 'month', :milestone => 'Dev Ready'
63
+ expect(last_response.body).to eq('[["2013-9",4.0]]')
64
+ end
65
+
66
+ it "returns average time in 'Dev In Progress' by month" do
67
+ get '/metrics', :aggregate_by => 'month', :milestone => 'Dev In Progress'
68
+ expect(last_response.body).to eq('[["2013-9",6.0]]')
69
+ end
70
+ end
71
+
72
+ it "returns an error when an invalid aggregation is specified" do
73
+ get '/metrics', :aggregate_by => 'engineer'
74
+ expect(last_response.status).to eq(400)
75
+ end
76
+
77
+ def app
78
+ Asanban::Service
79
+ end
80
+
81
+ def record_lead_time(task_id, started_at, ended_at)
82
+ record_time(task_id, started_at, ended_at, 'Dev Ready', 'Production', @lead_times_collection)
83
+ end
84
+
85
+ def record_time(task_id, started_at, ended_at, start_milestone, end_milestone, collection)
86
+ start_story = {'id' => "#{task_id}_#{start_milestone}", 'created_at' => started_at}
87
+ end_story = {'id' => "#{task_id}_#{end_milestone}", 'created_at' => ended_at}
88
+ Asanban::BulkLoad.record_time(task_id, start_story, start_milestone, end_story, end_milestone, collection)
89
+ end
90
+ end
data/static/metrics.html CHANGED
@@ -1,14 +1,17 @@
1
1
  <html>
2
2
  <head>
3
3
  <script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.8.3.min.js"></script>
4
- <script src="https://raw.github.com/DmitryBaranovskiy/raphael/master/raphael-min.js"></script>
5
- <script src="https://raw.github.com/jcarver989/raphy-charts/master/compiled/charts.min.js"></script>
4
+ <script src="https://rawgithub.com/DmitryBaranovskiy/raphael/master/raphael-min.js"></script>
5
+ <script src="https://rawgithub.com/jcarver989/raphy-charts/master/compiled/charts.min.js"></script>
6
6
  <script type="text/javascript">
7
7
  $(document).ready(function() {
8
- var chart = new Charts.LineChart('leadtime_chart');
8
+ var chart = new Charts.LineChart('leadtime_chart', { label_format: "%m/%d/%Y" });
9
9
  $.get("/metrics?aggregate_by=month", function(metric_data) {
10
+ var data = metric_data.map(function(item) {
11
+ return [parse(item[0]), item[1]];
12
+ });
10
13
  chart.add_line({
11
- data: metric_data,
14
+ data: data,
12
15
  options: {
13
16
  line_color: "#00aadd",
14
17
  x_label_size: 40
@@ -16,6 +19,11 @@
16
19
  });
17
20
  chart.draw();
18
21
  });
22
+
23
+ function parse(month_str) {
24
+ var month_parts = month_str.split("-");
25
+ return new Date(month_parts[0], month_parts[1] - 1, "1");
26
+ }
19
27
  });
20
28
  </script>
21
29
  </head>
metadata CHANGED
@@ -1,60 +1,99 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: asanban
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
5
- prerelease:
4
+ version: 0.0.4
6
5
  platform: ruby
7
6
  authors:
8
7
  - Pat Gannon
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2013-01-19 00:00:00.000000000 Z
11
+ date: 2013-10-30 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: mongo
16
- requirement: &70269646184500 !ruby/object:Gem::Requirement
17
- none: false
15
+ requirement: !ruby/object:Gem::Requirement
18
16
  requirements:
19
17
  - - ! '>='
20
18
  - !ruby/object:Gem::Version
21
19
  version: '0'
22
20
  type: :runtime
23
21
  prerelease: false
24
- version_requirements: *70269646184500
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ! '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
25
27
  - !ruby/object:Gem::Dependency
26
28
  name: json
27
- requirement: &70269646184060 !ruby/object:Gem::Requirement
28
- none: false
29
+ requirement: !ruby/object:Gem::Requirement
29
30
  requirements:
30
31
  - - ! '>='
31
32
  - !ruby/object:Gem::Version
32
33
  version: '0'
33
34
  type: :runtime
34
35
  prerelease: false
35
- version_requirements: *70269646184060
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ! '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
36
41
  - !ruby/object:Gem::Dependency
37
42
  name: json_pure
38
- requirement: &70269646183640 !ruby/object:Gem::Requirement
39
- none: false
43
+ requirement: !ruby/object:Gem::Requirement
40
44
  requirements:
41
45
  - - ! '>='
42
46
  - !ruby/object:Gem::Version
43
47
  version: '0'
44
48
  type: :runtime
45
49
  prerelease: false
46
- version_requirements: *70269646183640
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
47
55
  - !ruby/object:Gem::Dependency
48
56
  name: sinatra
49
- requirement: &70269646183180 !ruby/object:Gem::Requirement
50
- none: false
57
+ requirement: !ruby/object:Gem::Requirement
51
58
  requirements:
52
59
  - - ! '>='
53
60
  - !ruby/object:Gem::Version
54
61
  version: '0'
55
62
  type: :runtime
56
63
  prerelease: false
57
- version_requirements: *70269646183180
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ! '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ! '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ! '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rack-test
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ! '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ! '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
58
97
  description: ''
59
98
  email:
60
99
  - gannon@bizo.com
@@ -73,31 +112,33 @@ files:
73
112
  - bin/asanban-load
74
113
  - bin/asanban-service
75
114
  - lib/asanban.rb
115
+ - lib/asanban/bulk_load.rb
76
116
  - lib/asanban/service.rb
77
117
  - lib/asanban/version.rb
118
+ - spec/service_spec.rb
78
119
  - static/metrics.html
79
120
  homepage: ''
80
121
  licenses: []
122
+ metadata: {}
81
123
  post_install_message:
82
124
  rdoc_options: []
83
125
  require_paths:
84
126
  - lib
85
127
  required_ruby_version: !ruby/object:Gem::Requirement
86
- none: false
87
128
  requirements:
88
129
  - - ! '>='
89
130
  - !ruby/object:Gem::Version
90
131
  version: '0'
91
132
  required_rubygems_version: !ruby/object:Gem::Requirement
92
- none: false
93
133
  requirements:
94
134
  - - ! '>='
95
135
  - !ruby/object:Gem::Version
96
136
  version: '0'
97
137
  requirements: []
98
138
  rubyforge_project:
99
- rubygems_version: 1.8.6
139
+ rubygems_version: 2.1.4
100
140
  signing_key:
101
- specification_version: 3
141
+ specification_version: 4
102
142
  summary: Track Kanban metrics using Asana as a data source
103
- test_files: []
143
+ test_files:
144
+ - spec/service_spec.rb