actionview 7.0.1 → 7.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +281 -202
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +3 -3
  5. data/app/assets/javascripts/rails-ujs.esm.js +693 -0
  6. data/app/assets/javascripts/rails-ujs.js +630 -0
  7. data/lib/action_view/base.rb +33 -12
  8. data/lib/action_view/buffers.rb +106 -8
  9. data/lib/action_view/cache_expiry.rb +40 -43
  10. data/lib/action_view/context.rb +1 -1
  11. data/lib/action_view/deprecator.rb +7 -0
  12. data/lib/action_view/digestor.rb +1 -1
  13. data/lib/action_view/gem_version.rb +2 -2
  14. data/lib/action_view/helpers/active_model_helper.rb +1 -1
  15. data/lib/action_view/helpers/asset_tag_helper.rb +133 -48
  16. data/lib/action_view/helpers/asset_url_helper.rb +13 -12
  17. data/lib/action_view/helpers/atom_feed_helper.rb +5 -5
  18. data/lib/action_view/helpers/cache_helper.rb +3 -9
  19. data/lib/action_view/helpers/capture_helper.rb +26 -12
  20. data/lib/action_view/helpers/content_exfiltration_prevention_helper.rb +70 -0
  21. data/lib/action_view/helpers/controller_helper.rb +6 -0
  22. data/lib/action_view/helpers/csp_helper.rb +2 -2
  23. data/lib/action_view/helpers/csrf_helper.rb +3 -3
  24. data/lib/action_view/helpers/date_helper.rb +76 -64
  25. data/lib/action_view/helpers/debug_helper.rb +3 -3
  26. data/lib/action_view/helpers/form_helper.rb +62 -31
  27. data/lib/action_view/helpers/form_options_helper.rb +6 -3
  28. data/lib/action_view/helpers/form_tag_helper.rb +88 -44
  29. data/lib/action_view/helpers/javascript_helper.rb +1 -0
  30. data/lib/action_view/helpers/number_helper.rb +15 -13
  31. data/lib/action_view/helpers/output_safety_helper.rb +4 -4
  32. data/lib/action_view/helpers/rendering_helper.rb +5 -6
  33. data/lib/action_view/helpers/sanitize_helper.rb +34 -15
  34. data/lib/action_view/helpers/tag_helper.rb +27 -16
  35. data/lib/action_view/helpers/tags/base.rb +11 -52
  36. data/lib/action_view/helpers/tags/collection_check_boxes.rb +1 -0
  37. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +1 -0
  38. data/lib/action_view/helpers/tags/collection_select.rb +3 -0
  39. data/lib/action_view/helpers/tags/date_field.rb +1 -1
  40. data/lib/action_view/helpers/tags/date_select.rb +2 -0
  41. data/lib/action_view/helpers/tags/datetime_field.rb +14 -6
  42. data/lib/action_view/helpers/tags/datetime_local_field.rb +11 -2
  43. data/lib/action_view/helpers/tags/grouped_collection_select.rb +3 -0
  44. data/lib/action_view/helpers/tags/month_field.rb +1 -1
  45. data/lib/action_view/helpers/tags/select.rb +4 -1
  46. data/lib/action_view/helpers/tags/select_renderer.rb +56 -0
  47. data/lib/action_view/helpers/tags/time_field.rb +1 -1
  48. data/lib/action_view/helpers/tags/time_zone_select.rb +3 -0
  49. data/lib/action_view/helpers/tags/week_field.rb +1 -1
  50. data/lib/action_view/helpers/tags/weekday_select.rb +3 -0
  51. data/lib/action_view/helpers/tags.rb +2 -0
  52. data/lib/action_view/helpers/text_helper.rb +33 -17
  53. data/lib/action_view/helpers/translation_helper.rb +6 -6
  54. data/lib/action_view/helpers/url_helper.rb +90 -65
  55. data/lib/action_view/helpers.rb +2 -0
  56. data/lib/action_view/layouts.rb +13 -8
  57. data/lib/action_view/log_subscriber.rb +49 -32
  58. data/lib/action_view/lookup_context.rb +29 -13
  59. data/lib/action_view/path_registry.rb +57 -0
  60. data/lib/action_view/path_set.rb +13 -14
  61. data/lib/action_view/railtie.rb +26 -3
  62. data/lib/action_view/record_identifier.rb +16 -9
  63. data/lib/action_view/renderer/abstract_renderer.rb +1 -1
  64. data/lib/action_view/renderer/collection_renderer.rb +9 -1
  65. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +21 -3
  66. data/lib/action_view/renderer/partial_renderer.rb +3 -2
  67. data/lib/action_view/renderer/renderer.rb +2 -0
  68. data/lib/action_view/renderer/streaming_template_renderer.rb +3 -2
  69. data/lib/action_view/renderer/template_renderer.rb +3 -2
  70. data/lib/action_view/rendering.rb +24 -6
  71. data/lib/action_view/ripper_ast_parser.rb +6 -6
  72. data/lib/action_view/routing_url_for.rb +7 -4
  73. data/lib/action_view/template/error.rb +14 -1
  74. data/lib/action_view/template/handlers/builder.rb +4 -4
  75. data/lib/action_view/template/handlers/erb/erubi.rb +23 -27
  76. data/lib/action_view/template/handlers/erb.rb +73 -1
  77. data/lib/action_view/template/handlers.rb +1 -1
  78. data/lib/action_view/template/html.rb +1 -1
  79. data/lib/action_view/template/raw_file.rb +1 -1
  80. data/lib/action_view/template/renderable.rb +1 -1
  81. data/lib/action_view/template/resolver.rb +15 -5
  82. data/lib/action_view/template/text.rb +1 -1
  83. data/lib/action_view/template/types.rb +25 -34
  84. data/lib/action_view/template.rb +227 -53
  85. data/lib/action_view/template_path.rb +2 -0
  86. data/lib/action_view/test_case.rb +174 -21
  87. data/lib/action_view/unbound_template.rb +15 -5
  88. data/lib/action_view/version.rb +1 -1
  89. data/lib/action_view/view_paths.rb +19 -28
  90. data/lib/action_view.rb +4 -1
  91. data/lib/assets/compiled/rails-ujs.js +36 -5
  92. metadata +27 -27
