jirametrics 2.12.1 → 2.13pre2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7565c5b6320b9c4375a3feb27fcaf80f96d9c058041b71584f287e6a3d7eb399
4
- data.tar.gz: 4554c7edbc7c42e01ad4838094ab922819c2e71818a46907f25740e35c327aa6
3
+ metadata.gz: 86cc390c570ee4e3dcbcc8544ff9f78c02c648f65051c4765809728bc8f60dee
4
+ data.tar.gz: fc700a3285e4e0ed5aac26e7490aac787bdd777070dad27b3ece23b8bbfe7d4b
5
5
  SHA512:
6
- metadata.gz: 021c1f989117b1fbb8708183d9ad8e79b049060989c1f20d31121a486d25f1ae2d1c036e013279479cd3ac6f5b101a48d8c9f4943570107c9cc4b40485ea6a4f
7
- data.tar.gz: 7f7e334a2161e23ef5ae8a754aa3f12524c1bb130893ca3df3e8574f7a44286ef33c8a03b2ac2790ccb8e8b792ce0fb6c17b1d69d2da068ea48dbce1b4c514a1
6
+ metadata.gz: db561b3fb89c5e102cafb5a1becd55d198cd8ef81f433b18a7e4a967c618fdace5f7b960c3f29efe20f240dff673898e435c0656a92a8fda5974f7346bfc27ce
7
+ data.tar.gz: dbcd7a50423efc68a69f889bfcfeabe56e78820946c3d5d68b4b28bf434a71fd308c62dd70c14d06edee2e85ba7b5dfe9d48bf388811300cbf869fe7d6215105
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AtlassianDocumentFormat
4
+ attr_reader :users
5
+
6
+ def initialize users:
7
+ @users = users
8
+ end
9
+
10
+ def to_html input
11
+ if input.is_a? String
12
+ input
13
+ .gsub(/{color:(#\w{6})}([^{]+){color}/, '<span style="color: \1">\2</span>') # Colours
14
+ .gsub(/\[~accountid:([^\]]+)\]/) { expand_account_id $1 } # Tagged people
15
+ .gsub(/\[([^\|]+)\|(https?[^\]]+)\]/, '<a href="\2">\1</a>') # URLs
16
+ .gsub("\n", '<br />')
17
+ else
18
+ input['content'].collect { |element| adf_node_to_html element }.join("\n")
19
+ end
20
+ end
21
+
22
+ # ADF is Atlassian Document Format
23
+ # https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/
24
+ def adf_node_to_html node
25
+ closing_tag = nil
26
+ result = +''
27
+ case node['type']
28
+ when 'paragraph'
29
+ result << '<p>'
30
+ closing_tag = '</p>'
31
+ when 'text'
32
+ marks = adf_marks_to_html node['marks']
33
+ result << marks.collect(&:first).join
34
+ result << node['text']
35
+ result << marks.collect(&:last).join
36
+ when 'bulletList'
37
+ result << '<ul>'
38
+ closing_tag = '</ul>'
39
+ when 'orderedList'
40
+ result << '<ol>'
41
+ closing_tag = '</ol>'
42
+ when 'listItem'
43
+ result << '<li>'
44
+ closing_tag = '</li>'
45
+ when 'table'
46
+ result << '<table>'
47
+ closing_tag = '</table>'
48
+ when 'tableRow'
49
+ result << '<tr>'
50
+ closing_tag = '</tr>'
51
+ when 'tableCell'
52
+ result << '<td>'
53
+ closing_tag = '</td>'
54
+ when 'tableHeader'
55
+ result << '<th>'
56
+ closing_tag = '</th>'
57
+ when 'mention'
58
+ user = node['attrs']['text']
59
+ result << "<b>#{user}</b>"
60
+ when 'taskList'
61
+ result << "<ul class='taskList'>"
62
+ closing_tag = '</ul>'
63
+ when 'taskItem'
64
+ state = node['attrs']['state'] == 'TODO' ? '☐' : '☑'
65
+ result << "<li>#{state} "
66
+ closing_tag = '</li>'
67
+ when 'emoji'
68
+ result << node['attrs']['text']
69
+ else
70
+ result << "<p>Unparseable section: #{node['type']}</p>"
71
+ end
72
+
73
+ node['content']&.each do |child|
74
+ result << adf_node_to_html(child)
75
+ end
76
+
77
+ result << closing_tag if closing_tag
78
+ result
79
+ end
80
+
81
+ def adf_marks_to_html list
82
+ return [] if list.nil?
83
+
84
+ mappings = [
85
+ ['strong', '<b>', '</b>'],
86
+ ['code', '<code>', '</code>'],
87
+ ['em', '<em>', '</em>'],
88
+ ['strike', '<s>', '</s>'],
89
+ ['underline', '<u>', '</u>']
90
+ ]
91
+
92
+ list.filter_map do |mark|
93
+ type = mark['type']
94
+ if type == 'textColor'
95
+ color = mark['attrs']['color']
96
+ ["<span style='color: #{color}'>", '</span>']
97
+ elsif type == 'link'
98
+ href = mark['attrs']['href']
99
+ title = mark['attrs']['title']
100
+ ["<a href='#{href}' title='#{title}'>", '</a>']
101
+ else
102
+ line = mappings.find { |key, _open, _close| key == type }
103
+ [line[1], line[2]] if line
104
+ end
105
+ end
106
+ end
107
+
108
+ def expand_account_id account_id
109
+ user = @users.find { |u| u.account_id == account_id }
110
+ text = account_id
111
+ text = "@#{user.display_name}" if user
112
+ "<span class='account_id'>#{text}</span>"
113
+ end
114
+ end
@@ -30,6 +30,7 @@ class ChangeItem
30
30
  def artificial? = @artificial
31
31
  def assignee? = (field == 'assignee')
32
32
  def comment? = (field == 'comment')
33
+ def description? = (field == 'description')
33
34
  def due_date? = (field == 'duedate')
34
35
  def flagged? = (field == 'Flagged')
35
36
  def issue_type? = field == 'issuetype'
@@ -33,6 +33,10 @@ class DailyView < ChartBase
33
33
  result
34
34
  end
35
35
 
36
+ def atlassian_document_format
37
+ @atlassian_document_format ||= AtlassianDocumentFormat.new(users: users)
38
+ end
39
+
36
40
  def select_aging_issues
37
41
  aging_issues = issues.select do |issue|
38
42
  started_at, stopped_at = issue.board.cycletime.started_stopped_times(issue)
@@ -164,21 +168,6 @@ class DailyView < ChartBase
164
168
  lines
165
169
  end
166
170
 
167
- def jira_rich_text_to_html text
168
- text
169
- .gsub(/{color:(#\w{6})}([^{]+){color}/, '<span style="color: \1">\2</span>') # Colours
170
- .gsub(/\[~accountid:([^\]]+)\]/) { expand_account_id $1 } # Tagged people
171
- .gsub(/\[([^\|]+)\|(https?[^\]]+)\]/, '<a href="\2">\1</a>') # URLs
172
- .gsub("\n", '<br />')
173
- end
174
-
175
- def expand_account_id account_id
176
- user = @users.find { |u| u.account_id == account_id }
177
- text = account_id
178
- text = "@#{user.display_name}" if user
179
- "<span class='account_id'>#{text}</span>"
180
- end
181
-
182
171
  def make_history_lines issue
183
172
  history = issue.changes.reverse
184
173
  lines = []
@@ -207,8 +196,8 @@ class DailyView < ChartBase
207
196
  end
208
197
 
209
198
  def history_text change:, board:
210
- if change.comment?
211
- jira_rich_text_to_html(change.value)
199
+ if change.comment? || change.description?
200
+ atlassian_document_format.to_html(change.value)
212
201
  elsif change.status?
213
202
  convertor = ->(id) { format_status(board.possible_statuses.find_by_id(id), board: board) }
214
203
  to = convertor.call(change.value_id)
@@ -245,11 +234,19 @@ class DailyView < ChartBase
245
234
  .join(' ')]]
