blazer 2.6.5 → 3.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/LICENSE.txt +1 -1
  4. data/README.md +13 -28
  5. data/app/assets/javascripts/blazer/ace/ace.js +7235 -8906
  6. data/app/assets/javascripts/blazer/ace/ext-language_tools.js +762 -774
  7. data/app/assets/javascripts/blazer/ace/mode-sql.js +177 -72
  8. data/app/assets/javascripts/blazer/ace/snippets/sql.js +5 -29
  9. data/app/assets/javascripts/blazer/ace/snippets/text.js +1 -6
  10. data/app/assets/javascripts/blazer/ace/theme-twilight.js +8 -106
  11. data/app/assets/javascripts/blazer/application.js +9 -6
  12. data/app/assets/javascripts/blazer/chart.umd.js +13 -0
  13. data/app/assets/javascripts/blazer/chartjs-adapter-date-fns.bundle.js +6322 -0
  14. data/app/assets/javascripts/blazer/chartkick.js +1020 -914
  15. data/app/assets/javascripts/blazer/highlight.min.js +466 -3
  16. data/app/assets/javascripts/blazer/mapkick.bundle.js +1029 -0
  17. data/app/assets/javascripts/blazer/moment-timezone-with-data.js +39 -38
  18. data/app/assets/javascripts/blazer/moment.js +105 -88
  19. data/app/assets/javascripts/blazer/queries.js +10 -1
  20. data/app/assets/javascripts/blazer/rails-ujs.js +746 -0
  21. data/app/assets/javascripts/blazer/vue.global.prod.js +1 -0
  22. data/app/assets/stylesheets/blazer/bootstrap.css +1 -1
  23. data/app/assets/stylesheets/blazer/selectize.css +1 -1
  24. data/app/controllers/blazer/base_controller.rb +85 -84
  25. data/app/controllers/blazer/checks_controller.rb +6 -6
  26. data/app/controllers/blazer/dashboards_controller.rb +24 -24
  27. data/app/controllers/blazer/queries_controller.rb +208 -186
  28. data/app/controllers/blazer/uploads_controller.rb +49 -49
  29. data/app/helpers/blazer/base_helper.rb +0 -4
  30. data/app/models/blazer/query.rb +1 -12
  31. data/app/views/blazer/checks/index.html.erb +1 -1
  32. data/app/views/blazer/dashboards/_form.html.erb +11 -5
  33. data/app/views/blazer/queries/_form.html.erb +19 -14
  34. data/app/views/blazer/queries/docs.html.erb +11 -1
  35. data/app/views/blazer/queries/home.html.erb +9 -6
  36. data/app/views/blazer/queries/run.html.erb +17 -32
  37. data/app/views/blazer/queries/show.html.erb +12 -20
  38. data/app/views/layouts/blazer/application.html.erb +1 -5
  39. data/lib/blazer/adapters/sql_adapter.rb +1 -1
  40. data/lib/blazer/adapters.rb +17 -0
  41. data/lib/blazer/anomaly_detectors.rb +22 -0
  42. data/lib/blazer/data_source.rb +29 -40
  43. data/lib/blazer/engine.rb +11 -9
  44. data/lib/blazer/forecasters.rb +7 -0
  45. data/lib/blazer/result.rb +13 -71
  46. data/lib/blazer/result_cache.rb +71 -0
  47. data/lib/blazer/run_statement.rb +1 -1
  48. data/lib/blazer/run_statement_job.rb +2 -2
  49. data/lib/blazer/statement.rb +3 -1
  50. data/lib/blazer/version.rb +1 -1
  51. data/lib/blazer.rb +51 -53
  52. data/licenses/LICENSE-chart.js.txt +1 -1
  53. data/licenses/LICENSE-chartjs-adapter-date-fns.txt +9 -0
  54. data/licenses/LICENSE-chartkick.js.txt +1 -1
  55. data/licenses/LICENSE-date-fns.txt +21 -0
  56. data/licenses/LICENSE-kurkle-color.txt +9 -0
  57. data/licenses/LICENSE-mapkick-bundle.txt +1029 -0
  58. data/licenses/{LICENSE-jquery-ujs.txt → LICENSE-rails-ujs.txt} +1 -1
  59. data/licenses/LICENSE-vue.txt +1 -1
  60. metadata +26 -18
  61. data/app/assets/javascripts/blazer/Chart.js +0 -16172
  62. data/app/assets/javascripts/blazer/jquery-ujs.js +0 -555
  63. data/app/assets/javascripts/blazer/vue.js +0 -12014
  64. data/lib/blazer/adapters/mongodb_adapter.rb +0 -43
  65. data/lib/blazer/detect_anomalies.R +0 -19
