rails6-footnotes 5.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.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +25 -0
  3. data/.gitignore +15 -0
  4. data/.rspec.example +1 -0
  5. data/CHANGELOG +129 -0
  6. data/Gemfile +10 -0
  7. data/Gemfile.lock +187 -0
  8. data/MIT-LICENSE +21 -0
  9. data/README.rdoc +188 -0
  10. data/Rakefile +18 -0
  11. data/bin/rake +29 -0
  12. data/bin/rspec +29 -0
  13. data/gemfiles/Gemfile.rails-3.2.22 +5 -0
  14. data/gemfiles/Gemfile.rails-4.0.x +6 -0
  15. data/gemfiles/Gemfile.rails-4.1.x +5 -0
  16. data/gemfiles/Gemfile.rails-4.2.x +5 -0
  17. data/gemfiles/Gemfile.rails-edge +5 -0
  18. data/lib/generators/rails_footnotes/install_generator.rb +14 -0
  19. data/lib/generators/templates/rails_footnotes.rb +26 -0
  20. data/lib/rails-footnotes.rb +68 -0
  21. data/lib/rails-footnotes/abstract_note.rb +178 -0
  22. data/lib/rails-footnotes/each_with_rescue.rb +36 -0
  23. data/lib/rails-footnotes/extension.rb +24 -0
  24. data/lib/rails-footnotes/filter.rb +359 -0
  25. data/lib/rails-footnotes/notes/all.rb +1 -0
  26. data/lib/rails-footnotes/notes/assigns_note.rb +60 -0
  27. data/lib/rails-footnotes/notes/controller_note.rb +55 -0
  28. data/lib/rails-footnotes/notes/cookies_note.rb +17 -0
  29. data/lib/rails-footnotes/notes/env_note.rb +24 -0
  30. data/lib/rails-footnotes/notes/files_note.rb +49 -0
  31. data/lib/rails-footnotes/notes/filters_note.rb +51 -0
  32. data/lib/rails-footnotes/notes/javascripts_note.rb +16 -0
  33. data/lib/rails-footnotes/notes/layout_note.rb +26 -0
  34. data/lib/rails-footnotes/notes/log_note.rb +47 -0
  35. data/lib/rails-footnotes/notes/log_note/note_logger.rb +59 -0
  36. data/lib/rails-footnotes/notes/params_note.rb +21 -0
  37. data/lib/rails-footnotes/notes/partials_note.rb +38 -0
  38. data/lib/rails-footnotes/notes/queries_note.rb +121 -0
  39. data/lib/rails-footnotes/notes/routes_note.rb +60 -0
  40. data/lib/rails-footnotes/notes/session_note.rb +27 -0
  41. data/lib/rails-footnotes/notes/stylesheets_note.rb +16 -0
  42. data/lib/rails-footnotes/notes/view_note.rb +41 -0
  43. data/lib/rails-footnotes/version.rb +3 -0
  44. data/lib/rails6-footnotes.rb +1 -0
  45. data/rails-footnotes.gemspec +22 -0
  46. data/spec/abstract_note_spec.rb +89 -0
  47. data/spec/app/assets/config/manifest.js +2 -0
  48. data/spec/app/assets/javascripts/foobar.js +1 -0
  49. data/spec/app/assets/stylesheets/foobar.css +0 -0
  50. data/spec/app/views/files/index.html.erb +1 -0
  51. data/spec/app/views/layouts/application.html.erb +12 -0
  52. data/spec/app/views/partials/_foo.html.erb +1 -0
  53. data/spec/app/views/partials/index.html.erb +1 -0
  54. data/spec/controllers/files_note_controller_spec.rb +38 -0
  55. data/spec/controllers/footnotes_controller_spec.rb +128 -0
  56. data/spec/controllers/log_note_controller_spec.rb +32 -0
  57. data/spec/controllers/partials_note_controller_spec.rb +28 -0
  58. data/spec/env_note_spec.rb +73 -0
  59. data/spec/fixtures/html_download.html +5 -0
  60. data/spec/footnotes_spec.rb +234 -0
  61. data/spec/notes/assigns_note_spec.rb +50 -0
  62. data/spec/notes/controller_note_spec.rb +12 -0
  63. data/spec/notes/files_note_spec.rb +26 -0
  64. data/spec/notes/javascripts_note_spec.rb +18 -0
  65. data/spec/notes/stylesheets_note_spec.rb +19 -0
  66. data/spec/notes/view_note_spec.rb +12 -0
  67. data/spec/spec_helper.rb +68 -0
  68. metadata +153 -0