246
235
  end
247
236
 
237
+ def make_description_lines issue
238
+ description = issue.raw['fields']['description']
239
+ result = []
240
+ result << [atlassian_document_format.to_html(description)] if description
241
+ result
242
+ end
243
+
248
244
  def assemble_issue_lines issue, child:
249
245
  lines = []
250
246
  lines << [make_title_line(issue)]
251
247
  lines += make_parent_lines(issue) unless child
252
248
  lines += make_stats_lines(issue)
249
+ lines += make_description_lines(issue)
253
250
  lines += make_sprints_lines(issue)
254
251
  lines += make_blocked_stalled_lines(issue)
255
252
  lines += make_child_lines(issue)
@@ -97,30 +97,59 @@ class Downloader
97
97
  log " JQL: #{jql}"
98
98
  escaped_jql = CGI.escape jql
99
99
 
100
- max_results = 100
101
- start_at = 0
102
- total = 1
103
- while start_at < total
104
- json = @jira_gateway.call_url relative_url: '/rest/api/2/search' \
105
- "?jql=#{escaped_jql}&maxResults=#{max_results}&startAt=#{start_at}&expand=changelog&fields=*all"
106
-
107
- json['issues'].each do |issue_json|
108
- issue_json['exporter'] = {
109
- 'in_initial_query' => initial_query
110
- }
111
- identify_other_issues_to_be_downloaded raw_issue: issue_json, board: board
112
- file = "#{issue_json['key']}-#{board.id}.json"
113
-
114
- @file_system.save_json(json: issue_json, filename: File.join(path, file))
100
+ if @jira_gateway.cloud?
101
+ max_results = 5_000 # The maximum allowed by Jira
102
+ next_page_token = nil
103
+ issue_count = 0
104
+
105
+ loop do
106
+ json = @jira_gateway.call_url relative_url: '/rest/api/3/search/jql' \
107
+ "?jql=#{escaped_jql}&maxResults=#{max_results}&" \
108
+ "nextPageToken=#{next_page_token}&expand=changelog&fields=*all"
109
+ next_page_token = json['nextPageToken']
110
+
111
+ json['issues'].each do |issue_json|
112
+ issue_json['exporter'] = {
113
+ 'in_initial_query' => initial_query
114
+ }
115
+ identify_other_issues_to_be_downloaded raw_issue: issue_json, board: board
116
+ file = "#{issue_json['key']}-#{board.id}.json"
117
+
118
+ @file_system.save_json(json: issue_json, filename: File.join(path, file))
119
+ issue_count += 1
120
+ end
121
+
122
+ message = " Downloaded #{issue_count} issues"
123
+ log message, both: true
124
+
125
+ break unless next_page_token
126
+ end
127
+ else
128
+ max_results = 100
129
+ start_at = 0
130
+ total = 1
131
+ while start_at < total
132
+ json = @jira_gateway.call_url relative_url: '/rest/api/2/search' \
133
+ "?jql=#{escaped_jql}&maxResults=#{max_results}&startAt=#{start_at}&expand=changelog&fields=*all"
134
+
135
+ json['issues'].each do |issue_json|
136
+ issue_json['exporter'] = {
137
+ 'in_initial_query' => initial_query
138
+ }
139
+ identify_other_issues_to_be_downloaded raw_issue: issue_json, board: board
140
+ file = "#{issue_json['key']}-#{board.id}.json"
141
+
142
+ @file_system.save_json(json: issue_json, filename: File.join(path, file))
143
+ end
144
+
145
+ total = json['total'].to_i
146
+ max_results = json['maxResults']
147
+
148
+ message = " Downloaded #{start_at + 1}-#{[start_at + max_results, total].min} of #{total} issues to #{path} "
149
+ log message, both: true
150
+
151
+ start_at += json['issues'].size
115
152
  end
