repolho 0.1.8 → 0.2.1
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
- checksums.yaml.gz.sig +3 -4
- data/README.md +72 -2
- data/lib/repolho/container.rb +23 -1
- data/lib/repolho/main.rb +14 -1
- data/lib/repolho/page.rb +30 -0
- data/lib/repolho/template_generator.rb +151 -0
- data/lib/repolho/version.rb +1 -1
- data/lib/repolho.rb +8 -0
- data/test/main_test.rb +66 -0
- data/test/template_generator_test.rb +134 -0
- data.tar.gz.sig +0 -0
- metadata +3 -1
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8f4aac6b18333e8a6d790ad2e130f681419393b0771b98b35368cc34bb11ec7f
|
|
4
|
+
data.tar.gz: 327ddea1d9ecf3bcae6897eed05daba40fc6c1897e5390d0feb4552c19decbc3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d9f15437ff86ee242205fdd24dd52f994a9da1c2dcd0d83814a5ef6b6ca04c69927acc4a014d9c3946686dd2063e31c16b10f246107cfbad95b9d7d8f13eb00a
|
|
7
|
+
data.tar.gz: 4cf688c47fc6658d4c112246f1073f10bad0db3dfa10b85edf47151dd3379c0e002a12a6fdb45ce7cf90bcdc1cff59bd102f8d6f048a4ee32e63021384a9af36
|
checksums.yaml.gz.sig
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
R@���<��;σZ��Ϧ Yp��}�Zv12ؽV�5.�\0�iH����&q(��<���Zڹ�r�(܌���7t�uQ��B��@��w���� "Y����x����ކ�"��2�l������Oۅ����d�gk��)V�l�qK���B:�G�w>D���)��t����tm��be�fL
|
|
1
|
+
H�2�q�=V���8�Vό��_�Q��ˢ�y-�s�;B����hUdCi�0C���3���x�
|
|
2
|
+
}�C���σ���G�+D�Y�ص�`�s���NN��Ak�d��+�,d�`�:�>>��`eB�.��c�S�y�^����B��qw�˧4������9��`����q8����Va,(E�Wi<��>>�K�W�P(���0��
|
|
3
|
+
>��^�AZ$I��,��Yx!oWM��2`��.����;zq>�
|
data/README.md
CHANGED
|
@@ -48,19 +48,89 @@ render html: html.html_safe
|
|
|
48
48
|
# or File.write("report.html", html)
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
+
## Generating a template
|
|
52
|
+
|
|
53
|
+
`Repolho::TemplateGenerator` creates a `.rep.erb` file from a base template
|
|
54
|
+
and column metadata. The base template must already contain a `<report-page>`
|
|
55
|
+
element (the page is required, matching the runtime) and two markers:
|
|
56
|
+
|
|
57
|
+
- `<!-- REPORT:HEADERS -->` — replaced by the generated `<th>` cells
|
|
58
|
+
- `<!-- REPORT:ROW -->` — replaced by the generated `<report-row>`
|
|
59
|
+
|
|
60
|
+
Column names are taken from the data source without executing the full query
|
|
61
|
+
when possible (`ActiveRecord::Result#columns`, a Relation's selected columns
|
|
62
|
+
or model column names, an array of hashes/models, or an explicit array of
|
|
63
|
+
column names).
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
# From a Relation or Result (columns only)
|
|
67
|
+
Repolho::TemplateGenerator.new(
|
|
68
|
+
"app/views/reports/base.rep.erb",
|
|
69
|
+
Order.where(status: "paid")
|
|
70
|
+
).write("app/views/reports/orders.rep.erb")
|
|
71
|
+
|
|
72
|
+
# Explicit columns + custom labels + numeric formatting
|
|
73
|
+
Repolho::TemplateGenerator.new(
|
|
74
|
+
"app/views/reports/base.rep.erb",
|
|
75
|
+
%w[id customer_name total],
|
|
76
|
+
labels: { "total" => "Total (R$)" },
|
|
77
|
+
numeric: %w[total]
|
|
78
|
+
).write("app/views/reports/orders.rep.erb")
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
When running under Rails (or whenever `I18n` is available), header labels are
|
|
82
|
+
looked up case-insensitively under
|
|
83
|
+
`activerecord.attributes.<model>.<column>` (if a `model:` is given) or
|
|
84
|
+
`repolho.attributes.<column>`. The lookup tries the original key, the
|
|
85
|
+
downcase form and the upcase form. If no translation is found, a simple
|
|
86
|
+
humanized name is used.
|
|
87
|
+
|
|
88
|
+
Base template skeleton:
|
|
89
|
+
|
|
90
|
+
```html
|
|
91
|
+
<report>
|
|
92
|
+
<report-page rows="40">
|
|
93
|
+
<report-header>
|
|
94
|
+
<tr>
|
|
95
|
+
<!-- REPORT:HEADERS -->
|
|
96
|
+
</tr>
|
|
97
|
+
</report-header>
|
|
98
|
+
|
|
99
|
+
<!-- REPORT:ROW -->
|
|
100
|
+
|
|
101
|
+
<report-footer>
|
|
102
|
+
<!-- optional page totals -->
|
|
103
|
+
</report-footer>
|
|
104
|
+
</report-page>
|
|
105
|
+
</report>
|
|
106
|
+
```
|
|
107
|
+
|
|
51
108
|
## Template (`.rep.erb`)
|
|
52
109
|
|
|
53
110
|
Repolho uses a few custom elements inside a normal HTML document. Everything
|
|
54
111
|
outside `<report>…</report>` is copied as-is (the `previous` and `after`
|
|
55
112
|
sections). Everything inside the custom elements is processed as ERB.
|
|
56
113
|
|
|
114
|
+
### Why `<report>` instead of `<repolho>`?
|
|
115
|
+
|
|
116
|
+
The element names are intentionally generic (`<report>`, `<report-page>`,
|
|
117
|
+
`<report-group>`, etc.) rather than namespaced under the gem (`<repolho>`,
|
|
118
|
+
`<repolho-page>`, …).
|
|
119
|
+
|
|
120
|
+
Using `repolho` would tie the markup to this specific tool. With `report`,
|
|
121
|
+
the template describes a report structure, not a Ruby library. Someone
|
|
122
|
+
working in another language or with another report engine can reuse the same
|
|
123
|
+
markup (or a close subset of it) without carrying Repolho-specific names.
|
|
124
|
+
The goal is a small, readable convention for report templates — not a
|
|
125
|
+
proprietary dialect.
|
|
126
|
+
|
|
57
127
|
### Custom elements
|
|
58
128
|
|
|
59
129
|
| Element | Purpose | Attributes |
|
|
60
130
|
|---------|---------|------------|
|
|
61
|
-
| `<report>` | Root container of the report | `pagination` (optional, `"true"`): enables client-side page navigation. Also accepts `pagination-first`, `pagination-prev`, `pagination-next`, `pagination-last` for custom labels.<br>`orientation` (optional, `"portrait"` or `"landscape"`): page orientation for print.<br>`paper` (optional, e.g. `"A4"`, `"Letter"`): paper size for print. If either is present, a `@media print` style is injected at the end of `<body
|
|
131
|
+
| `<report>` | Root container of the report | `pagination` (optional, `"true"`): enables client-side page navigation. Also accepts `pagination-first`, `pagination-prev`, `pagination-next`, `pagination-last` for custom labels.<br>`orientation` (optional, `"portrait"` or `"landscape"`): page orientation for print.<br>`paper` (optional, e.g. `"A4"`, `"Letter"`): paper size for print. If either is present, a `@media print` style is injected at the end of `<body>`.<br>`min-row-for-break` (optional, integer): when a group has `page-break-after="true"`, the forced break is skipped if the current page row number is less than or equal to this value. Prevents an extra break when a page has just started. |
|
|
62
132
|
| `<report-page>` | Defines a page (required, only one) | `rows` (required): max data rows per page |
|
|
63
|
-
| `<report-group>` | Nested grouping | `id` (required), `attr` (required), `reprint-header-on-page-break` (optional, `"true"`) |
|
|
133
|
+
| `<report-group>` | Nested grouping | `id` (required), `attr` (required), `reprint-header-on-page-break` (optional, `"true"`), `page-break-after` (optional, `"true"`): after the group ends and its footer is emitted on the current page, fill the remaining rows with blanks and force a page break so the next group starts on a fresh page. Respects `min-row-for-break` on `<report>`. |
|
|
64
134
|
| `<report-header>` | Header of the current container | — |
|
|
65
135
|
| `<report-footer>` | Footer of the current container | — |
|
|
66
136
|
| `<report-row>` | Template for each data row (inside the innermost group) | — |
|
data/lib/repolho/container.rb
CHANGED
|
@@ -6,7 +6,7 @@ require_relative "stats"
|
|
|
6
6
|
|
|
7
7
|
module Repolho
|
|
8
8
|
class Container
|
|
9
|
-
attr_accessor :header, :footer, :containers, :row_template, :attr, :page, :report, :status, :reprint_header_on_page_break
|
|
9
|
+
attr_accessor :header, :footer, :containers, :row_template, :attr, :page, :report, :status, :reprint_header_on_page_break, :page_break_after, :footer_rows
|
|
10
10
|
attr_reader :id, :number, :stats
|
|
11
11
|
|
|
12
12
|
UNSTARTED = 0
|
|
@@ -28,6 +28,8 @@ module Repolho
|
|
|
28
28
|
@attr = nil
|
|
29
29
|
@page = nil
|
|
30
30
|
@report = nil
|
|
31
|
+
@page_break_after = false
|
|
32
|
+
@footer_rows = 0
|
|
31
33
|
|
|
32
34
|
create_locals
|
|
33
35
|
end
|
|
@@ -164,12 +166,32 @@ module Repolho
|
|
|
164
166
|
container.status = FINISHED
|
|
165
167
|
end
|
|
166
168
|
|
|
169
|
+
# Emit this group's footer on the current page (it stays with its data).
|
|
167
170
|
@page << finish
|
|
168
171
|
reset_stats
|
|
169
172
|
@status = FINISHED
|
|
173
|
+
|
|
174
|
+
# Then, if requested, fill the rest of the page with blanks and force
|
|
175
|
+
# a break so the *next* group starts on a fresh page.
|
|
176
|
+
# min-row-for-break skips this when the page has barely started
|
|
177
|
+
# (e.g. the footer itself already caused a break).
|
|
178
|
+
if page_break_after && should_force_page_break?
|
|
179
|
+
@page.pad_and_break
|
|
180
|
+
end
|
|
170
181
|
end
|
|
171
182
|
end
|
|
172
183
|
|
|
184
|
+
def should_force_page_break?
|
|
185
|
+
return false unless @page
|
|
186
|
+
|
|
187
|
+
min = @page.min_row_for_break
|
|
188
|
+
return true if min.nil? || min <= 0
|
|
189
|
+
|
|
190
|
+
# Only force when the current page has progressed past the threshold.
|
|
191
|
+
# Prevents a second break when the page has just started (e.g. row 5 < 35).
|
|
192
|
+
@page.row_number > min
|
|
193
|
+
end
|
|
194
|
+
|
|
173
195
|
def check_page_break
|
|
174
196
|
return unless self.is_a?(Page)
|
|
175
197
|
return unless @row_number >= @valid_rows
|
data/lib/repolho/main.rb
CHANGED
|
@@ -247,6 +247,10 @@ module Repolho
|
|
|
247
247
|
@orientation = report_node["orientation"]&.downcase
|
|
248
248
|
@paper = report_node["paper"]
|
|
249
249
|
|
|
250
|
+
if report_node["min-row-for-break"]
|
|
251
|
+
@page.min_row_for_break = report_node["min-row-for-break"].to_i
|
|
252
|
+
end
|
|
253
|
+
|
|
250
254
|
if @pagination
|
|
251
255
|
@pagination_labels[:first] = report_node["pagination-first"] if report_node["pagination-first"]
|
|
252
256
|
@pagination_labels[:prev] = report_node["pagination-prev"] if report_node["pagination-prev"]
|
|
@@ -297,14 +301,23 @@ module Repolho
|
|
|
297
301
|
def build_group(element)
|
|
298
302
|
container = Container.new(element["id"], @locals, make_stats)
|
|
299
303
|
container.header = info(element.search("> report-header").first)
|
|
300
|
-
|
|
304
|
+
footer_info = info(element.search("> report-footer").first)
|
|
305
|
+
container.footer = footer_info
|
|
306
|
+
container.footer_rows = count_trs(footer_info[:content])
|
|
301
307
|
container.attr = element["attr"]
|
|
302
308
|
container.reprint_header_on_page_break = element["reprint-header-on-page-break"] == "true"
|
|
309
|
+
container.page_break_after = element["page-break-after"] == "true"
|
|
303
310
|
container.page = @page
|
|
304
311
|
container.report = @report
|
|
305
312
|
container
|
|
306
313
|
end
|
|
307
314
|
|
|
315
|
+
def count_trs(html)
|
|
316
|
+
return 0 if html.nil? || html.strip.empty?
|
|
317
|
+
|
|
318
|
+
Nokogiri::HTML(html).search("tr").size
|
|
319
|
+
end
|
|
320
|
+
|
|
308
321
|
def info(element)
|
|
309
322
|
return { content: "" } unless element
|
|
310
323
|
|
data/lib/repolho/page.rb
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
module Repolho
|
|
5
5
|
class Page < Container
|
|
6
6
|
attr_reader :content, :row_number, :rows, :page_number
|
|
7
|
+
attr_accessor :min_row_for_break
|
|
7
8
|
|
|
8
9
|
def initialize(id, locals, stats)
|
|
9
10
|
super(id, locals, stats)
|
|
@@ -43,6 +44,35 @@ module Repolho
|
|
|
43
44
|
end
|
|
44
45
|
end
|
|
45
46
|
|
|
47
|
+
# Fills the current page with blank rows up to the limit and forces a break
|
|
48
|
+
# using the existing page-break path. Called *after* a group footer has
|
|
49
|
+
# already been emitted, so the footer stays on the current page and the
|
|
50
|
+
# next group starts on a fresh page.
|
|
51
|
+
#
|
|
52
|
+
# After the break, the new page is opened and its header is emitted
|
|
53
|
+
# immediately so subsequent content (the group footer) appears *after*
|
|
54
|
+
# the page header, not before it.
|
|
55
|
+
def pad_and_break
|
|
56
|
+
remaining = @valid_rows - @row_number
|
|
57
|
+
if remaining > 0
|
|
58
|
+
blank = %(<tr><td colspan="200"> </td></tr>)
|
|
59
|
+
remaining.times { self << blank }
|
|
60
|
+
end
|
|
61
|
+
check_page_break
|
|
62
|
+
|
|
63
|
+
# Open the new page and emit its header right away.
|
|
64
|
+
# check_page_break left status as FINISHED; we start the page now
|
|
65
|
+
# so later check_started does not emit the header again (late).
|
|
66
|
+
return if @page_open
|
|
67
|
+
|
|
68
|
+
open_page
|
|
69
|
+
if @status == FINISHED
|
|
70
|
+
header_html = start
|
|
71
|
+
self << header_html if header_html && !header_html.to_s.strip.empty?
|
|
72
|
+
@status = STARTED
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
46
76
|
def <<(content)
|
|
47
77
|
return unless content
|
|
48
78
|
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# typed: false
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "cgi"
|
|
5
|
+
|
|
6
|
+
module Repolho
|
|
7
|
+
# Generates a .rep.erb template from a base template and column metadata.
|
|
8
|
+
#
|
|
9
|
+
# The base template must contain a <report-page> element and the markers
|
|
10
|
+
# <!-- REPORT:HEADERS --> and <!-- REPORT:ROW -->. The page element is
|
|
11
|
+
# required, matching the runtime expectation.
|
|
12
|
+
#
|
|
13
|
+
# Column labels are resolved case-insensitively (original, downcase, upcase)
|
|
14
|
+
# via I18n when available. Otherwise a simple humanized form of the column
|
|
15
|
+
# name is used.
|
|
16
|
+
class TemplateGenerator
|
|
17
|
+
HEADERS_MARKER = "<!-- REPORT:HEADERS -->"
|
|
18
|
+
ROW_MARKER = "<!-- REPORT:ROW -->"
|
|
19
|
+
|
|
20
|
+
def initialize(base_path, data, model: nil, labels: {}, numeric: [])
|
|
21
|
+
@base_path = base_path
|
|
22
|
+
@data = data
|
|
23
|
+
@model = model
|
|
24
|
+
@labels = labels.transform_keys(&:to_s)
|
|
25
|
+
@numeric = Array(numeric).map(&:to_s)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Returns the generated template content as a String.
|
|
29
|
+
def generate
|
|
30
|
+
content = File.read(@base_path)
|
|
31
|
+
validate_base!(content)
|
|
32
|
+
|
|
33
|
+
columns = extract_columns(@data)
|
|
34
|
+
raise ArgumentError, "No columns could be extracted from the given data" if columns.empty?
|
|
35
|
+
|
|
36
|
+
headers = generate_headers(columns)
|
|
37
|
+
row = generate_row(columns)
|
|
38
|
+
|
|
39
|
+
content
|
|
40
|
+
.gsub(HEADERS_MARKER, headers)
|
|
41
|
+
.gsub(ROW_MARKER, row)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Writes the generated template to output_path.
|
|
45
|
+
def write(output_path)
|
|
46
|
+
File.write(output_path, generate)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def validate_base!(content)
|
|
52
|
+
unless content.include?("<report-page")
|
|
53
|
+
raise ArgumentError, "Base template must contain a <report-page> element"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
unless content.include?(HEADERS_MARKER)
|
|
57
|
+
raise ArgumentError, "Base template must contain the marker #{HEADERS_MARKER}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
unless content.include?(ROW_MARKER)
|
|
61
|
+
raise ArgumentError, "Base template must contain the marker #{ROW_MARKER}"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def extract_columns(data)
|
|
66
|
+
return data.map(&:to_s) if explicit_column_list?(data)
|
|
67
|
+
|
|
68
|
+
if data.respond_to?(:columns)
|
|
69
|
+
return data.columns.map(&:to_s)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
if data.respond_to?(:klass) && data.klass.respond_to?(:column_names)
|
|
73
|
+
if data.respond_to?(:select_values) && data.select_values.any?
|
|
74
|
+
return data.select_values.map { |v| extract_column_name(v) }
|
|
75
|
+
end
|
|
76
|
+
return data.klass.column_names.map(&:to_s)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
extract_from_rows(data)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def explicit_column_list?(data)
|
|
83
|
+
data.is_a?(Array) && data.all? { |c| c.is_a?(String) || c.is_a?(Symbol) }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def extract_column_name(value)
|
|
87
|
+
value.to_s.split(".").last.gsub(/["'`]/, "").strip
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def extract_from_rows(data)
|
|
91
|
+
rows = data.respond_to?(:to_a) ? data.to_a : Array(data)
|
|
92
|
+
return [] if rows.empty?
|
|
93
|
+
|
|
94
|
+
first = rows.first
|
|
95
|
+
if first.respond_to?(:attributes)
|
|
96
|
+
first.attributes.keys.map(&:to_s)
|
|
97
|
+
elsif first.is_a?(Hash)
|
|
98
|
+
first.keys.map(&:to_s)
|
|
99
|
+
else
|
|
100
|
+
[]
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def generate_headers(columns)
|
|
105
|
+
columns.map { |col| "<th>#{CGI.escapeHTML(column_label(col))}</th>" }.join("\n ")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def generate_row(columns)
|
|
109
|
+
cells = columns.map do |col|
|
|
110
|
+
expr = if @numeric.include?(col)
|
|
111
|
+
%(<%= number_with_precision(attr(:#{col}), precision: 2) %>)
|
|
112
|
+
else
|
|
113
|
+
%(<%= attr(:#{col}) %>)
|
|
114
|
+
end
|
|
115
|
+
"<td>#{expr}</td>"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
<<~HTML.strip
|
|
119
|
+
<report-row>
|
|
120
|
+
<tr>
|
|
121
|
+
#{cells.join("\n ")}
|
|
122
|
+
</tr>
|
|
123
|
+
</report-row>
|
|
124
|
+
HTML
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def column_label(column)
|
|
128
|
+
column = column.to_s
|
|
129
|
+
return @labels[column] if @labels.key?(column)
|
|
130
|
+
|
|
131
|
+
return humanize(column) unless defined?(I18n)
|
|
132
|
+
|
|
133
|
+
[column, column.downcase, column.upcase].uniq.each do |key|
|
|
134
|
+
full_key = if @model
|
|
135
|
+
"activerecord.attributes.#{@model.model_name.i18n_key}.#{key}"
|
|
136
|
+
else
|
|
137
|
+
"repolho.attributes.#{key}"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
result = I18n.t(full_key, default: nil)
|
|
141
|
+
return result if result
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
humanize(column)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def humanize(column)
|
|
148
|
+
column.to_s.tr("_", " ").capitalize
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
data/lib/repolho/version.rb
CHANGED
data/lib/repolho.rb
CHANGED
|
@@ -6,6 +6,7 @@ require_relative "repolho/container"
|
|
|
6
6
|
require_relative "repolho/page"
|
|
7
7
|
require_relative "repolho/report"
|
|
8
8
|
require_relative "repolho/main"
|
|
9
|
+
require_relative "repolho/template_generator"
|
|
9
10
|
|
|
10
11
|
module Repolho
|
|
11
12
|
# Includes Rails helper modules into Container so they become available
|
|
@@ -14,6 +15,11 @@ module Repolho
|
|
|
14
15
|
# By default includes ActionView::Helpers::NumberHelper.
|
|
15
16
|
# Pass additional modules if needed.
|
|
16
17
|
#
|
|
18
|
+
# The method is idempotent: modules already included are skipped.
|
|
19
|
+
#
|
|
20
|
+
# Outside a full Rails environment you must load the helpers correctly
|
|
21
|
+
# before calling this method (see README).
|
|
22
|
+
#
|
|
17
23
|
# Example:
|
|
18
24
|
# Repolho.include_rails_helpers!
|
|
19
25
|
# Repolho.include_rails_helpers!(ActionView::Helpers::NumberHelper, ActionView::Helpers::TextHelper)
|
|
@@ -24,5 +30,7 @@ module Repolho
|
|
|
24
30
|
|
|
25
31
|
Container.include(mod)
|
|
26
32
|
end
|
|
33
|
+
|
|
27
34
|
end
|
|
35
|
+
|
|
28
36
|
end
|
data/test/main_test.rb
CHANGED
|
@@ -457,5 +457,71 @@ module Repolho
|
|
|
457
457
|
ensure
|
|
458
458
|
template&.unlink
|
|
459
459
|
end
|
|
460
|
+
|
|
461
|
+
def test_page_break_after_group_pads_and_breaks
|
|
462
|
+
data = [
|
|
463
|
+
{ "id" => 1, "name" => "Alice", "value" => 10.0, "state" => "SP" },
|
|
464
|
+
{ "id" => 2, "name" => "Bob", "value" => 20.0, "state" => "SP" },
|
|
465
|
+
{ "id" => 3, "name" => "Carol", "value" => 15.0, "state" => "RJ" },
|
|
466
|
+
{ "id" => 4, "name" => "Dave", "value" => 25.0, "state" => "RJ" }
|
|
467
|
+
]
|
|
468
|
+
|
|
469
|
+
template_content = GROUP_TEMPLATE.gsub(
|
|
470
|
+
'<report-group id="state" attr="state">',
|
|
471
|
+
'<report-group id="state" attr="state" page-break-after="true">'
|
|
472
|
+
).gsub(
|
|
473
|
+
'<report-page rows="10">',
|
|
474
|
+
'<report-page rows="6">'
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
template = write_temp_template(template_content)
|
|
478
|
+
main = Main.new(template.path, data)
|
|
479
|
+
main.run
|
|
480
|
+
html = main.content.join
|
|
481
|
+
doc = html_doc(html)
|
|
482
|
+
|
|
483
|
+
pages = page_tbodies(doc)
|
|
484
|
+
assert pages.size >= 2, "Expected a forced page break after group, got #{pages.size}"
|
|
485
|
+
|
|
486
|
+
# Blank padding rows should be present
|
|
487
|
+
assert_includes html, " "
|
|
488
|
+
|
|
489
|
+
# Group footer of the first group should appear, and the second group header too
|
|
490
|
+
assert_includes html, "State total"
|
|
491
|
+
assert_includes html, "State: RJ"
|
|
492
|
+
ensure
|
|
493
|
+
template&.unlink
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def test_min_row_for_break_skips_early_page
|
|
497
|
+
data = [
|
|
498
|
+
{ "id" => 1, "name" => "Alice", "value" => 10.0, "state" => "SP" },
|
|
499
|
+
{ "id" => 2, "name" => "Bob", "value" => 20.0, "state" => "RJ" }
|
|
500
|
+
]
|
|
501
|
+
|
|
502
|
+
# High threshold: page has barely started, so no forced break
|
|
503
|
+
template_content = GROUP_TEMPLATE.gsub(
|
|
504
|
+
"<report>",
|
|
505
|
+
'<report min-row-for-break="20">'
|
|
506
|
+
).gsub(
|
|
507
|
+
'<report-group id="state" attr="state">',
|
|
508
|
+
'<report-group id="state" attr="state" page-break-after="true">'
|
|
509
|
+
).gsub(
|
|
510
|
+
'<report-page rows="10">',
|
|
511
|
+
'<report-page rows="30">'
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
template = write_temp_template(template_content)
|
|
515
|
+
main = Main.new(template.path, data)
|
|
516
|
+
main.run
|
|
517
|
+
html = main.content.join
|
|
518
|
+
doc = html_doc(html)
|
|
519
|
+
|
|
520
|
+
pages = page_tbodies(doc)
|
|
521
|
+
assert_equal 1, pages.size, "Threshold should have prevented an early forced break"
|
|
522
|
+
refute_includes html, " "
|
|
523
|
+
ensure
|
|
524
|
+
template&.unlink
|
|
525
|
+
end
|
|
460
526
|
end
|
|
461
527
|
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "test_helper"
|
|
4
|
+
|
|
5
|
+
class TemplateGeneratorTest < Minitest::Test
|
|
6
|
+
include TestHelpers
|
|
7
|
+
|
|
8
|
+
def setup
|
|
9
|
+
@base = <<~HTML
|
|
10
|
+
<!DOCTYPE html>
|
|
11
|
+
<html>
|
|
12
|
+
<body>
|
|
13
|
+
<table>
|
|
14
|
+
<report>
|
|
15
|
+
<report-page rows="40">
|
|
16
|
+
<report-header>
|
|
17
|
+
<tr>
|
|
18
|
+
<!-- REPORT:HEADERS -->
|
|
19
|
+
</tr>
|
|
20
|
+
</report-header>
|
|
21
|
+
|
|
22
|
+
<!-- REPORT:ROW -->
|
|
23
|
+
|
|
24
|
+
<report-footer>
|
|
25
|
+
<tr><td colspan="99">Footer</td></tr>
|
|
26
|
+
</report-footer>
|
|
27
|
+
</report-page>
|
|
28
|
+
</report>
|
|
29
|
+
</table>
|
|
30
|
+
</body>
|
|
31
|
+
</html>
|
|
32
|
+
HTML
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_generates_headers_and_row_from_array_of_hashes
|
|
36
|
+
data = [{ "id" => 1, "name" => "Alice", "total" => 10.5 }]
|
|
37
|
+
file = write_temp_template(@base)
|
|
38
|
+
|
|
39
|
+
generator = Repolho::TemplateGenerator.new(file.path, data)
|
|
40
|
+
result = generator.generate
|
|
41
|
+
|
|
42
|
+
assert_includes result, "<th>Id</th>"
|
|
43
|
+
assert_includes result, "<th>Name</th>"
|
|
44
|
+
assert_includes result, "<th>Total</th>"
|
|
45
|
+
assert_includes result, "<report-row>"
|
|
46
|
+
assert_includes result, "<%= attr(:id) %>"
|
|
47
|
+
assert_includes result, "<%= attr(:name) %>"
|
|
48
|
+
assert_includes result, "<%= attr(:total) %>"
|
|
49
|
+
refute_includes result, "<!-- REPORT:HEADERS -->"
|
|
50
|
+
refute_includes result, "<!-- REPORT:ROW -->"
|
|
51
|
+
ensure
|
|
52
|
+
file.unlink
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def test_uses_explicit_column_list
|
|
56
|
+
file = write_temp_template(@base)
|
|
57
|
+
generator = Repolho::TemplateGenerator.new(file.path, %w[id customer_name])
|
|
58
|
+
result = generator.generate
|
|
59
|
+
|
|
60
|
+
assert_includes result, "<th>Id</th>"
|
|
61
|
+
assert_includes result, "<th>Customer name</th>"
|
|
62
|
+
assert_includes result, "<%= attr(:customer_name) %>"
|
|
63
|
+
ensure
|
|
64
|
+
file.unlink
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def test_custom_labels_override
|
|
68
|
+
file = write_temp_template(@base)
|
|
69
|
+
generator = Repolho::TemplateGenerator.new(
|
|
70
|
+
file.path,
|
|
71
|
+
%w[id total],
|
|
72
|
+
labels: { "total" => "Total (R$)" }
|
|
73
|
+
)
|
|
74
|
+
result = generator.generate
|
|
75
|
+
|
|
76
|
+
assert_includes result, "<th>Total (R$)</th>"
|
|
77
|
+
assert_includes result, "<th>Id</th>"
|
|
78
|
+
ensure
|
|
79
|
+
file.unlink
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def test_numeric_columns_use_number_helper
|
|
83
|
+
file = write_temp_template(@base)
|
|
84
|
+
generator = Repolho::TemplateGenerator.new(
|
|
85
|
+
file.path,
|
|
86
|
+
%w[id total],
|
|
87
|
+
numeric: %w[total]
|
|
88
|
+
)
|
|
89
|
+
result = generator.generate
|
|
90
|
+
|
|
91
|
+
assert_includes result, "<%= number_with_precision(attr(:total), precision: 2) %>"
|
|
92
|
+
assert_includes result, "<%= attr(:id) %>"
|
|
93
|
+
ensure
|
|
94
|
+
file.unlink
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def test_raises_when_page_missing
|
|
98
|
+
bad = @base.gsub("<report-page rows=\"40\">", "").gsub("</report-page>", "")
|
|
99
|
+
file = write_temp_template(bad)
|
|
100
|
+
|
|
101
|
+
generator = Repolho::TemplateGenerator.new(file.path, %w[id])
|
|
102
|
+
error = assert_raises(ArgumentError) { generator.generate }
|
|
103
|
+
assert_match(/report-page/, error.message)
|
|
104
|
+
ensure
|
|
105
|
+
file.unlink
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def test_raises_when_markers_missing
|
|
109
|
+
bad = @base.gsub("<!-- REPORT:HEADERS -->", "").gsub("<!-- REPORT:ROW -->", "")
|
|
110
|
+
file = write_temp_template(bad)
|
|
111
|
+
|
|
112
|
+
generator = Repolho::TemplateGenerator.new(file.path, %w[id])
|
|
113
|
+
error = assert_raises(ArgumentError) { generator.generate }
|
|
114
|
+
assert_match(/REPORT:HEADERS/, error.message)
|
|
115
|
+
ensure
|
|
116
|
+
file.unlink
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def test_write_creates_file
|
|
120
|
+
file = write_temp_template(@base)
|
|
121
|
+
output = Tempfile.new(["out", ".rep.erb"])
|
|
122
|
+
output.close
|
|
123
|
+
|
|
124
|
+
generator = Repolho::TemplateGenerator.new(file.path, %w[id name])
|
|
125
|
+
generator.write(output.path)
|
|
126
|
+
|
|
127
|
+
content = File.read(output.path)
|
|
128
|
+
assert_includes content, "<report-row>"
|
|
129
|
+
assert_includes content, "<%= attr(:name) %>"
|
|
130
|
+
ensure
|
|
131
|
+
file.unlink
|
|
132
|
+
output.unlink
|
|
133
|
+
end
|
|
134
|
+
end
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: repolho
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Eustáquio Rangel
|
|
@@ -97,6 +97,7 @@ files:
|
|
|
97
97
|
- lib/repolho/page.rb
|
|
98
98
|
- lib/repolho/report.rb
|
|
99
99
|
- lib/repolho/stats.rb
|
|
100
|
+
- lib/repolho/template_generator.rb
|
|
100
101
|
- lib/repolho/version.rb
|
|
101
102
|
- test/container_test.rb
|
|
102
103
|
- test/fixtures/simple.rep.erb
|
|
@@ -104,6 +105,7 @@ files:
|
|
|
104
105
|
- test/page_test.rb
|
|
105
106
|
- test/report_test.rb
|
|
106
107
|
- test/stats_test.rb
|
|
108
|
+
- test/template_generator_test.rb
|
|
107
109
|
- test/test_helper.rb
|
|
108
110
|
homepage: https://github.com/taq/repolho
|
|
109
111
|
licenses:
|
metadata.gz.sig
CHANGED
|
Binary file
|