@@ -5,7 +5,7 @@ require "active_support/log_subscriber"
5
5
  module ActionView
6
6
  # = Action View Log Subscriber
7
7
  #
8
- # Provides functionality so that Rails can output logs from Action View.
8
+ # Provides functionality so that \Rails can output logs from Action View.
9
9
  class LogSubscriber < ActiveSupport::LogSubscriber
10
10
  VIEWS_PATTERN = /^app\/views\//
11
11
 
@@ -21,6 +21,7 @@ module ActionView
21
21
  message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
22
22
  end
23
23
  end
24
+ subscribe_log_level :render_template, :debug
24
25
 
25
26
  def render_partial(event)
26
27
  debug do
@@ -31,6 +32,7 @@ module ActionView
31
32
  message
32
33
  end
33
34
  end
35
+ subscribe_log_level :render_partial, :debug
34
36
 
35
37
  def render_layout(event)
36
38
  info do
@@ -38,6 +40,7 @@ module ActionView
38
40
  message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
39
41
  end
40
42
  end
43
+ subscribe_log_level :render_layout, :info
41
44
 
42
45
  def render_collection(event)
43
46
  identifier = event.payload[:identifier] || "templates"
@@ -49,29 +52,60 @@ module ActionView
49
52
  message
50
53
  end
51
54
  end
55
+ subscribe_log_level :render_collection, :debug
52
56
 
53
- def start(name, id, payload)
54
- log_rendering_start(payload, name)
57
+ module Utils # :nodoc:
58
+ def logger
59
+ ActionView::Base.logger
60
+ end
55
61
 
56
- super
57
- end
62
+ private
63
+ def from_rails_root(string)
64
+ string = string.sub(rails_root, "")
65
+ string.sub!(VIEWS_PATTERN, "")
66
+ string
67
+ end
58
68
 
59
- def logger
60
- ActionView::Base.logger
69
+ def rails_root # :doc:
70
+ @root ||= "#{Rails.root}/"
71
+ end
61
72
  end
