actionview 4.2.11.1 → 7.0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of actionview might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +229 -215
- data/MIT-LICENSE +1 -1
- data/README.rdoc +9 -8
- data/lib/action_view/base.rb +116 -43
- data/lib/action_view/buffers.rb +20 -3
- data/lib/action_view/cache_expiry.rb +66 -0
- data/lib/action_view/context.rb +8 -12
- data/lib/action_view/dependency_tracker/erb_tracker.rb +154 -0
- data/lib/action_view/dependency_tracker/ripper_tracker.rb +59 -0
- data/lib/action_view/dependency_tracker.rb +21 -122
- data/lib/action_view/digestor.rb +92 -85
- data/lib/action_view/flows.rb +15 -16
- data/lib/action_view/gem_version.rb +6 -4
- data/lib/action_view/helpers/active_model_helper.rb +17 -12
- data/lib/action_view/helpers/asset_tag_helper.rb +356 -101
- data/lib/action_view/helpers/asset_url_helper.rb +180 -74
- data/lib/action_view/helpers/atom_feed_helper.rb +21 -19
- data/lib/action_view/helpers/cache_helper.rb +156 -43
- data/lib/action_view/helpers/capture_helper.rb +21 -14
- data/lib/action_view/helpers/controller_helper.rb +16 -5
- 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 +288 -132
- data/lib/action_view/helpers/debug_helper.rb +9 -6
- data/lib/action_view/helpers/form_helper.rb +956 -173
- data/lib/action_view/helpers/form_options_helper.rb +178 -97
- data/lib/action_view/helpers/form_tag_helper.rb +220 -101
- data/lib/action_view/helpers/javascript_helper.rb +33 -19
- data/lib/action_view/helpers/number_helper.rb +88 -63
- data/lib/action_view/helpers/output_safety_helper.rb +38 -6
- data/lib/action_view/helpers/rendering_helper.rb +21 -10
- data/lib/action_view/helpers/sanitize_helper.rb +31 -32
- data/lib/action_view/helpers/tag_helper.rb +332 -71
- data/lib/action_view/helpers/tags/base.rb +123 -99
- data/lib/action_view/helpers/tags/check_box.rb +21 -20
- 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 +5 -3
- 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 +18 -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 +12 -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/weekday_select.rb +28 -0
- data/lib/action_view/helpers/tags.rb +5 -2
- data/lib/action_view/helpers/text_helper.rb +80 -51
- data/lib/action_view/helpers/translation_helper.rb +120 -69
- data/lib/action_view/helpers/url_helper.rb +398 -171
- data/lib/action_view/helpers.rb +29 -27
- data/lib/action_view/layouts.rb +68 -63
- data/lib/action_view/log_subscriber.rb +77 -10
- data/lib/action_view/lookup_context.rb +137 -113
- data/lib/action_view/model_naming.rb +4 -2
- data/lib/action_view/path_set.rb +28 -32
- data/lib/action_view/railtie.rb +74 -13
- data/lib/action_view/record_identifier.rb +53 -26
- data/lib/action_view/render_parser.rb +188 -0
- data/lib/action_view/renderer/abstract_renderer.rb +152 -15
- 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 +51 -333
- data/lib/action_view/renderer/renderer.rb +68 -11
- data/lib/action_view/renderer/streaming_template_renderer.rb +60 -56
- data/lib/action_view/renderer/template_renderer.rb +87 -74
- data/lib/action_view/rendering.rb +73 -47
- data/lib/action_view/ripper_ast_parser.rb +198 -0
- data/lib/action_view/routing_url_for.rb +35 -24
- data/lib/action_view/tasks/cache_digests.rake +25 -0
- data/lib/action_view/template/error.rb +151 -41
- 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 +29 -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 +14 -10
- data/lib/action_view/template/html.rb +12 -13
- 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 +139 -300
- 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 +10 -12
- data/lib/action_view/template/types.rb +28 -26
- data/lib/action_view/template.rb +123 -91
- data/lib/action_view/template_details.rb +66 -0
- data/lib/action_view/template_path.rb +64 -0
- data/lib/action_view/test_case.rb +70 -53
- data/lib/action_view/testing/resolvers.rb +25 -35
- data/lib/action_view/unbound_template.rb +57 -0
- data/lib/action_view/version.rb +3 -1
- data/lib/action_view/view_paths.rb +73 -58
- data/lib/action_view.rb +16 -11
- data/lib/assets/compiled/rails-ujs.js +746 -0
- metadata +52 -32
- data/lib/action_view/helpers/record_tag_helper.rb +0 -108
- data/lib/action_view/tasks/dependencies.rake +0 -23
@@ -1,38 +1,66 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/module"
|
4
|
+
require "action_view/model_naming"
|
3
5
|
|
4
6
|
module ActionView
|
5
|
-
#
|
6
|
-
#
|
7
|
-
# a higher logical level.
|
7
|
+
# RecordIdentifier encapsulates methods used by various ActionView helpers
|
8
|
+
# to associate records with DOM elements.
|
8
9
|
#
|
9
|
-
#
|
10
|
-
# resources :posts
|
10
|
+
# Consider for example the following code that form of post:
|
11
11
|
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
# <% end %> </div>
|
12
|
+
# <%= form_for(post) do |f| %>
|
13
|
+
# <%= f.text_field :body %>
|
14
|
+
# <% end %>
|
16
15
|
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
# post = Post.find(params[:id])
|
20
|
-
# post.update(params[:post])
|
16
|
+
# When +post+ is a new, unsaved ActiveRecord::Base instance, the resulting HTML
|
17
|
+
# is:
|
21
18
|
#
|
22
|
-
#
|
23
|
-
#
|
19
|
+
# <form class="new_post" id="new_post" action="/posts" accept-charset="UTF-8" method="post">
|
20
|
+
# <input type="text" name="post[body]" id="post_body" />
|
21
|
+
# </form>
|
22
|
+
#
|
23
|
+
# When +post+ is a persisted ActiveRecord::Base instance, the resulting HTML
|
24
|
+
# is:
|
25
|
+
#
|
26
|
+
# <form class="edit_post" id="edit_post_42" action="/posts/42" accept-charset="UTF-8" method="post">
|
27
|
+
# <input type="text" value="What a wonderful world!" name="post[body]" id="post_body" />
|
28
|
+
# </form>
|
29
|
+
#
|
30
|
+
# In both cases, the +id+ and +class+ of the wrapping DOM element are
|
31
|
+
# automatically generated, following naming conventions encapsulated by the
|
32
|
+
# RecordIdentifier methods #dom_id and #dom_class:
|
33
|
+
#
|
34
|
+
# dom_id(Post.new) # => "new_post"
|
35
|
+
# dom_class(Post.new) # => "post"
|
36
|
+
# dom_id(Post.find 42) # => "post_42"
|
37
|
+
# dom_class(Post.find 42) # => "post"
|
24
38
|
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
39
|
+
# Note that these methods do not strictly require +Post+ to be a subclass of
|
40
|
+
# ActiveRecord::Base.
|
41
|
+
# Any +Post+ class will work as long as its instances respond to +to_key+
|
42
|
+
# and +model_name+, given that +model_name+ responds to +param_key+.
|
43
|
+
# For instance:
|
44
|
+
#
|
45
|
+
# class Post
|
46
|
+
# attr_accessor :to_key
|
47
|
+
#
|
48
|
+
# def model_name
|
49
|
+
# OpenStruct.new param_key: 'post'
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# def self.find(id)
|
53
|
+
# new.tap { |post| post.to_key = [id] }
|
54
|
+
# end
|
55
|
+
# end
|
28
56
|
module RecordIdentifier
|
29
57
|
extend self
|
30
58
|
extend ModelNaming
|
31
59
|
|
32
60
|
include ModelNaming
|
33
61
|
|
34
|
-
JOIN =
|
35
|
-
NEW =
|
62
|
+
JOIN = "_"
|
63
|
+
NEW = "new"
|
36
64
|
|
37
65
|
# The DOM class convention is to use the singular form of an object or class.
|
38
66
|
#
|
@@ -66,8 +94,7 @@ module ActionView
|
|
66
94
|
end
|
67
95
|
end
|
68
96
|
|
69
|
-
|
70
|
-
|
97
|
+
private
|
71
98
|
# Returns a string representation of the key attribute(s) that is suitable for use in an HTML DOM id.
|
72
99
|
# This can be overwritten to customize the default generated string representation if desired.
|
73
100
|
# If you need to read back a key from a dom_id in order to query for the underlying database record,
|
@@ -76,9 +103,9 @@ module ActionView
|
|
76
103
|
# overwritten version of the method. By default, this implementation passes the key string through a
|
77
104
|
# method that replaces all characters that are invalid inside DOM ids, with valid ones. You need to
|
78
105
|
# make sure yourself that your dom ids are valid, in case you overwrite this method.
|
79
|
-
def record_key_for_dom_id(record)
|
106
|
+
def record_key_for_dom_id(record) # :doc:
|
80
107
|
key = convert_to_model(record).to_key
|
81
|
-
key ? key.join(
|
108
|
+
key ? key.join(JOIN) : key
|
82
109
|
end
|
83
110
|
end
|
84
111
|
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action_view/ripper_ast_parser"
|
4
|
+
|
5
|
+
module ActionView
|
6
|
+
class RenderParser # :nodoc:
|
7
|
+
def initialize(name, code)
|
8
|
+
@name = name
|
9
|
+
@code = code
|
10
|
+
@parser = RipperASTParser
|
11
|
+
end
|
12
|
+
|
13
|
+
def render_calls
|
14
|
+
render_nodes = @parser.parse_render_nodes(@code)
|
15
|
+
|
16
|
+
render_nodes.map do |method, nodes|
|
17
|
+
nodes.map { |n| send(:parse_render, n) }
|
18
|
+
end.flatten.compact
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
def directory
|
23
|
+
File.dirname(@name)
|
24
|
+
end
|
25
|
+
|
26
|
+
def resolve_path_directory(path)
|
27
|
+
if path.include?("/")
|
28
|
+
path
|
29
|
+
else
|
30
|
+
"#{directory}/#{path}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Convert
|
35
|
+
# render("foo", ...)
|
36
|
+
# into either
|
37
|
+
# render(template: "foo", ...)
|
38
|
+
# or
|
39
|
+
# render(partial: "foo", ...)
|
40
|
+
def normalize_args(string, options_hash)
|
41
|
+
if options_hash
|
42
|
+
{ partial: string, locals: options_hash }
|
43
|
+
else
|
44
|
+
{ partial: string }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def parse_render(node)
|
49
|
+
node = node.argument_nodes
|
50
|
+
|
51
|
+
if (node.length == 1 || node.length == 2) && !node[0].hash?
|
52
|
+
if node.length == 1
|
53
|
+
options = normalize_args(node[0], nil)
|
54
|
+
elsif node.length == 2
|
55
|
+
options = normalize_args(node[0], node[1])
|
56
|
+
end
|
57
|
+
|
58
|
+
return nil unless options
|
59
|
+
|
60
|
+
parse_render_from_options(options)
|
61
|
+
elsif node.length == 1 && node[0].hash?
|
62
|
+
options = parse_hash_to_symbols(node[0])
|
63
|
+
|
64
|
+
return nil unless options
|
65
|
+
|
66
|
+
parse_render_from_options(options)
|
67
|
+
else
|
68
|
+
nil
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def parse_hash(node)
|
73
|
+
node.hash? && node.to_hash
|
74
|
+
end
|
75
|
+
|
76
|
+
def parse_hash_to_symbols(node)
|
77
|
+
hash = parse_hash(node)
|
78
|
+
|
79
|
+
return unless hash
|
80
|
+
|
81
|
+
hash.transform_keys do |key_node|
|
82
|
+
key = parse_sym(key_node)
|
83
|
+
|
84
|
+
return unless key
|
85
|
+
|
86
|
+
key
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
ALL_KNOWN_KEYS = [:partial, :template, :layout, :formats, :locals, :object, :collection, :as, :status, :content_type, :location, :spacer_template]
|
91
|
+
|
92
|
+
RENDER_TYPE_KEYS =
|
93
|
+
[:partial, :template, :layout]
|
94
|
+
|
95
|
+
def parse_render_from_options(options_hash)
|
96
|
+
renders = []
|
97
|
+
keys = options_hash.keys
|
98
|
+
|
99
|
+
if (keys & RENDER_TYPE_KEYS).size < 1
|
100
|
+
# Must have at least one of render keys
|
101
|
+
return nil
|
102
|
+
end
|
103
|
+
|
104
|
+
if (keys - ALL_KNOWN_KEYS).any?
|
105
|
+
# de-opt in case of unknown option
|
106
|
+
return nil
|
107
|
+
end
|
108
|
+
|
109
|
+
render_type = (keys & RENDER_TYPE_KEYS)[0]
|
110
|
+
|
111
|
+
node = options_hash[render_type]
|
112
|
+
|
113
|
+
if node.string?
|
114
|
+
template = resolve_path_directory(node.to_string)
|
115
|
+
else
|
116
|
+
if node.variable_reference?
|
117
|
+
dependency = node.variable_name.sub(/\A(?:\$|@{1,2})/, "")
|
118
|
+
elsif node.vcall?
|
119
|
+
dependency = node.variable_name
|
120
|
+
elsif node.call?
|
121
|
+
dependency = node.call_method_name
|
122
|
+
else
|
123
|
+
return
|
124
|
+
end
|
125
|
+
|
126
|
+
object_template = true
|
127
|
+
template = "#{dependency.pluralize}/#{dependency.singularize}"
|
128
|
+
end
|
129
|
+
|
130
|
+
return unless template
|
131
|
+
|
132
|
+
if spacer_template = render_template_with_spacer?(options_hash)
|
133
|
+
virtual_path = partial_to_virtual_path(:partial, spacer_template)
|
134
|
+
renders << virtual_path
|
135
|
+
end
|
136
|
+
|
137
|
+
if options_hash.key?(:object) || options_hash.key?(:collection) || object_template
|
138
|
+
return nil if options_hash.key?(:object) && options_hash.key?(:collection)
|
139
|
+
return nil unless options_hash.key?(:partial)
|
140
|
+
end
|
141
|
+
|
142
|
+
virtual_path = partial_to_virtual_path(render_type, template)
|
143
|
+
renders << virtual_path
|
144
|
+
|
145
|
+
# Support for rendering multiple templates (i.e. a partial with a layout)
|
146
|
+
if layout_template = render_template_with_layout?(render_type, options_hash)
|
147
|
+
virtual_path = partial_to_virtual_path(:layout, layout_template)
|
148
|
+
|
149
|
+
renders << virtual_path
|
150
|
+
end
|
151
|
+
|
152
|
+
renders
|
153
|
+
end
|
154
|
+
|
155
|
+
def parse_str(node)
|
156
|
+
node.string? && node.to_string
|
157
|
+
end
|
158
|
+
|
159
|
+
def parse_sym(node)
|
160
|
+
node.symbol? && node.to_symbol
|
161
|
+
end
|
162
|
+
|
163
|
+
private
|
164
|
+
def render_template_with_layout?(render_type, options_hash)
|
165
|
+
if render_type != :layout && options_hash.key?(:layout)
|
166
|
+
parse_str(options_hash[:layout])
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def render_template_with_spacer?(options_hash)
|
171
|
+
if options_hash.key?(:spacer_template)
|
172
|
+
parse_str(options_hash[:spacer_template])
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def partial_to_virtual_path(render_type, partial_path)
|
177
|
+
if render_type == :partial || render_type == :layout
|
178
|
+
partial_path.gsub(%r{(/|^)([^/]*)\z}, '\1_\2')
|
179
|
+
else
|
180
|
+
partial_path
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def layout_to_virtual_path(layout_path)
|
185
|
+
"layouts/#{layout_path}"
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
@@ -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
|
-
class AbstractRenderer
|
18
|
-
delegate :
|
21
|
+
class AbstractRenderer # :nodoc:
|
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
|
+
LookupContext.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
|