blazer 0.0.8 → 1.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.
Potentially problematic release.
This version of blazer might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +261 -45
- data/app/assets/javascripts/blazer/Sortable.js +1144 -0
- data/app/assets/javascripts/blazer/application.js +2 -1
- data/app/assets/javascripts/blazer/chartkick.js +935 -0
- data/app/assets/javascripts/blazer/selectize.js +391 -201
- data/app/assets/stylesheets/blazer/application.css +17 -2
- data/app/assets/stylesheets/blazer/selectize.default.css +3 -2
- data/app/controllers/blazer/base_controller.rb +48 -0
- data/app/controllers/blazer/checks_controller.rb +51 -0
- data/app/controllers/blazer/dashboards_controller.rb +94 -0
- data/app/controllers/blazer/queries_controller.rb +29 -101
- data/app/helpers/blazer/{queries_helper.rb → base_helper.rb} +1 -1
- data/app/mailers/blazer/check_mailer.rb +21 -0
- data/app/models/blazer/check.rb +28 -0
- data/app/models/blazer/connection.rb +0 -1
- data/app/models/blazer/dashboard.rb +12 -0
- data/app/models/blazer/dashboard_query.rb +9 -0
- data/app/models/blazer/query.rb +5 -0
- data/app/views/blazer/check_mailer/failing_checks.html.erb +6 -0
- data/app/views/blazer/check_mailer/state_change.html.erb +6 -0
- data/app/views/blazer/checks/_form.html.erb +28 -0
- data/app/views/blazer/checks/edit.html.erb +1 -0
- data/app/views/blazer/checks/index.html.erb +41 -0
- data/app/views/blazer/checks/new.html.erb +1 -0
- data/app/views/blazer/checks/run.html.erb +9 -0
- data/app/views/blazer/dashboards/_form.html.erb +86 -0
- data/app/views/blazer/dashboards/edit.html.erb +1 -0
- data/app/views/blazer/dashboards/index.html.erb +21 -0
- data/app/views/blazer/dashboards/new.html.erb +1 -0
- data/app/views/blazer/dashboards/show.html.erb +148 -0
- data/app/views/blazer/queries/_form.html.erb +16 -5
- data/app/views/blazer/queries/_tables.html +5 -0
- data/app/views/blazer/queries/index.html.erb +6 -0
- data/app/views/blazer/queries/run.html.erb +59 -44
- data/app/views/blazer/queries/show.html.erb +20 -16
- data/config/routes.rb +5 -0
- data/lib/blazer.rb +46 -2
- data/lib/blazer/data_source.rb +70 -0
- data/lib/blazer/engine.rb +6 -2
- data/lib/blazer/tasks.rb +12 -0
- data/lib/blazer/version.rb +1 -1
- data/lib/generators/blazer/templates/config.yml +26 -6
- data/lib/generators/blazer/templates/install.rb +21 -0
- metadata +27 -3
@@ -14,9 +14,21 @@
|
|
14
14
|
<div class="form-group text-right">
|
15
15
|
<div class="pull-left" style="margin-top: 6px;">
|
16
16
|
<%= link_to "Back", :back %>
|
17
|
-
|
17
|
+
<% if Blazer.data_sources.size == 1 %>
|
18
|
+
<span class="text-muted" style="margin-left: 20px;"> Use {start_time} and {end_time} for time ranges</span>
|
19
|
+
<% end %>
|
18
20
|
</div>
|
19
|
-
<%=
|
21
|
+
<%= f.select :data_source, Blazer.data_sources.values.map { |ds| [ds.name, ds.id] }, {}, class: ("hide" if Blazer.data_sources.size == 1), style: "width: 140px;" %>
|
22
|
+
<div id="tables" style="display: inline-block; width: 260px; margin-right: 10px;" class="hide">
|
23
|
+
<%= render partial: "tables" %>
|
24
|
+
</div>
|
25
|
+
<script>
|
26
|
+
function updatePreviewSelect() {
|
27
|
+
$("#tables").load("<%= tables_queries_path %>?" + $.param({data_source: $("#query_data_source").val()}));
|
28
|
+
}
|
29
|
+
updatePreviewSelect();
|
30
|
+
$("#query_data_source").selectize().change(updatePreviewSelect);
|
31
|
+
</script>
|
20
32
|
<%= link_to "Run", "#", class: "btn btn-info", id: "run", style: "vertical-align: top;" %>
|
21
33
|
</div>
|
22
34
|
</div>
|
@@ -99,7 +111,7 @@
|
|
99
111
|
if (xhr) {
|
100
112
|
xhr.abort();
|
101
113
|
}
|
102
|
-
xhr = $.post("<%= run_queries_path %>", $.extend({}, params, {statement: editor.getValue()}), function (data) {
|
114
|
+
xhr = $.post("<%= run_queries_path %>", $.extend({}, params, {statement: editor.getValue(), data_source: $("#query_data_source").val()}), function (data) {
|
103
115
|
$("#results").html(data);
|
104
116
|
|
105
117
|
error_line = /LINE (\d+)/g.exec($("#results").find('.alert-danger').text());
|
@@ -118,8 +130,7 @@
|
|
118
130
|
$("#run").click();
|
119
131
|
}
|
120
132
|
|
121
|
-
$("#table_names")
|
122
|
-
}).change( function () {
|
133
|
+
$(document).on("change", "#table_names", function () {
|
123
134
|
var val = $(this).val();
|
124
135
|
if (val.length > 0) {
|
125
136
|
editor.setValue("SELECT * FROM " + val + " LIMIT 10");
|
@@ -2,6 +2,8 @@
|
|
2
2
|
<div id="header" style="margin-bottom: 20px;">
|
3
3
|
<div class="pull-right">
|
4
4
|
<%= link_to "New Query", new_query_path, class: "btn btn-info" %>
|
5
|
+
<%= link_to "Dashboards", dashboards_path, class: "btn btn-primary" %>
|
6
|
+
<%= link_to "Checks", checks_path, class: "btn btn-primary" %>
|
5
7
|
</div>
|
6
8
|
<input type="text" placeholder="Start typing a query or person" style="width: 300px; display: inline-block;" autofocus=true class="search form-control" />
|
7
9
|
</div>
|
@@ -18,12 +20,16 @@
|
|
18
20
|
<tr>
|
19
21
|
<td class="query">
|
20
22
|
<%= link_to query.name, query %>
|
23
|
+
<span style="color: #ccc;"><%= extract_vars(query.statement).join(", ") %></span>
|
21
24
|
<% if query.created_at > 2.days.ago %>
|
22
25
|
<small style="font-weight: bold; color: #5cb85c;">NEW</small>
|
23
26
|
<% end %>
|
24
27
|
<% if @trending_queries[query.id] %>
|
25
28
|
<small style="font-weight: bold; color: #f60;">TRENDING</small>
|
26
29
|
<% end %>
|
30
|
+
<% if @checks[query.id] %>
|
31
|
+
<small style="font-weight: bold; color: #f60;">CHECK</small>
|
32
|
+
<% end %>
|
27
33
|
<div class="hide"><%= query.name.gsub(/\s+/, "") %></div>
|
28
34
|
</td>
|
29
35
|
<td class="creator text-right text-muted">
|
@@ -1,61 +1,76 @@
|
|
1
1
|
<% if @error %>
|
2
2
|
<div class="alert alert-danger"><%= @error %></div>
|
3
3
|
<% elsif !@success %>
|
4
|
-
|
4
|
+
<% if @only_chart %>
|
5
|
+
<p class="text-muted">Select variables</p>
|
6
|
+
<% else %>
|
7
|
+
<div class="alert alert-info">Can’t preview queries with variables...yet!</div>
|
8
|
+
<% end %>
|
5
9
|
<% else %>
|
6
|
-
|
10
|
+
<% unless @only_chart %>
|
11
|
+
<p class="text-muted"><%= pluralize(@rows.size, "row") %></p>
|
12
|
+
<% end %>
|
7
13
|
<% if @rows.any? %>
|
8
14
|
<% values = @rows.first.values %>
|
9
|
-
<%
|
15
|
+
<% chart_id = SecureRandom.hex %>
|
16
|
+
<% if values.size >= 2 && (values.first.is_a?(Time) || values.first.is_a?(Date)) && values[1..-1].all?{|v| v.is_a?(Numeric) } %>
|
10
17
|
<% time_k = @columns.keys.first %>
|
11
|
-
<%= line_chart @columns.keys[1..-1].map{|k| {name: k, data: @rows.map{|r| [r[time_k], r[k]] }} } %>
|
12
|
-
<% elsif values.size == 3 && values.first.is_a?(Time) && values[1].is_a?(String) && values[2].is_a?(Numeric) %>
|
18
|
+
<%= line_chart @columns.keys[1..-1].map{|k| {name: k, data: @rows.map{|r| [r[time_k], r[k]] }} }, id: chart_id, min: nil %>
|
19
|
+
<% elsif values.size == 3 && (values.first.is_a?(Time) || values.first.is_a?(Date)) && values[1].is_a?(String) && values[2].is_a?(Numeric) %>
|
13
20
|
<% keys = @columns.keys %>
|
14
|
-
<%= line_chart @rows.group_by { |v| v[keys[1]] }.map { |name, v| {name: name, data: v.map { |v2| [v2[keys[0]], v2[keys[2]]] } } } %>
|
21
|
+
<%= line_chart @rows.group_by { |v| v[keys[1]] }.map { |name, v| {name: name, data: v.map { |v2| [v2[keys[0]], v2[keys[2]]] } } }, id: chart_id, min: nil %>
|
15
22
|
<% elsif values.size == 2 && values.first.is_a?(String) && values.last.is_a?(Numeric) %>
|
16
|
-
<%= pie_chart @rows.map(&:values), library: {sliceVisibilityThreshold: 1 / 40.0} %>
|
23
|
+
<%= pie_chart @rows.map(&:values), library: {sliceVisibilityThreshold: 1 / 40.0}, id: chart_id %>
|
24
|
+
<% elsif @only_chart %>
|
25
|
+
<% @no_chart = true %>
|
17
26
|
<% end %>
|
18
27
|
|
19
|
-
<%
|
20
|
-
|
21
|
-
<
|
22
|
-
<
|
23
|
-
|
24
|
-
<
|
25
|
-
|
26
|
-
<%=
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
<
|
35
|
-
<%
|
36
|
-
<
|
37
|
-
<%
|
38
|
-
|
39
|
-
|
28
|
+
<% unless @only_chart && !@no_chart %>
|
29
|
+
<% header_width = 100 / @rows.first.keys.size.to_f %>
|
30
|
+
<div class="results-container">
|
31
|
+
<table class="table results-table" style="margin-bottom: 0;">
|
32
|
+
<thead>
|
33
|
+
<tr>
|
34
|
+
<% @columns.each do |key, type| %>
|
35
|
+
<th style="width: <%= header_width %>%;" data-sort="<%= type %>">
|
36
|
+
<div style="min-width: <%= @min_width_types.include?(key) ? 180 : 60 %>px;">
|
37
|
+
<%= key %>
|
38
|
+
</div>
|
39
|
+
</th>
|
40
|
+
<% end %>
|
41
|
+
</tr>
|
42
|
+
</thead>
|
43
|
+
<tbody>
|
44
|
+
<% @rows.each do |row| %>
|
45
|
+
<tr>
|
46
|
+
<% row.each do |k, v| %>
|
47
|
+
<td>
|
48
|
+
<% if v.is_a?(Time) %>
|
49
|
+
<% v = v.in_time_zone(Blazer.time_zone) %>
|
50
|
+
<% end %>
|
40
51
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
52
|
+
<% unless v.nil? %>
|
53
|
+
<% if v == "" %>
|
54
|
+
<div class="text-muted">empty string</div>
|
55
|
+
<% elsif @linked_columns[k] %>
|
56
|
+
<%= link_to format_value(k, v), @linked_columns[k].gsub("{value}", u(v.to_s)), target: "_blank" %>
|
57
|
+
<% else %>
|
58
|
+
<%= format_value(k, v) %>
|
59
|
+
<% end %>
|
49
60
|
|
50
|
-
|
51
|
-
|
52
|
-
|
61
|
+
<% if v2 = (@boom[k] || {})[v] %>
|
62
|
+
<div class="text-muted"><%= v2 %></div>
|
63
|
+
<% end %>
|
64
|
+
<% end %>
|
65
|
+
</td>
|
53
66
|
<% end %>
|
54
|
-
</
|
67
|
+
</tr>
|
55
68
|
<% end %>
|
56
|
-
</
|
57
|
-
|
58
|
-
</
|
59
|
-
|
69
|
+
</tbody>
|
70
|
+
</table>
|
71
|
+
</div>
|
72
|
+
<% end %>
|
73
|
+
<% elsif @only_chart %>
|
74
|
+
<p class="text-muted">No rows</p>
|
60
75
|
<% end %>
|
61
76
|
<% end %>
|
@@ -1,5 +1,19 @@
|
|
1
1
|
<% title @query.name %>
|
2
2
|
|
3
|
+
<script>
|
4
|
+
function submitIfCompleted($form) {
|
5
|
+
var completed = true;
|
6
|
+
$form.find("input[name], select").each( function () {
|
7
|
+
if ($(this).val() == "") {
|
8
|
+
completed = false;
|
9
|
+
}
|
10
|
+
});
|
11
|
+
if (completed) {
|
12
|
+
$form.submit();
|
13
|
+
}
|
14
|
+
}
|
15
|
+
</script>
|
16
|
+
|
3
17
|
<div style="position: fixed; top: 0; left: 0; right: 0; background-color: whitesmoke; height: 60px; z-index: 1001;">
|
4
18
|
<div class="container">
|
5
19
|
<div class="row" style="padding-top: 13px;">
|
@@ -11,7 +25,7 @@
|
|
11
25
|
</div>
|
12
26
|
<div class="col-sm-3 text-right">
|
13
27
|
<%= link_to "Edit", edit_query_path(@query, variable_params), class: "btn btn-default" %>
|
14
|
-
<%= link_to "Fork", new_query_path(variable_params.merge(statement: @query.statement)), class: "btn btn-info" %>
|
28
|
+
<%= link_to "Fork", new_query_path(variable_params.merge(statement: @query.statement, data_source: @query.data_source)), class: "btn btn-info" %>
|
15
29
|
|
16
30
|
<% if !@error && @success %>
|
17
31
|
<%= button_to "Download", run_queries_path(statement: @statement, query_id: @query.id, format: "csv"), class: "btn btn-primary" %>
|
@@ -49,7 +63,7 @@
|
|
49
63
|
<% @bind_vars.each_with_index do |var, i| %>
|
50
64
|
<%= label_tag var, var %>
|
51
65
|
<% if (data = @smart_vars[var]) %>
|
52
|
-
<%= select_tag var, options_for_select([[nil, nil]] + data, selected: params[var]), style: "margin-right: 20px; width: 200px;" %>
|
66
|
+
<%= select_tag var, options_for_select([[nil, nil]] + data, selected: params[var]), style: "margin-right: 20px; width: 200px; display: none;" %>
|
53
67
|
<script>
|
54
68
|
$("#<%= var %>").selectize({
|
55
69
|
create: true
|
@@ -121,11 +135,13 @@
|
|
121
135
|
picker.setStartDate(moment.tz($("#start_time").val(), timeZone));
|
122
136
|
picker.setEndDate(moment.tz($("#end_time").val(), timeZone));
|
123
137
|
$("#reportrange").trigger('apply.daterangepicker', picker)
|
138
|
+
} else {
|
139
|
+
var picker = $("#reportrange").data('daterangepicker');
|
140
|
+
$("#reportrange").trigger('apply.daterangepicker', picker);
|
141
|
+
submitIfCompleted($("#start_time").closest("form"));
|
124
142
|
}
|
125
143
|
</script>
|
126
144
|
<% end %>
|
127
|
-
|
128
|
-
<input type="submit" class="btn btn-success" value="Run" style="vertical-align: top;" />
|
129
145
|
</form>
|
130
146
|
<% end %>
|
131
147
|
|
@@ -147,18 +163,6 @@
|
|
147
163
|
<script>
|
148
164
|
hljs.initHighlightingOnLoad();
|
149
165
|
|
150
|
-
function submitIfCompleted($form) {
|
151
|
-
var completed = true;
|
152
|
-
$form.find("input[name], select").each( function () {
|
153
|
-
if ($(this).val() == "") {
|
154
|
-
completed = false;
|
155
|
-
}
|
156
|
-
});
|
157
|
-
if (completed) {
|
158
|
-
$form.submit();
|
159
|
-
}
|
160
|
-
}
|
161
|
-
|
162
166
|
$(".form-inline input, .form-inline select").change( function () {
|
163
167
|
submitIfCompleted($(this).closest("form"));
|
164
168
|
});
|
data/config/routes.rb
CHANGED
@@ -1,6 +1,11 @@
|
|
1
1
|
Blazer::Engine.routes.draw do
|
2
2
|
resources :queries, except: [:index] do
|
3
3
|
post :run, on: :collection # err on the side of caution
|
4
|
+
get :tables, on: :collection
|
4
5
|
end
|
6
|
+
resources :checks, except: [:show] do
|
7
|
+
get :run, on: :member
|
8
|
+
end
|
9
|
+
resources :dashboards
|
5
10
|
root to: "queries#index"
|
6
11
|
end
|
data/lib/blazer.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
require "csv"
|
2
2
|
require "chartkick"
|
3
3
|
require "blazer/version"
|
4
|
+
require "blazer/data_source"
|
4
5
|
require "blazer/engine"
|
6
|
+
require "blazer/tasks"
|
5
7
|
|
6
8
|
module Blazer
|
7
9
|
class << self
|
@@ -9,13 +11,55 @@ module Blazer
|
|
9
11
|
attr_reader :time_zone
|
10
12
|
attr_accessor :user_name
|
11
13
|
attr_accessor :user_class
|
12
|
-
attr_accessor :
|
14
|
+
attr_accessor :from_email
|
13
15
|
end
|
14
16
|
self.audit = true
|
15
17
|
self.user_name = :name
|
16
|
-
self.timeout = 15
|
17
18
|
|
18
19
|
def self.time_zone=(time_zone)
|
19
20
|
@time_zone = time_zone.is_a?(ActiveSupport::TimeZone) ? time_zone : ActiveSupport::TimeZone[time_zone.to_s]
|
20
21
|
end
|
22
|
+
|
23
|
+
def self.settings
|
24
|
+
@settings ||= YAML.load(ERB.new(File.read(Rails.root.join("config", "blazer.yml"))).result)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.data_sources
|
28
|
+
@data_sources ||= begin
|
29
|
+
ds = Hash[
|
30
|
+
settings["data_sources"].map do |id, s|
|
31
|
+
[id, Blazer::DataSource.new(id, s)]
|
32
|
+
end
|
33
|
+
]
|
34
|
+
ds.default = ds.values.first
|
35
|
+
ds
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.run_checks
|
40
|
+
Blazer::Check.includes(:query).find_each do |check|
|
41
|
+
rows = nil
|
42
|
+
error = nil
|
43
|
+
tries = 0
|
44
|
+
# try 3 times on timeout errors
|
45
|
+
begin
|
46
|
+
rows, error = data_sources[check.query.data_source].run_statement(check.query.statement)
|
47
|
+
tries += 1
|
48
|
+
end while error && error.include?("canceling statement due to statement timeout") && tries < 3
|
49
|
+
check.update_state(rows, error)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.send_failing_checks
|
54
|
+
emails = {}
|
55
|
+
Blazer::Check.includes(:query).where(state: %w[failing error]).find_each do |check|
|
56
|
+
check.split_emails.each do |email|
|
57
|
+
(emails[email] ||= []) << check
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
emails.each do |email, checks|
|
62
|
+
Blazer::CheckMailer.failing_checks(email, checks).deliver_later
|
63
|
+
end
|
64
|
+
end
|
21
65
|
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Blazer
|
2
|
+
class DataSource
|
3
|
+
attr_reader :id, :settings, :connection_model
|
4
|
+
|
5
|
+
def initialize(id, settings)
|
6
|
+
@id = id
|
7
|
+
@settings = settings
|
8
|
+
@connection_model =
|
9
|
+
Class.new(Blazer::Connection) do
|
10
|
+
def self.name
|
11
|
+
"Blazer::Connection::#{object_id}"
|
12
|
+
end
|
13
|
+
establish_connection(settings["url"]) if settings["url"]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def name
|
18
|
+
settings["name"] || @id
|
19
|
+
end
|
20
|
+
|
21
|
+
def linked_columns
|
22
|
+
settings["linked_columns"] || {}
|
23
|
+
end
|
24
|
+
|
25
|
+
def smart_columns
|
26
|
+
settings["smart_columns"] || {}
|
27
|
+
end
|
28
|
+
|
29
|
+
def smart_variables
|
30
|
+
settings["smart_variables"] || {}
|
31
|
+
end
|
32
|
+
|
33
|
+
def timeout
|
34
|
+
settings["timeout"]
|
35
|
+
end
|
36
|
+
|
37
|
+
def run_statement(statement)
|
38
|
+
rows = []
|
39
|
+
error = nil
|
40
|
+
begin
|
41
|
+
connection_model.transaction do
|
42
|
+
connection_model.connection.execute("SET statement_timeout = #{timeout.to_i * 1000}") if timeout && postgresql?
|
43
|
+
result = connection_model.connection.select_all(statement)
|
44
|
+
result.each do |untyped_row|
|
45
|
+
row = {}
|
46
|
+
untyped_row.each do |k, v|
|
47
|
+
row[k] = result.column_types.empty? ? v : result.column_types[k].send(:type_cast, v)
|
48
|
+
end
|
49
|
+
rows << row
|
50
|
+
end
|
51
|
+
raise ActiveRecord::Rollback
|
52
|
+
end
|
53
|
+
rescue ActiveRecord::StatementInvalid => e
|
54
|
+
error = e.message.sub(/.+ERROR: /, "")
|
55
|
+
end
|
56
|
+
[rows, error]
|
57
|
+
end
|
58
|
+
|
59
|
+
def tables
|
60
|
+
default_schema = postgresql? ? "public" : connection_model.connection_config[:database]
|
61
|
+
schema = connection_model.connection_config[:schema] || default_schema
|
62
|
+
rows, error = run_statement(connection_model.send(:sanitize_sql_array, ["SELECT table_name, column_name, ordinal_position, data_type FROM information_schema.columns WHERE table_schema = ?", schema]))
|
63
|
+
Hash[rows.group_by { |r| r["table_name"] }.map { |t, f| [t, f.sort_by { |f| f["ordinal_position"] }.map { |f| f.slice("column_name", "data_type") }] }.sort_by { |t, _f| t }]
|
64
|
+
end
|
65
|
+
|
66
|
+
def postgresql?
|
67
|
+
["PostgreSQL", "Redshift"].include?(connection_model.connection.adapter_name)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/blazer/engine.rb
CHANGED
@@ -6,8 +6,12 @@ module Blazer
|
|
6
6
|
# use a proc instead of a string
|
7
7
|
app.config.assets.precompile << proc { |path| path =~ /\Ablazer\/application\.(js|css)\z/ }
|
8
8
|
|
9
|
-
Blazer.time_zone ||= Time.zone
|
10
|
-
Blazer.
|
9
|
+
Blazer.time_zone ||= Blazer.settings["time_zone"] || Time.zone
|
10
|
+
Blazer.audit = Blazer.settings.key?("audit") ? Blazer.settings["audit"] : true
|
11
|
+
Blazer.user_name = Blazer.settings["user_name"] if Blazer.settings["user_name"]
|
12
|
+
Blazer.from_email = Blazer.settings["from_email"] if Blazer.settings["from_email"]
|
13
|
+
|
14
|
+
Blazer.user_class ||= Blazer.settings["user_class"] || User rescue nil
|
11
15
|
Blazer::Query.belongs_to :creator, class_name: Blazer.user_class.to_s if Blazer.user_class
|
12
16
|
end
|
13
17
|
end
|