blazer 1.7.7 → 2.6.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (139) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +242 -33
  3. data/CONTRIBUTING.md +42 -0
  4. data/LICENSE.txt +1 -1
  5. data/README.md +621 -211
  6. data/app/assets/fonts/blazer/glyphicons-halflings-regular.eot +0 -0
  7. data/app/assets/fonts/blazer/glyphicons-halflings-regular.svg +0 -0
  8. data/app/assets/fonts/blazer/glyphicons-halflings-regular.ttf +0 -0
  9. data/app/assets/fonts/blazer/glyphicons-halflings-regular.woff +0 -0
  10. data/app/assets/fonts/blazer/glyphicons-halflings-regular.woff2 +0 -0
  11. data/app/assets/images/blazer/favicon.png +0 -0
  12. data/app/assets/javascripts/blazer/Chart.js +15658 -10011
  13. data/app/assets/javascripts/blazer/Sortable.js +3413 -848
  14. data/app/assets/javascripts/blazer/ace/ace.js +21294 -4
  15. data/app/assets/javascripts/blazer/ace/ext-language_tools.js +1991 -3
  16. data/app/assets/javascripts/blazer/ace/mode-sql.js +110 -1
  17. data/app/assets/javascripts/blazer/ace/snippets/sql.js +40 -1
  18. data/app/assets/javascripts/blazer/ace/snippets/text.js +14 -1
  19. data/app/assets/javascripts/blazer/ace/theme-twilight.js +116 -1
  20. data/app/assets/javascripts/blazer/application.js +5 -3
  21. data/app/assets/javascripts/blazer/bootstrap.js +842 -628
  22. data/app/assets/javascripts/blazer/chartkick.js +2015 -1244
  23. data/app/assets/javascripts/blazer/daterangepicker.js +372 -299
  24. data/app/assets/javascripts/blazer/highlight.min.js +3 -0
  25. data/app/assets/javascripts/blazer/{jquery_ujs.js → jquery-ujs.js} +161 -75
  26. data/app/assets/javascripts/blazer/jquery.js +10126 -9562
  27. data/app/assets/javascripts/blazer/jquery.stickytableheaders.js +321 -259
  28. data/app/assets/javascripts/blazer/moment-timezone-with-data.js +1546 -0
  29. data/app/assets/javascripts/blazer/moment.js +5085 -2460
  30. data/app/assets/javascripts/blazer/queries.js +18 -4
  31. data/app/assets/javascripts/blazer/routes.js +3 -0
  32. data/app/assets/javascripts/blazer/selectize.js +3828 -3604
  33. data/app/assets/javascripts/blazer/stupidtable-custom-settings.js +13 -0
  34. data/app/assets/javascripts/blazer/stupidtable.js +254 -87
  35. data/app/assets/javascripts/blazer/vue.js +11175 -6676
  36. data/app/assets/stylesheets/blazer/application.css +51 -6
  37. data/app/assets/stylesheets/blazer/bootstrap-propshaft.css +10 -0
  38. data/app/assets/stylesheets/blazer/bootstrap-sprockets.css.erb +10 -0
  39. data/app/assets/stylesheets/blazer/{bootstrap.css.erb → bootstrap.css} +1337 -711
  40. data/app/assets/stylesheets/blazer/{daterangepicker-bs3.css → daterangepicker.css} +207 -172
  41. data/app/assets/stylesheets/blazer/{selectize.default.css → selectize.css} +26 -10
  42. data/app/controllers/blazer/base_controller.rb +73 -46
  43. data/app/controllers/blazer/checks_controller.rb +1 -1
  44. data/app/controllers/blazer/dashboards_controller.rb +7 -13
  45. data/app/controllers/blazer/queries_controller.rb +171 -51
  46. data/app/controllers/blazer/uploads_controller.rb +147 -0
  47. data/app/helpers/blazer/base_helper.rb +6 -16
  48. data/app/models/blazer/audit.rb +3 -3
  49. data/app/models/blazer/check.rb +31 -5
  50. data/app/models/blazer/dashboard.rb +6 -2
  51. data/app/models/blazer/dashboard_query.rb +1 -1
  52. data/app/models/blazer/query.rb +30 -4
  53. data/app/models/blazer/record.rb +5 -0
  54. data/app/models/blazer/upload.rb +11 -0
  55. data/app/models/blazer/uploads_connection.rb +7 -0
  56. data/app/views/blazer/_nav.html.erb +3 -1
  57. data/app/views/blazer/_variables.html.erb +48 -23
  58. data/app/views/blazer/check_mailer/failing_checks.html.erb +1 -0
  59. data/app/views/blazer/check_mailer/state_change.html.erb +1 -0
  60. data/app/views/blazer/checks/_form.html.erb +17 -9
  61. data/app/views/blazer/checks/edit.html.erb +2 -0
  62. data/app/views/blazer/checks/index.html.erb +37 -5
  63. data/app/views/blazer/checks/new.html.erb +2 -0
  64. data/app/views/blazer/dashboards/_form.html.erb +5 -5
  65. data/app/views/blazer/dashboards/edit.html.erb +2 -0
  66. data/app/views/blazer/dashboards/new.html.erb +2 -0
  67. data/app/views/blazer/dashboards/show.html.erb +13 -7
  68. data/app/views/blazer/queries/_caching.html.erb +16 -0
  69. data/app/views/blazer/queries/_cohorts.html.erb +48 -0
  70. data/app/views/blazer/queries/_form.html.erb +23 -13
  71. data/app/views/blazer/queries/docs.html.erb +137 -0
  72. data/app/views/blazer/queries/home.html.erb +21 -7
  73. data/app/views/blazer/queries/run.html.erb +64 -29
  74. data/app/views/blazer/queries/schema.html.erb +44 -7
  75. data/app/views/blazer/queries/show.html.erb +15 -8
  76. data/app/views/blazer/uploads/_form.html.erb +27 -0
  77. data/app/views/blazer/uploads/edit.html.erb +3 -0
  78. data/app/views/blazer/uploads/index.html.erb +55 -0
  79. data/app/views/blazer/uploads/new.html.erb +3 -0
  80. data/app/views/layouts/blazer/application.html.erb +10 -5
  81. data/config/routes.rb +10 -1
  82. data/lib/blazer/adapters/athena_adapter.rb +182 -0
  83. data/lib/blazer/adapters/base_adapter.rb +24 -1
  84. data/lib/blazer/adapters/bigquery_adapter.rb +79 -0
  85. data/lib/blazer/adapters/cassandra_adapter.rb +70 -0
  86. data/lib/blazer/adapters/drill_adapter.rb +38 -0
  87. data/lib/blazer/adapters/druid_adapter.rb +102 -0
  88. data/lib/blazer/adapters/elasticsearch_adapter.rb +30 -18
  89. data/lib/blazer/adapters/hive_adapter.rb +55 -0
  90. data/lib/blazer/adapters/ignite_adapter.rb +64 -0
  91. data/lib/blazer/adapters/influxdb_adapter.rb +57 -0
  92. data/lib/blazer/adapters/mongodb_adapter.rb +5 -1
  93. data/lib/blazer/adapters/neo4j_adapter.rb +62 -0
  94. data/lib/blazer/adapters/opensearch_adapter.rb +52 -0
  95. data/lib/blazer/adapters/presto_adapter.rb +9 -0
  96. data/lib/blazer/adapters/salesforce_adapter.rb +50 -0
  97. data/lib/blazer/adapters/snowflake_adapter.rb +82 -0
  98. data/lib/blazer/adapters/soda_adapter.rb +105 -0
  99. data/lib/blazer/adapters/spark_adapter.rb +14 -0
  100. data/lib/blazer/adapters/sql_adapter.rb +187 -20
  101. data/{app/mailers → lib}/blazer/check_mailer.rb +0 -0
  102. data/lib/blazer/data_source.rb +107 -30
  103. data/lib/blazer/engine.rb +21 -23
  104. data/lib/blazer/result.rb +95 -29
  105. data/lib/blazer/run_statement.rb +8 -4
  106. data/lib/blazer/run_statement_job.rb +8 -9
  107. data/lib/blazer/slack_notifier.rb +94 -0
  108. data/lib/blazer/statement.rb +75 -0
  109. data/lib/blazer/version.rb +1 -1
  110. data/lib/blazer.rb +154 -26
  111. data/lib/generators/blazer/install_generator.rb +7 -18
  112. data/lib/generators/blazer/templates/{config.yml → config.yml.tt} +26 -3
  113. data/lib/generators/blazer/templates/{install.rb → install.rb.tt} +6 -4
  114. data/lib/generators/blazer/templates/uploads.rb.tt +10 -0
  115. data/lib/generators/blazer/uploads_generator.rb +18 -0
  116. data/lib/tasks/blazer.rake +11 -1
  117. data/licenses/LICENSE-ace.txt +24 -0
  118. data/licenses/LICENSE-bootstrap.txt +21 -0
  119. data/licenses/LICENSE-chart.js.txt +9 -0
  120. data/licenses/LICENSE-chartkick.js.txt +22 -0
  121. data/licenses/LICENSE-daterangepicker.txt +21 -0
  122. data/licenses/LICENSE-fuzzysearch.txt +20 -0
  123. data/licenses/LICENSE-highlight.js.txt +29 -0
  124. data/licenses/LICENSE-jquery-ujs.txt +20 -0
  125. data/licenses/LICENSE-jquery.txt +20 -0
  126. data/licenses/LICENSE-moment-timezone.txt +20 -0
  127. data/licenses/LICENSE-moment.txt +22 -0
  128. data/licenses/LICENSE-selectize.txt +202 -0
  129. data/licenses/LICENSE-sortable.txt +21 -0
  130. data/licenses/LICENSE-stickytableheaders.txt +20 -0
  131. data/licenses/LICENSE-stupidtable.txt +19 -0
  132. data/licenses/LICENSE-vue.txt +21 -0
  133. metadata +83 -53
  134. data/.gitignore +0 -14
  135. data/Gemfile +0 -4
  136. data/Rakefile +0 -1
  137. data/app/assets/javascripts/blazer/highlight.pack.js +0 -1
  138. data/app/assets/javascripts/blazer/moment-timezone.js +0 -1007
  139. data/blazer.gemspec +0 -26
