spreadsheet_architect 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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,13 @@
1
+ if defined? Axlsx
2
+ Axlsx::Col.class_eval do
3
+ def width=(v)
4
+ if v.nil?
5
+ @custom_width = false
6
+ @width = nil
7
+ elsif @width.nil? || @width < v+5
8
+ @custom_width = @best_fit = v != nil
9
+ @width = v + 5
10
+ end
11
+ end
12
+ end
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
@@ -0,0 +1,3 @@
1
+ module SpreadsheetArchitect
2
+ VERSION = "1.0.0"
3
+ end
data/test/database.yml ADDED
@@ -0,0 +1,3 @@
1
+ sqlite3:
2
+ adapter: sqlite3
3
+ database: test/spreadsheet_architect.sqlite3.db
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! }
@@ -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