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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +203 -67
- data/MIT-LICENSE +1 -1
- data/README.rdoc +4 -2
- data/lib/action_view.rb +3 -2
- data/lib/action_view/base.rb +107 -10
- data/lib/action_view/buffers.rb +15 -0
- data/lib/action_view/cache_expiry.rb +54 -0
- data/lib/action_view/context.rb +5 -9
- data/lib/action_view/digestor.rb +12 -20
- data/lib/action_view/gem_version.rb +3 -3
- data/lib/action_view/helpers.rb +0 -2
- data/lib/action_view/helpers/asset_tag_helper.rb +7 -30
- data/lib/action_view/helpers/asset_url_helper.rb +4 -3
- data/lib/action_view/helpers/cache_helper.rb +18 -10
- data/lib/action_view/helpers/capture_helper.rb +4 -0
- data/lib/action_view/helpers/csp_helper.rb +4 -2
- data/lib/action_view/helpers/csrf_helper.rb +1 -1
- data/lib/action_view/helpers/date_helper.rb +69 -25
- data/lib/action_view/helpers/form_helper.rb +240 -8
- data/lib/action_view/helpers/form_options_helper.rb +27 -18
- data/lib/action_view/helpers/form_tag_helper.rb +14 -9
- data/lib/action_view/helpers/javascript_helper.rb +9 -8
- data/lib/action_view/helpers/number_helper.rb +5 -0
- data/lib/action_view/helpers/output_safety_helper.rb +1 -1
- data/lib/action_view/helpers/rendering_helper.rb +6 -4
- data/lib/action_view/helpers/sanitize_helper.rb +12 -18
- data/lib/action_view/helpers/tag_helper.rb +7 -6
- data/lib/action_view/helpers/tags/base.rb +9 -5
- data/lib/action_view/helpers/tags/color_field.rb +1 -1
- data/lib/action_view/helpers/tags/translator.rb +1 -6
- data/lib/action_view/helpers/text_helper.rb +3 -3
- data/lib/action_view/helpers/translation_helper.rb +16 -12
- data/lib/action_view/helpers/url_helper.rb +15 -15
- data/lib/action_view/layouts.rb +5 -5
- data/lib/action_view/log_subscriber.rb +6 -6
- data/lib/action_view/lookup_context.rb +73 -31
- data/lib/action_view/path_set.rb +5 -10
- data/lib/action_view/railtie.rb +24 -1
- data/lib/action_view/record_identifier.rb +2 -2
- data/lib/action_view/renderer/abstract_renderer.rb +56 -3
- data/lib/action_view/renderer/partial_renderer.rb +66 -55
- data/lib/action_view/renderer/partial_renderer/collection_caching.rb +62 -16
- data/lib/action_view/renderer/renderer.rb +16 -4
- data/lib/action_view/renderer/streaming_template_renderer.rb +5 -5
- data/lib/action_view/renderer/template_renderer.rb +24 -18
- data/lib/action_view/rendering.rb +51 -31
- data/lib/action_view/routing_url_for.rb +12 -11
- data/lib/action_view/template.rb +102 -70
- data/lib/action_view/template/error.rb +21 -1
- data/lib/action_view/template/handlers.rb +27 -1
- data/lib/action_view/template/handlers/builder.rb +2 -2
- data/lib/action_view/template/handlers/erb.rb +17 -7
- data/lib/action_view/template/handlers/erb/erubi.rb +7 -3
- data/lib/action_view/template/handlers/html.rb +1 -1
- data/lib/action_view/template/handlers/raw.rb +2 -2
- data/lib/action_view/template/html.rb +14 -5
- data/lib/action_view/template/inline.rb +22 -0
- data/lib/action_view/template/raw_file.rb +28 -0
- data/lib/action_view/template/resolver.rb +136 -133
- data/lib/action_view/template/sources.rb +13 -0
- data/lib/action_view/template/sources/file.rb +17 -0
- data/lib/action_view/template/text.rb +5 -3
- data/lib/action_view/test_case.rb +1 -1
- data/lib/action_view/testing/resolvers.rb +33 -20
- data/lib/action_view/unbound_template.rb +32 -0
- data/lib/action_view/view_paths.rb +25 -1
- data/lib/assets/compiled/rails-ujs.js +33 -7
- metadata +26 -18
- data/lib/action_view/helpers/record_tag_helper.rb +0 -23
@@ -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 :
|
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
|
-
|
299
|
-
|
295
|
+
as = as_variable(options)
|
296
|
+
setup(context, options, as, block)
|
300
297
|
|
301
|
-
@
|
302
|
-
if @
|
303
|
-
@
|
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
|
-
|
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
|
-
|
320
|
-
|
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
|
-
|
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
|
-
|
327
|
-
|
328
|
-
|
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
|
-
|
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 =
|
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[
|
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
|
-
|
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
|
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(
|
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
|
-
|
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
|
-
|
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
|
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
|
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 <<
|
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] == "/"
|
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
|
-
|
18
|
-
|
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
|
-
|
38
|
-
|
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 =
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|