jirametrics 2.8 → 2.9.1pre1
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 +4 -4
- data/lib/jirametrics/aging_work_bar_chart.rb +7 -5
- data/lib/jirametrics/board.rb +2 -3
- data/lib/jirametrics/board_config.rb +6 -2
- data/lib/jirametrics/chart_base.rb +36 -9
- data/lib/jirametrics/cycletime_config.rb +10 -4
- data/lib/jirametrics/data_quality_report.rb +53 -34
- data/lib/jirametrics/examples/standard_project.rb +2 -2
- data/lib/jirametrics/exporter.rb +0 -12
- data/lib/jirametrics/file_config.rb +12 -0
- data/lib/jirametrics/file_system.rb +19 -4
- data/lib/jirametrics/groupable_issue_chart.rb +1 -3
- data/lib/jirametrics/html/aging_work_bar_chart.erb +1 -10
- data/lib/jirametrics/html/aging_work_table.erb +1 -1
- data/lib/jirametrics/html/cycletime_scatterplot.erb +1 -10
- data/lib/jirametrics/html/daily_wip_chart.erb +1 -10
- data/lib/jirametrics/html/expedited_chart.erb +1 -10
- data/lib/jirametrics/html/hierarchy_table.erb +1 -1
- data/lib/jirametrics/html/sprint_burndown.erb +1 -10
- data/lib/jirametrics/html/throughput_chart.erb +1 -10
- data/lib/jirametrics/html_report_config.rb +18 -23
- data/lib/jirametrics/issue.rb +51 -27
- data/lib/jirametrics/project_config.rb +102 -45
- data/lib/jirametrics/status.rb +23 -1
- data/lib/jirametrics/status_collection.rb +69 -68
- data/lib/jirametrics/value_equality.rb +2 -2
- data/lib/jirametrics.rb +0 -1
- metadata +4 -9
- data/lib/jirametrics/discard_changes_before.rb +0 -37
@@ -5,16 +5,15 @@ require 'jirametrics/self_or_issue_dispatcher'
|
|
5
5
|
|
6
6
|
class HtmlReportConfig
|
7
7
|
include SelfOrIssueDispatcher
|
8
|
-
include DiscardChangesBefore
|
9
8
|
|
10
|
-
attr_reader :file_config, :sections
|
9
|
+
attr_reader :file_config, :sections, :charts
|
11
10
|
|
12
11
|
def self.define_chart name:, classname:, deprecated_warning: nil, deprecated_date: nil
|
13
12
|
lines = []
|
14
13
|
lines << "def #{name} &block"
|
15
14
|
lines << ' block = ->(_) {} unless block'
|
16
15
|
if deprecated_warning
|
17
|
-
lines << " deprecated date: #{deprecated_date.inspect}, message: #{deprecated_warning.inspect}"
|
16
|
+
lines << " file_system.deprecated date: #{deprecated_date.inspect}, message: #{deprecated_warning.inspect}"
|
18
17
|
end
|
19
18
|
lines << " execute_chart #{classname}.new(block)"
|
20
19
|
lines << 'end'
|
@@ -43,14 +42,15 @@ class HtmlReportConfig
|
|
43
42
|
def initialize file_config:, block:
|
44
43
|
@file_config = file_config
|
45
44
|
@block = block
|
46
|
-
@sections = []
|
45
|
+
@sections = [] # Where we store the chunks of text that will be assembled into the HTML
|
46
|
+
@charts = [] # Where we store all the charts we executed so we can assert against them.
|
47
47
|
end
|
48
48
|
|
49
49
|
def cycletime label = nil, &block
|
50
50
|
@file_config.project_config.all_boards.each_value do |board|
|
51
51
|
raise 'Multiple cycletimes not supported' if board.cycletime
|
52
52
|
|
53
|
-
board.cycletime = CycleTimeConfig.new(parent_config: self, label: label, block: block)
|
53
|
+
board.cycletime = CycleTimeConfig.new(parent_config: self, label: label, block: block, file_system: file_system)
|
54
54
|
end
|
55
55
|
end
|
56
56
|
|
@@ -64,7 +64,7 @@ class HtmlReportConfig
|
|
64
64
|
|
65
65
|
# The quality report has to be generated last because otherwise cycletime won't have been
|
66
66
|
# set. Then we have to rotate it to the first position so it's at the top of the report.
|
67
|
-
execute_chart DataQualityReport.new(
|
67
|
+
execute_chart DataQualityReport.new(file_config.project_config.discarded_changes_data)
|
68
68
|
@sections.rotate!(-1)
|
69
69
|
|
70
70
|
html create_footer
|
@@ -101,9 +101,8 @@ class HtmlReportConfig
|
|
101
101
|
base_css
|
102
102
|
end
|
103
103
|
|
104
|
-
def board_id id
|
105
|
-
@board_id = id
|
106
|
-
@board_id
|
104
|
+
def board_id id
|
105
|
+
@board_id = id
|
107
106
|
end
|
108
107
|
|
109
108
|
def timezone_offset
|
@@ -143,19 +142,6 @@ class HtmlReportConfig
|
|
143
142
|
end
|
144
143
|
end
|
145
144
|
|
146
|
-
def discard_changes_before_hook issues_cutoff_times
|
147
|
-
# raise 'Cycletime must be defined before using discard_changes_before' unless @cycletime
|
148
|
-
|
149
|
-
@original_issue_times = {}
|
150
|
-
issues_cutoff_times.each do |issue, cutoff_time|
|
151
|
-
started = issue.board.cycletime.started_stopped_times(issue).first
|
152
|
-
if started && started <= cutoff_time
|
153
|
-
# We only need to log this if data was discarded
|
154
|
-
@original_issue_times[issue] = { cutoff_time: cutoff_time, started_time: started }
|
155
|
-
end
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
145
|
def dependency_chart &block
|
160
146
|
execute_chart DependencyChart.new block
|
161
147
|
end
|
@@ -175,7 +161,7 @@ class HtmlReportConfig
|
|
175
161
|
chart.settings = settings
|
176
162
|
|
177
163
|
chart.all_boards = project_config.all_boards
|
178
|
-
chart.board_id = find_board_id
|
164
|
+
chart.board_id = find_board_id
|
179
165
|
chart.holiday_dates = project_config.exporter.holiday_dates
|
180
166
|
|
181
167
|
time_range = @file_config.project_config.time_range
|
@@ -184,6 +170,7 @@ class HtmlReportConfig
|
|
184
170
|
|
185
171
|
after_init_block&.call chart
|
186
172
|
|
173
|
+
@charts << chart
|
187
174
|
html chart.run
|
188
175
|
end
|
189
176
|
|
@@ -216,4 +203,12 @@ class HtmlReportConfig
|
|
216
203
|
</section>
|
217
204
|
HTML
|
218
205
|
end
|
206
|
+
|
207
|
+
def discard_changes_before status_becomes: nil, &block
|
208
|
+
file_system.deprecated(
|
209
|
+
date: '2025-01-09',
|
210
|
+
message: 'discard_changes_before is now only supported at the project level'
|
211
|
+
)
|
212
|
+
file_config.project_config.discard_changes_before status_becomes: status_becomes, &block
|
213
|
+
end
|
219
214
|
end
|
data/lib/jirametrics/issue.rb
CHANGED
@@ -13,7 +13,12 @@ class Issue
|
|
13
13
|
@changes = []
|
14
14
|
@board = board
|
15
15
|
|
16
|
+
# We only check for this here because if a board isn't passed in then things will fail much
|
17
|
+
# later and be hard to find. Let's find out early.
|
16
18
|
raise "No board for issue #{key}" if board.nil?
|
19
|
+
|
20
|
+
# There are cases where we create an Issue of fragments like linked issues and those won't have
|
21
|
+
# changelogs.
|
17
22
|
return unless @raw['changelog']
|
18
23
|
|
19
24
|
load_history_into_changes
|
@@ -93,9 +98,7 @@ class Issue
|
|
93
98
|
|
94
99
|
def still_in
|
95
100
|
result = nil
|
96
|
-
|
97
|
-
next unless change.status?
|
98
|
-
|
101
|
+
status_changes.each do |change|
|
99
102
|
current_status_matched = yield change
|
100
103
|
|
101
104
|
if current_status_matched && result.nil?
|
@@ -117,63 +120,71 @@ class Issue
|
|
117
120
|
|
118
121
|
# If it ever entered one of these categories and it's still there then what was the last time it entered
|
119
122
|
def still_in_status_category *category_names
|
123
|
+
category_ids = find_status_category_ids_by_names category_names
|
124
|
+
|
120
125
|
still_in do |change|
|
121
|
-
status =
|
122
|
-
|
126
|
+
status = find_or_create_status id: change.value_id, name: change.value
|
127
|
+
category_ids.include? status.category.id
|
123
128
|
end
|
124
129
|
end
|
125
130
|
|
126
131
|
def most_recent_status_change
|
132
|
+
# We artificially insert a status change to represent creation so by definition there will always be at least one.
|
127
133
|
changes.reverse.find { |change| change.status? }
|
128
134
|
end
|
129
135
|
|
130
136
|
# Are we currently in this status? If yes, then return the most recent status change.
|
131
137
|
def currently_in_status *status_names
|
132
138
|
change = most_recent_status_change
|
133
|
-
return false if change.nil?
|
134
139
|
|
135
140
|
change if change.current_status_matches(*status_names)
|
136
141
|
end
|
137
142
|
|
138
143
|
# Are we currently in this status category? If yes, then return the most recent status change.
|
139
144
|
def currently_in_status_category *category_names
|
145
|
+
category_ids = find_status_category_ids_by_names category_names
|
146
|
+
|
140
147
|
change = most_recent_status_change
|
141
|
-
return false if change.nil?
|
142
148
|
|
143
|
-
status =
|
144
|
-
change if status &&
|
149
|
+
status = find_or_create_status id: change.value_id, name: change.value
|
150
|
+
change if status && category_ids.include?(status.category.id)
|
145
151
|
end
|
146
152
|
|
147
|
-
def
|
153
|
+
def find_or_create_status id:, name:
|
148
154
|
status = board.possible_statuses.find_by_id(id)
|
149
|
-
return status if status
|
150
155
|
|
151
|
-
status
|
156
|
+
unless status
|
157
|
+
# Have to pull this list before the call to fabricate or else the warning will incorrectly
|
158
|
+
# list this status as one it actually found
|
159
|
+
found_statuses = board.possible_statuses.to_s
|
160
|
+
|
161
|
+
status = board.possible_statuses.fabricate_status_for id: id, name: name
|
152
162
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
+
message = +'The history for issue '
|
164
|
+
message << key
|
165
|
+
message << ' references the status ('
|
166
|
+
message << "#{name.inspect}:#{id.inspect}"
|
167
|
+
message << ') that can\'t be found. We are guessing that this belongs to the '
|
168
|
+
message << status.category.to_s
|
169
|
+
message << ' status category but that may be wrong. See https://jirametrics.org/faq/#q1 for more '
|
170
|
+
message << 'details on defining statuses.'
|
171
|
+
board.project_config.file_system.warning message, more: "The statuses we did find are: #{found_statuses}"
|
172
|
+
end
|
163
173
|
|
164
174
|
status
|
165
175
|
end
|
166
176
|
|
167
177
|
def first_status_change_after_created
|
168
|
-
|
178
|
+
status_changes.find { |change| change.artificial? == false }
|
169
179
|
end
|
170
180
|
|
171
181
|
def first_time_in_status_category *category_names
|
172
|
-
|
173
|
-
next unless change.status?
|
182
|
+
category_ids = find_status_category_ids_by_names category_names
|
174
183
|
|
175
|
-
|
176
|
-
|
184
|
+
status_changes.each do |change|
|
185
|
+
to_status = find_or_create_status(id: change.value_id, name: change.value)
|
186
|
+
id = to_status.category.id
|
187
|
+
return change if category_ids.include? id
|
177
188
|
end
|
178
189
|
nil
|
179
190
|
end
|
@@ -645,6 +656,10 @@ class Issue
|
|
645
656
|
end
|
646
657
|
end
|
647
658
|
|
659
|
+
def status_changes
|
660
|
+
@changes.select { |change| change.status? }
|
661
|
+
end
|
662
|
+
|
648
663
|
private
|
649
664
|
|
650
665
|
def assemble_author raw
|
@@ -720,4 +735,13 @@ class Issue
|
|
720
735
|
'toString' => first_status
|
721
736
|
}
|
722
737
|
end
|
738
|
+
|
739
|
+
def find_status_category_ids_by_names category_names
|
740
|
+
category_names.filter_map do |name|
|
741
|
+
list = board.possible_statuses.find_all_categories_by_name name
|
742
|
+
raise "No status categories found for name: #{name}" if list.empty?
|
743
|
+
|
744
|
+
list
|
745
|
+
end.flatten.collect(&:id)
|
746
|
+
end
|
723
747
|
end
|
@@ -4,11 +4,9 @@ require 'time'
|
|
4
4
|
require 'jirametrics/status_collection'
|
5
5
|
|
6
6
|
class ProjectConfig
|
7
|
-
include DiscardChangesBefore
|
8
|
-
|
9
7
|
attr_reader :target_path, :jira_config, :all_boards, :possible_statuses,
|
10
8
|
:download_config, :file_configs, :exporter, :data_version, :name, :board_configs,
|
11
|
-
:settings, :aggregate_config
|
9
|
+
:settings, :aggregate_config, :discarded_changes_data
|
12
10
|
attr_accessor :time_range, :jira_url, :id
|
13
11
|
|
14
12
|
def initialize exporter:, jira_config:, block:, target_path: '.', name: '', id: nil
|
@@ -32,14 +30,13 @@ class ProjectConfig
|
|
32
30
|
end
|
33
31
|
|
34
32
|
def data_downloaded?
|
35
|
-
|
33
|
+
file_system.file_exist? File.join(@target_path, "#{get_file_prefix}_meta.json")
|
36
34
|
end
|
37
35
|
|
38
36
|
def load_data
|
39
37
|
return if @has_loaded_data
|
40
38
|
|
41
39
|
@has_loaded_data = true
|
42
|
-
load_all_boards
|
43
40
|
@id = guess_project_id
|
44
41
|
load_project_metadata
|
45
42
|
load_sprints
|
@@ -53,16 +50,13 @@ class ProjectConfig
|
|
53
50
|
|
54
51
|
return if load_only
|
55
52
|
|
56
|
-
@board_configs.each do |board_config|
|
57
|
-
board_config.run
|
58
|
-
end
|
59
53
|
@file_configs.each do |file_config|
|
60
54
|
file_config.run
|
61
55
|
end
|
62
56
|
end
|
63
57
|
|
64
58
|
def load_settings
|
65
|
-
# This is the
|
59
|
+
# This is the weird exception that we don't ever want mocked out so we skip FileSystem entirely.
|
66
60
|
JSON.parse(File.read(File.join(__dir__, 'settings.json'), encoding: 'UTF-8'))
|
67
61
|
end
|
68
62
|
|
@@ -112,6 +106,7 @@ class ProjectConfig
|
|
112
106
|
|
113
107
|
def board id:, &block
|
114
108
|
config = BoardConfig.new(id: id, block: block, project_config: self)
|
109
|
+
config.run if data_downloaded?
|
115
110
|
@board_configs << config
|
116
111
|
end
|
117
112
|
|
@@ -128,6 +123,8 @@ class ProjectConfig
|
|
128
123
|
# is set but before anything inside the project block is run. If only we had made file_prefix an attribute
|
129
124
|
# on project, we wouldn't have this ugliness. 🤷♂️
|
130
125
|
load_status_category_mappings
|
126
|
+
load_status_history
|
127
|
+
load_all_boards
|
131
128
|
|
132
129
|
@file_prefix
|
133
130
|
end
|
@@ -138,29 +135,28 @@ class ProjectConfig
|
|
138
135
|
@file_prefix
|
139
136
|
end
|
140
137
|
|
141
|
-
|
142
|
-
|
138
|
+
# Walk across all the issues and find any status with that name. Return a list of ids that match.
|
139
|
+
def find_ids_by_status_name_across_all_issues name
|
140
|
+
ids = Set.new
|
141
|
+
|
143
142
|
issues.each do |issue|
|
144
143
|
issue.changes.each do |change|
|
145
144
|
next unless change.status?
|
146
145
|
|
147
|
-
|
148
|
-
|
146
|
+
ids << change.value_id.to_i if change.value == name
|
147
|
+
ids << change.old_value_id.to_i if change.old_value == name
|
149
148
|
end
|
150
149
|
end
|
151
|
-
|
150
|
+
ids.to_a
|
152
151
|
end
|
153
152
|
|
154
153
|
def status_category_mapping status:, category:
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
status, status_id = parse_status.call status
|
159
|
-
category, category_id = parse_status.call category
|
154
|
+
status, status_id = possible_statuses.parse_name_id status
|
155
|
+
category, category_id = possible_statuses.parse_name_id category
|
160
156
|
|
161
157
|
if status_id.nil?
|
162
|
-
guesses =
|
163
|
-
if guesses.
|
158
|
+
guesses = find_ids_by_status_name_across_all_issues status
|
159
|
+
if guesses.empty?
|
164
160
|
file_system.warning "For status_category_mapping status: #{status.inspect}, category: #{category.inspect}\n" \
|
165
161
|
"Cannot guess status id for #{status.inspect} as no statuses found anywhere in the issues " \
|
166
162
|
"histories with that name. Since we can't find it, you probably don't need this mapping anymore so we're " \
|
@@ -174,21 +170,31 @@ class ProjectConfig
|
|
174
170
|
end
|
175
171
|
|
176
172
|
status_id = guesses.first
|
177
|
-
# TODO: This should be a deprecation instead but we can't currently assert against those
|
178
173
|
file_system.log "status_category_mapping for #{status.inspect} has been mapped to id #{status_id}. " \
|
179
174
|
"If that's incorrect then specify the status_id."
|
180
175
|
end
|
181
176
|
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
raise "
|
177
|
+
possible_categories = possible_statuses.find_all_categories_by_name category
|
178
|
+
if possible_categories.empty?
|
179
|
+
all = possible_statuses.find_all_categories.join(', ')
|
180
|
+
raise "No status categories found for name #{category.inspect} in [#{all}]. " \
|
181
|
+
'Either fix the name or add an ID.'
|
182
|
+
elsif possible_categories.size > 1
|
183
|
+
# Theoretically impossible and yet we've seen wierder things out of Jira so we're prepared.
|
184
|
+
raise "More than one status category found with the name #{category.inspect} in " \
|
185
|
+
"[#{possible_categories.join(', ')}]. Either fix the name or add an ID"
|
186
|
+
end
|
187
|
+
|
188
|
+
found_category = possible_categories.first
|
189
|
+
|
190
|
+
if category_id && category_id != found_category.id
|
191
|
+
raise "ID is incorrect for status category #{category.inspect}. Did you mean #{found_category.id}?"
|
186
192
|
end
|
187
193
|
|
188
194
|
add_possible_status(
|
189
195
|
Status.new(
|
190
196
|
name: status, id: status_id,
|
191
|
-
category_name: category, category_id:
|
197
|
+
category_name: category, category_id: found_category.id, category_key: found_category.key
|
192
198
|
)
|
193
199
|
)
|
194
200
|
end
|
@@ -211,7 +217,14 @@ class ProjectConfig
|
|
211
217
|
end
|
212
218
|
|
213
219
|
# We're registering one we already knew about. This may happen if someone specified a status_category_mapping
|
214
|
-
# for something that was already returned from jira.
|
220
|
+
# for something that was already returned from jira.
|
221
|
+
#
|
222
|
+
# You may be looking at this code and thinking of changing it to spit out a warning since obviously
|
223
|
+
# the user has made a mistake. Unfortunately, they may not have made any mistake. Due to inconsistency with the
|
224
|
+
# status API, it's possible for two different people to make a request to the same API at the same time and get
|
225
|
+
# back a different set of statuses. So that means that some people might need more status/categories mappings than
|
226
|
+
# other people for exactly the same instance. See this article for more on that API:
|
227
|
+
# https://agiletechnicalexcellence.com/2024/04/12/jira-api-statuses.html
|
215
228
|
existing_status
|
216
229
|
end
|
217
230
|
|
@@ -222,7 +235,6 @@ class ProjectConfig
|
|
222
235
|
board_id = $1.to_i
|
223
236
|
load_board board_id: board_id, filename: "#{@target_path}#{file}"
|
224
237
|
end
|
225
|
-
raise "No boards found for #{get_file_prefix.inspect} in #{@target_path.inspect}" if @all_boards.empty?
|
226
238
|
end
|
227
239
|
|
228
240
|
def load_board board_id:, filename:
|
@@ -235,9 +247,7 @@ class ProjectConfig
|
|
235
247
|
|
236
248
|
def load_status_category_mappings
|
237
249
|
filename = File.join @target_path, "#{get_file_prefix}_statuses.json"
|
238
|
-
|
239
|
-
# We may not always have this file. Load it if we can.
|
240
|
-
return unless File.exist? filename
|
250
|
+
return unless file_system.file_exist? filename
|
241
251
|
|
242
252
|
file_system
|
243
253
|
.load_json(filename)
|
@@ -245,6 +255,22 @@ class ProjectConfig
|
|
245
255
|
.each { |status| add_possible_status status }
|
246
256
|
end
|
247
257
|
|
258
|
+
def load_status_history
|
259
|
+
filename = File.join @target_path, "#{get_file_prefix}_status_history.json"
|
260
|
+
return unless file_system.file_exist? filename
|
261
|
+
|
262
|
+
file_system.log ' Loading historical statuses', also_write_to_stderr: true
|
263
|
+
file_system
|
264
|
+
.load_json(filename)
|
265
|
+
.map { |snippet| Status.from_raw(snippet) }
|
266
|
+
.each { |status| possible_statuses.historical_status_mappings[status.to_s] = status.category }
|
267
|
+
|
268
|
+
possible_statuses
|
269
|
+
rescue => e # rubocop:disable Style/RescueStandardError
|
270
|
+
file_system.warning "Unable to load status history due to #{e.message.inspect}. If this is because of a " \
|
271
|
+
'malformed file then it should be fixed on the next download.'
|
272
|
+
end
|
273
|
+
|
248
274
|
def load_sprints
|
249
275
|
file_system.foreach(@target_path) do |file|
|
250
276
|
next unless file =~ /^#{get_file_prefix}_board_(\d+)_sprints_\d+.json$/
|
@@ -478,21 +504,52 @@ class ProjectConfig
|
|
478
504
|
Anonymizer.new(project_config: self).run
|
479
505
|
end
|
480
506
|
|
481
|
-
def
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
507
|
+
def file_system
|
508
|
+
@exporter.file_system
|
509
|
+
end
|
510
|
+
|
511
|
+
def discard_changes_before status_becomes: nil, &block
|
512
|
+
if status_becomes
|
513
|
+
status_becomes = [status_becomes] unless status_becomes.is_a? Array
|
514
|
+
|
515
|
+
block = lambda do |issue|
|
516
|
+
trigger_statuses = status_becomes.collect do |status_name|
|
517
|
+
if status_name == :backlog
|
518
|
+
issue.board.backlog_statuses
|
519
|
+
else
|
520
|
+
possible_statuses.find_all_by_name status_name
|
521
|
+
end
|
522
|
+
end.flatten
|
523
|
+
|
524
|
+
next if trigger_statuses.empty?
|
525
|
+
|
526
|
+
trigger_status_ids = trigger_statuses.collect(&:id)
|
527
|
+
|
528
|
+
time = nil
|
529
|
+
issue.status_changes.each do |change|
|
530
|
+
time = change.time if trigger_status_ids.include?(change.value_id) # && change.artificial? == false
|
531
|
+
end
|
532
|
+
time
|
489
533
|
end
|
490
|
-
exporter.file_system.log message
|
491
534
|
end
|
492
|
-
exporter.file_system.log "Discarded data from #{issues_cutoff_times.count} issues out of a total #{issues.size}"
|
493
|
-
end
|
494
535
|
|
495
|
-
|
496
|
-
|
536
|
+
issues.each do |issue|
|
537
|
+
cutoff_time = block.call(issue)
|
538
|
+
next if cutoff_time.nil?
|
539
|
+
|
540
|
+
original_start_time = issue.board.cycletime.started_stopped_times(issue).first
|
541
|
+
next if original_start_time.nil?
|
542
|
+
|
543
|
+
issue.discard_changes_before cutoff_time
|
544
|
+
|
545
|
+
next unless cutoff_time
|
546
|
+
next if original_start_time > cutoff_time # ie the cutoff would have made no difference.
|
547
|
+
|
548
|
+
(@discarded_changes_data ||= []) << {
|
549
|
+
cutoff_time: cutoff_time,
|
550
|
+
original_start_time: original_start_time,
|
551
|
+
issue: issue
|
552
|
+
}
|
553
|
+
end
|
497
554
|
end
|
498
555
|
end
|
data/lib/jirametrics/status.rb
CHANGED
@@ -3,7 +3,6 @@
|
|
3
3
|
require 'jirametrics/value_equality'
|
4
4
|
|
5
5
|
class Status
|
6
|
-
include ValueEquality
|
7
6
|
attr_reader :id, :project_id, :category
|
8
7
|
attr_accessor :name
|
9
8
|
|
@@ -20,6 +19,17 @@ class Status
|
|
20
19
|
"#{name.inspect}:#{id.inspect}"
|
21
20
|
end
|
22
21
|
|
22
|
+
def <=> other
|
23
|
+
id <=> other.id
|
24
|
+
end
|
25
|
+
|
26
|
+
def == other
|
27
|
+
id == other.id
|
28
|
+
end
|
29
|
+
|
30
|
+
def eql?(other) = id.eql?(other.id)
|
31
|
+
def hash = id.hash
|
32
|
+
|
23
33
|
def new? = (@key == 'new')
|
24
34
|
def indeterminate? = (@key == 'indeterminate')
|
25
35
|
def done? = (@key == 'done')
|
@@ -74,9 +84,21 @@ class Status
|
|
74
84
|
end
|
75
85
|
|
76
86
|
def == other
|
87
|
+
return false unless other.is_a? Status
|
88
|
+
|
77
89
|
@id == other.id && @name == other.name && @category.id == other.category.id && @category.name == other.category.name
|
78
90
|
end
|
79
91
|
|
92
|
+
def eql?(other)
|
93
|
+
self == other
|
94
|
+
end
|
95
|
+
|
96
|
+
def <=> other
|
97
|
+
result = @name.casecmp(other.name)
|
98
|
+
result = @id <=> other.id if result.zero?
|
99
|
+
result
|
100
|
+
end
|
101
|
+
|
80
102
|
def inspect
|
81
103
|
result = []
|
82
104
|
result << "Status(name: #{@name.inspect}"
|