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.
- data/.gitignore +28 -0
- data/.rspec +3 -0
- data/.rvmrc.template +2 -0
- data/.travis.yml +20 -0
- data/CHANGELOG +32 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +110 -0
- data/LICENSE.txt +15 -0
- data/README.md +292 -0
- data/Rakefile +8 -0
- data/config/config.template.yml +6 -0
- data/config/config.yml.trackinguser_for_test +7 -0
- data/config/mongoid.template.yml +24 -0
- data/lib/patches/trello/card.rb +19 -0
- data/lib/patches/trello/member.rb +20 -0
- data/lib/startup_trello.rb +2 -0
- data/lib/tasks/rspec.rake +17 -0
- data/lib/tasks/tasks.rake +55 -0
- data/lib/tracco.rb +30 -0
- data/lib/tracco/effort.rb +35 -0
- data/lib/tracco/estimate.rb +25 -0
- data/lib/tracco/google_docs_exporter.rb +67 -0
- data/lib/tracco/member.rb +61 -0
- data/lib/tracco/mongoid_helper.rb +13 -0
- data/lib/tracco/tracked_card.rb +121 -0
- data/lib/tracco/tracking/base.rb +82 -0
- data/lib/tracco/tracking/card_done_tracking.rb +10 -0
- data/lib/tracco/tracking/effort_tracking.rb +45 -0
- data/lib/tracco/tracking/estimate_tracking.rb +23 -0
- data/lib/tracco/tracking/invalid_tracking.rb +19 -0
- data/lib/tracco/tracking_factory.rb +22 -0
- data/lib/tracco/trello_authorize.rb +16 -0
- data/lib/tracco/trello_configuration.rb +39 -0
- data/lib/tracco/trello_tracker.rb +44 -0
- data/lib/tracco/version.rb +3 -0
- data/script/ci/before_script.sh +1 -0
- data/script/ci/run_build.sh +2 -0
- data/script/crontab.template +8 -0
- data/script/mate.sh +1 -0
- data/spec/effort_spec.rb +59 -0
- data/spec/estimate_spec.rb +38 -0
- data/spec/integration/trello_authorization_spec.rb +12 -0
- data/spec/integration/trello_tracker_spec.rb +66 -0
- data/spec/member_spec.rb +81 -0
- data/spec/patches/trello/card_spec.rb +25 -0
- data/spec/spec_helper.rb +66 -0
- data/spec/support/database_cleaner.rb +12 -0
- data/spec/tracked_card_spec.rb +336 -0
- data/spec/tracking/card_done_tracking_spec.rb +18 -0
- data/spec/tracking/effort_tracking_spec.rb +114 -0
- data/spec/tracking/estimate_tracking_spec.rb +44 -0
- data/spec/tracking_factory_spec.rb +42 -0
- data/spec/trello_authorize_spec.rb +65 -0
- data/spec/trello_configuration_spec.rb +43 -0
- data/spec/trello_tracker_spec.rb +26 -0
- data/tracco.gemspec +41 -0
- data/tracco.sublime-project +10 -0
- metadata +316 -0
data/Rakefile
ADDED
@@ -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,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
|
data/lib/tracco.rb
ADDED
@@ -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
|