@@ -65,35 +65,35 @@ module Blazer
65
65
 
66
66
  private
67
67
 
68
- def dashboard_params
69
- params.require(:dashboard).permit(:name)
70
- end
68
+ def dashboard_params
69
+ params.require(:dashboard).permit(:name)
70
+ end
71
71
 
72
- def set_dashboard
73
- @dashboard = Blazer::Dashboard.find(params[:id])
74
- end
72
+ def set_dashboard
73
+ @dashboard = Blazer::Dashboard.find(params[:id])
74
+ end
75
75
 
76
- def update_dashboard(dashboard)
77
- dashboard.assign_attributes(dashboard_params)
78
- Blazer::Dashboard.transaction do
79
- if params[:query_ids].is_a?(Array)
80
- query_ids = params[:query_ids].map(&:to_i)
81
- @queries = Blazer::Query.find(query_ids).sort_by { |q| query_ids.index(q.id) }
82
- end
83
- if dashboard.save
84
- if @queries
85
- @queries.each_with_index do |query, i|
86
- dashboard_query = dashboard.dashboard_queries.where(query_id: query.id).first_or_initialize
87
- dashboard_query.position = i
88
- dashboard_query.save!
89
- end
90
- if dashboard.persisted?
91
- dashboard.dashboard_queries.where.not(query_id: query_ids).destroy_all
92
- end
76
+ def update_dashboard(dashboard)
77
+ dashboard.assign_attributes(dashboard_params)
78
+ Blazer::Dashboard.transaction do
79
+ if params[:query_ids].is_a?(Array)
80
+ query_ids = params[:query_ids].map(&:to_i)
81
+ @queries = Blazer::Query.find(query_ids).sort_by { |q| query_ids.index(q.id) }
82
+ end
83
+ if dashboard.save
84
+ if @queries
85
+ @queries.each_with_index do |query, i|
86
+ dashboard_query = dashboard.dashboard_queries.where(query_id: query.id).first_or_initialize
87
+ dashboard_query.position = i
88
+ dashboard_query.save!
89
+ end
90
+ if dashboard.persisted?
91
+ dashboard.dashboard_queries.where.not(query_id: query_ids).destroy_all
93
92
  end
94
- true
95
93
  end
94
+ true
96
95
  end
97
96
  end
97
+ end
98
98
  end
99
99
  end
@@ -73,6 +73,12 @@ module Blazer
73
73
  @query.update!(status: "active") if @query.respond_to?(:status) && @query.status.in?(["archived", nil])
74
74
 
75
75
  add_cohort_analysis_vars if @query.cohort_analysis?
76
+
77
+ if @success
78
+ @run_data = {statement: @query.statement, query_id: @query.id, data_source: @query.data_source, variables: variable_params(@query)}
79
+ @run_data[:forecast] = "t" if params[:forecast]
80
+ @run_data[:cohort_period] = params[:cohort_period] if params[:cohort_period]
81
+ end
76
82
  end
77
83
 
78
84
  def edit
@@ -82,7 +88,6 @@ module Blazer
82
88
  @query = Query.find_by(id: params[:query_id]) if params[:query_id]
83
89
 
84
90
  # use query data source when present
85
- # need to update viewable? logic below if this changes
86
91
  data_source = @query.data_source if @query && @query.data_source
87
92
  data_source ||= params[:data_source]
88
93
  @data_source = Blazer.data_sources[data_source]
@@ -100,10 +105,9 @@ module Blazer
100
105
 
101
106
  run_cohort_analysis if @cohort_analysis