62
73
 
63
- private
64
- EMPTY = ""
65
- def from_rails_root(string) # :doc:
66
- string = string.sub(rails_root, EMPTY)
67
- string.sub!(VIEWS_PATTERN, EMPTY)
68
- string
74
+ include Utils
75
+
76
+ class Start # :nodoc:
77
+ include Utils
78
+
79
+ def start(name, id, payload)
80
+ return unless logger
81
+ logger.debug do
82
+ qualifier =
83
+ if name == "render_template.action_view"
84
+ ""
85
+ elsif name == "render_layout.action_view"
86
+ "layout "
87
+ end
88
+
89
+ return unless qualifier
90
+
91
+ message = +" Rendering #{qualifier}#{from_rails_root(payload[:identifier])}"
92
+ message << " within #{from_rails_root(payload[:layout])}" if payload[:layout]
93
+ message
94
+ end
95
+ end
96
+
97
+ def finish(name, id, payload)
98
+ end
69
99
  end
70
100
 
71
- def rails_root # :doc:
72
- @root ||= "#{Rails.root}/"
101
+ def self.attach_to(*)
102
+ ActiveSupport::Notifications.subscribe("render_template.action_view", ActionView::LogSubscriber::Start.new)
103
+ ActiveSupport::Notifications.subscribe("render_layout.action_view", ActionView::LogSubscriber::Start.new)
104
+
105
+ super
73
106
  end
74
107
 
108
+ private
75
109
  def render_count(payload) # :doc:
76
110
  if payload[:cache_hits]
77
111
  "[#{payload[:cache_hits]} / #{payload[:count]} cache hits]"
@@ -88,23 +122,6 @@ module ActionView
88
122
  "[cache miss]"
89
123
  end
90
124
  end
91
-
92
- def log_rendering_start(payload, name)
93
- debug do
94
- qualifier =
95
- if name == "render_template.action_view"
96
- ""
97
- elsif name == "render_layout.action_view"
98
- "layout "
99
- end
100
-
101
- return unless qualifier
102
-
103
- message = +" Rendering #{qualifier}#{from_rails_root(payload[:identifier])}"
104
- message << " within #{from_rails_root(payload[:layout])}" if payload[:layout]
105
- message
106
- end
107
- end
108
125
  end
109
126
  end
110
127
 
@@ -13,7 +13,7 @@ module ActionView
13
13
  # view paths, used in the resolver cache lookup. Since this key is generated
14
14
  # only once during the request, it speeds up all cache accesses.
15
15
  class LookupContext # :nodoc:
16
- attr_accessor :prefixes, :rendered_format
16
+ attr_accessor :prefixes
17
17
 
18
18
  singleton_class.attr_accessor :registered_details
19
19
  self.registered_details = []
@@ -63,16 +63,20 @@ module ActionView
63
63
  end
64
64
 
65
65
  def self.details_cache_key(details)
66
- if details[:formats]
67
- details = details.dup
68
- details[:formats] &= Template::Types.symbols
66
+ @details_keys.fetch(details) do
67
+ if formats = details[:formats]
68
+ unless Template::Types.valid_symbols?(formats)
69
+ details = details.dup
70
+ details[:formats] &= Template::Types.symbols
71
+ end
72
+ end
73
+ @details_keys[details] ||= TemplateDetails::Requested.new(**details)
69
74
  end
70
- @details_keys[details] ||= TemplateDetails::Requested.new(**details)
71
75
  end
72
76
 
73
77
  def self.clear
74
- ActionView::ViewPaths.all_view_paths.each do |path_set|
75
- path_set.each(&:clear_cache)
78
+ ActionView::PathRegistry.all_resolvers.each do |resolver|
79
+ resolver.clear_cache
76
80
  end
77
81
  @view_context_class = nil
78
82
  @details_keys.clear
@@ -83,9 +87,9 @@ module ActionView
83
87
  @digest_cache.values
84
88
  end
85
89
 
86
- def self.view_context_class(klass)
90
+ def self.view_context_class
87
91
  @view_context_mutex.synchronize do
