trello_effort_tracker 0.0.3

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.
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")