query_reviewer 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.md +118 -0
  3. data/Rakefile +24 -0
  4. data/lib/query_reviewer/array_extensions.rb +29 -0
  5. data/lib/query_reviewer/controller_extensions.rb +65 -0
  6. data/lib/query_reviewer/mysql_adapter_extensions.rb +90 -0
  7. data/lib/query_reviewer/mysql_analyzer.rb +62 -0
  8. data/lib/query_reviewer/query_warning.rb +17 -0
  9. data/lib/query_reviewer/rails.rb +33 -0
  10. data/lib/query_reviewer/sql_query.rb +130 -0
  11. data/lib/query_reviewer/sql_query_collection.rb +103 -0
  12. data/lib/query_reviewer/sql_sub_query.rb +45 -0
  13. data/lib/query_reviewer/tasks.rb +8 -0
  14. data/lib/query_reviewer/views/_box.html.erb +11 -0
  15. data/lib/query_reviewer/views/_box_ajax.js +34 -0
  16. data/lib/query_reviewer/views/_box_body.html.erb +73 -0
  17. data/lib/query_reviewer/views/_box_disabled.html.erb +2 -0
  18. data/lib/query_reviewer/views/_box_header.html.erb +1 -0
  19. data/lib/query_reviewer/views/_box_includes.html.erb +234 -0
  20. data/lib/query_reviewer/views/_explain.html.erb +30 -0
  21. data/lib/query_reviewer/views/_js_includes.html.erb +68 -0
  22. data/lib/query_reviewer/views/_js_includes_new.html.erb +68 -0
  23. data/lib/query_reviewer/views/_profile.html.erb +26 -0
  24. data/lib/query_reviewer/views/_query_sql.html.erb +8 -0
  25. data/lib/query_reviewer/views/_query_trace.html.erb +31 -0
  26. data/lib/query_reviewer/views/_query_with_warning.html.erb +54 -0
  27. data/lib/query_reviewer/views/_spectrum.html.erb +10 -0
  28. data/lib/query_reviewer/views/_warning_no_query.html.erb +8 -0
  29. data/lib/query_reviewer/views/query_review_box_helper.rb +98 -0
  30. data/lib/query_reviewer.rb +54 -0
  31. data/query_reviewer_defaults.yml +39 -0
  32. metadata +96 -0