88
- @view_context_class ||= klass.with_empty_template_cache
92
+ @view_context_class ||= ActionView::Base.with_empty_template_cache
89
93
  end
90
94
  end
91
95
  end
@@ -148,11 +152,23 @@ module ActionView
148
152
  end
149
153
  alias :any_templates? :any?
150
154
 
155
+ def append_view_paths(paths)
156
+ @view_paths = build_view_paths(@view_paths.to_a + paths)
157
+ end
158
+
159
+ def prepend_view_paths(paths)
160
+ @view_paths = build_view_paths(paths + @view_paths.to_a)
161
+ end
162
+
151
163
  private
152
164
  # Whenever setting view paths, makes a copy so that we can manipulate them in
153
165
  # instance objects as we wish.
154
166
  def build_view_paths(paths)
155
- ActionView::PathSet.new(Array(paths))
167
+ if ActionView::PathSet === paths
168
+ paths
169
+ else
170
+ ActionView::PathSet.new(Array(paths))
171
+ end
156
172
  end
157
173
 
158
174
  # Compute details hash and key according to user options (e.g. passed from #render).
@@ -250,12 +266,12 @@ module ActionView
250
266
  values.concat(default_formats) if values.delete "*/*"
251
267
  values.uniq!
252
268
 
253
- invalid_values = (values - Template::Types.symbols)
254
- unless invalid_values.empty?
269
+ unless Template::Types.valid_symbols?(values)
270
+ invalid_values = values - Template::Types.symbols
255
271
  raise ArgumentError, "Invalid formats: #{invalid_values.map(&:inspect).join(", ")}"
256
272
  end
257
273
 
258
- if values == [:js]
274
+ if (values.length == 1) && (values[0] == :js)
259
275
  values << :html
260
276
  @html_fallback_for_js = true
261
277
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView # :nodoc:
4
+ module PathRegistry # :nodoc:
5
+ @view_paths_by_class = {}
6
+ @file_system_resolvers = {}
7
+ @file_system_resolver_mutex = Mutex.new
8
+ @file_system_resolver_hooks = []
9
+
10
+ class << self
11
+ attr_reader :file_system_resolver_hooks
12
+ end
13
+
14
+ def self.get_view_paths(klass)
15
+ @view_paths_by_class[klass] || get_view_paths(klass.superclass)
16
+ end
17
+
18
+ def self.set_view_paths(klass, paths)
19
+ @view_paths_by_class[klass] = paths
20
+ end
21
+
22
+ def self.cast_file_system_resolvers(paths)
23
+ paths = Array(paths)
24
+
25
+ @file_system_resolver_mutex.synchronize do
26
+ built_resolver = false
27
+ paths = paths.map do |path|
28
+ case path
29
+ when String, Pathname
30
+ path = File.expand_path(path)
31
+ @file_system_resolvers[path] ||=
32
+ begin
33
+ built_resolver = true
34
+ FileSystemResolver.new(path)
35
+ end
36
+ else
37
+ path
38
+ end
39
+ end
40
+
41
+ file_system_resolver_hooks.each(&:call) if built_resolver
42
+ end
43
+
44
+ paths
45
+ end
46
+
47
+ def self.all_resolvers
48
+ resolvers = [all_file_system_resolvers]
49
+ resolvers.concat @view_paths_by_class.values.map(&:to_a)
50
+ resolvers.flatten.uniq
51
+ end
52
+
53
+ def self.all_file_system_resolvers
54
+ @file_system_resolvers.values
55
+ end
56
+ end
57
+ end
@@ -13,14 +13,14 @@ module ActionView # :nodoc:
13
13
 
14
14
  attr_reader :paths
15
15
 
16
- delegate :[], :include?, :pop, :size, :each, to: :paths
16
+ delegate :[], :include?, :size, :each, to: :paths
17
17
 
18
18
  def initialize(paths = [])
19
- @paths = typecast paths
19
+ @paths = typecast(paths).freeze
20
20
  end
21
21
 
22
22
  def initialize_copy(other)
23
- @paths = other.paths.dup
23
+ @paths = other.paths.dup.freeze
24
24
  self
25
25
  end
26
26
 
