actionview 5.2.3 → 6.0.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of actionview might be problematic. Click here for more details.

Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +203 -67
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +4 -2
  5. data/lib/action_view.rb +3 -2
  6. data/lib/action_view/base.rb +107 -10
  7. data/lib/action_view/buffers.rb +15 -0
  8. data/lib/action_view/cache_expiry.rb +54 -0
  9. data/lib/action_view/context.rb +5 -9
  10. data/lib/action_view/digestor.rb +12 -20
  11. data/lib/action_view/gem_version.rb +3 -3
  12. data/lib/action_view/helpers.rb +0 -2
  13. data/lib/action_view/helpers/asset_tag_helper.rb +7 -30
  14. data/lib/action_view/helpers/asset_url_helper.rb +4 -3
  15. data/lib/action_view/helpers/cache_helper.rb +18 -10
  16. data/lib/action_view/helpers/capture_helper.rb +4 -0
  17. data/lib/action_view/helpers/csp_helper.rb +4 -2
  18. data/lib/action_view/helpers/csrf_helper.rb +1 -1
  19. data/lib/action_view/helpers/date_helper.rb +69 -25
  20. data/lib/action_view/helpers/form_helper.rb +240 -8
  21. data/lib/action_view/helpers/form_options_helper.rb +27 -18
  22. data/lib/action_view/helpers/form_tag_helper.rb +14 -9
  23. data/lib/action_view/helpers/javascript_helper.rb +9 -8
  24. data/lib/action_view/helpers/number_helper.rb +5 -0
  25. data/lib/action_view/helpers/output_safety_helper.rb +1 -1
  26. data/lib/action_view/helpers/rendering_helper.rb +6 -4
  27. data/lib/action_view/helpers/sanitize_helper.rb +12 -18
  28. data/lib/action_view/helpers/tag_helper.rb +7 -6
  29. data/lib/action_view/helpers/tags/base.rb +9 -5
  30. data/lib/action_view/helpers/tags/color_field.rb +1 -1
  31. data/lib/action_view/helpers/tags/translator.rb +1 -6
  32. data/lib/action_view/helpers/text_helper.rb +3 -3
  33. data/lib/action_view/helpers/translation_helper.rb +16 -12
  34. data/lib/action_view/helpers/url_helper.rb +15 -15
  35. data/lib/action_view/layouts.rb +5 -5
  36. data/lib/action_view/log_subscriber.rb +6 -6
  37. data/lib/action_view/lookup_context.rb +73 -31
  38. data/lib/action_view/path_set.rb +5 -10
  39. data/lib/action_view/railtie.rb +24 -1
  40. data/lib/action_view/record_identifier.rb +2 -2
  41. data/lib/action_view/renderer/abstract_renderer.rb +56 -3
  42. data/lib/action_view/renderer/partial_renderer.rb +66 -55
  43. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +62 -16
  44. data/lib/action_view/renderer/renderer.rb +16 -4
  45. data/lib/action_view/renderer/streaming_template_renderer.rb +5 -5
  46. data/lib/action_view/renderer/template_renderer.rb +24 -18
  47. data/lib/action_view/rendering.rb +51 -31
  48. data/lib/action_view/routing_url_for.rb +12 -11
  49. data/lib/action_view/template.rb +102 -70
  50. data/lib/action_view/template/error.rb +21 -1
  51. data/lib/action_view/template/handlers.rb +27 -1
  52. data/lib/action_view/template/handlers/builder.rb +2 -2
  53. data/lib/action_view/template/handlers/erb.rb +17 -7
  54. data/lib/action_view/template/handlers/erb/erubi.rb +7 -3
  55. data/lib/action_view/template/handlers/html.rb +1 -1
  56. data/lib/action_view/template/handlers/raw.rb +2 -2
  57. data/lib/action_view/template/html.rb +14 -5
  58. data/lib/action_view/template/inline.rb +22 -0
  59. data/lib/action_view/template/raw_file.rb +28 -0
  60. data/lib/action_view/template/resolver.rb +136 -133
  61. data/lib/action_view/template/sources.rb +13 -0
  62. data/lib/action_view/template/sources/file.rb +17 -0
  63. data/lib/action_view/template/text.rb +5 -3
  64. data/lib/action_view/test_case.rb +1 -1
  65. data/lib/action_view/testing/resolvers.rb +33 -20
  66. data/lib/action_view/unbound_template.rb +32 -0
  67. data/lib/action_view/view_paths.rb +25 -1
  68. data/lib/assets/compiled/rails-ujs.js +33 -7
  69. metadata +26 -18
  70. data/lib/action_view/helpers/record_tag_helper.rb +0 -23
