commenter 0.3.1 → 0.4.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/CONTEXT.md +27 -0
- data/README.adoc +57 -11
- data/commenter.gemspec +1 -0
- data/lib/commenter/ballot.rb +67 -0
- data/lib/commenter/ballot_report.rb +60 -0
- data/lib/commenter/cli.rb +58 -31
- data/lib/commenter/comment.rb +100 -115
- data/lib/commenter/comment_locality.rb +18 -0
- data/lib/commenter/comment_sheet.rb +28 -68
- data/lib/commenter/comment_type.rb +5 -1
- data/lib/commenter/disposition_status.rb +37 -0
- data/lib/commenter/filler.rb +91 -59
- data/lib/commenter/github_info.rb +23 -0
- data/lib/commenter/github_integration.rb +6 -11
- data/lib/commenter/parser/osd_xlsx_parser.rb +1 -3
- data/lib/commenter/parser/track_change_docx_parser.rb +3 -5
- data/lib/commenter/parser.rb +4 -5
- data/lib/commenter/version.rb +1 -1
- data/lib/commenter.rb +11 -5
- data/spec/commenter/ballot_report_spec.rb +62 -0
- data/spec/commenter/ballot_spec.rb +68 -0
- data/spec/commenter/cli_spec.rb +41 -0
- data/spec/commenter/comment_sheet_spec.rb +10 -10
- data/spec/commenter/comment_spec.rb +22 -9
- data/spec/commenter/disposition_status_spec.rb +40 -0
- data/spec/commenter/filler_spec.rb +119 -0
- data/spec/commenter/osd_xlsx_parser_spec.rb +3 -2
- data/spec/support/redline_docx_builder.rb +11 -4
- data/spec/support/xlsx_builder.rb +17 -12
- metadata +26 -2
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module Commenter
|
|
6
|
+
# Location of a comment within the reviewed document.
|
|
7
|
+
class CommentLocality < Lutaml::Model::Serializable
|
|
8
|
+
attribute :line_number, :string
|
|
9
|
+
attribute :clause, :string
|
|
10
|
+
attribute :element, :string
|
|
11
|
+
|
|
12
|
+
yaml do
|
|
13
|
+
map "line_number", to: :line_number
|
|
14
|
+
map "clause", to: :clause
|
|
15
|
+
map "element", to: :element
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -1,48 +1,33 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require "lutaml/model"
|
|
4
4
|
|
|
5
5
|
module Commenter
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
6
|
+
# One ballot submission: metadata plus its comments. Serialization is
|
|
7
|
+
# declared with lutaml-model.
|
|
8
|
+
class CommentSheet < Lutaml::Model::Serializable
|
|
9
|
+
attribute :version, :string, default: -> { "2012-03" }
|
|
10
|
+
attribute :date, :string
|
|
11
|
+
attribute :document, :string
|
|
12
|
+
attribute :project, :string
|
|
13
|
+
attribute :stage, :string
|
|
14
|
+
attribute :title_en, :string
|
|
15
|
+
attribute :title_fr, :string
|
|
16
|
+
attribute :comments, Comment, collection: true
|
|
17
|
+
|
|
18
|
+
yaml do
|
|
19
|
+
map "version", to: :version
|
|
20
|
+
map "date", to: :date
|
|
21
|
+
map "document", to: :document
|
|
22
|
+
map "project", to: :project
|
|
23
|
+
map "stage", to: :stage
|
|
24
|
+
map "title_en", to: :title_en
|
|
25
|
+
map "title_fr", to: :title_fr
|
|
26
|
+
map "comments", to: :comments
|
|
21
27
|
end
|
|
22
28
|
|
|
23
29
|
def add_comment(comment)
|
|
24
|
-
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def to_h
|
|
28
|
-
{
|
|
29
|
-
version: @version,
|
|
30
|
-
date: @date,
|
|
31
|
-
document: @document,
|
|
32
|
-
project: @project,
|
|
33
|
-
stage: @stage,
|
|
34
|
-
title_en: @title_en,
|
|
35
|
-
title_fr: @title_fr,
|
|
36
|
-
comments: @comments.map(&:to_h)
|
|
37
|
-
}
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def to_yaml_h
|
|
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)
|
|
30
|
+
self.comments = (comments || []) + [comment.is_a?(Comment) ? comment : Comment.from_hash(comment)]
|
|
46
31
|
end
|
|
47
32
|
|
|
48
33
|
def schema_name
|
|
@@ -50,40 +35,15 @@ module Commenter
|
|
|
50
35
|
end
|
|
51
36
|
|
|
52
37
|
def to_yaml_document(schema_dir = "schema")
|
|
53
|
-
"# yaml-language-server: $schema=#{File.join(schema_dir.to_s, schema_name)}\n\n#{
|
|
38
|
+
"# yaml-language-server: $schema=#{File.join(schema_dir.to_s, schema_name)}\n\n#{to_yaml}"
|
|
54
39
|
end
|
|
55
40
|
|
|
56
|
-
def
|
|
57
|
-
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
private
|
|
61
|
-
|
|
62
|
-
def symbolize_keys(hash)
|
|
63
|
-
return hash unless hash.is_a?(Hash)
|
|
64
|
-
|
|
65
|
-
hash.each_with_object({}) do |(key, value), result|
|
|
66
|
-
new_key = key.to_sym
|
|
67
|
-
new_value = value.is_a?(Hash) ? symbolize_keys(value) : value
|
|
68
|
-
result[new_key] = new_value
|
|
69
|
-
end
|
|
41
|
+
def to_yaml_h
|
|
42
|
+
CommentSheet.to_hash(self)
|
|
70
43
|
end
|
|
71
44
|
|
|
72
|
-
def
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
hash.each_with_object({}) do |(key, value), result|
|
|
76
|
-
new_key = key.to_s
|
|
77
|
-
new_value = case value
|
|
78
|
-
when Hash
|
|
79
|
-
stringify_keys(value)
|
|
80
|
-
when Array
|
|
81
|
-
value.map { |item| item.is_a?(Hash) ? stringify_keys(item) : item }
|
|
82
|
-
else
|
|
83
|
-
value
|
|
84
|
-
end
|
|
85
|
-
result[new_key] = new_value
|
|
86
|
-
end
|
|
45
|
+
def to_h
|
|
46
|
+
CommentSheet.to_hash(self)
|
|
87
47
|
end
|
|
88
48
|
end
|
|
89
49
|
end
|
|
@@ -25,7 +25,11 @@ module Commenter
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def full_name(type)
|
|
28
|
-
|
|
28
|
+
return nil if type.nil?
|
|
29
|
+
|
|
30
|
+
value = type.to_s.strip
|
|
31
|
+
downcased = value.downcase
|
|
32
|
+
FULL_NAMES[downcased] || (FULL_NAMES.values.include?(downcased) ? downcased : value)
|
|
29
33
|
end
|
|
30
34
|
|
|
31
35
|
def display_name(type)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Commenter
|
|
4
|
+
# Canonical disposition vocabulary, shared by cell shading (Filler) and
|
|
5
|
+
# ballot statistics (BallotReport). Matches free-text observations and
|
|
6
|
+
# OSD's structured resolution_status values. One home for the question
|
|
7
|
+
# "what counts as an accepted disposition".
|
|
8
|
+
module DispositionStatus
|
|
9
|
+
PATTERNS = [
|
|
10
|
+
[:accept_with_modifications, /awm|accept(ed)?[ ,]*with +(modifications|changes)/].freeze,
|
|
11
|
+
[:rejected, /reject(ed)?|not accepted/].freeze,
|
|
12
|
+
[:accepted, /accept(ed)?/].freeze,
|
|
13
|
+
[:noted, /noted/].freeze,
|
|
14
|
+
[:todo, /todo/].freeze
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# Returns the canonical status symbol for the given text, or nil when
|
|
20
|
+
# nothing matches. Order matters: "accept with modifications" and
|
|
21
|
+
# "not accepted" must be classified before the bare "accept" pattern.
|
|
22
|
+
def match(text)
|
|
23
|
+
value = text.to_s.downcase.strip
|
|
24
|
+
return nil if value.empty?
|
|
25
|
+
|
|
26
|
+
PATTERNS.each do |status, pattern|
|
|
27
|
+
return status if value.match?(pattern)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def statuses
|
|
34
|
+
PATTERNS.map(&:first)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/commenter/filler.rb
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "docx"
|
|
4
|
+
require "zip"
|
|
4
5
|
|
|
5
6
|
module Commenter
|
|
6
7
|
class Filler
|
|
8
|
+
# Labels of the sheet metadata fields in the template's page header
|
|
9
|
+
# (word/header*.xml). Values supplied to #fill are appended after each
|
|
10
|
+
# label; the docx gem cannot write header parts (see docx PR #73), so
|
|
11
|
+
# this is done by rewriting the entry directly.
|
|
12
|
+
HEADER_METADATA = { date: "Date", document: "Document", project: "Project" }.freeze
|
|
13
|
+
|
|
7
14
|
def fill(template_path, output_path, comments, options = {})
|
|
8
15
|
doc = Docx::Document.open(template_path)
|
|
9
16
|
table = doc.tables.first
|
|
@@ -11,48 +18,88 @@ module Commenter
|
|
|
11
18
|
raise "No table found in template" unless table
|
|
12
19
|
raise "Template table must have at least one row" if table.row_count < 1
|
|
13
20
|
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
21
|
+
# The template row (first row in the table) is copied for each comment;
|
|
22
|
+
# all original template rows are removed afterwards via their XML
|
|
23
|
+
# nodes (the shipped 2012-03 template carries extra blank rows beyond
|
|
24
|
+
# the first, and the docx gem's Row exposes no #remove).
|
|
25
|
+
template_rows = table.rows.to_a
|
|
26
|
+
comments.each { |comment| fill_row(template_rows.first, comment, options) }
|
|
27
|
+
template_rows.each { |row| row.node.remove }
|
|
28
|
+
|
|
29
|
+
doc.save(output_path)
|
|
30
|
+
write_header_metadata(output_path, options)
|
|
31
|
+
output_path
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def fill_row(template_row, comment, options)
|
|
37
|
+
comment = Comment.from_hash(comment) unless comment.is_a?(Comment)
|
|
38
|
+
|
|
39
|
+
new_row = template_row.copy
|
|
40
|
+
new_row.insert_before(template_row)
|
|
41
|
+
|
|
42
|
+
set_cell_text(new_row.cells[0], comment.id.to_s)
|
|
43
|
+
set_cell_text(new_row.cells[1], comment.line_number.to_s)
|
|
44
|
+
set_cell_text(new_row.cells[2], comment.clause.to_s)
|
|
45
|
+
set_cell_text(new_row.cells[3], comment.element.to_s)
|
|
46
|
+
set_cell_text(new_row.cells[4], comment.type.to_s)
|
|
47
|
+
set_cell_text(new_row.cells[5], comment.comments.to_s)
|
|
48
|
+
set_cell_text(new_row.cells[6], comment.proposed_change.to_s)
|
|
49
|
+
|
|
50
|
+
observations = comment.observations.to_s
|
|
51
|
+
return if observations.empty?
|
|
52
|
+
|
|
53
|
+
set_cell_text(new_row.cells[7], observations)
|
|
54
|
+
apply_shading(new_row.cells[7], observations) if options[:shading]
|
|
55
|
+
rescue StandardError => e
|
|
56
|
+
puts "Warning: Could not add row for comment #{comment.id}: #{e.message}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def write_header_metadata(output_path, options)
|
|
60
|
+
metadata = HEADER_METADATA.map do |key, label|
|
|
61
|
+
value = options[key].to_s.strip
|
|
62
|
+
value.empty? ? nil : [label, value]
|
|
63
|
+
end.compact
|
|
64
|
+
return if metadata.empty?
|
|
31
65
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
66
|
+
rewrite_headers(output_path, metadata)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def rewrite_headers(output_path, metadata)
|
|
70
|
+
# In-memory buffer + File.binwrite rather than rewriting the archive
|
|
71
|
+
# in place: rubyzip commits in-place edits with File.rename, which
|
|
72
|
+
# fails with EACCES on Windows.
|
|
73
|
+
buffer = Zip::OutputStream.write_buffer do |out|
|
|
74
|
+
Zip::File.open(output_path) do |zip|
|
|
75
|
+
zip.entries.each do |entry|
|
|
76
|
+
# Block form: the entry stream is closed eagerly, otherwise its
|
|
77
|
+
# handle keeps the file locked on Windows and the binwrite below
|
|
78
|
+
# fails with EACCES.
|
|
79
|
+
content = entry.get_input_stream(&:read)
|
|
80
|
+
out.put_next_entry(entry.name)
|
|
81
|
+
out.write(patch_header(entry.name, content, metadata))
|
|
82
|
+
end
|
|
46
83
|
end
|
|
47
84
|
end
|
|
85
|
+
File.binwrite(output_path, buffer.string)
|
|
86
|
+
end
|
|
48
87
|
|
|
49
|
-
|
|
50
|
-
|
|
88
|
+
def patch_header(entry_name, content, metadata)
|
|
89
|
+
return content unless entry_name.match?(%r{\Aword/header\d*\.xml\z})
|
|
51
90
|
|
|
52
|
-
|
|
91
|
+
xml = content.dup.force_encoding(Encoding::UTF_8)
|
|
92
|
+
metadata.each do |label, value|
|
|
93
|
+
xml.sub!(%r{(<w:t[^>]*>)\s*#{label}:\s*(</w:t>)}) do
|
|
94
|
+
"#{Regexp.last_match(1)}#{label}: #{escape(value)}#{Regexp.last_match(2)}"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
xml
|
|
53
98
|
end
|
|
54
99
|
|
|
55
|
-
|
|
100
|
+
def escape(text)
|
|
101
|
+
text.gsub("&", "&").gsub("<", "<").gsub(">", ">")
|
|
102
|
+
end
|
|
56
103
|
|
|
57
104
|
def set_cell_text(cell, text)
|
|
58
105
|
return if text.nil? || text.empty?
|
|
@@ -86,14 +133,16 @@ module Commenter
|
|
|
86
133
|
puts "Warning: Could not set text '#{text}' in cell: #{e.message}"
|
|
87
134
|
end
|
|
88
135
|
|
|
89
|
-
|
|
90
|
-
|
|
136
|
+
SHADING_COLORS = {
|
|
137
|
+
accept_with_modifications: "C4D79B", # Olive Green
|
|
138
|
+
accepted: "92D050", # Green
|
|
139
|
+
noted: "8DB4E2", # Blue
|
|
140
|
+
rejected: "FF99CC", # Pink
|
|
141
|
+
todo: "D9D9D9" # Light Gray (for diagonal stripes, we use solid for now)
|
|
142
|
+
}.freeze
|
|
91
143
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
new_value = value.is_a?(Hash) ? symbolize_keys(value) : value
|
|
95
|
-
result[new_key] = new_value
|
|
96
|
-
end
|
|
144
|
+
def determine_shading_color(observation)
|
|
145
|
+
SHADING_COLORS[DispositionStatus.match(observation)]
|
|
97
146
|
end
|
|
98
147
|
|
|
99
148
|
def apply_shading(cell, observation)
|
|
@@ -111,23 +160,6 @@ module Commenter
|
|
|
111
160
|
puts "Warning: Could not apply shading to cell: #{e.message}"
|
|
112
161
|
end
|
|
113
162
|
|
|
114
|
-
def determine_shading_color(observation)
|
|
115
|
-
text = observation.downcase.strip
|
|
116
|
-
|
|
117
|
-
case text
|
|
118
|
-
when /awm|accept with modifications/
|
|
119
|
-
"C4D79B" # Olive Green
|
|
120
|
-
when /accept(ed)?/
|
|
121
|
-
"92D050" # Green
|
|
122
|
-
when /noted/
|
|
123
|
-
"8DB4E2" # Blue
|
|
124
|
-
when /reject(ed)?/
|
|
125
|
-
"FF99CC" # Pink
|
|
126
|
-
when /todo/
|
|
127
|
-
"D9D9D9" # Light Gray (for diagonal stripes, we'll use solid for now)
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
|
|
131
163
|
def apply_cell_shading(cell, color)
|
|
132
164
|
# Access the cell's XML node
|
|
133
165
|
cell_node = cell.node
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module Commenter
|
|
6
|
+
# GitHub issue tracking state attached to a comment by github-create /
|
|
7
|
+
# github-retrieve.
|
|
8
|
+
class GithubInfo < Lutaml::Model::Serializable
|
|
9
|
+
attribute :issue_number, :integer
|
|
10
|
+
attribute :issue_url, :string
|
|
11
|
+
attribute :status, :string
|
|
12
|
+
attribute :created_at, :string
|
|
13
|
+
attribute :updated_at, :string
|
|
14
|
+
|
|
15
|
+
yaml do
|
|
16
|
+
map "issue_number", to: :issue_number
|
|
17
|
+
map "issue_url", to: :issue_url
|
|
18
|
+
map "status", to: :status
|
|
19
|
+
map "created_at", to: :created_at
|
|
20
|
+
map "updated_at", to: :updated_at
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -19,8 +19,7 @@ module Commenter
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def create_issues_from_yaml(yaml_file, options = {})
|
|
22
|
-
|
|
23
|
-
comment_sheet = CommentSheet.from_hash(data)
|
|
22
|
+
comment_sheet = CommentSheet.from_yaml(File.read(yaml_file))
|
|
24
23
|
|
|
25
24
|
# Override stage if provided
|
|
26
25
|
comment_sheet.stage = options[:stage] if options[:stage]
|
|
@@ -282,11 +281,8 @@ module Commenter
|
|
|
282
281
|
comment = comment_sheet.comments.find { |c| c.id == result[:comment_id] }
|
|
283
282
|
next unless comment
|
|
284
283
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
comment.github[:issue_url] = result[:issue_url]
|
|
288
|
-
comment.github[:status] = "open"
|
|
289
|
-
comment.github[:created_at] = Time.now.utc.iso8601
|
|
284
|
+
comment.record_github_issue(issue_number: result[:issue_number], issue_url: result[:issue_url],
|
|
285
|
+
status: "open", created_at: Time.now.utc.iso8601)
|
|
290
286
|
end
|
|
291
287
|
|
|
292
288
|
# Write updated YAML
|
|
@@ -304,8 +300,7 @@ module Commenter
|
|
|
304
300
|
end
|
|
305
301
|
|
|
306
302
|
def retrieve_observations_from_yaml(yaml_file, options = {})
|
|
307
|
-
|
|
308
|
-
comment_sheet = CommentSheet.from_hash(data)
|
|
303
|
+
comment_sheet = CommentSheet.from_yaml(File.read(yaml_file))
|
|
309
304
|
|
|
310
305
|
results = []
|
|
311
306
|
comment_sheet.comments.each do |comment|
|
|
@@ -349,8 +344,8 @@ module Commenter
|
|
|
349
344
|
if observation
|
|
350
345
|
# Update comment with observation and current status
|
|
351
346
|
comment.observations = observation
|
|
352
|
-
comment.github
|
|
353
|
-
comment.github
|
|
347
|
+
comment.github.status = issue.state
|
|
348
|
+
comment.github.updated_at = Time.now.utc.iso8601
|
|
354
349
|
|
|
355
350
|
{
|
|
356
351
|
comment_id: comment.id,
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "roo"
|
|
4
|
-
require_relative "../comment_sheet"
|
|
5
|
-
require_relative "../comment"
|
|
6
4
|
|
|
7
5
|
module Commenter
|
|
8
6
|
class Parser
|
|
@@ -201,7 +199,7 @@ module Commenter
|
|
|
201
199
|
clause: clause.empty? ? nil : clause,
|
|
202
200
|
element: clause_title.empty? ? nil : clause_title
|
|
203
201
|
}.compact,
|
|
204
|
-
type: CommentType.
|
|
202
|
+
type: CommentType.full_name(cell_value_str(row, col_map[:comment_type])),
|
|
205
203
|
comments: comment_text.to_s.strip,
|
|
206
204
|
proposed_change: cell_value_str(row, col_map[:proposed_change]) ||
|
|
207
205
|
cell_value_str(row, col_map[:proposal_on_text])
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require "zip"
|
|
4
4
|
require "nokogiri"
|
|
5
|
-
require_relative "../comment"
|
|
6
|
-
require_relative "../comment_sheet"
|
|
7
5
|
|
|
8
6
|
module Commenter
|
|
9
7
|
class Parser
|
|
@@ -54,7 +52,7 @@ module Commenter
|
|
|
54
52
|
raise Commenter::Error, "word/document.xml not found in #{path}" unless entry
|
|
55
53
|
|
|
56
54
|
remarks = read_remarks(zip)
|
|
57
|
-
changes =
|
|
55
|
+
changes = entry.get_input_stream { |stream| stream_changes(stream, anchors) }
|
|
58
56
|
end
|
|
59
57
|
|
|
60
58
|
CommentSheet.new(
|
|
@@ -80,7 +78,7 @@ module Commenter
|
|
|
80
78
|
entry = zip.glob("word/comments.xml").first
|
|
81
79
|
return {} unless entry
|
|
82
80
|
|
|
83
|
-
document = Nokogiri::XML(
|
|
81
|
+
document = entry.get_input_stream { |stream| Nokogiri::XML(stream) }
|
|
84
82
|
document.xpath("//w:comment", "w" => W_NS).each_with_object({}) do |node, remarks|
|
|
85
83
|
remarks[node["w:id"]] = {
|
|
86
84
|
author: node["w:author"],
|
|
@@ -102,7 +100,7 @@ module Commenter
|
|
|
102
100
|
id: format("%<body>s-%03<index>d", body: body, index: index + 1),
|
|
103
101
|
body: body,
|
|
104
102
|
locality: { clause: change[:clause], line_number: nil, element: change[:element] },
|
|
105
|
-
type: options[:type] || "te",
|
|
103
|
+
type: CommentType.full_name(options[:type] || "te"),
|
|
106
104
|
comments: "Track change (#{KIND_COMMENT.fetch(kind, kind)})",
|
|
107
105
|
proposed_change: "#{KIND_LABEL.fetch(kind, kind)}: \"#{change[:text].strip}\"",
|
|
108
106
|
observations: observations_for(options)
|
data/lib/commenter/parser.rb
CHANGED
|
@@ -2,13 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
require "docx"
|
|
4
4
|
require "pathname"
|
|
5
|
-
require_relative "comment_sheet"
|
|
6
|
-
require_relative "comment"
|
|
7
|
-
require_relative "parser/osd_xlsx_parser"
|
|
8
|
-
require_relative "parser/track_change_docx_parser"
|
|
9
5
|
|
|
10
6
|
module Commenter
|
|
11
7
|
class Parser
|
|
8
|
+
autoload :OsdXlsxParser, "commenter/parser/osd_xlsx_parser"
|
|
9
|
+
autoload :TrackChangeDocxParser, "commenter/parser/track_change_docx_parser"
|
|
10
|
+
|
|
12
11
|
def parse(input_path, options = {})
|
|
13
12
|
format = detect_format(input_path, options)
|
|
14
13
|
|
|
@@ -90,7 +89,7 @@ module Commenter
|
|
|
90
89
|
clause: presence(cells[2]),
|
|
91
90
|
element: presence(cells[3])
|
|
92
91
|
},
|
|
93
|
-
type: CommentType.
|
|
92
|
+
type: CommentType.full_name(cells[4]),
|
|
94
93
|
comments: cells[5] || "",
|
|
95
94
|
proposed_change: presence(cells[6]),
|
|
96
95
|
user_name: cells[0].to_s.strip
|
data/lib/commenter/version.rb
CHANGED
data/lib/commenter.rb
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "commenter/version"
|
|
4
|
-
require_relative "commenter/comment"
|
|
5
|
-
require_relative "commenter/comment_sheet"
|
|
6
|
-
require_relative "commenter/parser"
|
|
7
|
-
require_relative "commenter/filler"
|
|
8
|
-
require_relative "commenter/github_integration"
|
|
9
4
|
|
|
10
5
|
# Commenter converts ISO comment sheets (DOCX/XLSX) to structured YAML and
|
|
11
6
|
# syncs comments to GitHub issues.
|
|
@@ -13,5 +8,16 @@ module Commenter
|
|
|
13
8
|
class Error < StandardError; end
|
|
14
9
|
|
|
15
10
|
autoload :CommentType, "commenter/comment_type"
|
|
11
|
+
autoload :CommentLocality, "commenter/comment_locality"
|
|
12
|
+
autoload :GithubInfo, "commenter/github_info"
|
|
13
|
+
autoload :DispositionStatus, "commenter/disposition_status"
|
|
14
|
+
autoload :Ballot, "commenter/ballot"
|
|
15
|
+
autoload :BallotReport, "commenter/ballot_report"
|
|
16
|
+
autoload :Comment, "commenter/comment"
|
|
17
|
+
autoload :CommentSheet, "commenter/comment_sheet"
|
|
16
18
|
autoload :GitHubSession, "commenter/github_session"
|
|
19
|
+
autoload :Parser, "commenter/parser"
|
|
20
|
+
autoload :Filler, "commenter/filler"
|
|
21
|
+
autoload :GitHubIssueCreator, "commenter/github_integration"
|
|
22
|
+
autoload :GitHubIssueRetriever, "commenter/github_integration"
|
|
17
23
|
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Commenter::BallotReport do
|
|
6
|
+
def comment(body:, observations: nil, resolution_status: nil, github: {}, type: "technical")
|
|
7
|
+
Commenter::Comment.new(
|
|
8
|
+
id: "#{body}-1", body: body, type: type,
|
|
9
|
+
comments: "A comment", observations: observations,
|
|
10
|
+
resolution_status: resolution_status, github: github
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
let(:sheet) do
|
|
15
|
+
Commenter::CommentSheet.new(comments: [
|
|
16
|
+
comment(body: "DE", observations: "Accepted."),
|
|
17
|
+
comment(body: "DE", observations: "Accept with modifications"),
|
|
18
|
+
comment(body: "US", observations: "Noted"),
|
|
19
|
+
comment(body: "US", observations: "Rejected"),
|
|
20
|
+
comment(body: "US"),
|
|
21
|
+
comment(body: "CS", resolution_status: "Not accepted"),
|
|
22
|
+
comment(body: "CS", github: { status: "open" })
|
|
23
|
+
])
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
describe ".status_for" do
|
|
27
|
+
it "prefers observation text, then resolution status, then GitHub state" do
|
|
28
|
+
comments = sheet.comments
|
|
29
|
+
expect(described_class.status_for(comments[0])).to eq(:accepted)
|
|
30
|
+
expect(described_class.status_for(comments[4])).to eq(:undecided)
|
|
31
|
+
expect(described_class.status_for(comments[5])).to eq(:rejected)
|
|
32
|
+
expect(described_class.status_for(comments[6])).to eq(:open)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
describe ".counts" do
|
|
37
|
+
it "counts per member body, disposition, and type" do
|
|
38
|
+
counts = described_class.counts(sheet)
|
|
39
|
+
|
|
40
|
+
expect(counts[:total]).to eq(7)
|
|
41
|
+
expect(counts[:bodies]["DE"][:total]).to eq(2)
|
|
42
|
+
expect(counts[:bodies]["DE"][:accepted]).to eq(1)
|
|
43
|
+
expect(counts[:bodies]["DE"][:accept_with_modifications]).to eq(1)
|
|
44
|
+
expect(counts[:bodies]["US"][:noted]).to eq(1)
|
|
45
|
+
expect(counts[:bodies]["US"][:rejected]).to eq(1)
|
|
46
|
+
expect(counts[:bodies]["US"][:undecided]).to eq(1)
|
|
47
|
+
expect(counts[:bodies]["CS"][:open]).to eq(1)
|
|
48
|
+
expect(counts[:types]["technical"]).to eq(7)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
describe ".to_markdown" do
|
|
53
|
+
it "renders a ballot report table with totals" do
|
|
54
|
+
markdown = described_class.to_markdown(sheet)
|
|
55
|
+
|
|
56
|
+
expect(markdown).to start_with("| Body | Total | Accepted | AWM | Noted | Rejected | TODO | Open | Undecided |")
|
|
57
|
+
expect(markdown).to include("| DE | 2 | 1 | 1 | 0 | 0 | 0 | 0 | 0 |")
|
|
58
|
+
expect(markdown).to include("| **Total** | **7** | **1** | **1** | **1** | **2** | **0** | **1** | **1** |")
|
|
59
|
+
expect(markdown).to include("Comment types: technical 7")
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|