jirametrics 2.6 → 2.7
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/aggregate_config.rb +6 -1
 - data/lib/jirametrics/aging_work_bar_chart.rb +6 -6
 - data/lib/jirametrics/aging_work_in_progress_chart.rb +1 -1
 - data/lib/jirametrics/aging_work_table.rb +4 -5
 - data/lib/jirametrics/blocked_stalled_change.rb +1 -1
 - data/lib/jirametrics/board.rb +14 -12
 - data/lib/jirametrics/chart_base.rb +5 -7
 - data/lib/jirametrics/cycletime_config.rb +26 -7
 - data/lib/jirametrics/cycletime_histogram.rb +1 -1
 - data/lib/jirametrics/cycletime_scatterplot.rb +3 -6
 - data/lib/jirametrics/daily_wip_by_age_chart.rb +2 -4
 - data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +2 -2
 - data/lib/jirametrics/daily_wip_by_parent_chart.rb +0 -4
 - data/lib/jirametrics/daily_wip_chart.rb +7 -9
 - data/lib/jirametrics/data_quality_report.rb +45 -6
 - data/lib/jirametrics/dependency_chart.rb +3 -4
 - data/lib/jirametrics/discard_changes_before.rb +1 -1
 - data/lib/jirametrics/downloader.rb +14 -13
 - data/lib/jirametrics/estimate_accuracy_chart.rb +1 -2
 - data/lib/jirametrics/examples/aggregated_project.rb +1 -1
 - data/lib/jirametrics/examples/standard_project.rb +10 -7
 - data/lib/jirametrics/expedited_chart.rb +1 -2
 - data/lib/jirametrics/exporter.rb +25 -0
 - data/lib/jirametrics/file_system.rb +1 -1
 - data/lib/jirametrics/flow_efficiency_scatterplot.rb +113 -0
 - data/lib/jirametrics/html/data_quality_report.erb +12 -0
 - data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +85 -0
 - data/lib/jirametrics/html_report_config.rb +2 -1
 - data/lib/jirametrics/issue.rb +62 -10
 - data/lib/jirametrics/jira_gateway.rb +1 -1
 - data/lib/jirametrics/project_config.rb +27 -19
 - data/lib/jirametrics/self_or_issue_dispatcher.rb +2 -0
 - data/lib/jirametrics/sprint_burndown.rb +2 -2
 - data/lib/jirametrics/status.rb +1 -1
 - data/lib/jirametrics/status_collection.rb +4 -0
 - data/lib/jirametrics/throughput_chart.rb +1 -1
 - data/lib/jirametrics.rb +15 -5
 - metadata +5 -3
 
    
        checksums.yaml
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            ---
         
     | 
| 
       2 
2 
     | 
    
         
             
            SHA256:
         
     | 
| 
       3 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       4 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 3 
     | 
    
         
            +
              metadata.gz: 73e2f2e8408a55ccecb221ba0c6d241c77f19de18d2100a9b05213a6470ed65f
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: d0f9056615c44e783c824ca1c6566c95bf68f16a26f81a596475c1d458fee9f1
         
     | 
| 
       5 
5 
     | 
    
         
             
            SHA512:
         
     | 
| 
       6 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       7 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 6 
     | 
    
         
            +
              metadata.gz: e58594175d798269e0e4b8ec1fd4c0f8b693767670222bc6144be19646b04f83e5edb508e4f933f8466865018e8802f789ecb348db368c3f626318df7774c0e0
         
     | 
| 
      
 7 
     | 
    
         
            +
              data.tar.gz: 4555628c33a398cd3aaedd6e36a074b54d89f5a431f47d8617344e4af45e389e529ba54dd9cbd7d9e7d8ad7f673a68f664d00b951451b433d47f90c6feb439a5
         
     | 
| 
         @@ -62,7 +62,12 @@ class AggregateConfig 
     | 
|
| 
       62 
62 
     | 
    
         
             
                      'the first file section'
         
     | 
| 
       63 
63 
     | 
    
         
             
                  end
         
     | 
| 
       64 
64 
     | 
    
         
             
                end
         
     | 
| 
       65 
     | 
    
         
            -
             
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                if issues.nil?
         
     | 
| 
      
 67 
     | 
    
         
            +
                  log "No issues found for #{project_name}"
         
     | 
| 
      
 68 
     | 
    
         
            +
                else
         
     | 
| 
      
 69 
     | 
    
         
            +
                  @project_config.add_issues issues
         
     | 
| 
      
 70 
     | 
    
         
            +
                end
         
     | 
| 
       66 
71 
     | 
    
         
             
              end
         
     | 
| 
       67 
72 
     | 
    
         | 
| 
       68 
73 
     | 
    
         
             
              def find_time_range projects:
         
     | 
| 
         @@ -53,8 +53,8 @@ class AgingWorkBarChart < ChartBase 
     | 
|
| 
       53 
53 
     | 
    
         
             
                percentage_line_x = date_range.end - calculate_percent_line if percentage
         
     | 
| 
       54 
54 
     | 
    
         | 
| 
       55 
55 
     | 
    
         
             
                if aging_issues.empty?
         
     | 
| 
       56 
     | 
    
         
            -
                  @description_text =  
     | 
| 
       57 
     | 
    
         
            -
                  return render_top_text(binding) 
     | 
| 
      
 56 
     | 
    
         
            +
                  @description_text = '<p>There is no aging work</p>'
         
     | 
| 
      
 57 
     | 
    
         
            +
                  return render_top_text(binding)
         
     | 
| 
       58 
58 
     | 
    
         
             
                end
         
     | 
| 
       59 
59 
     | 
    
         | 
| 
       60 
60 
     | 
    
         
             
                wrap_and_render(binding, __FILE__)
         
     | 
| 
         @@ -62,7 +62,7 @@ class AgingWorkBarChart < ChartBase 
     | 
|
| 
       62 
62 
     | 
    
         | 
| 
       63 
63 
     | 
    
         
             
              def data_sets_for_one_issue issue:, today:
         
     | 
| 
       64 
64 
     | 
    
         
             
                cycletime = issue.board.cycletime
         
     | 
| 
       65 
     | 
    
         
            -
                issue_start_time = cycletime. 
     | 
| 
      
 65 
     | 
    
         
            +
                issue_start_time, _stopped_time = cycletime.started_stopped_times(issue)
         
     | 
| 
       66 
66 
     | 
    
         
             
                issue_start_date = issue_start_time.to_date
         
     | 
| 
       67 
67 
     | 
    
         
             
                issue_label = "[#{label_days cycletime.age(issue, today: today)}] #{issue.key}: #{issue.summary}"[0..60]
         
     | 
| 
       68 