102
107
 
103
- # ensure viewable
104
- if !(@query || Query.new(data_source: @data_source.id)).viewable?(blazer_user)
105
- render_forbidden
106
- elsif @run_id
108
+ query_running = !@run_id.nil?
109
+
110
+ if query_running
107
111
  @timestamp = blazer_params[:timestamp].to_i
108
112
 
109
113
  @result = @data_source.run_results(@run_id)
@@ -131,8 +135,10 @@ module Blazer
131
135
  elsif @success
132
136
  @run_id = blazer_run_id
133
137
 
134
- options = {user: blazer_user, query: @query, refresh_cache: params[:check], run_id: @run_id, async: Blazer.async}
135
- if Blazer.async && request.format.symbol != :csv
138
+ async = Blazer.async
139
+
140
+ options = {user: blazer_user, query: @query, refresh_cache: params[:check], run_id: @run_id, async: async}
141
+ if async && request.format.symbol != :csv
136
142
  Blazer::RunStatementJob.perform_later(@data_source.id, @statement.statement, options.merge(values: @statement.values))
137
143
  wait_start = Blazer.monotonic_time
138
144
  loop do
@@ -145,7 +151,7 @@ module Blazer
145
151
  end
146
152
 
147
153
  if @result
148
- @data_source.delete_results(@run_id) if @run_id
154
+ @data_source.delete_results(@run_id) if @run_id && async
149
155
 
150
156
  @columns = @result.columns
151
157
  @rows = @result.rows
@@ -217,229 +223,245 @@ module Blazer
217
223
 
218
224
  private
219
225
 
220
- def set_data_source
221
- @data_source = Blazer.data_sources[params[:data_source]]
226
+ def set_data_source
227
+ @data_source = Blazer.data_sources[params[:data_source]]
228
+ end
222
229
 
223
- unless Query.new(data_source: @data_source.id).editable?(blazer_user)
224
- render_forbidden
230
+ def continue_run
231
+ render json: {run_id: @run_id, timestamp: @timestamp}, status: :accepted
232
+ end
233
+
234
+ def render_run
235
+ @checks = @query ? @query.checks.order(:id) : []
236
+
237
+ @first_row = @rows.first || []
238
+ @column_types = []
239
+ if @rows.any?
240
+ @columns.each_with_index do |_, i|
241
+ @column_types << (
242
+ case @first_row[i]
243
+ when Integer
244
+ "int"
245
+ when Float, BigDecimal
246
+ "float"
247
+ else
248
+ "string-ins"
249
+ end
250
+ )
225
251
  end
226
252
  end
227
253
 
228
- def continue_run
229
- render json: {run_id: @run_id, timestamp: @timestamp}, status: :accepted
230
- end
254
+ @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)
231
255
 
232
- def render_run
233
- @checks = @query ? @query.checks.order(:id) : []
234
-
235
- @first_row = @rows.first || []
236
- @column_types = []
237
- if @rows.any?
238
- @columns.each_with_index do |_, i|
239
- @column_types << (
240
- case @first_row[i]
241
- when Integer
242
- "int"
243
- when Float, BigDecimal
244
- "float"
245
- else
246
- "string-ins"
247
- end
248
- )
249
- end
250
- end
256
+ @smart_values = @result.smart_values if @result
251
257
 
252
- @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)
253
-
254
- @boom = @result.boom if @result
255
-
256
- @linked_columns = @data_source.linked_columns
257
-
258
- @markers = []
259
- [["latitude", "longitude"], ["lat", "lon"], ["lat", "lng"]].each do |keys|
260
- lat_index = @columns.index(keys.first)
261
- lon_index = @columns.index(keys.last)
262
- if lat_index && lon_index
263
- @markers =
264
- @rows.select do |r|
265
- r[lat_index] && r[lon_index]
266
- end.map do |r|
267
- {
268
- # Mapbox.js does sanitization with https://github.com/mapbox/sanitize-caja
269
- # but we should do it here as well
270
- title: r.each_with_index.map { |v, i| i == lat_index || i == lon_index ? nil : "<strong>#{ERB::Util.html_escape(@columns[i])}:</strong> #{ERB::Util.html_escape(v)}" }.compact.join("<br />").truncate(140),
271
- latitude: r[lat_index],
272
- longitude: r[lon_index]
273
- }
274
- end
275
- end
276
- end
258
+ @linked_columns = @data_source.linked_columns
277
259
 
