railsblazer 2.0.0

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 (107) hide show
  1. checksums.yaml +7 -0
  2. data/.gitattributes +1 -0
  3. data/.github/ISSUE_TEMPLATE.md +0 -0
  4. data/.gitignore +14 -0
  5. data/CHANGELOG.md +247 -0
  6. data/CONTRIBUTING.md +42 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +855 -0
  10. data/Rakefile +1 -0
  11. data/app/assets/fonts/blazer/glyphicons-halflings-regular.eot +0 -0
  12. data/app/assets/fonts/blazer/glyphicons-halflings-regular.svg +288 -0
  13. data/app/assets/fonts/blazer/glyphicons-halflings-regular.ttf +0 -0
  14. data/app/assets/fonts/blazer/glyphicons-halflings-regular.woff +0 -0
  15. data/app/assets/fonts/blazer/glyphicons-halflings-regular.woff2 +0 -0
  16. data/app/assets/javascripts/blazer/Chart.js +14145 -0
  17. data/app/assets/javascripts/blazer/Sortable.js +1144 -0
  18. data/app/assets/javascripts/blazer/ace.js +6 -0
  19. data/app/assets/javascripts/blazer/ace/ace.js +11 -0
  20. data/app/assets/javascripts/blazer/ace/ext-language_tools.js +5 -0
  21. data/app/assets/javascripts/blazer/ace/mode-sql.js +1 -0
  22. data/app/assets/javascripts/blazer/ace/snippets/sql.js +1 -0
  23. data/app/assets/javascripts/blazer/ace/snippets/text.js +1 -0
  24. data/app/assets/javascripts/blazer/ace/theme-twilight.js +1 -0
  25. data/app/assets/javascripts/blazer/application.js +79 -0
  26. data/app/assets/javascripts/blazer/bootstrap.js +2366 -0
  27. data/app/assets/javascripts/blazer/chartkick.js +1693 -0
  28. data/app/assets/javascripts/blazer/daterangepicker.js +1505 -0
  29. data/app/assets/javascripts/blazer/fuzzysearch.js +24 -0
  30. data/app/assets/javascripts/blazer/highlight.pack.js +1 -0
  31. data/app/assets/javascripts/blazer/jquery.js +10308 -0
  32. data/app/assets/javascripts/blazer/jquery.stickytableheaders.js +263 -0
  33. data/app/assets/javascripts/blazer/jquery_ujs.js +469 -0
  34. data/app/assets/javascripts/blazer/moment-timezone.js +1007 -0
  35. data/app/assets/javascripts/blazer/moment.js +3043 -0
  36. data/app/assets/javascripts/blazer/queries.js +110 -0
  37. data/app/assets/javascripts/blazer/routes.js +23 -0
  38. data/app/assets/javascripts/blazer/selectize.js +3667 -0
  39. data/app/assets/javascripts/blazer/stupidtable.js +114 -0
  40. data/app/assets/javascripts/blazer/vue.js +7515 -0
  41. data/app/assets/stylesheets/blazer/application.css +198 -0
  42. data/app/assets/stylesheets/blazer/bootstrap.css.erb +6202 -0
  43. data/app/assets/stylesheets/blazer/daterangepicker-bs3.css +375 -0
  44. data/app/assets/stylesheets/blazer/github.css +125 -0
  45. data/app/assets/stylesheets/blazer/selectize.default.css +387 -0
  46. data/app/controllers/blazer/base_controller.rb +113 -0
  47. data/app/controllers/blazer/checks_controller.rb +56 -0
  48. data/app/controllers/blazer/dashboards_controller.rb +105 -0
  49. data/app/controllers/blazer/queries_controller.rb +337 -0
  50. data/app/helpers/blazer/base_helper.rb +57 -0
  51. data/app/mailers/blazer/check_mailer.rb +27 -0
  52. data/app/mailers/blazer/slack_notifier.rb +76 -0
  53. data/app/models/blazer/audit.rb +6 -0
  54. data/app/models/blazer/check.rb +104 -0
  55. data/app/models/blazer/connection.rb +5 -0
  56. data/app/models/blazer/dashboard.rb +13 -0
  57. data/app/models/blazer/dashboard_query.rb +9 -0
  58. data/app/models/blazer/query.rb +40 -0
  59. data/app/models/blazer/record.rb +5 -0
  60. data/app/views/blazer/_nav.html.erb +16 -0
  61. data/app/views/blazer/_variables.html.erb +102 -0
  62. data/app/views/blazer/check_mailer/failing_checks.html.erb +6 -0
  63. data/app/views/blazer/check_mailer/state_change.html.erb +47 -0
  64. data/app/views/blazer/checks/_form.html.erb +79 -0
  65. data/app/views/blazer/checks/edit.html.erb +1 -0
  66. data/app/views/blazer/checks/index.html.erb +43 -0
  67. data/app/views/blazer/checks/new.html.erb +1 -0
  68. data/app/views/blazer/dashboards/_form.html.erb +76 -0
  69. data/app/views/blazer/dashboards/edit.html.erb +1 -0
  70. data/app/views/blazer/dashboards/new.html.erb +1 -0
  71. data/app/views/blazer/dashboards/show.html.erb +47 -0
  72. data/app/views/blazer/queries/_form.html.erb +240 -0
  73. data/app/views/blazer/queries/edit.html.erb +2 -0
  74. data/app/views/blazer/queries/home.html.erb +152 -0
  75. data/app/views/blazer/queries/new.html.erb +2 -0
  76. data/app/views/blazer/queries/run.html.erb +165 -0
  77. data/app/views/blazer/queries/schema.html.erb +20 -0
  78. data/app/views/blazer/queries/show.html.erb +73 -0
  79. data/app/views/layouts/blazer/application.html.erb +24 -0
  80. data/blazer-0.0.1.gem +0 -0
  81. data/blazer.gemspec +27 -0
  82. data/config/routes.rb +16 -0
  83. data/lib/blazer.rb +223 -0
  84. data/lib/blazer/adapters/athena_adapter.rb +128 -0
  85. data/lib/blazer/adapters/base_adapter.rb +53 -0
  86. data/lib/blazer/adapters/bigquery_adapter.rb +68 -0
  87. data/lib/blazer/adapters/cassandra_adapter.rb +59 -0
  88. data/lib/blazer/adapters/drill_adapter.rb +28 -0
  89. data/lib/blazer/adapters/druid_adapter.rb +67 -0
  90. data/lib/blazer/adapters/elasticsearch_adapter.rb +46 -0
  91. data/lib/blazer/adapters/mongodb_adapter.rb +39 -0
  92. data/lib/blazer/adapters/presto_adapter.rb +45 -0
  93. data/lib/blazer/adapters/snowflake_adapter.rb +73 -0
  94. data/lib/blazer/adapters/sql_adapter.rb +182 -0
  95. data/lib/blazer/data_source.rb +195 -0
  96. data/lib/blazer/detect_anomalies.R +19 -0
  97. data/lib/blazer/engine.rb +30 -0
  98. data/lib/blazer/result.rb +170 -0
  99. data/lib/blazer/run_statement.rb +40 -0
  100. data/lib/blazer/run_statement_job.rb +21 -0
  101. data/lib/blazer/version.rb +3 -0
  102. data/lib/generators/blazer/install_generator.rb +39 -0
  103. data/lib/generators/blazer/templates/config.yml.tt +62 -0
  104. data/lib/generators/blazer/templates/install.rb.tt +46 -0
  105. data/lib/tasks/blazer.rake +11 -0
  106. data/railsblazer-0.0.1.gem +0 -0
  107. metadata +234 -0
