spreadsheet_architect 1.0.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/CHANGELOG.md +5 -0
- data/LICENSE +674 -0
- data/README.md +136 -0
- data/Rakefile +17 -0
- data/lib/spreadsheet_architect.rb +170 -0
- data/lib/spreadsheet_architect/action_controller_renderers.rb +13 -0
- data/lib/spreadsheet_architect/axlsx_column_width_patch.rb +13 -0
- data/lib/spreadsheet_architect/set_mime_types.rb +10 -0
- data/lib/spreadsheet_architect/version.rb +3 -0
- data/test/database.yml +3 -0
- data/test/helper.rb +52 -0
- data/test/spreadsheet_architect.sqlite3.db +0 -0
- data/test/tc_spreadsheet_architect.rb +84 -0
- metadata +159 -0
data/README.md
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
# Spreadsheet Architect
|
2
|
+
|
3
|
+
Spreadsheet Architect lets you turn any activerecord relation or plain ruby class object into a XLSX, ODS, or CSV spreadsheets. Generates columns from model activerecord column_names or from an array of ruby methods.
|
4
|
+
|
5
|
+
Spreadsheet Architect adds the following methods to your class:
|
6
|
+
```ruby
|
7
|
+
# Plain Ruby
|
8
|
+
Post.to_xlsx(data: posts_array)
|
9
|
+
Post.to_ods(data: posts_array)
|
10
|
+
Post.to_csv(data: posts_array)
|
11
|
+
|
12
|
+
# Rails
|
13
|
+
Post.order(name: :asc).where(published: true).to_xlsx
|
14
|
+
Post.order(name: :asc).where(published: true).to_ods
|
15
|
+
Post.order(name: :asc).where(published: true).to_csv
|
16
|
+
```
|
17
|
+
|
18
|
+
|
19
|
+
# Install
|
20
|
+
```ruby
|
21
|
+
gem install spreadsheet_architect
|
22
|
+
```
|
23
|
+
|
24
|
+
|
25
|
+
# Setup
|
26
|
+
|
27
|
+
### Model
|
28
|
+
```ruby
|
29
|
+
class Post < ActiveRecord::Base #activerecord not required
|
30
|
+
include SpreadsheetArchitect
|
31
|
+
|
32
|
+
belongs_to :author
|
33
|
+
|
34
|
+
#optional for activerecord classes, defaults to the models column_names
|
35
|
+
def self.spreadsheet_columns
|
36
|
+
#[[Label, Method/Statement to Call on each Instance]....]
|
37
|
+
[['Title', :title],['Content', 'content'],['Author','author.name rescue nil',['Published?', "(published ? 'Yes' : 'No')"]]]
|
38
|
+
|
39
|
+
# OR just humanize the method to use as the label ex. "Title", "Content", "Author Name", "Published"
|
40
|
+
[:title, 'content', 'author.name rescue nil', :published]
|
41
|
+
|
42
|
+
# OR a Combination of Both
|
43
|
+
[:title, :content, ['Author','author.name rescue nil'], :published]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
```
|
47
|
+
|
48
|
+
# Usage
|
49
|
+
|
50
|
+
### Method 1: Controller for a Rails Model
|
51
|
+
```ruby
|
52
|
+
class PostsController < ActionController::Base
|
53
|
+
def index
|
54
|
+
@posts = Post.order(published_at: :asc)
|
55
|
+
|
56
|
+
respond_to do |format|
|
57
|
+
format.html
|
58
|
+
format.xlsx { render xlsx: @posts.to_xlsx, filename: "posts.xlsx" }
|
59
|
+
format.ods { render ods: @posts.to_ods, filename: "posts.ods" }
|
60
|
+
format.csv { render csv: @posts.to_csv, filename: "posts.csv" }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
```
|
65
|
+
|
66
|
+
### Method 2: Controller for a Plain Ruby Model
|
67
|
+
```ruby
|
68
|
+
class PostsController < ActionController::Base
|
69
|
+
def index
|
70
|
+
posts_array = [Post.new, Post.new, Post.new]
|
71
|
+
|
72
|
+
respond_to do |format|
|
73
|
+
format.html
|
74
|
+
format.xlsx { render xlsx: Post.to_xlsx(data: posts_array), filename: "posts.xlsx" }
|
75
|
+
format.ods { render ods: Post.to_ods(data: posts_array), filename: "posts.ods" }
|
76
|
+
format.csv { render csv: Post.to_csv(data: posts_array), filename: "posts.csv" }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
```
|
81
|
+
|
82
|
+
### Method 3: Save to a file manually
|
83
|
+
```ruby
|
84
|
+
File.open('path/to/file.xlsx') do |f|
|
85
|
+
f.write{ Post.order(published_at: :asc).to_xlsx }
|
86
|
+
end
|
87
|
+
File.open('path/to/file.ods') do |f|
|
88
|
+
f.write{ Post.order(published_at: :asc).to_ods }
|
89
|
+
end
|
90
|
+
File.open('path/to/file.csv') do |f|
|
91
|
+
f.write{ Post.order(published_at: :asc).to_csv }
|
92
|
+
end
|
93
|
+
```
|
94
|
+
|
95
|
+
|
96
|
+
# Method Options
|
97
|
+
|
98
|
+
### to_xlsx, to_ods, to_csv
|
99
|
+
**data** - *Array* - Mainly for Plain Ruby objects pass in an array of instances. Optional for ActiveRecord relations, you can just chain the method to the end of your relation.
|
100
|
+
|
101
|
+
**headers** - *Boolean* - Default: true - Pass in false if you do not want a header row.
|
102
|
+
|
103
|
+
**spreadsheet_columns** - *Array* - A string array of attributes, methods, or ruby statements to be executed on each instance. Use this to override the models spreadsheet_columns/column_names method for one time.
|
104
|
+
|
105
|
+
### to_xlsx
|
106
|
+
**sheet_name** - *String*
|
107
|
+
|
108
|
+
**header_style** - *Hash* - Default: `{bg_color: "AAAAAA", fg_color: "FFFFFF", alignment: { horizontal: :center }, bold: true}`
|
109
|
+
|
110
|
+
**row_style** - Hash
|
111
|
+
|
112
|
+
### to_ods
|
113
|
+
**sheet_name** - *String*
|
114
|
+
|
115
|
+
**header_style** - *Hash* - Default: {bold: true} - Note: Currently only supports bold & fg_color style options
|
116
|
+
|
117
|
+
**row_style** - *Hash*
|
118
|
+
|
119
|
+
### to_csv
|
120
|
+
Only the generic options
|
121
|
+
|
122
|
+
|
123
|
+
# Style Options
|
124
|
+
**bg_color** - *6 Digit Hex Color without the # Symbol* - Ex. "AAAAAAA"
|
125
|
+
|
126
|
+
**fg_color** - *Text Color - 6 Digit Hex Color without the # Symbol* - Ex. "0000000"
|
127
|
+
|
128
|
+
**alignment** - *Hash* - Ex. {horizontal: :right, vertical: :top}
|
129
|
+
|
130
|
+
**bold** - *Boolean*
|
131
|
+
|
132
|
+
|
133
|
+
# Credits
|
134
|
+
Created by Weston Ganger - @westonganger
|
135
|
+
|
136
|
+
Heavily influenced by the dead gem `acts_as_xlsx` by @randym but adapted to work for more spreadsheet types and plain ruby models.
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/lib/spreadsheet_architect/version.rb')
|
2
|
+
require 'bundler/gem_tasks'
|
3
|
+
|
4
|
+
task :test do
|
5
|
+
require 'rake/testtask'
|
6
|
+
Rake::TestTask.new do |t|
|
7
|
+
t.libs << 'test'
|
8
|
+
t.test_files = FileList['test/**/tc_*.rb']
|
9
|
+
t.verbose = true
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
task release: :build do
|
14
|
+
system "gem push spreadsheet_architect-#{SpreadsheetArchitect::VERSION}.gem"
|
15
|
+
end
|
16
|
+
|
17
|
+
task default: :test
|
@@ -0,0 +1,170 @@
|
|
1
|
+
require 'spreadsheet_architect/set_mime_types'
|
2
|
+
require 'spreadsheet_architect/action_controller_renderers'
|
3
|
+
require 'axlsx'
|
4
|
+
require 'spreadsheet_architect/axlsx_column_width_patch'
|
5
|
+
require 'odf/spreadsheet'
|
6
|
+
require 'csv'
|
7
|
+
|
8
|
+
module SpreadsheetArchitect
|
9
|
+
def self.included(base)
|
10
|
+
base.send :extend, ClassMethods
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
def sa_str_humanize(str, capitalize = true)
|
15
|
+
str = str.sub(/\A_+/, '').sub(/_id\z/, '').gsub(/[_\.]/,' ').sub(' rescue nil','')
|
16
|
+
if capitalize
|
17
|
+
str = str.gsub(/(\A|\ )\w/){|x| x.upcase}
|
18
|
+
end
|
19
|
+
str
|
20
|
+
end
|
21
|
+
|
22
|
+
def sa_get_options(options={})
|
23
|
+
if self.ancestors.include?(ActiveRecord::Base) && !self.respond_to?(:spreadsheet_columns) && !options[:spreadsheet_columns]
|
24
|
+
headers = self.column_names.map{|x| x.humanize}
|
25
|
+
columns = self.column_names.map{|x| x.to_s}
|
26
|
+
elsif options[:spreadsheet_columns] || self.respond_to?(:spreadsheet_columns)
|
27
|
+
headers = []
|
28
|
+
columns = []
|
29
|
+
|
30
|
+
array = options[:spreadsheet_columns] || self.spreadsheet_columns || []
|
31
|
+
array.each do |x|
|
32
|
+
if x.is_a?(Array)
|
33
|
+
headers.push x[0].to_s
|
34
|
+
columns.push x[1].to_s
|
35
|
+
else
|
36
|
+
headers.push sa_str_humanize(x.to_s)
|
37
|
+
columns.push x.to_s
|
38
|
+
end
|
39
|
+
end
|
40
|
+
else
|
41
|
+
headers = []
|
42
|
+
columns = []
|
43
|
+
end
|
44
|
+
|
45
|
+
headers = (options[:headers] == false ? false : headers)
|
46
|
+
|
47
|
+
if options[:header_style]
|
48
|
+
header_style = options[:header_style]
|
49
|
+
elsif options[:header_style] == false
|
50
|
+
header_style = false
|
51
|
+
elsif options[:row_style]
|
52
|
+
header_style = options[:row_style]
|
53
|
+
else
|
54
|
+
header_style = {bg_color: "AAAAAA", fg_color: "FFFFFF", alignment: { horizontal: :center }, bold: true}
|
55
|
+
end
|
56
|
+
|
57
|
+
row_style = options[:row_style]
|
58
|
+
|
59
|
+
sheet_name = options[:sheet_name] || self.name
|
60
|
+
|
61
|
+
if options[:data]
|
62
|
+
data = options[:data].to_a
|
63
|
+
elsif self.ancestors.include?(ActiveRecord::Base)
|
64
|
+
data = where(options[:where]).order(options[:order]).to_a
|
65
|
+
else
|
66
|
+
# object must have a to_a method
|
67
|
+
data = self.to_a
|
68
|
+
end
|
69
|
+
|
70
|
+
types = (options[:types] || []).flatten
|
71
|
+
|
72
|
+
return {headers: headers, columns: columns, header_style: header_style, row_style: row_style, types: types, sheet_name: sheet_name, data: data}
|
73
|
+
end
|
74
|
+
|
75
|
+
def sa_get_row_data(the_columns=[], instance)
|
76
|
+
row_data = []
|
77
|
+
the_columns.each do |col|
|
78
|
+
col.split('.').each_with_index do |x,i|
|
79
|
+
if i == 0
|
80
|
+
col = instance.instance_eval(x)
|
81
|
+
else
|
82
|
+
col = col.instance_eval(x)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
row_data.push col
|
86
|
+
end
|
87
|
+
return row_data
|
88
|
+
end
|
89
|
+
|
90
|
+
def to_csv(opts={})
|
91
|
+
options = sa_get_options(opts)
|
92
|
+
|
93
|
+
CSV.generate do |csv|
|
94
|
+
csv << options[:headers] if options[:headers]
|
95
|
+
|
96
|
+
options[:data].each do |x|
|
97
|
+
csv << sa_get_row_data(options[:columns], x)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def to_ods(opts={})
|
103
|
+
options = sa_get_options(opts)
|
104
|
+
|
105
|
+
spreadsheet = ODF::Spreadsheet.new
|
106
|
+
|
107
|
+
spreadsheet.office_style :header_style, family: :cell do
|
108
|
+
if options[:header_style]
|
109
|
+
if options[:header_style][:bold]
|
110
|
+
property :text, 'font-weight': :bold
|
111
|
+
property :text, 'align': :center
|
112
|
+
end
|
113
|
+
if options[:header_style][:fg_color] && opts[:header_style] && opts[:header_style][:fg_color] #temporary
|
114
|
+
property :text, 'color': "##{options[:header_style][:fg_color]}"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
spreadsheet.office_style :row_style, family: :cell do
|
119
|
+
if options[:row_style]
|
120
|
+
if options[:row_style][:bold]
|
121
|
+
property :text, 'font-weight': :bold
|
122
|
+
end
|
123
|
+
if options[:row_style][:fg_color]
|
124
|
+
property :text, 'color': "##{options[:header_style][:fg_color]}"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
this_class = self
|
130
|
+
spreadsheet.table options[:sheet_name] do
|
131
|
+
if options[:headers]
|
132
|
+
row do
|
133
|
+
options[:headers].each do |header|
|
134
|
+
cell header, style: (:header_style if options[:header_style])
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
options[:data].each do |x|
|
139
|
+
row do
|
140
|
+
this_class.sa_get_row_data(options[:columns], x).each do |y|
|
141
|
+
cell y, style: (:row_style if options[:row_style])
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
return spreadsheet.bytes
|
148
|
+
end
|
149
|
+
|
150
|
+
def to_xlsx(opts={})
|
151
|
+
options = sa_get_options(opts)
|
152
|
+
|
153
|
+
package = opts[:package] || Axlsx::Package.new
|
154
|
+
|
155
|
+
return package if options[:data].empty?
|
156
|
+
|
157
|
+
package.workbook.add_worksheet(name: options[:sheet_name]) do |sheet|
|
158
|
+
if options[:headers]
|
159
|
+
sheet.add_row options[:headers], style: (package.workbook.styles.add_style(options[:header_style]) if options[:header_style])
|
160
|
+
end
|
161
|
+
|
162
|
+
options[:data].each do |x|
|
163
|
+
sheet.add_row sa_get_row_data(options[:columns], x), style: (package.workbook.styles.add_style(options[:row_style]) if options[:row_style]), types: options[:types]
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
return package.to_stream.read
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
if defined? ActionController
|
2
|
+
ActionController::Renderers.add :xlsx do |data, options|
|
3
|
+
send_data data, type: :xlsx, disposition: :attachment, filename: "#{options[:filename] ? options[:filename].sub(".xlsx",'') || 'data'}.xlsx"
|
4
|
+
end
|
5
|
+
ActionController::Renderers.add :ods do |data, options|
|
6
|
+
send_data data, type: :ods, disposition: :attachment, filename: "#{options[:filename] ? options[:filename].sub(".ods",'') || 'data'}.ods"
|
7
|
+
end
|
8
|
+
ActionController::Renderers.add :csv do |data, options|
|
9
|
+
send_data data, type: :csv, disposition: :attachment, filename: "#{options[:filename] ? options[:filename].sub(".csv",'') || 'data'}.csv"
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
if defined? Mime
|
2
|
+
unless defined? Mime::XLSX
|
3
|
+
Mime::Type.register "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", :xlsx
|
4
|
+
end
|
5
|
+
unless defined? Mime::ODS
|
6
|
+
Mime::Type.register "application/vnd.oasis.opendocument.spreadsheet", :ods
|
7
|
+
end
|
8
|
+
else
|
9
|
+
puts "Mime module not defined. Skipping registration of xlsx & ods"
|
10
|
+
end
|
data/test/database.yml
ADDED
data/test/helper.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
|
2
|
+
ActiveRecord::Base.establish_connection(config['sqlite3'])
|
3
|
+
ActiveRecord::Schema.define(version: 0) do
|
4
|
+
begin
|
5
|
+
drop_table :posts, :force => true
|
6
|
+
drop_table :other_posts, :force => true
|
7
|
+
rescue
|
8
|
+
#dont really care if the tables are not dropped
|
9
|
+
end
|
10
|
+
|
11
|
+
create_table(:posts, :force => true) do |t|
|
12
|
+
t.string :name
|
13
|
+
t.string :title
|
14
|
+
t.text :content
|
15
|
+
t.integer :votes
|
16
|
+
t.timestamps null: false
|
17
|
+
end
|
18
|
+
|
19
|
+
create_table(:other_posts, :force => true) do |t|
|
20
|
+
t.string :name
|
21
|
+
t.string :title
|
22
|
+
t.text :content
|
23
|
+
t.integer :votes
|
24
|
+
t.timestamps null: false
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Post < ActiveRecord::Base
|
29
|
+
include SpreadsheetArchitect
|
30
|
+
|
31
|
+
def spreadsheet_columns
|
32
|
+
[:name, :title, :content, :votes, :ranking]
|
33
|
+
end
|
34
|
+
|
35
|
+
def ranking
|
36
|
+
1
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class OtherPost < ActiveRecord::Base
|
41
|
+
include SpreadsheetArchitect
|
42
|
+
end
|
43
|
+
|
44
|
+
posts = []
|
45
|
+
posts << Post.new(name: "first post", title: "This is the first post", content: "I am a very good first post!", votes: 1)
|
46
|
+
posts << Post.new(name: "second post", title: "This is the second post", content: "I am the best post!", votes: 7)
|
47
|
+
posts.each { |p| p.save! }
|
48
|
+
|
49
|
+
posts = []
|
50
|
+
posts << OtherPost.new(name: "my other first", title: "first other post", content: "the first other post!", votes: 1)
|
51
|
+
posts << OtherPost.new(name: "my other second", title: "second other post", content: "last other post!", votes: 7)
|
52
|
+
posts.each { |p| p.save! }
|
Binary file
|
@@ -0,0 +1,84 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
require "spreadsheet_architect"
|
3
|
+
require 'yaml'
|
4
|
+
require 'active_record'
|
5
|
+
require 'minitest'
|
6
|
+
|
7
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
|
8
|
+
|
9
|
+
class TestSpreadsheetArchitect < MiniTest::Test
|
10
|
+
class Post < ActiveRecord::Base
|
11
|
+
include SpreadsheetArchitect
|
12
|
+
|
13
|
+
def self.spreadsheet_columns
|
14
|
+
[:name, :title, :content, :votes, :ranking]
|
15
|
+
end
|
16
|
+
|
17
|
+
def ranking
|
18
|
+
1
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class OtherPost < ActiveRecord::Base
|
23
|
+
include SpreadsheetArchitect
|
24
|
+
end
|
25
|
+
|
26
|
+
class PlainPost
|
27
|
+
include SpreadsheetArchitect
|
28
|
+
|
29
|
+
def self.spreadsheet_columns
|
30
|
+
[:name, :title, :content]
|
31
|
+
end
|
32
|
+
|
33
|
+
def name
|
34
|
+
"the name"
|
35
|
+
end
|
36
|
+
|
37
|
+
def title
|
38
|
+
"the title"
|
39
|
+
end
|
40
|
+
|
41
|
+
def content
|
42
|
+
"the content"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
test "test_spreadsheet_options" do
|
47
|
+
assert_equal([:name, :title, :content, :votes, :ranking], Post.spreadsheet_columns)
|
48
|
+
assert_equal([:name, :title, :content, :votes, :created_at, :updated_at], OtherPost.column_names)
|
49
|
+
assert_equal([:name, :title, :content], PlainPost.spreadsheet_columns)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class TestToCsv < MiniTest::Test
|
54
|
+
test "test_class_method" do
|
55
|
+
p = Post.to_csv(spreadsheet_columns: [:name, :votes, :content, :ranking])
|
56
|
+
assert_equal(true, p.is_a?(String))
|
57
|
+
end
|
58
|
+
test 'test_chained_method' do
|
59
|
+
p = Post.order("name asc").to_csv(spreadsheet_columns: [:name, :votes, :content, :ranking])
|
60
|
+
assert_equal(true, p.is_a?(String))
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class TestToOds < MiniTest::Test
|
65
|
+
test 'test_class_method' do
|
66
|
+
p = Post.to_ods(spreadsheet_columns: [:name, :votes, :content, :ranking])
|
67
|
+
assert_equal(true, p.is_a?(String))
|
68
|
+
end
|
69
|
+
test 'test_chained_method' do
|
70
|
+
p = Post.order("name asc").to_ods(spreadsheet_columns: [:name, :votes, :content, :ranking])
|
71
|
+
assert_equal(true, p.is_a?(String))
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class TestToXlsx < MiniTest::Test
|
76
|
+
test 'test_class_method' do
|
77
|
+
p = Post.to_xlsx(spreadsheet_columns: [:name, :votes, :content, :ranking])
|
78
|
+
assert_equal(true, p.is_a?(String))
|
79
|
+
end
|
80
|
+
test 'test_chained_method' do
|
81
|
+
p = Post.order("name asc").to_xlsx(spreadsheet_columns: [:name, :votes, :content, :ranking])
|
82
|
+
assert_equal(true, p.is_a?(String))
|
83
|
+
end
|
84
|
+
end
|