afterburn 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
};
|