rails-excel-reporter 0.1.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 +7 -0
- data/CLAUDE.md +94 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +306 -0
- data/README.md +506 -0
- data/lib/generators/report/report_generator.rb +32 -0
- data/lib/generators/report/templates/report.rb.erb +43 -0
- data/lib/rails_excel_reporter/base.rb +210 -0
- data/lib/rails_excel_reporter/configuration.rb +40 -0
- data/lib/rails_excel_reporter/controller_helpers.rb +34 -0
- data/lib/rails_excel_reporter/railtie.rb +35 -0
- data/lib/rails_excel_reporter/streaming.rb +81 -0
- data/lib/rails_excel_reporter/styling.rb +87 -0
- data/lib/rails_excel_reporter/version.rb +3 -0
- data/lib/rails_excel_reporter.rb +19 -0
- data/rails_excel_reporter.gemspec +36 -0
- data/spec/rails_excel_reporter/base_spec.rb +190 -0
- data/spec/rails_excel_reporter/configuration_spec.rb +75 -0
- data/spec/rails_excel_reporter/controller_helpers_spec.rb +121 -0
- data/spec/rails_excel_reporter/streaming_spec.rb +139 -0
- data/spec/rails_excel_reporter/styling_spec.rb +124 -0
- data/spec/rails_excel_reporter_spec.rb +25 -0
- data/spec/spec_helper.rb +30 -0
- metadata +211 -0
data/README.md
ADDED
@@ -0,0 +1,506 @@
|
|
1
|
+
# Rails Excel Reporter
|
2
|
+
|
3
|
+
[](https://badge.fury.io/rb/rails-excel-reporter)
|
4
|
+
[](https://github.com/rails-excel-reporter/rails-excel-reporter/actions)
|
5
|
+
|
6
|
+
A Ruby gem that integrates seamlessly with Ruby on Rails to generate Excel reports (.xlsx format) using a simple DSL. Features include streaming for large datasets, custom styling, callbacks, and Rails helpers.
|
7
|
+
|
8
|
+
## Features
|
9
|
+
|
10
|
+
- 🚀 **Simple DSL** - Define reports with a clean, intuitive syntax
|
11
|
+
- 📊 **Excel Generation** - Create .xlsx files using the powerful caxlsx gem
|
12
|
+
- 🎨 **Custom Styling** - Apply styles to headers, columns, and cells
|
13
|
+
- 🔄 **Streaming Support** - Handle large datasets efficiently with streaming
|
14
|
+
- 📱 **Rails Integration** - Auto-registers with Rails, includes controller helpers
|
15
|
+
- 🔧 **Callbacks** - Hook into the generation process with before/after callbacks
|
16
|
+
- 🎯 **Flexible Data Sources** - Works with ActiveRecord, arrays, and any enumerable
|
17
|
+
- 📁 **Rails Generator** - Scaffold reports quickly with `rails g report`
|
18
|
+
|
19
|
+
## Installation
|
20
|
+
|
21
|
+
Add this line to your application's Gemfile:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
gem 'rails-excel-reporter'
|
25
|
+
```
|
26
|
+
|
27
|
+
And then execute:
|
28
|
+
|
29
|
+
```bash
|
30
|
+
bundle install
|
31
|
+
```
|
32
|
+
|
33
|
+
Or install it yourself as:
|
34
|
+
|
35
|
+
```bash
|
36
|
+
gem install rails-excel-reporter
|
37
|
+
```
|
38
|
+
|
39
|
+
## Quick Start
|
40
|
+
|
41
|
+
### 1. Create a Report
|
42
|
+
|
43
|
+
Create a report in `app/reports/`:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
class UserReport < RailsExcelReporter::Base
|
47
|
+
attributes :id, :name, :email, :created_at
|
48
|
+
|
49
|
+
def created_at
|
50
|
+
object.created_at.strftime("%Y-%m-%d")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
```
|
54
|
+
|
55
|
+
### 2. Use in Controller
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
class ReportsController < ApplicationController
|
59
|
+
def users
|
60
|
+
@users = User.active.order(:created_at)
|
61
|
+
report = UserReport.new(@users)
|
62
|
+
send_excel_report(report)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
```
|
66
|
+
|
67
|
+
### 3. Generate with Rails Generator
|
68
|
+
|
69
|
+
```bash
|
70
|
+
rails g report User name:string email:string role:string
|
71
|
+
```
|
72
|
+
|
73
|
+
This creates `app/reports/user_report.rb` with the basic structure.
|
74
|
+
|
75
|
+
## Basic Usage
|
76
|
+
|
77
|
+
### Defining Attributes
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
class ProductReport < RailsExcelReporter::Base
|
81
|
+
# Simple attributes
|
82
|
+
attributes :id, :name, :price
|
83
|
+
|
84
|
+
# Custom headers
|
85
|
+
attributes :id, { name: :product_name, header: "Product Name" }, :price
|
86
|
+
|
87
|
+
# Or use individual attribute method
|
88
|
+
attribute :sku, header: "SKU Code"
|
89
|
+
end
|
90
|
+
```
|
91
|
+
|
92
|
+
### Custom Methods
|
93
|
+
|
94
|
+
Override attribute methods to customize output:
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
class OrderReport < RailsExcelReporter::Base
|
98
|
+
attributes :id, :customer_name, :total, :status
|
99
|
+
|
100
|
+
def customer_name
|
101
|
+
"#{object.customer.first_name} #{object.customer.last_name}"
|
102
|
+
end
|
103
|
+
|
104
|
+
def total
|
105
|
+
"$#{object.total.round(2)}"
|
106
|
+
end
|
107
|
+
|
108
|
+
def status
|
109
|
+
object.status.upcase
|
110
|
+
end
|
111
|
+
end
|
112
|
+
```
|
113
|
+
|
114
|
+
### Data Sources
|
115
|
+
|
116
|
+
Works with various data sources:
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
# ActiveRecord collections
|
120
|
+
report = UserReport.new(User.active)
|
121
|
+
|
122
|
+
# Arrays
|
123
|
+
users = [
|
124
|
+
{ id: 1, name: "John", email: "john@example.com" },
|
125
|
+
{ id: 2, name: "Jane", email: "jane@example.com" }
|
126
|
+
]
|
127
|
+
report = UserReport.new(users)
|
128
|
+
|
129
|
+
# Any enumerable
|
130
|
+
report = UserReport.new(csv_data.lazy.map(&:to_h))
|
131
|
+
```
|
132
|
+
|
133
|
+
## Advanced Features
|
134
|
+
|
135
|
+
### Streaming for Large Datasets
|
136
|
+
|
137
|
+
Automatically handles large datasets with streaming:
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
class LargeDataReport < RailsExcelReporter::Base
|
141
|
+
attributes :id, :name, :data
|
142
|
+
|
143
|
+
# Customize streaming threshold (default: 1000)
|
144
|
+
self.streaming_threshold = 5000
|
145
|
+
end
|
146
|
+
|
147
|
+
# Usage with progress tracking
|
148
|
+
report = LargeDataReport.new(huge_dataset) do |progress|
|
149
|
+
puts "Processing: #{progress.current}/#{progress.total} (#{progress.percentage}%)"
|
150
|
+
end
|
151
|
+
```
|
152
|
+
|
153
|
+
### Custom Styling
|
154
|
+
|
155
|
+
Apply custom styles to headers and columns:
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
class StyledReport < RailsExcelReporter::Base
|
159
|
+
attributes :id, :name, :email, :status
|
160
|
+
|
161
|
+
# Header styling
|
162
|
+
style :header, {
|
163
|
+
bg_color: "4472C4",
|
164
|
+
fg_color: "FFFFFF",
|
165
|
+
bold: true,
|
166
|
+
font_size: 12
|
167
|
+
}
|
168
|
+
|
169
|
+
# Column-specific styling
|
170
|
+
style :id, {
|
171
|
+
alignment: { horizontal: :center },
|
172
|
+
font_size: 10
|
173
|
+
}
|
174
|
+
|
175
|
+
style :status, {
|
176
|
+
bold: true,
|
177
|
+
bg_color: "E7E6E6"
|
178
|
+
}
|
179
|
+
end
|
180
|
+
```
|
181
|
+
|
182
|
+
### Callbacks
|
183
|
+
|
184
|
+
Hook into the generation process:
|
185
|
+
|
186
|
+
```ruby
|
187
|
+
class CallbackReport < RailsExcelReporter::Base
|
188
|
+
attributes :id, :name, :email
|
189
|
+
|
190
|
+
def before_render
|
191
|
+
Rails.logger.info "Starting report generation at #{Time.current}"
|
192
|
+
end
|
193
|
+
|
194
|
+
def after_render
|
195
|
+
Rails.logger.info "Report generated successfully"
|
196
|
+
end
|
197
|
+
|
198
|
+
def before_row(object)
|
199
|
+
# Called before each row is processed
|
200
|
+
end
|
201
|
+
|
202
|
+
def after_row(object)
|
203
|
+
# Called after each row is processed
|
204
|
+
end
|
205
|
+
end
|
206
|
+
```
|
207
|
+
|
208
|
+
## Controller Integration
|
209
|
+
|
210
|
+
### Helper Methods
|
211
|
+
|
212
|
+
The gem provides several helper methods for controllers:
|
213
|
+
|
214
|
+
```ruby
|
215
|
+
class ReportsController < ApplicationController
|
216
|
+
def download_users
|
217
|
+
report = UserReport.new(User.all)
|
218
|
+
|
219
|
+
# Simple download
|
220
|
+
send_excel_report(report)
|
221
|
+
|
222
|
+
# With custom filename
|
223
|
+
send_excel_report(report, filename: "users_#{Date.current}.xlsx")
|
224
|
+
|
225
|
+
# Stream large reports
|
226
|
+
stream_excel_report(report)
|
227
|
+
|
228
|
+
# Automatic streaming based on size
|
229
|
+
excel_report_response(report)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
```
|
233
|
+
|
234
|
+
### Response Methods
|
235
|
+
|
236
|
+
Available response methods:
|
237
|
+
|
238
|
+
```ruby
|
239
|
+
# Basic file download
|
240
|
+
send_excel_report(report)
|
241
|
+
|
242
|
+
# Streaming response (for large files)
|
243
|
+
stream_excel_report(report)
|
244
|
+
|
245
|
+
# Automatic selection based on report size
|
246
|
+
excel_report_response(report)
|
247
|
+
```
|
248
|
+
|
249
|
+
## API Reference
|
250
|
+
|
251
|
+
### Report Instance Methods
|
252
|
+
|
253
|
+
```ruby
|
254
|
+
report = UserReport.new(users)
|
255
|
+
|
256
|
+
# Get the generated file
|
257
|
+
file = report.file # Returns Tempfile
|
258
|
+
|
259
|
+
# Get binary data
|
260
|
+
xlsx_data = report.to_xlsx # Returns String
|
261
|
+
|
262
|
+
# Save to specific path
|
263
|
+
report.save_to("/path/to/file.xlsx")
|
264
|
+
|
265
|
+
# Get suggested filename
|
266
|
+
filename = report.filename # Returns "user_report_2024_01_15.xlsx"
|
267
|
+
|
268
|
+
# Get IO stream
|
269
|
+
stream = report.stream # Returns StringIO
|
270
|
+
|
271
|
+
# Get hash representation
|
272
|
+
hash = report.to_h # Returns Hash with metadata
|
273
|
+
```
|
274
|
+
|
275
|
+
### Configuration
|
276
|
+
|
277
|
+
Configure the gem globally:
|
278
|
+
|
279
|
+
```ruby
|
280
|
+
# config/initializers/rails_excel_reporter.rb
|
281
|
+
RailsExcelReporter.configure do |config|
|
282
|
+
config.default_styles = {
|
283
|
+
header: {
|
284
|
+
bg_color: "2E75B6",
|
285
|
+
fg_color: "FFFFFF",
|
286
|
+
bold: true
|
287
|
+
},
|
288
|
+
cell: {
|
289
|
+
border: { style: :thin, color: "CCCCCC" }
|
290
|
+
}
|
291
|
+
}
|
292
|
+
|
293
|
+
config.date_format = "%d/%m/%Y"
|
294
|
+
config.streaming_threshold = 2000
|
295
|
+
config.temp_directory = Rails.root.join("tmp", "reports")
|
296
|
+
end
|
297
|
+
```
|
298
|
+
|
299
|
+
## Error Handling
|
300
|
+
|
301
|
+
The gem includes comprehensive error handling:
|
302
|
+
|
303
|
+
```ruby
|
304
|
+
begin
|
305
|
+
report = UserReport.new(users)
|
306
|
+
report.to_xlsx
|
307
|
+
rescue RailsExcelReporter::AttributeNotFoundError => e
|
308
|
+
Rails.logger.error "Missing attribute: #{e.message}"
|
309
|
+
rescue RailsExcelReporter::InvalidConfigurationError => e
|
310
|
+
Rails.logger.error "Configuration error: #{e.message}"
|
311
|
+
rescue RailsExcelReporter::Error => e
|
312
|
+
Rails.logger.error "Report generation failed: #{e.message}"
|
313
|
+
end
|
314
|
+
```
|
315
|
+
|
316
|
+
## Performance Considerations
|
317
|
+
|
318
|
+
### Streaming
|
319
|
+
|
320
|
+
For large datasets (>1000 records by default), the gem automatically uses streaming:
|
321
|
+
|
322
|
+
```ruby
|
323
|
+
# This will stream automatically
|
324
|
+
large_report = UserReport.new(User.limit(10000))
|
325
|
+
large_report.should_stream? # => true
|
326
|
+
```
|
327
|
+
|
328
|
+
### Memory Usage
|
329
|
+
|
330
|
+
The gem is designed to be memory-efficient:
|
331
|
+
|
332
|
+
- Uses streaming for large datasets
|
333
|
+
- Lazy evaluation where possible
|
334
|
+
- Efficient Excel generation with caxlsx
|
335
|
+
- Automatic garbage collection of temporary files
|
336
|
+
|
337
|
+
## Testing
|
338
|
+
|
339
|
+
### Test Helpers
|
340
|
+
|
341
|
+
The gem includes test helpers for easier testing:
|
342
|
+
|
343
|
+
```ruby
|
344
|
+
RSpec.describe UserReport do
|
345
|
+
let(:users) { create_list(:user, 3) }
|
346
|
+
let(:report) { UserReport.new(users) }
|
347
|
+
|
348
|
+
describe "#to_xlsx" do
|
349
|
+
it "generates Excel file" do
|
350
|
+
xlsx_data = report.to_xlsx
|
351
|
+
expect(xlsx_data).to be_present
|
352
|
+
expect(xlsx_data[0, 4]).to eq("PK\x03\x04") # ZIP signature
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
describe "#filename" do
|
357
|
+
it "generates appropriate filename" do
|
358
|
+
expect(report.filename).to match(/user_report_\d{4}_\d{2}_\d{2}\.xlsx/)
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
362
|
+
```
|
363
|
+
|
364
|
+
### Running Tests
|
365
|
+
|
366
|
+
```bash
|
367
|
+
# Run all tests
|
368
|
+
bundle exec rspec
|
369
|
+
|
370
|
+
# Run specific test file
|
371
|
+
bundle exec rspec spec/rails_excel_reporter/base_spec.rb
|
372
|
+
|
373
|
+
# Run with coverage
|
374
|
+
bundle exec rspec --format documentation
|
375
|
+
```
|
376
|
+
|
377
|
+
## Examples
|
378
|
+
|
379
|
+
### Basic Report
|
380
|
+
|
381
|
+
```ruby
|
382
|
+
class ProductReport < RailsExcelReporter::Base
|
383
|
+
attributes :id, :name, :price, :category
|
384
|
+
|
385
|
+
def price
|
386
|
+
"$#{object.price.round(2)}"
|
387
|
+
end
|
388
|
+
|
389
|
+
def category
|
390
|
+
object.category.name
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
# Usage
|
395
|
+
products = Product.includes(:category).order(:name)
|
396
|
+
report = ProductReport.new(products)
|
397
|
+
report.save_to("products.xlsx")
|
398
|
+
```
|
399
|
+
|
400
|
+
### Advanced Report with Styling
|
401
|
+
|
402
|
+
```ruby
|
403
|
+
class SalesReport < RailsExcelReporter::Base
|
404
|
+
attributes :date, :product, :quantity, :revenue, :profit
|
405
|
+
|
406
|
+
style :header, {
|
407
|
+
bg_color: "1F4E79",
|
408
|
+
fg_color: "FFFFFF",
|
409
|
+
bold: true,
|
410
|
+
font_size: 14
|
411
|
+
}
|
412
|
+
|
413
|
+
style :revenue, {
|
414
|
+
bg_color: "E2EFDA",
|
415
|
+
alignment: { horizontal: :right }
|
416
|
+
}
|
417
|
+
|
418
|
+
style :profit, {
|
419
|
+
bg_color: "FCE4D6",
|
420
|
+
alignment: { horizontal: :right }
|
421
|
+
}
|
422
|
+
|
423
|
+
def date
|
424
|
+
object.created_at.strftime("%Y-%m-%d")
|
425
|
+
end
|
426
|
+
|
427
|
+
def product
|
428
|
+
object.product.name
|
429
|
+
end
|
430
|
+
|
431
|
+
def revenue
|
432
|
+
"$#{object.revenue.round(2)}"
|
433
|
+
end
|
434
|
+
|
435
|
+
def profit
|
436
|
+
"$#{object.profit.round(2)}"
|
437
|
+
end
|
438
|
+
|
439
|
+
def before_render
|
440
|
+
Rails.logger.info "Generating sales report for #{collection.count} records"
|
441
|
+
end
|
442
|
+
end
|
443
|
+
```
|
444
|
+
|
445
|
+
### Streaming Report with Progress
|
446
|
+
|
447
|
+
```ruby
|
448
|
+
class MassiveDataReport < RailsExcelReporter::Base
|
449
|
+
attributes :id, :data, :processed_at
|
450
|
+
|
451
|
+
self.streaming_threshold = 10000
|
452
|
+
|
453
|
+
def processed_at
|
454
|
+
object.processed_at.strftime("%Y-%m-%d %H:%M:%S")
|
455
|
+
end
|
456
|
+
end
|
457
|
+
|
458
|
+
# Usage with progress tracking
|
459
|
+
report = MassiveDataReport.new(huge_dataset) do |progress|
|
460
|
+
puts "Progress: #{progress.percentage}% (#{progress.current}/#{progress.total})"
|
461
|
+
end
|
462
|
+
```
|
463
|
+
|
464
|
+
## Requirements
|
465
|
+
|
466
|
+
- Ruby 2.7+
|
467
|
+
- Rails 6.0+
|
468
|
+
- caxlsx ~> 4.0
|
469
|
+
|
470
|
+
## Contributing
|
471
|
+
|
472
|
+
1. Fork it
|
473
|
+
2. Create your feature branch (`git checkout -b feature/my-new-feature`)
|
474
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
475
|
+
4. Push to the branch (`git push origin feature/my-new-feature`)
|
476
|
+
5. Create new Pull Request
|
477
|
+
|
478
|
+
## Development
|
479
|
+
|
480
|
+
```bash
|
481
|
+
# Clone the repository
|
482
|
+
git clone https://github.com/rails-excel-reporter/rails-excel-reporter.git
|
483
|
+
cd rails-excel-reporter
|
484
|
+
|
485
|
+
# Install dependencies
|
486
|
+
bundle install
|
487
|
+
|
488
|
+
# Run tests
|
489
|
+
bundle exec rspec
|
490
|
+
|
491
|
+
# Run linter
|
492
|
+
bundle exec rubocop
|
493
|
+
|
494
|
+
# Generate documentation
|
495
|
+
bundle exec yard
|
496
|
+
```
|
497
|
+
|
498
|
+
## License
|
499
|
+
|
500
|
+
This gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
501
|
+
|
502
|
+
## Support
|
503
|
+
|
504
|
+
- **Issues**: [GitHub Issues](https://github.com/rails-excel-reporter/rails-excel-reporter/issues)
|
505
|
+
- **Documentation**: [GitHub Wiki](https://github.com/rails-excel-reporter/rails-excel-reporter/wiki)
|
506
|
+
- **Changelog**: [CHANGELOG.md](https://github.com/rails-excel-reporter/rails-excel-reporter/blob/main/CHANGELOG.md)
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/named_base'
|
3
|
+
|
4
|
+
module Rails
|
5
|
+
module Generators
|
6
|
+
class ReportGenerator < NamedBase
|
7
|
+
source_root File.expand_path('templates', __dir__)
|
8
|
+
|
9
|
+
argument :attributes, type: :array, default: [], banner: 'field:type field:type'
|
10
|
+
|
11
|
+
def create_report_file
|
12
|
+
template 'report.rb.erb', File.join('app/reports', class_path, "#{file_name}_report.rb")
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def report_class_name
|
18
|
+
"#{class_name}Report"
|
19
|
+
end
|
20
|
+
|
21
|
+
def parsed_attributes
|
22
|
+
attributes.map do |attr|
|
23
|
+
{ name: attr.name, type: attr.type }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def attribute_names
|
28
|
+
parsed_attributes.map { |attr| ":#{attr[:name]}" }.join(', ')
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
class <%= report_class_name %> < RailsExcelReporter::Base
|
2
|
+
<% if parsed_attributes.any? %>
|
3
|
+
attributes <%= attribute_names %>
|
4
|
+
<% else %>
|
5
|
+
attributes :id, :name, :created_at
|
6
|
+
<% end %>
|
7
|
+
|
8
|
+
<% parsed_attributes.each do |attr| %>
|
9
|
+
<% if attr[:type] == "date" || attr[:type] == "datetime" %>
|
10
|
+
def <%= attr[:name] %>
|
11
|
+
object.<%= attr[:name] %>&.strftime("%Y-%m-%d")
|
12
|
+
end
|
13
|
+
<% end %>
|
14
|
+
<% end %>
|
15
|
+
|
16
|
+
# Example of custom styling
|
17
|
+
# style :header, {
|
18
|
+
# bg_color: "4472C4",
|
19
|
+
# fg_color: "FFFFFF",
|
20
|
+
# bold: true
|
21
|
+
# }
|
22
|
+
|
23
|
+
# style :id, {
|
24
|
+
# alignment: { horizontal: :center }
|
25
|
+
# }
|
26
|
+
|
27
|
+
# Example of callbacks
|
28
|
+
# def before_render
|
29
|
+
# Rails.logger.info "Starting <%= report_class_name %> generation"
|
30
|
+
# end
|
31
|
+
|
32
|
+
# def after_render
|
33
|
+
# Rails.logger.info "Completed <%= report_class_name %> generation"
|
34
|
+
# end
|
35
|
+
|
36
|
+
# def before_row(object)
|
37
|
+
# # Called before each row
|
38
|
+
# end
|
39
|
+
|
40
|
+
# def after_row(object)
|
41
|
+
# # Called after each row
|
42
|
+
# end
|
43
|
+
end
|