@@ -32,18 +32,11 @@ module ActionView # :nodoc:
32
32
  PathSet.new paths.compact
33
33
  end
34
34
 
35
- def +(array)
35
+ def +(other)
36
+ array = Array === other ? other : other.paths
36
37
  PathSet.new(paths + array)
37
38
  end
38
39
 
39
- %w(<< concat push insert unshift).each do |method|
40
- class_eval <<-METHOD, __FILE__, __LINE__ + 1
41
- def #{method}(*args)
42
- paths.#{method}(*typecast(args))
43
- end
44
- METHOD
45
- end
46
-
47
40
  def find(path, prefixes, partial, details, details_key, locals)
48
41
  find_all(path, prefixes, partial, details, details_key, locals).first ||
49
42
  raise(MissingTemplate.new(self, path, prefixes, partial, details, details_key, locals))
@@ -75,9 +68,15 @@ module ActionView # :nodoc:
75
68
  paths.map do |path|
76
69
  case path
77
70
  when Pathname, String
78
- FileSystemResolver.new path.to_s
79
- else
71
+ # This path should only be reached by "direct" users of
72
+ # ActionView::Base (not using the ViewPaths or Renderer modules).
73
+ # We can't cache/de-dup the file system resolver in this case as we
74
+ # don't know which compiled_method_container we'll be rendering to.
75
+ FileSystemResolver.new(path)
76
+ when Resolver
80
77
  path
78
+ else
79
+ raise TypeError, "#{path.inspect} is not a valid path: must be a String, Pathname, or Resolver"
81
80
  end
82
81
  end
83
82
  end
@@ -13,6 +13,7 @@ module ActionView
13
13
  config.action_view.image_loading = nil
14
14
  config.action_view.image_decoding = nil
15
15
  config.action_view.apply_stylesheet_media_default = true
16
+ config.action_view.prepend_content_exfiltration_prevention = false
16
17
 
17
18
  config.eager_load_namespaces << ActionView
18
19
 
@@ -40,6 +41,17 @@ module ActionView
40
41
  end
41
42
  end
42
43
 
44
+ config.after_initialize do |app|
45
+ prepend_content_exfiltration_prevention = app.config.action_view.delete(:prepend_content_exfiltration_prevention)
46
+ ActionView::Helpers::ContentExfiltrationPreventionHelper.prepend_content_exfiltration_prevention = prepend_content_exfiltration_prevention
47
+ end
48
+
49
+ config.after_initialize do |app|
50
+ if klass = app.config.action_view.delete(:sanitizer_vendor)
51
+ ActionView::Helpers::SanitizeHelper.sanitizer_vendor = klass
52
+ end
53
+ end
54
+
43
55
  config.after_initialize do |app|
44
56
  button_to_generates_button_tag = app.config.action_view.delete(:button_to_generates_button_tag)
45
57
  unless button_to_generates_button_tag.nil?
@@ -67,6 +79,10 @@ module ActionView
67
79
  end
68
80
  end
69
81
 
82
+ initializer "action_view.deprecator", before: :load_environment_config do |app|
83
+ app.deprecators[:action_view] = ActionView.deprecator
84
+ end
85
+
70
86
  initializer "action_view.logger" do
71
87
  ActiveSupport.on_load(:action_view) { self.logger ||= Rails.logger }
72
88
  end
@@ -74,7 +90,7 @@ module ActionView
74
90
  initializer "action_view.caching" do |app|
75
91
  ActiveSupport.on_load(:action_view) do
76
92
  if app.config.action_view.cache_template_loading.nil?
77
- ActionView::Resolver.caching = app.config.cache_classes
93
+ ActionView::Resolver.caching = !app.config.reloading_enabled?
78
94
  end
79
95
  end
80
96
  end
@@ -91,13 +107,20 @@ module ActionView
91
107
 
92
108
  config.after_initialize do |app|
93
109
  enable_caching = if app.config.action_view.cache_template_loading.nil?
94
- app.config.cache_classes
110
+ !app.config.reloading_enabled?
95
111
  else
96
112
  app.config.action_view.cache_template_loading
97
113
  end
98
114
 
99
115
  unless enable_caching
