to_spreadsheet 0.9.3 → 1.0.0.rc1
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.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
|