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