trello_effort_tracker 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/.gitignore +25 -0
  2. data/.rspec +3 -0
  3. data/.rvmrc.template +2 -0
  4. data/.travis.yml +21 -0
  5. data/Gemfile +22 -0
  6. data/Gemfile.lock +101 -0
  7. data/LICENSE.txt +15 -0
  8. data/README.md +159 -0
  9. data/Rakefile +72 -0
  10. data/config/config.template.yml +7 -0
  11. data/config/mongoid.template.yml +24 -0
  12. data/lib/patches/trello/member.rb +20 -0
  13. data/lib/startup_trello.rb +2 -0
  14. data/lib/trello_effort_tracker/effort.rb +27 -0
  15. data/lib/trello_effort_tracker/estimate.rb +25 -0
  16. data/lib/trello_effort_tracker/google_docs_exporter.rb +67 -0
  17. data/lib/trello_effort_tracker/member.rb +46 -0
  18. data/lib/trello_effort_tracker/mongoid_helper.rb +13 -0
  19. data/lib/trello_effort_tracker/tracked_card.rb +103 -0
  20. data/lib/trello_effort_tracker/tracking.rb +130 -0
  21. data/lib/trello_effort_tracker/trello_authorize.rb +32 -0
  22. data/lib/trello_effort_tracker/trello_configuration.rb +33 -0
  23. data/lib/trello_effort_tracker/trello_tracker.rb +48 -0
  24. data/lib/trello_effort_tracker/version.rb +3 -0
  25. data/lib/trello_effort_tracker.rb +23 -0
  26. data/script/ci/before_script.sh +1 -0
  27. data/script/ci/run_build.sh +2 -0
  28. data/script/crontab.template +8 -0
  29. data/script/mate.sh +1 -0
  30. data/spec/effort_spec.rb +52 -0
  31. data/spec/estimate_spec.rb +38 -0
  32. data/spec/integration/trello_authorization_spec.rb +12 -0
  33. data/spec/member_spec.rb +45 -0
  34. data/spec/spec_helper.rb +21 -0
  35. data/spec/tracked_card_spec.rb +267 -0
  36. data/spec/tracking_spec.rb +236 -0
  37. data/spec/trello_authorize_spec.rb +71 -0
  38. data/spec/trello_configuration_spec.rb +43 -0
  39. data/trello_effort_tracker.gemspec +19 -0
  40. metadata +86 -0
