jirametrics 2.7 → 2.8

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.
@@ -172,7 +172,7 @@ class SprintBurndown < ChartBase
172
172
  change_item.raw['to'].split(/\s*,\s*/).any? { |id| id.to_i == sprint.id }
173
173
  end
174
174
 
175
- def data_set_by_story_points sprint:, change_data_for_sprint:
175
+ def data_set_by_story_points sprint:, change_data_for_sprint: # rubocop:disable Metrics/CyclomaticComplexity
176
176
  summary_stats = SprintSummaryStats.new
177
177
  summary_stats.completed = 0.0
178
178
 
@@ -4,29 +4,57 @@ require 'jirametrics/value_equality'
4
4
 
5
5
  class Status
6
6
  include ValueEquality
7
- attr_reader :id, :category_name, :category_id, :project_id
7
+ attr_reader :id, :project_id, :category
8
8
  attr_accessor :name
9
9
 
10
- def initialize name: nil, id: nil, category_name: nil, category_id: nil, project_id: nil, raw: nil
11
- @name = name
12
- @id = id
13
- @category_name = category_name
14
- @category_id = category_id
15
- @project_id = project_id
10
+ class Category
11
+ attr_reader :id, :name, :key
12
+
13
+ def initialize id:, name:, key:
14
+ @id = id
15
+ @name = name
16
+ @key = key
17
+ end
16
18
 
17
- return unless raw
19
+ def to_s
20
+ "#{name.inspect}:#{id.inspect}"
21
+ end
18
22
 
19
- @raw = raw
20
- @name = raw['name']
21
- @id = raw['id'].to_i
23
+ def new? = (@key == 'new')
24
+ def indeterminate? = (@key == 'indeterminate')
25
+ def done? = (@key == 'done')
26
+ end
22
27
 
28
+ def self.from_raw raw
23
29
  category_config = raw['statusCategory']
24
- @category_name = category_config['name']
25
- @category_id = category_config['id'].to_i
26
30
 
27
- # If this is a NextGen project then this status may be project specific. When this field is
28
- # nil then the status is global.
29
- @project_id = raw['scope']&.[]('project')&.[]('id')
31
+ legal_keys = %w[new indeterminate done]
32
+ unless legal_keys.include? category_config['key']
33
+ puts "Category key #{category_config['key'].inspect} should be one of #{legal_keys.inspect}. Found:\n" \
34
+ "#{category_config}"
35
+ end
36
+
37
+ Status.new(
38
+ name: raw['name'],
39
+ id: raw['id'].to_i,
40
+ category_name: category_config['name'],
41
+ category_id: category_config['id'].to_i,
42
+ category_key: category_config['key'],
43
+ project_id: raw['scope']&.[]('project')&.[]('id'),
44
+ artificial: false
45
+ )
46
+ end
47
+
48
+ def initialize name:, id:, category_name:, category_id:, category_key:, project_id: nil, artificial: true
49
+ # These checks are needed because nils used to be possible and now they aren't.
50
+ raise 'id cannot be nil' if id.nil?
51
+ raise 'category_id cannot be nil' if category_id.nil?
52
+
53
+ @name = name
54
+ @id = id
55
+ @category = Category.new id: category_id, name: category_name, key: category_key
56
+ @project_id = project_id
57
+ @artificial = artificial
30
58
  end
31
59
 
32
60
  def project_scoped?
@@ -38,18 +66,26 @@ class Status
38
66
  end
39
67
 
40
68
  def to_s
41
- result = "Status(name=#{@name.inspect}," \
42
- " id=#{@id.inspect}," \
43
- " category_name=#{@category_name.inspect}," \
44
- " category_id=#{@category_id.inspect}," \
45
- " project_id=#{@project_id}"
46
- result << ' artificial' if artificial?
47
- result << ')'
48
- result
69
+ "#{name.inspect}:#{id.inspect}"
49
70
  end
50
71
 
51
72
  def artificial?
52
- @raw.nil?
73
+ @artificial
74
+ end
75
+
76
+ def == other
77
+ @id == other.id && @name == other.name && @category.id == other.category.id && @category.name == other.category.name
78
+ end
79
+
80
+ def inspect
81
+ result = []
82
+ result << "Status(name: #{@name.inspect}"
83
+ result << "id: #{@id.inspect}"
84
+ result << "project_id: #{@project_id}" if @project_id
85
+ category = self.category
86
+ result << "category: {name:#{category.name.inspect}, id: #{category.id.inspect}, key: #{category.key.inspect}}"
87
+ result << 'artificial' if artificial?
88
+ result.join(', ') << ')'
53
89
  end
54
90
 
55
91
  def value_equality_ignored_variables
@@ -13,7 +13,7 @@ class StatusCollection
13
13
  excluding = expand_statuses excluding
14
14
 
15
15
  @list.filter_map do |status|
16
- keep = status.category_name == category_name ||
16
+ keep = status.category.name == category_name ||
17
17
  including.any? { |s| s.name == status.name }