@@ -0,0 +1,103 @@
1
+ module QueryReviewer
2
+ # a collection of SQL SELECT queries
3
+ class SqlQueryCollection
4
+ COMMANDS = %w(SELECT DELETE INSERT UPDATE)
5
+
6
+ attr_reader :query_hash
7
+ attr_accessor :overhead_time
8
+ def initialize(query_hash = {})
9
+ @query_hash = query_hash
10
+ @overhead_time = 0.0
11
+ end
12
+
13
+ def queries
14
+ query_hash.values
15
+ end
16
+
17
+ def total_duration
18
+ self.queries.collect(&:durations).flatten.sum
19
+ end
20
+
21
+ def query_count
22
+ queries.collect(&:count).sum
23
+ end
24
+
25
+ def analyze!
26
+ self.queries.collect(&:analyze!)
27
+
28
+ @warnings = []
29
+
30
+ crit_severity = 9# ((QueryReviewer::CONFIGURATION["critical_severity"] + 10)/2).to_i
31
+ warn_severity = QueryReviewer::CONFIGURATION["critical_severity"] - 1 # ((QueryReviewer::CONFIGURATION["warn_severity"] + QueryReviewer::CONFIGURATION["critical_severity"])/2).to_i
32
+
33
+ COMMANDS.each do |command|
34
+ count = count_of_command(command)
35
+ if count > QueryReviewer::CONFIGURATION["critical_#{command.downcase}_count"]
36
+ warn(:severity => crit_severity, :problem => "#{count} #{command} queries on this page", :description => "Too many #{command} queries can severely slow down a page")
37
+ elsif count > QueryReviewer::CONFIGURATION["warn_#{command.downcase}_count"]
38
+ warn(:severity => warn_severity, :problem => "#{count} #{command} queries on this page", :description => "Too many #{command} queries can slow down a page")
39
+ end
40
+ end
41
+ end
42
+
43
+ def find_or_create_sql_query(sql, cols, run_time, profile, command, affected_rows)
44
+ sanitized_sql = SqlQuery.sanitize_strings_and_numbers_from_sql(sql)
45
+ trace = SqlQuery.generate_full_trace(Kernel.caller)
46
+ key = [sanitized_sql, trace]
47
+ if query_hash[key]
48
+ query_hash[key].add(sql, run_time, profile)
49
+ else
50
+ query_hash[key] = SqlQuery.new(sql, cols, trace, run_time, profile, command, affected_rows, sanitized_sql)
51
+ end
52
+ end
53
+
54
+ def warn(options)
55
+ @warnings << QueryWarning.new(options)
56
+ end
57
+
58
+ def warnings
59
+ self.queries.collect(&:warnings).flatten.sort{|a,b| b.severity <=> a.severity}
60
+ end
61
+
62
+ def without_warnings
63
+ self.queries.reject{|q| q.has_warnings?}.sort{|a,b| b.duration <=> a.duration}
64
+ end
65
+
66
+ def collection_warnings
67
+ @warnings
68
+ end
69
+
70
+ def max_severity
71
+ warnings.empty? && collection_warnings.empty? ? 0 : [warnings.empty? ? 0 : warnings.collect(&:severity).flatten.max, collection_warnings.empty? ? 0 : collection_warnings.collect(&:severity).flatten.max].max
72
+ end
73
+
74
+ def only_of_command(command, only_no_warnings = false)
75
+ qs = only_no_warnings ? self.without_warnings : self.queries
76
+ qs.select{|q| q.command == command}
77
+ end
78
+
79
+ def count_of_command(command, only_no_warnings = false)
80
+ only_of_command(command, only_no_warnings).collect(&:durations).collect(&:size).sum
81
+ end
82
+
83
+ def total_severity
84
+ warnings.collect(&:severity).sum
85
+ end
86
+
87
+ def total_with_warnings
88
+ queries.select(&:has_warnings?).length
89
+ end
90
+
91
+ def total_without_warnings
92
+ queries.length - total_with_warnings
93
+ end
94
+
95
+ def percent_with_warnings
96
+ queries.empty? ? 0 : (100.0 * total_with_warnings / queries.length).to_i
97
+ end
98
+
99
+ def percent_without_warnings
100
+ queries.empty? ? 0 : (100.0 * total_without_warnings / queries.length).to_i
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,45 @@
1
+ module QueryReviewer
2
+ # a single part of an SQL SELECT query
3
+ class SqlSubQuery < OpenStruct
4
+ include MysqlAnalyzer
5
+
6
+ delegate :sql, :to => :parent
7
+ attr_reader :cols, :warnings, :parent
8
+ def initialize(parent, cols)
9
+ @parent = parent
10
+ @warnings = []
11
+ @cols = cols.inject({}) {|memo, obj| memo[obj[0].to_s.downcase] = obj[1].to_s.downcase; memo }
12
+ @cols["query_type"] = @cols.delete("type")
13
+ super(@cols)
14
+ end
15
+
16
+ def analyze!
17
+ @warnings = []
18
+ adapter_name = ActiveRecord::Base.connection.instance_variable_get("@config")[:adapter]
19
+ adapter_name = 'mysql' if adapter_name == 'mysql2'
20
+ method_name = "do_#{adapter_name}_analysis!"
21
+ self.send(method_name.to_sym)
22
+ end
23
+
24
+ def table
25
+ @table[:table]
26
+ end
27
+
28
+ private
29
+
30
+ def warn(options)
31
+ if (options[:field])
32
+ field = options.delete(:field)
33
+ val = self.send(field)
34
+ options[:problem] = ("#{field.to_s.titleize}: #{val.blank? ? "(blank)" : val}")
35
+ end
36
+ options[:query] = self
37
+ options[:table] = self.table
38
+ @warnings << QueryWarning.new(options)
39
+ end
40
+
41
+ def praise(options)
42
+ # no credit, only pain
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,8 @@
1
+ namespace :query_reviewer do
2
+ desc "Create a default config/query_reviewer.yml"
3
+ task :setup do
4
+ defaults_path = File.join(File.dirname(__FILE__), "../..", "query_reviewer_defaults.yml")
5
+ dest_path = File.join(Rails.root.to_s, "config", "query_reviewer.yml")
6
+ FileUtils.copy(defaults_path, dest_path)
7
+ end
8
+ end
@@ -0,0 +1,11 @@
1
+ <div id="query_review_parent" class="query_review_parent">
2
+ <div id="query_review_0" class="query_review_container">
3
+ <%= render :partial => "/box_includes"%>
4
+ <div class="query_review <%= parent_div_class %>" id = "query_review_header_0">
5
+ <%= render :partial => "/box_header" %>
6
+ </div>
7
+ <div class="query_review_details" id="query_review_details_0" style="display: none;">
8
+ <%= render :partial => enabled_by_cookie ? "/box_body" : "/box_disabled" %>
9
+ </div>
10
+ </div>
11
+ </div>
@@ -0,0 +1,34 @@
1
+ var i = 0;
2
+ var last = null;
3
+ for(i=0; i<10; i++) {
4
+ if(document.getElementById("query_review_"+i)) {
5
+ last = document.getElementById("query_review_"+i);
6
+ } else {
7
+ break;
8
+ }
9
+ }
10
+ if(i < 10) {
11
+ var container_div = document.createElement("div");
12
+ container_div.style.left = ""+(i * 30 + 1)+"px";
13
+ container_div.setAttribute("id", "query_review_"+i);
14
+ container_div.className = "query_review_container";
15
+ var ihtml = '<%= escape_javascript(render(:partial => "/box_includes"))%>';
16
+ ihtml += '<div class="query_review <%= parent_div_class %>" id = "query_review_header_'+i+'">';
17
+ ihtml += '<%= escape_javascript(render(:partial => "/box_header"))%>';
18
+ ihtml += '</div> ';
19
+ ihtml += '<div class="query_review_details" id="query_review_details_'+i+'" style="display: none;">';
20
+ ihtml += '<%= escape_javascript(render(:partial => enabled_by_cookie ? "/box_body" : "/box_disabled")) %>';
21
+ ihtml += '</div>';
22
+
23
+ container_div.innerHTML = ihtml;
24
+
25
+ var parent_div = document.getElementById("query_review_parent")
26
+ if(!parent_div) {
27
+ parent_div = document.createElement("div");
28
+ parent_div.setAttribute("id", "query_review_parent");
29
+ parent_div.className = "query_reivew_parent";
30
+ document.getElementById("body")[0].appendChild(parent_div);
31
+ }
32
+
33
+ parent_div.appendChild(container_div);
34
+ }
@@ -0,0 +1,73 @@
1
+ <p>Total queries: <span class="number"><%= @queries.query_count %></span>&nbsp;&nbsp;
2
+ <% if @total_time %>Total time: <span class="number" title="TOTAL TIME: <%= @total_time %>s QR_OVERHEAD: <%= @queries.overhead_time %>s">
3
+ <%= '%.3f' % (@total_time - @queries.overhead_time) %></span>s&nbsp;&nbsp;
4
+ <% end %>
5
+ MySQL Database Time: <span class="number"><%= '%.3f' % @queries.total_duration %></span>s</p>
6
+ <p class="indent">With warnings: <span class="number bad"><%= @queries.total_with_warnings %></span> (<%= @queries.percent_with_warnings %>%)</p>
7
+ <p class="indent">Without warnings: <span class="number good"><%= @queries.total_without_warnings %></span> (<%= @queries.percent_without_warnings %>%)</p>
8
+ <p>Type:
9
+ <% QueryReviewer::SqlQueryCollection::COMMANDS.each do |command| %>
10
+ <% next if @queries.count_of_command(command).zero? %>
11
+ <span class="number"><%= @queries.count_of_command(command) %></span> <%= command %>s&nbsp;&nbsp;
12
+ <% end %>
13
+ </p>
14
+ <% if warnings_no_query_sorted.length + queries_with_warnings_sorted.length > 0 %>
15
+ <div class="divider"></div>
16
+ <% if warnings_no_query_sorted_nonignored.length + queries_with_warnings_sorted_nonignored.length > 0 %>
17
+ <p class="title"><%= warnings_no_query_sorted_nonignored.length + queries_with_warnings_sorted_nonignored.length %> Errors:</p>
18
+ <ul>
19
+ <%= render :partial => "/warning_no_query", :collection => warnings_no_query_sorted_nonignored %>
20
+ <%= render :partial => "/query_with_warning", :collection => queries_with_warnings_sorted_nonignored %>
21
+ </ul>
22
+ <% end %>
23
+ <% if warnings_no_query_sorted_ignored.length + queries_with_warnings_sorted_ignored.length > 0 %>
24
+ <%= warnings_no_query_sorted_ignored.length + queries_with_warnings_sorted_ignored.length %> Warnings:
25
+ <ul id="query_review_ignored_warnings">
26
+ <%= render :partial => "/warning_no_query", :collection => warnings_no_query_sorted_ignored %>
27
+ <%= render :partial => "/query_with_warning", :collection => queries_with_warnings_sorted_ignored %>
28
+ </ul>
29
+ <% end %>
30
+ <% end %>
31
+ <div class="divider"></div>
32
+ <p class="title">Safe queries:</p>
33
+ <% if @queries.queries.empty? %>
34
+ No queries to display.
35
+ <% else %>
36
+ <% QueryReviewer::SqlQueryCollection::COMMANDS.reverse.each do |command| %>
37
+ <% next if @queries.count_of_command(command, true).zero? %>
38
+ <ul class="small">
39
+ <% @queries.only_of_command(command, true).each do |query| %>
40
+ <li>
41
+ <% if QueryReviewer::CONFIGURATION["production_data"] %>
42
+ <%= duration_with_color(query) %>s
43
+ <% end %>
44
+ <% if query.count > 1 %>
45
+ <b title="<%= query.count %> queries were executed with the same stack trace and similar SQL structure">
46
+ <%= query.count %> identical queries
47
+ </b>
48
+ <% end %>
49
+ <%= render :partial => "/query_sql", :locals=>{ :query_sql => query } %>
50
+ <% if query.select? %>
51
+ <a href="javascript: query_review_toggle('warning_<%= query.id %>_explain')" title="show/hide sql">EXPLN</a>
52
+ <% end %>
53
+ <% if QueryReviewer::CONFIGURATION["profiling"] && query.profile %>
54
+ <a href="javascript: query_review_toggle('warning_<%= query.id %>_profile')" title="show/hide profile">PROF</a>
55
+ <% end %>
56
+ <a href="javascript: query_review_toggle('warning_<%= query.id %>_trace')" title="show/hide stack trace">TRACE</a>
57
+ <div style="display: none" id="warning_<%= query.id %>_explain" class="indent small tbpadded">
58
+ <%= render :partial => "/explain", :locals => {:query => query} %>
59
+ </div>
60
+ <% if QueryReviewer::CONFIGURATION["profiling"] && query.profile %>
61
+ <div style="display: none" id="warning_<%= query.id %>_profile" class="indent small">
62
+ <%= render :partial => "/profile", :locals => {:query => query} %>
63
+ </div>
64
+ <% end %>
65
+ <div style="display: none" id="warning_<%= query.id %>_trace" class="indent small">
66
+ <%= render :partial => "/query_trace", :object => query.relevant_trace, :locals => {:query_id => query.id, :full_trace => query.full_trace} %>
67
+ </div>
68
+ </li>
69
+ <% end %>
70
+ </ul>
71
+ <% end %>
72
+ <% end %>
73
+ <p id="query_review_disable_link"><a href="javascript: eraseCookie('query_review_enabled'); query_review_hide('query_review_disable_link'); alert('Cookie successfully set.');">Disable analysis report</a> on next page load and from now on.</p>
@@ -0,0 +1,2 @@
1
+ <p>SQL analysis has been disabled for you. A cookie must be set to enable analysis. This generally slows down your browser, so it's only recommended for users analyzing SQL queries.</p>
2
+ <p id="query_review_enable_link"><b><a href="javascript: createCookie('query_review_enabled', '1'); query_review_hide('query_review_enable_link'); alert('Cookie successfully set.');">Enabled it</a> on next page load and from now on.</b></p>
@@ -0,0 +1 @@
1
+ <a href="#" onclick="query_review_toggle(this.parentNode.nextSibling.nextSibling.id); return false;">SQL <%= parent_div_status %></a>
@@ -0,0 +1,234 @@
1
+ <style>
2
+ div#query_review_parent {
3
+ position: absolute;
4
+ top: 1px;
5
+ left: 1px;
6
+ z-index: 100000;
7
+ }
8
+
9
+ div.query_review_container {
10
+ float: left;
11
+ }
12
+
13
+ div.query_review {
14
+ height: 18px;
15
+ margin: 1px;
16
+ opacity: 0.75;
17
+ padding-left: 3px;
18
+ padding-right: 3px;
19
+ border: 1px solid black;
20
+ font-size: 12px;
21
+ font-weight: bold;
22
+ text-align: left;
23
+ }
24
+
25
+ div.query_review.sql_ok {
26
+ background-color: #090;
27
+ color: #020;
28
+ }
29
+
30
+ div.query_review.sql_warning {
31
+ background-color: #FAE100;
32
+ color: #F05000;
33
+ }
34
+
35
+ div.query_review.sql_critical {
36
+ background-color: #F99;
37
+ color: #A00;
38
+ }
39
+
40
+ div.query_review.sql_disabled {
41
+ background-color: #BBB;
42
+ color: #555;
43
+ }
44
+
45
+ div.query_review a {
46
+ color: inherit;
47
+ text-decoration: none;
48
+ }
49
+
50
+ div.query_review_details {
51
+ width: 900px;
52
+ height: 600px;
53
+ opacity: 0.9;
54
+ margin: 1px;
55
+ padding: 2px;
56
+ border: 1px solid black;
57
+ font-size: 12px;
58
+ overflow: auto;
59
+ background-color: #DDD;
60
+ text-align: left;
61
+ text-indent: 0px;
62
+ color: black;
63
+ }
64
+
65
+ div.query_review_details p {
66
+ margin: 2px 0 2px 0;
67
+ padding: 0px;
68
+ }
69
+
70
+ div.query_review_details a {
71
+ color: #29ABE2;
72
+ }
73
+
74
+ div.query_review_details ul {
75
+ list-style-type: circle;
76
+ padding-left: 15px;
77
+ }
78
+
79
+ div.query_review_details li {
80
+ clear: both;
81
+ }
82
+
83
+ div.query_review_details code {
84
+ white-space: normal;
85
+ line-height: 120%;
86
+ }
87
+
88
+ div.query_review_details table.explain th {
89
+ cell-padding: 5px;
90
+ }
91
+
92
+ div.query_review_details .title {
93
+ font-weight: bold;
94
+ }
95
+
96
+ div.query_review_details .indent {
97
+ padding-left: 10px;
98
+ }
99
+
100
+ div.query_review_details .number {
101
+ font-weight: bold;
102
+ }
103
+
104
+ div.query_review_details .bad {
105
+ color: #900;
106
+ }
107
+
108
+ div.query_review_details .good {
109
+ color: #090;
110
+ }
111
+
112
+ div.query_review_details div.divider {
113
+ width: 504px;
114
+ position: relative;
115
+ left: -2px;
116
+ height: 1px;
117
+ border-top: 1px dashed black;
118
+ margin: 2px 0 2px 0;
119
+ }
120
+
121
+ div.query_review_details .small {
122
+ font-size: 10px;
123
+ }
124
+
125
+ div.query_review_details .tbpadded {
126
+ padding-top: 3px;
127
+ padding-bottom: 3px;
128
+ }
129
+
130
+ div.query_review_details .trace {
131
+ background-color: #0C1021;
132
+ color: #7F908A;
133
+ padding: 5px;
134
+ margin-right: 30px;
135
+ }
136
+
137
+ div.query_review_details .trace .bold {
138
+ color: white;
139
+ }
140
+
141
+ div.query_review_details div.spectrum_container {
142
+ margin-top: 4px;
143
+ width: 35px;
144
+ display: block;
145
+ float: left;
146
+ position: relative;
147
+ height: 14px;
148
+ margin-right: 5px;
149
+ }
150
+
151
+ div.query_review_details div.spectrum_elem {
152
+ width: 1px;
153
+ display: block;
154
+ float: left;
155
+ height: 10px;
156
+ }
157
+ div.query_review_details div.spectrum_pointer {
158
+ width: 2px;
159
+ display: block;
160
+ float: left;
161
+ position: absolute;
162
+ height: 14px;
163
+ background-color: black;
164
+ top: -2px;
165
+ }
166
+
167
+ div.query_review_details table {
168
+ background-color: white;
169
+ border: 1px solid gray;
170
+ padding: 0px;
171
+ margin: 0px;
172
+ }
173
+
174
+ div.query_review_details tbody {
175
+ border: 0px;
176
+ padding: 0px;
177
+ margin: 0px;
178
+ }
179
+
180
+ div.query_review_details thead {
181
+ border-bottom: 1px solid gray;
182
+ padding: 0px;
183
+ margin: 0px;
184
+ }
185
+
186
+
187
+ div.query_review_details tr {
188
+ border: 0px;
189
+ padding: 0px;
190
+ margin: 0px;
191
+ }
192
+
193
+ div.query_review_details td {
194
+ border: 1px solid gray;
195
+ padding: 3px;
196
+ margin: 0px;
197
+ overflow: hidden;
198
+ }
199
+
200
+ div.query_review_details th {
201
+ border: 1px solid gray;
202
+ padding: 3px;
203
+ margin: 0px;
204
+ }
205
+
206
+ </style>
207
+
208
+ <style>
209
+ .sql {
210
+ color: black;
211
+ }
212
+ .sql .String {
213
+ color: #009933;
214
+ }
215
+ .sql .Keyword {
216
+ color: #0000FF;
217
+ }
218
+ .sql .Constant {
219
+ color: #6782D3;
220
+ }
221
+ .sql .Number {
222
+ color: #0066FF;
223
+ }
224
+ .sql .Comment {
225
+ color: #0066FF;
226
+ font-style: italic;
227
+ }
228
+ </style>
229
+
230
+ <% if defined?(Rails::Railtie) %>
231
+ <%= render :partial => "/js_includes_new" %>
232
+ <% else %>
233
+ <%= render :partial => "/js_includes" %>
234
+ <% end %>
@@ -0,0 +1,30 @@
1
+ <table class="explain">
2
+ <thead>
3
+ <tr>
4
+ <th>table</th>
5
+ <th>select_type</th>
6
+ <th>type</th>
7
+ <th>extra</th>
8
+ <th>possible_keys</th>
9
+ <th>key</th>
10
+ <th>key length</th>
11
+ <th>ref</th>
12
+ <th>rows</th>
13
+ </tr>
14
+ </thead>
15
+ <tbody>
16
+ <% query.subqueries.each do |subquery| %>
17
+ <tr>
18
+ <td title="<%= h subquery.table %>"><%= h subquery.table %></td>
19
+ <td title="<%= h subquery.select_type %>"><%= h subquery.select_type %></td>
20
+ <td title="<%= h subquery.query_type %>"><%= h subquery.query_type %></td>
21
+ <td title="<%= h subquery.extra %>"><%= h subquery.extra %></td>
22
+ <td title="<%= h subquery.possible_keys %>"><%= h subquery.possible_keys %></td>
23
+ <td title="<%= h subquery.key %>"><%= h subquery.key %></td>
24
+ <td title="<%= h subquery.key_len %>"><%= h subquery.key_len %></td>
25
+ <td title="<%= h subquery.ref %>"><%= h subquery.ref %></td>
26
+ <td title="<%= h subquery.rows %>"><%= h subquery.rows %></td>
27
+ </tr>
28
+ <% end %>
29
+ </tbody>
30
+ </table>
@@ -0,0 +1,68 @@
1
+ <% javascript_tag do %>
2
+ //Super lame show/hide functions so we don't need any 3rd party JS libs
3
+ function query_review_show(id) {
4
+ document.getElementById(id).style.display = "block";
5
+ }
6
+
7
+ function query_review_hide(id) {
8
+ document.getElementById(id).style.display = "none";
9
+ }
10
+ function query_review_toggle(id) {
11
+ if(document.getElementById(id).style.display == "none") {
12
+ document.getElementById(id).style.display = "block";
13
+ } else {
14
+ document.getElementById(id).style.display = "none";
15
+ }
16
+ }
17
+ function createCookie(name,value,days) {
18
+ if (days) {
19
+ var date = new Date();
20
+ date.setTime(date.getTime()+(days*24*60*60*1000));
21
+ var expires = "; expires="+date.toGMTString();
22
+ }
23
+ else var expires = "";
24
+ document.cookie = name+"="+value+expires+"; path=/";
25
+ }
26
+
27
+ function readCookie(name) {
28
+ var nameEQ = name + "=";
29
+ var ca = document.cookie.split(';');
30
+ for(var i=0;i < ca.length;i++) {
31
+ var c = ca[i];
32
+ while (c.charAt(0)==' ') c = c.substring(1,c.length);
33
+ if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
34
+ }
35
+ return null;
36
+ }
37
+
38
+ function eraseCookie(name) {
39
+ createCookie(name,"",-1);
40
+ }
41
+
42
+ function ignore_list() {
43
+ var ignore_list = readCookie("query_review_ignore_list")
44
+ if (!ignore_list)
45
+ ignore_list = []
46
+ else
47
+ ignore_list = ignore_list.split(",")
48
+ return ignore_list
49
+ }
50
+
51
+ function add_ignore_hash(h){
52
+ var list = ignore_list();
53
+ list[list.length] = h
54
+ createCookie("query_review_ignore_list", list.join(","))
55
+ }
56
+
57
+ function remove_ignore_hash(h) {
58
+ var list = ignore_list();
59
+ var new_list = []
60
+ for(var i=0; i<list.length; i++)
61
+ {
62
+ if(list[i].toString() != h.toString()) {
63
+ new_list[new_list.length] = list[i]
64
+ }
65
+ }
66
+ createCookie("query_review_ignore_list", new_list.join(","))
67
+ }
68
+ <% end %>
@@ -0,0 +1,68 @@
1
+ <%= javascript_tag do %>
2
+ //Super lame show/hide functions so we don't need any 3rd party JS libs
3
+ function query_review_show(id) {
4
+ document.getElementById(id).style.display = "block";
5
+ }
6
+
7
+ function query_review_hide(id) {
8
+ document.getElementById(id).style.display = "none";
9
+ }
10
+ function query_review_toggle(id) {
11
+ if(document.getElementById(id).style.display == "none") {
12
+ document.getElementById(id).style.display = "block";
13
+ } else {
14
+ document.getElementById(id).style.display = "none";
15
+ }
16
+ }
17
+ function createCookie(name,value,days) {
18
+ if (days) {
19
+ var date = new Date();
20
+ date.setTime(date.getTime()+(days*24*60*60*1000));
21
+ var expires = "; expires="+date.toGMTString();
22
+ }
23
+ else var expires = "";
24
+ document.cookie = name+"="+value+expires+"; path=/";
25
+ }
26
+
27
+ function readCookie(name) {
28
+ var nameEQ = name + "=";
29
+ var ca = document.cookie.split(';');
30
+ for(var i=0;i < ca.length;i++) {
31
+ var c = ca[i];
32
+ while (c.charAt(0)==' ') c = c.substring(1,c.length);
33
+ if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
34
+ }
35
+ return null;
36
+ }
37
+
38
+ function eraseCookie(name) {
39
+ createCookie(name,"",-1);
40
+ }
41
+
42
+ function ignore_list() {
43
+ var ignore_list = readCookie("query_review_ignore_list")
44
+ if (!ignore_list)
45
+ ignore_list = []
46
+ else
47
+ ignore_list = ignore_list.split(",")
48
+ return ignore_list
49
+ }
50
+
51
+ function add_ignore_hash(h){
52
+ var list = ignore_list();
53
+ list[list.length] = h
54
+ createCookie("query_review_ignore_list", list.join(","))
55
+ }
56
+
57
+ function remove_ignore_hash(h) {
58
+ var list = ignore_list();
59
+ var new_list = []
60
+ for(var i=0; i<list.length; i++)
61
+ {
62
+ if(list[i].toString() != h.toString()) {
63
+ new_list[new_list.length] = list[i]
64
+ }
65
+ }
66
+ createCookie("query_review_ignore_list", new_list.join(","))
67
+ }
68
+ <% end %>