to_spreadsheet 0.9.3 → 1.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.textile +107 -0
- data/Rakefile +15 -6
- data/lib/to_spreadsheet/action_pack_renderers.rb +16 -10
- data/lib/to_spreadsheet/axlsx/formatter.rb +86 -0
- data/lib/to_spreadsheet/axlsx/renderer.rb +52 -0
- data/lib/to_spreadsheet/context/pairing.rb +26 -0
- data/lib/to_spreadsheet/context.rb +130 -0
- data/lib/to_spreadsheet/formats.rb +56 -0
- data/lib/to_spreadsheet/helpers.rb +24 -0
- data/lib/to_spreadsheet/mime_types.rb +1 -1
- data/lib/to_spreadsheet/themes/default.rb +37 -0
- data/lib/to_spreadsheet/version.rb +2 -2
- data/lib/to_spreadsheet/xlsx.rb +0 -0
- data/lib/to_spreadsheet.rb +17 -1
- data/spec/defaults_spec.rb +29 -0
- data/spec/format_spec.rb +29 -0
- data/spec/types_spec.rb +37 -0
- data/spec/worksheets_spec.rb +16 -0
- metadata +62 -21
- data/README.rdoc +0 -63
- data/lib/to_spreadsheet/xls.rb +0 -42
data/README.textile
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
to_spreadsheet is a gem that lets you render xls from your existing haml/erb views from Rails (>= 3.0). !https://secure.travis-ci.org/glebm/to_spreadsheet.png?branch=master(Build Status)!:http://travis-ci.org/glebm/to_spreadsheet
|
2
|
+
|
3
|
+
h2. Installation
|
4
|
+
|
5
|
+
Add it to your Gemfile:
|
6
|
+
|
7
|
+
bc. gem 'to_spreadsheet'
|
8
|
+
|
9
|
+
h2. Usage
|
10
|
+
|
11
|
+
In your controller:
|
12
|
+
|
13
|
+
bc. # my_thingies_controller.rb
|
14
|
+
class MyThingiesController < ApplicationController
|
15
|
+
respond_to :xls, :html
|
16
|
+
def index
|
17
|
+
@my_thingies = MyItem.all
|
18
|
+
respond_to do |format|
|
19
|
+
format.html {}
|
20
|
+
format.xlsx { render xlsx: :index, filename: "thingies_index" }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
In your view partial:
|
26
|
+
|
27
|
+
bc. # _my_items.haml
|
28
|
+
%table
|
29
|
+
%caption My items
|
30
|
+
%thead
|
31
|
+
%tr
|
32
|
+
%td ID
|
33
|
+
%td Name
|
34
|
+
%tbody
|
35
|
+
- my_items.each do |my_item|
|
36
|
+
%tr
|
37
|
+
%td.number= my_item.id
|
38
|
+
%td= my_item.name
|
39
|
+
%tfoot
|
40
|
+
%tr
|
41
|
+
%td(colspan="2") #{my_items.length}
|
42
|
+
|
43
|
+
In your index.xls.haml:
|
44
|
+
|
45
|
+
bc. # index.xls.haml
|
46
|
+
= render 'my_items', my_items: @my_items
|
47
|
+
|
48
|
+
In your index.html.haml:
|
49
|
+
|
50
|
+
bc. # index.html.haml
|
51
|
+
= link_to 'Download spreadsheet', my_items_url(format: :xlsx)
|
52
|
+
= render 'my_items', my_items: @my_items
|
53
|
+
|
54
|
+
h3. Formatting
|
55
|
+
|
56
|
+
You can define formats in your view file (local to the view) or in the initializer
|
57
|
+
|
58
|
+
bc. format_xls 'table.my-table' do
|
59
|
+
workbook use_autowidth: true
|
60
|
+
sheet orientation: landscape
|
61
|
+
|
62
|
+
format 'th', b: true # bold
|
63
|
+
format 'tbody tr', color: lambda { |row| 'ddffdd' if row.index.odd? }
|
64
|
+
format 'A3:B10', i: true # italic
|
65
|
+
format column: 0, width: 35
|
66
|
+
format 'td.custom', lambda { |cell| modify cell somehow.}
|
67
|
+
|
68
|
+
# default value (fallback value when value is blank or 0 for integer / float)
|
69
|
+
default 'td.price', 10
|
70
|
+
|
71
|
+
For the full list of supported properties head here: http://rubydoc.info/github/randym/axlsx/Axlsx/Cell
|
72
|
+
In addition, for column formats, Axlsx columnInfo properties are also supported
|
73
|
+
|
74
|
+
h3. Themes
|
75
|
+
|
76
|
+
You can define "themes" - blocks of formatting code:
|
77
|
+
|
78
|
+
bc. ToSpreadsheet.theme :zebra do
|
79
|
+
format 'tr', color: lambda { |row| 'ddffdd' if row.index.odd? }
|
80
|
+
|
81
|
+
And then use them:
|
82
|
+
|
83
|
+
bc. format_xls 'table.zebra', ToSpreadsheet.theme(:zebra)
|
84
|
+
|
85
|
+
h3. Types
|
86
|
+
|
87
|
+
The default theme uses class names on td/th to cast values.
|
88
|
+
Here is the list of class to type mapping:
|
89
|
+
|
90
|
+
|_. ==CSS== class |_. Format |
|
91
|
+
| decimal or float | Decimal |
|
92
|
+
| num or int | Integer |
|
93
|
+
| datetime | DateTime (Chronic.parse) |
|
94
|
+
| date | Date (Date.parse) |
|
95
|
+
| time | Time (Chronic.parse) |
|
96
|
+
|
97
|
+
h3. Styling
|
98
|
+
|
99
|
+
|
100
|
+
h3. Default values
|
101
|
+
|
102
|
+
Add a `data-default="default value"` attribute to a cell to use the value as a default if the model value is nil.
|
103
|
+
|
104
|
+
h3. Worksheets
|
105
|
+
|
106
|
+
Every table in the view will be converted to a separate sheet.
|
107
|
+
The sheet title will be assigned to the value of the table's <caption> element if it exists.
|
data/Rakefile
CHANGED
@@ -1,13 +1,22 @@
|
|
1
1
|
# encoding: UTF-8
|
2
2
|
require 'rubygems'
|
3
|
-
|
4
|
-
require 'bundler/setup'
|
5
|
-
rescue LoadError
|
6
|
-
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
7
|
-
end
|
3
|
+
require 'bundler/setup'
|
8
4
|
|
9
5
|
require 'rake'
|
10
6
|
require 'rdoc/task'
|
11
7
|
require 'rspec/core/rake_task'
|
8
|
+
RSpec::Core::RakeTask.new(:spec)
|
9
|
+
|
10
|
+
task :env do
|
11
|
+
$: << File.expand_path('lib', File.dirname(__FILE__))
|
12
|
+
require 'to_spreadsheet'
|
13
|
+
include ToSpreadsheet::Helpers
|
14
|
+
end
|
12
15
|
|
13
|
-
|
16
|
+
task :write_test_xlsx => :env do
|
17
|
+
require 'haml'
|
18
|
+
path = '/tmp/spreadsheet.xlsx'
|
19
|
+
html = Haml::Engine.new(File.read('spec/support/table.html.haml')).render
|
20
|
+
ToSpreadsheet::Axlsx::Renderer.to_package(html).serialize(path)
|
21
|
+
puts "Written to #{path}"
|
22
|
+
end
|
@@ -2,21 +2,27 @@ require 'active_support'
|
|
2
2
|
require 'action_controller/metal/renderers'
|
3
3
|
require 'action_controller/metal/responder'
|
4
4
|
|
5
|
-
require 'to_spreadsheet/
|
5
|
+
require 'to_spreadsheet/axlsx/renderer'
|
6
6
|
|
7
|
-
# This will let us do thing like `render :
|
8
|
-
# This is
|
9
|
-
|
10
|
-
|
11
|
-
|
7
|
+
# This will let us do thing like `render :xlsx => 'index'`
|
8
|
+
# This is similar to how Rails internally implements its :json and :xml renderers
|
9
|
+
ActionController::Renderers.add :xlsx do |template, options|
|
10
|
+
filename = options[:filename] || options[:template] || 'data'
|
11
|
+
|
12
|
+
html = with_context ToSpreadsheet.context.derive do
|
13
|
+
render_to_string(options[:template], options)
|
14
|
+
end
|
15
|
+
|
16
|
+
data = ToSpreadsheet::Axlsx::Renderer.to_data(html)
|
17
|
+
send_data data, type: :xlsx, disposition: %(attachment; filename="#{filename}.xlsx")
|
12
18
|
end
|
13
19
|
|
14
20
|
class ActionController::Responder
|
15
21
|
# This sets up a default render call for when you do
|
16
22
|
# respond_to do |format|
|
17
|
-
# format.
|
23
|
+
# format.xlsx
|
18
24
|
# end
|
19
|
-
def
|
20
|
-
controller.render :
|
25
|
+
def to_xlsx
|
26
|
+
controller.render xlsx: controller.action_name
|
21
27
|
end
|
22
|
-
end
|
28
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module ToSpreadsheet
|
2
|
+
module Axlsx
|
3
|
+
module Formatter
|
4
|
+
COL_INFO_PROPS = %w(bestFit collapsed customWidth hidden phonetic width).map(&:to_sym)
|
5
|
+
|
6
|
+
def apply_formats(package, context)
|
7
|
+
package.workbook.worksheets.each do |sheet|
|
8
|
+
fmt = context.formats(sheet)
|
9
|
+
fmt.workbook_props.each do |prop, value|
|
10
|
+
package.send(:"#{prop}=", value)
|
11
|
+
end
|
12
|
+
fmt.sheet_props.each do |set, props|
|
13
|
+
apply_props sheet.send(set), props, context
|
14
|
+
end
|
15
|
+
fmt.column_props.each do |v|
|
16
|
+
idx, props = v[0], v[1]
|
17
|
+
apply_props sheet.column_info[0], props.slice(*COL_INFO_PROPS), context
|
18
|
+
props = props.except(*COL_INFO_PROPS)
|
19
|
+
sheet.col_style idx, props if props.present?
|
20
|
+
end
|
21
|
+
fmt.row_props.each do |v|
|
22
|
+
idx, props = v[0], v[1]
|
23
|
+
sheet.row_style idx, props
|
24
|
+
end
|
25
|
+
fmt.range_props.each do |v|
|
26
|
+
range, props = v[0], v[1]
|
27
|
+
apply_props sheet[range], props, context
|
28
|
+
end
|
29
|
+
fmt.css_props.each do |v|
|
30
|
+
css_sel, props = v[0], v[1]
|
31
|
+
context.node_from_entity(sheet).css(css_sel).each do |node|
|
32
|
+
apply_props context.entity_from_node(node), props, context
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
def apply_props(obj, props, context)
|
40
|
+
if props.is_a?(Proc)
|
41
|
+
context.instance_exec(obj, &props)
|
42
|
+
return
|
43
|
+
end
|
44
|
+
|
45
|
+
props = props.dup
|
46
|
+
props.each do |k, v|
|
47
|
+
props[k] = context.instance_exec(obj, &v) if v.is_a?(Proc)
|
48
|
+
end
|
49
|
+
|
50
|
+
props.each do |k, v|
|
51
|
+
next if v.nil?
|
52
|
+
if k == :default_value
|
53
|
+
unless obj.value.present? && !([:integer, :float].include?(obj.type) && obj.value.zero?)
|
54
|
+
obj.type = cell_type_from_value(v)
|
55
|
+
obj.value = v
|
56
|
+
end
|
57
|
+
else
|
58
|
+
setter = :"#{k}="
|
59
|
+
obj.send setter, v if obj.respond_to?(setter)
|
60
|
+
if obj.respond_to?(:cells)
|
61
|
+
obj.cells.each do |cell|
|
62
|
+
cell.send setter, v if cell.respond_to?(setter)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def cell_type_from_value(v)
|
70
|
+
if v.is_a?(Date)
|
71
|
+
:date
|
72
|
+
elsif v.is_a?(Time)
|
73
|
+
:time
|
74
|
+
elsif v.is_a?(TrueClass) || v.is_a?(FalseClass)
|
75
|
+
:boolean
|
76
|
+
elsif v.to_s.match(/\A[+-]?\d+?\Z/) #numeric
|
77
|
+
:integer
|
78
|
+
elsif v.to_s.match(/\A[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?\Z/) #float
|
79
|
+
:float
|
80
|
+
else
|
81
|
+
:string
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'axlsx'
|
2
|
+
require 'to_spreadsheet/axlsx/formatter'
|
3
|
+
module ToSpreadsheet
|
4
|
+
module Axlsx
|
5
|
+
module Renderer
|
6
|
+
include Formatter
|
7
|
+
extend self
|
8
|
+
|
9
|
+
def to_stream(html, context = ToSpreadsheet.context)
|
10
|
+
to_package(html, context).to_stream
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_data(html, context = ToSpreadsheet.context)
|
14
|
+
to_package(html, context).to_stream.read
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_package(html, context = ToSpreadsheet.context)
|
18
|
+
package = build_package(html, context)
|
19
|
+
apply_formats(package, context)
|
20
|
+
# Don't leak memory: clear all dom <-> axslsx associations
|
21
|
+
context.clear_assoc!
|
22
|
+
# Numbers compat
|
23
|
+
package.use_shared_strings = true
|
24
|
+
package
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def build_package(html, context)
|
30
|
+
package = ::Axlsx::Package.new
|
31
|
+
spreadsheet = package.workbook
|
32
|
+
doc = Nokogiri::HTML::Document.parse(html)
|
33
|
+
context.assoc! spreadsheet, doc
|
34
|
+
doc.css('table').each_with_index do |xml_table, i|
|
35
|
+
sheet = spreadsheet.add_worksheet(
|
36
|
+
name: xml_table.css('caption').inner_text.presence || xml_table['name'] || "Sheet #{i + 1}"
|
37
|
+
)
|
38
|
+
context.assoc! sheet, xml_table
|
39
|
+
xml_table.css('tr').each do |row_node|
|
40
|
+
xls_row = sheet.add_row
|
41
|
+
context.assoc! xls_row, row_node
|
42
|
+
row_node.css('th,td').each do |cell_node|
|
43
|
+
xls_col = xls_row.add_cell cell_node.inner_text
|
44
|
+
context.assoc! xls_col, cell_node
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
package
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module ToSpreadsheet
|
2
|
+
class Context
|
3
|
+
# Associating Axlsx entities and Nokogiri nodes
|
4
|
+
module Pairing
|
5
|
+
def assoc!(entity, node)
|
6
|
+
@entity_to_node ||= {}
|
7
|
+
@node_to_entity ||= {}
|
8
|
+
@entity_to_node[entity] = node
|
9
|
+
@node_to_entity[node] = entity
|
10
|
+
end
|
11
|
+
|
12
|
+
def entity_from_node(node)
|
13
|
+
@node_to_entity[node]
|
14
|
+
end
|
15
|
+
|
16
|
+
def node_from_entity(entity)
|
17
|
+
@entity_to_node[entity]
|
18
|
+
end
|
19
|
+
|
20
|
+
def clear_assoc!
|
21
|
+
@entity_to_node = {}
|
22
|
+
@node_to_entity = {}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'to_spreadsheet/context/pairing'
|
2
|
+
require 'to_spreadsheet/formats'
|
3
|
+
|
4
|
+
module ToSpreadsheet
|
5
|
+
class Context
|
6
|
+
include Pairing
|
7
|
+
|
8
|
+
def initialize(wb_options = nil)
|
9
|
+
@formats = []
|
10
|
+
@current_format = Formats.new
|
11
|
+
workbook wb_options if wb_options
|
12
|
+
end
|
13
|
+
|
14
|
+
# Returns a new formats jar for a given sheet
|
15
|
+
def formats(sheet)
|
16
|
+
format = Formats.new
|
17
|
+
@formats.each do |v|
|
18
|
+
sel, fmt = v[0], v[1]
|
19
|
+
format.merge!(fmt) if selects?(sel, sheet)
|
20
|
+
end
|
21
|
+
format
|
22
|
+
end
|
23
|
+
|
24
|
+
# Check if selector matches a given sheet / cell / row
|
25
|
+
def selects?(selector, entity)
|
26
|
+
return true if !selector
|
27
|
+
type, val = selector[0], selector[1]
|
28
|
+
sheet = entity.is_a?(::Axlsx::Workbook) ? entity : (entity.respond_to?(:workbook) ? entity.workbook : entity.worksheet.workbook)
|
29
|
+
doc = node_from_entity(sheet)
|
30
|
+
case type
|
31
|
+
when :css
|
32
|
+
doc.css(val).include?(node_from_entity(entity))
|
33
|
+
when :column
|
34
|
+
return false if entity.is_a?(Axlsx::Row)
|
35
|
+
entity.index == val if entity.is_a?(Axlsx::Cell)
|
36
|
+
when :row
|
37
|
+
return entity.index == val if entity.is_a?(Axlsx::Row)
|
38
|
+
entity.row.index == val if entity.is_a?(Axlsx::Cell)
|
39
|
+
when :range
|
40
|
+
if entity.is_a?(Axlsx::Cell)
|
41
|
+
pos = entity.pos
|
42
|
+
top_left, bot_right = val.split(':').map { |s| Axlsx.name_to_indices(s) }
|
43
|
+
pos[0] >= top_left[0] && pos[0] <= bot_right[0] && pos[1] >= top_left[1] && pos[1] <= bot_right[1]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# current format, used internally
|
49
|
+
attr_accessor :current_format
|
50
|
+
|
51
|
+
# format_xls 'table.zebra' do
|
52
|
+
# format 'td', lambda { |cell| {b: true} if cell.row.even? }
|
53
|
+
# end
|
54
|
+
def format_xls(selector = nil, theme = nil, &block)
|
55
|
+
selector, theme = nil, selector if selector.is_a?(Proc) && !theme
|
56
|
+
add_format(selector, &theme) if theme
|
57
|
+
add_format(selector, &block) if block
|
58
|
+
self
|
59
|
+
end
|
60
|
+
|
61
|
+
# format 'td.b', b: true # bold
|
62
|
+
# format column: 0, width: 50
|
63
|
+
# format 'A1:C30', b: true
|
64
|
+
# Accepted properties: http://rubydoc.info/github/randym/axlsx/Axlsx/Cell
|
65
|
+
# column format also accepts Axlsx columnInfo settings
|
66
|
+
def format(selector = nil, options)
|
67
|
+
options = options.dup
|
68
|
+
selector = extract_selector!(selector, options)
|
69
|
+
add selector[0], selector, options
|
70
|
+
end
|
71
|
+
|
72
|
+
# sheet 'table.landscape', page_setup: { orientation: landscape }
|
73
|
+
def sheet(selector = nil, options)
|
74
|
+
options = options.dup
|
75
|
+
selector = extract_selector!(selector, options)
|
76
|
+
add :sheet, selector, options
|
77
|
+
end
|
78
|
+
|
79
|
+
# default 'td.c', 5
|
80
|
+
def default(selector, value)
|
81
|
+
options = {default_value: value}
|
82
|
+
selector = extract_selector!(selector, options)
|
83
|
+
add selector[0], selector, options
|
84
|
+
end
|
85
|
+
|
86
|
+
def add(setting, selector, value)
|
87
|
+
@current_format[setting] << [selector.try(:[], 1), value] if selector || value
|
88
|
+
end
|
89
|
+
|
90
|
+
def workbook(selector = nil, value)
|
91
|
+
add :package, selector, value
|
92
|
+
end
|
93
|
+
|
94
|
+
def apply(theme = nil, &block)
|
95
|
+
add_format &theme if theme
|
96
|
+
add_format &block if block
|
97
|
+
self
|
98
|
+
end
|
99
|
+
|
100
|
+
def derive
|
101
|
+
derived = dup
|
102
|
+
derived.current_format = derived.current_format.derive
|
103
|
+
derived
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def add_format(sheet_sel = nil, &block)
|
109
|
+
format_was = @current_format
|
110
|
+
@current_format = @current_format.derive
|
111
|
+
instance_eval &block
|
112
|
+
@formats << [extract_selector!(sheet_sel), @current_format]
|
113
|
+
@current_format = format_was
|
114
|
+
end
|
115
|
+
|
116
|
+
def extract_selector!(selector, options = {})
|
117
|
+
if selector
|
118
|
+
if selector =~ /:/ && selector[0].upcase == selector[0]
|
119
|
+
return [:range, selector]
|
120
|
+
else
|
121
|
+
return [:css, selector]
|
122
|
+
end
|
123
|
+
end
|
124
|
+
[:column, :row, :range].each do |key|
|
125
|
+
return [key, options.delete(key)] if options.key?(key)
|
126
|
+
end
|
127
|
+
selector
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module ToSpreadsheet
|
2
|
+
class Formats
|
3
|
+
attr_accessor :styles_by_type
|
4
|
+
attr_writer :sheet_props
|
5
|
+
|
6
|
+
def [](type)
|
7
|
+
(@styles_by_type ||= {})[type.to_sym] ||= []
|
8
|
+
end
|
9
|
+
|
10
|
+
def each
|
11
|
+
@styles_by_type.each do |k, v|
|
12
|
+
yield(k, v)
|
13
|
+
end if @styles_by_type
|
14
|
+
end
|
15
|
+
|
16
|
+
# Sheet props without selectors
|
17
|
+
def sheet_props
|
18
|
+
self[:sheet].map(&:last).inject({}, &:merge)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Workbook props without selectors
|
22
|
+
def workbook_props
|
23
|
+
self[:workbook].map(&:last).inject({}, &:merge)
|
24
|
+
end
|
25
|
+
|
26
|
+
def range_props
|
27
|
+
self[:range]
|
28
|
+
end
|
29
|
+
|
30
|
+
def column_props
|
31
|
+
self[:column]
|
32
|
+
end
|
33
|
+
|
34
|
+
def row_props
|
35
|
+
self[:row]
|
36
|
+
end
|
37
|
+
|
38
|
+
def css_props
|
39
|
+
self[:css]
|
40
|
+
end
|
41
|
+
|
42
|
+
def derive
|
43
|
+
derived = Formats.new
|
44
|
+
each { |type, styles| derived[type].concat(styles) }
|
45
|
+
derived
|
46
|
+
end
|
47
|
+
|
48
|
+
def merge!(other_fmt)
|
49
|
+
other_fmt.each { |type, styles| self[type].concat(styles) }
|
50
|
+
end
|
51
|
+
|
52
|
+
def inspect
|
53
|
+
"Formats(sheet: #@sheet_props, styles: #@styles_by_type)"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module ToSpreadsheet
|
2
|
+
module Helpers
|
3
|
+
|
4
|
+
def to_spreadsheet(selector, &block)
|
5
|
+
context.apply(block)
|
6
|
+
end
|
7
|
+
|
8
|
+
def format_xls(selector = nil, &block)
|
9
|
+
context.format_xls selector, &block
|
10
|
+
end
|
11
|
+
|
12
|
+
def context
|
13
|
+
@context || ToSpreadsheet.context
|
14
|
+
end
|
15
|
+
|
16
|
+
def with_context(context, &block)
|
17
|
+
context_was = self.context
|
18
|
+
@context = context
|
19
|
+
result = block.call
|
20
|
+
@context = context_was
|
21
|
+
result
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -1,2 +1,2 @@
|
|
1
1
|
require 'action_dispatch/http/mime_type'
|
2
|
-
Mime::Type.register "application/vnd.
|
2
|
+
Mime::Type.register "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", :xlsx
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module ToSpreadsheet::Themes
|
2
|
+
module Default
|
3
|
+
::ToSpreadsheet.theme :default do
|
4
|
+
workbook use_autowidth: true
|
5
|
+
sheet page_setup: {
|
6
|
+
fit_to_height: 1,
|
7
|
+
fit_to_width: 1,
|
8
|
+
orientation: :landscape
|
9
|
+
}
|
10
|
+
# Set value type based on CSS class
|
11
|
+
format 'td,th', lambda { |cell|
|
12
|
+
val = cell.value
|
13
|
+
case node_from_entity(cell)[:class]
|
14
|
+
when /decimal|float/
|
15
|
+
cell.type = :float
|
16
|
+
when /num|int/
|
17
|
+
cell.type = :integer
|
18
|
+
when /bool/
|
19
|
+
cell.type = :boolean
|
20
|
+
# Parse (date)times and dates with Chronic and Date.parse
|
21
|
+
when /datetime|time/
|
22
|
+
val = Chronic.parse(val)
|
23
|
+
if val
|
24
|
+
cell.type = :time
|
25
|
+
cell.value = val
|
26
|
+
end
|
27
|
+
when /date/
|
28
|
+
val = (Date.parse(val) rescue val)
|
29
|
+
if val.present?
|
30
|
+
cell.type = :date
|
31
|
+
cell.value = val
|
32
|
+
end
|
33
|
+
end
|
34
|
+
}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -1,3 +1,3 @@
|
|
1
1
|
module ToSpreadsheet
|
2
|
-
VERSION = '0.
|
3
|
-
end
|
2
|
+
VERSION = '1.0.0.rc1'
|
3
|
+
end
|
File without changes
|
data/lib/to_spreadsheet.rb
CHANGED
@@ -2,7 +2,23 @@ require 'nokogiri'
|
|
2
2
|
require 'to_spreadsheet/action_pack_renderers'
|
3
3
|
require 'to_spreadsheet/mime_types'
|
4
4
|
require 'to_spreadsheet/version'
|
5
|
+
require 'to_spreadsheet/helpers'
|
6
|
+
require 'to_spreadsheet/context'
|
5
7
|
|
6
8
|
module ToSpreadsheet
|
9
|
+
class << self
|
10
|
+
attr_accessor :context
|
7
11
|
|
8
|
-
|
12
|
+
def theme(name, &formats)
|
13
|
+
@themes ||= {}
|
14
|
+
if formats
|
15
|
+
@themes[name] = formats
|
16
|
+
else
|
17
|
+
@themes[name]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
require 'to_spreadsheet/themes/default'
|
24
|
+
ToSpreadsheet.context = ToSpreadsheet::Context.new.apply ToSpreadsheet.theme(:default)
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
|
4
|
+
|
5
|
+
describe ToSpreadsheet::Axlsx::Formatter do
|
6
|
+
let :spreadsheet do
|
7
|
+
build_spreadsheet(haml: <<HAML)
|
8
|
+
- format_xls 'table' do
|
9
|
+
- default 'td.price', 100
|
10
|
+
%table
|
11
|
+
%tr
|
12
|
+
%td.price
|
13
|
+
%td.price 50
|
14
|
+
HAML
|
15
|
+
end
|
16
|
+
|
17
|
+
let :row do
|
18
|
+
spreadsheet.workbook.worksheets[0].rows[0]
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'default values' do
|
22
|
+
it 'get set when the cell is empty' do
|
23
|
+
row.cells[0].value.should == 100
|
24
|
+
end
|
25
|
+
it 'does not get set when the cell is not empty' do
|
26
|
+
row.cells[1].value.should == 50
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/spec/format_spec.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ToSpreadsheet::Axlsx::Formatter do
|
4
|
+
let :spreadsheet do
|
5
|
+
build_spreadsheet haml: <<-HAML
|
6
|
+
:ruby
|
7
|
+
format_xls do
|
8
|
+
format column: 0, width: 25
|
9
|
+
format 'tr', color: lambda { |row| 'cccccc' if row.index.odd? }
|
10
|
+
end
|
11
|
+
%table
|
12
|
+
%tr
|
13
|
+
%th
|
14
|
+
%tr
|
15
|
+
%td
|
16
|
+
HAML
|
17
|
+
end
|
18
|
+
|
19
|
+
let(:sheet) { spreadsheet.workbook.worksheets[0] }
|
20
|
+
|
21
|
+
context 'local styles' do
|
22
|
+
it 'sets column width' do
|
23
|
+
sheet.column_info[0].width.should == 25
|
24
|
+
end
|
25
|
+
it 'runs lambdas' do
|
26
|
+
sheet.rows[1].cells[0].color.rgb.should == Axlsx::Color.new(rgb: 'cccccc').rgb
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/spec/types_spec.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ToSpreadsheet::Themes::Default do
|
4
|
+
let :spreadsheet do
|
5
|
+
build_spreadsheet haml: <<-HAML
|
6
|
+
|
7
|
+
%table
|
8
|
+
%tr
|
9
|
+
%td.num 20
|
10
|
+
%td.float 1
|
11
|
+
%td.date 27/05/1991
|
12
|
+
%td.date
|
13
|
+
HAML
|
14
|
+
end
|
15
|
+
|
16
|
+
let :row do
|
17
|
+
spreadsheet.workbook.worksheets[0].rows[0]
|
18
|
+
end
|
19
|
+
|
20
|
+
context 'data types' do
|
21
|
+
it 'num' do
|
22
|
+
row.cells[0].value.should == 20
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'float' do
|
26
|
+
row.cells[1].type.should be :float
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'date' do
|
30
|
+
row.cells[2].type.should be :date
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'empty date' do
|
34
|
+
row.cells[3].type.should_not be :date
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ToSpreadsheet::Axlsx::Renderer do
|
4
|
+
let :spreadsheet do
|
5
|
+
build_spreadsheet haml: <<-HAML
|
6
|
+
%table
|
7
|
+
%table
|
8
|
+
HAML
|
9
|
+
end
|
10
|
+
|
11
|
+
context 'worksheets' do
|
12
|
+
it 'are created 1 per <table>' do
|
13
|
+
spreadsheet.workbook.should have(2).worksheets
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
metadata
CHANGED
@@ -1,19 +1,19 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: to_spreadsheet
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
5
|
-
prerelease:
|
4
|
+
version: 1.0.0.rc1
|
5
|
+
prerelease: 6
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Gleb Mazovetskiy
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-11-10 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
16
|
-
requirement:
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,15 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements:
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
25
30
|
- !ruby/object:Gem::Dependency
|
26
31
|
name: nokogiri
|
27
|
-
requirement:
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
28
33
|
none: false
|
29
34
|
requirements:
|
30
35
|
- - ! '>='
|
@@ -32,10 +37,15 @@ dependencies:
|
|
32
37
|
version: '0'
|
33
38
|
type: :runtime
|
34
39
|
prerelease: false
|
35
|
-
version_requirements:
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
36
46
|
- !ruby/object:Gem::Dependency
|
37
47
|
name: spreadsheet
|
38
|
-
requirement:
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
39
49
|
none: false
|
40
50
|
requirements:
|
41
51
|
- - ! '>='
|
@@ -43,10 +53,15 @@ dependencies:
|
|
43
53
|
version: '0'
|
44
54
|
type: :runtime
|
45
55
|
prerelease: false
|
46
|
-
version_requirements:
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
47
62
|
- !ruby/object:Gem::Dependency
|
48
63
|
name: mocha
|
49
|
-
requirement:
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
50
65
|
none: false
|
51
66
|
requirements:
|
52
67
|
- - ! '>='
|
@@ -54,10 +69,15 @@ dependencies:
|
|
54
69
|
version: '0'
|
55
70
|
type: :development
|
56
71
|
prerelease: false
|
57
|
-
version_requirements:
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
58
78
|
- !ruby/object:Gem::Dependency
|
59
79
|
name: sqlite3-ruby
|
60
|
-
requirement:
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
61
81
|
none: false
|
62
82
|
requirements:
|
63
83
|
- - ! '>='
|
@@ -65,22 +85,38 @@ dependencies:
|
|
65
85
|
version: '0'
|
66
86
|
type: :development
|
67
87
|
prerelease: false
|
68
|
-
version_requirements:
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
69
94
|
description: Rendering spreadsheets from Rails made easy
|
70
95
|
email: glex.spb@gmail.com
|
71
96
|
executables: []
|
72
97
|
extensions: []
|
73
98
|
extra_rdoc_files:
|
74
|
-
- README.
|
99
|
+
- README.textile
|
75
100
|
files:
|
76
|
-
- README.
|
101
|
+
- README.textile
|
77
102
|
- LICENSE
|
78
103
|
- Rakefile
|
104
|
+
- lib/to_spreadsheet/action_pack_renderers.rb
|
105
|
+
- lib/to_spreadsheet/axlsx/formatter.rb
|
106
|
+
- lib/to_spreadsheet/axlsx/renderer.rb
|
107
|
+
- lib/to_spreadsheet/context/pairing.rb
|
108
|
+
- lib/to_spreadsheet/context.rb
|
109
|
+
- lib/to_spreadsheet/formats.rb
|
110
|
+
- lib/to_spreadsheet/helpers.rb
|
79
111
|
- lib/to_spreadsheet/mime_types.rb
|
112
|
+
- lib/to_spreadsheet/themes/default.rb
|
80
113
|
- lib/to_spreadsheet/version.rb
|
81
|
-
- lib/to_spreadsheet/
|
82
|
-
- lib/to_spreadsheet/action_pack_renderers.rb
|
114
|
+
- lib/to_spreadsheet/xlsx.rb
|
83
115
|
- lib/to_spreadsheet.rb
|
116
|
+
- spec/defaults_spec.rb
|
117
|
+
- spec/format_spec.rb
|
118
|
+
- spec/types_spec.rb
|
119
|
+
- spec/worksheets_spec.rb
|
84
120
|
homepage: https://github.com/glebm/to_spreadsheet
|
85
121
|
licenses: []
|
86
122
|
post_install_message:
|
@@ -96,13 +132,18 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
96
132
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
97
133
|
none: false
|
98
134
|
requirements:
|
99
|
-
- - ! '
|
135
|
+
- - ! '>'
|
100
136
|
- !ruby/object:Gem::Version
|
101
|
-
version:
|
137
|
+
version: 1.3.1
|
102
138
|
requirements: []
|
103
139
|
rubyforge_project:
|
104
|
-
rubygems_version: 1.8.
|
140
|
+
rubygems_version: 1.8.24
|
105
141
|
signing_key:
|
106
142
|
specification_version: 3
|
107
143
|
summary: Adds various html -> spreadsheet (xls, odt, etc) renderers to Rails.
|
108
|
-
test_files:
|
144
|
+
test_files:
|
145
|
+
- spec/defaults_spec.rb
|
146
|
+
- spec/format_spec.rb
|
147
|
+
- spec/types_spec.rb
|
148
|
+
- spec/worksheets_spec.rb
|
149
|
+
has_rdoc: true
|
data/README.rdoc
DELETED
@@ -1,63 +0,0 @@
|
|
1
|
-
to_spreadsheet is a gem that lets you render xls from your existing haml/erb views from Rails (>= 3.0).
|
2
|
-
|
3
|
-
= Installation
|
4
|
-
|
5
|
-
Add it to your Gemfile:
|
6
|
-
|
7
|
-
gem 'to_spreadsheet'
|
8
|
-
|
9
|
-
= Usage
|
10
|
-
|
11
|
-
In your controller:
|
12
|
-
|
13
|
-
class MyThingiesController < ApplicationController
|
14
|
-
respond_to :xls, :html
|
15
|
-
|
16
|
-
def index
|
17
|
-
@my_thingies = MyThingie.all
|
18
|
-
respond_with(@my_thingies)
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
In your view partial:
|
23
|
-
|
24
|
-
# _my_thingie.haml
|
25
|
-
%table
|
26
|
-
%caption My thingies
|
27
|
-
%thead
|
28
|
-
%tr
|
29
|
-
%td ID
|
30
|
-
%td Name
|
31
|
-
%tbody
|
32
|
-
- my_thingies.each do |thingie|
|
33
|
-
%tr
|
34
|
-
%td.number= thingie.id
|
35
|
-
%td= thingie.name
|
36
|
-
%tfoot
|
37
|
-
%tr
|
38
|
-
%td(colspan="2") #{my_thingies.length}
|
39
|
-
|
40
|
-
In your index.xls.haml:
|
41
|
-
|
42
|
-
= render 'my_thingies', :my_thingies => @my_thingies
|
43
|
-
|
44
|
-
In your index.html.haml:
|
45
|
-
|
46
|
-
= link_to 'Download XLS', my_thingies_url(:format => :xls)
|
47
|
-
= render 'my_thingies', :my_thingies => @my_thingies
|
48
|
-
|
49
|
-
== Formatting
|
50
|
-
|
51
|
-
You can use class names on td/th for typed values. Here is the list of class to type mapping:
|
52
|
-
|
53
|
-
|_ Class |_ Format |
|
54
|
-
| /decimal|float/ | Decimal |
|
55
|
-
| /num|int/ | Integer |
|
56
|
-
| /datetime/ | DateTime |
|
57
|
-
| /date/ | Date |
|
58
|
-
| /time/ | Time |
|
59
|
-
|
60
|
-
== Worksheets
|
61
|
-
|
62
|
-
Every table in the view will be converted to a separate sheet.
|
63
|
-
The sheet title will be assigned to the value of the table's <caption> element if it exists.
|
data/lib/to_spreadsheet/xls.rb
DELETED
@@ -1,42 +0,0 @@
|
|
1
|
-
module ToSpreadsheet
|
2
|
-
require 'spreadsheet'
|
3
|
-
module XLS
|
4
|
-
extend self
|
5
|
-
|
6
|
-
def to_io(html)
|
7
|
-
spreadsheet = Spreadsheet::Workbook.new
|
8
|
-
Nokogiri::HTML::Document.parse(html).css('table').each_with_index do |xml_table, i|
|
9
|
-
sheet = spreadsheet.create_worksheet(:name => xml_table.css('caption').inner_text.presence || "Sheet #{i + 1}")
|
10
|
-
xml_table.css('tr').each_with_index do |row_node, row|
|
11
|
-
row_node.css('th,td').each_with_index do |col_node, col|
|
12
|
-
sheet[row, col] = typed_node_val(col_node)
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
16
|
-
io = StringIO.new
|
17
|
-
spreadsheet.write(io)
|
18
|
-
io.rewind
|
19
|
-
io
|
20
|
-
end
|
21
|
-
|
22
|
-
private
|
23
|
-
|
24
|
-
def typed_node_val(node)
|
25
|
-
val = node.inner_text
|
26
|
-
case node[:class]
|
27
|
-
when /decimal|float/
|
28
|
-
val.to_f
|
29
|
-
when /num|int/
|
30
|
-
val.to_i
|
31
|
-
when /datetime/
|
32
|
-
DateTime.parse(val)
|
33
|
-
when /date/
|
34
|
-
Date.parse(val)
|
35
|
-
when /time/
|
36
|
-
Time.parse(val)
|
37
|
-
else
|
38
|
-
val
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|