asanban 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in asanban.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Pat Gannon
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # Asanban
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
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/asanban.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'asanban/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "asanban"
8
+ gem.version = Asanban::VERSION
9
+ gem.authors = ["Pat Gannon"]
10
+ gem.email = ["gannon@bizo.com"]
11
+ gem.description = %q{}
12
+ gem.summary = %q{Track Kanban metrics using Asana as a data source}
13
+ gem.homepage = ""
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+ gem.add_dependency "mongo"
20
+ gem.add_dependency "json"
21
+ gem.add_dependency "json_pure"
22
+ gem.add_dependency "sinatra"
23
+ end
data/bin/asanban-load ADDED
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env ruby
2
+ require "rubygems"
3
+ require "json"
4
+ require "net/https"
5
+ require "mongo"
6
+ require "time"
7
+ require "yaml"
8
+
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!"
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ require 'asanban/service.rb'
@@ -0,0 +1,60 @@
1
+ require "rubygems"
2
+ require "json"
3
+ require "mongo"
4
+ require "sinatra"
5
+ require "yaml"
6
+
7
+ module Asanban
8
+ class Service < Sinatra::Base
9
+ set :public_folder, File.expand_path(File.join('..', '..', '..', 'static'), __FILE__)
10
+
11
+ if !File.exists?('asana.yml')
12
+ puts "Must specify configuration in asana.yml"
13
+ exit(1)
14
+ end
15
+ config = YAML::load(File.open('asana.yml'))
16
+
17
+ get '/metrics' do
18
+ conn = Mongo::Connection.from_uri(config['mongodb_uri'])
19
+ db = conn.db(config["mongodb_dbname"])
20
+ aggregate_by = params[:aggregate_by]
21
+ return [400, "Cannot aggregate by #{aggregate_by}"] unless ["year", "month", "day"].include? aggregate_by
22
+
23
+ map_function = "function() { emit(this.#{aggregate_by}, { count: 1, elapsed_days: this.elapsed_days }); };"
24
+
25
+ reduce_function = "function (name, values){
26
+ var n = {count : 0, elapsed_days : 0};
27
+ for ( var i=0; i<values.length; i++ ){
28
+ n.count += values[i].count;
29
+ n.elapsed_days += values[i].elapsed_days;
30
+ }
31
+ return n;
32
+ };"
33
+
34
+ finalize_function = "function(who, res){
35
+ res.avg = res.elapsed_days / res.count;
36
+ return res;
37
+ };"
38
+
39
+ map_reduce_options = {:finalize => finalize_function, :out => "mr_results"}
40
+ if (milestone = params[:milestone])
41
+ map_reduce_options[:query] = {"start_milestone" => milestone}
42
+ collection = db["milestone_times"]
43
+ else
44
+ collection = db["lead_times"]
45
+ end
46
+
47
+ results = collection.map_reduce(map_function, reduce_function, map_reduce_options)
48
+
49
+ content_type :json
50
+ results.find().map do |result|
51
+ date_str = result['_id']
52
+ date_str[0,2] = "" if aggregate_by == "month"
53
+ [date_str.sub("-","").to_i, result["value"]["avg"]]
54
+ end.sort {|r| r[0]}.to_json
55
+ end
56
+ end
57
+
58
+ puts "Running asanban web service"
59
+ Service.run!
60
+ end
@@ -0,0 +1,3 @@
1
+ module Asanban
2
+ VERSION = "0.0.1"
3
+ end
data/lib/asanban.rb ADDED
@@ -0,0 +1,5 @@
1
+ require "asanban/version"
2
+
3
+ module Asanban
4
+ # Your code goes here...
5
+ end
@@ -0,0 +1,26 @@
1
+ <html>
2
+ <head>
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>
6
+ <script type="text/javascript">
7
+ $(document).ready(function() {
8
+ var chart = new Charts.LineChart('leadtime_chart');
9
+ $.get("/metrics?aggregate_by=month", function(metric_data) {
10
+ chart.add_line({
11
+ data: metric_data,
12
+ options: {
13
+ line_color: "#00aadd",
14
+ x_label_size: 40
15
+ }
16
+ });
17
+ chart.draw();
18
+ });
19
+ });
20
+ </script>
21
+ </head>
22
+ <body>
23
+ <h1>Lead Times</h1>
24
+ <div id='leadtime_chart' style='width: 600px; height: 300px;'></div>
25
+ </body>
26
+ </html>
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: asanban
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Pat Gannon
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-01-19 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: mongo
16
+ requirement: &70187454070360 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70187454070360
25
+ - !ruby/object:Gem::Dependency
26
+ name: json
27
+ requirement: &70187454069880 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70187454069880
36
+ - !ruby/object:Gem::Dependency
37
+ name: json_pure
38
+ requirement: &70187454069460 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *70187454069460
47
+ - !ruby/object:Gem::Dependency
48
+ name: sinatra
49
+ requirement: &70187454069020 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *70187454069020
58
+ description: ''
59
+ email:
60
+ - gannon@bizo.com
61
+ executables:
62
+ - asanban-load
63
+ - asanban-service
64
+ extensions: []
65
+ extra_rdoc_files: []
66
+ files:
67
+ - .gitignore
68
+ - Gemfile
69
+ - LICENSE.txt
70
+ - README.md
71
+ - Rakefile
72
+ - asanban.gemspec
73
+ - bin/asanban-load
74
+ - bin/asanban-service
75
+ - lib/asanban.rb
76
+ - lib/asanban/service.rb
77
+ - lib/asanban/version.rb
78
+ - static/metrics.html
79
+ homepage: ''
80
+ licenses: []
81
+ post_install_message:
82
+ rdoc_options: []
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ! '>='
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ required_rubygems_version: !ruby/object:Gem::Requirement
92
+ none: false
93
+ requirements:
94
+ - - ! '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ requirements: []
98
+ rubyforge_project:
99
+ rubygems_version: 1.8.6
100
+ signing_key:
101
+ specification_version: 3
102
+ summary: Track Kanban metrics using Asana as a data source
103
+ test_files: []