mochigome 0.1.5 → 0.1.6
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +7 -0
- data/Gemfile.lock +15 -0
- data/TODO +3 -0
- data/lib/mochigome_ver.rb +1 -1
- data/lib/model_extensions.rb +1 -1
- data/lib/query.rb +6 -2
- data/test/app_root/app/controllers/application_controller.rb +15 -0
- data/test/app_root/app/controllers/report_controller.rb +388 -0
- data/test/app_root/app/helpers/application_helper.rb +8 -0
- data/test/app_root/app/models/product.rb +2 -2
- data/test/app_root/app/models/sale.rb +1 -1
- data/test/app_root/app/models/store.rb +4 -0
- data/test/app_root/app/transforms/report.fo.via-html.xslt.haml +173 -0
- data/test/app_root/app/transforms/report.html.xslt.haml +198 -0
- data/test/app_root/app/views/layouts/application.html.haml +15 -0
- data/test/app_root/app/views/report/edit.html.haml +56 -0
- data/test/app_root/app/views/report/show.html.haml +6 -0
- data/test/app_root/config/database.yml +7 -0
- data/test/app_root/config/environments/development.rb +17 -0
- data/test/app_root/config/initializers/mime_types.rb +8 -0
- data/test/app_root/config/routes.rb +3 -2
- data/test/app_root/db/development.sqlite3 +0 -0
- data/test/app_root/lib/apache_fop.rb +151 -0
- data/test/app_root/public/index.html +14 -0
- data/test/app_root/public/javascripts/prototype.js +6084 -0
- data/test/app_root/public/javascripts/report_edit.js +153 -0
- data/test/app_root/public/stylesheets/common.css +183 -0
- data/test/app_root/vendor/plugins/mochigome/init.rb +3 -1
- data/test/server.rb +3 -0
- data/test/test_helper.rb +1 -1
- data/test/unit/query_test.rb +12 -3
- metadata +20 -5
- data/test/app_root/app/controllers/owners_controller.rb +0 -2
data/Gemfile
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
source :rubygems
|
2
2
|
|
3
|
+
# Required by the plugin itself
|
3
4
|
gem 'rails', '2.3.12'
|
4
5
|
gem 'arel', '~> 2.1'
|
5
6
|
gem 'nokogiri'
|
6
7
|
gem 'ruport'
|
7
8
|
gem 'rgl'
|
8
9
|
|
10
|
+
# Used in the test suite and/or demo app
|
9
11
|
gem 'sqlite3'
|
10
12
|
gem 'mysql2', '~> 0.2.0'
|
11
13
|
gem 'factory_girl', '2.0.4'
|
@@ -17,3 +19,8 @@ gem 'mynyml-redgreen'
|
|
17
19
|
gem 'rev'
|
18
20
|
gem 'watchr'
|
19
21
|
gem 'autowatchr'
|
22
|
+
gem 'haml'
|
23
|
+
gem 'googlecharts', :require => 'gchart'
|
24
|
+
gem "simple_xlsx_writer", :require => "simple_xlsx"
|
25
|
+
gem 'xslt-morpheus', '0.1', :require => "morpheus"
|
26
|
+
gem 'fastercsv'
|
data/Gemfile.lock
CHANGED
@@ -16,7 +16,10 @@ GEM
|
|
16
16
|
watchr
|
17
17
|
color (1.4.1)
|
18
18
|
factory_girl (2.0.4)
|
19
|
+
fast_xs (0.8.0)
|
19
20
|
fastercsv (1.5.4)
|
21
|
+
googlecharts (1.6.8)
|
22
|
+
haml (3.1.4)
|
20
23
|
hoe (2.12.0)
|
21
24
|
rake (~> 0.8)
|
22
25
|
iobuffer (1.0.0)
|
@@ -45,15 +48,22 @@ GEM
|
|
45
48
|
rake
|
46
49
|
stream (>= 0.5)
|
47
50
|
ruby-prof (0.10.8)
|
51
|
+
rubyzip (0.9.4)
|
48
52
|
ruport (1.6.3)
|
49
53
|
fastercsv
|
50
54
|
pdf-writer (= 1.1.8)
|
55
|
+
simple_xlsx_writer (0.5.3)
|
56
|
+
fast_xs (>= 0.7.3)
|
57
|
+
rubyzip (>= 0.9.4)
|
51
58
|
sqlite3 (1.3.4)
|
52
59
|
stream (0.5)
|
53
60
|
term-ansicolor (1.0.6)
|
54
61
|
transaction-simple (1.4.0)
|
55
62
|
hoe (>= 1.1.7)
|
56
63
|
watchr (0.7)
|
64
|
+
xslt-morpheus (0.1)
|
65
|
+
actionpack
|
66
|
+
nokogiri
|
57
67
|
|
58
68
|
PLATFORMS
|
59
69
|
ruby
|
@@ -62,6 +72,9 @@ DEPENDENCIES
|
|
62
72
|
arel (~> 2.1)
|
63
73
|
autowatchr
|
64
74
|
factory_girl (= 2.0.4)
|
75
|
+
fastercsv
|
76
|
+
googlecharts
|
77
|
+
haml
|
65
78
|
minitest
|
66
79
|
mynyml-redgreen
|
67
80
|
mysql2 (~> 0.2.0)
|
@@ -73,5 +86,7 @@ DEPENDENCIES
|
|
73
86
|
rgl
|
74
87
|
ruby-prof
|
75
88
|
ruport
|
89
|
+
simple_xlsx_writer
|
76
90
|
sqlite3
|
77
91
|
watchr
|
92
|
+
xslt-morpheus (= 0.1)
|
data/TODO
CHANGED
@@ -3,3 +3,6 @@
|
|
3
3
|
- If there is more than one association from model A to model B and they're both focusable, pick the one with no conditions. If all the associations have conditions, complain and require that the correct association be manually specified (where "correct" might mean none of them should be valid)
|
4
4
|
- Alternately, always ignore conditional associations unless they're specifically provided to Mochigome by the model
|
5
5
|
- Named subsets of different fields and agg fields on a single model
|
6
|
+
- Some kind of single-page preview on the edit screen would be cool. Maybe use FOP with fake data and the PNG output option?
|
7
|
+
- Allow inwards-branching join patterns on layer list (i.e. Category->Store->Product)
|
8
|
+
- Automatically set default to 0 on sum and count aggregation
|
data/lib/mochigome_ver.rb
CHANGED
data/lib/model_extensions.rb
CHANGED
data/lib/query.rb
CHANGED
@@ -166,18 +166,22 @@ module Mochigome
|
|
166
166
|
if table.is_a? Array
|
167
167
|
fields = data_model.mochigome_aggregation_settings.options[:fields]
|
168
168
|
# Pre-fill the node with all fields in the right order
|
169
|
-
fields.each{|agg| node[agg[:name]] =
|
169
|
+
fields.each{|agg| node[agg[:name]] = agg[:default] unless agg[:hidden] }
|
170
170
|
agg_row = {} # Hold regular aggs here to be used in ruby-based aggs
|
171
171
|
fields.reject{|agg| agg[:in_ruby]}.zip(table).each do |agg, v|
|
172
|
+
v ||= agg[:default]
|
172
173
|
agg_row[agg[:name]] = v
|
173
174
|
node[agg[:name]] = v unless agg[:hidden]
|
174
175
|
end
|
175
176
|
fields.select{|agg| agg[:in_ruby]}.each do |agg|
|
176
177
|
node[agg[:name]] = agg[:ruby_proc].call(agg_row)
|
177
178
|
end
|
179
|
+
node.children.each do |c|
|
180
|
+
insert_aggregate_data_fields(c, [], data_model)
|
181
|
+
end
|
178
182
|
else
|
179
183
|
node.children.each do |c|
|
180
|
-
subtable = table[c[:id]]
|
184
|
+
subtable = table[c[:id]] || []
|
181
185
|
insert_aggregate_data_fields(c, subtable, data_model)
|
182
186
|
end
|
183
187
|
end
|
@@ -1,2 +1,17 @@
|
|
1
1
|
class ApplicationController < ActionController::Base
|
2
|
+
layout "application"
|
3
|
+
|
4
|
+
# Allows the use of ActionView helper methods elsewhere
|
5
|
+
# i.e. help.pluralize(3, "banana")
|
6
|
+
class Helper
|
7
|
+
include Singleton
|
8
|
+
include ActionView::Helpers
|
9
|
+
include ApplicationHelper
|
10
|
+
end
|
11
|
+
def self.help
|
12
|
+
Helper.instance
|
13
|
+
end
|
14
|
+
def help
|
15
|
+
self.class.help
|
16
|
+
end
|
2
17
|
end
|
@@ -0,0 +1,388 @@
|
|
1
|
+
# FIXME This whole controller needs a major cleanup, should really
|
2
|
+
# be largely moved into some base class in Mochigome.
|
3
|
+
|
4
|
+
class ReportController < ApplicationController
|
5
|
+
FOCUS_MODELS = {}
|
6
|
+
HUMAN_FOCUS_MODELS = {}
|
7
|
+
[
|
8
|
+
Owner,
|
9
|
+
Store,
|
10
|
+
Product,
|
11
|
+
Category
|
12
|
+
].each do |m|
|
13
|
+
FOCUS_MODELS[m.name] = m
|
14
|
+
HUMAN_FOCUS_MODELS[m.human_name.titleize] = m.name
|
15
|
+
end
|
16
|
+
|
17
|
+
AGGREGATE_SOURCES = [
|
18
|
+
["Sales", "Sale"],
|
19
|
+
["Product Count", "Product"],
|
20
|
+
["Store Count", "Store"]
|
21
|
+
]
|
22
|
+
|
23
|
+
def show
|
24
|
+
return redirect_to(:action => :edit) if params.size <= 2
|
25
|
+
begin
|
26
|
+
query = setup_query
|
27
|
+
data_node = run_report(query)
|
28
|
+
generate_charts(data_node)
|
29
|
+
output_report(data_node)
|
30
|
+
rescue Mochigome::QueryError => e
|
31
|
+
flash[:alert] = "Sorry, these report settings are not valid: #{e.message}"
|
32
|
+
return redirect_to(params.merge(:action => :edit))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def edit
|
37
|
+
@possible_layer_models = HUMAN_FOCUS_MODELS.map{|k, v| [k, v]}.sort
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def condition_desc(c)
|
43
|
+
cls = c['cls'].strip.constantize
|
44
|
+
cls_name = cls.human_name.titleize
|
45
|
+
|
46
|
+
ref_cls = nil
|
47
|
+
if c['fld'] == cls.primary_key
|
48
|
+
ref_cls = cls
|
49
|
+
else
|
50
|
+
cls.reflections.map(&:last).select{|r| r.belongs_to?}.each do |r|
|
51
|
+
if r.association_foreign_key == c['fld']
|
52
|
+
ref_cls = r.klass
|
53
|
+
break
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
if ref_cls && c['op'] == 'eq'
|
59
|
+
item = ref_cls.find(c['val'].to_i)
|
60
|
+
ref_name = cls_name + (cls == ref_cls ? "" : " - #{ref_cls.human_name}")
|
61
|
+
return "#{ref_name}: #{item.display_s}"
|
62
|
+
else
|
63
|
+
return [
|
64
|
+
cls_name,
|
65
|
+
c['fld'].humanize.downcase.gsub(cls_name.downcase, ''),
|
66
|
+
arel_op_humanize(c['op']),
|
67
|
+
c['val'].to_s
|
68
|
+
].join(" ")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def arel_op_humanize(op)
|
73
|
+
op.
|
74
|
+
gsub('gteq', 'greater than or equal to').
|
75
|
+
gsub('lteq', 'less than or equal to').
|
76
|
+
gsub('eq', 'equal to').
|
77
|
+
gsub('lt', 'less than').
|
78
|
+
gsub('gt', 'greater than').
|
79
|
+
humanize.downcase
|
80
|
+
end
|
81
|
+
|
82
|
+
helper_method :condition_desc, :arel_op_humanize
|
83
|
+
|
84
|
+
def setup_query
|
85
|
+
raise Mochigome::QueryError.new("No layers provided") if @layer_names.empty?
|
86
|
+
layers = @layer_names.map{|n| FOCUS_MODELS[n]}
|
87
|
+
|
88
|
+
aggregate_sources = @aggregate_source_names.map do |s|
|
89
|
+
# Each aggregate source is a focus type paired with a data type
|
90
|
+
r = s.split(":").map(&:strip).map(&:constantize)
|
91
|
+
r.size > 1 ? r : r.first
|
92
|
+
end
|
93
|
+
|
94
|
+
@report_name = "#{layers.last.human_name.pluralize.titleize} Report"
|
95
|
+
unless @aggregate_source_names.empty?
|
96
|
+
@report_name += " : " + @aggregate_source_names.map{|n|
|
97
|
+
n.gsub(":","").underscore.titleize.pluralize
|
98
|
+
}.join(", ")
|
99
|
+
end
|
100
|
+
|
101
|
+
Mochigome::Query.new(layers,
|
102
|
+
:aggregate_sources => aggregate_sources,
|
103
|
+
#:access_filter => cancan_access_filter_proc,
|
104
|
+
:root_name => @report_name
|
105
|
+
)
|
106
|
+
end
|
107
|
+
|
108
|
+
def run_report(query)
|
109
|
+
full_cond = nil
|
110
|
+
@condition_params.each do |c|
|
111
|
+
cls = c['cls'].strip.constantize # TODO : Verify it's AR::Base
|
112
|
+
# TODO: Join cls into the query if it's not already (join on what, though?)
|
113
|
+
raise "No such op #{c['op']}" unless Arel::Predications.instance_methods.include?(c['op'])
|
114
|
+
val = interpret_val(cls, c['fld'], c['val'])
|
115
|
+
cond = Arel::Table.new(cls.table_name)[c['fld']].send(c['op'], val)
|
116
|
+
full_cond = full_cond ? full_cond.and(cond) : cond
|
117
|
+
end
|
118
|
+
query.run(full_cond)
|
119
|
+
end
|
120
|
+
|
121
|
+
def interpret_val(cls, fld, val)
|
122
|
+
if val.is_a?(Array)
|
123
|
+
val.map{|v| interpret_val(cls, fld, v)}
|
124
|
+
else
|
125
|
+
val = val.to_s
|
126
|
+
column = cls.columns_hash[fld] or raise "Can't find column #{fld} in #{cls}"
|
127
|
+
begin
|
128
|
+
case column.type
|
129
|
+
when :boolean then (val.to_i != 0)
|
130
|
+
when :date then Date.parse(val)
|
131
|
+
when :datetime then DateTime.parse(val)
|
132
|
+
when :time then Time.parse(val) # FIXME: Time#parse silently defaults to cur time!
|
133
|
+
when :integer then val.to_i
|
134
|
+
when :string, :text then val
|
135
|
+
else raise "Unknown column type #{column.type} for #{fld} in #{cls}"
|
136
|
+
end
|
137
|
+
rescue ArgumentError
|
138
|
+
raise Mochigome::QueryError.new("Unable to interpret value: #{val}")
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def generate_charts(data_node)
|
144
|
+
@charts = []
|
145
|
+
return if data_node.children.size > 15 || data_node.children.empty?
|
146
|
+
|
147
|
+
@aggregate_source_names.each do |raw_name|
|
148
|
+
agg = AGGREGATE_SOURCES.select{|s| s[1] == raw_name}.first
|
149
|
+
next unless agg
|
150
|
+
|
151
|
+
chart_options = {
|
152
|
+
:type => 'bar',
|
153
|
+
:title => agg[0],
|
154
|
+
# :title_size => 20, # FIXME: Argh, googlecharts gem isn't reliable
|
155
|
+
:size => '720x250',
|
156
|
+
:bg => 'FFFFFF00', # White with fully-transparent alpha
|
157
|
+
:stacked => false,
|
158
|
+
:bar_colors => '859900,b58900,dc322f,268bd2',
|
159
|
+
:axis_with_labels => 'x,y',
|
160
|
+
:class => 'chart',
|
161
|
+
:alt => agg[0] + " Chart",
|
162
|
+
:custom => "chdlp=b|l"
|
163
|
+
}
|
164
|
+
|
165
|
+
data_model = agg[1].split(":").last.constantize
|
166
|
+
agg_fields = data_model.mochigome_aggregation_settings.options[:fields].
|
167
|
+
reject{|f| f[:hidden]}
|
168
|
+
|
169
|
+
# TODO: If there's only one agg field, we can use series names
|
170
|
+
# to go another level down in the report instead.
|
171
|
+
chart_options[:legend] = agg_fields.map{|f| f[:name]}
|
172
|
+
chart_options[:data] = agg_fields.map do |f|
|
173
|
+
data_node.children.map do |n|
|
174
|
+
(n[f[:name]] || "").to_f
|
175
|
+
end
|
176
|
+
end
|
177
|
+
chart_options[:axis_labels] = [data_node.children.map{|n| n.name}]
|
178
|
+
x_gap = [450/(chart_options[:axis_labels][0].size), 70].min
|
179
|
+
chart_options[:bar_width_and_spacing] = [8,2,x_gap]
|
180
|
+
|
181
|
+
min_val = [0, chart_options[:data].flatten.min].min
|
182
|
+
max_val = [100, chart_options[:data].flatten.max].max # FIXME No no no
|
183
|
+
chart_options[:axis_range] = [nil, [min_val, max_val]]
|
184
|
+
chart_options[:min_value] = chart_options[:axis_range][1][0]
|
185
|
+
chart_options[:max_value] = chart_options[:axis_range][1][1]
|
186
|
+
|
187
|
+
if chart_options[:axis_labels][0].size > 7
|
188
|
+
chart_options[:axis_labels][0].reverse!
|
189
|
+
chart_options[:orientation] = 'horizontal'
|
190
|
+
chart_options[:class] += ' horizontal'
|
191
|
+
chart_options[:axis_with_labels] = 'y,x'
|
192
|
+
chart_options[:size] = "350x#{50 + chart_options[:axis_labels][0].size*30}"
|
193
|
+
chart_options[:bar_width_and_spacing][2] = 10
|
194
|
+
end
|
195
|
+
|
196
|
+
@charts << Gchart.new(chart_options).image_tag
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def output_report(data_node)
|
201
|
+
# These instance variables are used by the HTML transform
|
202
|
+
# FIXME: Just do the non-data part of the sidebar in HAML instead
|
203
|
+
# To do that, use seperate transforms for sidebar links and main content
|
204
|
+
@print_path = report_path(params.merge(:format => "pdf", :auto_print => true))
|
205
|
+
@download_paths = []
|
206
|
+
[
|
207
|
+
[:pdf, "PDF Document"],
|
208
|
+
[:xlsx, "Excel 2007 Spreadsheet"],
|
209
|
+
[:csv, "CSV Spreadsheet"],
|
210
|
+
[:xml, "XML Raw Data"]
|
211
|
+
].each do |ext, name|
|
212
|
+
@download_paths << {
|
213
|
+
:path => report_path(params.merge(
|
214
|
+
:format => ext,
|
215
|
+
:auto_print => false,
|
216
|
+
:download => true
|
217
|
+
)),
|
218
|
+
:name => name,
|
219
|
+
:ext => ext
|
220
|
+
}
|
221
|
+
end
|
222
|
+
|
223
|
+
transform_opts = {
|
224
|
+
:src_type => "report",
|
225
|
+
:context => self
|
226
|
+
}
|
227
|
+
|
228
|
+
filename = "report"
|
229
|
+
respond_to do |format|
|
230
|
+
format.html do
|
231
|
+
@report_html = Morpheus.transform(
|
232
|
+
data_node.to_xml,
|
233
|
+
transform_opts.merge(:tgt_format => "html")
|
234
|
+
).to_html.html_safe
|
235
|
+
render
|
236
|
+
end
|
237
|
+
format.xlsx do
|
238
|
+
send_xlsx(data_node.to_flat_arrays, "#{filename}.xlsx")
|
239
|
+
end
|
240
|
+
format.csv do
|
241
|
+
send_csv(data_node.to_flat_arrays, "#{filename}.csv")
|
242
|
+
end
|
243
|
+
format.pdf do
|
244
|
+
fo_data = Morpheus.transform(
|
245
|
+
data_node.to_xml,
|
246
|
+
transform_opts.merge(:tgt_format => "fo")
|
247
|
+
)
|
248
|
+
send_pdf(fo_data, "#{filename}.pdf", params[:download],
|
249
|
+
:from_url => report_url(params.merge(:format => "html")),
|
250
|
+
:auto_print => !params[:download]
|
251
|
+
)
|
252
|
+
end
|
253
|
+
format.xml do
|
254
|
+
send_data(
|
255
|
+
data_node.to_xml.to_s,
|
256
|
+
:type => "application/xml",
|
257
|
+
:disposition => 'attachment',
|
258
|
+
:filename => "#{filename}.xml"
|
259
|
+
)
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
# If you're using CanCan, this access filter will restrict your report
|
265
|
+
# results by the current user's permissions.
|
266
|
+
def cancan_access_filter_proc
|
267
|
+
af = proc do |cls|
|
268
|
+
r = {}
|
269
|
+
return r unless cls.real_model?
|
270
|
+
rules = current_ability.send(:relevant_rules_for_query, :index, cls)
|
271
|
+
return r if rules.empty?
|
272
|
+
adapter = CanCan::ModelAdapters::ActiveRecordAdapter.new(cls, rules)
|
273
|
+
conditions = adapter.conditions
|
274
|
+
conditions = cls.send(:sanitize_sql, conditions) if conditions.is_a?(Hash)
|
275
|
+
conditions = nil if conditions == "1=1"
|
276
|
+
if conditions
|
277
|
+
r[:condition] =
|
278
|
+
(Arel::Table.new(cls.table_name)[cls.primary_key].eq(nil)).or(
|
279
|
+
Arel::Nodes::SqlLiteral.new("(#{conditions})"))
|
280
|
+
end
|
281
|
+
if adapter.joins
|
282
|
+
r[:join_paths] = pathify_cancan_joins(adapter.joins).map{|p| [cls] + p}
|
283
|
+
end
|
284
|
+
r
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
def pathify_cancan_joins(j)
|
289
|
+
case j
|
290
|
+
when Array then
|
291
|
+
j.map{|i| pathify_cancan_joins(i)}
|
292
|
+
when Hash then
|
293
|
+
j.map{|k,v| pathify_cancan_joins(k) + pathify_cancan_joins(v).flatten}.flatten
|
294
|
+
when Symbol then
|
295
|
+
[j.to_s.classify.constantize]
|
296
|
+
else raise "Invalid cancan join path element #{j.inspect}"
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
# FIXME: Factor the send_X methods below
|
301
|
+
|
302
|
+
def send_pdf(fo_data, filename, download, pdf_options = {})
|
303
|
+
pdf = ApacheFop::generate_pdf(fo_data, pdf_options)
|
304
|
+
begin
|
305
|
+
send_file(
|
306
|
+
pdf.path,
|
307
|
+
:stream => false, # If this was on, temp file would be deleted too early
|
308
|
+
:type => "application/pdf",
|
309
|
+
:filename => filename,
|
310
|
+
:disposition => download ? 'attachment' : 'inline'
|
311
|
+
)
|
312
|
+
ensure
|
313
|
+
# FIXME: To allow use of X-Send-File, use a separate cron task to delete
|
314
|
+
# stale pdf files periodically.
|
315
|
+
pdf.close!
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
def send_xlsx(table, filename)
|
320
|
+
file = Tempfile.new("excel")
|
321
|
+
begin
|
322
|
+
path = file.path
|
323
|
+
file.close!
|
324
|
+
SimpleXlsx::Serializer.new(path) do |bk|
|
325
|
+
bk.add_sheet("Report") do |sheet|
|
326
|
+
table.each do |row|
|
327
|
+
sheet.add_row row
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|
331
|
+
send_file(
|
332
|
+
path,
|
333
|
+
:stream => false, # If this was on, temp file would be deleted too early
|
334
|
+
:type => Mime::XLSX,
|
335
|
+
:filename => filename,
|
336
|
+
:disposition => 'attachment'
|
337
|
+
)
|
338
|
+
ensure
|
339
|
+
# FIXME: To allow use of X-Send-File, use a separate cron task to delete
|
340
|
+
# stale files periodically.
|
341
|
+
File.unlink(path) if File.exists?(path)
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
def send_csv(table, filename)
|
346
|
+
file = Tempfile.new("csv")
|
347
|
+
begin
|
348
|
+
path = file.path
|
349
|
+
file.close!
|
350
|
+
FasterCSV.open(path, "w") do |csv|
|
351
|
+
table.each do |row|
|
352
|
+
csv << row
|
353
|
+
end
|
354
|
+
end
|
355
|
+
send_file(
|
356
|
+
path,
|
357
|
+
:stream => false, # If this was on, temp file would be deleted too early
|
358
|
+
:type => Mime::CSV,
|
359
|
+
:filename => filename,
|
360
|
+
:disposition => 'attachment'
|
361
|
+
)
|
362
|
+
ensure
|
363
|
+
# FIXME: To allow use of X-Send-File, use a separate cron task to delete
|
364
|
+
# stale files periodically.
|
365
|
+
File.unlink(path) if File.exists?(path)
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
|
370
|
+
before_filter :load_params
|
371
|
+
def load_params
|
372
|
+
@layer_names = []
|
373
|
+
if params[:l].is_a?(Array)
|
374
|
+
@layer_names = params[:l].reject(&:blank?).map(&:strip)
|
375
|
+
end
|
376
|
+
|
377
|
+
@condition_params = []
|
378
|
+
if params[:c].is_a?(Hash)
|
379
|
+
@condition_params = params[:c].values.reject(&:blank?)
|
380
|
+
end
|
381
|
+
@condition_descs = @condition_params.map{|c| condition_desc(c)}
|
382
|
+
|
383
|
+
@aggregate_source_names = []
|
384
|
+
if params[:a].is_a?(Array)
|
385
|
+
@aggregate_source_names = params[:a].reject(&:blank?)
|
386
|
+
end
|
387
|
+
end
|
388
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
module ApplicationHelper
|
2
|
+
# Creates an xsl xpath match attribute for a tag with the given HTML class
|
3
|
+
# http://pivotallabs.com/users/alex/blog/articles/427-xpath-css-class-matching
|
4
|
+
# TODO: Move this into Morpheus
|
5
|
+
def xpath_class(cls)
|
6
|
+
"contains(concat(' ',normalize-space(@class),' '),' #{cls} ')"
|
7
|
+
end
|
8
|
+
end
|
@@ -13,8 +13,8 @@ class Product < ActiveRecord::Base
|
|
13
13
|
)
|
14
14
|
]}
|
15
15
|
]
|
16
|
-
a.hidden_fields [ {"Secret count" => :count} ]
|
17
|
-
a.fields_in_ruby [ {"Count squared" => lambda{|row| row["Secret count"]
|
16
|
+
a.hidden_fields [ {"Secret count" => [:count]} ]
|
17
|
+
a.fields_in_ruby [ {"Count squared" => lambda{|row| row["Secret count"].try(:**, 2)}} ]
|
18
18
|
end
|
19
19
|
|
20
20
|
belongs_to :category
|