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