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.
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