commenter 0.2.2 → 0.3.0
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/.rubocop.yml +4 -0
- data/.rubocop_todo.yml +7 -102
- data/CLAUDE.md +58 -0
- data/README.adoc +154 -14
- data/commenter.gemspec +3 -2
- data/data/github_config_sample.yaml +8 -0
- data/data/github_issue_body_template.liquid +1 -1
- data/data/github_issue_title_template.liquid +1 -1
- data/lib/commenter/cli.rb +20 -11
- data/lib/commenter/comment.rb +34 -2
- data/lib/commenter/comment_sheet.rb +10 -2
- data/lib/commenter/github_integration.rb +67 -21
- data/lib/commenter/parser/osd_xlsx_parser.rb +252 -0
- data/lib/commenter/parser.rb +95 -37
- data/lib/commenter/version.rb +1 -1
- data/schema/iso_comment_2012-03.yaml +2 -2
- data/schema/iso_comment_osd.yaml +112 -0
- data/spec/commenter/cli_spec.rb +35 -0
- data/spec/commenter/comment_spec.rb +2 -2
- data/spec/commenter/github_integration_spec.rb +3 -3
- data/spec/commenter/osd_xlsx_parser_spec.rb +180 -0
- data/spec/support/osd_fixtures.rb +94 -0
- data/spec/support/xlsx_builder.rb +147 -0
- metadata +26 -5
data/lib/commenter/comment.rb
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module Commenter
|
|
4
4
|
class Comment
|
|
5
|
-
attr_accessor :id, :body, :locality, :type, :comments, :proposed_change, :observations, :github
|
|
5
|
+
attr_accessor :id, :body, :locality, :type, :comments, :proposed_change, :observations, :github,
|
|
6
|
+
:user_name, :comment_type, :resolution_status, :resolution_date, :feedbacks,
|
|
7
|
+
:motivation, :created_date, :stage_code
|
|
6
8
|
|
|
7
9
|
def initialize(attributes = {})
|
|
8
10
|
# Normalize input to symbols
|
|
@@ -11,11 +13,33 @@ module Commenter
|
|
|
11
13
|
@id = attrs[:id]
|
|
12
14
|
@body = attrs[:body]
|
|
13
15
|
@locality = symbolize_keys(attrs[:locality] || {})
|
|
14
|
-
@type = attrs[:type]
|
|
16
|
+
@type = expand_comment_type(attrs[:type])
|
|
15
17
|
@comments = attrs[:comments]
|
|
16
18
|
@proposed_change = attrs[:proposed_change]
|
|
17
19
|
@observations = attrs[:observations]
|
|
18
20
|
@github = symbolize_keys(attrs[:github] || {})
|
|
21
|
+
|
|
22
|
+
# OSD-specific fields
|
|
23
|
+
@user_name = attrs[:user_name]
|
|
24
|
+
@comment_type = attrs[:comment_type]
|
|
25
|
+
@resolution_status = attrs[:resolution_status]
|
|
26
|
+
@resolution_date = attrs[:resolution_date]
|
|
27
|
+
@feedbacks = attrs[:feedbacks]
|
|
28
|
+
@motivation = attrs[:motivation]
|
|
29
|
+
@created_date = attrs[:created_date]
|
|
30
|
+
@stage_code = attrs[:stage_code]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Expands short type codes to full names. One-way by design: already
|
|
34
|
+
# expanded values pass through unchanged so that reloading YAML output
|
|
35
|
+
# stays stable.
|
|
36
|
+
def expand_comment_type(type)
|
|
37
|
+
case type&.downcase
|
|
38
|
+
when "ge" then "general"
|
|
39
|
+
when "te" then "technical"
|
|
40
|
+
when "ed" then "editorial"
|
|
41
|
+
else type
|
|
42
|
+
end
|
|
19
43
|
end
|
|
20
44
|
|
|
21
45
|
def line_number
|
|
@@ -108,6 +132,14 @@ module Commenter
|
|
|
108
132
|
comments: @comments,
|
|
109
133
|
proposed_change: @proposed_change,
|
|
110
134
|
observations: @observations,
|
|
135
|
+
user_name: @user_name,
|
|
136
|
+
comment_type: @comment_type,
|
|
137
|
+
resolution_status: @resolution_status,
|
|
138
|
+
resolution_date: @resolution_date,
|
|
139
|
+
feedbacks: @feedbacks,
|
|
140
|
+
motivation: @motivation,
|
|
141
|
+
created_date: @created_date,
|
|
142
|
+
stage_code: @stage_code,
|
|
111
143
|
github: @github.empty? ? nil : @github
|
|
112
144
|
}.compact
|
|
113
145
|
end
|
|
@@ -4,7 +4,7 @@ require_relative "comment"
|
|
|
4
4
|
|
|
5
5
|
module Commenter
|
|
6
6
|
class CommentSheet
|
|
7
|
-
attr_accessor :version, :date, :document, :project, :stage, :comments
|
|
7
|
+
attr_accessor :version, :date, :document, :project, :stage, :comments, :title_en, :title_fr
|
|
8
8
|
|
|
9
9
|
def initialize(attributes = {})
|
|
10
10
|
# Normalize input to symbols
|
|
@@ -15,6 +15,8 @@ module Commenter
|
|
|
15
15
|
@document = attrs[:document]
|
|
16
16
|
@project = attrs[:project]
|
|
17
17
|
@stage = attrs[:stage]
|
|
18
|
+
@title_en = attrs[:title_en]
|
|
19
|
+
@title_fr = attrs[:title_fr]
|
|
18
20
|
@comments = (attrs[:comments] || []).map { |c| c.is_a?(Comment) ? c : Comment.from_hash(c) }
|
|
19
21
|
end
|
|
20
22
|
|
|
@@ -29,12 +31,18 @@ module Commenter
|
|
|
29
31
|
document: @document,
|
|
30
32
|
project: @project,
|
|
31
33
|
stage: @stage,
|
|
34
|
+
title_en: @title_en,
|
|
35
|
+
title_fr: @title_fr,
|
|
32
36
|
comments: @comments.map(&:to_h)
|
|
33
37
|
}
|
|
34
38
|
end
|
|
35
39
|
|
|
36
40
|
def to_yaml_h
|
|
37
|
-
|
|
41
|
+
hash = to_h.merge(comments: @comments.map(&:to_yaml_h))
|
|
42
|
+
# Remove nil-valued keys for cleaner YAML output
|
|
43
|
+
hash.delete(:title_en) if hash[:title_en].nil?
|
|
44
|
+
hash.delete(:title_fr) if hash[:title_fr].nil?
|
|
45
|
+
stringify_keys(hash)
|
|
38
46
|
end
|
|
39
47
|
|
|
40
48
|
def self.from_hash(hash)
|
|
@@ -16,6 +16,7 @@ module Commenter
|
|
|
16
16
|
|
|
17
17
|
@title_template = load_liquid_template(title_template_path || default_title_template_path)
|
|
18
18
|
@body_template = load_liquid_template(body_template_path || default_body_template_path)
|
|
19
|
+
@unique_id_template = load_unique_id_template
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
def create_issues_from_yaml(yaml_file, options = {})
|
|
@@ -70,7 +71,26 @@ module Commenter
|
|
|
70
71
|
raise "Template file not found: #{template_path}"
|
|
71
72
|
end
|
|
72
73
|
|
|
74
|
+
def load_unique_id_template
|
|
75
|
+
unique_id_config = @config.dig("github", "templates", "unique_id")
|
|
76
|
+
|
|
77
|
+
if unique_id_config
|
|
78
|
+
# Check if it's a file path or inline template
|
|
79
|
+
if File.exist?(unique_id_config)
|
|
80
|
+
load_liquid_template(unique_id_config)
|
|
81
|
+
else
|
|
82
|
+
Liquid::Template.parse(unique_id_config)
|
|
83
|
+
end
|
|
84
|
+
else
|
|
85
|
+
# Default unique_id pattern: "[STAGE] COMMENT_ID"
|
|
86
|
+
Liquid::Template.parse("[{{ stage | upcase }}] {{ comment_id }}")
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
73
90
|
def template_variables(comment, comment_sheet)
|
|
91
|
+
# Render unique_id first so it can be used in other templates
|
|
92
|
+
unique_id = render_unique_id(comment, comment_sheet)
|
|
93
|
+
|
|
74
94
|
{
|
|
75
95
|
# Comment sheet variables
|
|
76
96
|
"stage" => comment_sheet.stage || "",
|
|
@@ -95,17 +115,30 @@ module Commenter
|
|
|
95
115
|
"line_number" => comment.line_number || "",
|
|
96
116
|
|
|
97
117
|
# Computed variables
|
|
118
|
+
"unique_id" => unique_id,
|
|
98
119
|
"has_observations" => !comment.observations.nil? && !comment.observations.strip.empty?,
|
|
99
120
|
"has_proposed_change" => !comment.proposed_change.nil? && !comment.proposed_change.strip.empty?,
|
|
100
121
|
"locality_summary" => format_locality(comment)
|
|
101
122
|
}
|
|
102
123
|
end
|
|
103
124
|
|
|
125
|
+
def render_unique_id(comment, comment_sheet)
|
|
126
|
+
base_variables = {
|
|
127
|
+
"stage" => comment_sheet.stage || "",
|
|
128
|
+
"document" => comment_sheet.document || "",
|
|
129
|
+
"project" => comment_sheet.project || "",
|
|
130
|
+
"comment_id" => comment.id || "",
|
|
131
|
+
"body" => comment.body || "",
|
|
132
|
+
"type" => comment.type || ""
|
|
133
|
+
}
|
|
134
|
+
@unique_id_template.render(base_variables).strip
|
|
135
|
+
end
|
|
136
|
+
|
|
104
137
|
def expand_comment_type(type)
|
|
105
138
|
case type&.downcase
|
|
106
|
-
when "ge" then "General"
|
|
107
|
-
when "te" then "Technical"
|
|
108
|
-
when "ed" then "Editorial"
|
|
139
|
+
when "ge", "general" then "General"
|
|
140
|
+
when "te", "technical" then "Technical"
|
|
141
|
+
when "ed", "editorial" then "Editorial"
|
|
109
142
|
else type || "Unknown"
|
|
110
143
|
end
|
|
111
144
|
end
|
|
@@ -119,9 +152,11 @@ module Commenter
|
|
|
119
152
|
end
|
|
120
153
|
|
|
121
154
|
def create_issue(comment, comment_sheet, options = {})
|
|
122
|
-
|
|
123
|
-
|
|
155
|
+
puts "[GitHubIssueCreator] Creating issue for comment ID: #{comment.id}"
|
|
156
|
+
# Check if issue already exists (stage-aware)
|
|
157
|
+
existing_issue = find_existing_issue(comment, comment_sheet)
|
|
124
158
|
if existing_issue
|
|
159
|
+
puts "[GitHubIssueCreator] Issue already exists for comment ID: #{comment.id} at stage #{comment_sheet.stage}, skipping creation."
|
|
125
160
|
return {
|
|
126
161
|
comment_id: comment.id,
|
|
127
162
|
status: :skipped,
|
|
@@ -139,8 +174,10 @@ module Commenter
|
|
|
139
174
|
milestone: determine_milestone(comment, comment_sheet, options)
|
|
140
175
|
}.compact
|
|
141
176
|
|
|
177
|
+
puts "[GitHubIssueCreator] Creating issue with title: #{title}"
|
|
142
178
|
begin
|
|
143
179
|
issue = @github_client.create_issue(@repo, title, body, issue_options)
|
|
180
|
+
puts "[GitHubIssueCreator] Issue created successfully: #{issue.html_url}"
|
|
144
181
|
{
|
|
145
182
|
comment_id: comment.id,
|
|
146
183
|
status: :created,
|
|
@@ -148,6 +185,7 @@ module Commenter
|
|
|
148
185
|
issue_url: issue.html_url
|
|
149
186
|
}
|
|
150
187
|
rescue Octokit::Error => e
|
|
188
|
+
puts "[GitHubIssueCreator] Error creating issue for comment ID: #{comment.id} - #{e.message}"
|
|
151
189
|
{
|
|
152
190
|
comment_id: comment.id,
|
|
153
191
|
status: :error,
|
|
@@ -157,6 +195,8 @@ module Commenter
|
|
|
157
195
|
end
|
|
158
196
|
|
|
159
197
|
def preview_issue(comment, comment_sheet)
|
|
198
|
+
puts "[GitHubIssueCreator] Previewing issue for comment ID: #{comment.id}"
|
|
199
|
+
|
|
160
200
|
title = @title_template.render(template_variables(comment, comment_sheet))
|
|
161
201
|
body = @body_template.render(template_variables(comment, comment_sheet))
|
|
162
202
|
|
|
@@ -170,9 +210,13 @@ module Commenter
|
|
|
170
210
|
}
|
|
171
211
|
end
|
|
172
212
|
|
|
173
|
-
def find_existing_issue(comment)
|
|
174
|
-
|
|
175
|
-
|
|
213
|
+
def find_existing_issue(comment, comment_sheet)
|
|
214
|
+
unique_id = render_unique_id(comment, comment_sheet)
|
|
215
|
+
puts "[GitHubIssueCreator] Searching for existing issue with unique_id: #{unique_id}"
|
|
216
|
+
|
|
217
|
+
# Search for existing issues with the unique_id in the title
|
|
218
|
+
# The unique_id is configurable and defaults to "[STAGE] COMMENT_ID"
|
|
219
|
+
query = "repo:#{@repo} in:title \"#{unique_id}\""
|
|
176
220
|
results = @github_client.search_issues(query)
|
|
177
221
|
results.items.first
|
|
178
222
|
rescue Octokit::Error
|
|
@@ -212,26 +256,25 @@ module Commenter
|
|
|
212
256
|
assignees.compact.uniq
|
|
213
257
|
end
|
|
214
258
|
|
|
215
|
-
def determine_milestone(_comment,
|
|
216
|
-
# Check for
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
#
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
return milestone_number if milestone_number
|
|
225
|
-
end
|
|
226
|
-
end
|
|
259
|
+
def determine_milestone(_comment, _comment_sheet, _options)
|
|
260
|
+
# # Check for stage-specific milestone
|
|
261
|
+
# if comment_sheet.stage
|
|
262
|
+
# stage_milestone = @config.dig("github", "stage_milestones", comment_sheet.stage)
|
|
263
|
+
# if stage_milestone
|
|
264
|
+
# milestone_number = resolve_milestone_by_name_or_number(stage_milestone)
|
|
265
|
+
# return milestone_number if milestone_number
|
|
266
|
+
# end
|
|
267
|
+
# end
|
|
227
268
|
|
|
228
269
|
# Use configured milestone
|
|
229
270
|
milestone_config = @config.dig("github", "milestone")
|
|
230
271
|
return nil unless milestone_config
|
|
231
272
|
|
|
232
273
|
if milestone_config["number"]
|
|
274
|
+
puts "[GitHubIssueCreator] Using milestone number: #{milestone_config["number"]}"
|
|
233
275
|
milestone_config["number"]
|
|
234
276
|
elsif milestone_config["name"]
|
|
277
|
+
puts "[GitHubIssueCreator] Using milestone name: #{milestone_config["name"]}"
|
|
235
278
|
resolve_milestone_by_name_or_number(milestone_config["name"])
|
|
236
279
|
end
|
|
237
280
|
end
|
|
@@ -246,6 +289,8 @@ module Commenter
|
|
|
246
289
|
|
|
247
290
|
def find_milestone_by_name(name)
|
|
248
291
|
milestones = @github_client.milestones(@repo, state: "all")
|
|
292
|
+
|
|
293
|
+
puts "[GitHubIssueCreator] Found #{milestones.size} milestones in repository #{@repo}" if milestones.any?
|
|
249
294
|
milestone = milestones.find { |m| m.title == name }
|
|
250
295
|
milestone&.number
|
|
251
296
|
rescue Octokit::Error
|
|
@@ -411,7 +456,8 @@ module Commenter
|
|
|
411
456
|
end
|
|
412
457
|
|
|
413
458
|
# Fallback to last comment if configured and no magic comment found
|
|
414
|
-
return comments.last.body.strip if @config.dig("github", "retrieval",
|
|
459
|
+
return comments.last.body.strip if @config.dig("github", "retrieval",
|
|
460
|
+
"fallback_to_last_comment") && !comments.empty?
|
|
415
461
|
|
|
416
462
|
nil
|
|
417
463
|
rescue Octokit::Error
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "roo"
|
|
4
|
+
require_relative "../comment_sheet"
|
|
5
|
+
require_relative "../comment"
|
|
6
|
+
|
|
7
|
+
module Commenter
|
|
8
|
+
class Parser
|
|
9
|
+
# Parser for ISO Online Standards Development (OSD) XLSX exports.
|
|
10
|
+
#
|
|
11
|
+
# Supports two variants, detected from the header row:
|
|
12
|
+
# - "resolved": single "Comments (N)" sheet with resolution data
|
|
13
|
+
# (columns: Comment ID, User name, Clause nb, ..., Resolution status,
|
|
14
|
+
# Motivation, Resolution Date, Stage code)
|
|
15
|
+
# - "unresolved": multi-sheet format with "Comments (N)",
|
|
16
|
+
# "Unresolved comments (N)", "Resolved comments (N)" sheets
|
|
17
|
+
# (columns: User name, Clause nb, ..., Comment type, Comment/Motivation,
|
|
18
|
+
# Comment on text, Proposal on Text, Proposed change, Replies,
|
|
19
|
+
# Resolution status, Justification, Resolution Date, Date, Comment number)
|
|
20
|
+
#
|
|
21
|
+
# Both variants share the same header structure in the first rows:
|
|
22
|
+
# Row 0: [Date, Reference, nil, "", Title EN, nil, nil, Title FR, ...]
|
|
23
|
+
# Row 1: ["2026-04-21", "ISO/DIS 5843-6(en)", ...]
|
|
24
|
+
# Row 2: empty
|
|
25
|
+
# Row 3: column headers
|
|
26
|
+
# Row 4+: data
|
|
27
|
+
class OsdXlsxParser
|
|
28
|
+
# Header names are mapped per variant onto common attribute keys so both
|
|
29
|
+
# variants share a single comment builder. Headers are mapped by real
|
|
30
|
+
# column position, tolerating gaps in the header row.
|
|
31
|
+
RESOLVED_COLUMN_KEYS = {
|
|
32
|
+
"Comment ID" => :id,
|
|
33
|
+
"User name" => :user_name,
|
|
34
|
+
"Clause nb" => :clause,
|
|
35
|
+
"Clause Title" => :clause_title,
|
|
36
|
+
"Subtype" => :comment_type,
|
|
37
|
+
"Comment" => :comment_text,
|
|
38
|
+
"Proposal on Text" => :proposal_on_text,
|
|
39
|
+
"Proposed change" => :proposed_change,
|
|
40
|
+
"Feedbacks" => :feedbacks,
|
|
41
|
+
"Created Date" => :created_date,
|
|
42
|
+
"Resolution status" => :resolution_status,
|
|
43
|
+
"Motivation" => :motivation,
|
|
44
|
+
"Resolution Date" => :resolution_date,
|
|
45
|
+
"Stage code" => :stage_code
|
|
46
|
+
}.freeze
|
|
47
|
+
|
|
48
|
+
UNRESOLVED_COLUMN_KEYS = {
|
|
49
|
+
"User name" => :user_name,
|
|
50
|
+
"Clause nb" => :clause,
|
|
51
|
+
"Clause Title" => :clause_title,
|
|
52
|
+
"Comment type" => :comment_type,
|
|
53
|
+
"Comment/Motivation" => :comment_text,
|
|
54
|
+
"Proposal on Text" => :proposal_on_text,
|
|
55
|
+
"Proposed change" => :proposed_change,
|
|
56
|
+
"Replies" => :feedbacks,
|
|
57
|
+
"Resolution status" => :resolution_status,
|
|
58
|
+
"Justification" => :motivation,
|
|
59
|
+
"Resolution Date" => :resolution_date,
|
|
60
|
+
"Date" => :created_date,
|
|
61
|
+
"Comment number" => :id
|
|
62
|
+
}.freeze
|
|
63
|
+
|
|
64
|
+
def parse(xlsx_path, options = {})
|
|
65
|
+
xlsx = Roo::Spreadsheet.open(xlsx_path)
|
|
66
|
+
|
|
67
|
+
sheet_name = select_sheet(xlsx, options)
|
|
68
|
+
sheet = xlsx.sheet(sheet_name)
|
|
69
|
+
header_row_num = find_header_row(sheet)
|
|
70
|
+
variant = detect_variant(sheet.row(header_row_num))
|
|
71
|
+
|
|
72
|
+
comments = parse_comments(sheet, header_row_num, variant, options)
|
|
73
|
+
CommentSheet.new(version: "osd", **extract_metadata(xlsx, sheet_name), comments: comments)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def select_sheet(xlsx, options)
|
|
79
|
+
sheets = xlsx.sheets
|
|
80
|
+
|
|
81
|
+
if options[:sheet]
|
|
82
|
+
raise "Sheet '#{options[:sheet]}' not found. Available: #{sheets.join(", ")}" unless sheets.include?(options[:sheet])
|
|
83
|
+
|
|
84
|
+
return options[:sheet]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
if options[:resolved_only]
|
|
88
|
+
sheets.find { |s| s.start_with?("Resolved") } || sheets.find { |s| s.start_with?("Comments") }
|
|
89
|
+
elsif options[:unresolved_only]
|
|
90
|
+
sheets.find { |s| s.start_with?("Unresolved") } || sheets.find { |s| s.start_with?("Comments") }
|
|
91
|
+
else
|
|
92
|
+
sheets.first # Default: first sheet (usually "Comments (N)" with all comments)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def find_header_row(sheet)
|
|
97
|
+
(1..10).each do |row_num|
|
|
98
|
+
headers = sheet.row(row_num).to_a.compact.map(&:to_s)
|
|
99
|
+
return row_num if headers.include?("User name") || headers.include?("Comment ID")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
raise "Could not find header row in XLSX sheet"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def detect_variant(header_row)
|
|
106
|
+
headers = header_row.to_a.compact.map(&:to_s)
|
|
107
|
+
if headers.include?("Comment ID") || headers.include?("Subtype")
|
|
108
|
+
:resolved
|
|
109
|
+
elsif headers.include?("Comment/Motivation") || headers.include?("Comment type")
|
|
110
|
+
:unresolved
|
|
111
|
+
else
|
|
112
|
+
headers.length >= 15 ? :resolved : :unresolved
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def extract_metadata(xlsx, sheet_name)
|
|
117
|
+
values = xlsx.sheet(sheet_name).row(2)
|
|
118
|
+
metadata = { date: nil, document: nil, project: nil, stage: nil, title_en: nil, title_fr: nil }
|
|
119
|
+
return metadata if values.nil?
|
|
120
|
+
|
|
121
|
+
metadata[:date] = header_value(values[0], "Date")
|
|
122
|
+
|
|
123
|
+
reference = header_value(values[1], "Reference")
|
|
124
|
+
if reference
|
|
125
|
+
metadata[:document] = reference
|
|
126
|
+
metadata[:stage] = extract_stage(reference)
|
|
127
|
+
metadata[:project] = extract_project(reference)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
metadata[:title_en] = header_value(values[4], "Title EN")
|
|
131
|
+
metadata[:title_fr] = header_value(values[7], "Title FR")
|
|
132
|
+
metadata
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def header_value(raw, label)
|
|
136
|
+
return nil if raw.nil?
|
|
137
|
+
|
|
138
|
+
value = raw.is_a?(Date) ? raw.strftime("%Y-%m-%d") : raw.to_s.strip
|
|
139
|
+
value.empty? || value == label ? nil : value
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def extract_stage(reference)
|
|
143
|
+
case reference
|
|
144
|
+
when %r{/WD\b}i then "WD"
|
|
145
|
+
when %r{/CD\b}i then "CD"
|
|
146
|
+
when %r{/DIS\b}i then "DIS"
|
|
147
|
+
when %r{/FDIS\b}i then "FDIS"
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def extract_project(reference)
|
|
152
|
+
# Extract ISO number from reference like "ISO/DIS 5843-6(en)"
|
|
153
|
+
match = reference.match(%r{(ISO[\s/]*\w*\s*\d+[\d-]*)})
|
|
154
|
+
match ? match[1].strip.gsub(/\s+/, " ") : nil
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def parse_comments(sheet, header_row_num, variant, options)
|
|
158
|
+
column_keys = variant == :resolved ? RESOLVED_COLUMN_KEYS : UNRESOLVED_COLUMN_KEYS
|
|
159
|
+
col_map = build_column_map(sheet.row(header_row_num), column_keys)
|
|
160
|
+
|
|
161
|
+
comments = []
|
|
162
|
+
(header_row_num + 1..sheet.last_row).each do |row_num|
|
|
163
|
+
row = sheet.row(row_num)
|
|
164
|
+
next if row.nil? || row.to_a.compact.empty?
|
|
165
|
+
|
|
166
|
+
comment = build_comment(row, col_map, options)
|
|
167
|
+
comments << comment if comment
|
|
168
|
+
end
|
|
169
|
+
comments
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def build_column_map(header_row, column_keys)
|
|
173
|
+
header_row.to_a.each_with_index.with_object({}) do |(header, index), map|
|
|
174
|
+
key = column_keys[header.to_s.strip]
|
|
175
|
+
map[key] = index if key
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def build_comment(row, col_map, options)
|
|
180
|
+
id = cell_value(row, col_map[:id])
|
|
181
|
+
return nil if id.nil?
|
|
182
|
+
|
|
183
|
+
id_str = id.is_a?(Float) ? id.to_i.to_s : id.to_s
|
|
184
|
+
comment_text = cell_value(row, col_map[:comment_text]) || ""
|
|
185
|
+
return nil if comment_text.strip.empty? && id_str.empty?
|
|
186
|
+
|
|
187
|
+
attrs = base_attributes(row, col_map, id_str, comment_text)
|
|
188
|
+
.merge(resolution_attributes(row, col_map))
|
|
189
|
+
attrs[:observations] = resolution_observations(row, col_map) unless options[:exclude_observations]
|
|
190
|
+
Comment.new(attrs)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def base_attributes(row, col_map, id_str, comment_text)
|
|
194
|
+
user_name = cell_value_str(row, col_map[:user_name]).to_s
|
|
195
|
+
clause = cell_value_str(row, col_map[:clause]).to_s
|
|
196
|
+
clause_title = cell_value_str(row, col_map[:clause_title]).to_s
|
|
197
|
+
{
|
|
198
|
+
id: id_str,
|
|
199
|
+
body: user_name,
|
|
200
|
+
locality: {
|
|
201
|
+
clause: clause.empty? ? nil : clause,
|
|
202
|
+
element: clause_title.empty? ? nil : clause_title
|
|
203
|
+
}.compact,
|
|
204
|
+
type: normalize_comment_type(cell_value_str(row, col_map[:comment_type]).to_s),
|
|
205
|
+
comments: comment_text.to_s.strip,
|
|
206
|
+
proposed_change: cell_value_str(row, col_map[:proposed_change]) ||
|
|
207
|
+
cell_value_str(row, col_map[:proposal_on_text])
|
|
208
|
+
}
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def resolution_attributes(row, col_map)
|
|
212
|
+
{
|
|
213
|
+
user_name: cell_value_str(row, col_map[:user_name]).to_s,
|
|
214
|
+
comment_type: cell_value_str(row, col_map[:comment_type]).to_s,
|
|
215
|
+
resolution_status: cell_value_str(row, col_map[:resolution_status]),
|
|
216
|
+
resolution_date: cell_value_str(row, col_map[:resolution_date]),
|
|
217
|
+
feedbacks: cell_value_str(row, col_map[:feedbacks]),
|
|
218
|
+
motivation: cell_value_str(row, col_map[:motivation]),
|
|
219
|
+
created_date: cell_value_str(row, col_map[:created_date]),
|
|
220
|
+
stage_code: cell_value_str(row, col_map[:stage_code])
|
|
221
|
+
}
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def resolution_observations(row, col_map)
|
|
225
|
+
parts = [cell_value_str(row, col_map[:resolution_status]),
|
|
226
|
+
cell_value_str(row, col_map[:motivation])].compact
|
|
227
|
+
parts.empty? ? nil : parts.join(". ")
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def normalize_comment_type(type_str)
|
|
231
|
+
case type_str.strip.downcase
|
|
232
|
+
when "editorial", "ed" then "ed"
|
|
233
|
+
when "technical", "te" then "te"
|
|
234
|
+
when "general", "ge" then "ge"
|
|
235
|
+
else type_str.strip
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def cell_value(row, index)
|
|
240
|
+
return nil if index.nil? || row.length <= index
|
|
241
|
+
|
|
242
|
+
value = row[index]
|
|
243
|
+
value.is_a?(String) && value.strip.empty? ? nil : value
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def cell_value_str(row, index)
|
|
247
|
+
value = cell_value(row, index)
|
|
248
|
+
value.nil? || value.to_s.strip.empty? ? nil : value.to_s.strip
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|