tracco 0.0.9

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 (58) hide show
  1. data/.gitignore +28 -0
  2. data/.rspec +3 -0
  3. data/.rvmrc.template +2 -0
  4. data/.travis.yml +20 -0
  5. data/CHANGELOG +32 -0
  6. data/Gemfile +9 -0
  7. data/Gemfile.lock +110 -0
  8. data/LICENSE.txt +15 -0
  9. data/README.md +292 -0
  10. data/Rakefile +8 -0
  11. data/config/config.template.yml +6 -0
  12. data/config/config.yml.trackinguser_for_test +7 -0
  13. data/config/mongoid.template.yml +24 -0
  14. data/lib/patches/trello/card.rb +19 -0
  15. data/lib/patches/trello/member.rb +20 -0
  16. data/lib/startup_trello.rb +2 -0
  17. data/lib/tasks/rspec.rake +17 -0
  18. data/lib/tasks/tasks.rake +55 -0
  19. data/lib/tracco.rb +30 -0
  20. data/lib/tracco/effort.rb +35 -0
  21. data/lib/tracco/estimate.rb +25 -0
  22. data/lib/tracco/google_docs_exporter.rb +67 -0
  23. data/lib/tracco/member.rb +61 -0
  24. data/lib/tracco/mongoid_helper.rb +13 -0
  25. data/lib/tracco/tracked_card.rb +121 -0
  26. data/lib/tracco/tracking/base.rb +82 -0
  27. data/lib/tracco/tracking/card_done_tracking.rb +10 -0
  28. data/lib/tracco/tracking/effort_tracking.rb +45 -0
  29. data/lib/tracco/tracking/estimate_tracking.rb +23 -0
  30. data/lib/tracco/tracking/invalid_tracking.rb +19 -0
  31. data/lib/tracco/tracking_factory.rb +22 -0
  32. data/lib/tracco/trello_authorize.rb +16 -0
  33. data/lib/tracco/trello_configuration.rb +39 -0
  34. data/lib/tracco/trello_tracker.rb +44 -0
  35. data/lib/tracco/version.rb +3 -0
  36. data/script/ci/before_script.sh +1 -0
  37. data/script/ci/run_build.sh +2 -0
  38. data/script/crontab.template +8 -0
  39. data/script/mate.sh +1 -0
  40. data/spec/effort_spec.rb +59 -0
  41. data/spec/estimate_spec.rb +38 -0
  42. data/spec/integration/trello_authorization_spec.rb +12 -0
  43. data/spec/integration/trello_tracker_spec.rb +66 -0
  44. data/spec/member_spec.rb +81 -0
  45. data/spec/patches/trello/card_spec.rb +25 -0
  46. data/spec/spec_helper.rb +66 -0
  47. data/spec/support/database_cleaner.rb +12 -0
  48. data/spec/tracked_card_spec.rb +336 -0
  49. data/spec/tracking/card_done_tracking_spec.rb +18 -0
  50. data/spec/tracking/effort_tracking_spec.rb +114 -0
  51. data/spec/tracking/estimate_tracking_spec.rb +44 -0
  52. data/spec/tracking_factory_spec.rb +42 -0
  53. data/spec/trello_authorize_spec.rb +65 -0
  54. data/spec/trello_configuration_spec.rb +43 -0
  55. data/spec/trello_tracker_spec.rb +26 -0
  56. data/tracco.gemspec +41 -0
  57. data/tracco.sublime-project +10 -0
  58. metadata +316 -0
