afterburn 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/MIT-LICENSE +20 -0
- data/README.rdoc +3 -0
- data/Rakefile +49 -0
- data/bin/burn +91 -0
- data/lib/afterburn.rb +35 -0
- data/lib/afterburn/authorization.rb +36 -0
- data/lib/afterburn/board.rb +22 -0
- data/lib/afterburn/board_interval.rb +47 -0
- data/lib/afterburn/engine.rb +11 -0
- data/lib/afterburn/helpers.rb +54 -0
- data/lib/afterburn/list.rb +98 -0
- data/lib/afterburn/list_interval_series.rb +41 -0
- data/lib/afterburn/list_metric.rb +39 -0
- data/lib/afterburn/member.rb +51 -0
- data/lib/afterburn/project.rb +100 -0
- data/lib/afterburn/redis_connection.rb +39 -0
- data/lib/afterburn/server.rb +103 -0
- data/lib/afterburn/server/config/setup.example.rb +12 -0
- data/lib/afterburn/server/config/setup.rb +7 -0
- data/lib/afterburn/server/public/afterburn.js +66 -0
- data/lib/afterburn/server/public/highcharts.js +239 -0
- data/lib/afterburn/server/public/jquery-1.7.min.js +4 -0
- data/lib/afterburn/server/public/underscore-1.3.3.min.js +32 -0
- data/lib/afterburn/server/views/edit_project.erb +53 -0
- data/lib/afterburn/server/views/layout.erb +39 -0
- data/lib/afterburn/server/views/members.erb +26 -0
- data/lib/afterburn/server/views/overview.erb +11 -0
- data/lib/afterburn/trello_object_wrapper.rb +82 -0
- data/lib/afterburn/version.rb +3 -0
- data/lib/tasks/afterburn_tasks.rake +17 -0
- metadata +194 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'matrix'
|
2
|
+
|
3
|
+
# TODO test
|
4
|
+
module Afterburn
|
5
|
+
class ListIntervalSeries
|
6
|
+
def initialize(lists, timestamps)
|
7
|
+
@lists, @timestamps = lists, timestamps
|
8
|
+
end
|
9
|
+
|
10
|
+
# deploy_list_counts + wip_list_counts + completed_list_counts
|
11
|
+
def to_json
|
12
|
+
aggregate(backlog_lists, :name => List::Role::BACKLOG) +
|
13
|
+
map(wip_lists) +
|
14
|
+
aggregate(deployed_lists, :name => List::Role::DEPLOYED)
|
15
|
+
end
|
16
|
+
|
17
|
+
def aggregate(lists, opts = {})
|
18
|
+
vectors = lists.map { |list| list.timestamp_count_vector(@timestamps) }
|
19
|
+
[{ "name" => opts[:name], "data" => vectors.inject(&:+).to_a }]
|
20
|
+
end
|
21
|
+
|
22
|
+
def map(lists)
|
23
|
+
lists.map do |list|
|
24
|
+
{ "name" => list.name, "data" => list.timestamp_count_vector(@timestamps).to_a }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def backlog_lists
|
29
|
+
@lists.select { |list| list.role == List::Role::BACKLOG }
|
30
|
+
end
|
31
|
+
|
32
|
+
def wip_lists
|
33
|
+
@lists.select { |list| list.role == List::Role::WIP }
|
34
|
+
end
|
35
|
+
|
36
|
+
def deployed_lists
|
37
|
+
@lists.select { |list| list.role == List::Role::DEPLOYED }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'matrix'
|
3
|
+
require 'redis/objects'
|
4
|
+
|
5
|
+
module Afterburn
|
6
|
+
class ListMetric
|
7
|
+
include Redis::Objects
|
8
|
+
|
9
|
+
attr_reader :timestamp
|
10
|
+
counter :card_count
|
11
|
+
|
12
|
+
def self.for_timestamp(lists, timestamp)
|
13
|
+
lists.map { |list| new(list, timestamp) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.for_list(list, timestamps)
|
17
|
+
timestamps.map { |timestamp| new(list, timestamp) }
|
18
|
+
end
|
19
|
+
|
20
|
+
# TODO test
|
21
|
+
def self.timestamp_count_vector(list, timestamps)
|
22
|
+
Vector[*for_list(list, timestamps).map { |metric| metric.card_count.to_i }]
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(list, timestamp = Time.now)
|
26
|
+
@list = list
|
27
|
+
@timestamp = timestamp
|
28
|
+
end
|
29
|
+
|
30
|
+
def id
|
31
|
+
@id ||= Base64.encode64("#{@list.id}:#{@timestamp.to_i}")
|
32
|
+
end
|
33
|
+
|
34
|
+
def count!
|
35
|
+
card_count.incr(@list.card_count)
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Afterburn
|
2
|
+
class Member < TrelloObjectWrapper
|
3
|
+
wrap :member
|
4
|
+
|
5
|
+
set :member_id_set, :global => true
|
6
|
+
|
7
|
+
%w[trello_user_key trello_user_secret trello_app_token].each do |trello_key|
|
8
|
+
value "#{trello_key}_value"
|
9
|
+
|
10
|
+
define_method(trello_key) do
|
11
|
+
send("#{trello_key}_value").value
|
12
|
+
end
|
13
|
+
|
14
|
+
define_method("#{trello_key}=") do |value|
|
15
|
+
send("#{trello_key}_value").value = value
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.ids
|
20
|
+
member_id_set.members
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.all
|
24
|
+
ids.map { |id| Member.find(id) }
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.first
|
28
|
+
Member.find(ids.first)
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.add_member(member)
|
32
|
+
member_id_set << member.id
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.clear
|
36
|
+
member_id_set.clear
|
37
|
+
end
|
38
|
+
|
39
|
+
def boards
|
40
|
+
trello_member.boards.map { |trello_board| Board.initialize_from_trello_object(trello_board) }
|
41
|
+
end
|
42
|
+
|
43
|
+
def name
|
44
|
+
trello_member.username
|
45
|
+
end
|
46
|
+
|
47
|
+
# export TRELLO_USER_KEY=3dca2797d175d70a1252cb502a5e49b9
|
48
|
+
# export TRELLO_USER_SECRET=1532c3edcd355ec3bd7767ab0ac4da190351bed6174358139d284f3d67d978df
|
49
|
+
# export TRELLO_APP_TOKEN=3aab0899ba564f58c610ce326fbdd3375a206831e7ba68dc95ef3319e90e8170
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'afterburn'
|
2
|
+
require 'redis/objects'
|
3
|
+
|
4
|
+
module Afterburn
|
5
|
+
class Project
|
6
|
+
include Redis::Objects
|
7
|
+
|
8
|
+
value :redis_name_value
|
9
|
+
value :enabled_value
|
10
|
+
sorted_set :interval_set
|
11
|
+
|
12
|
+
attr_reader :id
|
13
|
+
|
14
|
+
def self.by_member_name(member_name)
|
15
|
+
Board.fetch_by_member(member_name).map { |board| Project.new(board) }
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.by_member(member)
|
19
|
+
member.boards.map { |board| Project.new(board) }
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.find(id)
|
23
|
+
new(Board.find(id))
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_reader :board
|
27
|
+
def initialize(board)
|
28
|
+
@board = board
|
29
|
+
end
|
30
|
+
|
31
|
+
def id
|
32
|
+
@id ||= @board.id
|
33
|
+
end
|
34
|
+
|
35
|
+
def name=(name)
|
36
|
+
redis_name_value.value = name
|
37
|
+
end
|
38
|
+
|
39
|
+
def name
|
40
|
+
redis_name_value.value || @board.name
|
41
|
+
end
|
42
|
+
|
43
|
+
def enable!
|
44
|
+
enabled_value.value = "1"
|
45
|
+
end
|
46
|
+
|
47
|
+
def disable!
|
48
|
+
enabled_value.value = nil
|
49
|
+
end
|
50
|
+
|
51
|
+
def enable=(enabled)
|
52
|
+
enabled == "1" ? enable! : disable!
|
53
|
+
end
|
54
|
+
|
55
|
+
def enabled?
|
56
|
+
!!enabled_value.value
|
57
|
+
end
|
58
|
+
|
59
|
+
def lists
|
60
|
+
@board.lists
|
61
|
+
end
|
62
|
+
|
63
|
+
# TODO test
|
64
|
+
def record_interval
|
65
|
+
Time.now.tap do |timestamp|
|
66
|
+
interval = BoardInterval.record(@board, timestamp)
|
67
|
+
interval_set[interval.id] = timestamp.to_i
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# TODO handle BoardIntervals not found
|
72
|
+
def intervals
|
73
|
+
@intervals ||= BoardInterval.find_all(interval_set.members)
|
74
|
+
end
|
75
|
+
|
76
|
+
def to_json
|
77
|
+
{}.tap do |hash|
|
78
|
+
hash['id'] = id
|
79
|
+
hash['name'] = name
|
80
|
+
hash['categories'] = interval_timestamps.map(&:to_date)
|
81
|
+
hash['series'] = interval_series_json
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def interval_timestamps
|
86
|
+
intervals.map(&:timestamp)
|
87
|
+
end
|
88
|
+
|
89
|
+
def interval_series_json
|
90
|
+
ListIntervalSeries.new(lists, interval_timestamps).to_json
|
91
|
+
end
|
92
|
+
|
93
|
+
def update_attributes(attributes)
|
94
|
+
attributes.each do |key, value|
|
95
|
+
send("#{key}=", value)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'redis'
|
2
|
+
|
3
|
+
module Afterburn
|
4
|
+
module RedisConnection
|
5
|
+
|
6
|
+
# Accepts:
|
7
|
+
# 1. A 'hostname:port' String
|
8
|
+
# 2. A 'hostname:port:db' String (to select the Redis db)
|
9
|
+
# 3. A Redis URL String 'redis://host:port'
|
10
|
+
# 4. An instance of `Redis`, `Redis::Client`, `Redis::DistRedis`
|
11
|
+
def redis=(server)
|
12
|
+
case server
|
13
|
+
when String
|
14
|
+
if server =~ /redis\:\/\//
|
15
|
+
redis = Redis.connect(:url => server, :thread_safe => true)
|
16
|
+
else
|
17
|
+
host, port, db = server.split(':')
|
18
|
+
redis = Redis.new(:host => host, :port => port,
|
19
|
+
:thread_safe => true, :db => db)
|
20
|
+
end
|
21
|
+
|
22
|
+
@redis = redis
|
23
|
+
else
|
24
|
+
@redis = server
|
25
|
+
end
|
26
|
+
|
27
|
+
@redis
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns the current Redis connection. If none has been created, will
|
31
|
+
# create a new one.
|
32
|
+
def redis
|
33
|
+
return @redis if @redis
|
34
|
+
self.redis = Redis.respond_to?(:connect) ? Redis.connect : "localhost:6379"
|
35
|
+
self.redis
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'erb'
|
3
|
+
require "rack/csrf"
|
4
|
+
require 'afterburn'
|
5
|
+
require 'afterburn/version'
|
6
|
+
|
7
|
+
module Afterburn
|
8
|
+
class Server < Sinatra::Base
|
9
|
+
dir = File.dirname(File.expand_path(__FILE__))
|
10
|
+
|
11
|
+
set :views, "#{dir}/server/views"
|
12
|
+
set :public_folder, "#{dir}/server/public"
|
13
|
+
set :static, true
|
14
|
+
|
15
|
+
helpers do
|
16
|
+
include Rack::Utils
|
17
|
+
alias_method :h, :escape_html
|
18
|
+
|
19
|
+
def current_section
|
20
|
+
url_path request.path_info.sub('/','').split('/')[0].downcase
|
21
|
+
end
|
22
|
+
|
23
|
+
def current_page
|
24
|
+
url_path request.path_info.sub('/','')
|
25
|
+
end
|
26
|
+
|
27
|
+
def url_path(*path_parts)
|
28
|
+
[ path_prefix, path_parts ].join("/").squeeze('/')
|
29
|
+
end
|
30
|
+
alias_method :u, :url_path
|
31
|
+
|
32
|
+
def path_prefix
|
33
|
+
request.env['SCRIPT_NAME']
|
34
|
+
end
|
35
|
+
|
36
|
+
def partial(template, local_vars = {})
|
37
|
+
erb(template.to_sym, {:layout => false}, local_vars)
|
38
|
+
end
|
39
|
+
|
40
|
+
def afterburn
|
41
|
+
Afterburn
|
42
|
+
end
|
43
|
+
|
44
|
+
def current_member
|
45
|
+
Afterburn.current_member
|
46
|
+
end
|
47
|
+
|
48
|
+
def current_projects
|
49
|
+
[Afterburn.current_projects.first]
|
50
|
+
end
|
51
|
+
|
52
|
+
def csrf_token
|
53
|
+
Rack::Csrf.csrf_token(env)
|
54
|
+
end
|
55
|
+
|
56
|
+
def csrf_tag
|
57
|
+
Rack::Csrf.csrf_tag(env)
|
58
|
+
end
|
59
|
+
|
60
|
+
def csrt_metatag
|
61
|
+
Rack::Csrf.metatag(env)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def show(page, options = {})
|
66
|
+
response["Cache-Control"] = "max-age=0, private, must-revalidate"
|
67
|
+
begin
|
68
|
+
erb page.to_sym, options
|
69
|
+
rescue Errno::ECONNREFUSED
|
70
|
+
erb :error, {:layout => false}, :error => "Can't connect to Redis! (#{Resque.redis_id})"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
get "/" do
|
75
|
+
if current_member
|
76
|
+
show :overview
|
77
|
+
else
|
78
|
+
show :members
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
post "/members" do
|
83
|
+
member_attrs = params[:member]
|
84
|
+
raise Afterburn::Member.find(member_attrs[:name])
|
85
|
+
end
|
86
|
+
|
87
|
+
get "/projects/:id/edit" do
|
88
|
+
show :edit_project, locals: { project: Afterburn::Project.find(params[:id]) }
|
89
|
+
end
|
90
|
+
|
91
|
+
post "/projects/:id" do
|
92
|
+
puts params.inspect
|
93
|
+
Project.find(params[:id]).update_attributes(params[:project])
|
94
|
+
redirect "/"
|
95
|
+
end
|
96
|
+
|
97
|
+
put "/lists/:id" do
|
98
|
+
list = Afterburn::List.find(params[:id])
|
99
|
+
list.update_attributes(params[:list])
|
100
|
+
list.to_json
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
#
|
2
|
+
# 1. Get your trello key and secret:
|
3
|
+
# => https://trello.com/1/appKey/generate.
|
4
|
+
# 2. Generate an app token for afterburn:
|
5
|
+
# => https://trello.com/1/connect?key=PUBLIC_KEY_FROM_ABOVE&name=MyApp&response_type=token&scope=read,write,account&expiration=never
|
6
|
+
# 3. Use the afterburn authorization block in an initializer file
|
7
|
+
|
8
|
+
Afterburn.authorize 'rossta' do |auth|
|
9
|
+
auth.trello_user_key = "trello_user_key"
|
10
|
+
auth.trello_user_secret = "trello_user_secret"
|
11
|
+
auth.trello_app_token = "trello_app_token"
|
12
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
Afterburn.redis = 'redis://localhost:6379'
|
2
|
+
|
3
|
+
Afterburn.authorize 'rossta' do |auth|
|
4
|
+
auth.trello_user_key = "3dca2797d175d70a1252cb502a5e49b9"
|
5
|
+
auth.trello_user_secret = "1532c3edcd355ec3bd7767ab0ac4da190351bed6174358139d284f3d67d978df"
|
6
|
+
auth.trello_app_token = "f05683f1c41d6d17e500fcc22c334b3852d34b0fbc0c8d9cf4a4ee011a01ec8c"
|
7
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
var Afterburn = {
|
2
|
+
setup: function(projects) {
|
3
|
+
_(projects).each(function(project) {
|
4
|
+
new Afterburn.Chart(project).render();
|
5
|
+
});
|
6
|
+
}
|
7
|
+
};
|
8
|
+
|
9
|
+
Afterburn.Chart = function(project) {
|
10
|
+
console.log(project);
|
11
|
+
var self = this;
|
12
|
+
self.project = project;
|
13
|
+
self.id = project.id;
|
14
|
+
self.name = project.name;
|
15
|
+
};
|
16
|
+
|
17
|
+
Afterburn.Chart.prototype = {
|
18
|
+
|
19
|
+
render: function() {
|
20
|
+
var self = this;
|
21
|
+
new Highcharts.Chart({
|
22
|
+
chart: {
|
23
|
+
renderTo: "project_" + self.id,
|
24
|
+
type: 'area'
|
25
|
+
},
|
26
|
+
title: {
|
27
|
+
text: "Cumulative flow diagram for "+self.name+" project"
|
28
|
+
},
|
29
|
+
xAxis: {
|
30
|
+
categories: self.project.categories,
|
31
|
+
tickmarkPlacement: 'on',
|
32
|
+
title: {
|
33
|
+
enabled: false
|
34
|
+
}
|
35
|
+
},
|
36
|
+
yAxis: {
|
37
|
+
title: {
|
38
|
+
text: 'Cards'
|
39
|
+
},
|
40
|
+
labels: {
|
41
|
+
formatter: function() {
|
42
|
+
return this.value;
|
43
|
+
}
|
44
|
+
}
|
45
|
+
},
|
46
|
+
tooltip: {
|
47
|
+
formatter: function() {
|
48
|
+
return ''+
|
49
|
+
this.x +': '+ Highcharts.numberFormat(this.y, 0, ',') +' cards';
|
50
|
+
}
|
51
|
+
},
|
52
|
+
plotOptions: {
|
53
|
+
area: {
|
54
|
+
stacking: 'normal',
|
55
|
+
lineColor: '#666666',
|
56
|
+
lineWidth: 1,
|
57
|
+
marker: {
|
58
|
+
lineWidth: 1,
|
59
|
+
lineColor: '#666666'
|
60
|
+
}
|
61
|
+
}
|
62
|
+
},
|
63
|
+
series: self.project.series
|
64
|
+
});
|
65
|
+
}
|
66
|
+
};
|