100
- app.executor.register_hook ActionView::CacheExpiry::Executor.new(watcher: app.config.file_watcher)
116
+ view_reloader = ActionView::CacheExpiry::ViewReloader.new(watcher: app.config.file_watcher)
117
+
118
+ app.reloaders << view_reloader
119
+ view_reloader.execute
120
+ app.reloader.to_run do
121
+ require_unload_lock!
122
+ view_reloader.execute
123
+ end
101
124
  end
102
125
  end
103
126
 
@@ -4,6 +4,8 @@ require "active_support/core_ext/module"
4
4
  require "action_view/model_naming"
5
5
 
6
6
  module ActionView
7
+ # = Action View \Record \Identifier
8
+ #
7
9
  # RecordIdentifier encapsulates methods used by various ActionView helpers
8
10
  # to associate records with DOM elements.
9
11
  #
@@ -31,6 +33,8 @@ module ActionView
31
33
  # automatically generated, following naming conventions encapsulated by the
32
34
  # RecordIdentifier methods #dom_id and #dom_class:
33
35
  #
36
+ # dom_id(Post) # => "new_post"
37
+ # dom_class(Post) # => "post"
34
38
  # dom_id(Post.new) # => "new_post"
35
39
  # dom_class(Post.new) # => "post"
36
40
  # dom_id(Post.find 42) # => "post_42"
@@ -79,18 +83,21 @@ module ActionView
79
83
  # The DOM id convention is to use the singular form of an object or class with the id following an underscore.
80
84
  # If no id is found, prefix with "new_" instead.
81
85
  #
82
- # dom_id(Post.find(45)) # => "post_45"
83
- # dom_id(Post.new) # => "new_post"
86
+ # dom_id(Post.find(45)) # => "post_45"
87
+ # dom_id(Post) # => "new_post"
84
88
  #
85
89
  # If you need to address multiple instances of the same class in the same view, you can prefix the dom_id:
86
90
  #
87
91
  # dom_id(Post.find(45), :edit) # => "edit_post_45"
88
- # dom_id(Post.new, :custom) # => "custom_post"
89
- def dom_id(record, prefix = nil)
90
- if record_id = record_key_for_dom_id(record)
91
- "#{dom_class(record, prefix)}#{JOIN}#{record_id}"
92
+ # dom_id(Post, :custom) # => "custom_post"
93
+ def dom_id(record_or_class, prefix = nil)
94
+ raise ArgumentError, "dom_id must be passed a record_or_class as the first argument, you passed #{record_or_class.inspect}" unless record_or_class
95
+
96
+ record_id = record_key_for_dom_id(record_or_class) unless record_or_class.is_a?(Class)
97
+ if record_id
98
+ "#{dom_class(record_or_class, prefix)}#{JOIN}#{record_id}"
92
99
  else
93
- dom_class(record, prefix || NEW)
100
+ dom_class(record_or_class, prefix || NEW)
94
101
  end
95
102
  end
96
103
 
@@ -102,10 +109,10 @@ module ActionView
102
109
  # on the default implementation (which just joins all key attributes with '_') or on your own
103
110
  # overwritten version of the method. By default, this implementation passes the key string through a
104
111
  # method that replaces all characters that are invalid inside DOM ids, with valid ones. You need to
105
- # make sure yourself that your dom ids are valid, in case you overwrite this method.
112
+ # make sure yourself that your dom ids are valid, in case you override this method.
106
113
  def record_key_for_dom_id(record) # :doc:
107
114
  key = convert_to_model(record).to_key
108
- key ? key.join(JOIN) : key
115
+ key && key.all? ? key.join(JOIN) : nil
109
116
  end
110
117
  end
111
118
  end
@@ -31,7 +31,7 @@ module ActionView
31
31
 
32
32
  module ObjectRendering # :nodoc:
33
33
  PREFIXED_PARTIAL_NAMES = Concurrent::Map.new do |h, k|
34
- h[k] = Concurrent::Map.new
34
+ h.compute_if_absent(k) { Concurrent::Map.new }
35
35
  end
36
36
 
37
37
  def initialize(lookup_context, options)