@@ -59,8 +59,8 @@ module ActionView
59
59
 
60
60
  include ModelNaming
61
61
 
62
- JOIN = "_".freeze
63
- NEW = "new".freeze
62
+ JOIN = "_"
63
+ NEW = "new"
64
64
 
65
65
  # The DOM class convention is to use the singular form of an object or class.
66
66
  #
@@ -17,7 +17,7 @@ module ActionView
17
17
  # that new object is called in turn. This abstracts the setup and rendering
18
18
  # into a separate classes for partials and templates.
19
19
  class AbstractRenderer #:nodoc:
20
- delegate :find_template, :find_file, :template_exists?, :any_templates?, :with_fallbacks, :with_layout_format, :formats, to: :@lookup_context
20
+ delegate :template_exists?, :any_templates?, :formats, to: :@lookup_context
21
21
 
22
22
  def initialize(lookup_context)
23
23
  @lookup_context = lookup_context
@@ -27,6 +27,53 @@ module ActionView
27
27
  raise NotImplementedError
28
28
  end
29
29
 
30
+ class RenderedCollection # :nodoc:
31
+ def self.empty(format)
32
+ EmptyCollection.new format
33
+ end
34
+
35
+ attr_reader :rendered_templates
36
+
37
+ def initialize(rendered_templates, spacer)
38
+ @rendered_templates = rendered_templates
39
+ @spacer = spacer
40
+ end
41
+
42
+ def body
43
+ @rendered_templates.map(&:body).join(@spacer.body).html_safe
44
+ end
45
+
46
+ def format
47
+ rendered_templates.first.format
48
+ end
49
+
50
+ class EmptyCollection
51
+ attr_reader :format
52
+
53
+ def initialize(format)
54
+ @format = format
55
+ end
56
+
57
+ def body; nil; end
58
+ end
59
+ end
60
+
61
+ class RenderedTemplate # :nodoc:
62
+ attr_reader :body, :layout, :template
63
+
64
+ def initialize(body, layout, template)
65
+ @body = body
66
+ @layout = layout
67
+ @template = template
68
+ end
69
+
70
+ def format
71
+ template.format
72
+ end
73
+
74
+ EMPTY_SPACER = Struct.new(:body).new
75
+ end
76
+
30
77
  private
31
78
 
32
79
  def extract_details(options) # :doc:
@@ -38,8 +85,6 @@ module ActionView
38
85
  end
39
86
 
40
87
  def instrument(name, **options) # :doc:
41
- options[:identifier] ||= (@template && @template.identifier) || @path
42
-
43
88
  ActiveSupport::Notifications.instrument("render_#{name}.action_view", options) do |payload|
44
89
  yield payload
45
90
  end
@@ -51,5 +96,13 @@ module ActionView
51
96
 
52
97
  @lookup_context.formats = formats | @lookup_context.formats
53
98
  end
99
+
100
+ def build_rendered_template(content, template, layout = nil)
101
+ RenderedTemplate.new content, layout, template
102
+ end
103
+
104
+ def build_rendered_collection(templates, spacer)
105
+ RenderedCollection.new templates, spacer
106
+ end
54
107
  end
55
108
  end
@@ -105,9 +105,6 @@ module ActionView
105
105
  #
106
106
  # <%= render(partial: "ad", collection: @advertisements) || "There's no ad to be displayed" %>
107
107
  #
108
- # NOTE: Due to backwards compatibility concerns, the collection can't be one of hashes. Normally you'd also
109
- # just keep domain objects, like Active Records, in there.
110
- #
111
108
  # == \Rendering shared partials
112
109
  #
113
110
  # Two controllers can share a set of partials and render them like this:
@@ -295,43 +292,60 @@ module ActionView
295
292
  end
296
293
 
297
294
  def render(context, options, block)
298
- setup(context, options, block)
299
- @template = find_partial
295
+ as = as_variable(options)
296
+ setup(context, options, as, block)
300
297
 
301
- @lookup_context.rendered_format ||= begin
302
- if @template && @template.formats.present?
303
- @template.formats.first
298
+ if @path
299
+ if @has_object || @collection
300
+ @variable, @variable_counter, @variable_iteration = retrieve_variable(@path, as)
301
+ @template_keys = retrieve_template_keys(@variable)
304
302
  else
