actionview 4.2.11.1 → 6.1.5
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 +232 -186
- data/MIT-LICENSE +1 -2
- data/README.rdoc +9 -8
- data/lib/action_view/base.rb +115 -39
- data/lib/action_view/buffers.rb +18 -1
- data/lib/action_view/cache_expiry.rb +52 -0
- data/lib/action_view/context.rb +8 -12
- data/lib/action_view/dependency_tracker.rb +61 -21
- data/lib/action_view/digestor.rb +89 -85
- data/lib/action_view/flows.rb +11 -12
- data/lib/action_view/gem_version.rb +6 -4
- data/lib/action_view/helpers/active_model_helper.rb +16 -11
- data/lib/action_view/helpers/asset_tag_helper.rb +282 -83
- data/lib/action_view/helpers/asset_url_helper.rb +175 -69
- data/lib/action_view/helpers/atom_feed_helper.rb +20 -17
- data/lib/action_view/helpers/cache_helper.rb +107 -43
- data/lib/action_view/helpers/capture_helper.rb +20 -13
- data/lib/action_view/helpers/controller_helper.rb +15 -4
- data/lib/action_view/helpers/csp_helper.rb +26 -0
- data/lib/action_view/helpers/csrf_helper.rb +8 -6
- data/lib/action_view/helpers/date_helper.rb +232 -130
- data/lib/action_view/helpers/debug_helper.rb +7 -6
- data/lib/action_view/helpers/form_helper.rb +808 -146
- data/lib/action_view/helpers/form_options_helper.rb +124 -78
- data/lib/action_view/helpers/form_tag_helper.rb +120 -74
- data/lib/action_view/helpers/javascript_helper.rb +33 -17
- data/lib/action_view/helpers/number_helper.rb +87 -62
- data/lib/action_view/helpers/output_safety_helper.rb +36 -4
- data/lib/action_view/helpers/rendering_helper.rb +21 -10
- data/lib/action_view/helpers/sanitize_helper.rb +30 -31
- data/lib/action_view/helpers/tag_helper.rb +269 -68
- data/lib/action_view/helpers/tags/base.rb +141 -97
- data/lib/action_view/helpers/tags/check_box.rb +20 -19
- data/lib/action_view/helpers/tags/checkable.rb +4 -2
- data/lib/action_view/helpers/tags/collection_check_boxes.rb +12 -34
- data/lib/action_view/helpers/tags/collection_helpers.rb +69 -36
- data/lib/action_view/helpers/tags/collection_radio_buttons.rb +6 -12
- data/lib/action_view/helpers/tags/collection_select.rb +4 -2
- data/lib/action_view/helpers/tags/color_field.rb +4 -3
- data/lib/action_view/helpers/tags/date_field.rb +3 -2
- data/lib/action_view/helpers/tags/date_select.rb +38 -37
- data/lib/action_view/helpers/tags/datetime_field.rb +4 -3
- data/lib/action_view/helpers/tags/datetime_local_field.rb +3 -2
- data/lib/action_view/helpers/tags/datetime_select.rb +2 -0
- data/lib/action_view/helpers/tags/email_field.rb +2 -0
- data/lib/action_view/helpers/tags/file_field.rb +2 -0
- data/lib/action_view/helpers/tags/grouped_collection_select.rb +4 -2
- data/lib/action_view/helpers/tags/hidden_field.rb +6 -0
- data/lib/action_view/helpers/tags/label.rb +7 -2
- data/lib/action_view/helpers/tags/month_field.rb +3 -2
- data/lib/action_view/helpers/tags/number_field.rb +2 -0
- data/lib/action_view/helpers/tags/password_field.rb +3 -1
- data/lib/action_view/helpers/tags/placeholderable.rb +3 -1
- data/lib/action_view/helpers/tags/radio_button.rb +7 -6
- data/lib/action_view/helpers/tags/range_field.rb +2 -0
- data/lib/action_view/helpers/tags/search_field.rb +14 -9
- data/lib/action_view/helpers/tags/select.rb +11 -10
- data/lib/action_view/helpers/tags/tel_field.rb +2 -0
- data/lib/action_view/helpers/tags/text_area.rb +4 -2
- data/lib/action_view/helpers/tags/text_field.rb +8 -8
- data/lib/action_view/helpers/tags/time_field.rb +3 -2
- data/lib/action_view/helpers/tags/time_select.rb +2 -0
- data/lib/action_view/helpers/tags/time_zone_select.rb +3 -1
- data/lib/action_view/helpers/tags/translator.rb +15 -16
- data/lib/action_view/helpers/tags/url_field.rb +2 -0
- data/lib/action_view/helpers/tags/week_field.rb +3 -2
- data/lib/action_view/helpers/tags.rb +3 -1
- data/lib/action_view/helpers/text_helper.rb +56 -38
- data/lib/action_view/helpers/translation_helper.rb +150 -68
- data/lib/action_view/helpers/url_helper.rb +284 -117
- data/lib/action_view/helpers.rb +5 -3
- data/lib/action_view/layouts.rb +68 -63
- data/lib/action_view/log_subscriber.rb +77 -10
- data/lib/action_view/lookup_context.rb +134 -91
- data/lib/action_view/model_naming.rb +3 -1
- data/lib/action_view/path_set.rb +26 -24
- data/lib/action_view/railtie.rb +62 -13
- data/lib/action_view/record_identifier.rb +53 -26
- data/lib/action_view/renderer/abstract_renderer.rb +151 -14
- data/lib/action_view/renderer/collection_renderer.rb +196 -0
- data/lib/action_view/renderer/object_renderer.rb +34 -0
- data/lib/action_view/renderer/partial_renderer/collection_caching.rb +102 -0
- data/lib/action_view/renderer/partial_renderer.rb +55 -303
- data/lib/action_view/renderer/renderer.rb +66 -9
- data/lib/action_view/renderer/streaming_template_renderer.rb +58 -54
- data/lib/action_view/renderer/template_renderer.rb +82 -73
- data/lib/action_view/rendering.rb +71 -45
- data/lib/action_view/routing_url_for.rb +34 -23
- data/lib/action_view/tasks/cache_digests.rake +25 -0
- data/lib/action_view/template/error.rb +44 -29
- data/lib/action_view/template/handlers/builder.rb +12 -13
- data/lib/action_view/template/handlers/erb/erubi.rb +89 -0
- data/lib/action_view/template/handlers/erb.rb +23 -89
- data/lib/action_view/template/handlers/html.rb +11 -0
- data/lib/action_view/template/handlers/raw.rb +4 -4
- data/lib/action_view/template/handlers.rb +12 -8
- data/lib/action_view/template/html.rb +10 -11
- data/lib/action_view/template/inline.rb +22 -0
- data/lib/action_view/template/raw_file.rb +25 -0
- data/lib/action_view/template/renderable.rb +24 -0
- data/lib/action_view/template/resolver.rb +263 -197
- data/lib/action_view/template/sources/file.rb +17 -0
- data/lib/action_view/template/sources.rb +13 -0
- data/lib/action_view/template/text.rb +8 -10
- data/lib/action_view/template/types.rb +18 -18
- data/lib/action_view/template.rb +108 -92
- data/lib/action_view/test_case.rb +66 -53
- data/lib/action_view/testing/resolvers.rb +24 -33
- data/lib/action_view/unbound_template.rb +31 -0
- data/lib/action_view/version.rb +3 -1
- data/lib/action_view/view_paths.rb +73 -58
- data/lib/action_view.rb +14 -8
- data/lib/assets/compiled/rails-ujs.js +746 -0
- metadata +42 -29
- data/lib/action_view/helpers/record_tag_helper.rb +0 -108
- data/lib/action_view/tasks/dependencies.rake +0 -23
@@ -1,3 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "concurrent/map"
|
4
|
+
|
1
5
|
module ActionView
|
2
6
|
# This class defines the interface for a renderer. Each class that
|
3
7
|
# subclasses +AbstractRenderer+ is used by the base +Renderer+ class to
|
@@ -12,10 +16,10 @@ module ActionView
|
|
12
16
|
#
|
13
17
|
# Whenever the +render+ method is called on the base +Renderer+ class, a new
|
14
18
|
# renderer object of the correct type is created, and the +render+ method on
|
15
|
-
# that new object is called in turn. This abstracts the
|
19
|
+
# that new object is called in turn. This abstracts the set up and rendering
|
16
20
|
# into a separate classes for partials and templates.
|
17
21
|
class AbstractRenderer #:nodoc:
|
18
|
-
delegate :
|
22
|
+
delegate :template_exists?, :any_templates?, :formats, to: :@lookup_context
|
19
23
|
|
20
24
|
def initialize(lookup_context)
|
21
25
|
@lookup_context = lookup_context
|
@@ -25,25 +29,158 @@ module ActionView
|
|
25
29
|
raise NotImplementedError
|
26
30
|
end
|
27
31
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
value = options[key]
|
32
|
+
module ObjectRendering # :nodoc:
|
33
|
+
PREFIXED_PARTIAL_NAMES = Concurrent::Map.new do |h, k|
|
34
|
+
h[k] = Concurrent::Map.new
|
35
|
+
end
|
33
36
|
|
34
|
-
|
37
|
+
def initialize(lookup_context, options)
|
38
|
+
super
|
39
|
+
@context_prefix = lookup_context.prefixes.first
|
35
40
|
end
|
41
|
+
|
42
|
+
private
|
43
|
+
def local_variable(path)
|
44
|
+
if as = @options[:as]
|
45
|
+
raise_invalid_option_as(as) unless /\A[a-z_]\w*\z/.match?(as.to_s)
|
46
|
+
as.to_sym
|
47
|
+
else
|
48
|
+
base = path.end_with?("/") ? "" : File.basename(path)
|
49
|
+
raise_invalid_identifier(path) unless base =~ /\A_?(.*?)(?:\.\w+)*\z/
|
50
|
+
$1.to_sym
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " \
|
55
|
+
"make sure your partial name starts with underscore."
|
56
|
+
|
57
|
+
OPTION_AS_ERROR_MESSAGE = "The value (%s) of the option `as` is not a valid Ruby identifier; " \
|
58
|
+
"make sure it starts with lowercase letter, " \
|
59
|
+
"and is followed by any combination of letters, numbers and underscores."
|
60
|
+
|
61
|
+
def raise_invalid_identifier(path)
|
62
|
+
raise ArgumentError, IDENTIFIER_ERROR_MESSAGE % path
|
63
|
+
end
|
64
|
+
|
65
|
+
def raise_invalid_option_as(as)
|
66
|
+
raise ArgumentError, OPTION_AS_ERROR_MESSAGE % as
|
67
|
+
end
|
68
|
+
|
69
|
+
# Obtains the path to where the object's partial is located. If the object
|
70
|
+
# responds to +to_partial_path+, then +to_partial_path+ will be called and
|
71
|
+
# will provide the path. If the object does not respond to +to_partial_path+,
|
72
|
+
# then an +ArgumentError+ is raised.
|
73
|
+
#
|
74
|
+
# If +prefix_partial_path_with_controller_namespace+ is true, then this
|
75
|
+
# method will prefix the partial paths with a namespace.
|
76
|
+
def partial_path(object, view)
|
77
|
+
object = object.to_model if object.respond_to?(:to_model)
|
78
|
+
|
79
|
+
path = if object.respond_to?(:to_partial_path)
|
80
|
+
object.to_partial_path
|
81
|
+
else
|
82
|
+
raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.")
|
83
|
+
end
|
84
|
+
|
85
|
+
if view.prefix_partial_path_with_controller_namespace
|
86
|
+
PREFIXED_PARTIAL_NAMES[@context_prefix][path] ||= merge_prefix_into_object_path(@context_prefix, path.dup)
|
87
|
+
else
|
88
|
+
path
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def merge_prefix_into_object_path(prefix, object_path)
|
93
|
+
if prefix.include?(?/) && object_path.include?(?/)
|
94
|
+
prefixes = []
|
95
|
+
prefix_array = File.dirname(prefix).split("/")
|
96
|
+
object_path_array = object_path.split("/")[0..-3] # skip model dir & partial
|
97
|
+
|
98
|
+
prefix_array.each_with_index do |dir, index|
|
99
|
+
break if dir == object_path_array[index]
|
100
|
+
prefixes << dir
|
101
|
+
end
|
102
|
+
|
103
|
+
(prefixes << object_path).join("/")
|
104
|
+
else
|
105
|
+
object_path
|
106
|
+
end
|
107
|
+
end
|
36
108
|
end
|
37
109
|
|
38
|
-
|
39
|
-
|
110
|
+
class RenderedCollection # :nodoc:
|
111
|
+
def self.empty(format)
|
112
|
+
EmptyCollection.new format
|
113
|
+
end
|
114
|
+
|
115
|
+
attr_reader :rendered_templates
|
116
|
+
|
117
|
+
def initialize(rendered_templates, spacer)
|
118
|
+
@rendered_templates = rendered_templates
|
119
|
+
@spacer = spacer
|
120
|
+
end
|
121
|
+
|
122
|
+
def body
|
123
|
+
@rendered_templates.map(&:body).join(@spacer.body).html_safe
|
124
|
+
end
|
125
|
+
|
126
|
+
def format
|
127
|
+
rendered_templates.first.format
|
128
|
+
end
|
129
|
+
|
130
|
+
class EmptyCollection
|
131
|
+
attr_reader :format
|
132
|
+
|
133
|
+
def initialize(format)
|
134
|
+
@format = format
|
135
|
+
end
|
136
|
+
|
137
|
+
def body; nil; end
|
138
|
+
end
|
40
139
|
end
|
41
140
|
|
42
|
-
|
43
|
-
|
44
|
-
return if formats.empty? || @lookup_context.html_fallback_for_js
|
141
|
+
class RenderedTemplate # :nodoc:
|
142
|
+
attr_reader :body, :template
|
45
143
|
|
46
|
-
|
144
|
+
def initialize(body, template)
|
145
|
+
@body = body
|
146
|
+
@template = template
|
147
|
+
end
|
148
|
+
|
149
|
+
def format
|
150
|
+
template.format
|
151
|
+
end
|
152
|
+
|
153
|
+
EMPTY_SPACER = Struct.new(:body).new
|
47
154
|
end
|
155
|
+
|
156
|
+
private
|
157
|
+
NO_DETAILS = {}.freeze
|
158
|
+
|
159
|
+
def extract_details(options) # :doc:
|
160
|
+
details = nil
|
161
|
+
@lookup_context.registered_details.each do |key|
|
162
|
+
value = options[key]
|
163
|
+
|
164
|
+
if value
|
165
|
+
(details ||= {})[key] = Array(value)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
details || NO_DETAILS
|
169
|
+
end
|
170
|
+
|
171
|
+
def prepend_formats(formats) # :doc:
|
172
|
+
formats = Array(formats)
|
173
|
+
return if formats.empty? || @lookup_context.html_fallback_for_js
|
174
|
+
|
175
|
+
@lookup_context.formats = formats | @lookup_context.formats
|
176
|
+
end
|
177
|
+
|
178
|
+
def build_rendered_template(content, template)
|
179
|
+
RenderedTemplate.new content, template
|
180
|
+
end
|
181
|
+
|
182
|
+
def build_rendered_collection(templates, spacer)
|
183
|
+
RenderedCollection.new templates, spacer
|
184
|
+
end
|
48
185
|
end
|
49
186
|
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action_view/renderer/partial_renderer"
|
4
|
+
|
5
|
+
module ActionView
|
6
|
+
class PartialIteration
|
7
|
+
# The number of iterations that will be done by the partial.
|
8
|
+
attr_reader :size
|
9
|
+
|
10
|
+
# The current iteration of the partial.
|
11
|
+
attr_reader :index
|
12
|
+
|
13
|
+
def initialize(size)
|
14
|
+
@size = size
|
15
|
+
@index = 0
|
16
|
+
end
|
17
|
+
|
18
|
+
# Check if this is the first iteration of the partial.
|
19
|
+
def first?
|
20
|
+
index == 0
|
21
|
+
end
|
22
|
+
|
23
|
+
# Check if this is the last iteration of the partial.
|
24
|
+
def last?
|
25
|
+
index == size - 1
|
26
|
+
end
|
27
|
+
|
28
|
+
def iterate! # :nodoc:
|
29
|
+
@index += 1
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class CollectionRenderer < PartialRenderer # :nodoc:
|
34
|
+
include ObjectRendering
|
35
|
+
|
36
|
+
class CollectionIterator # :nodoc:
|
37
|
+
include Enumerable
|
38
|
+
|
39
|
+
def initialize(collection)
|
40
|
+
@collection = collection
|
41
|
+
end
|
42
|
+
|
43
|
+
def each(&blk)
|
44
|
+
@collection.each(&blk)
|
45
|
+
end
|
46
|
+
|
47
|
+
def size
|
48
|
+
@collection.size
|
49
|
+
end
|
50
|
+
|
51
|
+
def length
|
52
|
+
@collection.respond_to?(:length) ? @collection.length : size
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class SameCollectionIterator < CollectionIterator # :nodoc:
|
57
|
+
def initialize(collection, path, variables)
|
58
|
+
super(collection)
|
59
|
+
@path = path
|
60
|
+
@variables = variables
|
61
|
+
end
|
62
|
+
|
63
|
+
def from_collection(collection)
|
64
|
+
self.class.new(collection, @path, @variables)
|
65
|
+
end
|
66
|
+
|
67
|
+
def each_with_info
|
68
|
+
return enum_for(:each_with_info) unless block_given?
|
69
|
+
variables = [@path] + @variables
|
70
|
+
@collection.each { |o| yield(o, variables) }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class PreloadCollectionIterator < SameCollectionIterator # :nodoc:
|
75
|
+
def initialize(collection, path, variables, relation)
|
76
|
+
super(collection, path, variables)
|
77
|
+
relation.skip_preloading! unless relation.loaded?
|
78
|
+
@relation = relation
|
79
|
+
end
|
80
|
+
|
81
|
+
def from_collection(collection)
|
82
|
+
self.class.new(collection, @path, @variables, @relation)
|
83
|
+
end
|
84
|
+
|
85
|
+
def each_with_info
|
86
|
+
return super unless block_given?
|
87
|
+
@relation.preload_associations(@collection)
|
88
|
+
super
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
class MixedCollectionIterator < CollectionIterator # :nodoc:
|
93
|
+
def initialize(collection, paths)
|
94
|
+
super(collection)
|
95
|
+
@paths = paths
|
96
|
+
end
|
97
|
+
|
98
|
+
def each_with_info
|
99
|
+
return enum_for(:each_with_info) unless block_given?
|
100
|
+
@collection.each_with_index { |o, i| yield(o, @paths[i]) }
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def render_collection_with_partial(collection, partial, context, block)
|
105
|
+
iter_vars = retrieve_variable(partial)
|
106
|
+
|
107
|
+
collection = if collection.respond_to?(:preload_associations)
|
108
|
+
PreloadCollectionIterator.new(collection, partial, iter_vars, collection)
|
109
|
+
else
|
110
|
+
SameCollectionIterator.new(collection, partial, iter_vars)
|
111
|
+
end
|
112
|
+
|
113
|
+
template = find_template(partial, @locals.keys + iter_vars)
|
114
|
+
|
115
|
+
layout = if !block && (layout = @options[:layout])
|
116
|
+
find_template(layout.to_s, @locals.keys + iter_vars)
|
117
|
+
end
|
118
|
+
|
119
|
+
render_collection(collection, context, partial, template, layout, block)
|
120
|
+
end
|
121
|
+
|
122
|
+
def render_collection_derive_partial(collection, context, block)
|
123
|
+
paths = collection.map { |o| partial_path(o, context) }
|
124
|
+
|
125
|
+
if paths.uniq.length == 1
|
126
|
+
# Homogeneous
|
127
|
+
render_collection_with_partial(collection, paths.first, context, block)
|
128
|
+
else
|
129
|
+
if @options[:cached]
|
130
|
+
raise NotImplementedError, "render caching requires a template. Please specify a partial when rendering"
|
131
|
+
end
|
132
|
+
|
133
|
+
paths.map! { |path| retrieve_variable(path).unshift(path) }
|
134
|
+
collection = MixedCollectionIterator.new(collection, paths)
|
135
|
+
render_collection(collection, context, nil, nil, nil, block)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
def retrieve_variable(path)
|
141
|
+
variable = local_variable(path)
|
142
|
+
[variable, :"#{variable}_counter", :"#{variable}_iteration"]
|
143
|
+
end
|
144
|
+
|
145
|
+
def render_collection(collection, view, path, template, layout, block)
|
146
|
+
identifier = (template && template.identifier) || path
|
147
|
+
ActiveSupport::Notifications.instrument(
|
148
|
+
"render_collection.action_view",
|
149
|
+
identifier: identifier,
|
150
|
+
layout: layout && layout.virtual_path,
|
151
|
+
count: collection.length
|
152
|
+
) do |payload|
|
153
|
+
spacer = if @options.key?(:spacer_template)
|
154
|
+
spacer_template = find_template(@options[:spacer_template], @locals.keys)
|
155
|
+
build_rendered_template(spacer_template.render(view, @locals), spacer_template)
|
156
|
+
else
|
157
|
+
RenderedTemplate::EMPTY_SPACER
|
158
|
+
end
|
159
|
+
|
160
|
+
collection_body = if template
|
161
|
+
cache_collection_render(payload, view, template, collection) do |filtered_collection|
|
162
|
+
collection_with_template(view, template, layout, filtered_collection)
|
163
|
+
end
|
164
|
+
else
|
165
|
+
collection_with_template(view, nil, layout, collection)
|
166
|
+
end
|
167
|
+
|
168
|
+
return RenderedCollection.empty(@lookup_context.formats.first) if collection_body.empty?
|
169
|
+
|
170
|
+
build_rendered_collection(collection_body, spacer)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def collection_with_template(view, template, layout, collection)
|
175
|
+
locals = @locals
|
176
|
+
cache = {}
|
177
|
+
|
178
|
+
partial_iteration = PartialIteration.new(collection.size)
|
179
|
+
|
180
|
+
collection.each_with_info.map do |object, (path, as, counter, iteration)|
|
181
|
+
index = partial_iteration.index
|
182
|
+
|
183
|
+
locals[as] = object
|
184
|
+
locals[counter] = index
|
185
|
+
locals[iteration] = partial_iteration
|
186
|
+
|
187
|
+
_template = (cache[path] ||= (template || find_template(path, @locals.keys + [as, counter, iteration])))
|
188
|
+
|
189
|
+
content = _template.render(view, locals)
|
190
|
+
content = layout.render(view, locals) { content } if layout
|
191
|
+
partial_iteration.iterate!
|
192
|
+
build_rendered_template(content, _template)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionView
|
4
|
+
class ObjectRenderer < PartialRenderer # :nodoc:
|
5
|
+
include ObjectRendering
|
6
|
+
|
7
|
+
def initialize(lookup_context, options)
|
8
|
+
super
|
9
|
+
@object = nil
|
10
|
+
@local_name = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def render_object_with_partial(object, partial, context, block)
|
14
|
+
@object = object
|
15
|
+
@local_name = local_variable(partial)
|
16
|
+
render(partial, context, block)
|
17
|
+
end
|
18
|
+
|
19
|
+
def render_object_derive_partial(object, context, block)
|
20
|
+
path = partial_path(object, context)
|
21
|
+
render_object_with_partial(object, path, context, block)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
def template_keys(path)
|
26
|
+
super + [@local_name]
|
27
|
+
end
|
28
|
+
|
29
|
+
def render_partial_template(view, locals, template, layout, block)
|
30
|
+
locals[@local_name || template.variable] = @object
|
31
|
+
super(view, locals, template, layout, block)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/enumerable"
|
4
|
+
|
5
|
+
module ActionView
|
6
|
+
module CollectionCaching # :nodoc:
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
# Fallback cache store if Action View is used without Rails.
|
11
|
+
# Otherwise overridden in Railtie to use Rails.cache.
|
12
|
+
mattr_accessor :collection_cache, default: ActiveSupport::Cache::MemoryStore.new
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
def will_cache?(options, view)
|
17
|
+
options[:cached] && view.controller.respond_to?(:perform_caching) && view.controller.perform_caching
|
18
|
+
end
|
19
|
+
|
20
|
+
def cache_collection_render(instrumentation_payload, view, template, collection)
|
21
|
+
return yield(collection) unless will_cache?(@options, view)
|
22
|
+
|
23
|
+
collection_iterator = collection
|
24
|
+
|
25
|
+
# Result is a hash with the key represents the
|
26
|
+
# key used for cache lookup and the value is the item
|
27
|
+
# on which the partial is being rendered
|
28
|
+
keyed_collection, ordered_keys = collection_by_cache_keys(view, template, collection)
|
29
|
+
|
30
|
+
# Pull all partials from cache
|
31
|
+
# Result is a hash, key matches the entry in
|
32
|
+
# `keyed_collection` where the cache was retrieved and the
|
33
|
+
# value is the value that was present in the cache
|
34
|
+
cached_partials = collection_cache.read_multi(*keyed_collection.keys)
|
35
|
+
instrumentation_payload[:cache_hits] = cached_partials.size
|
36
|
+
|
37
|
+
# Extract the items for the keys that are not found
|
38
|
+
collection = keyed_collection.reject { |key, _| cached_partials.key?(key) }.values
|
39
|
+
|
40
|
+
rendered_partials = collection.empty? ? [] : yield(collection_iterator.from_collection(collection))
|
41
|
+
|
42
|
+
index = 0
|
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.
|
46
|
+
rendered_partials[index].tap { index += 1 }
|
47
|
+
end
|
48
|
+
|
49
|
+
ordered_keys.map do |key|
|
50
|
+
keyed_partials[key]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def callable_cache_key?
|
55
|
+
@options[:cached].respond_to?(:call)
|
56
|
+
end
|
57
|
+
|
58
|
+
def collection_by_cache_keys(view, template, collection)
|
59
|
+
seed = callable_cache_key? ? @options[:cached] : ->(i) { i }
|
60
|
+
|
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
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def expanded_cache_key(key, view, template, digest_path)
|
71
|
+
key = view.combined_fragment_cache_key(view.cache_fragment_name(key, digest_path: digest_path))
|
72
|
+
key.frozen? ? key.dup : key # #read_multi & #write may require mutability, Dalli 2.6.0.
|
73
|
+
end
|
74
|
+
|
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.index_with do |cache_key|
|
92
|
+
if content = cached_partials[cache_key]
|
93
|
+
build_rendered_template(content, template)
|
94
|
+
else
|
95
|
+
yield.tap do |rendered_partial|
|
96
|
+
collection_cache.write(cache_key, rendered_partial.body)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|