18
18
  keep = false if excluding.any? { |s| s.name == status.name }
19
19
 
@@ -56,12 +56,45 @@ class StatusCollection
56
56
  filter_status_names category_name: 'Done', including: including, excluding: excluding
57
57
  end
58
58
 
59
- def find_by_name name
60
- find { |status| status.name == name }
59
+ # Return the status matching this id or nil if it can't be found.
60
+ def find_by_id id
61
+ @list.find { |status| status.id == id }
62
+ end
63
+
64
+ def find_all_by_name name
65
+ @list.select { |status| status.name == name }
66
+ end
67
+
68
+ def find_category_by_name name
69
+ category = @list.find { |status| status.category.name == name }&.category
70
+ unless category
71
+ set = Set.new
72
+ @list.each do |status|
73
+ set << status.category.to_s
74
+ end
75
+ raise "Unable to find status category #{name.inspect} in [#{set.to_a.sort.join(', ')}]"
76
+ end
77
+ category
78
+ end
79
+
80
+ # This is used to create a status that was found in the history but has since been deleted.
81
+ def fabricate_status_for id:, name:
82
+ first_in_progress_status = @list.find { |s| s.category.indeterminate? }
83
+ raise "Can't find even one in-progress status in [#{set.to_a.sort.join(', ')}]" unless first_in_progress_status
84
+
85
+ status = Status.new(
86
+ name: name,
87
+ id: id,
88
+ category_name: first_in_progress_status.category.name,
89
+ category_id: first_in_progress_status.category.id,
90
+ category_key: first_in_progress_status.category.key
91
+ )
92
+ self << status
93
+ status
61
94
  end
62
95
 
63
- def find(&block)= @list.find(&block)
64
96
  def collect(&block) = @list.collect(&block)
97
+ def find(&block) = @list.find(&block)
65
98
  def each(&block) = @list.each(&block)
66
99
  def select(&block) = @list.select(&block)
67
100
  def <<(arg) = @list << arg
@@ -70,6 +103,6 @@ class StatusCollection
70
103
  def delete(object) = @list.delete(object)
71
104
 
72
105
  def inspect
73
- "StatusCollection(#{@list.collect(&:inspect).join(', ')})"
106
+ "StatusCollection(#{@list.join(', ')})"
74
107
  end
75
108
  end
@@ -82,7 +82,7 @@ class ThroughputChart < ChartBase
82
82
  def throughput_dataset periods:, completed_issues:
83
83
  periods.collect do |period|
84
84
  closed_issues = completed_issues.filter_map do |issue|
85
- stop_date = issue.board.cycletime.started_stopped_times(issue).last&.to_date
85
+ stop_date = issue.board.cycletime.started_stopped_dates(issue).last
86
86
  [stop_date, issue] if stop_date && period.include?(stop_date)
87
87
  end
88
88
 
data/lib/jirametrics.rb CHANGED
@@ -7,6 +7,13 @@ class JiraMetrics < Thor
7
7
  true
8
8
  end
9
9
 
10
+ map %w[--version -v] => :__print_version
11
+
12
+ desc '--version, -v', 'print the version'
13
+ def __print_version
14
+ puts Gem.loaded_specs['jirametrics'].version
15
+ end
16
+
10
17
  option :config
11
18
  option :name
12
19
  desc 'export', "Export data into either reports or CSV's as per the configuration"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jirametrics
3
3
  version: !ruby/object:Gem::Version
4
- version: '2.7'
4
+ version: '2.8'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Bowler
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-10-27 00:00:00.000000000 Z
11
+ date: 2024-12-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: random-word
@@ -106,7 +106,6 @@ files:
106
106
  - lib/jirametrics/html/cycletime_histogram.erb
107
107
  - lib/jirametrics/html/cycletime_scatterplot.erb
108
108
  - lib/jirametrics/html/daily_wip_chart.erb
109
- - lib/jirametrics/html/data_quality_report.erb
110
109
  - lib/jirametrics/html/estimate_accuracy_chart.erb
111
110
  - lib/jirametrics/html/expedited_chart.erb
112
111
  - lib/jirametrics/html/flow_efficiency_scatterplot.erb
@@ -132,14 +131,14 @@ files:
132
131
  - lib/jirametrics/tree_organizer.rb
133
132
  - lib/jirametrics/trend_line_calculator.rb
134
133
  - lib/jirametrics/value_equality.rb
135
- homepage: https://github.com/mikebowler/jirametrics
134
+ homepage: https://jirametrics.org
136
135
  licenses:
137
136
  - Apache-2.0
138
137
  metadata:
139
138
  rubygems_mfa_required: 'true'
140
139
  bug_tracker_uri: https://github.com/mikebowler/jirametrics/issues
141
- changelog_uri: https://github.com/mikebowler/jirametrics/wiki/Changes
142
- documentation_uri: https://github.com/mikebowler/jirametrics/wiki
140
+ changelog_uri: https://jirametrics.org/changes
141
+ documentation_uri: https://jirametrics.org
143
142
  post_install_message:
