railsblazer 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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