305
- formats.first
303
+ @template_keys = @locals.keys
306
304
  end
305
+ template = find_partial(@path, @template_keys)
306
+ @variable ||= template.variable
307
+ else
308
+ if options[:cached]
309
+ raise NotImplementedError, "render caching requires a template. Please specify a partial when rendering"
310
+ end
311
+ template = nil
307
312
  end
308
313
 
309
314
  if @collection
310
- render_collection
315
+ render_collection(context, template)
311
316
  else
312
- render_partial
317
+ render_partial(context, template)
313
318
  end
314
319
  end
315
320
 
316
321
  private
317
322
 
318
- def render_collection
319
- instrument(:collection, count: @collection.size) do |payload|
320
- return nil if @collection.blank?
323
+ def render_collection(view, template)
324
+ identifier = (template && template.identifier) || @path
325
+ instrument(:collection, identifier: identifier, count: @collection.size) do |payload|
326
+ return RenderedCollection.empty(@lookup_context.formats.first) if @collection.blank?
321
327
 
322
- if @options.key?(:spacer_template)
323
- spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals)
328
+ spacer = if @options.key?(:spacer_template)
329
+ spacer_template = find_template(@options[:spacer_template], @locals.keys)
330
+ build_rendered_template(spacer_template.render(view, @locals), spacer_template)
331
+ else
332
+ RenderedTemplate::EMPTY_SPACER
324
333
  end
325
334
 
326
- cache_collection_render(payload) do
327
- @template ? collection_with_template : collection_without_template
328
- end.join(spacer).html_safe
335
+ collection_body = if template
336
+ cache_collection_render(payload, view, template) do
337
+ collection_with_template(view, template)
338
+ end
339
+ else
340
+ collection_without_template(view)
341
+ end
342
+ build_rendered_collection(collection_body, spacer)
329
343
  end
330
344
  end
331
345
 
332
- def render_partial
333
- instrument(:partial) do |payload|
334
- view, locals, block = @view, @locals, @block
346
+ def render_partial(view, template)
347
+ instrument(:partial, identifier: template.identifier) do |payload|
348
+ locals, block = @locals, @block
335
349
  object, as = @object, @variable
336
350
 
337
351
  if !block && (layout = @options[:layout])
@@ -341,13 +355,13 @@ module ActionView
341
355
  object = locals[as] if object.nil? # Respect object when object is false
342
356
  locals[as] = object if @has_object
343
357
 
344
- content = @template.render(view, locals) do |*name|
358
+ content = template.render(view, locals) do |*name|
345
359
  view._layout_for(*name, &block)
346
360
  end
347
361
 
348
362
  content = layout.render(view, locals) { content } if layout
349
- payload[:cache_hit] = view.view_renderer.cache_hits[@template.virtual_path]
350
- content
363
+ payload[:cache_hit] = view.view_renderer.cache_hits[template.virtual_path]
364
+ build_rendered_template(content, template, layout)
351
365
  end
352
366
  end
353
367
 
@@ -358,16 +372,13 @@ module ActionView
358
372
  # If +options[:partial]+ is a string, then the <tt>@path</tt> instance variable is
359
373
  # set to that string. Otherwise, the +options[:partial]+ object must
360
374
  # respond to +to_partial_path+ in order to setup the path.
361
- def setup(context, options, block)
362
- @view = context
375
+ def setup(context, options, as, block)
363
376
  @options = options
364
377
  @block = block
365
378
 
366
379
  @locals = options[:locals] || {}
367
380
  @details = extract_details(options)
368
381
 
369
- prepend_formats(options[:formats])
370
-
371
382
  partial = options[:partial]
372
383
 
373
384
  if String === partial
@@ -381,26 +392,26 @@ module ActionView
381
392
  @collection = collection_from_object || collection_from_options
382
393
 
383
394
  if @collection
384
- paths = @collection_data = @collection.map { |o| partial_path(o) }
385
- @path = paths.uniq.one? ? paths.first : nil
395
+ paths = @collection_data = @collection.map { |o| partial_path(o, context) }
396
+ if paths.uniq.length == 1
397
+ @path = paths.first
398
+ else
399
+ paths.map! { |path| retrieve_variable(path, as).unshift(path) }
400
+ @path = nil
401
+ end
386
402
  else
387
- @path = partial_path
403
+ @path = partial_path(@object, context)
388
404
  end
389
405
  end
390
406
 
407
+ self
408
+ end
409
+
410
+ def as_variable(options)
391
411
  if as = options[:as]
