blazer 2.2.6 → 2.4.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of blazer might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/LICENSE.txt +1 -1
- data/README.md +105 -29
- data/app/assets/javascripts/blazer/Chart.js +13794 -12099
- data/app/assets/javascripts/blazer/Sortable.js +3695 -1526
- data/app/assets/javascripts/blazer/chartkick.js +296 -46
- data/app/assets/javascripts/blazer/daterangepicker.js +194 -269
- data/app/assets/javascripts/blazer/jquery.js +1150 -642
- data/app/assets/javascripts/blazer/moment-timezone-with-data.js +621 -287
- data/app/assets/javascripts/blazer/moment.js +5085 -2460
- data/app/assets/stylesheets/blazer/application.css +4 -0
- data/app/assets/stylesheets/blazer/daterangepicker.css +394 -253
- data/app/controllers/blazer/base_controller.rb +12 -4
- data/app/controllers/blazer/dashboards_controller.rb +7 -2
- data/app/controllers/blazer/queries_controller.rb +119 -3
- data/app/controllers/blazer/uploads_controller.rb +147 -0
- data/app/mailers/blazer/slack_notifier.rb +1 -1
- data/app/models/blazer/query.rb +8 -1
- data/app/models/blazer/upload.rb +11 -0
- data/app/models/blazer/uploads_connection.rb +7 -0
- data/app/views/blazer/_nav.html.erb +3 -0
- data/app/views/blazer/_variables.html.erb +3 -1
- data/app/views/blazer/checks/_form.html.erb +1 -1
- data/app/views/blazer/checks/index.html.erb +3 -0
- data/app/views/blazer/dashboards/_form.html.erb +1 -1
- data/app/views/blazer/dashboards/show.html.erb +1 -1
- data/app/views/blazer/queries/_caching.html.erb +16 -0
- data/app/views/blazer/queries/_cohorts.html.erb +48 -0
- data/app/views/blazer/queries/docs.html.erb +6 -0
- data/app/views/blazer/queries/home.html.erb +3 -0
- data/app/views/blazer/queries/run.html.erb +20 -22
- data/app/views/blazer/queries/show.html.erb +1 -1
- data/app/views/blazer/uploads/_form.html.erb +27 -0
- data/app/views/blazer/uploads/edit.html.erb +3 -0
- data/app/views/blazer/uploads/index.html.erb +55 -0
- data/app/views/blazer/uploads/new.html.erb +3 -0
- data/config/routes.rb +5 -0
- data/lib/blazer.rb +18 -0
- data/lib/blazer/adapters/base_adapter.rb +8 -0
- data/lib/blazer/adapters/sql_adapter.rb +42 -1
- data/lib/blazer/data_source.rb +1 -1
- data/lib/blazer/version.rb +1 -1
- data/lib/generators/blazer/templates/config.yml.tt +6 -0
- data/lib/generators/blazer/templates/install.rb.tt +3 -2
- data/lib/generators/blazer/templates/uploads.rb.tt +10 -0
- data/lib/generators/blazer/uploads_generator.rb +18 -0
- data/lib/tasks/blazer.rake +9 -0
- data/licenses/LICENSE-ace.txt +24 -0
- data/licenses/LICENSE-bootstrap.txt +21 -0
- data/licenses/LICENSE-chart.js.txt +9 -0
- data/licenses/LICENSE-chartkick.js.txt +22 -0
- data/licenses/LICENSE-daterangepicker.txt +21 -0
- data/licenses/LICENSE-fuzzysearch.txt +20 -0
- data/licenses/LICENSE-highlight.js.txt +29 -0
- data/licenses/LICENSE-jquery-ujs.txt +20 -0
- data/licenses/LICENSE-jquery.txt +20 -0
- data/licenses/LICENSE-moment-timezone.txt +20 -0
- data/licenses/LICENSE-moment.txt +22 -0
- data/licenses/LICENSE-selectize.txt +202 -0
- data/licenses/LICENSE-sortable.txt +21 -0
- data/licenses/LICENSE-stickytableheaders.txt +20 -0
- data/licenses/LICENSE-stupidtable.txt +19 -0
- data/licenses/LICENSE-vue.txt +21 -0
- metadata +34 -7
@@ -6,6 +6,8 @@ module Blazer
|
|
6
6
|
skip_after_action(*filters, raise: false)
|
7
7
|
skip_around_action(*filters, raise: false)
|
8
8
|
|
9
|
+
clear_helpers
|
10
|
+
|
9
11
|
protect_from_forgery with: :exception
|
10
12
|
|
11
13
|
if ENV["BLAZER_PASSWORD"]
|
@@ -65,6 +67,12 @@ module Blazer
|
|
65
67
|
end
|
66
68
|
end
|
67
69
|
|
70
|
+
def add_cohort_analysis_vars
|
71
|
+
@bind_vars << "cohort_period" unless @bind_vars.include?("cohort_period")
|
72
|
+
@smart_vars["cohort_period"] = ["day", "week", "month"]
|
73
|
+
params[:cohort_period] ||= "week"
|
74
|
+
end
|
75
|
+
|
68
76
|
def parse_smart_variables(var, data_source)
|
69
77
|
smart_var_data_source =
|
70
78
|
([data_source] + Array(data_source.settings["inherit_smart_settings"]).map { |ds| Blazer.data_sources[ds] }).find { |ds| ds.smart_variables[var] }
|
@@ -96,12 +104,12 @@ module Blazer
|
|
96
104
|
# when permitted parameters are passed in Rails 6,
|
97
105
|
# they appear to be added as GET parameters
|
98
106
|
# root_url(params.permit(:host))
|
99
|
-
|
107
|
+
UNPERMITTED_KEYS = [:controller, :action, :id, :host, :query, :dashboard, :query_id, :query_ids, :table_names, :authenticity_token, :utf8, :_method, :commit, :statement, :data_source, :name, :fork_query_id, :blazer, :run_id, :script_name, :original_script_name]
|
100
108
|
|
101
|
-
# remove
|
109
|
+
# remove unpermitted keys from both params and permitted keys for better sleep
|
102
110
|
def variable_params(resource)
|
103
|
-
permitted_keys = resource.variables -
|
104
|
-
params.except(*
|
111
|
+
permitted_keys = resource.variables - UNPERMITTED_KEYS.map(&:to_s)
|
112
|
+
params.except(*UNPERMITTED_KEYS).slice(*permitted_keys).permit!
|
105
113
|
end
|
106
114
|
helper_method :variable_params
|
107
115
|
|
@@ -21,8 +21,11 @@ module Blazer
|
|
21
21
|
|
22
22
|
def show
|
23
23
|
@queries = @dashboard.dashboard_queries.order(:position).preload(:query).map(&:query)
|
24
|
+
@statements = []
|
24
25
|
@queries.each do |query|
|
25
|
-
|
26
|
+
statement = query.statement.dup
|
27
|
+
process_vars(statement, query.data_source)
|
28
|
+
@statements << statement
|
26
29
|
end
|
27
30
|
@bind_vars ||= []
|
28
31
|
|
@@ -36,6 +39,8 @@ module Blazer
|
|
36
39
|
@sql_errors << error if error
|
37
40
|
end
|
38
41
|
end
|
42
|
+
|
43
|
+
add_cohort_analysis_vars if @queries.any?(&:cohort_analysis?)
|
39
44
|
end
|
40
45
|
|
41
46
|
def edit
|
@@ -51,7 +56,7 @@ module Blazer
|
|
51
56
|
|
52
57
|
def destroy
|
53
58
|
@dashboard.destroy
|
54
|
-
redirect_to
|
59
|
+
redirect_to root_path
|
55
60
|
end
|
56
61
|
|
57
62
|
def refresh
|
@@ -38,11 +38,18 @@ module Blazer
|
|
38
38
|
if params[:fork_query_id]
|
39
39
|
@query.statement ||= Blazer::Query.find(params[:fork_query_id]).try(:statement)
|
40
40
|
end
|
41
|
+
if params[:upload_id]
|
42
|
+
upload = Blazer::Upload.find(params[:upload_id])
|
43
|
+
upload_settings = Blazer.settings["uploads"]
|
44
|
+
@query.data_source ||= upload_settings["data_source"]
|
45
|
+
@query.statement ||= "SELECT * FROM #{upload.table_name} LIMIT 10"
|
46
|
+
end
|
41
47
|
end
|
42
48
|
|
43
49
|
def create
|
44
50
|
@query = Blazer::Query.new(query_params)
|
45
51
|
@query.creator = blazer_user if @query.respond_to?(:creator)
|
52
|
+
@query.status = "active" if @query.respond_to?(:status)
|
46
53
|
|
47
54
|
if @query.save
|
48
55
|
redirect_to query_path(@query, variable_params(@query))
|
@@ -64,7 +71,11 @@ module Blazer
|
|
64
71
|
@sql_errors << error if error
|
65
72
|
end
|
66
73
|
|
74
|
+
@query.update!(status: "active") if @query.try(:status) == "archived"
|
75
|
+
|
67
76
|
Blazer.transform_statement.call(data_source, @statement) if Blazer.transform_statement
|
77
|
+
|
78
|
+
add_cohort_analysis_vars if @query.cohort_analysis?
|
68
79
|
end
|
69
80
|
|
70
81
|
def edit
|
@@ -72,6 +83,8 @@ module Blazer
|
|
72
83
|
|
73
84
|
def run
|
74
85
|
@statement = params[:statement]
|
86
|
+
# before process_vars
|
87
|
+
@cohort_analysis = Query.new(statement: @statement).cohort_analysis?
|
75
88
|
data_source = params[:data_source]
|
76
89
|
process_vars(@statement, data_source)
|
77
90
|
@only_chart = params[:only_chart]
|
@@ -80,6 +93,8 @@ module Blazer
|
|
80
93
|
data_source = @query.data_source if @query && @query.data_source
|
81
94
|
@data_source = Blazer.data_sources[data_source]
|
82
95
|
|
96
|
+
run_cohort_analysis if @cohort_analysis
|
97
|
+
|
83
98
|
# ensure viewable
|
84
99
|
if !(@query || Query.new(data_source: @data_source.id)).viewable?(blazer_user)
|
85
100
|
render_forbidden
|
@@ -155,6 +170,7 @@ module Blazer
|
|
155
170
|
@statement = @query.statement.dup
|
156
171
|
process_vars(@statement, @query.data_source)
|
157
172
|
Blazer.transform_statement.call(data_source, @statement) if Blazer.transform_statement
|
173
|
+
@statement = cohort_analysis_statement(data_source, @statement) if @query.cohort_analysis?
|
158
174
|
data_source.clear_cache(@statement)
|
159
175
|
redirect_to query_path(@query, variable_params(@query))
|
160
176
|
end
|
@@ -232,7 +248,6 @@ module Blazer
|
|
232
248
|
end
|
233
249
|
end
|
234
250
|
|
235
|
-
@filename = @query.name.parameterize if @query
|
236
251
|
@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)
|
237
252
|
|
238
253
|
@boom = @result.boom if @result
|
@@ -259,6 +274,8 @@ module Blazer
|
|
259
274
|
end
|
260
275
|
end
|
261
276
|
|
277
|
+
render_cohort_analysis if @cohort_analysis && !@error
|
278
|
+
|
262
279
|
respond_to do |format|
|
263
280
|
format.html do
|
264
281
|
render layout: false
|
@@ -279,7 +296,7 @@ module Blazer
|
|
279
296
|
@queries = queries_by_ids(Blazer::Audit.where(user_id: blazer_user.id).order(created_at: :desc).limit(500).pluck(:query_id).uniq)
|
280
297
|
else
|
281
298
|
@queries = @queries.limit(limit) if limit
|
282
|
-
@queries = @queries.order(:name)
|
299
|
+
@queries = @queries.active.order(:name)
|
283
300
|
end
|
284
301
|
@queries = @queries.to_a
|
285
302
|
|
@@ -300,7 +317,7 @@ module Blazer
|
|
300
317
|
end
|
301
318
|
|
302
319
|
def queries_by_ids(favorite_query_ids)
|
303
|
-
queries = Blazer::Query.named.where(id: favorite_query_ids)
|
320
|
+
queries = Blazer::Query.active.named.where(id: favorite_query_ids)
|
304
321
|
queries = queries.includes(:creator) if Blazer.user_class
|
305
322
|
queries = queries.index_by(&:id)
|
306
323
|
favorite_query_ids.map { |query_id| queries[query_id] }.compact
|
@@ -343,5 +360,104 @@ module Blazer
|
|
343
360
|
def blazer_run_id
|
344
361
|
params[:run_id].to_s.gsub(/[^a-z0-9\-]/i, "")
|
345
362
|
end
|
363
|
+
|
364
|
+
def run_cohort_analysis
|
365
|
+
unless @data_source.supports_cohort_analysis?
|
366
|
+
@cohort_error = "This data source does not support cohort analysis"
|
367
|
+
end
|
368
|
+
|
369
|
+
@show_cohort_rows = !params[:query_id] || @cohort_error
|
370
|
+
|
371
|
+
unless @show_cohort_rows
|
372
|
+
@statement = cohort_analysis_statement(@data_source, @statement)
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
def cohort_analysis_statement(data_source, statement)
|
377
|
+
@cohort_period = params["cohort_period"] || "week"
|
378
|
+
@cohort_period = "week" unless ["day", "week", "month"].include?(@cohort_period)
|
379
|
+
|
380
|
+
# for now
|
381
|
+
@conversion_period = @cohort_period
|
382
|
+
@cohort_days =
|
383
|
+
case @cohort_period
|
384
|
+
when "day"
|
385
|
+
1
|
386
|
+
when "week"
|
387
|
+
7
|
388
|
+
when "month"
|
389
|
+
30
|
390
|
+
end
|
391
|
+
|
392
|
+
data_source.cohort_analysis_statement(statement, period: @cohort_period, days: @cohort_days)
|
393
|
+
end
|
394
|
+
|
395
|
+
def render_cohort_analysis
|
396
|
+
if @show_cohort_rows
|
397
|
+
@cohort_analysis = false
|
398
|
+
|
399
|
+
@row_limit = 1000
|
400
|
+
|
401
|
+
# check results
|
402
|
+
unless @cohort_error
|
403
|
+
# check names
|
404
|
+
expected_columns = ["user_id", "conversion_time"]
|
405
|
+
missing_columns = expected_columns - @result.columns
|
406
|
+
if missing_columns.any?
|
407
|
+
@cohort_error = "Expected user_id and conversion_time columns"
|
408
|
+
end
|
409
|
+
|
410
|
+
# check types (user_id can be any type)
|
411
|
+
unless @cohort_error
|
412
|
+
column_types = @result.columns.zip(@result.column_types).to_h
|
413
|
+
|
414
|
+
if !column_types["cohort_time"].in?(["time", nil])
|
415
|
+
@cohort_error = "cohort_time must be time column"
|
416
|
+
elsif !column_types["conversion_time"].in?(["time", nil])
|
417
|
+
@cohort_error = "conversion_time must be time column"
|
418
|
+
end
|
419
|
+
end
|
420
|
+
end
|
421
|
+
else
|
422
|
+
@today = Blazer.time_zone.today
|
423
|
+
@min_cohort_date, @max_cohort_date = @result.rows.map { |r| r[0] }.minmax
|
424
|
+
@buckets = {}
|
425
|
+
@rows.each do |r|
|
426
|
+
@buckets[[r[0], r[1]]] = r[2]
|
427
|
+
end
|
428
|
+
|
429
|
+
@cohort_dates = []
|
430
|
+
current_date = @max_cohort_date
|
431
|
+
while current_date && current_date >= @min_cohort_date
|
432
|
+
@cohort_dates << current_date
|
433
|
+
current_date =
|
434
|
+
case @cohort_period
|
435
|
+
when "day"
|
436
|
+
current_date - 1
|
437
|
+
when "week"
|
438
|
+
current_date - 7
|
439
|
+
else
|
440
|
+
current_date.prev_month
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
num_cols = @cohort_dates.size
|
445
|
+
@columns = ["Cohort", "Users"] + num_cols.times.map { |i| "#{@conversion_period.titleize} #{i + 1}" }
|
446
|
+
rows = []
|
447
|
+
date_format = @cohort_period == "month" ? "%b %Y" : "%b %-e, %Y"
|
448
|
+
@cohort_dates.each do |date|
|
449
|
+
row = [date.strftime(date_format), @buckets[[date, 0]] || 0]
|
450
|
+
|
451
|
+
num_cols.times do |i|
|
452
|
+
if @today >= date + (@cohort_days * i)
|
453
|
+
row << (@buckets[[date, i + 1]] || 0)
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
rows << row
|
458
|
+
end
|
459
|
+
@rows = rows
|
460
|
+
end
|
461
|
+
end
|
346
462
|
end
|
347
463
|
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
module Blazer
|
2
|
+
class UploadsController < BaseController
|
3
|
+
before_action :ensure_uploads
|
4
|
+
before_action :set_upload, only: [:show, :edit, :update, :destroy]
|
5
|
+
|
6
|
+
def index
|
7
|
+
@uploads = Blazer::Upload.order(:table)
|
8
|
+
end
|
9
|
+
|
10
|
+
def new
|
11
|
+
@upload = Blazer::Upload.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def create
|
15
|
+
@upload = Blazer::Upload.new(upload_params)
|
16
|
+
# use creator_id instead of creator
|
17
|
+
# since we setup association without checking if column exists
|
18
|
+
@upload.creator = blazer_user if @upload.respond_to?(:creator_id=) && blazer_user
|
19
|
+
|
20
|
+
success = params.require(:upload).key?(:file)
|
21
|
+
if success
|
22
|
+
Blazer::Upload.transaction do
|
23
|
+
success = @upload.save
|
24
|
+
if success
|
25
|
+
begin
|
26
|
+
update_file(@upload)
|
27
|
+
rescue CSV::MalformedCSVError, Blazer::UploadError => e
|
28
|
+
@upload.errors.add(:base, e.message)
|
29
|
+
success = false
|
30
|
+
raise ActiveRecord::Rollback
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
else
|
35
|
+
@upload.errors.add(:base, "File can't be blank")
|
36
|
+
end
|
37
|
+
|
38
|
+
if success
|
39
|
+
redirect_to upload_path(@upload)
|
40
|
+
else
|
41
|
+
render_errors @upload
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def show
|
46
|
+
redirect_to new_query_path(upload_id: @upload.id)
|
47
|
+
end
|
48
|
+
|
49
|
+
def edit
|
50
|
+
end
|
51
|
+
|
52
|
+
def update
|
53
|
+
original_table = @upload.table
|
54
|
+
@upload.assign_attributes(upload_params)
|
55
|
+
|
56
|
+
success = nil
|
57
|
+
Blazer::Upload.transaction do
|
58
|
+
success = @upload.save
|
59
|
+
if success
|
60
|
+
if params.require(:upload).key?(:file)
|
61
|
+
begin
|
62
|
+
update_file(@upload, drop: original_table)
|
63
|
+
rescue CSV::MalformedCSVError, Blazer::UploadError => e
|
64
|
+
@upload.errors.add(:base, e.message)
|
65
|
+
success = false
|
66
|
+
raise ActiveRecord::Rollback
|
67
|
+
end
|
68
|
+
elsif @upload.table != original_table
|
69
|
+
Blazer.uploads_connection.execute("ALTER TABLE #{Blazer.uploads_table_name(original_table)} RENAME TO #{Blazer.uploads_connection.quote_table_name(@upload.table)}")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
if success
|
75
|
+
redirect_to upload_path(@upload)
|
76
|
+
else
|
77
|
+
render_errors @upload
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def destroy
|
82
|
+
Blazer.uploads_connection.execute("DROP TABLE IF EXISTS #{@upload.table_name}")
|
83
|
+
@upload.destroy
|
84
|
+
redirect_to uploads_path
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
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
|
116
|
+
end
|
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
|
127
|
+
end
|
128
|
+
rescue ActiveRecord::StatementInvalid => e
|
129
|
+
raise Blazer::UploadError, "Table already exists" if e.message.include?("PG::DuplicateTable")
|
130
|
+
raise e
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def upload_params
|
135
|
+
params.require(:upload).except(:file).permit(:table, :description)
|
136
|
+
end
|
137
|
+
|
138
|
+
def set_upload
|
139
|
+
@upload = Blazer::Upload.find(params[:id])
|
140
|
+
end
|
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
|
146
|
+
end
|
147
|
+
end
|
@@ -62,7 +62,7 @@ module Blazer
|
|
62
62
|
|
63
63
|
# checks shouldn't have variables, but in any case,
|
64
64
|
# avoid passing variable params to url helpers
|
65
|
-
# (known unsafe parameters are removed, but
|
65
|
+
# (known unsafe parameters are removed, but still not ideal)
|
66
66
|
def self.query_url(id)
|
67
67
|
Blazer::Engine.routes.url_helpers.query_url(id, ActionMailer::Base.default_url_options)
|
68
68
|
end
|
data/app/models/blazer/query.rb
CHANGED
@@ -8,6 +8,7 @@ module Blazer
|
|
8
8
|
|
9
9
|
validates :statement, presence: true
|
10
10
|
|
11
|
+
scope :active, -> { column_names.include?("status") ? where(status: "active") : all }
|
11
12
|
scope :named, -> { where("blazer_queries.name <> ''") }
|
12
13
|
|
13
14
|
def to_param
|
@@ -34,7 +35,13 @@ module Blazer
|
|
34
35
|
end
|
35
36
|
|
36
37
|
def variables
|
37
|
-
Blazer.extract_vars(statement)
|
38
|
+
variables = Blazer.extract_vars(statement)
|
39
|
+
variables += ["cohort_period"] if cohort_analysis?
|
40
|
+
variables
|
41
|
+
end
|
42
|
+
|
43
|
+
def cohort_analysis?
|
44
|
+
/\/\*\s*cohort analysis\s*\*\//i.match?(statement)
|
38
45
|
end
|
39
46
|
end
|
40
47
|
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Blazer
|
2
|
+
class Upload < Record
|
3
|
+
belongs_to :creator, optional: true, class_name: Blazer.user_class.to_s if Blazer.user_class
|
4
|
+
|
5
|
+
validates :table, presence: true, uniqueness: true, format: {with: /\A[a-z0-9_]+\z/, message: "can only contain lowercase letters, numbers, and underscores"}, length: {maximum: 63}
|
6
|
+
|
7
|
+
def table_name
|
8
|
+
Blazer.uploads_table_name(table)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|