rails3-footnotes 4.0.0.pre

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.
@@ -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