jira_reporting 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.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +52 -0
- data/Rakefile +6 -0
- data/auth.yml.example +10 -0
- data/bin/jira_reporting +50 -0
- data/bin/jira_sla_update +54 -0
- data/bin/kpi_report +204 -0
- data/bin/maint_vs_enh +127 -0
- data/bin/platform_stability_kpi_report +215 -0
- data/bin/pr-report.rb +23 -0
- data/bin/quarter_report +65 -0
- data/bin/sla_update_closed_issues +38 -0
- data/bin/sla_warning +36 -0
- data/bin/time_in_dev +56 -0
- data/jira_reporting.gemspec +37 -0
- data/lib/auto_hash.rb +21 -0
- data/lib/code_climate.rb +35 -0
- data/lib/jira_issue.rb +288 -0
- data/lib/jira_reporting.rb +27 -0
- data/lib/jira_reporting/connection.rb +16 -0
- data/lib/jira_reporting/sla_report.rb +103 -0
- data/lib/jira_reporting/sla_tracker.rb +183 -0
- data/lib/jira_reporting/triage_tracker.rb +69 -0
- data/lib/jira_reporting/version.rb +3 -0
- data/lib/pull_request.rb +104 -0
- data/optoro_holidays.yaml +71 -0
- data/spec/jira_reporting_spec.rb +11 -0
- data/spec/spec_helper.rb +2 -0
- metadata +313 -0
data/bin/sla_warning
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
PROJECT_ROOT = "#{File.dirname(__FILE__)}/../"
|
4
|
+
|
5
|
+
$:.unshift "#{File.dirname(__FILE__)}/../lib"
|
6
|
+
require 'jira_reporting'
|
7
|
+
require 'active_support'
|
8
|
+
require 'yaml'
|
9
|
+
|
10
|
+
auth = YAML.load_file(File.join(PROJECT_ROOT,'auth.yml'))
|
11
|
+
jira_username = auth["jira"]["username"]
|
12
|
+
jira_password = auth["jira"]["password"]
|
13
|
+
JiraReporting.connect! jira_username, jira_password
|
14
|
+
|
15
|
+
# Open TS issues
|
16
|
+
query = %Q{
|
17
|
+
project = "Tech Support"
|
18
|
+
AND status not in (Resolved, "Not Accepted", "Can't Reproduce",
|
19
|
+
"Requester Review", Merged, MERGED_ST, "On Production")
|
20
|
+
}
|
21
|
+
|
22
|
+
ts_issues = JiraIssue.find(query)
|
23
|
+
|
24
|
+
ts_issues.each do |issue|
|
25
|
+
issue.set_sla_due_time! if issue.sla_due_time.nil?
|
26
|
+
issue.set_sla_triaged_at! if issue.sla_triaged_at.nil?
|
27
|
+
issue.set_over_triage_sla! if issue.over_triage_sla.nil?
|
28
|
+
issue.set_sla_closed_at! if issue.sla_closed_at.nil?
|
29
|
+
issue.set_sla_due_warning! if issue.sla_due_warning.nil?
|
30
|
+
issue.set_over_sla! if issue.over_sla.nil?
|
31
|
+
# issue.set_total_time_over_sla! if issue.total_time_over_sla.nil?
|
32
|
+
|
33
|
+
puts "#{issue.priority} #{issue.key} #{issue.summary} has been updated."
|
34
|
+
end
|
35
|
+
|
36
|
+
puts 'SLA due time and overdue status update complete.'
|
data/bin/time_in_dev
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$:.unshift "#{File.dirname(__FILE__)}/../lib"
|
4
|
+
require 'jira_reporting'
|
5
|
+
require 'pry-byebug'
|
6
|
+
require 'active_support'
|
7
|
+
require 'yaml'
|
8
|
+
require 'code_climate'
|
9
|
+
require 'auto_hash'
|
10
|
+
require 'pull_request'
|
11
|
+
|
12
|
+
auth = YAML.load_file('auth.yml')
|
13
|
+
|
14
|
+
jira_username = auth["jira"]["username"]
|
15
|
+
jira_password = auth["jira"]["password"]
|
16
|
+
JiraReporting.connect! jira_username, jira_password
|
17
|
+
|
18
|
+
# This takes a while
|
19
|
+
issues = JiraIssue.find(%Q{
|
20
|
+
project in (BLINQ.com, OptiTurn, "Tech Support", Tech)
|
21
|
+
and (status was Merged DURING ("2014-01-01", "2015-01-01")
|
22
|
+
OR status was Resolved DURING ("2014-01-01", "2015-01-01"))
|
23
|
+
})
|
24
|
+
|
25
|
+
type_times = Hash.new(0)
|
26
|
+
type_counts = Hash.new(0)
|
27
|
+
|
28
|
+
issues.each do |issue|
|
29
|
+
puts "#{issue.key} #{issue.type}"
|
30
|
+
type_times[issue.type] += (issue.status_ages["In Progress"] || 0)
|
31
|
+
type_counts[issue.type] += 1
|
32
|
+
puts type_times
|
33
|
+
puts type_counts
|
34
|
+
end
|
35
|
+
|
36
|
+
type_counts.keys.each { |type|
|
37
|
+
puts [type, type_counts[type], type_times[type]].join(",")
|
38
|
+
}
|
39
|
+
|
40
|
+
File.open("result.csv", "w") do |csv|
|
41
|
+
issues.each do |issue|
|
42
|
+
puts([
|
43
|
+
issue.key,
|
44
|
+
issue.type,
|
45
|
+
issue.status_ages["In Progress"] || 0,
|
46
|
+
].join(","))
|
47
|
+
csv.puts([
|
48
|
+
issue.key,
|
49
|
+
issue.type,
|
50
|
+
issue.status_ages["In Progress"] || 0,
|
51
|
+
].join(","))
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# (1..100).each { issue = issues.sample; puts issue.type ; puts issue.status_ages; type_times[issue.type] += (issue.status_ages["In Progress"] || 0); type_times["count"] += 1 }
|
56
|
+
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'jira_reporting/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "jira_reporting"
|
8
|
+
spec.version = JiraReporting::VERSION
|
9
|
+
spec.authors = ["Joshua Szmajda"]
|
10
|
+
spec.email = ["josh@optoro.com"]
|
11
|
+
spec.summary = %q{Misc tools for gathering data}
|
12
|
+
spec.description = %q{Misc tools for gathering data}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.5"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "rspec"
|
24
|
+
spec.add_development_dependency "byebug"
|
25
|
+
spec.add_development_dependency "pry"
|
26
|
+
spec.add_development_dependency "pry-byebug"
|
27
|
+
spec.add_development_dependency "minitest"
|
28
|
+
spec.add_dependency "jiralicious"
|
29
|
+
spec.add_dependency "thor"
|
30
|
+
spec.add_dependency "dot_notation"
|
31
|
+
spec.add_dependency "business_time"
|
32
|
+
spec.add_dependency "holidays"
|
33
|
+
spec.add_dependency "octokit"
|
34
|
+
spec.add_dependency "rubypython"
|
35
|
+
spec.add_dependency "andand"
|
36
|
+
spec.add_dependency "os"
|
37
|
+
end
|
data/lib/auto_hash.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
|
2
|
+
class AutoHash < Hash
|
3
|
+
def initialize(*args)
|
4
|
+
super()
|
5
|
+
@update, @update_index = args[0][:update], args[0][:update_key] unless args.empty?
|
6
|
+
end
|
7
|
+
|
8
|
+
def [](k)
|
9
|
+
if self.has_key?k
|
10
|
+
super(k)
|
11
|
+
else
|
12
|
+
AutoHash.new(:update => self, :update_key => k)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def []=(k, v)
|
17
|
+
@update[@update_index] = self if @update and @update_index
|
18
|
+
super
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
data/lib/code_climate.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
|
2
|
+
require 'httparty'
|
3
|
+
|
4
|
+
# API: https://codeclimate.com/docs/api
|
5
|
+
|
6
|
+
class CodeClimate
|
7
|
+
include HTTParty
|
8
|
+
format :json
|
9
|
+
base_uri 'https://codeclimate.com/api'
|
10
|
+
|
11
|
+
def initialize(api_token)
|
12
|
+
self.class.default_params api_token: api_token
|
13
|
+
end
|
14
|
+
|
15
|
+
# Expose some HTTParty class things
|
16
|
+
def get(*args)
|
17
|
+
self.class.get(*args)
|
18
|
+
end
|
19
|
+
|
20
|
+
def repo_list
|
21
|
+
get('/repos')
|
22
|
+
end
|
23
|
+
|
24
|
+
def repo_id(repo_name)
|
25
|
+
repo_list.find { |repo| repo["url"].include?(repo_name) }["id"]
|
26
|
+
end
|
27
|
+
|
28
|
+
def repo_details(repo_id)
|
29
|
+
get("/repos/#{repo_id}")
|
30
|
+
end
|
31
|
+
|
32
|
+
def repo_details_by_name(repo_name)
|
33
|
+
get("/repos/#{repo_id(repo_name)}")
|
34
|
+
end
|
35
|
+
end
|
data/lib/jira_issue.rb
ADDED
@@ -0,0 +1,288 @@
|
|
1
|
+
require 'dot_notation'
|
2
|
+
|
3
|
+
class JiraIssue
|
4
|
+
include SLATracker
|
5
|
+
include TriageTracker
|
6
|
+
# The FIVE_P_DATE variable exists for tickets created before
|
7
|
+
# 2015-03-06 15:55:16 -0500. Not used for any tickets after that.
|
8
|
+
# Tickets created after above date use differnt SLA measurements.
|
9
|
+
FIVE_P_DATE = Time.at(142_567_531_6).freeze
|
10
|
+
STORY_POINTS_FIELD = 'customfield_10007'.freeze
|
11
|
+
TRIAGER_FIELD = 'customfield_14100'.freeze
|
12
|
+
SLA_TRIAGED_AT_FIELD = 'customfield_14400'.freeze
|
13
|
+
OVER_TRIAGE_SLA_FIELD = 'customfield_14200'.freeze
|
14
|
+
OVER_TRIAGE_SLA_FIELD_YES = '12500'.freeze
|
15
|
+
OVER_TRIAGE_SLA_FIELD_NO = '12501'.freeze
|
16
|
+
API_OVER_TRIAGE_SLA_FIELD_YES = {"self"=>"https://optoro.atlassian.net/rest/api/2/customFieldOption/12500", "value"=>"Yes", "id"=>"12500"}.freeze
|
17
|
+
API_OVER_TRIAGE_SLA_FIELD_NO = {"self"=>"https://optoro.atlassian.net/rest/api/2/customFieldOption/12501", "value"=>"No", "id"=>"12501"}.freeze
|
18
|
+
SLA_DUE_TIME_FIELD = 'customfield_12900'.freeze
|
19
|
+
SLA_DUE_WARNING_FIELD = 'customfield_13401'.freeze
|
20
|
+
SLA_DUE_WARNING_FIELD_SET = '12100'.freeze
|
21
|
+
SLA_CLOSED_AT_FIELD = 'customfield_14000'.freeze
|
22
|
+
OVER_SLA_FIELD = 'customfield_12901'.freeze
|
23
|
+
OVER_SLA_FIELD_YES = '11901'.freeze
|
24
|
+
OVER_SLA_FIELD_NO = '11900'.freeze
|
25
|
+
TOTAL_TIME_OVER_SLA_FIELD = 'customfield_14201'.freeze
|
26
|
+
API_OVER_SLA_FIELD_YES = {"self"=>"https://optoro.atlassian.net/rest/api/2/customFieldOption/11901", "value"=>"Yes", "id"=>"11901"}.freeze
|
27
|
+
API_OVER_SLA_FIELD_NO = {"self"=>"https://optoro.atlassian.net/rest/api/2/customFieldOption/11900", "value"=>"No", "id"=>"11900"}.freeze
|
28
|
+
AFFECTED_USER_GROUP_FIELD = 'customfield_13602'.freeze
|
29
|
+
|
30
|
+
attr_accessor :key, :created_at, :type, :priority_name, :status, :orig,
|
31
|
+
:url, :project, :story_points, :raw_sprints, :description,
|
32
|
+
:summary, :labels, :assigned_team, :changelog, :sla_due_time,
|
33
|
+
:sla_due_warning, :over_sla,:affected_user_group,
|
34
|
+
:cant_reproduce, :in_progress, :not_accepted, :requester_denied,
|
35
|
+
:requester_review, :resolved, :merged, :merged_st,
|
36
|
+
:on_production, :deploy, :completed, :reverify, :pr_review,
|
37
|
+
:triaged, :sla_triaged_at, :over_triage_sla, :triager,
|
38
|
+
:verified, :sla_closed_at, :total_time_over_sla
|
39
|
+
|
40
|
+
def initialize(issue)
|
41
|
+
issue.extend(DotNotation)
|
42
|
+
self.key = issue.dot 'key'
|
43
|
+
self.created_at = Time.parse(issue.dot('fields.created'))
|
44
|
+
self.type = issue.dot 'fields.issuetype.name'
|
45
|
+
self.priority_name = issue.dot 'fields.priority.name'
|
46
|
+
self.status = issue.dot('fields.status.name').downcase
|
47
|
+
self.project = issue.dot 'fields.project.key'
|
48
|
+
self.description = issue.dot 'fields.description'
|
49
|
+
self.summary = issue.dot 'fields.summary'
|
50
|
+
self.labels = issue.dot 'fields.labels'
|
51
|
+
self.assigned_team = issue.dot 'fields.customfield_12601.value'
|
52
|
+
self.story_points = issue.dot "fields.#{STORY_POINTS_FIELD}"
|
53
|
+
self.orig = issue
|
54
|
+
self.url = "https://optoro.atlassian.net/browse/#{key}"
|
55
|
+
self.raw_sprints = issue.dot 'fields.customfield_10007'
|
56
|
+
self.sla_triaged_at = Time.parse(issue.dot("fields.#{SLA_TRIAGED_AT_FIELD}")) if issue.dot("fields.#{SLA_TRIAGED_AT_FIELD}").present?
|
57
|
+
self.over_triage_sla = issue.dot "fields.#{OVER_TRIAGE_SLA_FIELD}"
|
58
|
+
self.triager = issue.dot "fields.#{TRIAGER_FIELD}.name"
|
59
|
+
self.sla_due_time = Time.parse(issue.dot("fields.#{SLA_DUE_TIME_FIELD}")) if issue.dot("fields.#{SLA_DUE_TIME_FIELD}").present?
|
60
|
+
self.sla_due_warning = issue.dot "fields.#{SLA_DUE_WARNING_FIELD}"
|
61
|
+
self.sla_closed_at = Time.parse(issue.dot("fields.#{SLA_CLOSED_AT_FIELD}")) if issue.dot("fields.#{SLA_CLOSED_AT_FIELD}").present?
|
62
|
+
self.over_sla = issue.dot "fields.#{OVER_SLA_FIELD}"
|
63
|
+
self.total_time_over_sla = issue.dot "fields.#{TOTAL_TIME_OVER_SLA_FIELD}"
|
64
|
+
self.affected_user_group = issue.dot "fields.#{AFFECTED_USER_GROUP_FIELD}"
|
65
|
+
|
66
|
+
inv = self.class.custom_time_fields.invert
|
67
|
+
inv.each do |name, key|
|
68
|
+
x = name.gsub(/'/,'').gsub(/ +/, '_').downcase.gsub(/support_xt_/,'')
|
69
|
+
v = issue.dot "fields.#{key}"
|
70
|
+
v = Time.parse(v) if v
|
71
|
+
x = "pr_review" if x == "review" # rename
|
72
|
+
send("#{x}=", v)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def jiralicious_issue
|
77
|
+
Jiralicious::Issue.find(key)
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.find(query, offset = 0)
|
81
|
+
ask = 100
|
82
|
+
result = Jiralicious.search(query, start_at: offset, max_results: ask)
|
83
|
+
issues = result.issues_raw
|
84
|
+
issues.map! { |i| new(i) }
|
85
|
+
if issues.count == ask && result.num_results > offset + ask
|
86
|
+
issues += find(query, offset + ask)
|
87
|
+
end
|
88
|
+
issues
|
89
|
+
rescue Jiralicious::JqlError => e
|
90
|
+
$stderr.puts "Error: #{e}"
|
91
|
+
[]
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.custom_time_fields
|
95
|
+
Hash[[
|
96
|
+
[10812, "Support xt Can't Reproduce"],
|
97
|
+
[10808, 'Support xt In Progress'],
|
98
|
+
[10809, 'Support xt Not Accepted'],
|
99
|
+
[10817, 'Support xt Requester Denied'],
|
100
|
+
[10814, 'Support xt Requester Review'],
|
101
|
+
[13101, 'Support xt Merged'],
|
102
|
+
[13102, 'Support xt On Production'],
|
103
|
+
[10816, 'Support xt Resolved'],
|
104
|
+
[10815, 'Support xt Reverify'],
|
105
|
+
[10813, 'Support xt Review'],
|
106
|
+
[10810, 'Support xt Triaged'],
|
107
|
+
[10811, 'Support xt Verified']
|
108
|
+
].map{|k,v| ["customfield_#{k}", v]}]
|
109
|
+
end
|
110
|
+
|
111
|
+
def open?
|
112
|
+
!closed?
|
113
|
+
# ["unverified", "triaged", "verified",
|
114
|
+
# "in progress", "pr_review", "pr review"].include? status
|
115
|
+
end
|
116
|
+
|
117
|
+
def closed?
|
118
|
+
[ "requester review",
|
119
|
+
"merged",
|
120
|
+
"merged_st",
|
121
|
+
"on production",
|
122
|
+
"resolved",
|
123
|
+
"not accepted",
|
124
|
+
"can't reproduce",
|
125
|
+
"deploy",
|
126
|
+
"completed"
|
127
|
+
].include? status
|
128
|
+
end
|
129
|
+
|
130
|
+
def closed_at
|
131
|
+
[ requester_review,
|
132
|
+
merged,
|
133
|
+
merged_st,
|
134
|
+
on_production,
|
135
|
+
resolved,
|
136
|
+
not_accepted,
|
137
|
+
cant_reproduce,
|
138
|
+
deploy,
|
139
|
+
completed
|
140
|
+
].compact.min
|
141
|
+
end
|
142
|
+
|
143
|
+
def bug?
|
144
|
+
type == "Bug / Outage"
|
145
|
+
end
|
146
|
+
|
147
|
+
def operational?
|
148
|
+
type == "Operational Request"
|
149
|
+
end
|
150
|
+
|
151
|
+
def feature?
|
152
|
+
type == "Feature Enhancement"
|
153
|
+
end
|
154
|
+
|
155
|
+
def research?
|
156
|
+
type == "Research"
|
157
|
+
end
|
158
|
+
|
159
|
+
def affected_user_groups
|
160
|
+
affected_user_group.map {|user_group| user_group["value"]} unless affected_user_group.nil?
|
161
|
+
end
|
162
|
+
|
163
|
+
def affects_warehouse_users?
|
164
|
+
return false if affected_user_groups.nil?
|
165
|
+
affected_user_groups.include?("WH Users")
|
166
|
+
end
|
167
|
+
|
168
|
+
def affects_customers?
|
169
|
+
return false if affected_user_groups.nil?
|
170
|
+
affected_user_groups.include?("Customers")
|
171
|
+
end
|
172
|
+
|
173
|
+
def affects_client_corporate_users?
|
174
|
+
return false if affected_user_groups.nil?
|
175
|
+
affected_user_groups.include?("Client Corporate Users")
|
176
|
+
end
|
177
|
+
|
178
|
+
def affects_internal_optoro_users?
|
179
|
+
return false if affected_user_groups.nil?
|
180
|
+
affected_user_groups.include?("Internal Optoro Users")
|
181
|
+
end
|
182
|
+
|
183
|
+
def priority
|
184
|
+
case priority_name
|
185
|
+
when /P1/
|
186
|
+
:p1
|
187
|
+
when /P2/
|
188
|
+
:p2
|
189
|
+
when /P3/
|
190
|
+
:p3
|
191
|
+
when /P4/
|
192
|
+
:p4
|
193
|
+
else
|
194
|
+
:p5
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def time_to_qa
|
199
|
+
if triaged && (verified || cant_reproduce || in_progress)
|
200
|
+
if verified
|
201
|
+
verified - triaged
|
202
|
+
elsif cant_reproduce
|
203
|
+
cant_reproduce - triaged
|
204
|
+
else
|
205
|
+
in_progress - triaged
|
206
|
+
end
|
207
|
+
elsif triaged
|
208
|
+
Time.now - triaged
|
209
|
+
else
|
210
|
+
nil
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def sprints
|
215
|
+
if raw_sprints
|
216
|
+
raw_sprints.map {|s| Hash[s.match(/\[(.*)\]/)[1].split(',').map{|f|f.split('=')}]}
|
217
|
+
else
|
218
|
+
[]
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def changelog
|
223
|
+
return @changelog if @changelog
|
224
|
+
changelog = Jiralicious::Issue.fetch(key: key, body_to_params: true, body: {expand: 'changelog'})
|
225
|
+
@changelog = changelog["changelog"]["histories"]
|
226
|
+
end
|
227
|
+
|
228
|
+
def changelog_simple
|
229
|
+
changelog.map { |c|
|
230
|
+
c["items"].map { |i|
|
231
|
+
i.slice("field", "fromString","toString")
|
232
|
+
.merge({
|
233
|
+
"created" => c["created"],
|
234
|
+
"author_out" => c["author"]["name"] || ""
|
235
|
+
})
|
236
|
+
}
|
237
|
+
}.flatten
|
238
|
+
end
|
239
|
+
|
240
|
+
def status_changes
|
241
|
+
status_changes = changelog_simple.select{|c| c["field"] == "status"}
|
242
|
+
start = created_at
|
243
|
+
author = orig["fields"]["reporter"].andand["name"] || ""
|
244
|
+
status_changes.each { |c|
|
245
|
+
c["age_hours"] = (Time.parse(c["created"]).to_i - start.to_i)/60.0/60
|
246
|
+
c["started"] = start
|
247
|
+
start = Time.parse(c["created"])
|
248
|
+
cur_author = author
|
249
|
+
author = c["author_out"]
|
250
|
+
c["author_in"] = cur_author
|
251
|
+
}
|
252
|
+
statuses = status_changes.map { |s|
|
253
|
+
{
|
254
|
+
status: s["fromString"],
|
255
|
+
start: s["started"],
|
256
|
+
end: Time.parse(s["created"]),
|
257
|
+
author_in: s["author_in"],
|
258
|
+
author_out: s["author_out"],
|
259
|
+
age_hours: s["age_hours"]
|
260
|
+
}
|
261
|
+
}
|
262
|
+
statuses << {
|
263
|
+
status: status,
|
264
|
+
start: start,
|
265
|
+
author_in: author
|
266
|
+
}
|
267
|
+
|
268
|
+
end
|
269
|
+
|
270
|
+
def status_ages
|
271
|
+
ages = {}
|
272
|
+
status_changes.each{|c| ages[c[:status]] = ages.fetch(c[:status], 0) + c[:age_hours].to_r.round(2).to_f}
|
273
|
+
ages
|
274
|
+
end
|
275
|
+
|
276
|
+
def status_duration_during(status, start_date, end_date)
|
277
|
+
ages = {}
|
278
|
+
status_changes.each{|c| ages[c["fromString"]] = ages.fetch(c["fromString"],0) + c["age_hours"]}
|
279
|
+
status_changes.each{|c| ages[c[:status]] = ages.fetch(c[:status], 0) + c[:age_hours].to_r.round(2).to_f}
|
280
|
+
last_status = status_changes.last
|
281
|
+
if last_status
|
282
|
+
ages[last_status["toString"]] =
|
283
|
+
ages.fetch(last_status["toString"], 0) +
|
284
|
+
((Time.now.to_f - Time.parse(last_status["created"]).to_f)/60.0/60)
|
285
|
+
end
|
286
|
+
ages
|
287
|
+
end
|
288
|
+
end
|