392
412
  raise_invalid_option_as(as) unless /\A[a-z_]\w*\z/.match?(as.to_s)
393
- as = as.to_sym
394
- end
395
-
396
- if @path
397
- @variable, @variable_counter, @variable_iteration = retrieve_variable(@path, as)
398
- @template_keys = retrieve_template_keys
399
- else
400
- paths.map! { |path| retrieve_variable(path, as).unshift(path) }
413
+ as.to_sym
401
414
  end
402
-
403
- self
404
415
  end
405
416
 
406
417
  def collection_from_options
@@ -414,8 +425,8 @@ module ActionView
414
425
  @object.to_ary if @object.respond_to?(:to_ary)
415
426
  end
416
427
 
417
- def find_partial
418
- find_template(@path, @template_keys) if @path
428
+ def find_partial(path, template_keys)
429
+ find_template(path, template_keys)
419
430
  end
420
431
 
421
432
  def find_template(path, locals)
@@ -423,8 +434,8 @@ module ActionView
423
434
  @lookup_context.find_template(path, prefixes, true, locals, @details)
424
435
  end
425
436
 
426
- def collection_with_template
427
- view, locals, template = @view, @locals, @template
437
+ def collection_with_template(view, template)
438
+ locals = @locals
428
439
  as, counter, iteration = @variable, @variable_counter, @variable_iteration
429
440
 
430
441
  if layout = @options[:layout]
@@ -441,12 +452,12 @@ module ActionView
441
452
  content = template.render(view, locals)
442
453
  content = layout.render(view, locals) { content } if layout
443
454
  partial_iteration.iterate!
444
- content
455
+ build_rendered_template(content, template, layout)
445
456
  end
446
457
  end
447
458
 
448
- def collection_without_template
449
- view, locals, collection_data = @view, @locals, @collection_data
459
+ def collection_without_template(view)
460
+ locals, collection_data = @locals, @collection_data
450
461
  cache = {}
451
462
  keys = @locals.keys
452
463
 
@@ -463,7 +474,7 @@ module ActionView
463
474
  template = (cache[path] ||= find_template(path, keys + [as, counter, iteration]))
464
475
  content = template.render(view, locals)
465
476
  partial_iteration.iterate!
466
- content
477
+ build_rendered_template(content, template)
467
478
  end
468
479
  end
469
480
 
@@ -474,7 +485,7 @@ module ActionView
474
485
  #
475
486
  # If +prefix_partial_path_with_controller_namespace+ is true, then this
476
487
  # method will prefix the partial paths with a namespace.
477
- def partial_path(object = @object)
488
+ def partial_path(object, view)
478
489
  object = object.to_model if object.respond_to?(:to_model)
479
490
 
480
491
  path = if object.respond_to?(:to_partial_path)
@@ -483,7 +494,7 @@ module ActionView
483
494
  raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.")
484
495
  end
485
496
 
486
- if @view.prefix_partial_path_with_controller_namespace
497
+ if view.prefix_partial_path_with_controller_namespace
487
498
  prefixed_partial_names[path] ||= merge_prefix_into_object_path(@context_prefix, path.dup)
488
499
  else
489
500
  path
@@ -511,9 +522,9 @@ module ActionView
511
522
  end
512
523
  end
513
524
 
514
- def retrieve_template_keys
525
+ def retrieve_template_keys(variable)
515
526
  keys = @locals.keys
516
- keys << @variable if @has_object || @collection
527
+ keys << variable
517
528
  if @collection
518
529
  keys << @variable_counter
519
530
  keys << @variable_iteration
@@ -523,7 +534,7 @@ module ActionView
523
534
 
524
535
  def retrieve_variable(path, as)
525
536
  variable = as || begin
526
- base = path[-1] == "/".freeze ? "".freeze : File.basename(path)
537
+ base = path[-1] == "/" ? "" : File.basename(path)
527
538
  raise_invalid_identifier(path) unless base =~ /\A_?(.*?)(?:\.\w+)*\z/
528
539
  $1.to_sym
529
540
  end
@@ -11,47 +11,93 @@ module ActionView
11
11
  end
12
12
 
13
13
  private
14
- def cache_collection_render(instrumentation_payload)
14
+ def cache_collection_render(instrumentation_payload, view, template)
15
15
  return yield unless @options[:cached]
16
16
 