@@ -51,6 +51,10 @@ module ActionView
51
51
  def length
52
52
  @collection.respond_to?(:length) ? @collection.length : size
53
53
  end
54
+
55
+ def preload!
56
+ # no-op
57
+ end
54
58
  end
55
59
 
56
60
  class SameCollectionIterator < CollectionIterator # :nodoc:
@@ -84,9 +88,13 @@ module ActionView
84
88
 
85
89
  def each_with_info
86
90
  return super unless block_given?
87
- @relation.preload_associations(@collection)
91
+ preload!
88
92
  super
89
93
  end
94
+
95
+ def preload!
96
+ @relation.preload_associations(@collection)
97
+ end
90
98
  end
91
99
 
92
100
  class MixedCollectionIterator < CollectionIterator # :nodoc:
@@ -59,6 +59,7 @@ module ActionView
59
59
  seed = callable_cache_key? ? @options[:cached] : ->(i) { i }
60
60
 
61
61
  digest_path = view.digest_path_from_template(template)
62
+ collection.preload! if callable_cache_key?
62
63
 
63
64
  collection.each_with_object([{}, []]) do |item, (hash, ordered_keys)|
64
65
  key = expanded_cache_key(seed.call(item), view, template, digest_path)
@@ -88,15 +89,32 @@ module ActionView
88
89
  # If the partial is not already cached it will also be
89
90
  # written back to the underlying cache store.
90
91
  def fetch_or_cache_partial(cached_partials, template, order_by:)
91
- order_by.index_with do |cache_key|
92
+ entries_to_write = {}
93
+
94
+ keyed_partials = order_by.index_with do |cache_key|
92
95
  if content = cached_partials[cache_key]
93
96
  build_rendered_template(content, template)
94
97
  else
95
- yield.tap do |rendered_partial|
96
- collection_cache.write(cache_key, rendered_partial.body)
98
+ rendered_partial = yield
99
+ body = rendered_partial.body
100
+
101
+ # We want to cache buffers as raw strings. This both improve performance and
102
+ # avoid creating forward compatibility issues with the internal representation
103
+ # of these two types.
104
+ if body.is_a?(ActionView::OutputBuffer) || body.is_a?(ActiveSupport::SafeBuffer)
105
+ body = body.to_str
97
106
  end
107
+
108
+ entries_to_write[cache_key] = body
109
+ rendered_partial
98
110
  end
99
111
  end
112
+
113
+ unless entries_to_write.empty?
114
+ collection_cache.write_multi(entries_to_write)
115
+ end
116
+
117
+ keyed_partials
100
118
  end
101
119
  end
102
120
  end
@@ -27,7 +27,7 @@ module ActionView
27
27
  # This would first render <tt>advertiser/_account.html.erb</tt> with <tt>@buyer</tt> passed in as the local variable +account+, then
28
28
  # render <tt>advertiser/_ad.html.erb</tt> and pass the local variable +ad+ to the template for display.
29
29
  #
30
- # == The :as and :object options
30
+ # == The +:as+ and +:object+ options
31
31
  #
32
32
  # By default ActionView::PartialRenderer doesn't have any local variables.
33
33
  # The <tt>:object</tt> option can be used to pass an object to the partial. For instance:
@@ -246,7 +246,8 @@ module ActionView
246
246
  ActiveSupport::Notifications.instrument(
247
247
  "render_partial.action_view",
248
248
  identifier: template.identifier,
249
- layout: layout && layout.virtual_path
249
+ layout: layout && layout.virtual_path,
250
+ locals: locals
250
251
  ) do |payload|
251
252
  content = template.render(view, locals, add_to_stack: !block) do |*name|
252
253
  view._layout_for(*name, &block)
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionView
4
+ # = Action View \Renderer
5
+ #
4
6
  # This is the main entry point for rendering. It basically delegates
5
7
  # to other objects like TemplateRenderer and PartialRenderer which
6
8
  # actually renders the template.
@@ -46,7 +46,7 @@ module ActionView
46
46
  return [super.body] unless layout_name && template.supports_streaming?
47
47
 
48
48
  locals ||= {}
