afterburn 0.0.1

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