google_data_source 0.7.6

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