116
-
117
- total = json['total'].to_i
118
- max_results = json['maxResults']
119
-
120
- message = " Downloaded #{start_at + 1}-#{[start_at + max_results, total].min} of #{total} issues to #{path} "
121
- log message, both: true
122
-
123
- start_at += json['issues'].size
124
153
  end
125
154
  end
126
155
 
@@ -50,8 +50,8 @@
50
50
  </head>
51
51
  <body>
52
52
  <noscript>
53
- <div style="padding: 1em; background: gray; color: white; font-size: 2em;">
54
- Javascript is currently disabled and that means that almost all of the charts in this report won't render. If you'd loaded this from a folder on SharePoint then save it locally and load it again.
53
+ <div style="padding: 1em; background: red; color: white; font-size: 2em;">
54
+ Javascript is currently disabled and that means that almost all of the charts in this report won't render. If you've loaded this from a folder on SharePoint then save it locally and load it again.
55
55
  </div>
56
56
  </noscript>
57
57
  <%= "\n" + @sections.collect { |text, type| text if type == :header }.compact.join("\n\n") %>
@@ -77,4 +77,8 @@ class JiraGateway
77
77
 
78
78
  true
79
79
  end
80
+
81
+ def cloud?
82
+ @jira_url.downcase.end_with? '.atlassian.net'
83
+ end
80
84
  end
data/lib/jirametrics.rb CHANGED
@@ -117,6 +117,7 @@ class JiraMetrics < Thor
117
117
  require 'jirametrics/board'
118
118
  require 'jirametrics/daily_view'
119
119
  require 'jirametrics/user'
120
+ require 'jirametrics/atlassian_document_format'
120
121
  load config_file
121
122
  end
122
123
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jirametrics
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.12.1
4
+ version: 2.13pre2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Bowler
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-07-13 00:00:00.000000000 Z
10
+ date: 2025-07-18 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: random-word
@@ -65,6 +65,7 @@ files:
65
65
  - lib/jirametrics/aging_work_in_progress_chart.rb
66
66
  - lib/jirametrics/aging_work_table.rb
67
67
  - lib/jirametrics/anonymizer.rb
68
+ - lib/jirametrics/atlassian_document_format.rb
68
69
  - lib/jirametrics/blocked_stalled_change.rb
69
70
  - lib/jirametrics/board.rb
70
71
  - lib/jirametrics/board_column.rb