@@ -0,0 +1,8 @@
1
+ # encoding: utf-8
2
+ require 'bundler'
3
+ require 'tracco'
4
+
5
+ Bundler::GemHelper.install_tasks
6
+
7
+ import 'lib/tasks/tasks.rake'
8
+ import 'lib/tasks/rspec.rake'
@@ -0,0 +1,6 @@
1
+ trello:
2
+ developer_public_key: <here put the key taken from https://trello.com/1/appKey/generate>
3
+ access_token_key: <here put the token taken in the 'Reading Private Data' section from https://trello.com/1/appKey/generate>
4
+
5
+ tracker_username: <here put the Trello member used as recipient of the tracking notifications>
6
+ google_docs_username: <here put your Google Docs username>
@@ -0,0 +1,7 @@
1
+ # read-only access to @trackinguser_for_test/testinguser! boards
2
+ trello:
3
+ developer_public_key: ef7c400e711057d7ba5e00be20139a33
4
+ access_token_key: 9047d8fdbfdc960d41910673e300516cc8630dd4967e9b418fc27e410516362e
5
+
6
+ tracker_username: trackinguser_for_test
7
+ google_docs_username: pietro.dibello@xpeppers.com
@@ -0,0 +1,24 @@
1
+ development:
2
+ sessions:
3
+ default:
4
+ database: tracco_dev
5
+ hosts:
6
+ - localhost:27017
7
+
8
+ test:
9
+ sessions:
10
+ default:
11
+ database: tracco_test
12
+ hosts:
13
+ - localhost:27017
14
+
15
+
16
+ production:
17
+ autocreate_indexes: true
18
+ persist_in_safe_mode: true
19
+
20
+ sessions:
21
+ default:
22
+ database: tracco_production
23
+ hosts:
24
+ - localhost:27017
@@ -0,0 +1,19 @@
1
+ module Trello
2
+
3
+ class Card
4
+ # Reopening the Trello::Card class to add a method detecting a card moved in a DONE column
5
+ def in_done_column?
6
+ list_name = ""
7
+ begin
8
+ list_name = list.name.strip
9
+ rescue Trello::Error => e
10
+ Trello::Logger.error("Cannot find column for card #{name}")
11
+ end
12
+
13
+ !!(list_name =~ /^DONE/i)
14
+ end
15
+ end
16
+
17
+ end
18
+
19
+
@@ -0,0 +1,20 @@
1
+ module Trello
2
+ class Member
3
+ # Reopening the Trello::Member class to add a notifications helper method
4
+ def notifications_from(starting_date)
5
+ notifications(limit:1000).select(&greater_than_or_equal_to(starting_date)).select(&tracking_notification?)
6
+ end
7
+
8
+ private
9
+
10
+ def greater_than_or_equal_to(starting_date)
11
+ lambda { |notification| Chronic.parse(notification.date) >= starting_date }
12
+ end
13
+
14
+ def tracking_notification?
15
+ lambda { |notification| notification.type == "mentionedOnCard" }
16
+ end
17
+
18
+ end
19
+
20
+ end
@@ -0,0 +1,2 @@
1
+ include TrelloAuthorize
2
+ authorize_on_trello
@@ -0,0 +1,17 @@
1
+ require 'rspec/core/rake_task'
2
+ RSpec::Core::RakeTask.new(:spec)
3
+
4
+ task :default => :spec
5
+ task :specs => :spec
6
+
7
+ namespace :spec do
8
+ desc "Run fast specs"
9
+ RSpec::Core::RakeTask.new(:fast) do |t|
10
+ t.rspec_opts = '--tag ~needs_valid_configuration'
11
+ end
12
+
13
+ desc "Run slow specs"
14
+ RSpec::Core::RakeTask.new(:slow) do |t|
15
+ t.rspec_opts = '--tag needs_valid_configuration'
16
+ end
17
+ end
@@ -0,0 +1,55 @@
1
+ desc "Open an irb session preloaded with this library, e.g. rake 'console[production]' will open a irb session with the production db env"
2
+ task :console, [:db_env] do |t, args|
3
+ args.with_defaults(db_env: "development")
4
+ sh "export MONGOID_ENV=#{args.db_env}; irb -rubygems -I lib -r tracco.rb -r startup_trello.rb"
5
+ end
6
+
7
+ task :c, [:db_env] do |t, args|
8
+ Rake::Task[:console].invoke(args.db_env)
9
+ end
10
+
11
+ namespace :run do
12
+ include TrelloConfiguration
13
+
14
+ desc "Run on the cards tracked starting from a given day, e.g. rake 'run:from_day[2012-11-1]'"
15
+ task :from_day, [:starting_date, :db_env] => [:ensure_environment] do |t, args|
16
+ args.with_defaults(starting_date: Date.today.to_s, db_env: "development")
17
+ TrelloConfiguration::Database.load_env(args.db_env)
18
+
19
+ tracker = TrelloTracker.new
20
+ tracker.track(Date.parse(args.starting_date))
21
+ end
22
+
23
+ desc "Run on the cards tracked today, #{Date.today}"
24
+ task :today, [:db_env] => [:ensure_environment] do |t, args|
25
+ args.with_defaults(db_env: "development")
26
+ Rake.application.invoke_task("run:from_day[#{Date.today.to_s}, #{args.db_env}]")
27
+ end
28
+ end
29
+
30
+ namespace :export do
31
+ desc "Export all cards to a google docs spreadsheet, e.g. rake \"export:google_docs[my_sheet,tracking,production]\""
32
+ task :google_docs, [:spreadsheet, :worksheet, :db_env] => [:ensure_environment] do |t, args|
33
+ args.with_defaults(db_env: "development")
34
+ TrelloConfiguration::Database.load_env(args.db_env)
35
+
36
+ exporter = GoogleDocsExporter.new(args.spreadsheet, args.worksheet)
37
+ spreadsheet_url = exporter.export
38
+
39
+ puts "[DONE]".color(:green)
40
+ puts "Go to #{spreadsheet_url}"
41
+ end
42
+ end
43
+
44
+ task :ensure_environment do
45
+ %w{developer_public_key access_token_key}.each do |each_name|
46
+ unless ENV[each_name] || authorization_params_from_config_file[each_name]
47
+ puts "ERROR: Missing <#{each_name}> configuration parameter: set it as environment variable or in the config/config.yml file."
48
+ exit 1
49
+ end
50
+ end
51
+ unless tracker_username
52
+ puts "ERROR: Missing <tracker_username> configuration parameter: set it as environment variable or in the config/config.yml file."
53
+ exit 1
54
+ end
55
+ end
@@ -0,0 +1,30 @@
1
+ require 'trello'
2
+ require 'rainbow'
3
+ require 'set'
4
+ require 'yaml'
5
+ require 'chronic'
6
+ require 'mongoid'
7
+ require 'forwardable'
8
+
9
+ require 'tracco/mongoid_helper'
10
+ require 'tracco/trello_configuration'
11
+ require 'tracco/trello_authorize'
12
+ require 'tracco/tracked_card'
13
+ require 'tracco/member'
14
+ require 'tracco/estimate'
15
+ require 'tracco/effort'
16
+ require 'tracco/tracking/base'
17
+ require 'tracco/tracking/estimate_tracking'
18
+ require 'tracco/tracking/effort_tracking'
19
+ require 'tracco/tracking/card_done_tracking'
20
+ require 'tracco/tracking/invalid_tracking'
21
+ require 'tracco/tracking_factory'
22
+ require 'tracco/trello_tracker'
23
+ require 'tracco/google_docs_exporter'
24
+
25
+ require 'patches/trello/member'
26
+ require 'patches/trello/card'
27
+
28
+ TrelloConfiguration::Database.load_env(ENV['MONGOID_ENV'] || "development", ENV['MONGOID_CONFIG_PATH'])
29
+
30
+ Trello.logger.level = Logger::DEBUG
@@ -0,0 +1,35 @@
1
+ class Effort
2
+ include Mongoid::Document
3
+ include Mongoid::Timestamps
4
+
5
+ field :amount, type: BigDecimal
6
+ field :date, type: Date
7
+ field :tracking_notification_id
8
+
9
+ embeds_many :members
10
+ embedded_in :tracked_card
11
+
12
+ default_scope asc(:date)
13
+
14
+ validates_presence_of :amount, :date, :members
15
+
16
+ def amount_per_member
17
+ amount / members.size
18
+ end
19
+
20
+ def include?(member)
21
+ members.include?(member)
22
+ end
23
+
24
+ def ==(other)
25
+ return true if other.equal?(self)
26
+ return false unless other.kind_of?(self.class)
27
+
28
+ amount == other.amount && date == other.date && Set.new(members) == Set.new(other.members)
29
+ end
30
+
31
+ def to_s
32
+ "[#{date}] spent #{amount} hours by #{members.map(&:at_username).join(", ")}"
33
+ end
34
+
35
+ end
@@ -0,0 +1,25 @@
1
+ class Estimate
2
+ include Mongoid::Document
3
+ include Mongoid::Timestamps
4
+
5
+ field :amount, type: BigDecimal
6
+ field :date, type: Date
7
+ field :tracking_notification_id
8
+
9
+ embedded_in :tracked_card
10
+
11
+ default_scope asc(:date)
12
+
13
+ validates_presence_of :amount, :date
14
+
15
+ def ==(other)
16
+ return true if other.equal?(self)
17
+ return false unless other.kind_of?(self.class)
18
+
19
+ amount == other.amount && date == other.date
20
+ end
21
+
22
+ def to_s
23
+ "[#{date}] estimated #{amount} hours"
24
+ end
25
+ end
@@ -0,0 +1,67 @@
1
+ require "google_drive"
2
+ require 'highline/import'
3
+
4
+ class GoogleDocsExporter
5
+ include TrelloConfiguration
6
+
7
+ def initialize(spreadsheet_name, worksheet_name)
8
+ @spreadsheet_name = spreadsheet_name || "trello effort tracking"
9
+ @worksheet_name = worksheet_name || "tracking"
10
+ end
11
+
12
+ def export
13
+ Trello.logger.info "Running exporter from db env '#{db_environment}' to google docs '#{@spreadsheet_name.color(:green)}##{@worksheet_name.color(:green)}'..."
14
+
15
+ spreadsheet = google_docs_session.spreadsheet_by_title(@spreadsheet_name) || google_docs_session.create_spreadsheet(@spreadsheet_name)
16
+ worksheet = spreadsheet.worksheet_by_title(@worksheet_name) || spreadsheet.add_worksheet(@worksheet_name)
17
+
18
+ create_header(worksheet)
19
+ index = 2 # skip the header
20
+
21
+ cards = TrackedCard.all.reject(&:no_tracking?).sort_by(&:first_activity_date).reverse
22
+ cards.each do |card|
23
+ print ".".color(:green)
24
+ worksheet[index, columns[:user_story_id]] = card.short_id
25
+ worksheet[index, columns[:user_story_name]] = card.name
26
+ worksheet[index, columns[:start_date]] = card.working_start_date
27
+ worksheet[index, columns[:total_effort]] = card.total_effort
28
+ worksheet[index, columns[:last_estimate_error]] = card.last_estimate_error
29
+ card.estimates.each_with_index do |estimate, i|
30
+ worksheet[index, columns[:estimate]+i] = estimate.amount
31
+ end
32
+ index += 1
33
+ end
34
+
35
+ saved = worksheet.save
36
+ spreadsheet.human_url if saved
37
+ end
38
+
39
+ private
40
+
41
+ def google_docs_session(email=configuration["google_docs_username"])
42
+ @session ||= login(email)
43
+ end
44
+
45
+ def login(email)
46
+ username = ask("Enter your google docs username: ") { |q| q.default = email }
47
+ password = ask("Enter your google docs password: ") { |q| q.echo = false }
48
+
49
+ GoogleDrive.login(username, password)
50
+ end
51
+
52
+ def columns
53
+ @columns ||= {
54
+ user_story_id: 1,
55
+ user_story_name: 2,
56
+ start_date: 3,
57
+ total_effort: 4,
58
+ last_estimate_error: 5,
59
+ estimate: 6,
60
+ }
61
+ end
62
+
63
+ def create_header(worksheet)
64
+ worksheet.update_cells(1,1, [["ID", "Story Name", "Start Date", "Total Effort (hours)", "Last estimate error (%)", "First Estimate", "2nd estimate", "3rd estimate"]])
65
+ end
66
+
67
+ end
@@ -0,0 +1,61 @@
1
+ class Member
2
+ include Mongoid::Document
3
+ include Mongoid::Timestamps
4
+
5
+ field :trello_id
6
+ field :username
7
+ field :full_name
8
+ field :avatar_id
9
+ field :bio
10
+ field :url
11
+
12
+ embedded_in :effort
13
+
14
+ validates_presence_of :username
15
+
16
+ def self.build_from(trello_member)
17
+ trello_member_id = trello_member.id
18
+ trello_member.attributes.delete(:id)
19
+ new(trello_member.attributes.merge(trello_id: trello_member_id))
20
+ end
21
+
22
+ def at_username
23
+ "@#{username}"
24
+ end
25
+
26
+ def avatar_url
27
+ trello_member.avatar_url(size: :small)
28
+ end
29
+
30
+ def effort_spent(from_date=nil)
31
+ cards = TrackedCard.where("efforts.members.username" => username)
32
+ efforts = cards.map(&:efforts).compact.flatten
33
+ efforts = efforts.select {|e| e.date >= from_date} if from_date
34
+ efforts.select { |effort| effort.include?(self) }.inject(0) { |total, effort| total + effort.amount_per_member }
35
+ end
36
+ alias_method :effort_spent_since, :effort_spent
37
+
38
+ def ==(other)
39
+ return true if other.equal?(self)
40
+ return false unless other.kind_of?(self.class)
41
+
42
+ username == other.username
43
+ end
44
+
45
+ def eql?(other)
46
+ return false unless other.instance_of?(self.class)
47
+ username == other.username
48
+ end
49
+
50
+ def hash
51
+ username.hash
52
+ end
53
+
54
+ private
55
+
56
+ def trello_member
57
+ @trello_member ||= Trello::Member.new("id" => trello_id, "fullName" => full_name, "username" => username,
58
+ "avatarHash" => avatar_id, "bio" => bio, "url" => url)
59
+ end
60
+
61
+ end
@@ -0,0 +1,13 @@
1
+ module MongoidHelper
2
+
3
+ def without_mongo_raising_errors(&block)
4
+ original_value = Mongoid.raise_not_found_error
5
+ Mongoid.raise_not_found_error = false
6
+ begin
7
+ block.call if block
8
+ ensure
9
+ Mongoid.raise_not_found_error = original_value
10
+ end
11
+ end
12
+
13
+ end
@@ -0,0 +1,121 @@
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 :done, type: Boolean
11
+ field :due, type: Date
12
+ field :closed, type: Boolean
13
+ field :url
14
+ field :pos
15
+
16
+ embeds_many :estimates
17
+ embeds_many :efforts
18
+
19
+ validates_presence_of :name, :short_id, :trello_id
20
+ validates_numericality_of :short_id
21
+
22
+ def self.find_by_trello_id(trello_id)
23
+ without_mongo_raising_errors do
24
+ find_by(trello_id: trello_id)
25
+ end
26
+ end
27
+
28
+ def self.update_or_create_with(trello_card)
29
+ tracked_card = find_or_create_by(trello_id: trello_card.id)
30
+ trello_card.attributes.delete(:id)
31
+ tracked_card_attributes = trello_card.attributes.merge(done: trello_card.in_done_column?)
32
+ updated_successfully = tracked_card.update_attributes(tracked_card_attributes)
33
+ return tracked_card if updated_successfully
34
+ end
35
+
36
+ def status
37
+ if done?
38
+ :done
39
+ elsif efforts.empty?
40
+ :todo
41
+ else
42
+ :in_progress
43
+ end
44
+ end
45
+
46
+ def self.build_from(trello_card)
47
+ trello_card_id = trello_card.id
48
+ trello_card.attributes.delete(:id)
49
+ new(trello_card.attributes.merge(trello_id: trello_card_id))
50
+ end
51
+
52
+ def add(tracking)
53
+ tracking.add_to(self)
54
+ end
55
+
56
+ def add!(tracking)
57
+ add(tracking) && save!
58
+ end
59
+
60
+ def contains_effort?(effort)
61
+ efforts.any? { |e| e.tracking_notification_id == effort.tracking_notification_id }
62
+ end
63
+
64
+ def contains_estimate?(estimate)
65
+ estimates.any? { |e| e.tracking_notification_id == estimate.tracking_notification_id }
66
+ end
67
+
68
+ def no_tracking?
69
+ first_activity_date.nil?
70
+ end
71
+
72
+ def first_activity_date
73
+ [working_start_date, first_estimate_date].compact.min
74
+ end
75
+
76
+ def working_start_date
77
+ efforts.sort_by(&:date).first.date if efforts.present?
78
+ end
79
+
80
+ def first_estimate_date
81
+ estimates.sort_by(&:date).first.date if estimates.present?
82
+ end
83
+
84
+ def last_estimate_date
85
+ estimates.sort_by(&:date).last.date if estimates.present?
86
+ end
87
+
88
+ def total_effort
89
+ efforts.map(&:amount).inject(0, &:+)
90
+ end
91
+
92
+ def members
93
+ efforts.map(&:members).flatten.uniq
94
+ end
95
+
96
+ def last_estimate_error
97
+ estimate_errors.last
98
+ end
99
+
100
+ def estimate_errors
101
+ return [] if estimates.empty? || efforts.empty?
102
+
103
+ estimate_errors = []
104
+ estimates.each do |each|
105
+ estimate_errors << (100 * ((total_effort - each.amount) / each.amount * 1.0)).round(2)
106
+ end
107
+
108
+ estimate_errors
109
+ end
110
+
111
+ def to_s
112
+ "[#{name}]. Total effort: #{total_effort}h. Estimates #{estimates.map(&:to_s)}. Efforts: #{efforts.map(&:to_s)}"
113
+ end
114
+
115
+ def ==(other)
116
+ return true if other.equal?(self)
117
+ return false unless other.kind_of?(self.class)
118
+ trello_id == other.trello_id
119
+ end
120
+
121
+ end