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