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,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
@@ -0,0 +1,7 @@
1
+ coverage/*
2
+ rdoc/*
3
+ doc/*
4
+ .yardoc
5
+ .bundle
6
+ pkg/*
7
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source "http://rubygems.org"
2
+ gemspec
3
+
4
+ gem "rparsec", :git => 'https://github.com/adyard/rparsec.git'
5
+
6
+ # Add dependencies to develop your gem here.
7
+ # Include everything needed to run rake, tests, features, etc.
8
+ group :development do
9
+ gem 'ci_reporter'
10
+ gem 'geminabox'
11
+ end
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Claas Abert, Tobias Schlottke
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,25 @@
1
+ = google_data_source
2
+
3
+ The google data source plugin consists of two parts:
4
+ * The Reporting engine is a flexible model toolkit to generate rich queries over multiple tables. It is able to transparently aggregate data from multiple tables, regroup it and prepare it for displaying
5
+ * The Datasource engine is the representation layer. It renders the data using the google datasource standard (http://code.google.com/intl/de-DE/apis/charttools/index.html). This makes visualizing the data quite easy.
6
+
7
+ == TODO
8
+ * Separate Reporting & Datasource logic into two independent plugins
9
+ * Add Javascripts and howtos
10
+
11
+ == Contributing to google_data_source
12
+
13
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
14
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
15
+ * Fork the project
16
+ * Start a feature/bugfix branch
17
+ * Commit and push until you are happy with your contribution
18
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
19
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
20
+
21
+ == Copyright
22
+
23
+ Copyright (c) 2011 Tobias Schlottke. See LICENSE.txt for
24
+ further details.
25
+
@@ -0,0 +1,31 @@
1
+ require 'rubygems'
2
+ require 'bundler/gem_tasks'
3
+ require 'rake'
4
+
5
+ require 'rake/testtask'
6
+ task :test => ['test:environment', 'test:units', 'test:rails']
7
+ namespace :test do
8
+
9
+ task :environment do
10
+ ENV['RACK_ENV'] = 'test'
11
+ end
12
+
13
+ [ :units, :rails ].each do |category|
14
+ Rake::TestTask.new(category) do |t|
15
+ t.libs << "lib" << "test"
16
+ t.test_files = Dir["test/#{category}/**/*_test.rb"]
17
+ t.verbose = true
18
+ end
19
+ end
20
+ end
21
+
22
+
23
+ task :default => :test
24
+
25
+ require 'rake/rdoctask'
26
+ Rake::RDocTask.new do |rdoc|
27
+ rdoc.rdoc_dir = 'rdoc'
28
+ rdoc.title = "google_data_source #{GoogleDataSource::VERSION}"
29
+ rdoc.rdoc_files.include('README*')
30
+ rdoc.rdoc_files.include('lib/**/*.rb')
31
+ end
@@ -0,0 +1,32 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/google_data_source/version', __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = %q{google_data_source}
6
+ s.version = GoogleDataSource::VERSION
7
+
8
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
9
+ s.authors = [%q{Claas Abert}, %q{Tobias Schlottke}]
10
+ s.summary = %q{A highly sophisticated reporting framework}
11
+ s.description = %q{A highly sophisticated reporting framework}
12
+ s.email = %q{tobias.schlottke@metrigo.de}
13
+
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
+ s.require_paths = ["lib"]
18
+
19
+ s.add_runtime_dependency(%q<activesupport>, ["~> 3.1.4"])
20
+ s.add_runtime_dependency(%q<activerecord>, ["~> 3.1.4"])
21
+ s.add_runtime_dependency(%q<actionpack>, ["~> 3.1.4"])
22
+ s.add_runtime_dependency(%q<rails>, ["~> 3.1.4"])
23
+ s.add_runtime_dependency(%q<nokogiri>, [">= 0"])
24
+
25
+ s.add_development_dependency(%q<test-unit>, [">= 0"])
26
+ s.add_development_dependency(%q<shoulda>, [">= 0"])
27
+ s.add_development_dependency(%q<mocha>, [">= 0.9"])
28
+ s.add_development_dependency(%q<rparsec>, [">= 1.0.1"])
29
+ s.add_development_dependency(%q<sqlite3>, [">= 0"])
30
+
31
+ end
32
+
@@ -0,0 +1,3 @@
1
+ if (typeof window.DataSource == 'undefined') window.DataSource = new Object();
2
+
3
+ google.load('visualization', '1', {'packages': ['table', 'annotatedtimeline', 'linechart', 'charteditor']});
@@ -0,0 +1,76 @@
1
+ /**
2
+ * DataTable subclass with extended features
3
+ * * Handling of a sum row
4
+ * * Case insensitive sorting
5
+ */
6
+ function ExtendedDataTable(data, options) {
7
+ if (typeof data.toJSON == 'function') data = data.toJSON();
8
+ this.options = options || {};
9
+ google.visualization.DataTable.call(this, data, this.options['version']);
10
+ if (this.getNumberOfRows() < 1) return;
11
+ if (this.options['sumRow']) this._setSumRowStyle();
12
+ }
13
+
14
+ google.load('visualization', '1', {'callback' : function() {
15
+ ExtendedDataTable.prototype = new google.visualization.DataTable();
16
+ ExtendedDataTable.prototype._getSumRowIndex = function() {
17
+ return this.getNumberOfRows() - 1;
18
+ };
19
+
20
+ ExtendedDataTable.prototype._setSumRowStyle = function() {
21
+ for (var i=0; i<this.getNumberOfColumns(); i++) {
22
+ this.setProperty(this._getSumRowIndex(), i, 'style', 'font-weight: bold; background: #ccc;');
23
+ }
24
+ };
25
+
26
+ ExtendedDataTable.prototype.setOption = function(name, value) {
27
+ this.options[name] = value;
28
+ };
29
+
30
+ // Override the getSortedRows method to keep the sum row in place while sorting
31
+ // This also affects the sort method
32
+ ExtendedDataTable.prototype.getSortedRows = function(sortColumns) {
33
+ var sorted = [];
34
+ // use standard sorting if caseSensitive option is set
35
+ if (this.options['caseSensitive']) {
36
+ sorted = google.visualization.DataTable.prototype.getSortedRows.call(this, sortColumns);
37
+ // use custom sort method if caseSensitive option is not set
38
+ } else {
39
+ if (sortColumns instanceof Array) sortColumns = sortColumns[0];
40
+ if (typeof sortColumns == 'number') sortColumns = {'column': sortColumns, 'desc': false};
41
+ var column = sortColumns['column'];
42
+ var desc = sortColumns['desc'];
43
+
44
+ var toSort = [];
45
+ for (var i=0; i<this.getNumberOfRows(); i++) {
46
+ toSort.push([i, this.getValue(i, column)]);
47
+ }
48
+ toSort.sort(function(a, b) {
49
+ //TODO use better check for String value
50
+ var aa = (a[1] && a[1].toLowerCase) ? a[1].toLowerCase() : a[1];
51
+ var bb = (b[1] && b[1].toLowerCase) ? b[1].toLowerCase() : b[1];
52
+ var result = (aa < bb) ? 1 : -1;
53
+ return desc ? result : -result;
54
+ });
55
+ for (var i=0; i<toSort.length; i++) {
56
+ sorted.push(toSort[i][0]);
57
+ }
58
+ }
59
+
60
+ if (!this.options['sumRow']) return sorted;
61
+
62
+ // pull sum row out of the sorted bunch and put it at the end
63
+ var result = [];
64
+ var sumRowIndex = this._getSumRowIndex();
65
+ for (var i=0; i<sorted.length; i++) {
66
+ if (sorted[i] != sumRowIndex) result.push(sorted[i]);
67
+ }
68
+ result.push(sumRowIndex);
69
+ return result;
70
+ };
71
+ }});
72
+
73
+
74
+
75
+
76
+
@@ -0,0 +1,180 @@
1
+ /*
2
+ * Helper Methods for the form serialization to the Google query language.
3
+ * Each field becomes a condition, the conditions are connected with 'and'.
4
+ * Fields whose names start on 'from_' or 'to_' are translated to '<' and '>' comparisons.
5
+ * Multiparameter field (date) are put in a single parameter.
6
+ */
7
+ DataSource.FilterForm = function(form) {
8
+ this._form = $(form);
9
+ var id = this._form[0].id;
10
+ // load hooks
11
+ this._hooks = DataSource.FilterForm._hooks[id] || {};
12
+ }
13
+
14
+ DataSource.FilterForm._hooks = {};
15
+ DataSource.FilterForm.setHooks = function(id, hooks) {
16
+ DataSource.FilterForm._hooks[id] = hooks;
17
+ };
18
+
19
+ DataSource.FilterForm.prototype = {
20
+ getNoQueryValues: function(form) {
21
+ var result = [];
22
+ var formdata = this._form.serializeArray();
23
+ $.each(this._form.find('input.noquery'), function(i, input) {
24
+ $.each(formdata, function(i, pair) {
25
+ if (pair.name == input.name && ! $(input).hasClass('ignore')) {
26
+ result.push(pair);
27
+ }
28
+ });
29
+ });
30
+ return jQuery.param(result);
31
+ },
32
+
33
+ getValues: function() {
34
+ var formdata = this._form.serializeArray();
35
+ $.each(this._form.find('input.noquery'), function(i, input) {
36
+ var tmp = [];
37
+ $.each(formdata, function(i, pair) {
38
+ if (pair.name != input.name) {
39
+ tmp.push(pair);
40
+ }
41
+ });
42
+ formdata = tmp;
43
+ });
44
+
45
+ var values = {};
46
+ $.each(formdata, function(i, pair) {
47
+ // use only the key if field is named model[key]
48
+ var match = pair.name.match(/\[([^\[\]]+)\](\[\])?$/);
49
+ if (match) pair.name = match[1] + (match[2] || '');
50
+
51
+ // multiparameter
52
+ var match;
53
+ if (match = pair.name.match(/^(.*)\(([0-9]+)i\)$/)) {
54
+ var name = match[1];
55
+ var part = match[2];
56
+ if (typeof values[name] == "undefined") values[name] = {'multiparameter': true};
57
+ values[name][part] = pair.value;
58
+ }
59
+ // multiselect
60
+ else if(match = pair.name.match(/^(.*)\[\]$/)) {
61
+ var name = match[1];
62
+ if (typeof values[name] == "undefined") {
63
+ values[name] = [];
64
+ values[name].multiselect = true;
65
+ }
66
+ values[name].push(pair.value);
67
+ }
68
+ // no multiparam field
69
+ else {
70
+ values[pair.name] = pair.value;
71
+ }
72
+ });
73
+
74
+ var result = {};
75
+ $.each(values, function(key, value) {
76
+ // convert multiparams field from hash to array
77
+ if (value.multiparameter) {
78
+ var i = 1;
79
+ var subvalues = [];
80
+ while (typeof value['' + i] != 'undefined') {
81
+ subvalues.push(value[''+i]);
82
+ i++;
83
+ }
84
+ result[key] = subvalues;
85
+ result[key].multiparameter = true;
86
+ }
87
+ // store non multiparam fields
88
+ else {
89
+ result[key] = value;
90
+ }
91
+ });
92
+ return result;
93
+ },
94
+
95
+ toWhereClause: function() {
96
+ var conditions = [];
97
+ // putting together the query
98
+ var values = this.getValues();
99
+ delete values.groupby;
100
+ delete values.select;
101
+ $.each(values, function(key, value) {
102
+ // handle dates
103
+ if (value.multiparameter) {
104
+ if (value[0] && value[1] && value[2]) {
105
+ value = value.join('-');
106
+ }
107
+ else {
108
+ // TODO continue?
109
+ value = '';
110
+ }
111
+ }
112
+
113
+ // default operator
114
+ var operator = '='
115
+
116
+
117
+ if (value.multiselect) {
118
+ value = value.join(',');
119
+ }
120
+
121
+ // greater operator
122
+ var match = key.match(/^from_(.*)$/)
123
+ if (match) {key = match[1];operator = '>=';}
124
+
125
+ // less operator
126
+ var match = key.match(/^to_(.*)$/)
127
+ if (match) {key = match[1];operator = '<=';}
128
+
129
+ // in operator
130
+ var match = key.match(/^in_(.*)$/)
131
+ if (match) {key = match[1];operator = 'in';}
132
+
133
+ if (value == '') {
134
+ // skip empty values
135
+ return;
136
+ }
137
+
138
+ if (operator == 'in') {
139
+ var values = value.match(/([^\s;,]+)/g);
140
+ for (var i=0; i<values.length; i++) {
141
+ values[i] = '\'' + values[i].replace('\\', '\\\\').replace('\'','\\\'') + '\'';
142
+ }
143
+ conditions.push('`' + key + '` in (' + values.join(',') + ')');
144
+ } else {
145
+ conditions.push('`' + key + '`' + operator + '\'' + value.replace('\\', '\\\\').replace('\'','\\\'') + '\'');
146
+ }
147
+ });
148
+ return (conditions.length > 0) ? 'where ' + conditions.join(' and ') : '';
149
+ },
150
+
151
+ getGroupByColumns: function() {
152
+ var result = this.getValues().groupby || [];
153
+ if (!$.isArray(result)) result = [result];
154
+ return result;
155
+ },
156
+
157
+ toGroupByClause: function() {
158
+ var hook = this._hooks.groupby || this.getGroupByColumns;
159
+ var escaped = [];
160
+ $.each(hook.call(this), function(i, column) {
161
+ if (column != '') escaped.push('`' + column + '`');
162
+ });
163
+ return (escaped.length > 0) ? 'group by ' + escaped.join(', ') : '';
164
+ },
165
+
166
+ getSelectColumns: function() {
167
+ var result = this.getValues().select || [];
168
+ if (!$.isArray(result)) result = [result];
169
+ return result;
170
+ },
171
+
172
+ toSelectClause: function() {
173
+ var hook = this._hooks.select || this.getSelectColumns;
174
+ var escaped = [];
175
+ $.each(hook.call(this), function(i, column) {
176
+ if (column != '') escaped.push('`' + column + '`');
177
+ });
178
+ return (escaped.length > 0) ? 'select ' + escaped.join(', ') : '';
179
+ }
180
+ }
@@ -0,0 +1,113 @@
1
+ DataSource.ComboTable = function(query, container, options) {
2
+ DataSource.Table.call(this, query, container, options);
3
+ this.activeGraphed = [ 0 ];
4
+ }
5
+
6
+
7
+ DataSource.ComboTable.prototype = new DataSource.Table();
8
+
9
+ DataSource.ComboTable.prototype.edit = function(){
10
+ if (typeof this.chartWrapper == 'undefined') {
11
+ return;
12
+ }
13
+ // Handler for the "Open Editor" button.
14
+ var editor = new google.visualization.ChartEditor();
15
+ self = this;
16
+ google.visualization.events.addListener(editor, 'ok', function() {
17
+ editor.getChartWrapper().draw(self.graphContainer);
18
+
19
+ self.drawEditButton();
20
+ });
21
+ editor.openDialog(this.chartWrapper);
22
+ }
23
+
24
+ DataSource.ComboTable.prototype.drawEditButton = function(){
25
+ var edit = document.createElement('a');
26
+ jQuery(edit).addClass('graphEditButton');
27
+ jQuery(this.graphContainer).append(edit);
28
+ var self = this;
29
+ jQuery(edit).click(function(){
30
+ self.edit();
31
+ })
32
+ }
33
+ // draw the combotable
34
+ DataSource.ComboTable.prototype.draw = function() {
35
+ //draw the table itself
36
+ this.visualization.draw(this.currentDataTable, this.options);
37
+
38
+
39
+ // add a chart
40
+ var self = this;
41
+
42
+ window.setTimeout(function(){
43
+ jQuery(self.container).find('.google-visualization-table-th').each(function(){
44
+ var a = document.createElement('img')
45
+ jQuery(a).addClass('graphIcon')
46
+ a.src = '<%= asset_path('google_data_source/chart_bar_add.png') %>'
47
+
48
+ a.onclick = function(event){
49
+ // find out which column is being clicked
50
+ var clicked = this.parentNode;
51
+ var i = 0;
52
+
53
+ // find all active columns
54
+ jQuery(self.container).find('.google-visualization-table-th').each(function() {
55
+ if (this == clicked) {
56
+ // only add the column if it is not already in
57
+ var activeIndex = false;
58
+ for (var col=0; col < self.activeGraphed.length; col++) {
59
+ if (self.activeGraphed[col] == i) {
60
+ activeIndex = col;
61
+ }
62
+ }
63
+
64
+ // remove the deleted items, add the active
65
+ if (!activeIndex) {
66
+ self.activeGraphed.push(i);
67
+ } else {
68
+ self.activeGraphed.splice(activeIndex, activeIndex);
69
+ }
70
+ }
71
+ var active = false;
72
+
73
+ i++;
74
+ });
75
+ var dataView = new google.visualization.DataView(self.currentDataTable);
76
+ dataView.setColumns(self.activeGraphed.sort());
77
+
78
+
79
+ // hide the last row as it is the sum row
80
+ var lastRow = dataView.getViewRows().length - 1;
81
+ if (lastRow > 0) {
82
+ dataView.hideRows([ lastRow ]);
83
+ }
84
+
85
+
86
+ // create a graph container
87
+ if (typeof self.graphContainer == 'undefined') {
88
+ self.graphContainer = document.createElement('div');
89
+ self.container.parentNode.insertBefore(self.graphContainer, self.container);
90
+ }
91
+
92
+ //actually draw the chart
93
+ var wrapper = new google.visualization.ChartWrapper({
94
+ chartType: 'LineChart',
95
+ dataTable: dataView,
96
+ width: $(self.graphContainer).width(),
97
+ height: 200
98
+ });
99
+ wrapper.draw(self.graphContainer);
100
+
101
+ self.chartWrapper = wrapper;
102
+ }
103
+
104
+ this.appendChild(a);
105
+ })
106
+
107
+ for (var col=0; col < self.activeGraphed.length; col++) {
108
+ jQuery(self.container).find(".google-visualization-table-th:nth("+self.activeGraphed[col]+") img").attr('src', '<%= asset_path('google_data_source/chart_bar_delete.png') %>')
109
+ }
110
+
111
+ self.drawEditButton();
112
+ }, 100);
113
+ }