@@ -1,14 +1,12 @@
1
1
  module Blazer
2
2
  class BaseController < ApplicationController
3
- # skip all filters
4
- filters = _process_action_callbacks.map(&:filter)
5
- if Rails::VERSION::MAJOR >= 5
6
- skip_before_action(*filters, raise: false)
7
- skip_after_action(*filters, raise: false)
8
- skip_around_action(*filters, raise: false)
9
- else
10
- skip_action_callback *filters
11
- end
3
+ # skip filters
4
+ filters = _process_action_callbacks.map(&:filter) - [:activate_authlogic]
5
+ skip_before_action(*filters, raise: false)
6
+ skip_after_action(*filters, raise: false)
7
+ skip_around_action(*filters, raise: false)
8
+
9
+ clear_helpers
12
10
 
13
11
  protect_from_forgery with: :exception
14
12
 
@@ -16,46 +14,52 @@ module Blazer
16
14
  http_basic_authenticate_with name: ENV["BLAZER_USERNAME"], password: ENV["BLAZER_PASSWORD"]
17
15
  end
18
16
 
17
+ if Blazer.settings["before_action"]
18
+ raise Blazer::Error, "The docs for protecting Blazer with a custom before_action had an incorrect example from August 2017 to June 2018. The example method had a boolean return value. However, you must render or redirect if a user is unauthorized rather than return a falsy value. Double check that your before_action works correctly for unauthorized users (if it worked when added, there should be no issue). Then, change before_action to before_action_method in config/blazer.yml."
19
+ end
20
+
19
21
  if Blazer.before_action