@@ -0,0 +1,56 @@
1
+ module Blazer
2
+ class ChecksController < BaseController
3
+ before_action :set_check, only: [:edit, :update, :destroy, :run]
4
+
5
+ def index
6
+ state_order = [nil, "disabled", "error", "timed out", "failing", "passing"]
7
+ @checks = Blazer::Check.joins(:query).includes(:query).order("blazer_queries.name, blazer_checks.id").to_a.sort_by { |q| state_order.index(q.state) || 99 }
8
+ @checks.select! { |c| "#{c.query.name} #{c.emails}".downcase.include?(params[:q]) } if params[:q]
9
+ end
10
+
11
+ def new
12
+ @check = Blazer::Check.new(query_id: params[:query_id])
13
+ end
14
+
15
+ def create
16
+ @check = Blazer::Check.new(check_params)
17
+ # use creator_id instead of creator
18
+ # since we setup association without checking if column exists
19
+ @check.creator = blazer_user if @check.respond_to?(:creator_id=) && blazer_user
20
+
21
+ if @check.save
22
+ redirect_to query_path(@check.query)
23
+ else
24
+ render_errors @check
25
+ end
26
+ end
27
+
28
+ def update
29
+ if @check.update(check_params)
30
+ redirect_to query_path(@check.query)
31
+ else
32
+ render_errors @check
33
+ end
34
+ end
35
+
36
+ def destroy
37
+ @check.destroy
38
+ redirect_to checks_path
39
+ end
40
+
41
+ def run
42
+ @query = @check.query
43
+ redirect_to query_path(@query)
44
+ end
45
+
46
+ private
47
+
48
+ def check_params
49
+ params.require(:check).permit(:query_id, :emails, :slack_channels, :invert, :check_type, :schedule)
50
+ end
51
+
52
+ def set_check
53
+ @check = Blazer::Check.find(params[:id])
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,105 @@
1
+ module Blazer
2
+ class DashboardsController < BaseController
3
+ before_action :set_dashboard, only: [:show, :edit, :update, :destroy, :refresh]
4
+
5
+ def index
6
+ redirect_to root_path(filter: "dashboards")
7
+ end
8
+
9
+ def new
10
+ @dashboard = Blazer::Dashboard.new
11
+ end
12
+
13
+ def create
14
+ @dashboard = Blazer::Dashboard.new
15
+ # use creator_id instead of creator
16
+ # since we setup association without checking if column exists
17
+ @dashboard.creator = blazer_user if @dashboard.respond_to?(:creator_id=) && blazer_user
18
+
19
+ if update_dashboard(@dashboard)
20
+ redirect_to dashboard_path(@dashboard)
21
+ else
22
+ render_errors @dashboard
23
+ end
24
+ end
25
+
26
+ def show
27
+ @queries = @dashboard.dashboard_queries.order(:position).preload(:query).map(&:query)
28
+ @queries.each do |query|
29
+ process_vars(query.statement, query.data_source)
30
+ end
31
+ @bind_vars ||= []
32
+
33
+ @smart_vars = {}
34
+ @sql_errors = []
35
+ @data_sources = @queries.map { |q| Blazer.data_sources[q.data_source] }.uniq
36
+ @bind_vars.each do |var|
37
+ @data_sources.each do |data_source|
38
+ smart_var, error = parse_smart_variables(var, data_source)
39
+ ((@smart_vars[var] ||= []).concat(smart_var)).uniq! if smart_var
40
+ @sql_errors << error if error
41
+ end
42
+ end
43
+ end
44
+
45
+ def edit
46
+ end
47
+
48
+ def update
49
+ if update_dashboard(@dashboard)
50
+ redirect_to dashboard_path(@dashboard, variable_params)
51
+ else
52
+ render_errors @dashboard
53
+ end
54
+ end
55
+
56
+ def destroy
57
+ @dashboard.destroy
58
+ redirect_to dashboards_path
59
+ end
60
+
61
+ def refresh
62
+ @dashboard.queries.each do |query|
63
+ data_source = Blazer.data_sources[query.data_source]
64
+ statement = query.statement.dup
65
+ process_vars(statement, query.data_source)
66
+ Blazer.transform_statement.call(data_source, statement) if Blazer.transform_statement
67
+ data_source.clear_cache(statement)
68
+ end
69
+ redirect_to dashboard_path(@dashboard, variable_params)
70
+ end
71
+
72
+ private
73
+
74
+ def dashboard_params
75
+ params.require(:dashboard).permit(:name)
76
+ end
77
+
78
+ def set_dashboard
79
+ @dashboard = Blazer::Dashboard.find(params[:id])
80
+ end
81
+
82
+ def update_dashboard(dashboard)
83
+ dashboard.assign_attributes(dashboard_params)
84
+ Blazer::Dashboard.transaction do
85
+ if params[:query_ids].is_a?(Array)
86
+ query_ids = params[:query_ids].map(&:to_i)
87
+ @queries = Blazer::Query.find(query_ids).sort_by { |q| query_ids.index(q.id) }
88
+ end
89
+ if dashboard.save
90
+ if @queries
91
+ @queries.each_with_index do |query, i|
92
+ dashboard_query = dashboard.dashboard_queries.where(query_id: query.id).first_or_initialize
93
+ dashboard_query.position = i
94
+ dashboard_query.save!
95
+ end
96
+ if dashboard.persisted?
97
+ dashboard.dashboard_queries.where.not(query_id: query_ids).destroy_all
98
+ end
99
+ end
100
+ true
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,337 @@
1
+ module Blazer
2
+ class QueriesController < BaseController
3
+ before_action :set_query, only: [:show, :edit, :update, :destroy, :refresh]
4
+ before_action :set_data_source, only: [:tables, :schema, :cancel]
5
+
6
+ def home
7
+ if params[:filter] == "dashboards"
8
+ @queries = []
9
+ else
10
+ set_queries(1000)
11
+ end
12
+
13
+ if params[:filter] && params[:filter] != "dashboards"
14
+ @dashboards = [] # TODO show my dashboards
15
+ else
16
+ @dashboards = Blazer::Dashboard.order(:name)
17
+ @dashboards = @dashboards.includes(:creator) if Blazer.user_class
18
+ end
19
+
20
+ @dashboards =
21
+ @dashboards.map do |d|
22
+ {
23
+ id: d.id,
24
+ name: d.name,
25
+ creator: blazer_user && d.try(:creator) == blazer_user ? "You" : d.try(:creator).try(Blazer.user_name),
26
+ to_param: d.to_param,
27
+ dashboard: true
28
+ }
29
+ end
30
+ end
31
+
32
+ def index
33
+ set_queries
34
+ render json: @queries
35
+ end
36
+
37
+ def new
38
+ @query = Blazer::Query.new(
39
+ data_source: params[:data_source],
40
+ name: params[:name]
41
+ )
42
+ if params[:fork_query_id]
43
+ @query.statement ||= Blazer::Query.find(params[:fork_query_id]).try(:statement)
44
+ end
45
+ end
46
+
47
+ def create
48
+ @query = Blazer::Query.new(query_params)
49
+ @query.creator = blazer_user if @query.respond_to?(:creator)
50
+
51
+ if @query.save
52
+ redirect_to query_path(@query, variable_params)
53
+ else
54
+ render_errors @query
55
+ end
56
+ end
57
+
58
+ def show
59
+ @statement = @query.statement.dup
60
+ process_vars(@statement, @query.data_source)
61
+
62
+ @smart_vars = {}
63
+ @sql_errors = []
64
+ data_source = Blazer.data_sources[@query.data_source]
65
+ @bind_vars.each do |var|
66
+ smart_var, error = parse_smart_variables(var, data_source)
67
+ @smart_vars[var] = smart_var if smart_var
68
+ @sql_errors << error if error
69
+ end
70
+
71
+ Blazer.transform_statement.call(data_source, @statement) if Blazer.transform_statement
72
+ end
73
+
74
+ def edit
75
+ end
76
+
77
+ def run
78
+ @statement = params[:statement]
79
+ data_source = params[:data_source]
80
+ process_vars(@statement, data_source)
81
+ @only_chart = params[:only_chart]
82
+ @run_id = blazer_params[:run_id]
83
+ @query = Query.find_by(id: params[:query_id]) if params[:query_id]
84
+ data_source = @query.data_source if @query && @query.data_source
85
+ @data_source = Blazer.data_sources[data_source]
86
+
87
+ # ensure viewable
88
+ if !(@query || Query.new(data_source: @data_source.id)).viewable?(blazer_user)
89
+ render_forbidden
90
+ elsif @run_id
91
+ @timestamp = blazer_params[:timestamp].to_i
92
+
93
+ @result = @data_source.run_results(@run_id)
94
+ @success = !@result.nil?
95
+
96
+ if @success
97
+ @data_source.delete_results(@run_id)
98
+ @columns = @result.columns
99
+ @rows = @result.rows
100
+ @error = @result.error
101
+ @just_cached = !@result.error && @result.cached_at.present?
102
+ @cached_at = nil
103
+ params[:data_source] = nil
104
+ render_run
105
+ elsif Time.now > Time.at(@timestamp + (@data_source.timeout || 600).to_i + 5)
106
+ # query lost
107
+ Rails.logger.info "[blazer lost query] #{@run_id}"
108
+ @error = "We lost your query :("
109
+ @rows = []
110
+ @columns = []
111
+ render_run
112
+ else
113
+ continue_run
114
+ end
115
+ elsif @success
116
+ @run_id = blazer_run_id
117
+
118
+ options = {user: blazer_user, query: @query, refresh_cache: params[:check], run_id: @run_id, async: Blazer.async}
119
+ if Blazer.async && request.format.symbol != :csv
120
+ result = []
121
+ Blazer::RunStatementJob.perform_async(result, @data_source, @statement, options)
122
+ wait_start = Time.now
123
+ loop do
124
+ sleep(0.02)
125
+ break if result.any? || Time.now - wait_start > 3
126
+ end
127
+ @result = result.first
128
+ else
129
+ @result = Blazer::RunStatement.new.perform(@data_source, @statement, options)
130
+ end
131
+
132
+ if @result
133
+ @data_source.delete_results(@run_id) if @run_id
134
+
135
+ @columns = @result.columns
136
+ @rows = @result.rows
137
+ @error = @result.error
138
+ @cached_at = @result.cached_at
139
+ @just_cached = @result.just_cached
140
+
141
+ render_run
142
+ else
143
+ @timestamp = Time.now.to_i
144
+ continue_run
145
+ end
146
+ else
147
+ render layout: false
148
+ end
149
+ end
150
+
151
+ def refresh
152
+ data_source = Blazer.data_sources[@query.data_source]
153
+ @statement = @query.statement.dup
154
+ process_vars(@statement, @query.data_source)
155
+ Blazer.transform_statement.call(data_source, @statement) if Blazer.transform_statement
156
+ data_source.clear_cache(@statement)
157
+ redirect_to query_path(@query, variable_params)
158
+ end
159
+
160
+ def update
161
+ if params[:commit] == "Fork"
162
+ @query = Blazer::Query.new
163
+ @query.creator = blazer_user if @query.respond_to?(:creator)
164
+ end
165
+ unless @query.editable?(blazer_user)
166
+ @query.errors.add(:base, "Sorry, permission denied")
167
+ end
168
+ if @query.errors.empty? && @query.update(query_params)
169
+ redirect_to query_path(@query, variable_params)
170
+ else
171
+ render_errors @query
172
+ end
173
+ end
174
+
175
+ def destroy
176
+ @query.destroy if @query.editable?(blazer_user)
177
+ redirect_to root_url
178
+ end
179
+
180
+ def tables
181
+ render json: @data_source.tables
182
+ end
183
+
184
+ def schema
185
+ @schema = @data_source.schema
186
+ end
187
+
188
+ def cancel
189
+ @data_source.cancel(blazer_run_id)
190
+ head :ok
191
+ end
192
+
193
+ private
194
+
195
+ def set_data_source
196
+ @data_source = Blazer.data_sources[params[:data_source]]
197
+
198
+ unless Query.new(data_source: @data_source.id).editable?(blazer_user)
199
+ render_forbidden
200
+ end
201
+ end
202
+
203
+ def continue_run
204
+ render json: {run_id: @run_id, timestamp: @timestamp}, status: :accepted
205
+ end
206
+
207
+ def render_run
208
+ @checks = @query ? @query.checks.order(:id) : []
209
+
210
+ @first_row = @rows.first || []
211
+ @column_types = []
212
+ if @rows.any?
213
+ @columns.each_with_index do |_, i|
214
+ @column_types << (
215
+ case @first_row[i]
216
+ when Integer
217
+ "int"
218
+ when Float, BigDecimal
219
+ "float"
220
+ else
221
+ "string-ins"
222
+ end
223
+ )
224
+ end
225
+ end
226
+
227
+ @filename = @query.name.parameterize if @query
228
+ @min_width_types = @columns.each_with_index.select { |c, i| @first_row[i].is_a?(Time) || @first_row[i].is_a?(String) || @data_source.smart_columns[c] }.map(&:last)
229
+
230
+ @boom = @result.boom if @result
231
+
232
+ @linked_columns = @data_source.linked_columns
233
+
234
+ @markers = []
235
+ [["latitude", "longitude"], ["lat", "lon"], ["lat", "lng"]].each do |keys|
236
+ lat_index = @columns.index(keys.first)
237
+ lon_index = @columns.index(keys.last)
238
+ if lat_index && lon_index
239
+ @markers =
240
+ @rows.select do |r|
241
+ r[lat_index] && r[lon_index]
242
+ end.map do |r|
243
+ {
244
+ title: r.each_with_index.map{ |v, i| i == lat_index || i == lon_index ? nil : "<strong>#{@columns[i]}:</strong> #{v}" }.compact.join("<br />").truncate(140),
245
+ latitude: r[lat_index],
246
+ longitude: r[lon_index]
247
+ }
248
+ end
249
+ end
250
+ end
251
+
252
+ respond_to do |format|
253
+ format.html do
254
+ render layout: false
255
+ end
256
+ format.csv do
257
+ send_data csv_data(@columns, @rows, @data_source), type: "text/csv; charset=utf-8; header=present", disposition: "attachment; filename=\"#{@query.try(:name).try(:parameterize).presence || 'query'}.csv\""
258
+ end
259
+ end
260
+ end
261
+
262
+ def set_queries(limit = nil)
263
+ @queries = Blazer::Query.named.select(:id, :name, :creator_id, :statement)
264
+ @queries = @queries.includes(:creator) if Blazer.user_class
265
+
266
+ if blazer_user && params[:filter] == "mine"
267
+ @queries = @queries.where(creator_id: blazer_user.id).reorder(updated_at: :desc)
268
+ elsif blazer_user && params[:filter] == "viewed" && Blazer.audit
269
+ @queries = queries_by_ids(Blazer::Audit.where(user_id: blazer_user.id).order(created_at: :desc).limit(500).pluck(:query_id).uniq)
270
+ else
271
+ @queries = @queries.limit(limit) if limit
272
+ @queries = @queries.order(:name)
273
+ end
274
+ @queries = @queries.to_a
275
+
276
+ @more = limit && @queries.size >= limit
277
+
278
+ @queries = @queries.select { |q| !q.name.to_s.start_with?("#") || q.try(:creator).try(:id) == blazer_user.try(:id) }
279
+
280
+ @queries =
281
+ @queries.map do |q|
282
+ {
283
+ id: q.id,
284
+ name: q.name,
285
+ creator: blazer_user && q.try(:creator) == blazer_user ? "You" : q.try(:creator).try(Blazer.user_name),
286
+ vars: q.variables.join(", "),
287
+ to_param: q.to_param
288
+ }
289
+ end
290
+ end
291
+
292
+ def queries_by_ids(favorite_query_ids)
293
+ queries = Blazer::Query.named.where(id: favorite_query_ids)
294
+ queries = queries.includes(:creator) if Blazer.user_class
295
+ queries = queries.index_by(&:id)
296
+ favorite_query_ids.map { |query_id| queries[query_id] }.compact
297
+ end
298
+
299
+ def set_query
300
+ @query = Blazer::Query.find(params[:id].to_s.split("-").first)
301
+
302
+ unless @query.viewable?(blazer_user)
303
+ render_forbidden
304
+ end
305
+ end
306
+
307
+ def render_forbidden
308
+ render plain: "Access denied", status: :forbidden
309
+ end
310
+
311
+ def query_params
312
+ params.require(:query).permit(:name, :description, :statement, :data_source)
313
+ end
314
+
315
+ def blazer_params
316
+ params[:blazer] || {}
317
+ end
318
+
319
+ def csv_data(columns, rows, data_source)
320
+ CSV.generate do |csv|
321
+ csv << columns
322
+ rows.each do |row|
323
+ csv << row.each_with_index.map { |v, i| v.is_a?(Time) ? blazer_time_value(data_source, columns[i], v) : v }
324
+ end
325
+ end
326
+ end
327
+
328
+ def blazer_time_value(data_source, k, v)
329
+ data_source.local_time_suffix.any? { |s| k.ends_with?(s) } ? v.to_s.sub(" UTC", "") : v.in_time_zone(Blazer.time_zone)
330
+ end
331
+ helper_method :blazer_time_value
332
+
333
+ def blazer_run_id
334
+ params[:run_id].to_s.gsub(/[^a-z0-9\-]/i, "")
335
+ end
336
+ end
337
+ end