278
- render_cohort_analysis if @cohort_analysis && !@error
260
+ @markers = []
261
+ @geojson = []
262
+ set_map_data if Blazer.maps?
279
263
 
280
- respond_to do |format|
281
- format.html do
282
- render layout: false
283
- end
284
- format.csv do
285
- # not ideal, but useful for testing
286
- raise Error, @error if @error && Rails.env.test?
264
+ render_cohort_analysis if @cohort_analysis && !@error
287
265
 
288
- 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\""
289
- end
266
+ respond_to do |format|
267
+ format.html do
268
+ render layout: false
290
269
  end
291
- end
292
-
293
- def set_queries(limit = nil)
294
- @queries = Blazer::Query.named.select(:id, :name, :creator_id, :statement)
295
- @queries = @queries.includes(:creator) if Blazer.user_class
270
+ format.csv do
271
+ # not ideal, but useful for testing
272
+ raise Error, @error if @error && Rails.env.test?
296
273
 
297
- if blazer_user && params[:filter] == "mine"
298
- @queries = @queries.where(creator_id: blazer_user.id).reorder(updated_at: :desc)
299
- elsif blazer_user && params[:filter] == "viewed" && Blazer.audit
300
- @queries = queries_by_ids(Blazer::Audit.where(user_id: blazer_user.id).order(created_at: :desc).limit(500).pluck(:query_id).uniq)
301
- else
302
- @queries = @queries.limit(limit) if limit
303
- @queries = @queries.active.order(:name)
274
+ data = csv_data(@columns, @rows, @data_source)
275
+ filename = "#{@query.try(:name).try(:parameterize).presence || 'query'}.csv"
276
+ send_data data, type: "text/csv; charset=utf-8", disposition: "attachment", filename: filename
304
277
  end
305
- @queries = @queries.to_a
278
+ end
279
+ end
306
280
 
307
- @more = limit && @queries.size >= limit
281
+ def set_map_data
282
+ [["latitude", "longitude"], ["lat", "lon"], ["lat", "lng"]].each do |keys|
283
+ lat_index = @columns.index(keys.first)
284
+ lon_index = @columns.index(keys.last)
285
+ if lat_index && lon_index
286
+ @markers =
287
+ @rows.select do |r|
288
+ r[lat_index] && r[lon_index]
289
+ end.map do |r|
290
+ {
291
+ tooltip: map_tooltip(r.each_with_index.reject { |v, i| i == lat_index || i == lon_index }),
292
+ latitude: r[lat_index],
293
+ longitude: r[lon_index]
294
+ }
295
+ end
308
296
 
309
- @queries = @queries.select { |q| !q.name.to_s.start_with?("#") || q.try(:creator).try(:id) == blazer_user.try(:id) }
297
+ return if @markers.any?
298
+ end
299
+ end
310
300
 
311
- @queries =
312
- @queries.map do |q|
313
- {
314
- id: q.id,
315
- name: q.name,
316
- creator: blazer_user && q.try(:creator) == blazer_user ? "You" : q.try(:creator).try(Blazer.user_name),
317
- vars: q.variables.join(", "),
318
- to_param: q.to_param
319
- }
301
+ geo_index = @columns.index("geojson")
302
+ if geo_index
303
+ @geojson =
304
+ @rows.filter_map do |r|
305
+ if r[geo_index].is_a?(String) && (geometry = (JSON.parse(r[geo_index]) rescue nil)) && geometry.is_a?(Hash)
306
+ {
307
+ tooltip: map_tooltip(r.each_with_index.reject { |v, i| i == geo_index }),
308
+ geometry: geometry
309
+ }
310
+ end
320
311
  end
321
312
  end
313
+ end
322
314
 