20
22
  before_action Blazer.before_action.to_sym
21
23
  end
22
24
 
25
+ if Blazer.override_csp
26
+ after_action do
27
+ response.headers['Content-Security-Policy'] = "default-src 'self' https: 'unsafe-inline' 'unsafe-eval' data:"
28
+ end
29
+ end
30
+
23
31
  layout "blazer/application"
24
32
 
25
33
  private
26
34
 
27
- def process_vars(statement, data_source)
28
- (@bind_vars ||= []).concat(extract_vars(statement)).uniq!
35
+ def process_vars(statement, var_params = nil)
36
+ var_params ||= request.query_parameters
37
+ (@bind_vars ||= []).concat(statement.variables).uniq!
38
+ # update in-place so populated in view and consistent across queries on dashboard
29
39
  @bind_vars.each do |var|
30
- params[var] ||= Blazer.data_sources[data_source].variable_defaults[var]
31
- end
32
- @success = @bind_vars.all? { |v| params[v] }
33
-
34
- if @success
35
- @bind_vars.each do |var|
36
- value = params[var].presence
37
- if value
38
- if ["start_time", "end_time"].include?(var)
39
- value = value.to_s.gsub(" ", "+") # fix for Quip bug
40
- end
41
-
42
- if var.end_with?("_at")
43
- begin
44
- value = Blazer.time_zone.parse(value)
45
- rescue
46
- # do nothing
47
- end
48
- end
49
-
50
- if value =~ /\A\d+\z/
51
- value = value.to_i
52
- elsif value =~ /\A\d+\.\d+\z/
53
- value = value.to_f
54
- end
55
- end
56
- statement.gsub!("{#{var}}", ActiveRecord::Base.connection.quote(value))
40
+ if !var_params[var]
41
+ default = statement.data_source.variable_defaults[var]
42
+ # only add if default exists
43
+ var_params[var] = default if default
57
44
  end
