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,27 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ # This connection adapter does the quoting the way our reporting class should generate it
4
+ #
5
+ #
6
+ class ReportingAdapter < AbstractAdapter
7
+
8
+ def initialize
9
+ super(nil)
10
+ end
11
+
12
+ def quoted_true
13
+ "'1'"
14
+ end
15
+
16
+ def quoted_false
17
+ "'0'"
18
+ end
19
+
20
+ # Quotes a string, escaping any ' (single quote) and \ (backslash)
21
+ # characters.
22
+ def quote_string(s)
23
+ s.gsub(/\\/, '\&\&').gsub(/'/, "\\\\'") # ' (for ruby-mode)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,147 @@
1
+ # This class represents a single reporting entry.
2
+ # Casting for numerical values and a '+' for the addition of different object
3
+ # is included.
4
+ class ReportingEntry
5
+ # The raw attributes hash
6
+ attr_reader :attributes # TODO remove
7
+
8
+ # These constants define fields which are casted to integers/float
9
+ # in the accessor. Also these fields are added when adding two
10
+ # SuperReportingEntries.
11
+ #
12
+ # DON'T USE THIS FOR IDs (e.g. ad_id)
13
+ SUMMABLE_INT_FIELDS_REGEXP = /OVERWRITE_THIS_IN_SUBCLASS/
14
+ SUMMABLE_FLOAT_FIELDS_REGEXP = /OVERWRITE_THIS_IN_SUBCLASS/
15
+
16
+ # Columns listed in this array are explicitely not summable
17
+ NOT_SUMMABLE_FIELDS = %w()
18
+
19
+ # Standard constructor
20
+ # takes attributes as hash (like OpenStruct)
21
+ def initialize(attributes = {})
22
+ @attributes = HashWithIndifferentAccess.new(attributes)
23
+ end
24
+
25
+ # Offers an +OpenStruct+ like access to the values stored in +@attributes+
26
+ # Uses the information from +SUMMABLE_*_FIELDS_REGEXP+ to cast to numerical
27
+ # values.
28
+ def method_missing(method_id, *args)
29
+ return self.send("lazy_#{method_id}", *args) if self.respond_to?("lazy_#{method_id}")
30
+ if @attributes.has_key?(method_id) || method_id.to_s =~ summable_fields_regexp
31
+ return cast(method_id, @attributes[method_id])
32
+ end
33
+ super(method_id, *args)
34
+ end
35
+
36
+ # Add another +SuperReportingEntry+ and returns the resulting entry.
37
+ # All attributes specified by +SUMMABLE_FIELD_REGEXP+ are summed up.
38
+ # Further attributes are merged (the own values has priority).
39
+ def +(other)
40
+ return self.class.composite([self, other])
41
+ end
42
+
43
+ # Returns a ReportingEntry object with all non addable values set to nil
44
+ # This is thought be used for sum(mary) rows
45
+ def to_sum_entry
46
+ subject = self
47
+ klass = Class.new(self.class) do
48
+ # overwrite explicitely not summable columns
49
+ #
50
+ subject.send(:not_summable_fields).each do |method_id|
51
+ define_method(method_id) { nil }
52
+ end
53
+
54
+ # method missing decides if the columns is displayed or not
55
+ #
56
+ define_method(:method_missing) do |method_id, *args|
57
+ (method_id.to_s =~ summable_fields_regexp) ? subject.send(method_id, *args) : nil
58
+ end
59
+
60
+ # yes, this actually is a sum entry ;-)
61
+ #
62
+ def is_sum_entry?
63
+ return true
64
+ end
65
+ end
66
+ klass.new
67
+ end
68
+
69
+ # Returns a composite element which lazily sums up the summable values of the children
70
+ def self.composite(entries)
71
+ public_methods = self.instance_methods - Object.public_methods
72
+ summable_methods = public_methods.select { |method| method.to_s =~ summable_fields_regexp }
73
+
74
+ klass = Class.new(self) do
75
+ define_method(:method_missing) do |method_id, *args|
76
+ if (method_id.to_s =~ summable_fields_regexp)
77
+ return entries.inject(0) do |sum, entry|
78
+ sum + entry.send(method_id, *args)
79
+ end
80
+ else
81
+ return entries.first.send(method_id, *args)
82
+ end
83
+ end
84
+
85
+ # Delegate all summable method calls to the children
86
+ # by using the method_missing method
87
+ summable_methods.each do |method_id|
88
+ define_method(method_id) do |*args|
89
+ self.method_missing(method_id, *args)
90
+ end
91
+ end
92
+
93
+ # For better debuggability
94
+ #
95
+ define_method :inspect do
96
+ "CompositeReportingEntry [entries: #{entries.inspect} ]"
97
+ end
98
+ end
99
+ klass.new
100
+ end
101
+
102
+ # Returns true if entry is a sum entry (like returned by to_sum_entry)
103
+ def is_sum_entry?
104
+ return false
105
+ end
106
+
107
+ protected
108
+ # Helper function to cast string values to numeric values if +key+
109
+ # matches either +SUMMABLE_INT_FIELDS_REGEXP+ or +SUMMABLE_FLOAT_FIELDS_REGEXP+
110
+ def cast(key, value)
111
+ return value.to_i if key.to_s =~ summable_int_fields_regexp
112
+ return value.to_f if key.to_s =~ summable_float_fields_regexp
113
+ value
114
+ end
115
+
116
+ # Returns the union of int and float defining regexps
117
+ def self.summable_fields_regexp
118
+ Regexp.union(self::SUMMABLE_INT_FIELDS_REGEXP, self::SUMMABLE_FLOAT_FIELDS_REGEXP)
119
+ end
120
+
121
+ # reader for the regexp defining int fields
122
+ def summable_int_fields_regexp
123
+ self.class::SUMMABLE_INT_FIELDS_REGEXP
124
+ end
125
+
126
+ # reader for the regexp defining float fields
127
+ def summable_float_fields_regexp
128
+ self.class::SUMMABLE_FLOAT_FIELDS_REGEXP
129
+ end
130
+
131
+ # convenience accessor for summable_fields_regexp
132
+ def summable_fields_regexp
133
+ self.class.summable_fields_regexp
134
+ end
135
+
136
+ # Returns an array of explicitely not summable fields
137
+ #
138
+ def not_summable_fields
139
+ self.class::NOT_SUMMABLE_FIELDS
140
+ end
141
+
142
+ # For debugging purpose
143
+ #
144
+ def inspect
145
+ "#{self.class.name}: <#{@attributes.inspect}>\n"
146
+ end
147
+ end
@@ -0,0 +1,220 @@
1
+ # Subclass of +Reporting+ class
2
+ # Offers a variety of helpers to manage reportings that are generated via pure SQL queries
3
+ #
4
+ class SqlReporting < Reporting
5
+ attr_reader :columns_used
6
+ class_attribute :sql_tables, {:instance_reader => false, :instance_writer => false}
7
+
8
+ # Container for columns used by any (select, group by, where) statement.
9
+ # Used by the joins method to retrieve the joins needed
10
+ def columns_used
11
+ @columns_used ||= []
12
+ end
13
+
14
+ # Marks a column as used so the connected joins will be included
15
+ def mark_as_used(column)
16
+ columns_used << column.to_sym
17
+ end
18
+
19
+ # Returns the columns string for the select clause
20
+ def sql_select(additional_columns = [], mapping = {})
21
+ (map_columns(required_columns, mapping, true) << additional_columns).flatten.join(', ')
22
+ end
23
+
24
+ # Returns the columns string for the group by clause
25
+ #def sql_group_by(additional_columns = [], mapping = {})
26
+ def sql_group_by(*args)
27
+ mapping = args.extract_options! || {}
28
+ additional_columns = args.first || []
29
+ result = (map_columns(group_by, mapping) << additional_columns).flatten.join(', ')
30
+ result.empty? ? nil : result
31
+ end
32
+
33
+ def sql_order_by(mapping = {})
34
+ return nil if order_by.nil?
35
+ column = order_by[0]
36
+ direction = order_by[1]
37
+ column = mapping.has_key?(column) ? mapping[column] : sql_column_name(column)
38
+ return nil unless column
39
+ "#{column} #{direction.to_s.upcase}"
40
+ end
41
+
42
+ # TODO make protected?
43
+ def map_columns(columns, mapping = {}, with_alias = false)
44
+ mapped = []
45
+ columns.each do |column|
46
+ mapped << column if is_sql_column?(column)
47
+ end
48
+ mapped.collect do |column|
49
+ if mapping.has_key?(column)
50
+ with_alias ? "#{mapping[column]} #{column}" : mapping[column]
51
+ else
52
+ sql_column_name(column, :with_alias => with_alias)
53
+ end
54
+ end
55
+ end
56
+
57
+ # Returns the join statements which are needed for the given +columns+
58
+ def sql_joins(*columns)
59
+ if columns.empty?
60
+ columns = columns_used + required_columns
61
+ columns += where.keys if self.respond_to?(:where)
62
+ end
63
+ columns.uniq!
64
+ columns.flatten!
65
+
66
+ # get all tables needed
67
+ tables = columns.inject([]) do |tables, c|
68
+ sql = datasource_columns[c.to_s][:sql] if is_sql_column?(c)
69
+ tables << sql[:table] if sql && sql.is_a?(Hash)
70
+ tables
71
+ end.compact.uniq
72
+
73
+ # explode dependencies
74
+ sql_joins = tables.collect do |table|
75
+ result = [self.class.sql_tables[table]]
76
+ while result.last.has_key?(:depends)
77
+ result << self.class.sql_tables[result.last[:depends]]
78
+ end
79
+ result.reverse
80
+ end.flatten.uniq
81
+
82
+ sql_joins.collect { |t| t[:join] }.join(' ')
83
+ end
84
+
85
+ # Returns all datasource columns that correcpond to a SQL column
86
+ def sql_columns
87
+ datasource_columns.keys.delete_if { |c| !is_sql_column?(c) }.collect(&:to_sym)
88
+ end
89
+
90
+ # Returns +true+ if +column+ is a SQL column
91
+ def is_sql_column?(column)
92
+ datasource_columns.has_key?(column.to_s) && datasource_columns[column.to_s][:sql]
93
+ end
94
+
95
+ # Maps the column name using the +:sql+ definitions of the columns
96
+ #
97
+ # === options
98
+ # * +:with_alias+ Returns 'mapped_name name' instead of mapped_name
99
+ #
100
+ def sql_column_name(column, options = {})
101
+ column = column.to_s
102
+ return false unless is_sql_column?(column)
103
+ sql = datasource_columns[column][:sql]
104
+ return column.to_s if sql == true
105
+
106
+ parts = []
107
+ parts << sql[:table] if sql[:table] && !sql[:column].is_a?(String)
108
+ parts << (sql[:column] || column).to_s
109
+ sql_name = parts.join('.')
110
+
111
+ sql_name << " #{column}" if options[:with_alias]
112
+ sql_name
113
+ end
114
+
115
+ # Returns the SQL condition for the given column, depending on the columns type
116
+ #
117
+ def sql_condition_for(column_name, value)
118
+ column_name = column_name.to_s
119
+ # use the sql column name to be sure the table name is included if an sql column is defined
120
+ sql_name = is_sql_column?(column_name) ? sql_column_name(column_name) : column_name
121
+ bind_vars = [ ]
122
+ condition = ''
123
+ type = self.class.datasource_filters[column_name][:type].to_sym
124
+ case type
125
+ when :boolean then
126
+ bind_vars = value
127
+ condition = "(#{sql_name} = ?" + (value == false ? " OR ISNULL(#{sql_name}))" : ')')
128
+ else
129
+ bind_vars = value
130
+ # special handling for zero numbers = allow them to be null too
131
+ if [ :number, :integer ].include?(type) and not value.kind_of?(Array)
132
+ condition = "(#{sql_name} = ? #{(value.to_i.zero? ? "OR ISNULL(#{sql_name})" : "")})"
133
+ else
134
+ condition = value.kind_of?(Array) ? "(#{sql_name} IN(?))" : "(#{sql_name} = ?)"
135
+ end
136
+ end
137
+ self.class.send(:sanitize_sql_array, [ condition, bind_vars ])
138
+ end
139
+
140
+ # Returns all key value pairs that will be bound to the query
141
+ #
142
+ def sql_bind_variables
143
+ bind_vars = {}
144
+ self.class.datasource_filters.each do |column_name, options|
145
+ # just filter sql columns and nil values
146
+ if options[:sql] and not attributes[column_name].nil?
147
+ bind_vars[column_name] = attributes[column_name]
148
+ end
149
+ end
150
+
151
+ bind_vars.symbolize_keys
152
+ end
153
+
154
+ # Concat all where conditions for all bind variables
155
+ #
156
+ def sql_conditions
157
+ sql_bind_variables.collect do |column_name, value|
158
+ mark_as_used(column_name)
159
+ sql_condition_for(column_name, value)
160
+ end.compact.join(' AND ')
161
+ end
162
+
163
+ # Marks all columns used in filters or in the select fields as used
164
+ #
165
+ #
166
+ def mark_used_columns
167
+ sql_bind_variables.each { |col, value| mark_as_used(col) unless value.nil? }
168
+ end
169
+
170
+ class << self
171
+
172
+ # Defines a SQL table that is not the 'main table'
173
+ #
174
+ # === Options
175
+ # * +join+ Defines the SQL JOIN statement to join this table (mandatory)
176
+ # * +depends+ Defines a table, this table depends on to join correclty (optional)
177
+ #
178
+ def table(name, options = {})
179
+ self.sql_tables ||= HashWithIndifferentAccess.new.freeze
180
+ new_entry = HashWithIndifferentAccess.new({name => options}).freeze
181
+ # frozen, to prevent modifications on class_attribute
182
+ self.sql_tables = self.sql_tables.merge(new_entry).freeze
183
+ end
184
+
185
+ def add_sql_columns_for(clazz, options = {})
186
+ except = (options[:except] || []).map(&:to_sym)
187
+ model_columns = model_columns(clazz)
188
+ column_names = (options[:columns] || model_columns.keys) # can be used to define column order
189
+
190
+ column_names.each do |col_name|
191
+ next if except.include?(col_name)
192
+
193
+ type = model_columns[col_name]
194
+ groupable = [:string, :date, :boolean].include?(type)
195
+ use_sql_sum = (type == :number) # SUM() to get correct data with grouping
196
+ send(:column, col_name.to_sym,
197
+ {
198
+ :type => type,
199
+ :sql => use_sql_sum ? {:column => "SUM(#{col_name})"} : true,
200
+ :grouping => groupable })
201
+ end
202
+ end
203
+
204
+ def model_columns(clazz)
205
+ hsh={}
206
+ clazz.columns_hash.each do |name, col|
207
+ hsh[name.to_sym] = if col.number?
208
+ :number
209
+ elsif col.text?
210
+ :string
211
+ else
212
+ # try to map 1:1
213
+ col.type
214
+ end
215
+ end
216
+ hsh
217
+ end
218
+
219
+ end
220
+ end
@@ -0,0 +1,2 @@
1
+ class EmptyReporting < Reporting
2
+ end
@@ -0,0 +1,33 @@
1
+ class TestReporting < Reporting
2
+ attr_reader :aggregate_calls
3
+
4
+ filter :name,
5
+ :default => 'foobar'
6
+
7
+
8
+ filter :from_date, :type => :date
9
+ filter :to_date, :type => :date
10
+ filter :in_foo
11
+
12
+ select_default %w(name age)
13
+ group_by_default %w(name)
14
+
15
+ column :name, :type => :string
16
+ column :age, :type => :number, :grouping => true
17
+ column :address, :type => :string, :grouping => true
18
+ column :fullname, :type => :string, :requires => :name
19
+ column :fullfullname, :type => :string, :requires => :fullname
20
+ column :circle_a, :requires => :circle_b
21
+ column :circle_b, :requires => :circle_a
22
+
23
+ def initialize(*args)
24
+ #@select = %w(name age)
25
+ @aggregate_calls = 0
26
+ super(*args)
27
+ end
28
+
29
+ def aggregate
30
+ @aggregate_calls += 1
31
+ []
32
+ end
33
+ end
@@ -0,0 +1,9 @@
1
+ class TestReportingB < TestReporting
2
+ filter :name_b
3
+
4
+ column :name_b, :type => :string
5
+ column :age_b, :type => :number
6
+
7
+ select_default %w(name_b)
8
+ group_by_default %w(age_b)
9
+ end
@@ -0,0 +1,3 @@
1
+ class TestReportingC < TestReporting
2
+ column :name_c, :type => :string
3
+ end