144
143
  rdoc_options: []
145
144
  require_paths:
@@ -1,138 +0,0 @@
1
- <%
2
- problems = problems_for :discarded_changes
3
- unless problems.empty?
4
- %>
5
- <p>
6
- <span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> have had information discarded. This configuration is set
7
- to "reset the clock" if an item is moved back to the backlog after it's been started. This hides important
8
- information and makes the data less accurate. <b>Moving items back to the backlog is strongly discouraged.</b>
9
- <%= collapsible_issues_panel problems %>
10
- </p>
11
- <%
12
- end
13
- %>
14
-
15
- <%
16
- problems = problems_for :completed_but_not_started
17
- unless problems.empty?
18
- percentage_work_included = ((issues.size - problems.size).to_f / issues.size * 100).to_i
19
- %>
20
- <p>
21
- <span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> were discarded from all charts using cycletime (scatterplot, histogram, etc) as we couldn't determine when they started.
22
- <% if percentage_work_included < 85 %>
23
- Consider whether looking at only <%= percentage_work_included %>% of the total data points is enough to come to any reasonable conclusions. See <a href="https://en.wikipedia.org/wiki/Survivorship_bias">Survivorship Bias</a>.
24
- <% end %>
25
- <%= collapsible_issues_panel problems %>
26
- </p>
27
- <%
28
- end
29
- %>
30
-
31
- <%
32
- problems = problems_for :status_changes_after_done
33
- unless problems.empty?
34
- %>
35
- <p>
36
- <span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> had a status change after being identified as done. We should question whether they were really done at that point or if we stopped the clock too early.
37
- <%= collapsible_issues_panel problems %>
38
- </p>
39
- <%
40
- end
41
- %>
42
-
43
- <%
44
- problems = problems_for :backwards_through_status_categories
45
- unless problems.empty?
46
- %>
47
- <p>
48
- <span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> moved backwards across the board, <b>crossing status categories</b>. This will almost certainly have impacted timings as the end times are often taken at status category boundaries. You should assume that any timing measurements for this item are wrong.
49
- <%= collapsible_issues_panel problems %>
50
- </p>
51
- <%
52
- end
53
- %>
54
-
55
- <%
56
- problems = problems_for :backwords_through_statuses
57
- unless problems.empty?
58
- %>
59
- <p>
60
- <span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> moved backwards across the board. Depending where we have set the start and end points, this may give us incorrect timing data. Note that these items did not cross a status category and may not have affected metrics.
61
- <%= collapsible_issues_panel problems %>
62
- </p>
63
- <%
64
- end
65
- %>
66
-
67
- <%
68
- problems = problems_for :status_not_on_board
69
- unless problems.empty?
70
- %>
71
- <p>
72
- <span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> were not visible on the board for some period of time. This may impact timings as the work was likely to have been forgotten if it wasn't visible.
73
- <%= collapsible_issues_panel problems %>
74
- </p>
75
- <%
76
- end
77
- %>
78
-
79
- <%
80
- problems = problems_for :created_in_wrong_status
81
- unless problems.empty?
82
- %>
83
- <p>
84
- <span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> were created in a status not designated as Backlog. This will impact the measurement of start times and will therefore impact whether it's shown as in progress or not.
85
- <%= collapsible_issues_panel problems %>
86
- </p>
87
- <%
88
- end
89
- %>
90
-
91
- <%
92
- problems = problems_for :stopped_before_started
93
- unless problems.empty?
94
- %>
95
- <p>
96
- <span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> were stopped before they were started and this will play havoc with any cycletime or WIP calculations. The most common case for this is when an item gets closed and then moved back into an in-progress status.
97
- <%= collapsible_issues_panel problems %>
98
- </p>
99
- <%
100
- end
101
- %>
102
-
103
- <%
104
- problems = problems_for :issue_not_started_but_subtasks_have
105
- unless problems.empty?
106
- %>
107
- <p>
108
- <span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> still showing 'not started' while sub-tasks underneath them have started. This is almost always a mistake; if we're working on subtasks, the top level
109
- item should also have started.
110
- <%= collapsible_issues_panel problems %>
111
- </p>
112
- <%
113
- end
114
- %>
115
-
116
- <%
117
- problems = problems_for :incomplete_subtasks_when_issue_done
118
- unless problems.empty?
119
- %>
120
- <p>
121
- <span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> issues were marked as done while subtasks were still not done.
122
- <%= collapsible_issues_panel problems %>
123
- </p>
124
- <%
125
- end
126
- %>
127
-
128
- <%
129
- problems = problems_for :issue_on_multiple_boards
130
- unless problems.empty?
131
- %>
132
- <p>
133
- <span class="quality_note_bullet">⮕</span> For <%= label_issues problems.size %>, we have an issue that shows up on more than one board. This could result in more data points showing up on a chart then there really should be.
134
- <%= collapsible_issues_panel problems, :hide_board_column %>
135
- </p>
136
- <%
137
- end
138
- %>