17
- keyed_collection = collection_by_cache_keys
18
- cached_partials = collection_cache.read_multi(*keyed_collection.keys)
17
+ # Result is a hash with the key represents the
18
+ # key used for cache lookup and the value is the item
19
+ # on which the partial is being rendered
20
+ keyed_collection, ordered_keys = collection_by_cache_keys(view, template)
21
+
22
+ # Pull all partials from cache
23
+ # Result is a hash, key matches the entry in
24
+ # `keyed_collection` where the cache was retrieved and the
25
+ # value is the value that was present in the cache
26
+ cached_partials = collection_cache.read_multi(*keyed_collection.keys)
19
27
  instrumentation_payload[:cache_hits] = cached_partials.size
20
28
 
29
+ # Extract the items for the keys that are not found
30
+ # Set the uncached values to instance variable @collection
31
+ # which is used by the caller
21
32
  @collection = keyed_collection.reject { |key, _| cached_partials.key?(key) }.values
33
+
34
+ # If all elements are already in cache then
35
+ # rendered partials will be an empty array
36
+ #
37
+ # If the cache is missing elements then
38
+ # the block will be called against the remaining items
39
+ # in the @collection.
22
40
  rendered_partials = @collection.empty? ? [] : yield
23
41
 
24
42
  index = 0
25
- fetch_or_cache_partial(cached_partials, order_by: keyed_collection.each_key) do
43
+ keyed_partials = fetch_or_cache_partial(cached_partials, template, order_by: keyed_collection.each_key) do
44
+ # This block is called once
45
+ # for every cache miss while preserving order.
26
46
  rendered_partials[index].tap { index += 1 }
27
47
  end
48
+
49
+ ordered_keys.map do |key|
50
+ keyed_partials[key]
51
+ end
28
52
  end
29
53
 
30
54
  def callable_cache_key?
31
55
  @options[:cached].respond_to?(:call)
32
56
  end
33
57
 
34
- def collection_by_cache_keys
58
+ def collection_by_cache_keys(view, template)
35
59
  seed = callable_cache_key? ? @options[:cached] : ->(i) { i }
36
60
 
37
- @collection.each_with_object({}) do |item, hash|
38
- hash[expanded_cache_key(seed.call(item))] = item
61
+ digest_path = view.digest_path_from_template(template)
62
+
63
+ @collection.each_with_object([{}, []]) do |item, (hash, ordered_keys)|
64
+ key = expanded_cache_key(seed.call(item), view, template, digest_path)
65
+ ordered_keys << key
66
+ hash[key] = item
39
67
  end
40
68
  end
41
69
 
42
- def expanded_cache_key(key)
43
- key = @view.combined_fragment_cache_key(@view.cache_fragment_name(key, virtual_path: @template.virtual_path))
70
+ def expanded_cache_key(key, view, template, digest_path)
71
+ key = view.combined_fragment_cache_key(view.cache_fragment_name(key, virtual_path: template.virtual_path, digest_path: digest_path))
44
72
  key.frozen? ? key.dup : key # #read_multi & #write may require mutability, Dalli 2.6.0.
45
73
  end
46
74
 
47
- def fetch_or_cache_partial(cached_partials, order_by:)
48
- order_by.map do |cache_key|
49
- cached_partials.fetch(cache_key) do
50
- yield.tap do |rendered_partial|
51
- collection_cache.write(cache_key, rendered_partial)
52
- end
75
+ # `order_by` is an enumerable object containing keys of the cache,
76
+ # all keys are passed in whether found already or not.
77
+ #
78
+ # `cached_partials` is a hash. If the value exists
79
+ # it represents the rendered partial from the cache
80
+ # otherwise `Hash#fetch` will take the value of its block.
81
+ #
82
+ # This method expects a block that will return the rendered
83
+ # partial. An example is to render all results
84
+ # for each element that was not found in the cache and store it as an array.
85
+ # Order it so that the first empty cache element in `cached_partials`
86
+ # corresponds to the first element in `rendered_partials`.
87
+ #
88
+ # If the partial is not already cached it will also be
89
+ # written back to the underlying cache store.
90
+ def fetch_or_cache_partial(cached_partials, template, order_by:)
91
+ order_by.each_with_object({}) do |cache_key, hash|
92
+ hash[cache_key] =
93
+ if content = cached_partials[cache_key]
94
+ build_rendered_template(content, template)
95
+ else
96
+ yield.tap do |rendered_partial|
97
+ collection_cache.write(cache_key, rendered_partial.body)
98
+ end
99
+ end
53
100
  end
54
- end
55
101
  end
56
102
  end
57
103
  end