323
- def queries_by_ids(favorite_query_ids)
324
- queries = Blazer::Query.active.named.where(id: favorite_query_ids)
325
- queries = queries.includes(:creator) if Blazer.user_class
326
- queries = queries.index_by(&:id)
327
- favorite_query_ids.map { |query_id| queries[query_id] }.compact
315
+ def map_tooltip(r)
316
+ r.map { |v, i| "<strong>#{ERB::Util.html_escape(@columns[i])}:</strong> #{ERB::Util.html_escape(v)}" }.join("<br>").truncate(140, separator: " ")
317
+ end
318
+
319
+ def set_queries(limit = nil)
320
+ @queries = Blazer::Query.named.select(:id, :name, :creator_id, :statement)
321
+ @queries = @queries.includes(:creator) if Blazer.user_class
322
+
323
+ if blazer_user && params[:filter] == "mine"
324
+ @queries = @queries.where(creator_id: blazer_user.id).reorder(updated_at: :desc)
325
+ elsif blazer_user && params[:filter] == "viewed" && Blazer.audit
326
+ @queries = queries_by_ids(Blazer::Audit.where(user_id: blazer_user.id).order(created_at: :desc).limit(500).pluck(:query_id).uniq)
327
+ else
328
+ @queries = @queries.limit(limit) if limit
329
+ @queries = @queries.active.order(:name)
328
330
  end
331
+ @queries = @queries.to_a
332
+
333
+ @more = limit && @queries.size >= limit
329
334
 
330
- def set_query
331
- @query = Blazer::Query.find(params[:id].to_s.split("-").first)
335
+ @queries = @queries.select { |q| !q.name.to_s.start_with?("#") || q.try(:creator).try(:id) == blazer_user.try(:id) }
332
336
 
333
- unless @query.viewable?(blazer_user)
334
- render_forbidden
337
+ @queries =
338
+ @queries.map do |q|
339
+ {
340
+ id: q.id,
341
+ name: q.name,
342
+ creator: blazer_user && q.try(:creator) == blazer_user ? "You" : q.try(:creator).try(Blazer.user_name),
343
+ vars: q.variables.join(", "),
344
+ to_param: q.to_param
345
+ }
335
346
  end
336
- end
347
+ end
337
348
 
338
- def render_forbidden
339
- render plain: "Access denied", status: :forbidden
340
- end
349
+ def queries_by_ids(favorite_query_ids)
350
+ queries = Blazer::Query.active.named.where(id: favorite_query_ids)
351
+ queries = queries.includes(:creator) if Blazer.user_class
352
+ queries = queries.index_by(&:id)
353
+ favorite_query_ids.map { |query_id| queries[query_id] }.compact
354
+ end
341
355
 
342
- def query_params
343
- params.require(:query).permit(:name, :description, :statement, :data_source)
344
- end
356
+ def set_query
357
+ @query = Blazer::Query.find(params[:id].to_s.split("-").first)
358
+ end
345
359
 
346
- def blazer_params
347
- params[:blazer] || {}
348
- end
360
+ def render_forbidden
361
+ render plain: "Access denied", status: :forbidden
362
+ end
349
363
 
350
- def csv_data(columns, rows, data_source)
351
- CSV.generate do |csv|
352
- csv << columns
353
- rows.each do |row|
354
- csv << row.each_with_index.map { |v, i| v.is_a?(Time) ? blazer_time_value(data_source, columns[i], v) : v }
355
- end
364
+ def query_params
365
+ params.require(:query).permit(:name, :description, :statement, :data_source)
366
+ end
367
+
368
+ def blazer_params
369
+ params[:blazer] || {}
370
+ end
371
+
372
+ def csv_data(columns, rows, data_source)
373
+ CSV.generate do |csv|
374
+ csv << columns
375
+ rows.each do |row|
376
+ csv << row.each_with_index.map { |v, i| v.is_a?(Time) ? blazer_time_value(data_source, columns[i], v) : v }
356
377
  end
357
378
  end
379
+ end
358
380
 