@@ -0,0 +1,17 @@
1
+ module Footnotes
2
+ module Notes
3
+ class CookiesNote < AbstractNote
4
+ def initialize(controller)
5
+ @cookies = controller.request.cookies
6
+ end
7
+
8
+ def title
9
+ "Cookies (#{@cookies.length})"
10
+ end
11
+
12
+ def content
13
+ mount_table_for_hash(@cookies, :summary => "Debug information for #{title}")
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,24 @@
1
+ module Footnotes
2
+ module Notes
3
+ class EnvNote < AbstractNote
4
+ def initialize(controller)
5
+ @env = controller.request.env.dup
6
+ end
7
+
8
+ def content
9
+ env_data = @env.map { |k, v|
10
+ case k
11
+ when 'HTTP_COOKIE'
12
+ # Replace HTTP_COOKIE for a link
13
+ [k.to_s, '<a href="#" style="color:#009" onclick="Footnotes.hideAllAndToggle(\'cookies_debug_info\');return false;">See cookies on its tab</a>']
14
+ else
15
+ [k.to_s, escape(v.to_s)]
16
+ end
17
+ }.sort.unshift([ :key, escape('value') ])
18
+
19
+ # Create the env table
20
+ mount_table(env_data)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,49 @@
1
+ module Footnotes
2
+ module Notes
3
+ class FilesNote < AbstractNote
4
+ def initialize(controller)
5
+ @files = scan_text(controller.response.body)
6
+ parse_files!
7
+ end
8
+
9
+ def row
10
+ :edit
11
+ end
12
+
13
+ def content
14
+ if @files.empty?
15
+ ""
16
+ else
17
+ "<ul><li>%s</li></ul>" % @files.join("</li><li>")
18
+ end
19
+ end
20
+
21
+ def valid?
22
+ prefix?
23
+ end
24
+
25
+ protected
26
+ def scan_text(text)
27
+ raise NotImplementedError, "implement this in your subclass"
28
+ end
29
+
30
+ def parse_files!
31
+ asset_paths = Rails.application.config.try(:assets).try(:paths) || []
32
+ linked_files = []
33
+
34
+ @files.collect do |file|
35
+ file.gsub!(/-[a-f0-9]{64}\./, '.')
36
+ base_name = File.basename(file)
37
+ asset_paths.each do |asset_path|
38
+ results = Dir[File.expand_path(base_name, asset_path) + '*']
39
+ results.each do |r|
40
+ linked_files << %[<a href="#{Footnotes::Filter.prefix(r, 1, 1)}">#{File.basename(r)}</a>]
41
+ end
42
+ break if results.present?
43
+ end
44
+ end
45
+ @files = linked_files
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,51 @@
1
+ module Footnotes
2
+ module Notes
3
+ class FiltersNote < AbstractNote
4
+ def initialize(controller)
5
+ @controller = controller
6
+ @parsed_filters = parse_filters
7
+ end
8
+
9
+ def legend
10
+ "Filter chain for #{@controller.class.to_s}"
11
+ end
12
+
13
+ def content
14
+ mount_table(@parsed_filters.unshift([:name, :type, :actions]), :summary => "Debug information for #{title}")
15
+ end
16
+
17
+ protected
18
+ # Get controller filter chain
19
+ #
20
+ def parse_filters
21
+ return [] # TODO @controller.class.filter_chain.collect do |filter|
22
+ # [parse_method(filter.method), filter.type.inspect, controller_filtered_actions(filter).inspect]
23
+ # end
24
+ end
25
+
26
+ # This receives a filter, creates a mock controller and check in which
27
+ # actions the filter is performed
28
+ #
29
+ def controller_filtered_actions(filter)
30
+ mock_controller = Footnotes::Extensions::MockController.new
31
+
32
+ return @controller.class.action_methods.select { |action|
33
+ mock_controller.action_name = action
34
+
35
+ #remove conditions (this would call a Proc on the mock_controller)
36
+ filter.options.merge!(:if => nil, :unless => nil)
37
+
38
+ filter.__send__(:should_run_callback?, mock_controller)
39
+ }.map(&:to_sym)
40
+ end
41
+
42
+ def parse_method(method = '')
43
+ escape(method.inspect.gsub(RAILS_ROOT, ''))
44
+ end
45
+ end
46
+ end
47
+
48
+ module Extensions
49
+ class MockController < Struct.new(:action_name); end
50
+ end
51
+ end
@@ -0,0 +1,16 @@
1
+ require "rails-footnotes/notes/files_note"
2
+
3
+ module Footnotes
4
+ module Notes
5
+ class JavascriptsNote < FilesNote
6
+ def title
7
+ "Javascripts (#{@files.length})"
8
+ end
9
+
10
+ protected
11
+ def scan_text(text)
12
+ text.scan(/<script[^>]+src\s*=\s*['"]([^>?'"]+\.js)/im).flatten
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,26 @@
1
+ module Footnotes
2
+ module Notes
3
+ class LayoutNote < AbstractNote
4
+ def initialize(controller)
5
+ @controller = controller
6
+ end
7
+
8
+ def row
9
+ :edit
10
+ end
11
+
12
+ def link
13
+ escape(Footnotes::Filter.prefix(filename, 1, 1))
14
+ end
15
+
16
+ def valid?
17
+ prefix? && nil#@controller.active_layout TODO doesn't work with Rails 3
18
+ end
19
+
20
+ protected
21
+ def filename
22
+ File.join(File.expand_path(Rails.root), 'app', 'layouts', "#{@controller.active_layout.to_s.underscore}").sub('/layouts/layouts/', '/views/layouts/')
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,47 @@
1
+ module Footnotes
2
+ module Notes
3
+ class LogNote < AbstractNote
4
+
5
+ autoload :NoteLogger, 'rails-footnotes/notes/log_note/note_logger'
6
+
7
+ cattr_accessor :logs
8
+ cattr_accessor :original_logger
9
+
10
+ def self.start!(controller)
11
+ self.logs = []
12
+ self.original_logger = Rails.logger
13
+ note_logger = NoteLogger.new(self.logs)
14
+ note_logger.level = self.original_logger.level
15
+ note_logger.formatter =
16
+ if self.original_logger.kind_of?(Logger)
17
+ self.original_logger.formatter
18
+ else
19
+ defined?(ActiveSupport::Logger) ? ActiveSupport::Logger::SimpleFormatter.new : Logger::SimpleFormatter.new
20
+ end
21
+ # Rails 3 don't have ActiveSupport::Logger#broadcast so we backported it
22
+ extend_module = defined?(ActiveSupport::Logger) ? ActiveSupport::Logger.broadcast(note_logger) : NoteLogger.broadcast(note_logger)
23
+ Rails.logger = self.original_logger.clone.extend(extend_module)
24
+ end
25
+
26
+ def title
27
+ "Log (#{log.count})"
28
+ end
29
+
30
+ def content
31
+ result = '<table>'
32
+ log.compact.each do |l|
33
+ result << "<tr><td>#{l.gsub(/\e\[.+?m/, '')}</td></tr>"
34
+ end
35
+ result << '</table>'
36
+ # Restore formatter
37
+ Rails.logger = self.class.original_logger
38
+ result
39
+ end
40
+
41
+ def log
42
+ self.class.logs
43
+ end
44
+
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,59 @@
1
+ module Footnotes
2
+ module Notes
3
+ class LogNote
4
+ class NoteLogger < Logger
5
+
6
+ def initialize(logs)
7
+ @logs = logs
8
+ end
9
+
10
+ def add(severity, message = nil, progname = nil, &block)
11
+ severity ||= UNKNOWN
12
+ if severity < level
13
+ return true
14
+ end
15
+ formatter = @formatter || Logger::Formatter.new
16
+ @logs << formatter.call(format_severity(severity), Time.now, message, progname)
17
+ end
18
+
19
+ ## Backport from rails 4 for handling logging broadcast, should be removed when rails 3 is deprecated :
20
+
21
+ # Broadcasts logs to multiple loggers.
22
+ def self.broadcast(logger) # :nodoc:
23
+ Module.new do
24
+ define_method(:add) do |*args, &block|
25
+ logger.add(*args, &block)
26
+ super(*args, &block)
27
+ end
28
+
29
+ define_method(:<<) do |x|
30
+ logger << x
31
+ super(x)
32
+ end
33
+
34
+ define_method(:close) do
35
+ logger.close
36
+ super()
37
+ end
38
+
39
+ define_method(:progname=) do |name|
40
+ logger.progname = name
41
+ super(name)
42
+ end
43
+
44
+ define_method(:formatter=) do |formatter|
45
+ logger.formatter = formatter
46
+ super(formatter)
47
+ end
48
+
49
+ define_method(:level=) do |level|
50
+ logger.level = level
51
+ super(level)
52
+ end
53
+ end
54
+ end
55
+
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,21 @@
1
+ module Footnotes
2
+ module Notes
3
+ class ParamsNote < AbstractNote
4
+ def initialize(controller)
5
+ @params = if Rails::VERSION::MAJOR >= 5
6
+ controller.params.to_unsafe_h
7
+ else
8
+ controller.params
9
+ end
10
+ end
11
+
12
+ def title
13
+ "Params (#{@params.length})"
14
+ end
15
+
16
+ def content
17
+ mount_table_for_hash(@params, :summary => "Debug information for #{title}")
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,38 @@
1
+ module Footnotes
2
+ module Notes
3
+ class PartialsNote < AbstractNote
4
+
5
+ cattr_accessor :partials
6
+
7
+ def self.start!(controller)
8
+ self.partials = []
9
+ @subscriber ||= ActiveSupport::Notifications.subscribe('render_partial.action_view') do |*args|
10
+ event = ActiveSupport::Notifications::Event.new *args
11
+ self.partials << {:file => event.payload[:identifier], :duration => event.duration}
12
+ end
13
+ end
14
+
15
+ def initialize(controller)
16
+ @controller = controller
17
+ end
18
+
19
+ def row
20
+ :edit
21
+ end
22
+
23
+ def title
24
+ "Partials (#{partials.size})"
25
+ end
26
+
27
+ def content
28
+ rows = self.class.partials.map do |partial|
29
+ href = Footnotes::Filter.prefix(partial[:file],1,1)
30
+ shortened_name = partial[:file].gsub(File.join(Rails.root,"app/views/"),"")
31
+ [%{<a href="#{href}">#{shortened_name}</a>},"#{partial[:duration]}ms"]
32
+ end
33
+ mount_table(rows.unshift(%w(Partial Time)), :summary => "Partials for #{title}")
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,121 @@
1
+ module Footnotes
2
+ module Notes
3
+ class QueriesNote < AbstractNote
4
+ cattr_accessor :alert_db_time, :alert_sql_number, :orm, :ignored_regexps, :instance_writer => false
5
+ @@alert_db_time = 16.0
6
+ @@alert_sql_number = 8
7
+ @@query_subscriber = nil
8
+ @@orm = [:active_record, :data_mapper]
9
+ @@ignored_regexps = [%r{(pg_table|pg_attribute|pg_namespace|show\stables|pragma|sqlite_master)}i]
10
+
11
+ def self.start!(controller)
12
+ self.query_subscriber.reset!
13
+ end
14
+
15
+ def self.query_subscriber
16
+ @@query_subscriber ||= Footnotes::Notes::QuerySubscriber.new(self.orm)
17
+ end
18
+
19
+ def events
20
+ self.class.query_subscriber.events
21
+ end
22
+
23
+ def title
24
+ queries = self.events.count
25
+ total_time = self.events.map(&:duration).sum
26
+ query_color = alert_color(self.events.count, alert_sql_number)
27
+ db_color = alert_color(total_time, alert_db_time)
28
+
29
+ <<-TITLE
30
+ <span style="background-color:#{query_color}">Queries (#{queries})</span>
31
+ <span style="background-color:#{db_color}">DB (#{"%.3f" % total_time}ms)</span>
32
+ TITLE
33
+ end
34
+
35
+ def content
36
+ html = '<table>'
37
+ self.events.each_with_index do |event, index|
38
+ sql_links = []
39
+ sql_links << "<a href=\"javascript:Footnotes.toggle('qtrace_#{index}')\" style=\"color:#00A;\">trace</a>"
40
+
41
+ html << <<-HTML
42
+ <tr>
43
+ <td>
44
+ <b id="qtitle_#{index}">#{escape(event.type.to_s.upcase)}</b> (#{sql_links.join(' | ')})
45
+ <p id="qtrace_#{index}" style="display:none;">#{parse_trace(event.trace)}</p><br />
46
+ </td>
47
+ <td>
48
+ <span id="sql_#{index}">#{print_query(event.payload[:sql])}</span>
49
+ </td>
50
+ <td>#{print_name_and_time(event.payload[:name], event.duration)}</td>
51
+ </tr>
52
+ HTML
53
+ end
54
+ html << '</table>'
55
+ return html
56
+ end
57
+
58
+ protected
59
+ def print_name_and_time(name, time)
60
+ "<span style='background-color:#{alert_color(time, alert_ratio)}'>#{escape(name || 'SQL')} (#{'%.3fms' % time})</span>"
61
+ end
62
+
63
+ def print_query(query)
64
+ escape(query.to_s.gsub(/(\s)+/, ' ').gsub('`', ''))
65
+ end
66
+
67
+ def alert_color(value, threshold)
68
+ return 'transparent' if value < threshold
69
+ '#ffff00'
70
+ end
71
+
72
+ def alert_ratio
73
+ alert_db_time / alert_sql_number
74
+ end
75
+
76
+ def parse_trace(trace)
77
+ trace.map do |t|
78
+ s = t.split(':')
79
+ %[<a href="#{escape(Footnotes::Filter.prefix("#{Rails.root.to_s}/#{s[0]}", s[1].to_i, 1))}">#{escape(t)}</a><br />]
80
+ end.join
81
+ end
82
+ end
83
+
84
+ class QuerySubscriberNotifactionEvent
85
+ attr_reader :event, :trace, :query
86
+ delegate :name, :payload, :duration, :time, :type, :to => :event
87
+
88
+ def initialize(event, ctrace)
89
+ @event, @ctrace, @query = event, ctrace, event.payload[:sql]
90
+ end
91
+
92
+ def trace
93
+ @trace ||= @ctrace.collect(&:strip).select{|i| i.gsub!(/^#{Rails.root.to_s}\//, '') } || []
94
+ end
95
+
96
+ def type
97
+ @type ||= self.query.match(/^(\s*)(select|insert|update|delete|alter)\b/im) || 'Unknown'
98
+ end
99
+ end
100
+
101
+ class QuerySubscriber < ActiveSupport::LogSubscriber
102
+ attr_accessor :events, :ignore_regexps
103
+
104
+ def initialize(orm)
105
+ super()
106
+ @events = []
107
+ orm.each {|adapter| ActiveSupport::LogSubscriber.attach_to adapter, self}
108
+ end
109
+
110
+ def reset!
111
+ self.events.clear
112
+ end
113
+
114
+ def sql(event)
115
+ unless QueriesNote.ignored_regexps.any? {|rex| event.payload[:sql] =~ rex }
116
+ @events << QuerySubscriberNotifactionEvent.new(event.dup, caller)
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end