jirametrics 2.11 → 2.12.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 +4 -4
- data/lib/jirametrics/aging_work_table.rb +6 -11
- data/lib/jirametrics/board.rb +8 -9
- data/lib/jirametrics/board_config.rb +1 -0
- data/lib/jirametrics/board_movement_calculator.rb +8 -0
- data/lib/jirametrics/change_item.rb +29 -15
- data/lib/jirametrics/chart_base.rb +3 -4
- data/lib/jirametrics/cycletime_config.rb +1 -1
- data/lib/jirametrics/daily_view.rb +277 -0
- data/lib/jirametrics/downloader.rb +11 -0
- data/lib/jirametrics/estimate_accuracy_chart.rb +34 -10
- data/lib/jirametrics/estimation_configuration.rb +25 -0
- data/lib/jirametrics/examples/standard_project.rb +2 -0
- data/lib/jirametrics/file_config.rb +1 -1
- data/lib/jirametrics/html/aging_work_table.erb +2 -0
- data/lib/jirametrics/html/index.css +71 -0
- data/lib/jirametrics/html/index.erb +17 -0
- data/lib/jirametrics/html_report_config.rb +2 -0
- data/lib/jirametrics/issue.rb +39 -19
- data/lib/jirametrics/issue_collection.rb +33 -0
- data/lib/jirametrics/jira_gateway.rb +4 -1
- data/lib/jirametrics/project_config.rb +24 -7
- data/lib/jirametrics/settings.json +2 -1
- data/lib/jirametrics/sprint.rb +1 -0
- data/lib/jirametrics/sprint_burndown.rb +35 -33
- data/lib/jirametrics/sprint_issue_change_data.rb +3 -3
- data/lib/jirametrics/status_collection.rb +6 -0
- data/lib/jirametrics/user.rb +12 -0
- data/lib/jirametrics.rb +4 -0
- metadata +6 -2
@@ -67,6 +67,9 @@
|
|
67
67
|
--sprint-burndown-sprint-color-4: red;
|
68
68
|
--sprint-burndown-sprint-color-5: brown;
|
69
69
|
|
70
|
+
--daily-view-selected-issue-background: lightgray;
|
71
|
+
--daily-view-issue-border: green;
|
72
|
+
--daily-view-selected-issue-border: red;
|
70
73
|
|
71
74
|
}
|
72
75
|
|
@@ -142,6 +145,64 @@ ul.quality_report {
|
|
142
145
|
border-top: 1px solid gray;
|
143
146
|
}
|
144
147
|
|
148
|
+
div.daily_issue:hover {
|
149
|
+
background: var(--daily-view-selected-issue-background);
|
150
|
+
border-color: var(--daily-view-selected-issue-border);
|
151
|
+
}
|
152
|
+
|
153
|
+
div.daily_issue {
|
154
|
+
border: 1px solid var(--daily-view-issue-border);
|
155
|
+
padding: 0.5em;
|
156
|
+
.heading {
|
157
|
+
vertical-align: middle;
|
158
|
+
display: flex;
|
159
|
+
flex-wrap: wrap;
|
160
|
+
column-gap: 0.5em;
|
161
|
+
align-items: center;
|
162
|
+
}
|
163
|
+
table {
|
164
|
+
margin-left: 1em;
|
165
|
+
td {
|
166
|
+
vertical-align: top;
|
167
|
+
}
|
168
|
+
.time {
|
169
|
+
white-space: nowrap;
|
170
|
+
font-size: 0.8em;
|
171
|
+
}
|
172
|
+
}
|
173
|
+
.icon {
|
174
|
+
width: 1em;
|
175
|
+
height: 1em;
|
176
|
+
}
|
177
|
+
.account_id {
|
178
|
+
font-weight: bold;
|
179
|
+
}
|
180
|
+
.field {
|
181
|
+
border: 1px solid black;
|
182
|
+
color: white;
|
183
|
+
background: black;
|
184
|
+
padding-left: 0.2em;
|
185
|
+
padding-right: 0.2em;
|
186
|
+
border-radius: 0.2em;
|
187
|
+
}
|
188
|
+
.label {
|
189
|
+
border: 1px solid black;
|
190
|
+
padding-left: 0.2em;
|
191
|
+
padding-right: 0.2em;
|
192
|
+
border-radius: 0.2em;
|
193
|
+
}
|
194
|
+
margin-bottom: 0.5em;
|
195
|
+
}
|
196
|
+
div.child_issue:hover {
|
197
|
+
background: var(--body-background);
|
198
|
+
}
|
199
|
+
div.child_issue {
|
200
|
+
border: 1px dashed green;
|
201
|
+
margin: 0.2em;
|
202
|
+
margin-left: 1.5em;
|
203
|
+
padding: 0.5em;
|
204
|
+
}
|
205
|
+
|
145
206
|
@media screen and (prefers-color-scheme: dark) {
|
146
207
|
:root {
|
147
208
|
--warning-banner: #9F2B00;
|
@@ -174,6 +235,8 @@ ul.quality_report {
|
|
174
235
|
--wip-chart-duration-two-weeks-or-less-color: #cf9400;
|
175
236
|
--wip-chart-duration-four-weeks-or-less-color: #c25e00;
|
176
237
|
--wip-chart-duration-more-than-four-weeks-color: #8e0000;
|
238
|
+
|
239
|
+
--daily-view-selected-issue-background: #474747;
|
177
240
|
}
|
178
241
|
|
179
242
|
h1 {
|
@@ -206,4 +269,12 @@ ul.quality_report {
|
|
206
269
|
div.color_block {
|
207
270
|
border: 1px solid lightgray;
|
208
271
|
}
|
272
|
+
|
273
|
+
div.daily_issue {
|
274
|
+
.field {
|
275
|
+
color: var(--default-text-color);
|
276
|
+
}
|
277
|
+
}
|
278
|
+
}
|
279
|
+
|
209
280
|
}
|
@@ -18,6 +18,23 @@
|
|
18
18
|
document.getElementById(issues_id).style.display = 'none'
|
19
19
|
}
|
20
20
|
}
|
21
|
+
|
22
|
+
function toggle_visibility(open_link_id, close_link_id, toggleable_id) {
|
23
|
+
let open_link = document.getElementById(open_link_id)
|
24
|
+
let close_link = document.getElementById(close_link_id)
|
25
|
+
let toggleable_element = document.getElementById(toggleable_id)
|
26
|
+
|
27
|
+
if(open_link.style.display == 'none') {
|
28
|
+
open_link.style.display = 'block'
|
29
|
+
close_link.style.display = 'none'
|
30
|
+
toggleable_element.style.display = 'none'
|
31
|
+
}
|
32
|
+
else {
|
33
|
+
open_link.style.display = 'none'
|
34
|
+
close_link.style.display = 'block'
|
35
|
+
toggleable_element.style.display = 'block'
|
36
|
+
}
|
37
|
+
}
|
21
38
|
// If we switch between light/dark mode then force a refresh so all charts will redraw correctly
|
22
39
|
// in the other colour scheme.
|
23
40
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
|
@@ -33,6 +33,7 @@ class HtmlReportConfig
|
|
33
33
|
define_chart name: 'estimate_accuracy_chart', classname: 'EstimateAccuracyChart'
|
34
34
|
define_chart name: 'hierarchy_table', classname: 'HierarchyTable'
|
35
35
|
define_chart name: 'flow_efficiency_scatterplot', classname: 'FlowEfficiencyScatterplot'
|
36
|
+
define_chart name: 'daily_view', classname: 'DailyView'
|
36
37
|
|
37
38
|
define_chart name: 'daily_wip_by_type', classname: 'DailyWipChart',
|
38
39
|
deprecated_warning: 'This is the same as daily_wip_chart. Please use that one', deprecated_date: '2024-05-23'
|
@@ -159,6 +160,7 @@ class HtmlReportConfig
|
|
159
160
|
chart.time_range = project_config.time_range
|
160
161
|
chart.timezone_offset = timezone_offset
|
161
162
|
chart.settings = settings
|
163
|
+
chart.users = project_config.users
|
162
164
|
|
163
165
|
chart.all_boards = project_config.all_boards
|
164
166
|
chart.board_id = find_board_id
|
data/lib/jirametrics/issue.rb
CHANGED
@@ -44,9 +44,11 @@ class Issue
|
|
44
44
|
def key = @raw['key']
|
45
45
|
|
46
46
|
def type = @raw['fields']['issuetype']['name']
|
47
|
-
|
48
47
|
def type_icon_url = @raw['fields']['issuetype']['iconUrl']
|
49
48
|
|
49
|
+
def priority_name = @raw['fields']['priority']['name']
|
50
|
+
def priority_url = @raw['fields']['priority']['iconUrl']
|
51
|
+
|
50
52
|
def summary = @raw['fields']['summary']
|
51
53
|
|
52
54
|
def labels = @raw['fields']['labels'] || []
|
@@ -205,6 +207,10 @@ class Issue
|
|
205
207
|
nil
|
206
208
|
end
|
207
209
|
|
210
|
+
def first_time_visible_on_board
|
211
|
+
first_time_in_status(*board.visible_columns.collect(&:status_ids).flatten)
|
212
|
+
end
|
213
|
+
|
208
214
|
def parse_time text
|
209
215
|
Time.parse(text).getlocal(@timezone_offset)
|
210
216
|
end
|
@@ -230,6 +236,10 @@ class Issue
|
|
230
236
|
@raw['fields']&.[]('assignee')&.[]('displayName')
|
231
237
|
end
|
232
238
|
|
239
|
+
def assigned_to_icon_url
|
240
|
+
@raw['fields']&.[]('assignee')&.[]('avatarUrls')&.[]('16x16')
|
241
|
+
end
|
242
|
+
|
233
243
|
# Many test failures are simply unreadable because the default inspect on this class goes
|
234
244
|
# on for pages. Shorten it up.
|
235
245
|
def inspect
|
@@ -315,7 +325,7 @@ class Issue
|
|
315
325
|
|
316
326
|
# This mock change is to force the writing of one last entry at the end of the time range.
|
317
327
|
# By doing this, we're able to eliminate a lot of duplicated code in charts.
|
318
|
-
mock_change = ChangeItem.new time: end_time,
|
328
|
+
mock_change = ChangeItem.new time: end_time, artificial: true, raw: { 'field' => '' }, author_raw: nil
|
319
329
|
|
320
330
|
(changes + [mock_change]).each do |change|
|
321
331
|
previous_was_active = false if check_for_stalled(
|
@@ -462,8 +472,6 @@ class Issue
|
|
462
472
|
end
|
463
473
|
|
464
474
|
def expedited?
|
465
|
-
return false unless @board&.project_config
|
466
|
-
|
467
475
|
names = @board.project_config.settings['expedited_priority_names']
|
468
476
|
return false unless names
|
469
477
|
|
@@ -580,7 +588,7 @@ class Issue
|
|
580
588
|
/(?<project_code1>[^-]+)-(?<id1>.+)/ =~ key
|
581
589
|
/(?<project_code2>[^-]+)-(?<id2>.+)/ =~ other.key
|
582
590
|
comparison = project_code1 <=> project_code2
|
583
|
-
comparison = id1 <=> id2 if comparison.zero?
|
591
|
+
comparison = id1.to_i <=> id2.to_i if comparison.zero?
|
584
592
|
comparison
|
585
593
|
end
|
586
594
|
|
@@ -611,9 +619,13 @@ class Issue
|
|
611
619
|
end
|
612
620
|
history = [] # time, type, detail
|
613
621
|
|
614
|
-
|
615
|
-
|
616
|
-
|
622
|
+
if board.cycletime
|
623
|
+
started_at, stopped_at = board.cycletime.started_stopped_times(self)
|
624
|
+
history << [started_at, nil, '↓↓↓↓ Started here ↓↓↓↓', true] if started_at
|
625
|
+
history << [stopped_at, nil, '↑↑↑↑ Finished here ↑↑↑↑', true] if stopped_at
|
626
|
+
else
|
627
|
+
result << " Unable to determine start/end times as board #{board.id} has no cycletime specified\n"
|
628
|
+
end
|
617
629
|
|
618
630
|
@discarded_change_times&.each do |time|
|
619
631
|
history << [time, nil, '↑↑↑↑ Changes discarded ↑↑↑↑', true]
|
@@ -681,34 +693,40 @@ class Issue
|
|
681
693
|
@changes.select { |change| change.status? }
|
682
694
|
end
|
683
695
|
|
684
|
-
|
696
|
+
def sprints
|
697
|
+
sprint_ids = []
|
685
698
|
|
686
|
-
|
687
|
-
|
699
|
+
changes.each do |change|
|
700
|
+
next unless change.sprint?
|
701
|
+
|
702
|
+
sprint_ids << change.raw['to'].split(/\s*,\s*/).collect { |id| id.to_i }
|
703
|
+
end
|
704
|
+
sprint_ids.flatten!
|
705
|
+
|
706
|
+
board.sprints.select { |s| sprint_ids.include? s.id }
|
688
707
|
end
|
689
708
|
|
709
|
+
private
|
710
|
+
|
690
711
|
def load_history_into_changes
|
691
712
|
@raw['changelog']['histories']&.each do |history|
|
692
713
|
created = parse_time(history['created'])
|
693
714
|
|
694
|
-
# It should be impossible to not have an author but we've seen it in production
|
695
|
-
author = assemble_author history
|
696
715
|
history['items']&.each do |item|
|
697
|
-
@changes << ChangeItem.new(raw: item, time: created,
|
716
|
+
@changes << ChangeItem.new(raw: item, time: created, author_raw: history['author'])
|
698
717
|
end
|
699
718
|
end
|
700
719
|
end
|
701
720
|
|
702
721
|
def load_comments_into_changes
|
703
722
|
@raw['fields']['comment']['comments']&.each do |comment|
|
704
|
-
raw = {
|
723
|
+
raw = comment.merge({
|
705
724
|
'field' => 'comment',
|
706
725
|
'to' => comment['id'],
|
707
726
|
'toString' => comment['body']
|
708
|
-
}
|
709
|
-
author = assemble_author comment
|
727
|
+
})
|
710
728
|
created = parse_time(comment['created'])
|
711
|
-
@changes << ChangeItem.new(raw: raw, time: created,
|
729
|
+
@changes << ChangeItem.new(raw: raw, time: created, artificial: true, author_raw: comment['author'])
|
712
730
|
end
|
713
731
|
end
|
714
732
|
|
@@ -750,7 +768,9 @@ class Issue
|
|
750
768
|
first_status = first_change.old_value
|
751
769
|
first_status_id = first_change.old_value_id
|
752
770
|
end
|
753
|
-
|
771
|
+
|
772
|
+
creator = raw['fields']['creator']
|
773
|
+
ChangeItem.new time: created_time, artificial: true, author_raw: creator, raw: {
|
754
774
|
'field' => field_name,
|
755
775
|
'to' => first_status_id,
|
756
776
|
'toString' => first_status
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class IssueCollection < Array
|
4
|
+
attr_reader :hidden
|
5
|
+
|
6
|
+
def self.[] *issues
|
7
|
+
collection = new
|
8
|
+
issues.each { |i| collection << i }
|
9
|
+
collection
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
super
|
14
|
+
@hidden = []
|
15
|
+
end
|
16
|
+
|
17
|
+
def reject! &block
|
18
|
+
select(&block).each do |issue|
|
19
|
+
@hidden << issue
|
20
|
+
end
|
21
|
+
super
|
22
|
+
end
|
23
|
+
|
24
|
+
def find_by_key key:, include_hidden: false
|
25
|
+
block = ->(issue) { issue.key == key }
|
26
|
+
issue = find(&block)
|
27
|
+
issue = hidden.find(&block) if issue.nil? && include_hidden
|
28
|
+
issue
|
29
|
+
end
|
30
|
+
def clone
|
31
|
+
raise 'baboom'
|
32
|
+
end
|
33
|
+
end
|
@@ -26,7 +26,10 @@ class JiraGateway
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def call_command command
|
29
|
-
|
29
|
+
log_entry = " #{command.gsub(/\s+/, ' ')}"
|
30
|
+
log_entry = log_entry.gsub(@jira_api_token, '[API_TOKEN]') if @jira_api_token
|
31
|
+
@file_system.log log_entry
|
32
|
+
|
30
33
|
result = `#{command}`
|
31
34
|
@file_system.log result unless $CHILD_STATUS.success?
|
32
35
|
return result if $CHILD_STATUS.success?
|
@@ -6,7 +6,7 @@ require 'jirametrics/status_collection'
|
|
6
6
|
class ProjectConfig
|
7
7
|
attr_reader :target_path, :jira_config, :all_boards, :possible_statuses,
|
8
8
|
:download_config, :file_configs, :exporter, :data_version, :name, :board_configs,
|
9
|
-
:settings, :aggregate_config, :discarded_changes_data
|
9
|
+
:settings, :aggregate_config, :discarded_changes_data, :users
|
10
10
|
attr_accessor :time_range, :jira_url, :id
|
11
11
|
|
12
12
|
def initialize exporter:, jira_config:, block:, target_path: '.', name: '', id: nil
|
@@ -40,6 +40,7 @@ class ProjectConfig
|
|
40
40
|
@id = guess_project_id
|
41
41
|
load_project_metadata
|
42
42
|
load_sprints
|
43
|
+
load_users
|
43
44
|
end
|
44
45
|
|
45
46
|
def run load_only: false
|
@@ -151,6 +152,8 @@ class ProjectConfig
|
|
151
152
|
end
|
152
153
|
|
153
154
|
def status_category_mapping status:, category:
|
155
|
+
return if @exporter.downloading?
|
156
|
+
|
154
157
|
status, status_id = possible_statuses.parse_name_id status
|
155
158
|
category, category_id = possible_statuses.parse_name_id category
|
156
159
|
|
@@ -323,6 +326,15 @@ class ProjectConfig
|
|
323
326
|
raise
|
324
327
|
end
|
325
328
|
|
329
|
+
def load_users
|
330
|
+
@users = []
|
331
|
+
filename = File.join @target_path, "#{get_file_prefix}_users.json"
|
332
|
+
return unless File.exist? filename
|
333
|
+
|
334
|
+
json = file_system.load_json(filename)
|
335
|
+
json.each { |user_data| @users << User.new(raw: user_data) }
|
336
|
+
end
|
337
|
+
|
326
338
|
def to_time string, end_of_day: false
|
327
339
|
time = end_of_day ? '23:59:59' : '00:00:00'
|
328
340
|
string = "#{string}T#{time}#{exporter.timezone_offset}" if string.match?(/^\d{4}-\d{2}-\d{2}$/)
|
@@ -356,7 +368,7 @@ class ProjectConfig
|
|
356
368
|
|
357
369
|
# To be used by the aggregate_config only. Not intended to be part of the public API
|
358
370
|
def add_issues issues_list
|
359
|
-
@issues =
|
371
|
+
@issues = IssueCollection.new if @issues.nil?
|
360
372
|
@all_boards = {}
|
361
373
|
|
362
374
|
issues_list.each do |issue|
|
@@ -373,7 +385,7 @@ class ProjectConfig
|
|
373
385
|
'declaration but none are here. Check your config.'
|
374
386
|
end
|
375
387
|
|
376
|
-
return @issues =
|
388
|
+
return @issues = IssueCollection.new if @exporter.downloading?
|
377
389
|
raise 'No data found. Must do a download before an export' unless data_downloaded?
|
378
390
|
|
379
391
|
load_data if all_boards.empty?
|
@@ -385,7 +397,7 @@ class ProjectConfig
|
|
385
397
|
issues = load_issues_from_issues_directory path: issues_path, timezone_offset: timezone_offset
|
386
398
|
else
|
387
399
|
file_system.log "Can't find directory #{issues_path}. Has a download been done?", also_write_to_stderr: true
|
388
|
-
return
|
400
|
+
return IssueCollection.new
|
389
401
|
end
|
390
402
|
|
391
403
|
# Attach related issues
|
@@ -397,7 +409,8 @@ class ProjectConfig
|
|
397
409
|
|
398
410
|
# We'll have some issues that are in the list that weren't part of the initial query. Once we've
|
399
411
|
# attached them in the appropriate places, remove any that aren't part of that initial set.
|
400
|
-
|
412
|
+
issues.reject! { |i| !i.in_initial_query? } # rubocop:disable Style/InverseMethods
|
413
|
+
@issues = issues
|
401
414
|
end
|
402
415
|
|
403
416
|
@issues
|
@@ -438,7 +451,7 @@ class ProjectConfig
|
|
438
451
|
end
|
439
452
|
|
440
453
|
def load_issues_from_issues_directory path:, timezone_offset:
|
441
|
-
issues =
|
454
|
+
issues = IssueCollection.new
|
442
455
|
default_board = nil
|
443
456
|
|
444
457
|
group_filenames_and_board_ids(path: path).each do |filename, board_ids|
|
@@ -450,6 +463,10 @@ class ProjectConfig
|
|
450
463
|
end
|
451
464
|
|
452
465
|
boards.each do |board|
|
466
|
+
if board.cycletime.nil?
|
467
|
+
raise "The board declaration for board #{board.id} must come before the " \
|
468
|
+
"first usage of 'issues' in the configuration"
|
469
|
+
end
|
453
470
|
issues << Issue.new(raw: content, timezone_offset: timezone_offset, board: board)
|
454
471
|
end
|
455
472
|
end
|
@@ -462,7 +479,7 @@ class ProjectConfig
|
|
462
479
|
# board ids appropriately.
|
463
480
|
def group_filenames_and_board_ids path:
|
464
481
|
hash = {}
|
465
|
-
|
482
|
+
file_system.foreach(path) do |filename|
|
466
483
|
# Matches either FAKE-123.json or FAKE-123-456.json
|
467
484
|
if /^(?<key>[^-]+-\d+)(?<_>-(?<board_id>\d+))?\.json$/ =~ filename
|
468
485
|
(hash[key] ||= []) << [filename, board_id&.to_i || :unknown]
|
data/lib/jirametrics/sprint.rb
CHANGED
@@ -121,11 +121,13 @@ class SprintBurndown < ChartBase
|
|
121
121
|
|
122
122
|
# select all the changes that are relevant for the sprint. If this issue never appears in this sprint then return [].
|
123
123
|
def changes_for_one_issue issue:, sprint:
|
124
|
-
|
124
|
+
estimate = 0.0
|
125
125
|
ever_in_sprint = false
|
126
126
|
currently_in_sprint = false
|
127
127
|
change_data = []
|
128
128
|
|
129
|
+
estimate_display_name = current_board.estimation_configuration.display_name
|
130
|
+
|
129
131
|
issue_completed_time = issue.board.cycletime.started_stopped_times(issue).last
|
130
132
|
completed_has_been_tracked = false
|
131
133
|
|
@@ -140,26 +142,26 @@ class SprintBurndown < ChartBase
|
|
140
142
|
if currently_in_sprint == false && in_change_item
|
141
143
|
action = :enter_sprint
|
142
144
|
ever_in_sprint = true
|
143
|
-
value =
|
145
|
+
value = estimate
|
144
146
|
elsif currently_in_sprint && in_change_item == false
|
145
147
|
action = :leave_sprint
|
146
|
-
value = -
|
148
|
+
value = -estimate
|
147
149
|
end
|
148
150
|
currently_in_sprint = in_change_item
|
149
|
-
elsif change.
|
151
|
+
elsif change.field == estimate_display_name && (issue_completed_time.nil? || change.time < issue_completed_time)
|
150
152
|
action = :story_points
|
151
|
-
|
152
|
-
value =
|
153
|
+
estimate = change.value.to_f
|
154
|
+
value = estimate - change.old_value.to_f
|
153
155
|
elsif completed_has_been_tracked == false && change.time == issue_completed_time
|
154
156
|
completed_has_been_tracked = true
|
155
157
|
action = :issue_stopped
|
156
|
-
value = -
|
158
|
+
value = -estimate
|
157
159
|
end
|
158
160
|
|
159
161
|
next unless action
|
160
162
|
|
161
163
|
change_data << SprintIssueChangeData.new(
|
162
|
-
time: change.time, issue: issue, action: action, value: value,
|
164
|
+
time: change.time, issue: issue, action: action, value: value, estimate: estimate
|
163
165
|
)
|
164
166
|
end
|
165
167
|
|
@@ -176,7 +178,7 @@ class SprintBurndown < ChartBase
|
|
176
178
|
summary_stats = SprintSummaryStats.new
|
177
179
|
summary_stats.completed = 0.0
|
178
180
|
|
179
|
-
|
181
|
+
estimate = 0.0
|
180
182
|
start_data_written = false
|
181
183
|
data_set = []
|
182
184
|
|
@@ -185,11 +187,11 @@ class SprintBurndown < ChartBase
|
|
185
187
|
change_data_for_sprint.each do |change_data|
|
186
188
|
if start_data_written == false && change_data.time >= sprint.start_time
|
187
189
|
data_set << {
|
188
|
-
y:
|
190
|
+
y: estimate,
|
189
191
|
x: chart_format(sprint.start_time),
|
190
|
-
title: "Sprint started with #{
|
192
|
+
title: "Sprint started with #{estimate} points"
|
191
193
|
}
|
192
|
-
summary_stats.started =
|
194
|
+
summary_stats.started = estimate
|
193
195
|
start_data_written = true
|
194
196
|
end
|
195
197
|
|
@@ -198,12 +200,12 @@ class SprintBurndown < ChartBase
|
|
198
200
|
case change_data.action
|
199
201
|
when :enter_sprint
|
200
202
|
issues_currently_in_sprint << change_data.issue.key
|
201
|
-
|
203
|
+
estimate += change_data.estimate
|
202
204
|
when :leave_sprint
|
203
205
|
issues_currently_in_sprint.delete change_data.issue.key
|
204
|
-
|
206
|
+
estimate -= change_data.estimate
|
205
207
|
when :story_points
|
206
|
-
|
208
|
+
estimate += change_data.value if issues_currently_in_sprint.include? change_data.issue.key
|
207
209
|
end
|
208
210
|
|
209
211
|
next unless change_data.time >= sprint.start_time
|
@@ -213,26 +215,26 @@ class SprintBurndown < ChartBase
|
|
213
215
|
when :story_points
|
214
216
|
next unless issues_currently_in_sprint.include? change_data.issue.key
|
215
217
|
|
216
|
-
|
217
|
-
message = "Story points changed from #{
|
218
|
+
old_estimate = change_data.estimate - change_data.value
|
219
|
+
message = "Story points changed from #{old_estimate} points to #{change_data.estimate} points"
|
218
220
|
summary_stats.points_values_changed = true
|
219
221
|
when :enter_sprint
|
220
|
-
message = "Added to sprint with #{change_data.
|
221
|
-
summary_stats.added += change_data.
|
222
|
+
message = "Added to sprint with #{change_data.estimate || 'no'} points"
|
223
|
+
summary_stats.added += change_data.estimate
|
222
224
|
when :issue_stopped
|
223
|
-
|
224
|
-
message = "Completed with #{change_data.
|
225
|
+
estimate -= change_data.estimate
|
226
|
+
message = "Completed with #{change_data.estimate || 'no'} points"
|
225
227
|
issues_currently_in_sprint.delete change_data.issue.key
|
226
|
-
summary_stats.completed += change_data.
|
228
|
+
summary_stats.completed += change_data.estimate
|
227
229
|
when :leave_sprint
|
228
|
-
message = "Removed from sprint with #{change_data.
|
229
|
-
summary_stats.removed += change_data.
|
230
|
+
message = "Removed from sprint with #{change_data.estimate || 'no'} points"
|
231
|
+
summary_stats.removed += change_data.estimate
|
230
232
|
else
|
231
233
|
raise "Unexpected action: #{change_data.action}"
|
232
234
|
end
|
233
235
|
|
234
236
|
data_set << {
|
235
|
-
y:
|
237
|
+
y: estimate,
|
236
238
|
x: chart_format(change_data.time),
|
237
239
|
title: "#{change_data.issue.key} #{message}"
|
238
240
|
}
|
@@ -241,27 +243,27 @@ class SprintBurndown < ChartBase
|
|
241
243
|
unless start_data_written
|
242
244
|
# There was nothing that triggered us to write the sprint started block so do it now.
|
243
245
|
data_set << {
|
244
|
-
y:
|
246
|
+
y: estimate,
|
245
247
|
x: chart_format(sprint.start_time),
|
246
|
-
title: "Sprint started with #{
|
248
|
+
title: "Sprint started with #{estimate} points"
|
247
249
|
}
|
248
|
-
summary_stats.started =
|
250
|
+
summary_stats.started = estimate
|
249
251
|
end
|
250
252
|
|
251
253
|
if sprint.completed_time
|
252
254
|
data_set << {
|
253
|
-
y:
|
255
|
+
y: estimate,
|
254
256
|
x: chart_format(sprint.completed_time),
|
255
|
-
title: "Sprint ended with #{
|
257
|
+
title: "Sprint ended with #{estimate} points unfinished"
|
256
258
|
}
|
257
|
-
summary_stats.remaining =
|
259
|
+
summary_stats.remaining = estimate
|
258
260
|
end
|
259
261
|
|
260
262
|
unless sprint.completed_at?(time_range.end)
|
261
263
|
data_set << {
|
262
|
-
y:
|
264
|
+
y: estimate,
|
263
265
|
x: chart_format(time_range.end),
|
264
|
-
title: "Sprint still active. #{
|
266
|
+
title: "Sprint still active. #{estimate} points still in progress."
|
265
267
|
}
|
266
268
|
end
|
267
269
|
|
@@ -4,14 +4,14 @@ require 'jirametrics/value_equality'
|
|
4
4
|
|
5
5
|
class SprintIssueChangeData
|
6
6
|
include ValueEquality
|
7
|
-
attr_reader :time, :action, :value, :issue, :
|
7
|
+
attr_reader :time, :action, :value, :issue, :estimate
|
8
8
|
|
9
|
-
def initialize time:, action:, value:, issue:,
|
9
|
+
def initialize time:, action:, value:, issue:, estimate:
|
10
10
|
@time = time
|
11
11
|
@action = action
|
12
12
|
@value = value
|
13
13
|
@issue = issue
|
14
|
-
@
|
14
|
+
@estimate = estimate
|
15
15
|
end
|
16
16
|
|
17
17
|
def inspect
|
@@ -16,6 +16,12 @@ class StatusCollection
|
|
16
16
|
@list.find { |status| status.id == id }
|
17
17
|
end
|
18
18
|
|
19
|
+
def find_by_id! id
|
20
|
+
status = @list.find { |status| status.id == id }
|
21
|
+
raise "Can't find any status for id #{id} in #{self}" unless status
|
22
|
+
status
|
23
|
+
end
|
24
|
+
|
19
25
|
def find_all_by_name identifier
|
20
26
|
name, id = parse_name_id identifier
|
21
27
|
|
data/lib/jirametrics.rb
CHANGED
@@ -69,6 +69,7 @@ class JiraMetrics < Thor
|
|
69
69
|
require 'jirametrics/daily_wip_chart'
|
70
70
|
require 'jirametrics/groupable_issue_chart'
|
71
71
|
require 'jirametrics/css_variable'
|
72
|
+
require 'jirametrics/issue_collection'
|
72
73
|
|
73
74
|
require 'jirametrics/aggregate_config'
|
74
75
|
require 'jirametrics/expedited_chart'
|
@@ -112,7 +113,10 @@ class JiraMetrics < Thor
|
|
112
113
|
require 'jirametrics/download_config'
|
113
114
|
require 'jirametrics/columns_config'
|
114
115
|
require 'jirametrics/hierarchy_table'
|
116
|
+
require 'jirametrics/estimation_configuration'
|
115
117
|
require 'jirametrics/board'
|
118
|
+
require 'jirametrics/daily_view'
|
119
|
+
require 'jirametrics/user'
|
116
120
|
load config_file
|
117
121
|
end
|
118
122
|
end
|