359
- def blazer_time_value(data_source, k, v)
360
- data_source.local_time_suffix.any? { |s| k.ends_with?(s) } ? v.to_s.sub(" UTC", "") : v.in_time_zone(Blazer.time_zone)
361
- end
362
- helper_method :blazer_time_value
381
+ def blazer_time_value(data_source, k, v)
382
+ data_source.local_time_suffix.any? { |s| k.ends_with?(s) } ? v.to_s.sub(" UTC", "") : v.in_time_zone(Blazer.time_zone)
383
+ end
384
+ helper_method :blazer_time_value
385
+
386
+ def blazer_run_id
387
+ params[:run_id].to_s.gsub(/[^a-z0-9\-]/i, "")
388
+ end
363
389
 
364
- def blazer_run_id
365
- params[:run_id].to_s.gsub(/[^a-z0-9\-]/i, "")
390
+ def run_cohort_analysis
391
+ unless @statement.data_source.supports_cohort_analysis?
392
+ @cohort_error = "This data source does not support cohort analysis"
366
393
  end
367
394
 
368
- def run_cohort_analysis
369
- unless @statement.data_source.supports_cohort_analysis?
370
- @cohort_error = "This data source does not support cohort analysis"
371
- end
395
+ @show_cohort_rows = !params[:query_id] || @cohort_error
396
+ cohort_analysis_statement(@statement) unless @show_cohort_rows
397
+ end
372
398
 
373
- @show_cohort_rows = !params[:query_id] || @cohort_error
374
- cohort_analysis_statement(@statement) unless @show_cohort_rows
375
- end
399
+ def render_cohort_analysis
400
+ if @show_cohort_rows
401
+ @cohort_analysis = false
376
402
 
377
- def render_cohort_analysis
378
- if @show_cohort_rows
379
- @cohort_analysis = false
403
+ @row_limit = 1000
380
404
 
381
- @row_limit = 1000
405
+ # check results
406
+ unless @cohort_error
407
+ # check names
408
+ expected_columns = ["user_id", "conversion_time"]
409
+ missing_columns = expected_columns - @result.columns
410
+ if missing_columns.any?
411
+ @cohort_error = "Expected user_id and conversion_time columns"
412
+ end
382
413
 
383
- # check results
414
+ # check types (user_id can be any type)
384
415
  unless @cohort_error
385
- # check names
386
- expected_columns = ["user_id", "conversion_time"]
387
- missing_columns = expected_columns - @result.columns
388
- if missing_columns.any?
389
- @cohort_error = "Expected user_id and conversion_time columns"
390
- end
391
-
392
- # check types (user_id can be any type)
393
- unless @cohort_error
394
- column_types = @result.columns.zip(@result.column_types).to_h
416
+ column_types = @result.columns.zip(@result.column_types).to_h
395
417
 
396
- if !column_types["cohort_time"].in?(["time", nil])
397
- @cohort_error = "cohort_time must be time column"
398
- elsif !column_types["conversion_time"].in?(["time", nil])
399
- @cohort_error = "conversion_time must be time column"
400
- end
418
+ if !column_types["cohort_time"].in?(["time", nil])
419
+ @cohort_error = "cohort_time must be time column"
420
+ elsif !column_types["conversion_time"].in?(["time", nil])
421
+ @cohort_error = "conversion_time must be time column"
401
422
  end
402
423
  end
403
- else
404
- @today = Blazer.time_zone.today
405
- @min_cohort_date, @max_cohort_date = @result.rows.map { |r| r[0] }.minmax
406
- @buckets = {}
407
- @rows.each do |r|
408
- @buckets[[r[0], r[1]]] = r[2]
409
- end
410
-
411
- @cohort_dates = []
412
- current_date = @max_cohort_date
413
- while current_date && current_date >= @min_cohort_date
414
- @cohort_dates << current_date
415
- current_date =
416
- case @cohort_period
417
- when "day"
418
- current_date - 1
419
- when "week"
420
- current_date - 7
421
- else
422
- current_date.prev_month
423
- end
424
- end
424
+ end
425
+ else
426
+ @today = Blazer.time_zone.today
427
+ @min_cohort_date, @max_cohort_date = @result.rows.map { |r| r[0] }.minmax
428
+ @buckets = {}
429
+ @rows.each do |r|
430
+ @buckets[[r[0], r[1]]] = r[2]
431
+ end
425
432
 