@@ -0,0 +1,103 @@
1
+ class TrackedCard
2
+ include Mongoid::Document
3
+ include Mongoid::Timestamps
4
+ extend MongoidHelper
5
+
6
+ field :name
7
+ field :description
8
+ field :short_id, type: Integer
9
+ field :trello_id
10
+ field :due, type: Date
11
+ field :closed, type: Boolean
12
+ field :url
13
+ field :pos
14
+
15
+ embeds_many :estimates
16
+ embeds_many :efforts
17
+
18
+ validates_presence_of :name, :short_id, :trello_id
19
+ validates_numericality_of :short_id
20
+
21
+ def self.find_by_trello_id(trello_id)
22
+ without_mongo_raising_errors do
23
+ find_by(trello_id: trello_id)
24
+ end
25
+ end
26
+
27
+ def self.update_or_create_with(trello_card)
28
+ card = TrackedCard.find_or_create_by(trello_id: trello_card.id)
29
+ trello_card.attributes.delete(:id)
30
+ success = card.update_attributes(trello_card.attributes)
31
+ return card if success
32
+ end
33
+
34
+ def self.build_from(trello_card)
35
+ trello_card_id = trello_card.id
36
+ trello_card.attributes.delete(:id)
37
+ new(trello_card.attributes.merge(trello_id: trello_card_id))
38
+ end
39
+
40
+ def add(tracking)
41
+ if tracking.estimate? && estimates.none? {|e| e.tracking_notification_id == tracking.estimate.tracking_notification_id}
42
+ estimates << tracking.estimate
43
+ elsif tracking.effort? && efforts.none? {|e| e.tracking_notification_id == tracking.effort.tracking_notification_id}
44
+ efforts << tracking.effort
45
+ else
46
+ Trello.logger.warn "Ignoring tracking notification: #{tracking}" if tracking.unknown_format?
47
+ end
48
+ end
49
+
50
+ def no_tracking?
51
+ first_activity_date.nil?
52
+ end
53
+
54
+ def first_activity_date
55
+ [working_start_date, first_estimate_date].compact.min
56
+ end
57
+
58
+ def working_start_date
59
+ efforts.sort_by(&:date).first.date if efforts.present?
60
+ end
61
+
62
+ def first_estimate_date
63
+ estimates.sort_by(&:date).first.date if estimates.present?
64
+ end
65
+
66
+ def last_estimate_date
67
+ estimates.sort_by(&:date).last.date if estimates.present?
68
+ end
69
+
70
+ def total_effort
71
+ efforts.map(&:amount).inject(0, &:+)
72
+ end
73
+
74
+ def members
75
+ efforts.map(&:members).flatten.uniq
76
+ end
77
+
78
+ def last_estimate_error
79
+ estimate_errors.last
80
+ end
81
+
82
+ def estimate_errors
83
+ return [] if estimates.empty? || efforts.empty?
84
+
85
+ estimate_errors = []
86
+ estimates.each do |each|
87
+ estimate_errors << (100 * ((total_effort - each.amount) / each.amount * 1.0)).round(2)
88
+ end
89
+
90
+ estimate_errors
91
+ end
92
+
93
+ def to_s
94
+ "[#{name}]. Total effort: #{total_effort}h. Estimates #{estimates.map(&:to_s)}. Efforts: #{efforts.map(&:to_s)}"
95
+ end
96
+
97
+ def ==(other)
98
+ return true if other.equal?(self)
99
+ return false unless other.kind_of?(self.class)
100
+ trello_id == other.trello_id
101
+ end
102
+
103
+ end
@@ -0,0 +1,130 @@
1
+ class Tracking
2
+ extend Forwardable
3
+ include TrelloConfiguration
4
+
5
+ TIME_CONVERTERS = {
6
+ 'h' => lambda { |estimate| estimate },
7
+ 'd' => lambda { |estimate| estimate * 8 },
8
+ 'g' => lambda { |estimate| estimate * 8 },
9
+ 'p' => lambda { |estimate| estimate / 2 }
10
+ }
11
+
12
+ DURATION_REGEXP = '(\d+\.?\d*[phdg])'
13
+ DATE_REGEXP = /(\d{2})\.(\d{2})\.(\d{4})/
14
+
15
+ # delegate to the trello notification the member_creator method aliased as 'notifier'
16
+ def_delegator :@tracking_notification, :member_creator, :notifier
17
+
18
+ def initialize(tracking_notification)
19
+ @tracking_notification = tracking_notification
20
+ end
21
+
22
+ def date
23
+ Chronic.parse(date_as_string).to_date
24
+ end
25
+
26
+ def estimate?
27
+ !raw_estimate.nil?
28
+ end
29
+
30
+ def estimate
31
+ estimate = convert_to_hours(raw_estimate)
32
+ Estimate.new(amount: estimate, date: date, tracking_notification_id: @tracking_notification.id) if estimate
33
+ end
34
+
35
+ def effort?
36
+ !raw_effort.nil?
37
+ end
38
+
39
+ def effort
40
+ effort_amount = convert_to_hours(raw_effort)
41
+ if effort_amount
42
+ total_effort = effort_amount * effort_members.size
43
+ Effort.new(amount: total_effort, date: date, members: effort_members, tracking_notification_id: @tracking_notification.id)
44
+ end
45
+ end
46
+
47
+ def unknown_format?
48
+ !estimate? && !effort?
49
+ end
50
+
51
+ def to_s
52
+ "[#{date}] From #{notifier.username.color(:green)}\t on card '#{trello_card.name.color(:yellow)}': #{raw_text}"
53
+ end
54
+
55
+ private
56
+
57
+ def effort_members
58
+ @effort_members ||= users_involved_in_the_effort.map do |username|
59
+ Member.build_from(Trello::Member.find(username))
60
+ end
61
+
62
+ @effort_members
63
+ end
64
+
65
+ def users_involved_in_the_effort
66
+ users_involved_in_the_effort = raw_tracking.scan(/@(\w+)/).flatten
67
+ users_involved_in_the_effort << notifier.username unless should_count_only_listed_members?
68
+
69
+ users_involved_in_the_effort
70
+ end
71
+
72
+ def should_count_only_listed_members?
73
+ raw_tracking =~ /\((@\w+\W*\s*)+\)/
74
+ end
75
+
76
+ def raw_tracking
77
+ raw_text.gsub("@#{tracker_username}", "")
78
+ end
79
+
80
+ def raw_text
81
+ @tracking_notification.data['text']
82
+ end
83
+
84
+ def raw_estimate
85
+ extract_match_from_raw_tracking(/\[#{DURATION_REGEXP}\]/)
86
+ end
87
+
88
+ def raw_effort
89
+ extract_match_from_raw_tracking(/\+#{DURATION_REGEXP}/)
90
+ end
91
+
92
+ def convert_to_hours(duration_as_string)
93
+ return if duration_as_string.nil?
94
+
95
+ time_scale = duration_as_string.slice!(-1)
96
+ converter = TIME_CONVERTERS[time_scale]
97
+ converter.call(Float(duration_as_string))
98
+ end
99
+
100
+ def date_as_string
101
+ case raw_tracking
102
+ when DATE_REGEXP
103
+ day, month, year = raw_tracking.scan(DATE_REGEXP).flatten
104
+ "#{year}-#{month}-#{day}"
105
+ when /yesterday\s+\+#{DURATION_REGEXP}/, /\+#{DURATION_REGEXP}\s+yesterday/
106
+ (notification_date - 1).to_s
107
+ else
108
+ @tracking_notification.date
109
+ end
110
+ end
111
+
112
+ def extract_match_from_raw_tracking(regexp)
113
+ extracted = nil
114
+ raw_tracking.scan(regexp) do |match|
115
+ extracted = match.first
116
+ end
117
+
118
+ extracted
119
+ end
120
+
121
+ def trello_card
122
+ @trello_card ||= @tracking_notification.card
123
+ end
124
+
125
+ def notification_date
126
+ Chronic.parse(@tracking_notification.date).to_date
127
+ end
128
+
129
+ end
130
+
@@ -0,0 +1,32 @@
1
+ module TrelloAuthorize
2
+ include TrelloConfiguration
3
+ include Trello::Authorization
4
+
5
+ def authorize_on_trello(auth_params={})
6
+ %w{developer_public_key access_token_key developer_secret}.each do |key|
7
+ auth_params[key] ||= ENV[key] || authorization_params_from_config_file[key]
8
+ end
9
+
10
+ init_trello(auth_params)
11
+ end
12
+
13
+ private
14
+
15
+ def init_trello(auth_params)
16
+ ignoring_warnings do
17
+ Trello::Authorization.const_set(:AuthPolicy, OAuthPolicy)
18
+ end
19
+
20
+ OAuthPolicy.consumer_credential = OAuthCredential.new(auth_params["developer_public_key"], auth_params["developer_secret"])
21
+ OAuthPolicy.token = OAuthCredential.new(auth_params["access_token_key"], nil)
22
+ end
23
+
24
+ def ignoring_warnings(&block)
25
+ begin
26
+ v, $VERBOSE = $VERBOSE, nil
27
+ block.call if block
28
+ ensure
29
+ $VERBOSE = v
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,33 @@
1
+ module TrelloConfiguration
2
+
3
+ def tracker_username
4
+ @tracker_username ||= ENV["tracker_username"] || configuration["tracker_username"]
5
+ end
6
+
7
+ def authorization_params_from_config_file
8
+ configuration["trello"]
9
+ end
10
+
11
+ class Database
12
+ def self.load_env(db_env)
13
+ ENV['MONGOID_ENV'] = db_env
14
+ Mongoid.load!("config/mongoid.yml", db_env)
15
+ Trello.logger.info "Mongo db env: #{db_env.color(:green)}."
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def db_environment
22
+ ENV['MONGOID_ENV']
23
+ end
24
+
25
+ def configuration
26
+ @configuration ||= load_configuration
27
+ end
28
+
29
+ def load_configuration
30
+ YAML.load_file("config/config.yml")
31
+ end
32
+
33
+ end
@@ -0,0 +1,48 @@
1
+ class TrelloTracker
2
+ include TrelloAuthorize
3
+ include Trello
4
+
5
+ trap("SIGINT") { exit! }
6
+
7
+ def initialize(custom_auth_params = {})
8
+ authorize_on_trello(custom_auth_params)
9
+ end
10
+
11
+ def track(starting_date=Date.today)
12
+ notifications = tracker.notifications_from(starting_date)
13
+
14
+ oldest, latest = boundary_dates_in(notifications)
15
+ Trello.logger.info "Processing #{notifications.size} tracking notifications (from #{oldest} to #{latest}) starting from #{starting_date}..."
16
+
17
+ notifications.each do |notification|
18
+ tracking = Tracking.new(notification)
19
+ begin
20
+ card = TrackedCard.update_or_create_with(notification.card)
21
+ card.add(tracking)
22
+ Trello.logger.info tracking
23
+
24
+ rescue StandardError => e
25
+ Trello.logger.error "skipping tracking: #{e.message}".color(:magenta)
26
+ Trello.logger.error "#{e.backtrace}"
27
+ end
28
+ end
29
+ Trello.logger.info "Done tracking cards!".color(:green)
30
+ print_all_cards
31
+ end
32
+
33
+ private
34
+
35
+ def tracker
36
+ @tracker ||= Member.find(tracker_username)
37
+ end
38
+
39
+ def boundary_dates_in(notifications)
40
+ dates = notifications.map { |each_notification| Chronic.parse(each_notification.date) }
41
+ [dates.min, dates.max]
42
+ end
43
+
44
+ def print_all_cards
45
+ TrackedCard.all.each { |tracked_card| Trello.logger.info(tracked_card.to_s.color(:yellow)) }
46
+ end
47
+
48
+ end
@@ -0,0 +1,3 @@
1
+ class TrelloEffortTracker
2
+ VERSION = '0.0.3'
3
+ end
@@ -0,0 +1,23 @@
1
+ require 'trello'
2
+ require 'rainbow'
3
+ require 'set'
4
+ require 'yaml'
5
+ require 'chronic'
6
+ require 'mongoid'
7
+
8
+ require 'trello_effort_tracker/mongoid_helper'
9
+ require 'trello_effort_tracker/trello_configuration'
10
+ require 'trello_effort_tracker/trello_authorize'
11
+ require 'trello_effort_tracker/tracked_card'
12
+ require 'trello_effort_tracker/member'
13
+ require 'trello_effort_tracker/estimate'
14
+ require 'trello_effort_tracker/effort'
15
+ require 'trello_effort_tracker/tracking'
16
+ require 'trello_effort_tracker/trello_tracker'
17
+ require 'trello_effort_tracker/google_docs_exporter'
18
+
19
+ require 'patches/trello/member'
20
+
21
+ TrelloConfiguration::Database.load_env(ENV['MONGOID_ENV'] || "development")
22
+
23
+ Trello.logger.level = Logger::DEBUG
@@ -0,0 +1 @@
1
+ cp -f config/mongoid.template.yml config/mongoid.yml
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env sh
2
+ bundle exec rake spec:fast
@@ -0,0 +1,8 @@
1
+ SHELL=/Users/pierodibello/.rvm/bin/rvm-shell
2
+
3
+ GEMSET="ruby-1.9.3-p194@spikes"
4
+ PROJECT_PATH="/Users/$USER/Documents/workspace/trello_effort_tracker"
5
+ LC_ALL=en_US.UTF-8
6
+
7
+ # m h dom mon dow command
8
+ */10 * * * * rvm-shell $GEMSET -c "cd $PROJECT_PATH; bundle exec rake run:today[production]" >> /tmp/crontab.out 2>&1
data/script/mate.sh ADDED
@@ -0,0 +1 @@
1
+ mate $(ls -a | grep -v '\.\.$' | grep -v '\.$' | grep -v '\.git$'| grep -v '\.DS_Store'| grep -v '\.tmtags' | grep -v '\tags')
@@ -0,0 +1,52 @@
1
+ require 'spec_helper'
2
+ require 'mongoid-rspec'
3
+
4
+ describe Effort do
5
+
6
+ it { should have_fields(:amount, :date) }
7
+ it { should be_embedded_in(:tracked_card) }
8
+ it { should embed_many(:members) }
9
+
10
+ describe "validation" do
11
+ it { should validate_presence_of(:amount) }
12
+ it { should validate_presence_of(:date) }
13
+ it { should validate_presence_of(:members) }
14
+ end
15
+
16
+ describe "equality" do
17
+ %w{piero tommaso tom ugo}.each do |username|
18
+ let(username.to_sym) { Member.new(username: username) }
19
+ end
20
+
21
+ it "is equal to another effort with same amount, date and members" do
22
+ effort = Effort.new(amount: 3, date: Date.parse("2012-11-09"), members: [piero, tommaso])
23
+ same_effort = Effort.new(amount: 3, date: Date.parse("2012-11-09"), members: [piero, tommaso])
24
+ yet_same_effort = Effort.new(amount: 3, date: Date.parse("2012-11-09"), members: [tommaso, piero])
25
+
26
+ effort.should == same_effort
27
+ effort.should == yet_same_effort
28
+ end
29
+
30
+ it "is not equal when amount differs" do
31
+ effort = Effort.new(amount: 3, date: Date.today, members: [tom, ugo])
32
+ another_effort = Effort.new(amount: 1, date: Date.today, members: [tom, ugo])
33
+
34
+ effort.should_not == another_effort
35
+ end
36
+
37
+ it "is not equal when date differs" do
38
+ effort = Effort.new(amount: 3, date: Date.parse("2012-11-09"), members: [tom, ugo])
39
+ another_effort = Effort.new(amount: 3, date: Date.parse("2011-10-08"), members: [tom, ugo])
40
+
41
+ effort.should_not == another_effort
42
+ end
43
+
44
+ it "is not equal when members differ" do
45
+ effort = Effort.new(amount: 3, date: Date.today, members: [tom, ugo])
46
+ another_effort = Effort.new(amount: 3, date: Date.today, members: [piero, ugo])
47
+
48
+ effort.should_not == another_effort
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+ require 'mongoid-rspec'
3
+
4
+ describe Estimate do
5
+
6
+ it { should have_fields(:amount, :date) }
7
+ it { should be_embedded_in(:tracked_card) }
8
+
9
+ describe "validation" do
10
+ it { should validate_presence_of(:amount) }
11
+ it { should validate_presence_of(:date) }
12
+ end
13
+
14
+ describe "equality" do
15
+
16
+ it "is equal to another estimate with same amount and date" do
17
+ estimate = Estimate.new(amount: 3, date: Date.parse("2012-11-09"))
18
+ same_estimate = Estimate.new(amount: 3, date: Date.parse("2012-11-09"))
19
+
20
+ estimate.should == same_estimate
21
+ end
22
+
23
+ it "is not equal when amount differs" do
24
+ estimate = Estimate.new(amount: 3, date: Date.today)
25
+ another_estimate = Estimate.new(amount: 1, date: Date.today)
26
+
27
+ estimate.should_not == another_estimate
28
+ end
29
+
30
+ it "is not equal when date differs" do
31
+ estimate = Estimate.new(amount: 3, date: Date.parse("2012-11-09"))
32
+ another_estimate = Estimate.new(amount: 3, date: Date.parse("2011-10-08"))
33
+
34
+ estimate.should_not == another_estimate
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,12 @@
1
+ require 'spec_helper'
2
+ require 'trello'
3
+
4
+ describe "TrelloAuthorization" do
5
+ include TrelloAuthorize
6
+
7
+ it "authorizes connection to Trello", :needs_valid_configuration => true do
8
+ authorize_on_trello
9
+
10
+ Trello::Member.find("me").should_not be_nil
11
+ end
12
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+ require 'mongoid-rspec'
3
+
4
+ describe Member do
5
+
6
+ it { should have_fields(:trello_id, :username, :full_name, :avatar_id, :bio, :url) }
7
+ it { should be_embedded_in(:effort) }
8
+
9
+ describe "validation" do
10
+ it { should validate_presence_of(:username) }
11
+ end
12
+
13
+ describe "equality" do
14
+ it "is equal to another member with the same username" do
15
+ member = Member.new(username: "piero")
16
+ same_member = Member.new(username: "piero")
17
+ different_member = Member.new(username: "tommaso")
18
+
19
+ member.should == same_member
20
+ member.should_not == different_member
21
+ end
22
+ end
23
+
24
+ describe ".build_from" do
25
+ it "builds a Member from a Trello Member" do
26
+ member = Member.build_from(Trello::Member.new("username" => "piero"))
27
+
28
+ member.username.should == "piero"
29
+ end
30
+
31
+ it "takes the Trello Member id and set it as trello_id" do
32
+ member = Member.build_from(Trello::Member.new("username" => "piero", "id" => "1234567abc"))
33
+
34
+ member.id.should_not == "1234567abc"
35
+ member.trello_id.should == "1234567abc"
36
+ end
37
+ end
38
+
39
+ describe "#avatar_url" do
40
+ it "points to the avatar thumbnail image" do
41
+ member = Member.new(avatar_id: "123xyz")
42
+ member.avatar_url.should == "https://trello-avatars.s3.amazonaws.com/123xyz/30.png"
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,21 @@
1
+ require 'rubygems'
2
+
3
+ # Set up gems listed in the Gemfile.
4
+ begin
5
+ ENV['BUNDLE_GEMFILE'] = File.expand_path('../Gemfile', File.dirname(__FILE__))
6
+ require 'bundler'
7
+ Bundler.setup
8
+ rescue Bundler::GemNotFound => e
9
+ STDERR.puts e.message
10
+ STDERR.puts "Try running `bundle install`."
11
+ exit!
12
+ end
13
+
14
+ Bundler.require
15
+
16
+ RSpec.configure do |configuration|
17
+ configuration.include Mongoid::Matchers
18
+ end
19
+
20
+ # force test env for the mongodb configuration
21
+ TrelloConfiguration::Database.load_env("test")