58
45
  end
46
+ runnable = @bind_vars.all? { |v| var_params[v] }
47
+ statement.add_values(var_params) if runnable
48
+ runnable
49
+ end
50
+
51
+ def refresh_query(query)
52
+ statement = query.statement_object
53
+ runnable = process_vars(statement)
54
+ cohort_analysis_statement(statement) if statement.cohort_analysis?
55
+ statement.clear_cache if runnable
56
+ end
57
+
58
+ def add_cohort_analysis_vars
59
+ @bind_vars << "cohort_period" unless @bind_vars.include?("cohort_period")
60
+ @smart_vars["cohort_period"] = ["day", "week", "month"] if @smart_vars
61
+ # TODO create var_params method
62
+ request.query_parameters["cohort_period"] ||= "week"
59
63
  end
60
64
 
61
65
  def parse_smart_variables(var, data_source)
@@ -79,20 +83,38 @@ module Blazer
79
83
  [smart_var, error]
80
84
  end
81
85
 
82
- def extract_vars(statement)
83
- # strip commented out lines
84
- # and regex {1} or {1,2}
85
- statement.gsub(/\-\-.+/, "").gsub(/\/\*.+\*\//m, "").scan(/\{\w*?\}/i).map { |v| v[1...-1] }.reject { |v| /\A\d+(\,\d+)?\z/.match(v) || v.empty? }.uniq
86
+ def cohort_analysis_statement(statement)
87
+ @cohort_period = params["cohort_period"] || "week"
88
+ @cohort_period = "week" unless ["day", "week", "month"].include?(@cohort_period)
89
+
90
+ # for now
91
+ @conversion_period = @cohort_period
92
+ @cohort_days =
93
+ case @cohort_period
94
+ when "day"
95
+ 1
96
+ when "week"
97
+ 7
98
+ when "month"
99
+ 30
100
+ end
101
+
102
+ statement.apply_cohort_analysis(period: @cohort_period, days: @cohort_days)
86
103
  end
87
- helper_method :extract_vars
88
104
 
89
- def variable_params
90
- params.except(: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).permit!
105
+ # TODO allow all keys
106
+ # or show error message for disallowed keys
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]
108
+
109
+ def variable_params(resource, var_params = nil)
110
+ permitted_keys = resource.variables - UNPERMITTED_KEYS.map(&:to_s)
111
+ var_params ||= request.query_parameters
112
+ var_params.slice(*permitted_keys)
91
113
  end
92
114
  helper_method :variable_params
93
115
 
94
116
  def blazer_user
95
- send(Blazer.user_method) if Blazer.user_method && respond_to?(Blazer.user_method)
117
+ send(Blazer.user_method) if Blazer.user_method && respond_to?(Blazer.user_method, true)
96
118
  end
97
119
  helper_method :blazer_user
98
120
 
@@ -101,5 +123,10 @@ module Blazer
101
123
  action = resource.persisted? ? :edit : :new