49
- layout = layout_name && find_layout(layout_name, locals.keys, [formats.first])
49
+ layout = find_layout(layout_name, locals.keys, [formats.first])
50
50
 
51
51
  Body.new do |buffer|
52
52
  delayed_render(buffer, template, layout, view, locals)
@@ -65,7 +65,8 @@ module ActionView
65
65
  ActiveSupport::Notifications.instrument(
66
66
  "render_template.action_view",
67
67
  identifier: template.identifier,
68
- layout: layout && layout.virtual_path
68
+ layout: layout && layout.virtual_path,
69
+ locals: locals
69
70
  ) do
70
71
  outer_config = I18n.config
71
72
  fiber = Fiber.new do
@@ -49,7 +49,7 @@ module ActionView
49
49
  @lookup_context.find_template(options[:template], options[:prefixes], false, keys, @details)
50
50
  end
51
51
  else
52
- raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :plain, :html or :body option."
52
+ raise ArgumentError, "You invoked render but did not give any of :body, :file, :html, :inline, :partial, :plain, :renderable, or :template option."
53
53
  end
54
54
  end
55
55
 
@@ -60,7 +60,8 @@ module ActionView
60
60
  ActiveSupport::Notifications.instrument(
61
61
  "render_template.action_view",
62
62
  identifier: template.identifier,
63
- layout: layout && layout.virtual_path
63
+ layout: layout && layout.virtual_path,
64
+ locals: locals
64
65
  ) do
65
66
  template.render(view, locals) { |*name| view._layout_for(*name) }
66
67
  end
@@ -10,7 +10,8 @@ module ActionView
10
10
 
11
11
  def initialize(original_config, lookup_context)
12
12
  original_config = original_config.original_config if original_config.respond_to?(:original_config)
13
- @original_config, @lookup_context = original_config, lookup_context
13
+ @original_config = original_config
14
+ @lookup_context = lookup_context
14
15
  end
15
16
 
16
17
  def locale
@@ -33,8 +34,8 @@ module ActionView
33
34
  super
34
35
  end
35
36
 
36
- # Overwrite process to set up I18n proxy.
37
- def process(*) # :nodoc:
37
+ # Override process to set up I18n proxy.
38
+ def process(...) # :nodoc:
38
39
  old_config, I18n.config = I18n.config, I18nProxy.new(I18n.config, lookup_context)
39
40
  super
40
41
  ensure
@@ -48,7 +49,18 @@ module ActionView
48
49
  def _helpers
49
50
  end
50
51
 
52
+ def inherit_view_context_class?
53
+ superclass.respond_to?(:view_context_class) &&
54
+ supports_path? == superclass.supports_path? &&
55
+ _routes.equal?(superclass._routes) &&
56
+ _helpers.equal?(superclass._helpers)
57
+ end
58
+
51
59
  def build_view_context_class(klass, supports_path, routes, helpers)
60
+ if inherit_view_context_class?
61
+ return superclass.view_context_class
62
+ end
63
+
52
64
  Class.new(klass) do
53
65
  if routes
54
66
  include routes.url_helpers(supports_path)
@@ -61,8 +73,14 @@ module ActionView
61
73
  end
62
74
  end
63
75
 
76
+ def eager_load!
77
+ super
78
+ view_context_class
79
+ nil
80
+ end
81
+
64
82
  def view_context_class
65
- klass = ActionView::LookupContext::DetailsKey.view_context_class(ActionView::Base)
83
+ klass = ActionView::LookupContext::DetailsKey.view_context_class
66
84
 
67
85
  @view_context_class ||= build_view_context_class(klass, supports_path?, _routes, _helpers)
68
86
 
@@ -129,8 +147,8 @@ module ActionView
129
147
  lookup_context.formats = [format.to_sym] if format.to_sym
130
148
  end
131
149
 
132
- # Normalize args by converting render "foo" to render :action => "foo" and
133
- # render "foo/bar" to render :template => "foo/bar".
150
+ # Normalize args by converting render "foo" to render action: "foo" and
151
+ # render "foo/bar" to render template: "foo/bar".
134
152
  def _normalize_args(action = nil, options = {})
135
153
  options = super(action, options)
136
154
  case action