spreadsheet_architect 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|