102
124
  render action, status: :unprocessable_entity
103
125
  end
126
+
127
+ # do not inherit from ApplicationController - #120
128
+ def default_url_options
129
+ {}
130
+ end
104
131
  end
105
132
  end
@@ -46,7 +46,7 @@ module Blazer
46
46
  private
47
47
 
48
48
  def check_params
49
- params.require(:check).permit(:query_id, :emails, :invert, :check_type, :schedule)
49
+ params.require(:check).permit(:query_id, :emails, :slack_channels, :invert, :check_type, :schedule)
50
50
  end
51
51
 
52
52
  def set_check
@@ -2,10 +2,6 @@ module Blazer
2
2
  class DashboardsController < BaseController
3
3
  before_action :set_dashboard, only: [:show, :edit, :update, :destroy, :refresh]
4
4
 
5
- def index
6
- redirect_to root_path(filter: "dashboards")
7
- end
8
-
9
5
  def new
10
6
  @dashboard = Blazer::Dashboard.new
11
7
  end
@@ -26,7 +22,7 @@ module Blazer
26
22
  def show
27
23
  @queries = @dashboard.dashboard_queries.order(:position).preload(:query).map(&:query)
28
24
  @queries.each do |query|
29
- process_vars(query.statement, query.data_source)
25
+ @success = process_vars(query.statement_object)
30
26
  end
31
27
  @bind_vars ||= []
32
28
 
@@ -40,6 +36,8 @@ module Blazer
40
36
  @sql_errors << error if error
41
37
  end
42
38
  end
39
+
40
+ add_cohort_analysis_vars if @queries.any?(&:cohort_analysis?)
43
41
  end
44
42
 
45
43
  def edit
@@ -47,7 +45,7 @@ module Blazer
47
45
 
48
46
  def update
49
47
  if update_dashboard(@dashboard)
50
- redirect_to dashboard_path(@dashboard, variable_params)
48
+ redirect_to dashboard_path(@dashboard, params: variable_params(@dashboard))
51
49
  else
52
50
  render_errors @dashboard
53
51
  end
@@ -55,18 +53,14 @@ module Blazer
55
53
 
56
54
  def destroy
57
55
  @dashboard.destroy
58
- redirect_to dashboards_path
56
+ redirect_to root_path
59
57
  end
60
58
 
61
59
  def refresh
62
60
  @dashboard.queries.each do |query|
63
- data_source = Blazer.data_sources[query.data_source]
64
- statement = query.statement.dup
65
- process_vars(statement, query.data_source)
66
- Blazer.transform_statement.call(data_source, statement) if Blazer.transform_statement
67
- data_source.clear_cache(statement)
61
+ refresh_query(query)
68
62
  end
69
- redirect_to dashboard_path(@dashboard, variable_params)
63
+ redirect_to dashboard_path(@dashboard, params: variable_params(@dashboard))
70
64
  end
71
65
 
72
66
  private
@@ -1,15 +1,12 @@
1
1
  module Blazer
2
2
  class QueriesController < BaseController
3
3
  before_action :set_query, only: [:show, :edit, :update, :destroy, :refresh]
4
+ before_action :set_data_source, only: [:tables, :docs, :schema, :cancel]
4
5
 
5
6
  def home
6
- if params[:filter] == "dashboards"
7
- @queries = []
8
- else
9
- set_queries(1000)
10
- end
7
+ set_queries(1000)
11
8
 
12
- if params[:filter] && params[:filter] != "dashboards"
9
+ if params[:filter]
13
10
  @dashboards = [] # TODO show my dashboards
14
11
  else
15
12
  @dashboards = Blazer::Dashboard.order(:name)
@@ -41,49 +38,72 @@ module Blazer
41
38
  if params[:fork_query_id]
42
39
  @query.statement ||= Blazer::Query.find(params[:fork_query_id]).try(:statement)
43
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
44
47
  end
45
48
 
46
49
  def create
47
50
  @query = Blazer::Query.new(query_params)
48
51
  @query.creator = blazer_user if @query.respond_to?(:creator)
52
+ @query.status = "active" if @query.respond_to?(:status)
49
53
 
50
54
  if @query.save