426
- num_cols = @cohort_dates.size
427
- @columns = ["Cohort", "Users"] + num_cols.times.map { |i| "#{@conversion_period.titleize} #{i + 1}" }
428
- rows = []
429
- date_format = @cohort_period == "month" ? "%b %Y" : "%b %-e, %Y"
430
- @cohort_dates.each do |date|
431
- row = [date.strftime(date_format), @buckets[[date, 0]] || 0]
432
-
433
- num_cols.times do |i|
434
- if @today >= date + (@cohort_days * i)
435
- row << (@buckets[[date, i + 1]] || 0)
436
- end
433
+ @cohort_dates = []
434
+ current_date = @max_cohort_date
435
+ while current_date && current_date >= @min_cohort_date
436
+ @cohort_dates << current_date
437
+ current_date =
438
+ case @cohort_period
439
+ when "day"
440
+ current_date - 1
441
+ when "week"
442
+ current_date - 7
443
+ else
444
+ current_date.prev_month
437
445
  end
446
+ end
438
447
 
439
- rows << row
448
+ num_cols = @cohort_dates.size
449
+ @columns = ["Cohort", "Users"] + num_cols.times.map { |i| "#{@conversion_period.titleize} #{i + 1}" }
450
+ rows = []
451
+ date_format = @cohort_period == "month" ? "%b %Y" : "%b %-e, %Y"
452
+ @cohort_dates.each do |date|
453
+ row = [date.strftime(date_format), @buckets[[date, 0]] || 0]
454
+
455
+ num_cols.times do |i|
456
+ if @today >= date + (@cohort_days * i)
457
+ row << (@buckets[[date, i + 1]] || 0)
458
+ end
440
459
  end
441
- @rows = rows
460
+
461
+ rows << row
442
462
  end
463
+ @rows = rows
443
464
  end
465
+ end
444
466
  end
445
467
  end
@@ -86,62 +86,62 @@ module Blazer
86
86
 
87
87
  private
88
88
 
89
- def update_file(upload, drop: nil)
90
- file = params.require(:upload).fetch(:file)
91
- raise Blazer::UploadError, "File is not a CSV" if file.content_type != "text/csv"
92
- raise Blazer::UploadError, "File is too large (maximum is 100MB)" if file.size > 100.megabytes
93
-
94
- contents = file.read
95
- rows = CSV.parse(contents, converters: %i[numeric date date_time])
96
-
97
- # friendly column names
98
- columns = rows.shift.map { |v| v.to_s.encode("UTF-8").gsub("%", " pct ").parameterize.gsub("-", "_") }
99
- duplicate_column = columns.find { |c| columns.count(c) > 1 }
100
- raise Blazer::UploadError, "Duplicate column name: #{duplicate_column}" if duplicate_column
101
-
102
- column_types =
103
- columns.size.times.map do |i|
104
- values = rows.map { |r| r[i] }.uniq.compact
105
- if values.all? { |v| v.is_a?(Integer) && v >= -9223372036854775808 && v <= 9223372036854775807 }
106
- "bigint"
107
- elsif values.all? { |v| v.is_a?(Numeric) }
108
- "decimal"
109
- elsif values.all? { |v| v.is_a?(DateTime) }
110
- "timestamptz"
111
- elsif values.all? { |v| v.is_a?(Date) }
112
- "date"
113
- else
114
- "text"
115
- end
89
+ def update_file(upload, drop: nil)
90
+ file = params.require(:upload).fetch(:file)
91
+ raise Blazer::UploadError, "File is not a CSV" if file.content_type != "text/csv"
92
+ raise Blazer::UploadError, "File is too large (maximum is 100MB)" if file.size > 100.megabytes
93
+
94
+ contents = file.read
95
+ rows = CSV.parse(contents, converters: %i[numeric date date_time])
96
+
97
+ # friendly column names
98
+ columns = rows.shift.map { |v| v.to_s.encode("UTF-8").gsub("%", " pct ").parameterize.gsub("-", "_") }
99
+ duplicate_column = columns.find { |c| columns.count(c) > 1 }
100
+ raise Blazer::UploadError, "Duplicate column name: #{duplicate_column}" if duplicate_column
101
+
102
+ column_types =
103
+ columns.size.times.map do |i|
104
+ values = rows.map { |r| r[i] }.uniq.compact
105
+ if values.all? { |v| v.is_a?(Integer) && v >= -9223372036854775808 && v <= 9223372036854775807 }
106
+ "bigint"
107
+ elsif values.all? { |v| v.is_a?(Numeric) }
108
+ "decimal"
109
+ elsif values.all? { |v| v.is_a?(DateTime) }
110
+ "timestamptz"
111
+ elsif values.all? { |v| v.is_a?(Date) }
112
+ "date"
113
+ else
114
+ "text"
116
115
  end
