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,239 @@
1
+ module GoogleDataSource
2
+ module DataSource
3
+ module Sql
4
+ class ::Method; include RParsec::FunctorMixin; end
5
+ class ::Proc; include RParsec::FunctorMixin; end
6
+
7
+ module Parser
8
+ include RParsec
9
+ include Functors
10
+ include Parsers
11
+
12
+ extend Parsers
13
+ # TODO drop keywords
14
+ MyKeywords = Keywords.case_insensitive(%w{
15
+ select from where group by having order desc asc
16
+ inner left right full outer inner join on cross
17
+ union all distinct as exists in between limit offset
18
+ case when else end and or not true false
19
+ })
20
+ MyOperators = Operators.new(%w{+ - * / % = > < >= <= <> != : ( ) . ,})
21
+ def self.operators(*ops)
22
+ result = []
23
+ ops.each do |op|
24
+ result << (MyOperators[op] >> op.to_sym)
25
+ end
26
+ sum(*result)
27
+ end
28
+ Comparators = operators(*%w{= > < >= <= <> !=})
29
+ quote_mapper = Proc.new do |raw|
30
+ # is this really different to raw.gsub! ???
31
+ raw.replace(raw.gsub(/\\'/, "'").gsub(/\\\\/, "\\"))
32
+ end
33
+
34
+ StringLiteral = char(?') >> ((str("\\\\")|str("\\'")|not_char(?')).many_.fragment).map(&quote_mapper) << char(?')
35
+ QuotedName = char(?`) >> not_char(?`).many_.fragment << char(?`)
36
+ Variable = char(?$) >> word
37
+ MyLexer = number.token(:number) | StringLiteral.token(:string) | Variable.token(:var) |
38
+ QuotedName.token(:word) | MyKeywords.lexer | MyOperators.lexer
39
+ MyLexeme = MyLexer.lexeme(whitespaces | comment_line('#')) << eof
40
+
41
+
42
+ ######################################### utilities #########################################
43
+ def keyword
44
+ MyKeywords
45
+ end
46
+
47
+ def operator
48
+ MyOperators
49
+ end
50
+
51
+ def comma
52
+ operator[',']
53
+ end
54
+
55
+ def list expr
56
+ paren(expr.delimited(comma))
57
+ end
58
+
59
+ def word(&block)
60
+ if block.nil?
61
+ token(:word, &Id)
62
+ else
63
+ token(:word, &block)
64
+ end
65
+ end
66
+
67
+ def paren parser
68
+ operator['('] >> parser << operator[')']
69
+ end
70
+
71
+ def ctor cls
72
+ cls.method :new
73
+ end
74
+
75
+ def rctor cls, arity=2
76
+ ctor(cls).reverse_curry arity
77
+ end
78
+
79
+ ################################### predicate parser #############################
80
+ def logical_operator op
81
+ proc{|a,b|CompoundPredicate.new(a,op,b)}
82
+ end
83
+
84
+ def make_predicate expr, rel
85
+ expr_list = list expr
86
+ comparison = make_comparison_predicate expr, rel
87
+ group_comparison = sequence(expr_list, Comparators, expr_list, &ctor(GroupComparisonPredicate))
88
+ bool = nil
89
+ lazy_bool = lazy{bool}
90
+ bool_term = keyword[:true] >> true | keyword[:false] >> false |
91
+ comparison | group_comparison | paren(lazy_bool) |
92
+ make_exists(rel) | make_not_exists(rel)
93
+ bool_table = OperatorTable.new.
94
+ infixl(keyword[:or] >> logical_operator(:or), 20).
95
+ infixl(keyword[:and] >> logical_operator(:and), 30).
96
+ prefix(keyword[:not] >> ctor(NotPredicate), 40)
97
+ bool = Expressions.build(bool_term, bool_table)
98
+ end
99
+
100
+ def make_exists rel
101
+ keyword[:exists] >> rel.map(&ctor(ExistsPredicate))
102
+ end
103
+
104
+ def make_not_exists rel
105
+ keyword[:not] >> keyword[:exists] >> rel.map(&ctor(NotExistsPredicate))
106
+ end
107
+
108
+ def make_in expr
109
+ keyword[:in] >> list(expr) >> map(&rctor(InPredicate))
110
+ end
111
+
112
+ def make_not_in expr
113
+ keyword[:not] >> keyword[:in] >> list(expr) >> map(&rctor(NotInPredicate))
114
+ end
115
+
116
+ def make_in_relation rel
117
+ keyword[:in] >> rel.map(&rctor(InRelationPredicate))
118
+ end
119
+
120
+ def make_not_in_relation rel
121
+ keyword[:not] >> keyword[:in] >> rel.map(&rctor(NotInRelationPredicate))
122
+ end
123
+
124
+ def make_between expr
125
+ make_between_clause(expr, &ctor(BetweenPredicate))
126
+ end
127
+
128
+ def make_not_between expr
129
+ keyword[:not] >> make_between_clause(expr, &ctor(NotBetweenPredicate))
130
+ end
131
+
132
+ def make_comparison_predicate expr, rel
133
+ comparison = sequence(Comparators, expr) do |op,e2|
134
+ proc{|e1|ComparePredicate.new(e1, op, e2)}
135
+ end
136
+ in_clause = make_in expr
137
+ not_in_clause = make_not_in expr
138
+ in_relation = make_in_relation rel
139
+ not_in_relation = make_not_in_relation rel
140
+ between = make_between expr
141
+ not_between = make_not_between expr
142
+ compare_with = comparison | in_clause | not_in_clause |
143
+ in_relation | not_in_relation | between | not_between
144
+ sequence(expr, compare_with, &Feed)
145
+ end
146
+
147
+ def make_between_clause expr, &maker
148
+ factory = proc do |a,_,b|
149
+ proc {|v|maker.call(v,a,b)}
150
+ end
151
+ variant1 = keyword[:between] >> paren(sequence(expr, comma, expr, &factory))
152
+ variant2 = keyword[:between] >> sequence(expr, keyword[:and], expr, &factory)
153
+ variant1 | variant2
154
+ end
155
+
156
+ ################################ expression parser ###############################
157
+ def calculate_simple_cases(val, cases, default)
158
+ SimpleCaseExpr.new(val, cases, default)
159
+ end
160
+
161
+ def calculate_full_cases(cases, default)
162
+ CaseExpr.new(cases, default)
163
+ end
164
+
165
+ def make_expression predicate, rel
166
+ expr = nil
167
+ lazy_expr = lazy{expr}
168
+
169
+ wildcard = operator[:*] >> WildcardExpr::Instance
170
+ lit = token(:number, :string, &ctor(LiteralExpr)) | token(:var, &ctor(VarExpr))
171
+ atom = lit | wildcard | word(&ctor(WordExpr))
172
+ term = atom | (operator['('] >> lazy_expr << operator[')'])
173
+
174
+ table = OperatorTable.new.
175
+ infixl(operator['+'] >> Plus, 20).
176
+ infixl(operator['-'] >> Minus, 20).
177
+ infixl(operator['*'] >> Mul, 30).
178
+ infixl(operator['/'] >> Div, 30).
179
+ infixl(operator['%'] >> Mod, 30).
180
+ prefix(operator['-'] >> Neg, 50)
181
+ expr = Expressions.build(term, table)
182
+ end
183
+
184
+ ################################ relation parser ###############################
185
+ def make_relation expr, pred
186
+ where_clause = keyword[:where] >> pred
187
+ order_element = sequence(expr, (keyword[:asc] >> true | keyword[:desc] >> false).optional(true),
188
+ &ctor(OrderElement))
189
+ order_elements = order_element.separated1(comma)
190
+ exprs = expr.separated1(comma)
191
+
192
+ # setup clauses
193
+ select_clause = keyword[:select] >> exprs
194
+ order_by_clause = keyword[:order] >> keyword[:by] >> order_elements
195
+ group_by = keyword[:group] >> keyword[:by] >> exprs
196
+ group_by_clause = sequence(group_by, (keyword[:having] >> pred).optional, &ctor(GroupByClause))
197
+ limit_clause = keyword[:limit] >> token(:number, &To_i)
198
+ offset_clause = keyword[:offset] >> token(:number, &To_i)
199
+
200
+ # build relation
201
+ relation = sequence(
202
+ select_clause.optional([WildcardExpr.new]),
203
+ where_clause.optional, group_by_clause.optional, order_by_clause.optional,
204
+ limit_clause.optional, offset_clause.optional
205
+ ) do |select, where, groupby, orderby, limit, offset|
206
+ SelectRelation.new(select, where, groupby, orderby, limit, offset)
207
+ end
208
+ end
209
+
210
+ ########################## put together ###############################
211
+ def expression
212
+ assemble[0]
213
+ end
214
+
215
+ def relation
216
+ assemble[2]
217
+ end
218
+ def predicate
219
+ assemble[1]
220
+ end
221
+
222
+ def assemble
223
+ pred = nil
224
+ rel = nil
225
+ lazy_predicate = lazy{pred}
226
+ lazy_rel = lazy{rel}
227
+ expr = make_expression lazy_predicate, lazy_rel
228
+ pred = make_predicate expr, lazy_rel
229
+ rel = make_relation expr, pred
230
+ return expr, pred, rel
231
+ end
232
+
233
+ def make parser
234
+ MyLexeme.nested(parser << eof)
235
+ end
236
+ end
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,82 @@
1
+ module GoogleDataSource
2
+ module DataSource
3
+ class SimpleSqlException < Exception; end
4
+
5
+ class SqlParser
6
+ include Sql::Parser
7
+
8
+ class << self
9
+ # Parses a simple SQL query and return the result
10
+ # The where statement may only contain equality comparisons that are connected with 'and'
11
+ # Only a single ordering parameter is accepted
12
+ # Throws a +SimpleSqlException+ if these conditions are not satisfied
13
+ def simple_parse(query)
14
+ result = parse(query)
15
+ OpenStruct.new({
16
+ :select => result.select.collect(&:to_s),
17
+ :conditions => simple_where_parser(result.where),
18
+ :orderby => simple_orderby_parser(result.orderby),
19
+ :groupby => simple_groupby_parser(result.groupby),
20
+ :limit => result.limit,
21
+ :offset => result.offset
22
+ })
23
+ end
24
+
25
+ # Parses a SQL query and returns the result
26
+ def parse(query)
27
+ parser.parse(query)
28
+ end
29
+
30
+ protected
31
+ # Helper to the +simple_parse+ method
32
+ def simple_where_parser(predicate, result = Hash.new)
33
+ case predicate.class.name.split('::').last
34
+ when 'CompoundPredicate'
35
+ raise SimpleSqlException.new("Operator forbidden (use only 'and')") unless predicate.op == :and
36
+ simple_where_parser(predicate.left, result)
37
+ simple_where_parser(predicate.right, result)
38
+ when 'ComparePredicate'
39
+ case predicate.op
40
+ when :"="
41
+ result[predicate.left.to_s] = predicate.right.to_s
42
+ when :"<", :">", :">=", :"<=", :"<>", :"!="
43
+ result[predicate.left.to_s] ||= Array.new
44
+ raise SimpleSqlException.new("Condition clach") unless result[predicate.left.to_s].is_a?(Array)
45
+ result[predicate.left.to_s] << OpenStruct.new(:op => predicate.op.to_s, :value => predicate.right.to_s)
46
+ else
47
+ raise SimpleSqlException.new("Comparator forbidden (use only '=,<,>')") unless predicate.op == :"="
48
+ end
49
+ when 'InPredicate'
50
+ result[predicate.expr.to_s] ||= Array.new
51
+ raise SimpleSqlException.new("Condition clach") unless result[predicate.expr.to_s].is_a?(Array)
52
+ result[predicate.expr.to_s] << OpenStruct.new(:op => 'in', :value => predicate.vals.map(&:to_s))
53
+ when 'NilClass'
54
+ # do nothing
55
+ else
56
+ raise SimpleSqlException.new("Unknown syntax error")
57
+ end
58
+ result
59
+ end
60
+
61
+ # Helper to the +simple_parse+ method
62
+ def simple_orderby_parser(orderby)
63
+ return nil if orderby.nil?
64
+ raise SimpleSqlException.new("Too many ordering arguments (1 allowed)") if orderby.size > 1
65
+ [orderby.first.expr.to_s, orderby.first.asc ? :asc : :desc]
66
+ end
67
+
68
+ # Helper to the +simple_parse+ method
69
+ def simple_groupby_parser(groupby)
70
+ return [] if groupby.nil?
71
+ groupby.exprs.collect(&:to_s)
72
+ end
73
+
74
+ # Returns the parser
75
+ def parser
76
+ sql = self.new
77
+ sql.make(sql.relation)
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,31 @@
1
+ module GoogleDataSource
2
+ module DataSource
3
+ # A simple template handler for a data source
4
+ # Provides a GoogleDataSource::Base object datasource in the template so the template
5
+ # can fill it with data
6
+ class TemplateHandler
7
+
8
+ def self.call(template)
9
+ <<-EOT
10
+ datasource = GoogleDataSource::DataSource::Base.from_params(params)
11
+ #{template.source.dup}
12
+ if !datasource.reporting.nil? && datasource.reporting.has_form?
13
+ datasource.callback = "$('\\\#\#{datasource.reporting.form_id}').html(\#{render(:partial => datasource.reporting.partial).to_json});"
14
+ end
15
+
16
+ if datasource.format == 'csv'
17
+ headers['Content-Type'] = 'text/csv; charset=utf-8'
18
+ headers['Content-Disposition'] = "attachment; filename=\\"\#{datasource.export_filename}.csv\\""
19
+ end
20
+
21
+ if datasource.format == 'xml'
22
+ headers['Content-Type'] = 'application/xml; charset=utf-8'
23
+ headers['Content-Disposition'] = "attachment; filename=\\"\#{datasource.export_filename}.xml\\""
24
+ end
25
+
26
+ datasource.response
27
+ EOT
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,26 @@
1
+ module GoogleDataSource
2
+ # Some usefull helpers for testing datasources
3
+ module TestHelper
4
+
5
+ # Returns the parsed JSON argument of the datasource response
6
+ def datasource_response
7
+ first_cmd = @response.body.match(/([^;"]*|"(\\"|[^"])*;?(\\"|[^"])*")*;/)[0]
8
+ response = OpenStruct.new(JSON.parse(first_cmd.match(/^[^(]*\((.*)\);$/)[1]))
9
+ response.table = OpenStruct.new(response.table)
10
+ response
11
+ end
12
+
13
+ # Returns the columns array of the JSON response
14
+ def datasource_column(column)
15
+ response = datasource_response
16
+ column_no = response.table.cols.collect { |c| c['id'] }.index(column.to_s)
17
+ response.table.rows.collect { |r| r['c'][column_no] }
18
+ end
19
+
20
+ # Returns the column ids of the JSON response
21
+ def datasource_column_ids
22
+ datasource_response.table.cols.collect { |c| c['id'] }
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,3 @@
1
+ module GoogleDataSource
2
+ VERSION = '0.7.6'
3
+ end
@@ -0,0 +1,25 @@
1
+ require 'nokogiri'
2
+
3
+ module GoogleDataSource
4
+ module DataSource
5
+ class XmlData < Base
6
+ #include ActionView::Helpers::NumberHelper
7
+ def response
8
+ cols = columns.map { |col| col.id || col.type }
9
+ builder = ::Nokogiri::XML::Builder.new(:encoding => 'UTF-8') do |xml|
10
+ xml.send(xml_class.pluralize, :type => 'array') do
11
+ data.each do |datarow|
12
+ xml.send(xml_class) do
13
+ datarow.zip(cols).each do |val, key|
14
+ xml.send("#{key}", val.is_a?(Hash) ? val[:v] : val)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ builder.to_xml
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,5 @@
1
+ de:
2
+ google_data_source:
3
+ export_links:
4
+ export_as: 'Exportieren als:'
5
+ csv: CSV
@@ -0,0 +1,19 @@
1
+ module GoogleDataSource
2
+ module Reporting
3
+ module ActionControllerExtension
4
+ # Add the option +:reporting+ to the +render+ method.
5
+ # Takes a +Reporting+ object and renders the data-source response
6
+ # including the form validation results
7
+ def render_with_reporting(*args)
8
+ if !args.first.nil? && args.first.is_a?(Hash) && args.first.has_key?(:reporting)
9
+ reporting = args.first[:reporting]
10
+ datasource = GoogleDataSource::DataSource::Base.from_params(params)
11
+ datasource.set(reporting)
12
+ render_for_text datasource.response
13
+ else
14
+ render_without_reporting(*args)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,58 @@
1
+ # Represents an array of Objects which can be grouped according to grouping keys
2
+ # or a grouping block, which returns the hash used for grouping
3
+ #
4
+ # The objects must support the '+' operation which is used to implode sets of objects
5
+ # with the same grouping hash.
6
+ class GroupedSet < Array
7
+ #attr_reader :data
8
+
9
+ # Standard constructor
10
+ # +data+ is an Array of Objects implementing a '+' operation
11
+ #def initialize(data)
12
+ # @data = data
13
+ #end
14
+
15
+ # Bang method for the regrouping of the data
16
+ # The method takes either a set of keys or a block as grouping criterion
17
+ #
18
+ # If keys are set the grouping hash is built by calling +object.send(key).to_s+
19
+ # and concatening for all keys. (e.g. if +Object+ supports the method +date+ the
20
+ # grouping by +date+ is calculated by calling +regroup!(:date)+
21
+ #
22
+ # If a block is passed, it is called for every entry and it's result is taken as
23
+ # grouping hash. (e.g. +regroup! { |entry| entry.date }+ for grouping by date
24
+ #
25
+ # Objects with identical grouping hash are collapsed by calling the '+' operator
26
+ # on them.
27
+ #
28
+ # ATTENTION:
29
+ # This method won't recognize senseless grouping (e.g. calling
30
+ # +regroup! { |entry| entry.date.month }+ followed by
31
+ # +regroup! { |entry| entry.date }+ )
32
+ def regroup(*keys, &block)
33
+ # handle block
34
+ return regroup_by_proc(block) if block_given?
35
+
36
+ # handle keys
37
+ block = Proc.new do |entry|
38
+ keys.inject([]) { |memo, column| memo.push entry.send(column.to_sym).to_s }.join('-')
39
+ end
40
+ regroup_by_proc(block)
41
+ end
42
+
43
+ # Collapse the collection to a single entry
44
+ def collapse
45
+ regroup.first
46
+ end
47
+
48
+ protected
49
+ # Helper method for regroup with groups the data using a proc as grouping hash generator
50
+ def regroup_by_proc(grouping_proc)
51
+ data = self.group_by { |entry| grouping_proc.call(entry) }.values
52
+ result = data.collect do |entries|
53
+ # Uses the class of the first element to build the composite element
54
+ entries.first.class.composite(entries)
55
+ end
56
+ self.class.new(result)
57
+ end
58
+ end