51
- redirect_to query_path(@query, variable_params)
55
+ redirect_to query_path(@query, params: variable_params(@query))
52
56
  else
53
57
  render_errors @query
54
58
  end
55
59
  end
56
60
 
57
61
  def show
58
- @statement = @query.statement.dup
59
- process_vars(@statement, @query.data_source)
62
+ @statement = @query.statement_object
63
+ @success = process_vars(@statement)
60
64
 
61
65
  @smart_vars = {}
62
66
  @sql_errors = []
63
- data_source = Blazer.data_sources[@query.data_source]
64
67
  @bind_vars.each do |var|
65
- smart_var, error = parse_smart_variables(var, data_source)
68
+ smart_var, error = parse_smart_variables(var, @statement.data_source)
66
69
  @smart_vars[var] = smart_var if smart_var
67
70
  @sql_errors << error if error
68
71
  end
69
72
 
70
- Blazer.transform_statement.call(data_source, @statement) if Blazer.transform_statement
73
+ @query.update!(status: "active") if @query.respond_to?(:status) && @query.status.in?(["archived", nil])
74
+
75
+ add_cohort_analysis_vars if @query.cohort_analysis?
71
76
  end
72
77
 
73
78
  def edit
74
79
  end
75
80
 
76
81
  def run
77
- @statement = params[:statement]
78
- data_source = params[:data_source]
79
- process_vars(@statement, data_source)
80
- @only_chart = params[:only_chart]
81
- @run_id = blazer_params[:run_id]
82
82
  @query = Query.find_by(id: params[:query_id]) if params[:query_id]
83
+
84
+ # use query data source when present
85
+ # need to update viewable? logic below if this changes
83
86
  data_source = @query.data_source if @query && @query.data_source
87
+ data_source ||= params[:data_source]
84
88
  @data_source = Blazer.data_sources[data_source]
85
89
 
86
- if @run_id
90
+ @statement = Blazer::Statement.new(params[:statement], @data_source)
91
+ # before process_vars
92
+ @cohort_analysis = @statement.cohort_analysis?
93
+
94
+ # fallback for now for users with open tabs
95
+ # TODO remove fallback in future version
96
+ @var_params = request.request_parameters["variables"] || request.request_parameters
97
+ @success = process_vars(@statement, @var_params)
98
+ @only_chart = params[:only_chart]
99
+ @run_id = blazer_params[:run_id]
100
+
101
+ run_cohort_analysis if @cohort_analysis
102
+
103
+ # ensure viewable
104
+ if !(@query || Query.new(data_source: @data_source.id)).viewable?(blazer_user)
105
+ render_forbidden
106
+ elsif @run_id
87
107
  @timestamp = blazer_params[:timestamp].to_i
88
108
 
89
109
  @result = @data_source.run_results(@run_id)
@@ -113,16 +133,15 @@ module Blazer
113
133
 
114
134
  options = {user: blazer_user, query: @query, refresh_cache: params[:check], run_id: @run_id, async: Blazer.async}
115
135
  if Blazer.async && request.format.symbol != :csv
116
- result = []
117
- Blazer::RunStatementJob.perform_async(result, @data_source, @statement, options)
136
+ Blazer::RunStatementJob.perform_later(@data_source.id, @statement.statement, options.merge(values: @statement.values))
118
137
  wait_start = Time.now
119
138
  loop do
120
- sleep(0.02)
121
- break if result.any? || Time.now - wait_start > 3
139
+ sleep(0.1)
140
+ @result = @data_source.run_results(@run_id)
141
+ break if @result || Time.now - wait_start > 3
122
142
  end
123
- @result = result.first
124
143
  else
125
- @result = Blazer::RunStatement.new.perform(@data_source, @statement, options)
144
+ @result = Blazer::RunStatement.new.perform(@statement, options)
126
145
  end
127
146
 
128
147
  if @result
@@ -134,6 +153,13 @@ module Blazer
134
153
  @cached_at = @result.cached_at
135
154
  @just_cached = @result.just_cached
136
155
 
156
+ @forecast = @query && @result.forecastable? && params[:forecast]
157
+ if @forecast
158
+ @result.forecast
159
+ @forecast_error = @result.forecast_error
160
+ @forecast = @forecast_error.nil?
161
+ end
162
+
137
163
  render_run