68 
     | 
    
         
             
                [
         
     | 
| 
         @@ -92,8 +92,8 @@ class AgingWorkBarChart < ChartBase 
     | 
|
| 
       92 
92 
     | 
    
         | 
| 
       93 
93 
     | 
    
         
             
              def select_aging_issues issues:
         
     | 
| 
       94 
94 
     | 
    
         
             
                issues.select do |issue|
         
     | 
| 
       95 
     | 
    
         
            -
                   
     | 
| 
       96 
     | 
    
         
            -
                   
     | 
| 
      
 95 
     | 
    
         
            +
                  started_time, stopped_time = issue.board.cycletime.started_stopped_times(issue)
         
     | 
| 
      
 96 
     | 
    
         
            +
                  started_time && stopped_time.nil?
         
     | 
| 
       97 
97 
     | 
    
         
             
                end
         
     | 
| 
       98 
98 
     | 
    
         
             
              end
         
     | 
| 
       99 
99 
     | 
    
         | 
| 
         @@ -107,7 +107,7 @@ class AgingWorkBarChart < ChartBase 
     | 
|
| 
       107 
107 
     | 
    
         
             
              def status_data_sets issue:, label:, today:
         
     | 
| 
       108 
108 
     | 
    
         
             
                cycletime = issue.board.cycletime
         
     | 
| 
       109 
109 
     | 
    
         | 
| 
       110 
     | 
    
         
            -
                issue_started_time = cycletime. 
     | 
| 
      
 110 
     | 
    
         
            +
                issue_started_time, _issue_stopped_time = cycletime.started_stopped_times(issue)
         
     | 
| 
       111 
111 
     | 
    
         | 
| 
       112 
112 
     | 
    
         
             
                previous_start = nil
         
     | 
| 
       113 
113 
     | 
    
         
             
                previous_status = nil
         
     | 
| 
         @@ -114,7 +114,7 @@ class AgingWorkInProgressChart < ChartBase 
     | 
|
| 
       114 
114 
     | 
    
         
             
              def ages_of_issues_that_crossed_column_boundary issues:, status_ids:
         
     | 
| 
       115 
115 
     | 
    
         
             
                issues.filter_map do |issue|
         
     | 
| 
       116 
116 
     | 
    
         
             
                  stop = issue.first_time_in_status(*status_ids)
         
     | 
| 
       117 
     | 
    
         
            -
                  start = issue.board.cycletime. 
     | 
| 
      
 117 
     | 
    
         
            +
                  start, = issue.board.cycletime.started_stopped_times(issue)
         
     | 
| 
       118 
118 
     | 
    
         | 
| 
       119 
119 
     | 
    
         
             
                  # Skip if either it hasn't crossed the boundary or we can't tell when it started.
         
     | 
| 
       120 
120 
     | 
    
         
             
                  next if stop.nil? || start.nil?
         
     | 
| 
         @@ -36,16 +36,15 @@ class AgingWorkTable < ChartBase 
     | 
|
| 
       36 
36 
     | 
    
         | 
| 
       37 
37 
     | 
    
         
             
              def expedited_but_not_started
         
     | 
| 
       38 
38 
     | 
    
         
             
                @issues.select do |issue|
         
     | 
| 
       39 
     | 
    
         
            -
                   
     | 
| 
       40 
     | 
    
         
            -
                   
     | 
| 
      
 39 
     | 
    
         
            +
                  started_time, stopped_time = issue.board.cycletime.started_stopped_times(issue)
         
     | 
| 
      
 40 
     | 
    
         
            +
                  started_time.nil? && stopped_time.nil? && issue.expedited?
         
     | 
| 
       41 
41 
     | 
    
         
             
                end.sort_by(&:created)
         
     | 
| 
       42 
42 
     | 
    
         
             
              end
         
     | 
| 
       43 
43 
     | 
    
         | 
| 
       44 
44 
     | 
    
         
             
              def select_aging_issues
         
     | 
| 
       45 
45 
     | 
    
         
             
                aging_issues = @issues.select do |issue|
         
     | 
| 
       46 
46 
     | 
    
         
             
                  cycletime = issue.board.cycletime
         
     | 
| 
       47 
     | 
    
         
            -
                  started = cycletime. 
     | 
| 
       48 
     | 
    
         
            -
                  stopped = cycletime.stopped_time(issue)
         
     | 
| 
      
 47 
     | 
    
         
            +
                  started, stopped = cycletime.started_stopped_times(issue)
         
     | 
| 
       49 
48 
     | 
    
         
             
                  next false if started.nil? || stopped
         
     | 
| 
       50 
49 
     | 
    
         
             
                  next true if issue.blocked_on_date?(@today, end_time: time_range.end) || issue.expedited?
         
     | 
| 
       51 
50 
     | 
    
         | 
| 
         @@ -64,7 +63,7 @@ class AgingWorkTable < ChartBase 
     | 
|
| 
       64 
63 
     | 
    
         
             
              end
         
     | 
| 
       65 
64 
     | 
    
         | 
| 
       66 
65 
     | 
    
         
             
              def blocked_text issue
         
     | 
| 
       67 
     | 
    
         
            -
                started_time = issue.board.cycletime. 
     | 
| 
      
 66 
     | 
    
         
            +
                started_time, _stopped_time = issue.board.cycletime.started_stopped_times(issue)
         
     | 
| 
       68 
67 
     | 
    
         
             
                return nil if started_time.nil?
         
     | 
| 
       69 
68 
     | 
    
         | 
| 
       70 
69 
     | 
    
         
             
                current = issue.blocked_stalled_changes(end_time: time_range.end)[-1]
         
     | 
    
        data/lib/jirametrics/board.rb
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            # frozen_string_literal: true
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
       3 
3 
     | 
    
         
             
            class Board
         
     | 
| 
       4 
     | 
    
         
            -
              attr_reader :visible_columns, :raw, :possible_statuses, :sprints, : 
     | 
| 
      
 4 
     | 
    
         
            +
              attr_reader :visible_columns, :raw, :possible_statuses, :sprints, :board_type
         
     | 
| 
       5 
5 
     | 
    
         
             
              attr_accessor :cycletime, :project_config
         
     | 
| 
       6 
6 
     | 
    
         | 
| 
       7 
7 
     | 
    
         
             
              def initialize raw:, possible_statuses: StatusCollection.new
         
     | 
| 
         @@ -15,24 +15,26 @@ class Board 
     | 
|
| 
       15 
15 
     | 
    
         
             
                # For a Kanban board, the first column here will always be called 'Backlog' and will NOT be
         
     | 
| 
       16 
16 
     | 
    
         
             
                # visible on the board. If the board is configured to have a kanban backlog then it will have
         
     | 
| 
       17 
17 
     | 
    
         
             
                # statuses matched to it and otherwise, there will be no statuses.
         
     | 
| 
       18 
     | 
    
         
            -
                if kanban?
         
     | 
| 
       19 
     | 
    
         
            -
                  @backlog_statuses = @possible_statuses.expand_statuses(status_ids_from_column columns[0]) do |unknown_status|
         
     | 
| 
       20 
     | 
    
         
            -
                    # There is a status defined as being 'backlog' that is no longer being returned in statuses.
         
     | 
| 
       21 
     | 
    
         
            -
                    # We used to display a warning for this but honestly, there is nothing that anyone can do about it
         
     | 
| 
       22 
     | 
    
         
            -
                    # so now we just quietly ignore it.
         
     | 
| 
       23 
     | 
    
         
            -
                  end
         
     | 
| 
       24 
     | 
    
         
            -
                  columns = columns[1..]
         
     | 
| 
       25 
     | 
    
         
            -
                else
         
     | 
| 
       26 
     | 
    
         
            -
                  # We currently don't know how to get the backlog status for a Scrum board
         
     | 
| 
       27 
     | 
    
         
            -
                  @backlog_statuses = []
         
     | 
| 
       28 
     | 
    
         
            -
                end
         
     | 
| 
      
 18 
     | 
    
         
            +
                columns = columns[1..] if kanban?
         
     | 
| 
       29 
19 
     | 
    
         | 
| 
      
 20 
     | 
    
         
            +
                @backlog_statuses = []
         
     | 
| 
       30 
21 
     | 
    
         
             
                @visible_columns = columns.filter_map do |column|
         
     | 
| 
       31 
22 
     | 
    
         
             
                  # It's possible for a column to be defined without any statuses and in this case, it won't be visible.
         
     | 
| 
       32 
23 
     | 
    
         
             
                  BoardColumn.new column unless status_ids_from_column(column).empty?
         
     | 
| 
       33 
24 
     | 
    
         
             
                end
         
     | 
| 
       34 
25 
     | 
    
         
             
              end
         
     | 
| 
       35 
26 
     | 
    
         | 
| 
      
 27 
     | 
    
         
            +
              def backlog_statuses
         
     | 
| 
      
 28 
     | 
    
         
            +
                if @backlog_statuses.empty? && kanban?
         
     | 
| 
      
 29 
     | 
    
         
            +
                  status_ids = status_ids_from_column raw['columnConfig']['columns'].first
         
     | 
| 
      
 30 
     | 
    
         
            +
                  @backlog_statuses = @possible_statuses.expand_statuses(status_ids) do |unknown_status|
         
     | 
| 
      
 31 
     | 
    
         
            +
                    # If a status is returned here that is no longer in the system then there's nothing useful
         
     | 
| 
      
 32 
     | 
    
         
            +
                    # we can do about it. Ignore it.
         
     | 
| 
      
 33 
     | 
    
         
            +
                  end
         
     | 
| 
      
 34 
     | 
    
         
            +
                end
         
     | 
| 
      
 35 
     | 
    
         
            +
                @backlog_statuses
         
     | 
| 
      
 36 
     | 
    
         
            +
              end
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
       36 
38 
     | 
    
         
             
              def server_url_prefix
         
     | 
| 
       37 
39 
     | 
    
         
             
                raise "Cannot parse self: #{@raw['self'].inspect}" unless @raw['self'] =~ /^(https?:\/\/.+)\/rest\//
         
     | 
| 
       38 
40 
     | 
    
         | 
| 
         @@ -144,8 +144,7 @@ class ChartBase 
     | 
|
| 
       144 
144 
     | 
    
         
             
              def completed_issues_in_range include_unstarted: false
         
     | 
| 
       145 
145 
     | 
    
         
             
                issues.select do |issue|
         
     | 
| 
       146 
146 
     | 
    
         
             
                  cycletime = issue.board.cycletime
         
     | 
| 
       147 
     | 
    
         
            -
                  stopped_time = cycletime. 
     | 
| 
       148 
     | 
    
         
            -
                  started_time = cycletime.started_time(issue)
         
     | 
| 
      
 147 
     | 
    
         
            +
                  started_time, stopped_time = cycletime.started_stopped_times(issue)
         
     | 
| 
       149 
148 
     | 
    
         | 
| 
       150 
149 
     | 
    
         
             
                  stopped_time &&
         
     | 
| 
       151 
150 
     | 
    
         
             
                    date_range.include?(stopped_time.to_date) && # Remove outside range
         
     | 
| 
         @@ -179,7 +178,7 @@ class ChartBase 
     | 
|
| 
       179 
178 
     | 
    
         
             
              def format_status name_or_id, board:, is_category: false
         
     | 
| 
       180 
179 
     | 
    
         
             
                begin
         
     | 
| 
       181 
180 
     | 
    
         
             
                  statuses = board.possible_statuses.expand_statuses([name_or_id])
         
     | 
| 
       182 
     | 
    
         
            -
                rescue StatusNotFoundError 
     | 
| 
      
 181 
     | 
    
         
            +
                rescue StatusNotFoundError
         
     | 
| 
       183 
182 
     | 
    
         
             
                  return "<span style='color: red'>#{name_or_id}</span>"
         
     | 
| 
       184 
183 
     | 
    
         
             
                end
         
     | 
| 
       185 
184 
     | 
    
         | 
| 
         @@ -189,10 +188,9 @@ class ChartBase 
     | 
|
| 
       189 
188 
     | 
    
         
             
                visibility = ''
         
     | 
| 
       190 
189 
     | 
    
         
             
                if is_category == false && board.visible_columns.none? { |column| column.status_ids.include? status.id }
         
     | 
| 
       191 
190 
     | 
    
         
             
                  visibility = icon_span(
         
     | 
| 
       192 
     | 
    
         
            -
             
     | 
| 
       193 
     | 
    
         
            -
             
     | 
| 
       194 
     | 
    
         
            -
             
     | 
| 
       195 
     | 
    
         
            -
             
     | 
| 
      
 191 
     | 
    
         
            +
                    title: "Not visible: The status #{status.name.inspect} is not mapped to any column and will not be visible",
         
     | 
| 
      
 192 
     | 
    
         
            +
                    icon: ' 👀'
         
     | 
| 
      
 193 
     | 
    
         
            +
                  )
         
     | 
| 
       196 
194 
     | 
    
         
             
                end
         
     | 
| 
       197 
195 
     | 
    
         
             
                text = is_category ? status.category_name : status.name
         
     | 
| 
       198 
196 
     | 
    
         
             
                "<span title='Category: #{status.category_name}'>#{color_block color.name} #{text}</span>#{visibility}"
         
     | 
| 
         @@ -26,31 +26,50 @@ class CycleTimeConfig 
     | 
|
| 
       26 
26 
     | 
    
         
             
              end
         
     | 
| 
       27 
27 
     | 
    
         | 
| 
       28 
28 
     | 
    
         
             
              def in_progress? issue
         
     | 
| 
       29 
     | 
    
         
            -
                started_time 
     | 
| 
      
 29 
     | 
    
         
            +
                started_time, stopped_time = started_stopped_times(issue)
         
     | 
| 
      
 30 
     | 
    
         
            +
                started_time && stopped_time.nil?
         
     | 
| 
       30 
31 
     | 
    
         
             
              end
         
     | 
| 
       31 
32 
     | 
    
         | 
| 
       32 
33 
     | 
    
         
             
              def done? issue
         
     | 
| 
       33 
     | 
    
         
            -
                 
     | 
| 
      
 34 
     | 
    
         
            +
                started_stopped_times(issue).last
         
     | 
| 
       34 
35 
     | 
    
         
             
              end
         
     | 
| 
       35 
36 
     | 
    
         | 
| 
       36 
37 
     | 
    
         
             
              def started_time issue
         
     | 
| 
       37 
     | 
    
         
            -
                 
     | 
| 
      
 38 
     | 
    
         
            +
                deprecated date: '2024-10-16', message: 'Use started_stopped_times() instead'
         
     | 
| 
      
 39 
     | 
    
         
            +
                started_stopped_times(issue).first
         
     | 
| 
       38 
40 
     | 
    
         
             
              end
         
     | 
| 
       39 
41 
     | 
    
         | 
| 
       40 
42 
     | 
    
         
             
              def stopped_time issue
         
     | 
| 
       41 
     | 
    
         
            -
                 
     | 
| 
      
 43 
     | 
    
         
            +
                deprecated date: '2024-10-16', message: 'Use started_stopped_times() instead'
         
     | 
| 
      
 44 
     | 
    
         
            +
                started_stopped_times(issue).last
         
     | 
| 
      
 45 
     | 
    
         
            +
              end
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
              def started_stopped_times issue
         
     | 
| 
      
 48 
     | 
    
         
            +
                started = @start_at.call(issue)
         
     | 
| 
      
 49 
     | 
    
         
            +
                stopped = @stop_at.call(issue)
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
                # In the case where started and stopped are exactly the same time, we pretend that
         
     | 
| 
      
 52 
     | 
    
         
            +
                # it just stopped and never started. This allows us to have logic like 'in or right of'
         
     | 
| 
      
 53 
     | 
    
         
            +
                # for the start and not have it conflict.
         
     | 
| 
      
 54 
     | 
    
         
            +
                started = nil if started == stopped
         
     | 
| 
      
 55 
     | 
    
         
            +
             
     | 
| 
      
 56 
     | 
    
         
            +
                [started, stopped]
         
     | 
| 
      
 57 
     | 
    
         
            +
              end
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
              def started_stopped_dates issue
         
     | 
| 
      
 60 
     | 
    
         
            +
                started_time, stopped_time = started_stopped_times(issue)
         
     | 
| 
      
 61 
     | 
    
         
            +
                [started_time&.to_date, stopped_time&.to_date]
         
     | 
| 
       42 
62 
     | 
    
         
             
              end
         
     | 
| 
       43 
63 
     | 
    
         | 
| 
       44 
64 
     | 
    
         
             
              def cycletime issue
         
     | 
| 
       45 
     | 
    
         
            -
                start =  
     | 
| 
       46 
     | 
    
         
            -
                stop = stopped_time(issue)
         
     | 
| 
      
 65 
     | 
    
         
            +
                start, stop = started_stopped_times(issue)
         
     | 
| 
       47 
66 
     | 
    
         
             
                return nil if start.nil? || stop.nil?
         
     | 
| 
       48 
67 
     | 
    
         | 
| 
       49 
68 
     | 
    
         
             
                (stop.to_date - start.to_date).to_i + 1
         
     | 
| 
       50 
69 
     | 
    
         
             
              end
         
     | 
| 
       51 
70 
     | 
    
         | 
| 
       52 
71 
     | 
    
         
             
              def age issue, today: nil
         
     | 
| 
       53 
     | 
    
         
            -
                start =  
     | 
| 
      
 72 
     | 
    
         
            +
                start = started_stopped_times(issue).first
         
     | 
| 
       54 
73 
     | 
    
         
             
                stop = today || @today || Date.today
         
     | 
| 
       55 
74 
     | 
    
         
             
                return nil if start.nil? || stop.nil?
         
     | 
| 
       56 
75 
     | 
    
         | 
| 
         @@ -30,7 +30,7 @@ class CycletimeHistogram < ChartBase 
     | 
|
| 
       30 
30 
     | 
    
         
             
                stopped_issues = completed_issues_in_range include_unstarted: true
         
     | 
| 
       31 
31 
     | 
    
         | 
| 
       32 
32 
     | 
    
         
             
                # For the histogram, we only want to consider items that have both a start and a stop time.
         
     | 
| 
       33 
     | 
    
         
            -
                histogram_issues = stopped_issues.select { |issue| issue.board.cycletime. 
     | 
| 
      
 33 
     | 
    
         
            +
                histogram_issues = stopped_issues.select { |issue| issue.board.cycletime.started_stopped_times(issue).first }
         
     | 
| 
       34 
34 
     | 
    
         
             
                rules_to_issues = group_issues histogram_issues
         
     | 
| 
       35 
35 
     | 
    
         | 
| 
       36 
36 
     | 
    
         
             
                data_sets = rules_to_issues.keys.collect do |rules|
         
     | 
| 
         @@ -24,7 +24,7 @@ class CycletimeScatterplot < ChartBase 
     | 
|
| 
       24 
24 
     | 
    
         
             
                    predict that most work of this type will complete in <%= overall_percent_line %> days or
         
     | 
| 
       25 
25 
     | 
    
         
             
                    less. The other lines reflect the 85% line for that respective type of work.
         
     | 
| 
       26 
26 
     | 
    
         
             
                  </div>
         
     | 
| 
       27 
     | 
    
         
            -
                  #{ 
     | 
| 
      
 27 
     | 
    
         
            +
                  #{describe_non_working_days}
         
     | 
| 
       28 
28 
     | 
    
         
             
                HTML
         
     | 
| 
       29 
29 
     | 
    
         | 
| 
       30 
30 
     | 
    
         
             
                init_configuration_block block do
         
     | 
| 
         @@ -53,10 +53,7 @@ class CycletimeScatterplot < ChartBase 
     | 
|
| 
       53 
53 
     | 
    
         
             
              def create_datasets completed_issues
         
     | 
| 
       54 
54 
     | 
    
         
             
                data_sets = []
         
     | 
| 
       55 
55 
     | 
    
         | 
| 
       56 
     | 
    
         
            -
                 
     | 
| 
       57 
     | 
    
         
            -
             
     | 
| 
       58 
     | 
    
         
            -
                groups.each_key do |rules|
         
     | 
| 
       59 
     | 
    
         
            -
                  completed_issues_by_type = groups[rules]
         
     | 
| 
      
 56 
     | 
    
         
            +
                group_issues(completed_issues).each do |rules, completed_issues_by_type|
         
     | 
| 
       60 
57 
     | 
    
         
             
                  label = rules.label
         
     | 
| 
       61 
58 
     | 
    
         
             
                  color = rules.color
         
     | 
| 
       62 
59 
     | 
    
         
             
                  percent_line = calculate_percent_line completed_issues_by_type
         
     | 
| 
         @@ -117,7 +114,7 @@ class CycletimeScatterplot < ChartBase 
     | 
|
| 
       117 
114 
     | 
    
         | 
| 
       118 
115 
     | 
    
         
             
                {
         
     | 
| 
       119 
116 
     | 
    
         
             
                  y: cycle_time,
         
     | 
| 
       120 
     | 
    
         
            -
                  x: chart_format(issue.board.cycletime. 
     | 
| 
      
 117 
     | 
    
         
            +
                  x: chart_format(issue.board.cycletime.started_stopped_times(issue).last),
         
     | 
| 
       121 
118 
     | 
    
         
             
                  title: ["#{issue.key} : #{issue.summary} (#{label_days(cycle_time)})"]
         
     | 
| 
       122 
119 
     | 
    
         
             
                }
         
     | 
| 
       123 
120 
     | 
    
         
             
              end
         
     | 
| 
         @@ -4,7 +4,7 @@ require 'jirametrics/daily_wip_chart' 
     | 
|
| 
       4 
4 
     | 
    
         | 
| 
       5 
5 
     | 
    
         
             
            class DailyWipByAgeChart < DailyWipChart
         
     | 
| 
       6 
6 
     | 
    
         
             
              def initialize block
         
     | 
| 
       7 
     | 
    
         
            -
                super 
     | 
| 
      
 7 
     | 
    
         
            +
                super
         
     | 
| 
       8 
8 
     | 
    
         | 
| 
       9 
9 
     | 
    
         
             
                add_trend_line line_color: '--aging-work-in-progress-by-age-trend-line-color', group_labels: [
         
     | 
| 
       10 
10 
     | 
    
         
             
                  'Less than a day',
         
     | 
| 
         @@ -49,9 +49,7 @@ class DailyWipByAgeChart < DailyWipChart 
     | 
|
| 
       49 
49 
     | 
    
         
             
              end
         
     | 
| 
       50 
50 
     | 
    
         | 
| 
       51 
51 
     | 
    
         
             
              def default_grouping_rules issue:, rules:
         
     | 
| 
       52 
     | 
    
         
            -
                 
     | 
| 
       53 
     | 
    
         
            -
                started = cycletime.started_time(issue)&.to_date
         
     | 
| 
       54 
     | 
    
         
            -
                stopped = cycletime.stopped_time(issue)&.to_date
         
     | 
| 
      
 52 
     | 
    
         
            +
                started, stopped = issue.board.cycletime.started_stopped_dates(issue)
         
     | 
| 
       55 
53 
     | 
    
         | 
| 
       56 
54 
     | 
    
         
             
                rules.issue_hint = "(age: #{label_days (rules.current_date - started + 1).to_i})" if started
         
     | 
| 
       57 
55 
     | 
    
         | 
| 
         @@ -39,8 +39,8 @@ class DailyWipByBlockedStalledChart < DailyWipChart 
     | 
|
| 
       39 
39 
     | 
    
         
             
              end
         
     | 
| 
       40 
40 
     | 
    
         | 
| 
       41 
41 
     | 
    
         
             
              def default_grouping_rules issue:, rules:
         
     | 
| 
       42 
     | 
    
         
            -
                started = issue.board.cycletime. 
     | 
| 
       43 
     | 
    
         
            -
                stopped_date =  
     | 
| 
      
 42 
     | 
    
         
            +
                started, stopped = issue.board.cycletime.started_stopped_times(issue)
         
     | 
| 
      
 43 
     | 
    
         
            +
                stopped_date = stopped&.to_date
         
     | 
| 
       44 
44 
     | 
    
         | 
| 
       45 
45 
     | 
    
         
             
                date = rules.current_date
         
     | 
| 
       46 
46 
     | 
    
         
             
                change = issue.blocked_stalled_by_date(date_range: date..date, chart_end_time: time_range.end)[date]
         
     | 
| 
         @@ -6,7 +6,7 @@ class DailyGroupingRules < GroupingRules 
     | 
|
| 
       6 
6 
     | 
    
         
             
              attr_accessor :current_date, :group_priority, :issue_hint
         
     | 
| 
       7 
7 
     | 
    
         | 
| 
       8 
8 
     | 
    
         
             
              def initialize
         
     | 
| 
       9 
     | 
    
         
            -
                super 
     | 
| 
      
 9 
     | 
    
         
            +
                super
         
     | 
| 
       10 
10 
     | 
    
         
             
                @group_priority = 0
         
     | 
| 
       11 
11 
     | 
    
         
             
              end
         
     | 
| 
       12 
12 
     | 
    
         
             
            end
         
     | 
| 
         @@ -22,10 +22,10 @@ class DailyWipChart < ChartBase 
     | 
|
| 
       22 
22 
     | 
    
         | 
| 
       23 
23 
     | 
    
         
             
                instance_eval(&block) if block
         
     | 
| 
       24 
24 
     | 
    
         | 
| 
       25 
     | 
    
         
            -
                 
     | 
| 
       26 
     | 
    
         
            -
             
     | 
| 
       27 
     | 
    
         
            -
             
     | 
| 
       28 
     | 
    
         
            -
                   
     | 
| 
      
 25 
     | 
    
         
            +
                return if @group_by_block
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                grouping_rules do |issue, rules|
         
     | 
| 
      
 28 
     | 
    
         
            +
                  default_grouping_rules issue: issue, rules: rules
         
     | 
| 
       29 
29 
     | 
    
         
             
                end
         
     | 
| 
       30 
30 
     | 
    
         
             
              end
         
     | 
| 
       31 
31 
     | 
    
         | 
| 
         @@ -66,9 +66,7 @@ class DailyWipChart < ChartBase 
     | 
|
| 
       66 
66 
     | 
    
         
             
                hash = {}
         
     | 
| 
       67 
67 
     | 
    
         | 
| 
       68 
68 
     | 
    
         
             
                @issues.each do |issue|
         
     | 
| 
       69 
     | 
    
         
            -
                   
     | 
| 
       70 
     | 
    
         
            -
                  start = cycletime.started_time(issue)&.to_date
         
     | 
| 
       71 
     | 
    
         
            -
                  stop = cycletime.stopped_time(issue)&.to_date
         
     | 
| 
      
 69 
     | 
    
         
            +
                  start, stop = issue.board.cycletime.started_stopped_dates(issue)
         
     | 
| 
       72 
70 
     | 
    
         
             
                  next if start.nil? && stop.nil?
         
     | 
| 
       73 
71 
     | 
    
         | 
| 
       74 
72 
     | 
    
         
             
                  # If it stopped but never started then assume it started at creation so the data points
         
     | 
| 
         @@ -158,7 +156,7 @@ class DailyWipChart < ChartBase 
     | 
|
| 
       158 
156 
     | 
    
         | 
| 
       159 
157 
     | 
    
         
             
                {
         
     | 
| 
       160 
158 
     | 
    
         
             
                  type: 'line',
         
     | 
| 
       161 
     | 
    
         
            -
                  label:  
     | 
| 
      
 159 
     | 
    
         
            +
                  label: 'Trendline',
         
     | 
| 
       162 
160 
     | 
    
         
             
                  data: data_points,
         
     | 
| 
       163 
161 
     | 
    
         
             
                  fill: false,
         
     | 
| 
       164 
162 
     | 
    
         
             
                  borderWidth: 1,
         
     | 
| 
         @@ -48,6 +48,7 @@ class DataQualityReport < ChartBase 
     | 
|
| 
       48 
48 
     | 
    
         
             
                  scan_for_issues_not_created_in_a_backlog_status entry: entry, backlog_statuses: backlog_statuses
         
     | 
| 
       49 
49 
     | 
    
         
             
                  scan_for_stopped_before_started entry: entry
         
     | 
| 
       50 
50 
     | 
    
         
             
                  scan_for_issues_not_started_with_subtasks_that_have entry: entry
         
     | 
| 
      
 51 
     | 
    
         
            +
                  scan_for_incomplete_subtasks_when_issue_done entry: entry
         
     | 
| 
       51 
52 
     | 
    
         
             
                  scan_for_discarded_data entry: entry
         
     | 
| 
       52 
53 
     | 
    
         
             
                end
         
     | 
| 
       53 
54 
     | 
    
         | 
| 
         @@ -87,9 +88,7 @@ class DataQualityReport < ChartBase 
     | 
|
| 
       87 
88 
     | 
    
         | 
| 
       88 
89 
     | 
    
         
             
              def initialize_entries
         
     | 
| 
       89 
90 
     | 
    
         
             
                @entries = @issues.filter_map do |issue|
         
     | 
| 
       90 
     | 
    
         
            -
                   
     | 
| 
       91 
     | 
    
         
            -
                  started = cycletime.started_time(issue)
         
     | 
| 
       92 
     | 
    
         
            -
                  stopped = cycletime.stopped_time(issue)
         
     | 
| 
      
 91 
     | 
    
         
            +
                  started, stopped = issue.board.cycletime.started_stopped_times(issue)
         
     | 
| 
       93 
92 
     | 
    
         
             
                  next if stopped && stopped < time_range.begin
         
     | 
| 
       94 
93 
     | 
    
         
             
                  next if started && started > time_range.end
         
     | 
| 
       95 
94 
     | 
    
         | 
| 
         @@ -223,14 +222,13 @@ class DataQualityReport < ChartBase 
     | 
|
| 
       223 
222 
     | 
    
         | 
| 
       224 
223 
     | 
    
         
             
                started_subtasks = []
         
     | 
| 
       225 
224 
     | 
    
         
             
                entry.issue.subtasks.each do |subtask|
         
     | 
| 
       226 
     | 
    
         
            -
                  started_subtasks << subtask if subtask.board.cycletime. 
     | 
| 
      
 225 
     | 
    
         
            +
                  started_subtasks << subtask if subtask.board.cycletime.started_stopped_times(subtask).first
         
     | 
| 
       227 
226 
     | 
    
         
             
                end
         
     | 
| 
       228 
227 
     | 
    
         | 
| 
       229 
228 
     | 
    
         
             
                return if started_subtasks.empty?
         
     | 
| 
       230 
229 
     | 
    
         | 
| 
       231 
230 
     | 
    
         
             
                subtask_labels = started_subtasks.collect do |subtask|
         
     | 
| 
       232 
     | 
    
         
            -
                   
     | 
| 
       233 
     | 
    
         
            -
                    "#{subtask.summary[..50].inspect}"
         
     | 
| 
      
 231 
     | 
    
         
            +
                  subtask_label(subtask)
         
     | 
| 
       234 
232 
     | 
    
         
             
                end
         
     | 
| 
       235 
233 
     | 
    
         
             
                entry.report(
         
     | 
| 
       236 
234 
     | 
    
         
             
                  problem_key: :issue_not_started_but_subtasks_have,
         
     | 
| 
         @@ -238,6 +236,47 @@ class DataQualityReport < ChartBase 
     | 
|
| 
       238 
236 
     | 
    
         
             
                )
         
     | 
| 
       239 
237 
     | 
    
         
             
              end
         
     | 
| 
       240 
238 
     | 
    
         | 
| 
      
 239 
     | 
    
         
            +
              def subtask_label subtask
         
     | 
| 
      
 240 
     | 
    
         
            +
                "<img src='#{subtask.type_icon_url}' /> #{link_to_issue(subtask)} #{subtask.summary[..50].inspect}"
         
     | 
| 
      
 241 
     | 
    
         
            +
              end
         
     | 
| 
      
 242 
     | 
    
         
            +
             
     | 
| 
      
 243 
     | 
    
         
            +
              def time_as_english(from_time, to_time)
         
     | 
| 
      
 244 
     | 
    
         
            +
                delta = (to_time - from_time).to_i
         
     | 
| 
      
 245 
     | 
    
         
            +
                return "#{delta} seconds" if delta < 60
         
     | 
| 
      
 246 
     | 
    
         
            +
             
     | 
| 
      
 247 
     | 
    
         
            +
                delta /= 60
         
     | 
| 
      
 248 
     | 
    
         
            +
                return "#{delta} minutes" if delta < 60
         
     | 
| 
      
 249 
     | 
    
         
            +
             
     | 
| 
      
 250 
     | 
    
         
            +
                delta /= 60
         
     | 
| 
      
 251 
     | 
    
         
            +
                return "#{delta} hours" if delta < 24
         
     | 
| 
      
 252 
     | 
    
         
            +
             
     | 
| 
      
 253 
     | 
    
         
            +
                delta /= 24
         
     | 
| 
      
 254 
     | 
    
         
            +
                "#{delta} days"
         
     | 
| 
      
 255 
     | 
    
         
            +
              end
         
     | 
| 
      
 256 
     | 
    
         
            +
             
     | 
| 
      
 257 
     | 
    
         
            +
              def scan_for_incomplete_subtasks_when_issue_done entry:
         
     | 
| 
      
 258 
     | 
    
         
            +
                return unless entry.stopped
         
     | 
| 
      
 259 
     | 
    
         
            +
             
     | 
| 
      
 260 
     | 
    
         
            +
                subtask_labels = entry.issue.subtasks.filter_map do |subtask|
         
     | 
| 
      
 261 
     | 
    
         
            +
                  subtask_started, subtask_stopped = subtask.board.cycletime.started_stopped_times(subtask)
         
     | 
| 
      
 262 
     | 
    
         
            +
             
     | 
| 
      
 263 
     | 
    
         
            +
                  if !subtask_started && !subtask_stopped
         
     | 
| 
      
 264 
     | 
    
         
            +
                    "#{subtask_label subtask} (Not even started)"
         
     | 
| 
      
 265 
     | 
    
         
            +
                  elsif !subtask_stopped
         
     | 
| 
      
 266 
     | 
    
         
            +
                    "#{subtask_label subtask} (Still not done)"
         
     | 
| 
      
 267 
     | 
    
         
            +
                  elsif subtask_stopped > entry.stopped
         
     | 
| 
      
 268 
     | 
    
         
            +
                    "#{subtask_label subtask} (Closed #{time_as_english entry.stopped, subtask_stopped} later)"
         
     | 
| 
      
 269 
     | 
    
         
            +
                  end
         
     | 
| 
      
 270 
     | 
    
         
            +
                end
         
     | 
| 
      
 271 
     | 
    
         
            +
             
     | 
| 
      
 272 
     | 
    
         
            +
                return if subtask_labels.empty?
         
     | 
| 
      
 273 
     | 
    
         
            +
             
     | 
| 
      
 274 
     | 
    
         
            +
                entry.report(
         
     | 
| 
      
 275 
     | 
    
         
            +
                  problem_key: :incomplete_subtasks_when_issue_done,
         
     | 
| 
      
 276 
     | 
    
         
            +
                  detail: subtask_labels.join('<br />')
         
     | 
| 
      
 277 
     | 
    
         
            +
                )
         
     | 
| 
      
 278 
     | 
    
         
            +
              end
         
     | 
| 
      
 279 
     | 
    
         
            +
             
     | 
| 
       241 
280 
     | 
    
         
             
              def label_issues number
         
     | 
| 
       242 
281 
     | 
    
         
             
                return '1 item' if number == 1
         
     | 
| 
       243 
282 
     | 
    
         | 
| 
         @@ -183,9 +183,8 @@ class DependencyChart < ChartBase 
     | 
|
| 
       183 
183 
     | 
    
         
             
                  return stdout.read
         
     | 
| 
       184 
184 
     | 
    
         
             
                end
         
     | 
| 
       185 
185 
     | 
    
         
             
              rescue # rubocop:disable Style/RescueStandardError
         
     | 
| 
       186 
     | 
    
         
            -
                message =  
     | 
| 
       187 
     | 
    
         
            -
             
     | 
| 
       188 
     | 
    
         
            -
                puts message
         
     | 
| 
      
 186 
     | 
    
         
            +
                message = 'Unable to generate the dependency chart because graphviz could not be found in the path.'
         
     | 
| 
      
 187 
     | 
    
         
            +
                file_system.log message, also_write_to_stderr: true
         
     | 
| 
       189 
188 
     | 
    
         
             
                message
         
     | 
| 
       190 
189 
     | 
    
         
             
              end
         
     | 
| 
       191 
190 
     | 
    
         | 
| 
         @@ -229,7 +228,7 @@ class DependencyChart < ChartBase 
     | 
|
| 
       229 
228 
     | 
    
         
             
                  elsif is_done
         
     | 
| 
       230 
229 
     | 
    
         
             
                    line2 << 'Done'
         
     | 
| 
       231 
230 
     | 
    
         
             
                  else
         
     | 
| 
       232 
     | 
    
         
            -
                    started_at = issue.board.cycletime. 
     | 
| 
      
 231 
     | 
    
         
            +
                    started_at = issue.board.cycletime.started_stopped_times(issue).first
         
     | 
| 
       233 
232 
     | 
    
         
             
                    if started_at.nil?
         
     | 
| 
       234 
233 
     | 
    
         
             
                      line2 << 'Not started'
         
     | 
| 
       235 
234 
     | 
    
         
             
                    else
         
     | 
| 
         @@ -31,7 +31,7 @@ module DiscardChangesBefore 
     | 
|
| 
       31 
31 
     | 
    
         
             
                discard_changes_before_hook issues_cutoff_times
         
     | 
| 
       32 
32 
     | 
    
         | 
| 
       33 
33 
     | 
    
         
             
                issues_cutoff_times.each do |issue, cutoff_time|
         
     | 
| 
       34 
     | 
    
         
            -
                  issue. 
     | 
| 
      
 34 
     | 
    
         
            +
                  issue.discard_changes_before cutoff_time
         
     | 
| 
       35 
35 
     | 
    
         
             
                end
         
     | 
| 
       36 
36 
     | 
    
         
             
              end
         
     | 
| 
       37 
37 
     | 
    
         
             
            end
         
     | 
| 
         @@ -41,8 +41,8 @@ class Downloader 
     | 
|
| 
       41 
41 
     | 
    
         
             
                remove_old_files
         
     | 
| 
       42 
42 
     | 
    
         
             
                download_statuses
         
     | 
| 
       43 
43 
     | 
    
         
             
                find_board_ids.each do |id|
         
     | 
| 
       44 
     | 
    
         
            -
                  download_board_configuration board_id: id
         
     | 
| 
       45 
     | 
    
         
            -
                  download_issues  
     | 
| 
      
 44 
     | 
    
         
            +
                  board = download_board_configuration board_id: id
         
     | 
| 
      
 45 
     | 
    
         
            +
                  download_issues board: board
         
     | 
| 
       46 
46 
     | 
    
         
             
                end
         
     | 
| 
       47 
47 
     | 
    
         | 
| 
       48 
48 
     | 
    
         
             
                save_metadata
         
     | 
| 
         @@ -64,19 +64,19 @@ class Downloader 
     | 
|
| 
       64 
64 
     | 
    
         
             
                ids
         
     | 
| 
       65 
65 
     | 
    
         
             
              end
         
     | 
| 
       66 
66 
     | 
    
         | 
| 
       67 
     | 
    
         
            -
              def download_issues  
     | 
| 
       68 
     | 
    
         
            -
                log "  Downloading primary issues for board #{ 
     | 
| 
      
 67 
     | 
    
         
            +
              def download_issues board:
         
     | 
| 
      
 68 
     | 
    
         
            +
                log "  Downloading primary issues for board #{board.id}", both: true
         
     | 
| 
       69 
69 
     | 
    
         
             
                path = "#{@target_path}#{@download_config.project_config.file_prefix}_issues/"
         
     | 
| 
       70 
70 
     | 
    
         
             
                unless Dir.exist?(path)
         
     | 
| 
       71 
71 
     | 
    
         
             
                  log "  Creating path #{path}"
         
     | 
| 
       72 
72 
     | 
    
         
             
                  Dir.mkdir(path)
         
     | 
| 
       73 
73 
     | 
    
         
             
                end
         
     | 
| 
       74 
74 
     | 
    
         | 
| 
       75 
     | 
    
         
            -
                filter_id = @board_id_to_filter_id[ 
     | 
| 
      
 75 
     | 
    
         
            +
                filter_id = @board_id_to_filter_id[board.id]
         
     | 
| 
       76 
76 
     | 
    
         
             
                jql = make_jql(filter_id: filter_id)
         
     | 
| 
       77 
     | 
    
         
            -
                jira_search_by_jql(jql: jql, initial_query: true,  
     | 
| 
      
 77 
     | 
    
         
            +
                jira_search_by_jql(jql: jql, initial_query: true, board: board, path: path)
         
     | 
| 
       78 
78 
     | 
    
         | 
| 
       79 
     | 
    
         
            -
                log "  Downloading linked issues for board #{ 
     | 
| 
      
 79 
     | 
    
         
            +
                log "  Downloading linked issues for board #{board.id}", both: true
         
     | 
| 
       80 
80 
     | 
    
         
             
                loop do
         
     | 
| 
       81 
81 
     | 
    
         
             
                  @issue_keys_pending_download.reject! { |key| @issue_keys_downloaded_in_current_run.include? key }
         
     | 
| 
       82 
82 
     | 
    
         
             
                  break if @issue_keys_pending_download.empty?
         
     | 
| 
         @@ -84,11 +84,11 @@ class Downloader 
     | 
|
| 
       84 
84 
     | 
    
         
             
                  keys_to_request = @issue_keys_pending_download[0..99]
         
     | 
| 
       85 
85 
     | 
    
         
             
                  @issue_keys_pending_download.reject! { |key| keys_to_request.include? key }
         
     | 
| 
       86 
86 
     | 
    
         
             
                  jql = "key in (#{keys_to_request.join(', ')})"
         
     | 
| 
       87 
     | 
    
         
            -
                  jira_search_by_jql(jql: jql, initial_query: false,  
     | 
| 
      
 87 
     | 
    
         
            +
                  jira_search_by_jql(jql: jql, initial_query: false, board: board, path: path)
         
     | 
| 
       88 
88 
     | 
    
         
             
                end
         
     | 
| 
       89 
89 
     | 
    
         
             
              end
         
     | 
| 
       90 
90 
     | 
    
         | 
| 
       91 
     | 
    
         
            -
              def jira_search_by_jql jql:, initial_query:,  
     | 
| 
      
 91 
     | 
    
         
            +
              def jira_search_by_jql jql:, initial_query:, board:, path:
         
     | 
| 
       92 
92 
     | 
    
         
             
                intercept_jql = @download_config.project_config.settings['intercept_jql']
         
     | 
| 
       93 
93 
     | 
    
         
             
                jql = intercept_jql.call jql if intercept_jql
         
     | 
| 
       94 
94 
     | 
    
         | 
| 
         @@ -108,8 +108,8 @@ class Downloader 
     | 
|
| 
       108 
108 
     | 
    
         
             
                    issue_json['exporter'] = {
         
     | 
| 
       109 
109 
     | 
    
         
             
                      'in_initial_query' => initial_query
         
     | 
| 
       110 
110 
     | 
    
         
             
                    }
         
     | 
| 
       111 
     | 
    
         
            -
                    identify_other_issues_to_be_downloaded issue_json
         
     | 
| 
       112 
     | 
    
         
            -
                    file = "#{issue_json['key']}-#{ 
     | 
| 
      
 111 
     | 
    
         
            +
                    identify_other_issues_to_be_downloaded raw_issue: issue_json, board: board
         
     | 
| 
      
 112 
     | 
    
         
            +
                    file = "#{issue_json['key']}-#{board.id}.json"
         
     | 
| 
       113 
113 
     | 
    
         | 
| 
       114 
114 
     | 
    
         
             
                    @file_system.save_json(json: issue_json, filename: File.join(path, file))
         
     | 
| 
       115 
115 
     | 
    
         
             
                  end
         
     | 
| 
         @@ -124,8 +124,8 @@ class Downloader 
     | 
|
| 
       124 
124 
     | 
    
         
             
                end
         
     | 
| 
       125 
125 
     | 
    
         
             
              end
         
     | 
| 
       126 
126 
     | 
    
         | 
| 
       127 
     | 
    
         
            -
              def identify_other_issues_to_be_downloaded raw_issue
         
     | 
| 
       128 
     | 
    
         
            -
                issue = Issue.new raw: raw_issue, board:  
     | 
| 
      
 127 
     | 
    
         
            +
              def identify_other_issues_to_be_downloaded raw_issue:, board:
         
     | 
| 
      
 128 
     | 
    
         
            +
                issue = Issue.new raw: raw_issue, board: board
         
     | 
| 
       129 
129 
     | 
    
         
             
                @issue_keys_downloaded_in_current_run << issue.key
         
     | 
| 
       130 
130 
     | 
    
         | 
| 
       131 
131 
     | 
    
         
             
                # Parent
         
     | 
| 
         @@ -171,6 +171,7 @@ class Downloader 
     | 
|
| 
       171 
171 
     | 
    
         
             
                @board_id_to_filter_id[board_id] = json['filter']['id'].to_i
         
     | 
| 
       172 
172 
     | 
    
         | 
| 
       173 
173 
     | 
    
         
             
                download_sprints board_id: board_id if json['type'] == 'scrum'
         
     | 
| 
      
 174 
     | 
    
         
            +
                Board.new raw: json
         
     | 
| 
       174 
175 
     | 
    
         
             
              end
         
     | 
| 
       175 
176 
     | 
    
         | 
| 
       176 
177 
     | 
    
         
             
              def download_sprints board_id:
         
     | 
| 
         @@ -83,8 +83,7 @@ class EstimateAccuracyChart < ChartBase 
     | 
|
| 
       83 
83 
     | 
    
         | 
| 
       84 
84 
     | 
    
         
             
                issues.each do |issue|
         
     | 
| 
       85 
85 
     | 
    
         
             
                  cycletime = issue.board.cycletime
         
     | 
| 
       86 
     | 
    
         
            -
                  start_time = cycletime. 
     | 
| 
       87 
     | 
    
         
            -
                  stop_time = cycletime.stopped_time(issue)
         
     | 
| 
      
 86 
     | 
    
         
            +
                  start_time, stop_time = cycletime.started_stopped_times(issue)
         
     | 
| 
       88 
87 
     | 
    
         | 
| 
       89 
88 
     | 
    
         
             
                  next unless start_time
         
     | 
| 
       90 
89 
     | 
    
         | 
| 
         @@ -33,7 +33,7 @@ class Exporter 
     | 
|
| 
       33 
33 
     | 
    
         
             
                      html '<h1>Boards included in this report</h1><ul>', type: :header
         
     | 
| 
       34 
34 
     | 
    
         
             
                      board_lines = []
         
     | 
| 
       35 
35 
     | 
    
         
             
                      included_projects.each do |project|
         
     | 
| 
       36 
     | 
    
         
            -
                        project.all_boards. 
     | 
| 
      
 36 
     | 
    
         
            +
                        project.all_boards.each_value do |board|
         
     | 
| 
       37 
37 
     | 
    
         
             
                          board_lines << "<a href='#{project.file_prefix}.html'>#{board.name}</a> from project #{project.name}"
         
     | 
| 
       38 
38 
     | 
    
         
             
                        end
         
     | 
| 
       39 
39 
     | 
    
         
             
                      end
         
     | 
| 
         @@ -7,7 +7,8 @@ 
     | 
|
| 
       7 
7 
     | 
    
         
             
            class Exporter
         
     | 
| 
       8 
8 
     | 
    
         
             
              def standard_project name:, file_prefix:, ignore_issues: nil, starting_status: nil, boards: {},
         
     | 
| 
       9 
9 
     | 
    
         
             
                  default_board: nil, anonymize: false, settings: {}, status_category_mappings: {},
         
     | 
| 
       10 
     | 
    
         
            -
                  rolling_date_count: 90, no_earlier_than: nil
         
     | 
| 
      
 10 
     | 
    
         
            +
                  rolling_date_count: 90, no_earlier_than: nil, ignore_types: %w[Sub-task Subtask Epic],
         
     | 
| 
      
 11 
     | 
    
         
            +
                  show_experimental_charts: false
         
     | 
| 
       11 
12 
     | 
    
         | 
| 
       12 
13 
     | 
    
         
             
                project name: name do
         
     | 
| 
       13 
14 
     | 
    
         
             
                  puts name
         
     | 
| 
         @@ -38,11 +39,14 @@ class Exporter 
     | 
|
| 
       38 
39 
     | 
    
         
             
                    end
         
     | 
| 
       39 
40 
     | 
    
         
             
                  end
         
     | 
| 
       40 
41 
     | 
    
         | 
| 
      
 42 
     | 
    
         
            +
                  issues.reject! do |issue|
         
     | 
| 
      
 43 
     | 
    
         
            +
                    ignore_types.include? issue.type
         
     | 
| 
      
 44 
     | 
    
         
            +
                  end
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                  discard_changes_before status_becomes: (starting_status || :backlog) # rubocop:disable Style/RedundantParentheses
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
       41 
48 
     | 
    
         
             
                  file do
         
     | 
| 
       42 
49 
     | 
    
         
             
                    file_suffix '.html'
         
     | 
| 
       43 
     | 
    
         
            -
                    issues.reject! do |issue|
         
     | 
| 
       44 
     | 
    
         
            -
                      %w[Sub-task Epic].include? issue.type
         
     | 
| 
       45 
     | 
    
         
            -
                    end
         
     | 
| 
       46 
50 
     | 
    
         | 
| 
       47 
51 
     | 
    
         
             
                    issues.reject! { |issue| ignore_issues.include? issue.key } if ignore_issues
         
     | 
| 
       48 
52 
     | 
    
         | 
| 
         @@ -52,12 +56,10 @@ class Exporter 
     | 
|
| 
       52 
56 
     | 
    
         
             
                      html "<H1>#{name}</H1>", type: :header
         
     | 
| 
       53 
57 
     | 
    
         
             
                      boards.each_key do |id|
         
     | 
| 
       54 
58 
     | 
    
         
             
                        board = find_board id
         
     | 
| 
       55 
     | 
    
         
            -
                        html "<div><a href='#{board.url}'>#{id} #{board.name}</a 
     | 
| 
      
 59 
     | 
    
         
            +
                        html "<div><a href='#{board.url}'>#{id} #{board.name}</a> (#{board.board_type})</div>",
         
     | 
| 
       56 
60 
     | 
    
         
             
                             type: :header
         
     | 
| 
       57 
61 
     | 
    
         
             
                      end
         
     | 
| 
       58 
62 
     | 
    
         | 
| 
       59 
     | 
    
         
            -
                      discard_changes_before status_becomes: (starting_status || :backlog) # rubocop:disable Style/RedundantParentheses
         
     | 
| 
       60 
     | 
    
         
            -
             
     | 
| 
       61 
63 
     | 
    
         
             
                      cycletime_scatterplot do
         
     | 
| 
       62 
64 
     | 
    
         
             
                        show_trend_lines
         
     | 
| 
       63 
65 
     | 
    
         
             
                      end
         
     | 
| 
         @@ -84,6 +86,7 @@ class Exporter 
     | 
|
| 
       84 
86 
     | 
    
         
             
                      daily_wip_by_age_chart
         
     | 
| 
       85 
87 
     | 
    
         
             
                      daily_wip_by_blocked_stalled_chart
         
     | 
| 
       86 
88 
     | 
    
         
             
                      daily_wip_by_parent_chart
         
     | 
| 
      
 89 
     | 
    
         
            +
                      flow_efficiency_scatterplot if show_experimental_charts
         
     | 
| 
       87 
90 
     | 
    
         
             
                      expedited_chart
         
     | 
| 
       88 
91 
     | 
    
         
             
                      sprint_burndown
         
     | 
| 
       89 
92 
     | 
    
         
             
                      estimate_accuracy_chart
         
     | 
| 
         @@ -109,8 +109,7 @@ class ExpeditedChart < ChartBase 
     | 
|
| 
       109 
109 
     | 
    
         | 
| 
       110 
110 
     | 
    
         
             
              def make_expedite_lines_data_set issue:, expedite_data:
         
     | 
| 
       111 
111 
     | 
    
         
             
                cycletime = issue.board.cycletime
         
     | 
| 
       112 
     | 
    
         
            -
                started_time = cycletime. 
     | 
| 
       113 
     | 
    
         
            -
                stopped_time = cycletime.stopped_time(issue)
         
     | 
| 
      
 112 
     | 
    
         
            +
                started_time, stopped_time = cycletime.started_stopped_times(issue)
         
     | 
| 
       114 
113 
     | 
    
         | 
| 
       115 
114 
     | 
    
         
             
                expedite_data << [started_time, :issue_started] if started_time
         
     | 
| 
       116 
115 
     | 
    
         
             
                expedite_data << [stopped_time, :issue_stopped] if stopped_time
         
     |