jira_reporting 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|