138
164
  else
139
165
  @timestamp = Time.now.to_i
@@ -145,12 +171,8 @@ module Blazer
145
171
  end
146
172
 
147
173
  def refresh
148
- data_source = Blazer.data_sources[@query.data_source]
149
- @statement = @query.statement.dup
150
- process_vars(@statement, @query.data_source)
151
- Blazer.transform_statement.call(data_source, @statement) if Blazer.transform_statement
152
- data_source.clear_cache(@statement)
153
- redirect_to query_path(@query, variable_params)
174
+ refresh_query(@query)
175
+ redirect_to query_path(@query, params: variable_params(@query))
154
176
  end
155
177
 
156
178
  def update
@@ -158,11 +180,12 @@ module Blazer
158
180
  @query = Blazer::Query.new
159
181
  @query.creator = blazer_user if @query.respond_to?(:creator)
160
182
  end
183
+ @query.status = "active" if @query.respond_to?(:status)
161
184
  unless @query.editable?(blazer_user)
162
185
  @query.errors.add(:base, "Sorry, permission denied")
163
186
  end
164
187
  if @query.errors.empty? && @query.update(query_params)
165
- redirect_to query_path(@query, variable_params)
188
+ redirect_to query_path(@query, params: variable_params(@query))
166
189
  else
167
190
  render_errors @query
168
191
  end
@@ -170,24 +193,38 @@ module Blazer
170
193
 
171
194
  def destroy
172
195
  @query.destroy if @query.editable?(blazer_user)
173
- redirect_to root_url
196
+ redirect_to root_path
174
197
  end
175
198
 
176
199
  def tables
177
- render json: Blazer.data_sources[params[:data_source]].tables
200
+ render json: @data_source.tables
201
+ end
202
+
203
+ def docs
204
+ @smart_variables = @data_source.smart_variables
205
+ @linked_columns = @data_source.linked_columns
206
+ @smart_columns = @data_source.smart_columns
178
207
  end
179
208
 
180
209
  def schema
181
- @schema = Blazer.data_sources[params[:data_source]].schema
210
+ @schema = @data_source.schema
182
211
  end
183
212
 
184
213
  def cancel
185
- Blazer.data_sources[params[:data_source]].cancel(blazer_run_id)
186
- render json: {}
214
+ @data_source.cancel(blazer_run_id)
215
+ head :ok
187
216
  end
188
217
 
189
218
  private
190
219
 
220
+ def set_data_source
221
+ @data_source = Blazer.data_sources[params[:data_source]]
222
+
223
+ unless Query.new(data_source: @data_source.id).editable?(blazer_user)
224
+ render_forbidden
225
+ end
226
+ end
227
+
191
228
  def continue_run
192
229
  render json: {run_id: @run_id, timestamp: @timestamp}, status: :accepted
193
230
  end
@@ -198,7 +235,7 @@ module Blazer
198
235
  @first_row = @rows.first || []
199
236
  @column_types = []
200
237
  if @rows.any?
201
- @columns.each_with_index do |column, i|
238
+ @columns.each_with_index do |_, i|
202
239
  @column_types << (
203
240
  case @first_row[i]
204
241
  when Integer
@@ -212,7 +249,6 @@ module Blazer
212
249
  end
213
250
  end
214
251
 
215
- @filename = @query.name.parameterize if @query
216
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)
217
253
 
218
254
  @boom = @result.boom if @result
@@ -229,7 +265,9 @@ module Blazer
229
265
  r[lat_index] && r[lon_index]
230
266
  end.map do |r|
231
267
  {
232
- title: r.each_with_index.map{ |v, i| i == lat_index || i == lon_index ? nil : "<strong>#{@columns[i]}:</strong> #{v}" }.compact.join("<br />").truncate(140),
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),
233
271
  latitude: r[lat_index],
234
272
  longitude: r[lon_index]
235
273
  }
@@ -237,24 +275,22 @@ module Blazer
237
275
  end
238
276
  end
239
277
 
278
+ render_cohort_analysis if @cohort_analysis && !@error
279
+
240
280
  respond_to do |format|