116
+ end
117
117
 
118
- begin
119
- # maybe SET LOCAL statement_timeout = '30s'
120
- # maybe regenerate CSV in Ruby to ensure consistent parsing
121
- Blazer.uploads_connection.transaction do
122
- Blazer.uploads_connection.execute("DROP TABLE IF EXISTS #{Blazer.uploads_table_name(drop)}") if drop
123
- Blazer.uploads_connection.execute("CREATE TABLE #{upload.table_name} (#{columns.map.with_index { |c, i| "#{Blazer.uploads_connection.quote_column_name(c)} #{column_types[i]}" }.join(", ")})")
124
- Blazer.uploads_connection.raw_connection.copy_data("COPY #{upload.table_name} FROM STDIN CSV HEADER") do
125
- Blazer.uploads_connection.raw_connection.put_copy_data(contents)
126
- end
118
+ begin
119
+ # maybe SET LOCAL statement_timeout = '30s'
120
+ # maybe regenerate CSV in Ruby to ensure consistent parsing
121
+ Blazer.uploads_connection.transaction do
122
+ Blazer.uploads_connection.execute("DROP TABLE IF EXISTS #{Blazer.uploads_table_name(drop)}") if drop
123
+ Blazer.uploads_connection.execute("CREATE TABLE #{upload.table_name} (#{columns.map.with_index { |c, i| "#{Blazer.uploads_connection.quote_column_name(c)} #{column_types[i]}" }.join(", ")})")
124
+ Blazer.uploads_connection.raw_connection.copy_data("COPY #{upload.table_name} FROM STDIN CSV HEADER") do
125
+ Blazer.uploads_connection.raw_connection.put_copy_data(contents)
127
126
  end
128
- rescue ActiveRecord::StatementInvalid => e
129
- raise Blazer::UploadError, "Table already exists" if e.message.include?("PG::DuplicateTable")
130
- raise e
131
127
  end
128
+ rescue ActiveRecord::StatementInvalid => e
129
+ raise Blazer::UploadError, "Table already exists" if e.message.include?("PG::DuplicateTable")
130
+ raise e
132
131
  end
132
+ end
133
133
 
134
- def upload_params
135
- params.require(:upload).except(:file).permit(:table, :description)
136
- end
134
+ def upload_params
135
+ params.require(:upload).except(:file).permit(:table, :description)
136
+ end
137
137
 
138
- def set_upload
139
- @upload = Blazer::Upload.find(params[:id])
140
- end
138
+ def set_upload
139
+ @upload = Blazer::Upload.find(params[:id])
140
+ end
141
141
 
142
- # routes aren't added, but also check here
143
- def ensure_uploads
144
- render plain: "Uploads not enabled" unless Blazer.uploads?
145
- end
142
+ # routes aren't added, but also check here
143
+ def ensure_uploads
144
+ render plain: "Uploads not enabled" unless Blazer.uploads?
145
+ end
146
146
  end
147
147
  end
@@ -28,10 +28,6 @@ module Blazer
28
28
  end
29
29
  end
30
30
 
31
- def blazer_maps?
32
- Blazer.mapbox_access_token.present?
33
- end
34
-
35
31
  def blazer_js_var(name, value)
36
32
  "var #{name} = #{json_escape(value.to_json(root: false))};".html_safe
37
33
  end