rails3-footnotes 4.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,187 @@
1
+ require "#{File.dirname(__FILE__)}/abstract_note"
2
+
3
+ module Footnotes
4
+ module Notes
5
+ class QueriesNote < AbstractNote
6
+ @@alert_db_time = 0.16
7
+ @@alert_sql_number = 8
8
+ @@sql = []
9
+ @@include_when_new_relic_installed = false
10
+ @@loaded = false
11
+
12
+ cattr_accessor :sql, :alert_db_time, :alert_sql_number, :alert_explain, :loaded, :sql_explain, :instance_writter => false
13
+ cattr_reader :include_when_new_relic_installed
14
+
15
+ def self.include_when_new_relic_installed=(include_me)
16
+ @@include_when_new_relic_installed = include_me
17
+ load if include_me
18
+ end
19
+
20
+ def self.start!(controller)
21
+ @@sql = []
22
+ end
23
+
24
+ def self.to_sym
25
+ :queries
26
+ end
27
+
28
+ def title
29
+ db_time = @@sql.inject(0){|sum, item| sum += item.time }
30
+ query_color = generate_red_color(@@sql.length, alert_sql_number)
31
+ db_color = generate_red_color(db_time, alert_db_time)
32
+
33
+ <<-TITLE
34
+ <span style="background-color:#{query_color}">Queries (#{@@sql.length})</span>
35
+ <span style="background-color:#{db_color}">DB (#{"%.6f" % db_time}s)</span>
36
+ TITLE
37
+ end
38
+
39
+ def stylesheet
40
+ <<-STYLESHEET
41
+ #queries_debug_info table td, #queries_debug_info table th{border:1px solid #A00; padding:0 3px; text-align:center;}
42
+ #queries_debug_info table thead, #queries_debug_info table tbody {color:#A00;}
43
+ #queries_debug_info p {background-color:#F3F3FF; border:1px solid #CCC; margin:12px; padding:4px 6px;}
44
+ #queries_debug_info a:hover {text-decoration:underline;}
45
+ STYLESHEET
46
+ end
47
+
48
+ def content
49
+ html = ''
50
+
51
+ @@sql.each_with_index do |item, i|
52
+ sql_links = []
53
+ sql_links << "<a href=\"javascript:Footnotes.toggle('qtable_#{i}')\" style=\"color:#A00;\">explain</a>" if item.explain
54
+ sql_links << "<a href=\"javascript:Footnotes.toggle('qtrace_#{i}')\" style=\"color:#00A;\">trace</a>" if item.trace
55
+
56
+ html << <<-HTML
57
+ <b id="qtitle_#{i}">#{escape(item.type.to_s.upcase)}</b> (#{sql_links.join(' | ')})<br />
58
+ #{print_name_and_time(item.name, item.time)}<br />
59
+ <span id="explain_#{i}">#{print_query(item.query)}</span><br />
60
+ #{print_explain(i, item.explain) if item.explain}
61
+ <p id="qtrace_#{i}" style="display:none;">#{parse_trace(item.trace) if item.trace}</p><br />
62
+ HTML
63
+ end
64
+
65
+ return html
66
+ end
67
+
68
+ def self.load
69
+ #only include when NewRelic is installed if configured to do so
70
+ if !loaded and included? and defined?(ActiveRecord)
71
+ ActiveRecord::ConnectionAdapters::AbstractAdapter.send :include, Footnotes::Extensions::AbstractAdapter
72
+ ActiveRecord::ConnectionAdapters.local_constants.each do |adapter|
73
+ next unless adapter =~ /.*[^Abstract]Adapter$/
74
+ next if adapter =~ /(SQLite|Salesforce)Adapter$/
75
+ eval("ActiveRecord::ConnectionAdapters::#{adapter}").send :include, Footnotes::Extensions::QueryAnalyzer
76
+ self.loaded = true
77
+ end
78
+ end
79
+ end
80
+
81
+ protected
82
+ def parse_explain(results)
83
+ table = []
84
+ table << results.fetch_fields.map(&:name)
85
+ results.each do |row|
86
+ table << row
87
+ end
88
+ table
89
+ end
90
+
91
+ def parse_trace(trace)
92
+ trace.map do |t|
93
+ s = t.split(':')
94
+ %[<a href="#{escape(Footnotes::Filter.prefix("#{Rails.root.to_s}/#{s[0]}", s[1].to_i, 1))}">#{escape(t)}</a><br />]
95
+ end.join
96
+ end
97
+
98
+ def print_name_and_time(name, time)
99
+ "<span style='background-color:#{generate_red_color(time, alert_ratio)}'>#{escape(name || 'SQL')} (#{sprintf('%f', time)}s)</span>"
100
+ end
101
+
102
+ def print_query(query)
103
+ escape(query.to_s.gsub(/(\s)+/, ' ').gsub('`', ''))
104
+ end
105
+
106
+ def print_explain(i, explain)
107
+ mount_table(parse_explain(explain), :id => "qtable_#{i}", :style => 'margin:10px;display:none;', :summary => "Debug information for #{title}")
108
+ end
109
+
110
+ def generate_red_color(value, alert)
111
+ c = ((value.to_f/alert).to_i - 1) * 16
112
+ c = 0 if c < 0
113
+ c = 15 if c > 15
114
+
115
+ c = (15-c).to_s(16)
116
+ "#ff#{c*4}"
117
+ end
118
+
119
+ def alert_ratio
120
+ alert_db_time / alert_sql_number
121
+ end
122
+
123
+ end
124
+ end
125
+
126
+ module Extensions
127
+ class Sql
128
+ attr_accessor :type, :name, :time, :query, :explain, :trace
129
+
130
+ def initialize(type, name, time, query, explain)
131
+ @type = type
132
+ @name = name
133
+ @time = time
134
+ @query = query
135
+ @explain = explain
136
+
137
+ # Strip, select those ones from app and reject first two, because they
138
+ # are from the plugin
139
+ @trace = Kernel.caller.collect(&:strip).select{|i| i.gsub!(/^#{Rails.root}\//im, '') }[2..-1]
140
+ end
141
+ end
142
+
143
+ module QueryAnalyzer
144
+ def self.included(base)
145
+ base.class_eval do
146
+ alias_method_chain :execute, :analyzer
147
+ end
148
+ end
149
+
150
+ def execute_with_analyzer(query, name = nil)
151
+ query_results = nil
152
+ time = Benchmark.realtime { query_results = execute_without_analyzer(query, name) }
153
+
154
+ if query =~ /^(select|create|update|delete)\b/i
155
+ type = $&.downcase.to_sym
156
+ explain = nil
157
+
158
+ if adapter_name == 'MySQL' && type == :select && Footnotes::Notes::QueriesNote.sql_explain
159
+ log_silence do
160
+ explain = execute_without_analyzer("EXPLAIN #{query}", name)
161
+ end
162
+ end
163
+ Footnotes::Notes::QueriesNote.sql << Footnotes::Extensions::Sql.new(type, name, time, query, explain)
164
+ end
165
+
166
+ query_results
167
+ end
168
+ end
169
+
170
+ module AbstractAdapter
171
+ def log_silence
172
+ result = nil
173
+ if @logger
174
+ @logger.silence do
175
+ result = yield
176
+ end
177
+ else
178
+ result = yield
179
+ end
180
+ result
181
+ end
182
+ end
183
+
184
+ end
185
+ end
186
+
187
+ Footnotes::Notes::QueriesNote.load
@@ -0,0 +1,63 @@
1
+ require "#{File.dirname(__FILE__)}/abstract_note"
2
+
3
+ module Footnotes
4
+ module Notes
5
+ class RoutesNote < AbstractNote
6
+ def initialize(controller)
7
+ @controller = controller
8
+ @parsed_routes = parse_routes
9
+ end
10
+
11
+ def legend
12
+ "Routes for #{@controller.class.to_s}"
13
+ end
14
+
15
+ def content
16
+ mount_table(@parsed_routes.unshift([:path, :name, :options, :requirements]), :summary => "Debug information for #{title}")
17
+ end
18
+
19
+ protected
20
+ def parse_routes
21
+ routes_with_name = Rails.application.routes.named_routes.to_a.flatten
22
+
23
+ return Rails.application.routes.filtered_routes(:controller => @controller.controller_path).collect do |route|
24
+ # Catch routes name if exists
25
+ i = routes_with_name.index(route)
26
+ name = i ? routes_with_name[i-1].to_s : ''
27
+
28
+ # Catch segments requirements
29
+ req = {}
30
+ if Rails.version < '3.0'
31
+ route.segments.each do |segment|
32
+ next unless segment.is_a?(ActionController::Routing::DynamicSegment) && segment.regexp
33
+ req[segment.key.to_sym] = segment.regexp
34
+ end
35
+ [escape(name), route.segments.join, route.requirements.reject{|key,value| key == :controller}.inspect, req.inspect]
36
+ else
37
+ req = route.conditions
38
+ [escape(name), route.conditions.keys.join, route.requirements.reject{|key,value| key == :controller}.inspect, req.inspect]
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ module Extensions
46
+ module Routes
47
+ # Filter routes according to the filter sent
48
+ #
49
+ def filtered_routes(filter = {})
50
+ return [] unless filter.is_a?(Hash)
51
+ return routes.reject do |r|
52
+ filter_diff = filter.diff(r.requirements)
53
+ route_diff = r.requirements.diff(filter)
54
+ (filter_diff == filter) || (filter_diff != route_diff)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ if Footnotes::Notes::RoutesNote.included?
62
+ ActionController::Routing::RouteSet.send :include, Footnotes::Extensions::Routes
63
+ end
@@ -0,0 +1,30 @@
1
+ require "#{File.dirname(__FILE__)}/abstract_note"
2
+
3
+ if defined?(NewRelic)
4
+ module Footnotes
5
+ module Notes
6
+ class RpmNote < AbstractNote
7
+ def initialize(controller)
8
+ @rpm_id=NewRelic::Agent.instance.transaction_sampler.current_sample_id
9
+ end
10
+
11
+ def row
12
+ :edit
13
+ end
14
+
15
+ def link
16
+ #{:controller => 'newrelic', :action => 'show_sample_detail', :id => @rpm_id}
17
+ "/newrelic/show_sample_detail/#{@rpm_id}" if @rpm_id
18
+ end
19
+
20
+ def valid?
21
+ if defined?(NewRelic::Control)
22
+ !NewRelic::Control.instance['skip_developer_route']
23
+ else
24
+ !NewRelic::Config.instance['skip_developer_route']
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,29 @@
1
+ require "#{File.dirname(__FILE__)}/abstract_note"
2
+
3
+ module Footnotes
4
+ module Notes
5
+ class SessionNote < AbstractNote
6
+ def initialize(controller)
7
+ session = controller.session
8
+ if session
9
+ if session.respond_to? :to_hash
10
+ # rails >= 2.3
11
+ session = session.to_hash
12
+ else
13
+ #rails < 2.3
14
+ session = session.data
15
+ end
16
+ end
17
+ @session = (session || {}).symbolize_keys
18
+ end
19
+
20
+ def title
21
+ "Session (#{@session.length})"
22
+ end
23
+
24
+ def content
25
+ mount_table_for_hash(@session, :summary => "Debug information for #{title}")
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,16 @@
1
+ require "#{File.dirname(__FILE__)}/files_note"
2
+
3
+ module Footnotes
4
+ module Notes
5
+ class StylesheetsNote < FilesNote
6
+ def title
7
+ "Stylesheets (#{@files.length})"
8
+ end
9
+
10
+ protected
11
+ def scan_text(text)
12
+ text.scan(/<link[^>]+href\s*=\s*['"]([^>?'"]+\.css)/im).flatten
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,35 @@
1
+ require "#{File.dirname(__FILE__)}/abstract_note"
2
+
3
+ module Footnotes
4
+ module Notes
5
+ class ViewNote < AbstractNote
6
+ def initialize(controller)
7
+ @controller = controller
8
+ @template = controller.instance_variable_get(:@template)
9
+ end
10
+
11
+ def row
12
+ :edit
13
+ end
14
+
15
+ def link
16
+ escape(Footnotes::Filter.prefix(filename, 1, 1))
17
+ end
18
+
19
+ def valid?
20
+ prefix? && first_render?
21
+ end
22
+
23
+ protected
24
+
25
+ def first_render?
26
+ @template.instance_variable_get(:@_first_render)
27
+ end
28
+
29
+ def filename
30
+ @filename ||= @template.instance_variable_get(:@_first_render).filename
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,22 @@
1
+ module Footnotes
2
+
3
+ # The footnotes are applied by default to all actions. You can change this
4
+ # behavior commenting the after_filter line below and putting it in Your
5
+ # application. Then you can cherrypick in which actions it will appear.
6
+ #
7
+ module RailsFootnotesExtension
8
+ def self.included(base)
9
+ base.prepend_before_filter Footnotes::BeforeFilter
10
+ base.after_filter Footnotes::AfterFilter
11
+ end
12
+ end
13
+
14
+ def self.run!
15
+ require 'rails-footnotes/footnotes'
16
+ require 'rails-footnotes/backtracer'
17
+
18
+ Dir[File.join(File.dirname(__FILE__), 'rails-footnotes', 'notes', '*.rb')].each { |note| require note }
19
+
20
+ ActionController::Base.send(:include, RailsFootnotesExtension)
21
+ end
22
+ end
@@ -0,0 +1,207 @@
1
+ require 'test_helper'
2
+
3
+ require 'action_controller'
4
+ require 'action_controller/test_case'
5
+
6
+ class FootnotesController < ActionController::Base; attr_accessor :template, :performed_render; end
7
+
8
+ module Footnotes::Notes
9
+ class TestNote < AbstractNote
10
+ def self.to_sym; :test; end
11
+ def valid?; true; end
12
+ end
13
+ end
14
+
15
+ class FootnotesTest < Test::Unit::TestCase
16
+ def setup
17
+ @controller = FootnotesController.new
18
+ @controller.template = Object.new
19
+ @controller.request = ActionController::TestRequest.new
20
+ @controller.response = ActionController::TestResponse.new
21
+ @controller.response.body = $html.dup
22
+ @controller.params = {}
23
+
24
+ Footnotes::Filter.notes = [ :test ]
25
+ Footnotes::Filter.multiple_notes = false
26
+ @footnotes = Footnotes::Filter.new(@controller)
27
+ end
28
+
29
+ def test_footnotes_controller
30
+ index = @controller.response.body.index(/This is the HTML page/)
31
+ assert_equal 334, index
32
+ end
33
+
34
+ def test_foonotes_included
35
+ footnotes_perform!
36
+ assert_not_equal $html, @controller.response.body
37
+ end
38
+
39
+ def test_footnotes_not_included_when_request_is_xhr
40
+ @controller.request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
41
+ @controller.request.env['HTTP_ACCEPT'] = 'text/javascript, text/html, application/xml, text/xml, */*'
42
+
43
+ footnotes_perform!
44
+ assert_equal $html, @controller.response.body
45
+ end
46
+
47
+ def test_footnotes_not_included_when_content_type_is_javascript
48
+ @controller.response.headers['Content-Type'] = 'text/javascript'
49
+
50
+ footnotes_perform!
51
+ assert_equal $html, @controller.response.body
52
+ end
53
+
54
+ def test_footnotes_included_when_content_type_is_html
55
+ @controller.response.headers['Content-Type'] = 'text/html'
56
+
57
+ footnotes_perform!
58
+ assert_not_equal $html, @controller.response.body
59
+ end
60
+
61
+ def test_footnotes_included_when_content_type_is_nil
62
+ footnotes_perform!
63
+ assert_not_equal $html, @controller.response.body
64
+ end
65
+
66
+ def test_not_included_when_body_is_not_a_string
67
+ @controller.response.body = Proc.new{ Time.now }
68
+ assert_nothing_raised do
69
+ footnotes_perform!
70
+ end
71
+ end
72
+
73
+ def test_footnotes_prefix
74
+ assert_equal 'txmt://open?url=file://%s&amp;line=%d&amp;column=%d', Footnotes::Filter.prefix
75
+ assert_equal 'txmt://open?url=file://file&amp;line=0&amp;column=0', Footnotes::Filter.prefix('file', 0, 0)
76
+ assert_equal 'txmt://open?url=file://file&amp;line=10&amp;column=10', Footnotes::Filter.prefix('file', 10, 10)
77
+ assert_equal 'txmt://open?url=file://file&amp;line=10&amp;column=10', Footnotes::Filter.prefix('file', 10, 10, 10)
78
+ assert_equal 'txmt://open?url=file://file&amp;line=10&amp;column=10', Footnotes::Filter.prefix('file', '10', '10')
79
+ end
80
+
81
+ def test_notes_are_initialized
82
+ footnotes_perform!
83
+ test_note = @footnotes.instance_variable_get('@notes').first
84
+ assert_equal 'Footnotes::Notes::TestNote', test_note.class.name
85
+ assert_equal :test, test_note.to_sym
86
+ end
87
+
88
+ def test_notes_links
89
+ note = Footnotes::Notes::TestNote.new
90
+ note.expects(:row).times(2)
91
+ @footnotes.instance_variable_set(:@notes, [note])
92
+ footnotes_perform!
93
+ end
94
+
95
+ def test_notes_fieldset
96
+ note = Footnotes::Notes::TestNote.new
97
+ note.expects(:has_fieldset?).times(3)
98
+ @footnotes.instance_variable_set(:@notes, [note])
99
+ footnotes_perform!
100
+ end
101
+
102
+ def test_multiple_notes
103
+ Footnotes::Filter.multiple_notes = true
104
+ note = Footnotes::Notes::TestNote.new
105
+ note.expects(:has_fieldset?).times(2)
106
+ @footnotes.instance_variable_set(:@notes, [note])
107
+ footnotes_perform!
108
+ end
109
+
110
+ def test_notes_are_reset
111
+ note = Footnotes::Notes::TestNote.new
112
+ note.class.expects(:close!)
113
+ @footnotes.instance_variable_set(:@notes, [note])
114
+ @footnotes.send(:close!, @controller)
115
+ end
116
+
117
+ def test_links_helper
118
+ note = Footnotes::Notes::TestNote.new
119
+ assert_equal '<a href="#" onclick="">Test</a>', @footnotes.send(:link_helper, note)
120
+
121
+ note.expects(:link).times(1).returns(:link)
122
+ assert_equal '<a href="link" onclick="">Test</a>', @footnotes.send(:link_helper, note)
123
+ end
124
+
125
+ def test_links_helper_has_fieldset?
126
+ note = Footnotes::Notes::TestNote.new
127
+ note.expects(:has_fieldset?).times(1).returns(true)
128
+ assert_equal '<a href="#" onclick="Footnotes.hideAllAndToggle(\'test_debug_info\');return false;">Test</a>', @footnotes.send(:link_helper, note)
129
+ end
130
+
131
+ def test_links_helper_onclick
132
+ note = Footnotes::Notes::TestNote.new
133
+ note.expects(:onclick).times(2).returns(:onclick)
134
+ assert_equal '<a href="#" onclick="onclick">Test</a>', @footnotes.send(:link_helper, note)
135
+
136
+ note.expects(:has_fieldset?).times(1).returns(true)
137
+ assert_equal '<a href="#" onclick="onclick">Test</a>', @footnotes.send(:link_helper, note)
138
+ end
139
+
140
+ def test_insert_style
141
+ @controller.response.body = "<head></head><split><body></body>"
142
+ @footnotes = Footnotes::Filter.new(@controller)
143
+ footnotes_perform!
144
+ assert @controller.response.body.split('<split>').first.include?('<!-- Footnotes Style -->')
145
+ end
146
+
147
+ def test_insert_footnotes_inside_body
148
+ @controller.response.body = "<head></head><split><body></body>"
149
+ @footnotes = Footnotes::Filter.new(@controller)
150
+ footnotes_perform!
151
+ assert @controller.response.body.split('<split>').last.include?('<!-- End Footnotes -->')
152
+ end
153
+
154
+ def test_insert_footnotes_inside_holder
155
+ @controller.response.body = "<head></head><split><div id='footnotes_holder'></div>"
156
+ @footnotes = Footnotes::Filter.new(@controller)
157
+ footnotes_perform!
158
+ assert @controller.response.body.split('<split>').last.include?('<!-- End Footnotes -->')
159
+ end
160
+
161
+ def test_insert_text
162
+ @footnotes.send(:insert_text, :after, /<head>/, "Graffiti")
163
+ after = " <head>Graffiti"
164
+ assert_equal after, @controller.response.body.split("\n")[2]
165
+
166
+ @footnotes.send(:insert_text, :before, /<\/body>/, "Notes")
167
+ after = " Notes</body>"
168
+ assert_equal after, @controller.response.body.split("\n")[12]
169
+ end
170
+
171
+ protected
172
+ # First we make sure that footnotes will perform (long life to mocha!)
173
+ # Then we call add_footnotes!
174
+ #
175
+ def footnotes_perform!
176
+ template_expects('html')
177
+ @controller.performed_render = true
178
+
179
+ Footnotes::Filter.start!(@controller)
180
+ @footnotes.add_footnotes!
181
+ end
182
+
183
+ def template_expects(format)
184
+ if @controller.template.respond_to?(:template_format)
185
+ @controller.template.expects(:template_format).returns(format)
186
+ else
187
+ @controller.template.expects(:format).returns(format)
188
+ end
189
+ end
190
+ end
191
+
192
+ $html = <<HTML
193
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
194
+ <html>
195
+ <head>
196
+ <title>HTML to XHTML Example: HTML page</title>
197
+ <link rel="Stylesheet" href="htmltohxhtml.css" type="text/css" media="screen">
198
+ <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
199
+ </head>
200
+ <body>
201
+ <p>This is the HTML page. It works and is encoded just like any HTML page you
202
+ have previously done. View <a href="htmltoxhtml2.htm">the XHTML version</a> of
203
+ this page to view the difference between HTML and XHTML.</p>
204
+ <p>You will be glad to know that no changes need to be made to any of your CSS files.</p>
205
+ </body>
206
+ </html>
207
+ HTML