google_data_source 0.7.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. data/.document +5 -0
  2. data/.gitignore +7 -0
  3. data/Gemfile +11 -0
  4. data/LICENSE.txt +20 -0
  5. data/README.rdoc +25 -0
  6. data/Rakefile +31 -0
  7. data/google_data_source.gemspec +32 -0
  8. data/lib/assets/images/google_data_source/chart_bar_add.png +0 -0
  9. data/lib/assets/images/google_data_source/chart_bar_delete.png +0 -0
  10. data/lib/assets/images/google_data_source/loader.gif +0 -0
  11. data/lib/assets/javascripts/google_data_source/data_source_init.js +3 -0
  12. data/lib/assets/javascripts/google_data_source/extended_data_table.js +76 -0
  13. data/lib/assets/javascripts/google_data_source/filter_form.js +180 -0
  14. data/lib/assets/javascripts/google_data_source/google_visualization/combo_table.js.erb +113 -0
  15. data/lib/assets/javascripts/google_data_source/google_visualization/table.js +116 -0
  16. data/lib/assets/javascripts/google_data_source/google_visualization/timeline.js +13 -0
  17. data/lib/assets/javascripts/google_data_source/google_visualization/visualization.js.erb +141 -0
  18. data/lib/assets/javascripts/google_data_source/index.js +7 -0
  19. data/lib/dummy_engine.rb +5 -0
  20. data/lib/google_data_source.rb +33 -0
  21. data/lib/google_data_source/base.rb +281 -0
  22. data/lib/google_data_source/column.rb +31 -0
  23. data/lib/google_data_source/csv_data.rb +23 -0
  24. data/lib/google_data_source/data_date.rb +17 -0
  25. data/lib/google_data_source/data_date_time.rb +17 -0
  26. data/lib/google_data_source/helper.rb +69 -0
  27. data/lib/google_data_source/html_data.rb +6 -0
  28. data/lib/google_data_source/invalid_data.rb +14 -0
  29. data/lib/google_data_source/json_data.rb +78 -0
  30. data/lib/google_data_source/railtie.rb +36 -0
  31. data/lib/google_data_source/sql/models.rb +266 -0
  32. data/lib/google_data_source/sql/parser.rb +239 -0
  33. data/lib/google_data_source/sql_parser.rb +82 -0
  34. data/lib/google_data_source/template_handler.rb +31 -0
  35. data/lib/google_data_source/test_helper.rb +26 -0
  36. data/lib/google_data_source/version.rb +3 -0
  37. data/lib/google_data_source/xml_data.rb +25 -0
  38. data/lib/locale/de.yml +5 -0
  39. data/lib/reporting/action_controller_extension.rb +19 -0
  40. data/lib/reporting/grouped_set.rb +58 -0
  41. data/lib/reporting/helper.rb +110 -0
  42. data/lib/reporting/reporting.rb +352 -0
  43. data/lib/reporting/reporting_adapter.rb +27 -0
  44. data/lib/reporting/reporting_entry.rb +147 -0
  45. data/lib/reporting/sql_reporting.rb +220 -0
  46. data/test/lib/empty_reporting.rb +2 -0
  47. data/test/lib/test_reporting.rb +33 -0
  48. data/test/lib/test_reporting_b.rb +9 -0
  49. data/test/lib/test_reporting_c.rb +3 -0
  50. data/test/locales/en.models.yml +6 -0
  51. data/test/locales/en.reportings.yml +5 -0
  52. data/test/rails/reporting_renderer_test.rb +47 -0
  53. data/test/test_helper.rb +50 -0
  54. data/test/units/base_test.rb +340 -0
  55. data/test/units/csv_data_test.rb +36 -0
  56. data/test/units/grouped_set_test.rb +60 -0
  57. data/test/units/json_data_test.rb +68 -0
  58. data/test/units/reporting_adapter_test.rb +20 -0
  59. data/test/units/reporting_entry_test.rb +149 -0
  60. data/test/units/reporting_test.rb +374 -0
  61. data/test/units/sql_parser_test.rb +111 -0
  62. data/test/units/sql_reporting_test.rb +307 -0
  63. data/test/units/xml_data_test.rb +32 -0
  64. metadata +286 -0
