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.
@@ -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
+ };