blazer 2.6.5 → 3.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -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 +3 -2
  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