google_data_source 0.7.6
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +7 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +25 -0
- data/Rakefile +31 -0
- data/google_data_source.gemspec +32 -0
- data/lib/assets/images/google_data_source/chart_bar_add.png +0 -0
- data/lib/assets/images/google_data_source/chart_bar_delete.png +0 -0
- data/lib/assets/images/google_data_source/loader.gif +0 -0
- data/lib/assets/javascripts/google_data_source/data_source_init.js +3 -0
- data/lib/assets/javascripts/google_data_source/extended_data_table.js +76 -0
- data/lib/assets/javascripts/google_data_source/filter_form.js +180 -0
- data/lib/assets/javascripts/google_data_source/google_visualization/combo_table.js.erb +113 -0
- data/lib/assets/javascripts/google_data_source/google_visualization/table.js +116 -0
- data/lib/assets/javascripts/google_data_source/google_visualization/timeline.js +13 -0
- data/lib/assets/javascripts/google_data_source/google_visualization/visualization.js.erb +141 -0
- data/lib/assets/javascripts/google_data_source/index.js +7 -0
- data/lib/dummy_engine.rb +5 -0
- data/lib/google_data_source.rb +33 -0
- data/lib/google_data_source/base.rb +281 -0
- data/lib/google_data_source/column.rb +31 -0
- data/lib/google_data_source/csv_data.rb +23 -0
- data/lib/google_data_source/data_date.rb +17 -0
- data/lib/google_data_source/data_date_time.rb +17 -0
- data/lib/google_data_source/helper.rb +69 -0
- data/lib/google_data_source/html_data.rb +6 -0
- data/lib/google_data_source/invalid_data.rb +14 -0
- data/lib/google_data_source/json_data.rb +78 -0
- data/lib/google_data_source/railtie.rb +36 -0
- data/lib/google_data_source/sql/models.rb +266 -0
- data/lib/google_data_source/sql/parser.rb +239 -0
- data/lib/google_data_source/sql_parser.rb +82 -0
- data/lib/google_data_source/template_handler.rb +31 -0
- data/lib/google_data_source/test_helper.rb +26 -0
- data/lib/google_data_source/version.rb +3 -0
- data/lib/google_data_source/xml_data.rb +25 -0
- data/lib/locale/de.yml +5 -0
- data/lib/reporting/action_controller_extension.rb +19 -0
- data/lib/reporting/grouped_set.rb +58 -0
- data/lib/reporting/helper.rb +110 -0
- data/lib/reporting/reporting.rb +352 -0
- data/lib/reporting/reporting_adapter.rb +27 -0
- data/lib/reporting/reporting_entry.rb +147 -0
- data/lib/reporting/sql_reporting.rb +220 -0
- data/test/lib/empty_reporting.rb +2 -0
- data/test/lib/test_reporting.rb +33 -0
- data/test/lib/test_reporting_b.rb +9 -0
- data/test/lib/test_reporting_c.rb +3 -0
- data/test/locales/en.models.yml +6 -0
- data/test/locales/en.reportings.yml +5 -0
- data/test/rails/reporting_renderer_test.rb +47 -0
- data/test/test_helper.rb +50 -0
- data/test/units/base_test.rb +340 -0
- data/test/units/csv_data_test.rb +36 -0
- data/test/units/grouped_set_test.rb +60 -0
- data/test/units/json_data_test.rb +68 -0
- data/test/units/reporting_adapter_test.rb +20 -0
- data/test/units/reporting_entry_test.rb +149 -0
- data/test/units/reporting_test.rb +374 -0
- data/test/units/sql_parser_test.rb +111 -0
- data/test/units/sql_reporting_test.rb +307 -0
- data/test/units/xml_data_test.rb +32 -0
- metadata +286 -0
@@ -0,0 +1,116 @@
|
|
1
|
+
|
2
|
+
/**
|
3
|
+
* Google DataTable
|
4
|
+
*/
|
5
|
+
DataSource.Table = function(query, container, options) {
|
6
|
+
DataSource.Visualization.call(this, query, container, options);
|
7
|
+
};
|
8
|
+
|
9
|
+
|
10
|
+
DataSource.Table.prototype = new DataSource.Visualization();
|
11
|
+
|
12
|
+
DataSource.Table.prototype.initialize = function(){
|
13
|
+
this.initialized = true;
|
14
|
+
this.visualization = new google.visualization.Table(this.container);
|
15
|
+
|
16
|
+
this.options['allowHtml'] = true;
|
17
|
+
|
18
|
+
var self = this;
|
19
|
+
|
20
|
+
this.csvSize = (this.options['csvSize'] > 0) ? this.options['csvSize'] : 1000;
|
21
|
+
if (this.options['serverPaging']) {
|
22
|
+
this.options['pageSize'] = (this.options['pageSize'] > 0) ? this.options['pageSize'] : 10;
|
23
|
+
this.pageSize = this.options['pageSize'];
|
24
|
+
|
25
|
+
var addListener = google.visualization.events.addListener;
|
26
|
+
addListener(this.visualization, 'page', function(e) {self.handlePage(e)});
|
27
|
+
addListener(this.visualization, 'sort', function(e) {self.handleSort(e)});
|
28
|
+
|
29
|
+
this.options['sort'] = 'event';
|
30
|
+
this.options['page'] = 'event';
|
31
|
+
|
32
|
+
var buttonConfig = 'pagingButtonsConfiguration';
|
33
|
+
this.options[buttonConfig] = this.options[buttonConfig] || 'both';
|
34
|
+
|
35
|
+
this.currentPageIndex = 0;
|
36
|
+
this.setPageQueryClause(0);
|
37
|
+
// TODO default sorting???
|
38
|
+
}
|
39
|
+
else {
|
40
|
+
this.options['sortColumn'] = 0;
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
DataSource.Table.prototype.beforeSendAndDraw = function() {
|
45
|
+
if (typeof this.initialized == 'undefined') {
|
46
|
+
this.initialize();
|
47
|
+
}
|
48
|
+
|
49
|
+
this.visualization.setSelection([]);
|
50
|
+
};
|
51
|
+
|
52
|
+
DataSource.Table.prototype.beforeSubmit = function() {
|
53
|
+
if (typeof this.initialized == 'undefined') {
|
54
|
+
this.initialize();
|
55
|
+
}
|
56
|
+
|
57
|
+
if (this.options['serverPaging']) this.setPageQueryClause(0);
|
58
|
+
};
|
59
|
+
|
60
|
+
/** Handles a sort event with the given properties. Will page to page=0. */
|
61
|
+
DataSource.Table.prototype.handleSort = function(properties) {
|
62
|
+
var columnIndex = properties['column'];
|
63
|
+
var isAscending = properties['ascending'];
|
64
|
+
this.options['sortColumn'] = columnIndex;
|
65
|
+
this.options['sortAscending'] = isAscending;
|
66
|
+
|
67
|
+
// dataTable exists since the user clicked the table.
|
68
|
+
var colID = this.currentDataTable.getColumnId(columnIndex);
|
69
|
+
this.queryClauses.orderby = 'order by `' + colID + (!isAscending ? '` desc' : '`');
|
70
|
+
|
71
|
+
// Calls sendAndDraw internally.
|
72
|
+
this.handlePage({'page': 0});
|
73
|
+
};
|
74
|
+
|
75
|
+
|
76
|
+
/** Handles a page event with the given properties. */
|
77
|
+
DataSource.Table.prototype.handlePage = function(properties) {
|
78
|
+
var localTableNewPage = properties['page']; // 1, -1 or 0
|
79
|
+
var newPage = 0;
|
80
|
+
if (localTableNewPage != 0) {
|
81
|
+
newPage = this.currentPageIndex + localTableNewPage;
|
82
|
+
}
|
83
|
+
if (this.setPageQueryClause(newPage)) {
|
84
|
+
this.sendAndDraw();
|
85
|
+
}
|
86
|
+
};
|
87
|
+
|
88
|
+
/**
|
89
|
+
* Sets the pageQueryClause and table options for a new page request.
|
90
|
+
* In case the next page is requested - checks that another page exists
|
91
|
+
* based on the previous request.
|
92
|
+
* Returns true if a new page query clause was set, false otherwise.
|
93
|
+
*/
|
94
|
+
DataSource.Table.prototype.setPageQueryClause = function(pageIndex) {
|
95
|
+
var pageSize = this.pageSize;
|
96
|
+
|
97
|
+
if (pageIndex < 0) {
|
98
|
+
return false;
|
99
|
+
}
|
100
|
+
var dataTable = this.currentDataTable;
|
101
|
+
if ((pageIndex == this.currentPageIndex + 1) && dataTable) {
|
102
|
+
if (dataTable.getNumberOfRows() <= pageSize) {
|
103
|
+
return false;
|
104
|
+
}
|
105
|
+
}
|
106
|
+
this.currentPageIndex = pageIndex;
|
107
|
+
var newStartRow = this.currentPageIndex * pageSize;
|
108
|
+
|
109
|
+
// Get the pageSize + 1 so that we can know when the last page is reached.
|
110
|
+
this.queryClauses.limit = 'limit ' + (pageSize + 1);
|
111
|
+
this.queryClauses.offset = 'offset ' + newStartRow;
|
112
|
+
|
113
|
+
// Note: row numbers are 1-based yet dataTable rows are 0-based.
|
114
|
+
this.options['firstRowNumber'] = newStartRow + 1;
|
115
|
+
return true;
|
116
|
+
};
|
@@ -0,0 +1,13 @@
|
|
1
|
+
/**
|
2
|
+
* TimeLine Chart
|
3
|
+
*/
|
4
|
+
//google.load("visualization", "1", {packages:["corechart"]});
|
5
|
+
|
6
|
+
|
7
|
+
DataSource.TimeLine = function(query, container, options) {
|
8
|
+
DataSource.Visualization.call(this, query, container, options);
|
9
|
+
//this.visualization = new google.visualization.LineChart(this.container);
|
10
|
+
this.visualization = new google.visualization.AnnotatedTimeLine(this.container);
|
11
|
+
}
|
12
|
+
|
13
|
+
DataSource.TimeLine.prototype = new DataSource.Visualization();
|
@@ -0,0 +1,141 @@
|
|
1
|
+
|
2
|
+
/**
|
3
|
+
* Google DataSource Visualization
|
4
|
+
* SuperClass for all Google Visualizations
|
5
|
+
* */
|
6
|
+
DataSource.Visualization = function(query, container, options) {
|
7
|
+
// TODO throw exception rename query to url and restrict to strings
|
8
|
+
this.query = (typeof query == 'string') ? new google.visualization.Query(query) : query;
|
9
|
+
this.baseurl = query;
|
10
|
+
this.container = (typeof container == 'string') ? document.getElementById(container) : container;
|
11
|
+
this.currentDataTable = null;
|
12
|
+
this.options = options;
|
13
|
+
this.queryClauses = {};
|
14
|
+
}
|
15
|
+
DataSource.Visualization.prototype = {
|
16
|
+
draw: function() {
|
17
|
+
this.visualization.draw(this.currentDataTable, this.options);
|
18
|
+
},
|
19
|
+
|
20
|
+
// TODO make configurable
|
21
|
+
showBusyStatus: function() {
|
22
|
+
$(this.container).prepend('<img src="<%= asset_path('google_data_source/loader.gif') %>" class="loader" />');
|
23
|
+
},
|
24
|
+
|
25
|
+
hideBusyStatus: function() {
|
26
|
+
$(this.container).children('.loader').remove();
|
27
|
+
},
|
28
|
+
|
29
|
+
addParamsToUrl: function(url, params) {
|
30
|
+
var separator = '?';
|
31
|
+
if (url.match(/\?/)) separator = '&';
|
32
|
+
return url + separator + params;
|
33
|
+
},
|
34
|
+
|
35
|
+
download: function(format) {
|
36
|
+
format = format || 'csv';
|
37
|
+
var self = this;
|
38
|
+
this.queryClauses.limit = 'limit ' + this.csvSize;
|
39
|
+
// prepare query
|
40
|
+
var queryClause = '';
|
41
|
+
$.each(['select', 'where', 'groupby', 'orderby', 'limit'], function(i, key) {
|
42
|
+
if (self.queryClauses[key]) queryClause += self.queryClauses[key] + ' ';
|
43
|
+
});
|
44
|
+
var url = this.addParamsToUrl(this.baseurl, $.param({tqx: "out:csv", tq: queryClause}));
|
45
|
+
location.href = url;
|
46
|
+
},
|
47
|
+
|
48
|
+
sendAndDraw: function(params) {
|
49
|
+
var self = this;
|
50
|
+
this.query.abort();
|
51
|
+
|
52
|
+
this.query = params ? new google.visualization.Query(this.addParamsToUrl(this.baseurl, params)) : this.query;
|
53
|
+
this.showBusyStatus();
|
54
|
+
|
55
|
+
// prepare query
|
56
|
+
var queryClause = '';
|
57
|
+
$.each(['select', 'where', 'groupby', 'orderby', 'limit', 'offset'], function(i, key) {
|
58
|
+
if (self.queryClauses[key]) queryClause += self.queryClauses[key] + ' ';
|
59
|
+
});
|
60
|
+
this.lastQueryClause = queryClause;
|
61
|
+
this.query.setQuery(queryClause);
|
62
|
+
|
63
|
+
this.beforeSendAndDraw();
|
64
|
+
this.query.send(function(response) {self.handleResponse(response)});
|
65
|
+
},
|
66
|
+
|
67
|
+
handleResponse: function(response) {
|
68
|
+
this.hideBusyStatus();
|
69
|
+
this.currentDataTable = null;
|
70
|
+
if (response.isError()) {
|
71
|
+
google.visualization.errors.addError(this.container, response.getMessage(),
|
72
|
+
response.getDetailedMessage(), {'showInTooltip': false});
|
73
|
+
} else {
|
74
|
+
this.setDataTableFromResponse(response);
|
75
|
+
|
76
|
+
this.draw();
|
77
|
+
}
|
78
|
+
},
|
79
|
+
|
80
|
+
beforeSendAndDraw: function() {
|
81
|
+
// called by sendAndDraw.
|
82
|
+
// Can be overridden to preare a query etc.
|
83
|
+
},
|
84
|
+
|
85
|
+
beforeSubmit: function() {
|
86
|
+
// called before form submission
|
87
|
+
},
|
88
|
+
|
89
|
+
// Sets the instances datatable from a query response
|
90
|
+
setDataTableFromResponse: function(response) {
|
91
|
+
// Set rows in a data view.
|
92
|
+
this.currentDataTable = new ExtendedDataTable(response.getDataTable(), this.options);
|
93
|
+
}
|
94
|
+
};
|
95
|
+
|
96
|
+
DataSource.Visualization.create = function(type, query, container, options) {
|
97
|
+
if(!options['asyncLoading'])
|
98
|
+
google.setOnLoadCallback(DataSource.Visualization.onloadsetup(type, query, container, options));
|
99
|
+
else
|
100
|
+
DataSource.Visualization.onloadsetup(type, query, container, options);
|
101
|
+
};
|
102
|
+
|
103
|
+
DataSource.Visualization.onloadsetup = function(type, query, container, options) {
|
104
|
+
var klass = eval("DataSource." + type);
|
105
|
+
var visualization = new klass(query, container, options);
|
106
|
+
// store visualization in container data
|
107
|
+
$('#' + container).data('visualization', visualization);
|
108
|
+
|
109
|
+
// TODO: Fix this dirty issue
|
110
|
+
window.setTimeout(function(){
|
111
|
+
$('#' + container + '_controls a.export_as_csv').click(function() {
|
112
|
+
visualization.download();
|
113
|
+
return false;
|
114
|
+
});
|
115
|
+
}, 500);
|
116
|
+
|
117
|
+
var onSubmit = function() {
|
118
|
+
var form = new DataSource.FilterForm(this);
|
119
|
+
|
120
|
+
visualization.queryClauses['select'] = form.toSelectClause();
|
121
|
+
visualization.queryClauses['where'] = form.toWhereClause();
|
122
|
+
visualization.queryClauses['groupby'] = form.toGroupByClause();
|
123
|
+
|
124
|
+
visualization.beforeSubmit();
|
125
|
+
|
126
|
+
visualization.sendAndDraw(form.getNoQueryValues());
|
127
|
+
return false;
|
128
|
+
};
|
129
|
+
|
130
|
+
// Submit form to display results
|
131
|
+
if (options['form']) {
|
132
|
+
$('#' + options['form']).submit(onSubmit);
|
133
|
+
|
134
|
+
if (options['autosubmit']) {
|
135
|
+
onSubmit.call($('#' + options['form']));
|
136
|
+
}
|
137
|
+
} else {
|
138
|
+
// If no form is defined, just get the redular results
|
139
|
+
visualization.sendAndDraw();
|
140
|
+
}
|
141
|
+
}
|
@@ -0,0 +1,7 @@
|
|
1
|
+
//= require './data_source_init'
|
2
|
+
//= require './extended_data_table'
|
3
|
+
//= require './filter_form'
|
4
|
+
//= require './google_visualization/visualization'
|
5
|
+
//= require './google_visualization/table'
|
6
|
+
//= require './google_visualization/combo_table'
|
7
|
+
//= require './google_visualization/timeline'
|
data/lib/dummy_engine.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'rparsec'
|
3
|
+
require 'active_support/all'
|
4
|
+
require 'action_controller'
|
5
|
+
require 'active_record'
|
6
|
+
|
7
|
+
require 'google_data_source/base'
|
8
|
+
require 'google_data_source/version'
|
9
|
+
require 'google_data_source/json_data'
|
10
|
+
require 'google_data_source/csv_data'
|
11
|
+
require 'google_data_source/xml_data'
|
12
|
+
require 'google_data_source/html_data'
|
13
|
+
require 'google_data_source/invalid_data'
|
14
|
+
require 'google_data_source/column'
|
15
|
+
require 'google_data_source/data_date'
|
16
|
+
require 'google_data_source/data_date_time'
|
17
|
+
require 'google_data_source/template_handler'
|
18
|
+
|
19
|
+
require 'google_data_source/sql/models'
|
20
|
+
require 'google_data_source/sql/parser'
|
21
|
+
require 'google_data_source/sql_parser'
|
22
|
+
|
23
|
+
# the reporting models
|
24
|
+
require 'reporting/reporting'
|
25
|
+
require 'reporting/reporting_adapter'
|
26
|
+
require 'reporting/sql_reporting'
|
27
|
+
require 'reporting/reporting_entry'
|
28
|
+
require 'reporting/grouped_set'
|
29
|
+
|
30
|
+
if defined?(::Rails::Railtie)
|
31
|
+
require File.join(File.dirname(__FILE__), *%w[google_data_source railtie])
|
32
|
+
require 'dummy_engine'
|
33
|
+
end
|
@@ -0,0 +1,281 @@
|
|
1
|
+
module GoogleDataSource
|
2
|
+
module DataSource
|
3
|
+
# Superclass for all data source implementations
|
4
|
+
# Offers methods for getting and setting the data and column definitions of
|
5
|
+
# the data source
|
6
|
+
class Base
|
7
|
+
# Callback defines a JavaScript snippet that is appended to the regular
|
8
|
+
# data-source reponse. This is currently used to refresh the form in
|
9
|
+
# reportings (validation)
|
10
|
+
attr_accessor :callback, :reporting, :column_labels, :formatters, :virtual_columns, :export_filename, :xml_class
|
11
|
+
|
12
|
+
# Define accessors for the data source data, columns and errors
|
13
|
+
attr_reader :errors
|
14
|
+
|
15
|
+
# Creates a new instance and validates it.
|
16
|
+
# Protected method so it can be used from the subclasses
|
17
|
+
def initialize(gdata_params)
|
18
|
+
@virtual_columns = HashWithIndifferentAccess.new
|
19
|
+
@formatters = HashWithIndifferentAccess.new
|
20
|
+
@column_labels = HashWithIndifferentAccess.new
|
21
|
+
@required_columns = {}
|
22
|
+
|
23
|
+
@raw_data = []
|
24
|
+
@columns = []
|
25
|
+
|
26
|
+
@params = gdata_params
|
27
|
+
@errors = {}
|
28
|
+
@version = "0.6"
|
29
|
+
|
30
|
+
validate
|
31
|
+
end
|
32
|
+
protected :initialize
|
33
|
+
|
34
|
+
|
35
|
+
# Returns true if formatter formatter for a certain column is defined
|
36
|
+
def has_formatter?(column_name)
|
37
|
+
@formatters.has_key?(column_name.to_sym)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Convenience method for formatter definition
|
41
|
+
def formatter(column, options = {}, &block)
|
42
|
+
set_required_columns(column.to_sym, options[:requires]) if options.has_key?(:requires)
|
43
|
+
formatters[column.to_sym] = block
|
44
|
+
end
|
45
|
+
|
46
|
+
# Return true if virtual column with name exists
|
47
|
+
def is_virtual_column?(name)
|
48
|
+
virtual_columns.has_key?(name)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Sets up a virtual column with name and block with is called with a row
|
52
|
+
# and returns a string or a hash like {v: "real value", f: "formatted value"}
|
53
|
+
def virtual_column(name, options = {}, &block)
|
54
|
+
@raw_data.add_virtual_column(name.to_sym, options[:type] || :string) if @raw_data.respond_to?(:add_virtual_column)
|
55
|
+
set_required_columns(name.to_sym, options[:requires]) if options.has_key?(:requires)
|
56
|
+
virtual_columns[name.to_sym] = {
|
57
|
+
:type => options[:type] || :string,
|
58
|
+
:proc => block
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
62
|
+
# Getter with empty array default
|
63
|
+
def required_columns
|
64
|
+
required = column_ids.inject([]) { |columns, column| columns << @required_columns[column.to_sym] }.flatten.compact
|
65
|
+
(required + column_ids).map(&:to_s).uniq
|
66
|
+
end
|
67
|
+
|
68
|
+
# Add a list of columns to the list of required columns (columns that have to be fetched)
|
69
|
+
def set_required_columns(column, requires = [])
|
70
|
+
@required_columns[column.to_sym] = requires
|
71
|
+
end
|
72
|
+
|
73
|
+
# Returns the filename for export without extension.
|
74
|
+
# Defaults to "export"
|
75
|
+
def export_filename
|
76
|
+
@export_filename || 'export'
|
77
|
+
end
|
78
|
+
|
79
|
+
def xml_class
|
80
|
+
@xml_class || 'item'
|
81
|
+
end
|
82
|
+
|
83
|
+
# Creates a new data source object from the get parameters of the data-source
|
84
|
+
# request.
|
85
|
+
def self.from_params(params)
|
86
|
+
# Exract GDataSource params from the request.
|
87
|
+
gdata_params = {}
|
88
|
+
tqx = params[:tqx]
|
89
|
+
unless tqx.blank?
|
90
|
+
gdata_params[:tqx] = true
|
91
|
+
tqx.split(';').each do |kv|
|
92
|
+
key, value = kv.split(':')
|
93
|
+
gdata_params[key.to_sym] = value
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Create the appropriate GDataSource instance from the gdata-specific parameters
|
98
|
+
gdata_params[:out] ||= "json"
|
99
|
+
gdata = from_gdata_params(gdata_params)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Factory method to create a GDataSource instance from a serie of valid GData
|
103
|
+
# parameters, as described in the official documentation (see above links).
|
104
|
+
#
|
105
|
+
# +gdata_params+ can be any map-like object that maps keys (like +:out+, +:reqId+
|
106
|
+
# and so forth) to their values. Keys must be symbols.
|
107
|
+
def self.from_gdata_params(gdata_params)
|
108
|
+
case gdata_params[:out]
|
109
|
+
when "json"
|
110
|
+
JsonData.new(gdata_params)
|
111
|
+
when "html"
|
112
|
+
HtmlData.new(gdata_params)
|
113
|
+
when "csv"
|
114
|
+
CsvData.new(gdata_params)
|
115
|
+
when "xml"
|
116
|
+
XmlData.new(gdata_params)
|
117
|
+
else
|
118
|
+
InvalidData.new(gdata_params)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Access a GData parameter. +k+ must be symbols, like +:out+, +:reqId+.
|
123
|
+
def [](k)
|
124
|
+
@params[k]
|
125
|
+
end
|
126
|
+
|
127
|
+
# Sets a GData parameter. +k+ must be symbols, like +:out+, +:reqId+.
|
128
|
+
# The instance is re-validated afterward.
|
129
|
+
def []=(k, v)
|
130
|
+
@params[k] = v
|
131
|
+
validate
|
132
|
+
end
|
133
|
+
|
134
|
+
# Checks whether this instance is valid (in terms of configuration parameters)
|
135
|
+
# or not.
|
136
|
+
def valid?
|
137
|
+
@errors.size == 0
|
138
|
+
end
|
139
|
+
|
140
|
+
# Manually adds a new validation error. +key+ should be a symbol pointing
|
141
|
+
# to the invalid parameter or element.
|
142
|
+
def add_error(key, message)
|
143
|
+
@errors[key] = message
|
144
|
+
return self
|
145
|
+
end
|
146
|
+
|
147
|
+
# Sets the unformatted data of the datasource
|
148
|
+
# +data+ is an array either of Hash objects or arbitrary objects
|
149
|
+
# * Hashes must have keys as column ids
|
150
|
+
# * Arbitrary object must repond to column id named methods
|
151
|
+
#
|
152
|
+
def data=(data)
|
153
|
+
# reset formatted data
|
154
|
+
@data = nil
|
155
|
+
|
156
|
+
# set unformatted data
|
157
|
+
@raw_data = data
|
158
|
+
validate
|
159
|
+
|
160
|
+
# register virtual columns
|
161
|
+
if data.respond_to?(:add_virtual_column)
|
162
|
+
@virtual_columns.each { |k, v| data.add_virtual_column(k.to_sym, v[:type]) }
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# Returns the formatted data in the datasource format
|
167
|
+
#
|
168
|
+
def data
|
169
|
+
@data unless @data.nil?
|
170
|
+
|
171
|
+
# get data from object (eg. Reporting)
|
172
|
+
data = @raw_data.respond_to?(:data) ?
|
173
|
+
@raw_data.data(:required_columns => required_columns) :
|
174
|
+
@raw_data
|
175
|
+
|
176
|
+
# Run formatters and virtual columns
|
177
|
+
@data = data.collect do |row|
|
178
|
+
row = OpenStruct.new(row) if row.is_a?(Hash)
|
179
|
+
|
180
|
+
column_ids.inject([]) do |columns, column|
|
181
|
+
if is_virtual_column?(column)
|
182
|
+
columns << virtual_columns[column][:proc].call(row)
|
183
|
+
elsif has_formatter?(column)
|
184
|
+
columns << {
|
185
|
+
:f => formatters[column.to_sym].call(row),
|
186
|
+
:v => row.send(column)
|
187
|
+
}
|
188
|
+
else
|
189
|
+
columns << row.send(column)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
# Returns the ids of the columns
|
196
|
+
#
|
197
|
+
def column_ids
|
198
|
+
columns.collect(&:id)
|
199
|
+
end
|
200
|
+
|
201
|
+
# Sets the columns which should be sent by the datasource
|
202
|
+
# +columns+ is an array of either Hashes with keys (:id, :type, :label)
|
203
|
+
# or +Column+ objects
|
204
|
+
#
|
205
|
+
def columns=(columns)
|
206
|
+
@columns = columns.map { |c| c.is_a?(Column) ? c : Column.new(c) }
|
207
|
+
@columns.each do |col|
|
208
|
+
raise ArgumentError, "Invalid column type: #{col.type}" unless col.valid?
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
# Returns the columns in a datasource compatible format
|
213
|
+
# Applies all labels set
|
214
|
+
def columns
|
215
|
+
@columns.each { |c| c.label = column_labels.delete(c.id) if column_labels.has_key?(c.id) }
|
216
|
+
@columns
|
217
|
+
end
|
218
|
+
|
219
|
+
# Make cols alias for json ... classes
|
220
|
+
alias_method :cols, :columns
|
221
|
+
deprecate :cols
|
222
|
+
|
223
|
+
# Sets the raw data and the columns simultaniously and tries to guess the
|
224
|
+
# columns if not set.
|
225
|
+
# +data+ may be an array of rows or an object of a class that support a data(options)
|
226
|
+
# and a columns method
|
227
|
+
#
|
228
|
+
def set(data, columns = nil)
|
229
|
+
self.data = data
|
230
|
+
self.columns = columns || guess_columns(@raw_data)
|
231
|
+
end
|
232
|
+
|
233
|
+
# Tries to get a clever column selection from the items collection.
|
234
|
+
# Currently only accounts for ActiveRecord objects
|
235
|
+
# +items+ is an arbitrary collection of items as passed to the +set+ method
|
236
|
+
def guess_columns(data)
|
237
|
+
return data.columns if data.respond_to? :columns
|
238
|
+
|
239
|
+
columns = []
|
240
|
+
klass = data.first.class
|
241
|
+
klass.columns.each do |column|
|
242
|
+
columns << Column.new({
|
243
|
+
:id => column.name,
|
244
|
+
:label => column.name.humanize,
|
245
|
+
:type => 'string' # TODO get the right type
|
246
|
+
})
|
247
|
+
end
|
248
|
+
columns
|
249
|
+
end
|
250
|
+
|
251
|
+
# Validates this instance by checking that the configuration parameters
|
252
|
+
# conform to the official specs.
|
253
|
+
def validate
|
254
|
+
@errors.clear
|
255
|
+
|
256
|
+
# check validity
|
257
|
+
if @raw_data.respond_to?(:valid?) && ! @raw_data.valid?
|
258
|
+
add_error(:reqId, "Form validation failed")
|
259
|
+
end
|
260
|
+
|
261
|
+
if @params[:tqx]
|
262
|
+
add_error(:reqId, "Missing required parameter reqId") unless @params[:reqId]
|
263
|
+
|
264
|
+
if @params[:version] && @params[:version] != @version
|
265
|
+
add_error(:version, "Unsupported version #{@params[:version]}")
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
# Empty method. This is a placeholder implemented by subclasses that
|
271
|
+
# produce the response according to a given format.
|
272
|
+
def response
|
273
|
+
end
|
274
|
+
|
275
|
+
# Empty method. This is a placeholder implemented by subclasses that return the correct format
|
276
|
+
def format
|
277
|
+
self[:out]
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|