@@ -0,0 +1,110 @@
1
+ module GoogleDataSource
2
+ module Reporting
3
+ module Helper
4
+ # Shows a reporting consisting of a visualization and a form for configuration
5
+ # +type+ can be either
6
+ # * 'Table'
7
+ # * 'TimeLine'
8
+ # +reporting+ is the +Reporting+ object to be displayed
9
+ # Have a look at +google_visualization+ helper for +url+ and +options+ parameter
10
+ def google_reporting(type, reporting, url = nil, options = {})
11
+ # no form
12
+ return google_visualization(type, url, options) unless options.delete(:form)
13
+
14
+ # form
15
+ # default options
16
+ options = {
17
+ :form => reporting_form_id(reporting),
18
+ :autosubmit => true,
19
+ :form_position => :bottom
20
+ }.update(options)
21
+
22
+ visualization_html = google_visualization(type, url, options)
23
+
24
+ form_html = content_tag("form", :id => reporting_form_id(reporting), :class => 'formtastic') do
25
+ render options[:form_partial] || reporting_form_partial(reporting)
26
+ end
27
+
28
+ options[:form_position] == :bottom ? visualization_html << form_html : form_html << visualization_html
29
+ end
30
+
31
+ # Shows a timeline reporting
32
+ def google_reporting_timeline(reporting, url = nil, options = {})
33
+ google_reporting('TimeLine', reporting, url, options)
34
+ end
35
+
36
+ # Shows a table reporting
37
+ def google_reporting_table(reporting, url = nil, options = {})
38
+ google_reporting('Table', reporting, url, options)
39
+ end
40
+
41
+ # Shows a combo table reporting
42
+ def google_reporting_combo_table(reporting, url = nil, options = {})
43
+ google_reporting('ComboTable', reporting, url, options)
44
+ end
45
+
46
+ # Returns callback JS code that sends the rendered form partial
47
+ # So validation errors can be displayed
48
+ def form_render_callback(reporting, options = {})
49
+ out = "$('##{reporting_form_id(reporting)}').html(#{render(options[:form_partial] || reporting_form_partial(reporting), :reporting => reporting).to_json});"
50
+ out << '$(document).trigger("formRendered");'
51
+ out
52
+ end
53
+
54
+ # Shows a select tag for grouping selection on a given reporting
55
+ # TODO more docu
56
+ # TODO really take namespace from classname?
57
+ def reporting_group_by_select(reporting, select_options, i = 1, options = {})
58
+ select_options = reporting_options_for_select(reporting, select_options, options)
59
+
60
+ tag_name = "#{reporting.class.name.underscore}[groupby(#{i}i)]"
61
+ current_option = (reporting.group_by.size < i) ? nil : reporting.group_by[i-1]
62
+ option_tags = options_for_select(select_options, current_option)
63
+ select_tag(tag_name, option_tags, options)
64
+ end
65
+
66
+ # Shows a Multiselect box for the columns to 'select'
67
+ def reporting_select_select(reporting, select_options, options = {})
68
+ select_options = reporting_options_for_select(reporting, select_options, options)
69
+
70
+ tag_name = "#{reporting.class.name.underscore}[select]"
71
+ option_tags = options_for_select(select_options, reporting.select)
72
+ select_tag(tag_name, option_tags, :multiple => true)
73
+ end
74
+
75
+ # Adds labels to the select options when columns are passed in
76
+ def reporting_options_for_select(reporting, select_options, options = {})
77
+ if (select_options.is_a?(Array))
78
+ select_options = select_options.collect { |column| [reporting.column_label(column), column] }
79
+ select_options.unshift('') if options.delete(:include_blank)
80
+ end
81
+ select_options
82
+ end
83
+
84
+ # Registers form subit hooks
85
+ # This way the standard form serialization can be overwritten
86
+ def reporting_form_hooks(reporting)
87
+ hooks = OpenStruct.new
88
+ yield(hooks)
89
+
90
+ json = []
91
+ %w(select).each do |hook|
92
+ next if hooks.send(hook).nil?
93
+ json << "#{hook}: function(){#{hooks.send hook}}"
94
+ end
95
+ js = "DataSource.FilterForm.setHooks(#{reporting_form_id(reporting).to_json}, {#{json.join(', ')}});"
96
+ javascript_tag(js)
97
+ end
98
+
99
+ # Returns the standard DOM id for reporting forms
100
+ def reporting_form_id(reporting)
101
+ "#{reporting.id}_form"
102
+ end
103
+
104
+ # Returns the standard partial for reporting forms
105
+ def reporting_form_partial(reporting)
106
+ "#{reporting.id}_form.html"
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,352 @@
1
+ class CircularDependencyException < Exception; end
2
+
3
+ # Super class for reportings to be rendered with Google visualizations
4
+ # Encapsulates the aggregation of the reporting data based on a configuration.
5
+ #
6
+ # The configuration is implemented by inherting from +ActiveRecord+ and switching off
7
+ # the database stuff. Thus the regular +ActiveRecord+ validators and parameter assignments
8
+ # including type casting cab be used
9
+ #
10
+ # The ActiveRecord extension is copied from the ActiveForm plugin (http://github.com/remvee/active_form)
11
+ class Reporting < ActiveRecord::Base
12
+ attr_accessor :query, :group_by, :select, :order_by, :limit, :offset
13
+ attr_reader :virtual_columns #, :required_columns
14
+ attr_writer :id
15
+
16
+ class_attribute :datasource_filters,
17
+ :datasource_columns,
18
+ :datasource_defaults,
19
+ {:instance_reader => false, :instance_writer => false}
20
+
21
+ def initialize(*args)
22
+ @required_columns = []
23
+ @virtual_columns = {}
24
+ super(*args)
25
+ end
26
+
27
+ # Returns an instance of our own connection adapter
28
+ #
29
+ def self.connection
30
+ @adapter ||= ActiveRecord::ConnectionAdapters::ReportingAdapter.new
31
+ end
32
+
33
+ # Returns an ID which is used by frontend components to generate unique dom ids
34
+ # Defaults to the underscoreized classname
35
+ def id
36
+ @id || self.class.name.underscore.split('/').last #gsub('/', '_')
37
+ end
38
+
39
+ # helper method used by required_columns
40
+ # Returns all columns required by a certain column resolving the dependencies recursively
41
+ def required_columns_for(column, start = nil)
42
+ return [] unless self.datasource_columns.has_key?(column)
43
+ raise CircularDependencyException.new("Column #{start} has a circular dependency") if column.to_sym == start
44
+ columns = [ self.datasource_columns[column][:requires] ].flatten.compact.collect(&:to_s)
45
+ columns.collect { |c| [c, required_columns_for(c, start || column.to_sym)] }.flatten
46
+ end
47
+
48
+ # Returns the columns that have to be selected
49
+ def required_columns
50
+ (select + group_by + @required_columns).inject([]) do |columns, column|
51
+ columns << required_columns_for(column)
52
+ columns << column
53
+ end.flatten.map(&:to_s).uniq
54
+ end
55
+
56
+ # Adds required columns
57
+ def add_required_columns(*columns)
58
+ @required_columns = (@required_columns + columns.flatten.collect(&:to_s)).uniq
59
+ end
60
+
61
+ # 'Abstract' method that has to be overridden by subclasses
62
+ # Returns an array of rows written to #rows and accessible in DataSource compatible format in #rows_as_datasource
63
+ #
64
+ def aggregate
65
+ []
66
+ end
67
+
68
+ # Returns the data rows
69
+ # Calls the aggregate method first if rows do not exist
70
+ #
71
+ def data(options = {})
72
+ add_required_columns(options[:required_columns])
73
+ @rows ||= aggregate
74
+ end
75
+
76
+ # Lazy getter for the columns object
77
+ def columns
78
+ select.inject([]) do |columns, column|
79
+ columns << {
80
+ :type => all_columns[column][:type]
81
+ }.merge({
82
+ :id => column.to_s,
83
+ :label => column_label(column)
84
+ }) if all_columns.key?(column)
85
+
86
+ columns
87
+ end
88
+ end
89
+
90
+ # Retrieves the I18n translation of the column label
91
+ def column_label(column, default = nil)
92
+ return '' if column.blank?
93
+ defaults = ['reportings.{{model}}.{{column}}', 'models.attributes.{{model}}.{{column}}'].collect do |scope|
94
+ scope.gsub!('{{model}}', self.class.name.underscore.gsub('/', '.'))
95
+ scope.gsub('{{column}}', column.to_s)
96
+ end.collect(&:to_sym)
97
+ defaults << column.to_s.humanize
98
+ I18n.t(defaults.shift, :default => defaults)
99
+ end
100
+
101
+ # Returns the select columns as array
102
+ def select
103
+ (@select ||= (defaults[:select] || [])).collect { |c| c == '*' ? all_columns.keys : c }.flatten
104
+ end
105
+
106
+ # Returns the grouping columns as array
107
+ def group_by
108
+ @group_by ||= (defaults[:group_by] || [])
109
+ end
110
+
111
+ # add a virtual column
112
+ def add_virtual_column(name, type = :string)
113
+ virtual_columns[name.to_s] = {
114
+ :type => type
115
+ }
116
+ end
117
+
118
+ # Returns a list of all columns (real and virtual)
119
+ def all_columns
120
+ datasource_columns.merge(virtual_columns)
121
+ end
122
+
123
+ # Returns an array of columns which are allowed for grouping
124
+ #
125
+ def groupable_columns
126
+ datasource_columns.collect do |key, options|
127
+ key.to_sym if options[:grouping]
128
+ end.compact
129
+ end
130
+
131
+ # Returns the +defaults+ Hash
132
+ # Convenience wrapper for instance access
133
+ def defaults
134
+ self.class.defaults #.merge(@defaults || {})
135
+ end
136
+
137
+ # Attribute reader for datasource_columns
138
+ def datasource_columns
139
+ self.class.datasource_columns || { }.freeze
140
+ end
141
+
142
+ # Returns a serialized representation of the reporting
143
+ def serialize
144
+ to_param.to_json
145
+ end
146
+
147
+ def to_param # :nodoc:
148
+ attributes.merge({
149
+ :select => select,
150
+ :group_by => group_by,
151
+ :order_by => order_by,
152
+ :limit => limit,
153
+ :offset => offset
154
+ })
155
+ end
156
+
157
+ def limit
158
+ (@limit || 1000).to_i
159
+ end
160
+
161
+ # Returns the serialized Reporting in a Hash that can be used for links
162
+ # and which is deserialized by from_params
163
+ def to_params(key = self.class.name.underscore.gsub('/', '_'))
164
+ HashWithIndifferentAccess.new( key => to_param )
165
+ end
166
+
167
+ # Returns true if the given column is available for filtering
168
+ #
169
+ def filterable_by?(column_name)
170
+ self.class.datasource_filters.key?(column_name.to_s)
171
+ end
172
+
173
+ class << self
174
+ # Defines a displayable column of the datasource
175
+ # Type defaults to string
176
+ def column(name, options = {})
177
+ self.datasource_columns ||= Hash.new.freeze
178
+ self.datasource_columns = column_or_filter(name, options, self.datasource_columns)
179
+ end
180
+
181
+ # Marks a column as filterable / adds it in the background
182
+ #
183
+ #
184
+ def filter(name, options = {})
185
+ self.datasource_filters ||= Hash.new.freeze
186
+
187
+ # update the column options
188
+ self.datasource_filters = column_or_filter(name, options, self.datasource_filters)
189
+ end
190
+
191
+ # Returns the defaults class variable
192
+ def defaults
193
+ self.datasource_defaults ||= Hash.new.freeze
194
+ end
195
+
196
+ # Sets the default value for select
197
+ def select_default(select)
198
+ self.datasource_defaults = defaults.merge({:select => select})
199
+ end
200
+
201
+ # Sets the default value for group_by
202
+ def group_by_default(group_by)
203
+ self.datasource_defaults = defaults.merge({:group_by => group_by})
204
+ end
205
+
206
+ # Returns a reporting from a serialized representation
207
+ def deserialize(value)
208
+ self.new(JSON.parse(value))
209
+ end
210
+
211
+ # Uses the +simple_parse+ method of the SqlParser to setup a reporting
212
+ # from a query. The where clause is intepreted as reporting configuration (activerecord attributes)
213
+ def from_params(params, key = self.name.underscore.gsub('/', '_'))
214
+ return self.deserialize(params[key]) if params.has_key?(key) && params[key].is_a?(String)
215
+
216
+ reporting = self.new(params[key])
217
+ reporting = from_query_params(params["query"]) if params.has_key?("query")
218
+ return reporting unless params.has_key?(:tq)
219
+
220
+ query = GoogleDataSource::DataSource::SqlParser.simple_parse(params[:tq])
221
+ attributes = Hash.new
222
+ query.conditions.each do |k, v|
223
+ if v.is_a?(Array)
224
+ v.each do |condition|
225
+ case condition.op
226
+ when '<='
227
+ attributes["to_#{k}"] = condition.value
228
+ when '>='
229
+ attributes["from_#{k}"] = condition.value
230
+ when 'in'
231
+ attributes["in_#{k}"] = condition.value
232
+ else
233
+ # raise exception for unsupported operator?
234
+ end
235
+ end
236
+ else
237
+ attributes[k] = v
238
+ end
239
+ end
240
+ attributes[:group_by] = query.groupby
241
+ attributes[:select] = query.select
242
+ attributes[:order_by] = query.orderby
243
+ attributes[:limit] = query.limit
244
+ attributes[:offset] = query.offset
245
+ attributes.merge!(params[key]) if params.has_key?(key)
246
+ #reporting.update_attributes(attributes)
247
+ reporting.attributes = attributes
248
+ reporting.query = params[:tq]
249
+ reporting
250
+ end
251
+
252
+ ############################
253
+ # ActiveRecord overrides
254
+ ############################
255
+ # Needed to build column accessors
256
+ #
257
+ def columns_hash
258
+ @columns_hash ||= begin
259
+ ret = { }
260
+ data = { }
261
+ data.update(self.datasource_columns) if self.datasource_columns
262
+ data.update(self.datasource_filters) if self.datasource_filters
263
+
264
+ data.each do |k, v|
265
+ ret[k.to_s] = ActiveRecord::ConnectionAdapters::Column.new(k.to_s,
266
+ v[:default],
267
+ v[:type].to_s,
268
+ v.key?(:null) ? v[:null] : true)
269
+
270
+ # define a humanize method which returns the human name
271
+ if v.key?(:human_name)
272
+ ret[k.to_s].define_singleton_method(:humanize) do
273
+ v[:human_name]
274
+ end
275
+ end
276
+ end
277
+
278
+ ret
279
+ end
280
+ end
281
+
282
+ # Returns a hash with column name as key and default value as value
283
+ #
284
+ def column_defaults(*args)
285
+ columns_hash.keys.inject({}) do |mem, var|
286
+ mem[var] = columns_hash[var].default
287
+ mem
288
+ end
289
+ end
290
+
291
+ # TODO: merge with columns hash
292
+ def columns
293
+ columns_hash.values
294
+ end
295
+
296
+ def primary_key
297
+ 'id'
298
+ end
299
+
300
+ def abstract_class # :nodoc:
301
+ true
302
+ end
303
+
304
+ protected
305
+ # Used to decorate a filter or column
306
+ # used by #column and #filter internally
307
+ #
308
+ def column_or_filter(name, options, registry)
309
+ name = name.to_s
310
+
311
+ # whatever this is.
312
+ # options.each { |k,v| options[k] = v.to_s if Symbol === v }
313
+
314
+ default_options = { :type => :string }
315
+
316
+ new_entry = { name => (registry[name] || {}) }
317
+ new_entry[name].reverse_merge!(default_options.merge(options))
318
+ new_entry.freeze
319
+
320
+ # frozen, to prevent modifications on class_attribute
321
+ registry.merge(new_entry).freeze
322
+ end
323
+
324
+ def from_query_params(query)
325
+ page = query.delete('page')
326
+ reporting = self.new(query)
327
+ reporting.limit = query['limit']
328
+ if page
329
+ reporting.offset = reporting.limit * (page.to_i - 1)
330
+ else
331
+ reporting.offset = query['offset'].to_i
332
+ end
333
+
334
+ reporting
335
+ end
336
+ end
337
+
338
+ ############################
339
+ # ActiveRecord overrides
340
+ ############################
341
+
342
+ def save # :nodoc:
343
+ if result = valid?
344
+ end
345
+
346
+ result
347
+ end
348
+
349
+ def save! # :nodoc:
350
+ save or raise ActiveRecord::RecordInvalid.new(self)
351
+ end
352
+ end