241
281
  format.html do
242
282
  render layout: false
243
283
  end
244
284
  format.csv do
285
+ # not ideal, but useful for testing
286
+ raise Error, @error if @error && Rails.env.test?
287
+
245
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\""
246
289
  end
247
290
  end
248
291
  end
249
292
 
250
293
  def set_queries(limit = nil)
251
- @my_queries =
252
- if limit && blazer_user && !params[:filter] && Blazer.audit
253
- queries_by_ids(Blazer::Audit.where(user_id: blazer_user.id).where("created_at > ?", 30.days.ago).where("query_id IS NOT NULL").group(:query_id).order("count_all desc").count.keys)
254
- else
255
- []
256
- end
257
-
258
294
  @queries = Blazer::Query.named.select(:id, :name, :creator_id, :statement)
259
295
  @queries = @queries.includes(:creator) if Blazer.user_class
260
296
 
@@ -263,15 +299,14 @@ module Blazer
263
299
  elsif blazer_user && params[:filter] == "viewed" && Blazer.audit
264
300
  @queries = queries_by_ids(Blazer::Audit.where(user_id: blazer_user.id).order(created_at: :desc).limit(500).pluck(:query_id).uniq)
265
301
  else
266
- @queries = @queries.where("id NOT IN (?)", @my_queries.map(&:id)) if @my_queries.any?
267
302
  @queries = @queries.limit(limit) if limit
268
- @queries = @queries.order(:name)
303
+ @queries = @queries.active.order(:name)
269
304
  end
270
305
  @queries = @queries.to_a
271
306
 
272
307
  @more = limit && @queries.size >= limit
273
308
 
274
- @queries = (@my_queries + @queries).select { |q| !q.name.to_s.start_with?("#") || q.try(:creator).try(:id) == blazer_user.try(:id) }
309
+ @queries = @queries.select { |q| !q.name.to_s.start_with?("#") || q.try(:creator).try(:id) == blazer_user.try(:id) }
275
310
 
276
311
  @queries =
277
312
  @queries.map do |q|
@@ -279,14 +314,14 @@ module Blazer
279
314
  id: q.id,
280
315
  name: q.name,
281
316
  creator: blazer_user && q.try(:creator) == blazer_user ? "You" : q.try(:creator).try(Blazer.user_name),
282
- vars: extract_vars(q.statement).join(", "),
317
+ vars: q.variables.join(", "),
283
318
  to_param: q.to_param
284
319
  }
285
320
  end
286
321
  end
287
322
 
288
323
  def queries_by_ids(favorite_query_ids)
289
- queries = Blazer::Query.named.where(id: favorite_query_ids)
324
+ queries = Blazer::Query.active.named.where(id: favorite_query_ids)
290
325
  queries = queries.includes(:creator) if Blazer.user_class
291
326
  queries = queries.index_by(&:id)
292
327
  favorite_query_ids.map { |query_id| queries[query_id] }.compact
@@ -294,6 +329,14 @@ module Blazer
294
329
 
295
330
  def set_query
296
331
  @query = Blazer::Query.find(params[:id].to_s.split("-").first)
332
+
333
+ unless @query.viewable?(blazer_user)
334
+ render_forbidden
335
+ end
336
+ end
337
+
338
+ def render_forbidden
339
+ render plain: "Access denied", status: :forbidden
297
340
  end
298
341
 
299
342
  def query_params
@@ -321,5 +364,82 @@ module Blazer
321
364
  def blazer_run_id
322
365
  params[:run_id].to_s.gsub(/[^a-z0-9\-]/i, "")
323
366
  end
367
+
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
372
+
373
+ @show_cohort_rows = !params[:query_id] || @cohort_error
374
+ cohort_analysis_statement(@statement) unless @show_cohort_rows
375
+ end
376
+
377
+ def render_cohort_analysis
378
+ if @show_cohort_rows
379
+ @cohort_analysis = false
380
+
381
+ @row_limit = 1000
382
+
383
+ # check results
384
+ 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
395
+
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
401
+ end
402
+ 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
425
+
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
437
+ end
438
+
439
+ rows << row
440
+ end
441
+ @rows = rows
442
+ end
443
+ end
324
444
  end
325
445
  end