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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/LICENSE.txt +1 -1
- data/README.md +13 -28
- data/app/assets/javascripts/blazer/ace/ace.js +7235 -8906
- data/app/assets/javascripts/blazer/ace/ext-language_tools.js +762 -774
- data/app/assets/javascripts/blazer/ace/mode-sql.js +177 -72
- data/app/assets/javascripts/blazer/ace/snippets/sql.js +5 -29
- data/app/assets/javascripts/blazer/ace/snippets/text.js +1 -6
- data/app/assets/javascripts/blazer/ace/theme-twilight.js +8 -106
- data/app/assets/javascripts/blazer/application.js +9 -6
- data/app/assets/javascripts/blazer/chart.umd.js +13 -0
- data/app/assets/javascripts/blazer/chartjs-adapter-date-fns.bundle.js +6322 -0
- data/app/assets/javascripts/blazer/chartkick.js +1020 -914
- data/app/assets/javascripts/blazer/highlight.min.js +466 -3
- data/app/assets/javascripts/blazer/mapkick.bundle.js +1029 -0
- data/app/assets/javascripts/blazer/moment-timezone-with-data.js +39 -38
- data/app/assets/javascripts/blazer/moment.js +105 -88
- data/app/assets/javascripts/blazer/queries.js +10 -1
- data/app/assets/javascripts/blazer/rails-ujs.js +746 -0
- data/app/assets/javascripts/blazer/vue.global.prod.js +1 -0
- data/app/assets/stylesheets/blazer/bootstrap.css +1 -1
- data/app/assets/stylesheets/blazer/selectize.css +1 -1
- data/app/controllers/blazer/base_controller.rb +85 -84
- data/app/controllers/blazer/checks_controller.rb +6 -6
- data/app/controllers/blazer/dashboards_controller.rb +24 -24
- data/app/controllers/blazer/queries_controller.rb +208 -186
- data/app/controllers/blazer/uploads_controller.rb +49 -49
- data/app/helpers/blazer/base_helper.rb +0 -4
- data/app/models/blazer/query.rb +1 -12
- data/app/views/blazer/checks/index.html.erb +1 -1
- data/app/views/blazer/dashboards/_form.html.erb +11 -5
- data/app/views/blazer/queries/_form.html.erb +19 -14
- data/app/views/blazer/queries/docs.html.erb +11 -1
- data/app/views/blazer/queries/home.html.erb +9 -6
- data/app/views/blazer/queries/run.html.erb +17 -32
- data/app/views/blazer/queries/show.html.erb +12 -20
- data/app/views/layouts/blazer/application.html.erb +1 -5
- data/lib/blazer/adapters/sql_adapter.rb +1 -1
- data/lib/blazer/adapters.rb +17 -0
- data/lib/blazer/anomaly_detectors.rb +22 -0
- data/lib/blazer/data_source.rb +29 -40
- data/lib/blazer/engine.rb +11 -9
- data/lib/blazer/forecasters.rb +7 -0
- data/lib/blazer/result.rb +13 -71
- data/lib/blazer/result_cache.rb +71 -0
- data/lib/blazer/run_statement.rb +1 -1
- data/lib/blazer/run_statement_job.rb +2 -2
- data/lib/blazer/statement.rb +3 -1
- data/lib/blazer/version.rb +1 -1
- data/lib/blazer.rb +51 -53
- data/licenses/LICENSE-chart.js.txt +1 -1
- data/licenses/LICENSE-chartjs-adapter-date-fns.txt +9 -0
- data/licenses/LICENSE-chartkick.js.txt +1 -1
- data/licenses/LICENSE-date-fns.txt +21 -0
- data/licenses/LICENSE-kurkle-color.txt +9 -0
- data/licenses/LICENSE-mapkick-bundle.txt +1029 -0
- data/licenses/{LICENSE-jquery-ujs.txt → LICENSE-rails-ujs.txt} +1 -1
- data/licenses/LICENSE-vue.txt +1 -1
- metadata +26 -18
- data/app/assets/javascripts/blazer/Chart.js +0 -16172
- data/app/assets/javascripts/blazer/jquery-ujs.js +0 -555
- data/app/assets/javascripts/blazer/vue.js +0 -12014
- data/lib/blazer/adapters/mongodb_adapter.rb +0 -43
- data/lib/blazer/detect_anomalies.R +0 -19
@@ -65,35 +65,35 @@ module Blazer
|
|
65
65
|
|
66
66
|
private
|
67
67
|
|
68
|
-
|
69
|
-
|
70
|
-
|
68
|
+
def dashboard_params
|
69
|
+
params.require(:dashboard).permit(:name)
|
70
|
+
end
|
71
71
|
|
72
|
-
|
73
|
-
|
74
|
-
|
72
|
+
def set_dashboard
|
73
|
+
@dashboard = Blazer::Dashboard.find(params[:id])
|
74
|
+
end
|
75
75
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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
|
-
|
104
|
-
|
105
|
-
|
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
|
-
|
135
|
-
|
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
|
-
|
221
|
-
|
226
|
+
def set_data_source
|
227
|
+
@data_source = Blazer.data_sources[params[:data_source]]
|
228
|
+
end
|
222
229
|
|
223
|
-
|
224
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
260
|
+
@markers = []
|
261
|
+
@geojson = []
|
262
|
+
set_map_data if Blazer.maps?
|
279
263
|
|
280
|
-
|
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
|
-
|
289
|
-
|
266
|
+
respond_to do |format|
|
267
|
+
format.html do
|
268
|
+
render layout: false
|
290
269
|
end
|
291
|
-
|
292
|
-
|
293
|
-
|
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
|
-
|
298
|
-
|
299
|
-
|
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
|
-
|
278
|
+
end
|
279
|
+
end
|
306
280
|
|
307
|
-
|
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
|
-
|
297
|
+
return if @markers.any?
|
298
|
+
end
|
299
|
+
end
|
310
300
|
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
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
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
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
|
-
|
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
|
-
|
334
|
-
|
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
|
-
|
347
|
+
end
|
337
348
|
|
338
|
-
|
339
|
-
|
340
|
-
|
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
|
-
|
343
|
-
|
344
|
-
|
356
|
+
def set_query
|
357
|
+
@query = Blazer::Query.find(params[:id].to_s.split("-").first)
|
358
|
+
end
|
345
359
|
|
346
|
-
|
347
|
-
|
348
|
-
|
360
|
+
def render_forbidden
|
361
|
+
render plain: "Access denied", status: :forbidden
|
362
|
+
end
|
349
363
|
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
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
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
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
|
-
|
365
|
-
|
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
|
-
|
369
|
-
|
370
|
-
|
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
|
-
|
374
|
-
|
375
|
-
|
399
|
+
def render_cohort_analysis
|
400
|
+
if @show_cohort_rows
|
401
|
+
@cohort_analysis = false
|
376
402
|
|
377
|
-
|
378
|
-
if @show_cohort_rows
|
379
|
-
@cohort_analysis = false
|
403
|
+
@row_limit = 1000
|
380
404
|
|
381
|
-
|
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
|
414
|
+
# check types (user_id can be any type)
|
384
415
|
unless @cohort_error
|
385
|
-
|
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
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
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
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
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
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
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
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
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
|
-
|
135
|
-
|
136
|
-
|
134
|
+
def upload_params
|
135
|
+
params.require(:upload).except(:file).permit(:table, :description)
|
136
|
+
end
|
137
137
|
|
138
|
-
|
139
|
-
|
140
|
-
|
138
|
+
def set_upload
|
139
|
+
@upload = Blazer::Upload.find(params[:id])
|
140
|
+
end
|
141
141
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
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
|