loading
Generated 2020-08-25T23:37:22-04:00

All Files ( 29.57% covered at 3.42 hits/line )

107 files in total.
5418 relevant lines, 1602 lines covered and 3816 lines missed. ( 29.57% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
lib/action_view.rb 94.64 % 102 56 53 3 8.52
lib/action_view/base.rb 43.62 % 322 94 41 53 1.31
lib/action_view/buffers.rb 0.00 % 67 41 0 41 0.00
lib/action_view/cache_expiry.rb 0.00 % 52 43 0 43 0.00
lib/action_view/context.rb 55.56 % 31 9 5 4 3.33
lib/action_view/dependency_tracker.rb 84.93 % 181 73 62 11 21.25
lib/action_view/digestor.rb 38.46 % 127 65 25 40 1.15
lib/action_view/flows.rb 0.00 % 75 52 0 52 0.00
lib/action_view/gem_version.rb 88.89 % 17 9 8 1 8.00
lib/action_view/helpers.rb 96.43 % 66 56 54 2 8.68
lib/action_view/helpers/active_model_helper.rb 55.56 % 54 27 15 12 5.00
lib/action_view/helpers/asset_tag_helper.rb 22.41 % 497 116 26 90 2.02
lib/action_view/helpers/asset_url_helper.rb 41.05 % 472 95 39 56 3.69
lib/action_view/helpers/atom_feed_helper.rb 29.09 % 206 55 16 39 2.62
lib/action_view/helpers/cache_helper.rb 28.26 % 264 46 13 33 2.54
lib/action_view/helpers/capture_helper.rb 28.13 % 216 32 9 23 2.53
lib/action_view/helpers/controller_helper.rb 58.82 % 36 17 10 7 5.29
lib/action_view/helpers/csp_helper.rb 50.00 % 26 8 4 4 4.50
lib/action_view/helpers/csrf_helper.rb 71.43 % 35 7 5 2 6.43
lib/action_view/helpers/date_helper.rb 23.05 % 1199 308 71 237 2.22
lib/action_view/helpers/debug_helper.rb 55.56 % 36 9 5 4 5.00
lib/action_view/helpers/form_helper.rb 31.21 % 2571 282 88 194 3.30
lib/action_view/helpers/form_options_helper.rb 32.17 % 895 115 37 78 2.90
lib/action_view/helpers/form_tag_helper.rb 30.91 % 923 165 51 114 2.78
lib/action_view/helpers/javascript_helper.rb 44.00 % 97 25 11 14 3.96
lib/action_view/helpers/number_helper.rb 41.07 % 457 56 23 33 3.70
lib/action_view/helpers/output_safety_helper.rb 33.33 % 70 21 7 14 3.00
lib/action_view/helpers/rendering_helper.rb 29.41 % 109 17 5 12 2.65
lib/action_view/helpers/sanitize_helper.rb 62.96 % 171 27 17 10 5.67
lib/action_view/helpers/tag_helper.rb 41.58 % 361 101 42 59 3.74
lib/action_view/helpers/tags.rb 100.00 % 44 37 37 0 3.00
lib/action_view/helpers/tags/base.rb 25.00 % 199 108 27 81 0.75
lib/action_view/helpers/tags/check_box.rb 0.00 % 65 55 0 55 0.00
lib/action_view/helpers/tags/checkable.rb 0.00 % 18 16 0 16 0.00
lib/action_view/helpers/tags/collection_check_boxes.rb 0.00 % 35 28 0 28 0.00
lib/action_view/helpers/tags/collection_helpers.rb 0.00 % 118 93 0 93 0.00
lib/action_view/helpers/tags/collection_radio_buttons.rb 0.00 % 30 24 0 24 0.00
lib/action_view/helpers/tags/collection_select.rb 0.00 % 30 25 0 25 0.00
lib/action_view/helpers/tags/color_field.rb 0.00 % 26 23 0 23 0.00
lib/action_view/helpers/tags/date_field.rb 0.00 % 14 12 0 12 0.00
lib/action_view/helpers/tags/date_select.rb 0.00 % 73 56 0 56 0.00
lib/action_view/helpers/tags/datetime_field.rb 0.00 % 31 27 0 27 0.00
lib/action_view/helpers/tags/datetime_local_field.rb 0.00 % 20 17 0 17 0.00
lib/action_view/helpers/tags/datetime_select.rb 0.00 % 10 8 0 8 0.00
lib/action_view/helpers/tags/email_field.rb 0.00 % 10 8 0 8 0.00
lib/action_view/helpers/tags/file_field.rb 0.00 % 10 8 0 8 0.00
lib/action_view/helpers/tags/grouped_collection_select.rb 0.00 % 31 26 0 26 0.00
lib/action_view/helpers/tags/hidden_field.rb 0.00 % 10 8 0 8 0.00
lib/action_view/helpers/tags/label.rb 0.00 % 80 64 0 64 0.00
lib/action_view/helpers/tags/month_field.rb 0.00 % 14 12 0 12 0.00
lib/action_view/helpers/tags/number_field.rb 0.00 % 20 16 0 16 0.00
lib/action_view/helpers/tags/password_field.rb 0.00 % 14 12 0 12 0.00
lib/action_view/helpers/tags/placeholderable.rb 0.00 % 24 20 0 20 0.00
lib/action_view/helpers/tags/radio_button.rb 0.00 % 32 26 0 26 0.00
lib/action_view/helpers/tags/range_field.rb 0.00 % 10 8 0 8 0.00
lib/action_view/helpers/tags/search_field.rb 0.00 % 27 22 0 22 0.00
lib/action_view/helpers/tags/select.rb 0.00 % 42 30 0 30 0.00
lib/action_view/helpers/tags/tel_field.rb 0.00 % 10 8 0 8 0.00
lib/action_view/helpers/tags/text_area.rb 0.00 % 24 18 0 18 0.00
lib/action_view/helpers/tags/text_field.rb 0.00 % 33 27 0 27 0.00
lib/action_view/helpers/tags/time_field.rb 0.00 % 14 12 0 12 0.00
lib/action_view/helpers/tags/time_select.rb 0.00 % 10 8 0 8 0.00
lib/action_view/helpers/tags/time_zone_select.rb 0.00 % 22 18 0 18 0.00
lib/action_view/helpers/tags/translator.rb 0.00 % 39 33 0 33 0.00
lib/action_view/helpers/tags/url_field.rb 0.00 % 10 8 0 8 0.00
lib/action_view/helpers/tags/week_field.rb 0.00 % 14 12 0 12 0.00
lib/action_view/helpers/text_helper.rb 28.69 % 485 122 35 87 2.58
lib/action_view/helpers/translation_helper.rb 30.91 % 144 55 17 38 2.84
lib/action_view/helpers/url_helper.rb 22.50 % 770 160 36 124 1.99
lib/action_view/layouts.rb 63.10 % 431 84 53 31 53.86
lib/action_view/log_subscriber.rb 33.33 % 111 63 21 42 1.00
lib/action_view/lookup_context.rb 43.98 % 315 166 73 93 1.54
lib/action_view/model_naming.rb 66.67 % 14 6 4 2 6.00
lib/action_view/path_set.rb 68.18 % 94 44 30 14 9.75
lib/action_view/railtie.rb 0.00 % 107 87 0 87 0.00
lib/action_view/record_identifier.rb 65.00 % 111 20 13 7 5.85
lib/action_view/renderer/abstract_renderer.rb 43.68 % 188 87 38 49 1.31
lib/action_view/renderer/collection_renderer.rb 32.35 % 193 102 33 69 0.97
lib/action_view/renderer/object_renderer.rb 0.00 % 34 27 0 27 0.00
lib/action_view/renderer/partial_renderer.rb 37.04 % 300 27 10 17 1.11
lib/action_view/renderer/partial_renderer/collection_caching.rb 32.50 % 102 40 13 27 0.98
lib/action_view/renderer/renderer.rb 0.00 % 114 77 0 77 0.00
lib/action_view/renderer/streaming_template_renderer.rb 0.00 % 107 63 0 63 0.00
lib/action_view/renderer/template_renderer.rb 0.00 % 110 92 0 92 0.00
lib/action_view/rendering.rb 32.53 % 172 83 27 56 2.93
lib/action_view/routing_url_for.rb 25.64 % 146 39 10 29 2.31
lib/action_view/template.rb 38.35 % 391 133 51 82 3.45
lib/action_view/template/error.rb 0.00 % 156 122 0 122 0.00
lib/action_view/template/handlers.rb 80.00 % 90 45 36 9 14.33
lib/action_view/template/handlers/builder.rb 58.33 % 25 12 7 5 5.25
lib/action_view/template/handlers/erb.rb 51.22 % 102 41 21 20 5.27
lib/action_view/template/handlers/erb/erubi.rb 30.61 % 89 49 15 34 2.76
lib/action_view/template/handlers/html.rb 80.00 % 11 5 4 1 7.20
lib/action_view/template/handlers/raw.rb 80.00 % 11 5 4 1 7.20
lib/action_view/template/html.rb 0.00 % 41 30 0 30 0.00
lib/action_view/template/inline.rb 0.00 % 22 17 0 17 0.00
lib/action_view/template/raw_file.rb 0.00 % 28 21 0 21 0.00
lib/action_view/template/resolver.rb 47.25 % 440 218 103 115 5.78
lib/action_view/template/sources.rb 0.00 % 13 10 0 10 0.00
lib/action_view/template/sources/file.rb 0.00 % 17 14 0 14 0.00
lib/action_view/template/text.rb 0.00 % 35 25 0 25 0.00
lib/action_view/template/types.rb 68.97 % 57 29 20 9 2.28
lib/action_view/test_case.rb 50.35 % 299 141 71 70 4.04
lib/action_view/testing/resolvers.rb 60.00 % 49 25 15 10 4.92
lib/action_view/unbound_template.rb 0.00 % 31 25 0 25 0.00
lib/action_view/version.rb 75.00 % 10 4 3 1 6.75
lib/action_view/view_paths.rb 76.74 % 126 43 33 10 9.56

lib/action_view.rb

94.64% lines covered

56 relevant lines. 53 lines covered and 3 lines missed.
    
  1. # frozen_string_literal: true
  2. #--
  3. # Copyright (c) 2004-2020 David Heinemeier Hansson
  4. #
  5. # Permission is hereby granted, free of charge, to any person obtaining
  6. # a copy of this software and associated documentation files (the
  7. # "Software"), to deal in the Software without restriction, including
  8. # without limitation the rights to use, copy, modify, merge, publish,
  9. # distribute, sublicense, and/or sell copies of the Software, and to
  10. # permit persons to whom the Software is furnished to do so, subject to
  11. # the following conditions:
  12. #
  13. # The above copyright notice and this permission notice shall be
  14. # included in all copies or substantial portions of the Software.
  15. #
  16. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  17. # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  18. # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  19. # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  20. # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  21. # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  22. # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  23. #++
  24. 9 require "active_support"
  25. 9 require "active_support/rails"
  26. 9 require "action_view/version"
  27. 9 module ActionView
  28. 9 extend ActiveSupport::Autoload
  29. 9 ENCODING_FLAG = '#.*coding[:=]\s*(\S+)[ \t]*'
  30. 9 eager_autoload do
  31. 9 autoload :Base
  32. 9 autoload :Context
  33. 9 autoload :Digestor
  34. 9 autoload :Helpers
  35. 9 autoload :LookupContext
  36. 9 autoload :Layouts
  37. 9 autoload :PathSet
  38. 9 autoload :RecordIdentifier
  39. 9 autoload :Rendering
  40. 9 autoload :RoutingUrlFor
  41. 9 autoload :Template
  42. 9 autoload :UnboundTemplate
  43. 9 autoload :ViewPaths
  44. 9 autoload_under "renderer" do
  45. 9 autoload :Renderer
  46. 9 autoload :AbstractRenderer
  47. 9 autoload :PartialRenderer
  48. 9 autoload :CollectionRenderer
  49. 9 autoload :ObjectRenderer
  50. 9 autoload :TemplateRenderer
  51. 9 autoload :StreamingTemplateRenderer
  52. end
  53. 9 autoload_at "action_view/template/resolver" do
  54. 9 autoload :Resolver
  55. 9 autoload :PathResolver
  56. 9 autoload :FileSystemResolver
  57. 9 autoload :OptimizedFileSystemResolver
  58. 9 autoload :FallbackFileSystemResolver
  59. end
  60. 9 autoload_at "action_view/buffers" do
  61. 9 autoload :OutputBuffer
  62. 9 autoload :StreamingBuffer
  63. end
  64. 9 autoload_at "action_view/flows" do
  65. 9 autoload :OutputFlow
  66. 9 autoload :StreamingFlow
  67. end
  68. 9 autoload_at "action_view/template/error" do
  69. 9 autoload :MissingTemplate
  70. 9 autoload :ActionViewError
  71. 9 autoload :EncodingError
  72. 9 autoload :TemplateError
  73. 9 autoload :SyntaxErrorInTemplate
  74. 9 autoload :WrongEncodingError
  75. end
  76. end
  77. 9 autoload :CacheExpiry
  78. 9 autoload :TestCase
  79. 9 def self.eager_load!
  80. super
  81. ActionView::Helpers.eager_load!
  82. ActionView::Template.eager_load!
  83. end
  84. end
  85. 9 require "active_support/core_ext/string/output_safety"
  86. 9 ActiveSupport.on_load(:i18n) do
  87. 9 I18n.load_path << File.expand_path("action_view/locale/en.yml", __dir__)
  88. end

lib/action_view/base.rb

43.62% lines covered

94 relevant lines. 41 lines covered and 53 lines missed.
    
  1. # frozen_string_literal: true
  2. 3 require "active_support/core_ext/module/attr_internal"
  3. 3 require "active_support/core_ext/module/attribute_accessors"
  4. 3 require "active_support/ordered_options"
  5. 3 require "action_view/log_subscriber"
  6. 3 require "action_view/helpers"
  7. 3 require "action_view/context"
  8. 3 require "action_view/template"
  9. 3 require "action_view/lookup_context"
  10. 3 module ActionView #:nodoc:
  11. # = Action View Base
  12. #
  13. # Action View templates can be written in several ways.
  14. # If the template file has a <tt>.erb</tt> extension, then it uses the erubi[https://rubygems.org/gems/erubi]
  15. # template system which can embed Ruby into an HTML document.
  16. # If the template file has a <tt>.builder</tt> extension, then Jim Weirich's Builder::XmlMarkup library is used.
  17. #
  18. # == ERB
  19. #
  20. # You trigger ERB by using embeddings such as <tt><% %></tt>, <tt><% -%></tt>, and <tt><%= %></tt>. The <tt><%= %></tt> tag set is used when you want output. Consider the
  21. # following loop for names:
  22. #
  23. # <b>Names of all the people</b>
  24. # <% @people.each do |person| %>
  25. # Name: <%= person.name %><br/>
  26. # <% end %>
  27. #
  28. # The loop is set up in regular embedding tags <tt><% %></tt>, and the name is written using the output embedding tag <tt><%= %></tt>. Note that this
  29. # is not just a usage suggestion. Regular output functions like print or puts won't work with ERB templates. So this would be wrong:
  30. #
  31. # <%# WRONG %>
  32. # Hi, Mr. <% puts "Frodo" %>
  33. #
  34. # If you absolutely must write from within a function use +concat+.
  35. #
  36. # When on a line that only contains whitespaces except for the tag, <tt><% %></tt> suppresses leading and trailing whitespace,
  37. # including the trailing newline. <tt><% %></tt> and <tt><%- -%></tt> are the same.
  38. # Note however that <tt><%= %></tt> and <tt><%= -%></tt> are different: only the latter removes trailing whitespaces.
  39. #
  40. # === Using sub templates
  41. #
  42. # Using sub templates allows you to sidestep tedious replication and extract common display structures in shared templates. The
  43. # classic example is the use of a header and footer (even though the Action Pack-way would be to use Layouts):
  44. #
  45. # <%= render "shared/header" %>
  46. # Something really specific and terrific
  47. # <%= render "shared/footer" %>
  48. #
  49. # As you see, we use the output embeddings for the render methods. The render call itself will just return a string holding the
  50. # result of the rendering. The output embedding writes it to the current template.
  51. #
  52. # But you don't have to restrict yourself to static includes. Templates can share variables amongst themselves by using instance
  53. # variables defined using the regular embedding tags. Like this:
  54. #
  55. # <% @page_title = "A Wonderful Hello" %>
  56. # <%= render "shared/header" %>
  57. #
  58. # Now the header can pick up on the <tt>@page_title</tt> variable and use it for outputting a title tag:
  59. #
  60. # <title><%= @page_title %></title>
  61. #
  62. # === Passing local variables to sub templates
  63. #
  64. # You can pass local variables to sub templates by using a hash with the variable names as keys and the objects as values:
  65. #
  66. # <%= render "shared/header", { headline: "Welcome", person: person } %>
  67. #
  68. # These can now be accessed in <tt>shared/header</tt> with:
  69. #
  70. # Headline: <%= headline %>
  71. # First name: <%= person.first_name %>
  72. #
  73. # The local variables passed to sub templates can be accessed as a hash using the <tt>local_assigns</tt> hash. This lets you access the
  74. # variables as:
  75. #
  76. # Headline: <%= local_assigns[:headline] %>
  77. #
  78. # This is useful in cases where you aren't sure if the local variable has been assigned. Alternatively, you could also use
  79. # <tt>defined? headline</tt> to first check if the variable has been assigned before using it.
  80. #
  81. # === Template caching
  82. #
  83. # By default, Rails will compile each template to a method in order to render it. When you alter a template,
  84. # Rails will check the file's modification time and recompile it in development mode.
  85. #
  86. # == Builder
  87. #
  88. # Builder templates are a more programmatic alternative to ERB. They are especially useful for generating XML content. An XmlMarkup object
  89. # named +xml+ is automatically made available to templates with a <tt>.builder</tt> extension.
  90. #
  91. # Here are some basic examples:
  92. #
  93. # xml.em("emphasized") # => <em>emphasized</em>
  94. # xml.em { xml.b("emph & bold") } # => <em><b>emph &amp; bold</b></em>
  95. # xml.a("A Link", "href" => "http://onestepback.org") # => <a href="http://onestepback.org">A Link</a>
  96. # xml.target("name" => "compile", "option" => "fast") # => <target option="fast" name="compile"\>
  97. # # NOTE: order of attributes is not specified.
  98. #
  99. # Any method with a block will be treated as an XML markup tag with nested markup in the block. For example, the following:
  100. #
  101. # xml.div do
  102. # xml.h1(@person.name)
  103. # xml.p(@person.bio)
  104. # end
  105. #
  106. # would produce something like:
  107. #
  108. # <div>
  109. # <h1>David Heinemeier Hansson</h1>
  110. # <p>A product of Danish Design during the Winter of '79...</p>
  111. # </div>
  112. #
  113. # Here is a full-length RSS example actually used on Basecamp:
  114. #
  115. # xml.rss("version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/") do
  116. # xml.channel do
  117. # xml.title(@feed_title)
  118. # xml.link(@url)
  119. # xml.description "Basecamp: Recent items"
  120. # xml.language "en-us"
  121. # xml.ttl "40"
  122. #
  123. # @recent_items.each do |item|
  124. # xml.item do
  125. # xml.title(item_title(item))
  126. # xml.description(item_description(item)) if item_description(item)
  127. # xml.pubDate(item_pubDate(item))
  128. # xml.guid(@person.firm.account.url + @recent_items.url(item))
  129. # xml.link(@person.firm.account.url + @recent_items.url(item))
  130. #
  131. # xml.tag!("dc:creator", item.author_name) if item_has_creator?(item)
  132. # end
  133. # end
  134. # end
  135. # end
  136. #
  137. # For more information on Builder please consult the {source
  138. # code}[https://github.com/jimweirich/builder].
  139. 3 class Base
  140. 3 include Helpers, ::ERB::Util, Context
  141. # Specify the proc used to decorate input tags that refer to attributes with errors.
  142. 3 cattr_accessor :field_error_proc, default: Proc.new { |html_tag, instance| "<div class=\"field_with_errors\">#{html_tag}</div>".html_safe }
  143. # How to complete the streaming when an exception occurs.
  144. # This is our best guess: first try to close the attribute, then the tag.
  145. 3 cattr_accessor :streaming_completion_on_exception, default: %("><script>window.location = "/500.html"</script></html>)
  146. # Specify whether rendering within namespaced controllers should prefix
  147. # the partial paths for ActiveModel objects with the namespace.
  148. # (e.g., an Admin::PostsController would render @post using /admin/posts/_post.erb)
  149. 3 cattr_accessor :prefix_partial_path_with_controller_namespace, default: true
  150. # Specify default_formats that can be rendered.
  151. 3 cattr_accessor :default_formats
  152. # Specify whether an error should be raised for missing translations
  153. 3 cattr_accessor :raise_on_missing_translations, default: false
  154. # Specify whether submit_tag should automatically disable on click
  155. 3 cattr_accessor :automatically_disable_submit_tag, default: true
  156. # Annotate rendered view with file names
  157. 3 cattr_accessor :annotate_rendered_view_with_filenames, default: false
  158. 3 class_attribute :_routes
  159. 3 class_attribute :logger
  160. 3 class << self
  161. 3 delegate :erb_trim_mode=, to: "ActionView::Template::Handlers::ERB"
  162. 3 def cache_template_loading
  163. ActionView::Resolver.caching?
  164. end
  165. 3 def cache_template_loading=(value)
  166. ActionView::Resolver.caching = value
  167. end
  168. 3 def xss_safe? #:nodoc:
  169. true
  170. end
  171. 3 def with_empty_template_cache # :nodoc:
  172. subclass = Class.new(self) {
  173. # We can't implement these as self.class because subclasses will
  174. # share the same template cache as superclasses, so "changed?" won't work
  175. # correctly.
  176. define_method(:compiled_method_container) { subclass }
  177. define_singleton_method(:compiled_method_container) { subclass }
  178. def self.name
  179. superclass.name
  180. end
  181. def inspect
  182. "#<#{self.class.name}:#{'%#016x' % (object_id << 1)}>"
  183. end
  184. }
  185. end
  186. 3 def changed?(other) # :nodoc:
  187. compiled_method_container != other.compiled_method_container
  188. end
  189. end
  190. 3 attr_reader :view_renderer, :lookup_context
  191. 3 attr_internal :config, :assigns
  192. 3 delegate :formats, :formats=, :locale, :locale=, :view_paths, :view_paths=, to: :lookup_context
  193. 3 def assign(new_assigns) # :nodoc:
  194. @_assigns = new_assigns.each { |key, value| instance_variable_set("@#{key}", value) }
  195. end
  196. # :stopdoc:
  197. 3 def self.build_lookup_context(context)
  198. case context
  199. when ActionView::Renderer
  200. context.lookup_context
  201. when Array
  202. ActionView::LookupContext.new(context)
  203. when ActionView::PathSet
  204. ActionView::LookupContext.new(context)
  205. when nil
  206. ActionView::LookupContext.new([])
  207. else
  208. raise NotImplementedError, context.class.name
  209. end
  210. end
  211. 3 def self.empty
  212. with_view_paths([])
  213. end
  214. 3 def self.with_view_paths(view_paths, assigns = {}, controller = nil)
  215. with_context ActionView::LookupContext.new(view_paths), assigns, controller
  216. end
  217. 3 def self.with_context(context, assigns = {}, controller = nil)
  218. new context, assigns, controller
  219. end
  220. 3 NULL = Object.new
  221. # :startdoc:
  222. 3 def initialize(lookup_context = nil, assigns = {}, controller = nil, formats = NULL) #:nodoc:
  223. @_config = ActiveSupport::InheritableOptions.new
  224. unless formats == NULL
  225. ActiveSupport::Deprecation.warn <<~eowarn.squish
  226. Passing formats to ActionView::Base.new is deprecated
  227. eowarn
  228. end
  229. case lookup_context
  230. when ActionView::LookupContext
  231. @lookup_context = lookup_context
  232. else
  233. ActiveSupport::Deprecation.warn <<~eowarn.squish
  234. ActionView::Base instances should be constructed with a lookup context,
  235. assignments, and a controller.
  236. eowarn
  237. @lookup_context = self.class.build_lookup_context(lookup_context)
  238. end
  239. @view_renderer = ActionView::Renderer.new @lookup_context
  240. @current_template = nil
  241. @cache_hit = {}
  242. assign(assigns)
  243. assign_controller(controller)
  244. _prepare_context
  245. end
  246. 3 def _run(method, template, locals, buffer, add_to_stack: true, &block)
  247. _old_output_buffer, _old_template = @output_buffer, @current_template
  248. @current_template = template if add_to_stack
  249. @output_buffer = buffer
  250. send(method, locals, buffer, &block)
  251. ensure
  252. @output_buffer, @current_template = _old_output_buffer, _old_template
  253. end
  254. 3 def compiled_method_container
  255. if self.class == ActionView::Base
  256. ActiveSupport::Deprecation.warn <<~eowarn.squish
  257. ActionView::Base instances must implement `compiled_method_container`
  258. or use the class method `with_empty_template_cache` for constructing
  259. an ActionView::Base instance that has an empty cache.
  260. eowarn
  261. end
  262. self.class
  263. end
  264. 3 def in_rendering_context(options)
  265. old_view_renderer = @view_renderer
  266. old_lookup_context = @lookup_context
  267. if !lookup_context.html_fallback_for_js && options[:formats]
  268. formats = Array(options[:formats])
  269. if formats == [:js]
  270. formats << :html
  271. end
  272. @lookup_context = lookup_context.with_prepended_formats(formats)
  273. @view_renderer = ActionView::Renderer.new @lookup_context
  274. end
  275. yield @view_renderer
  276. ensure
  277. @view_renderer = old_view_renderer
  278. @lookup_context = old_lookup_context
  279. end
  280. 3 ActiveSupport.run_load_hooks(:action_view, self)
  281. end
  282. end

lib/action_view/buffers.rb

0.0% lines covered

41 relevant lines. 0 lines covered and 41 lines missed.
    
  1. # frozen_string_literal: true
  2. require "active_support/core_ext/string/output_safety"
  3. module ActionView
  4. # Used as a buffer for views
  5. #
  6. # The main difference between this and ActiveSupport::SafeBuffer
  7. # is for the methods `<<` and `safe_expr_append=` the inputs are
  8. # checked for nil before they are assigned and `to_s` is called on
  9. # the input. For example:
  10. #
  11. # obuf = ActionView::OutputBuffer.new "hello"
  12. # obuf << 5
  13. # puts obuf # => "hello5"
  14. #
  15. # sbuf = ActiveSupport::SafeBuffer.new "hello"
  16. # sbuf << 5
  17. # puts sbuf # => "hello\u0005"
  18. #
  19. class OutputBuffer < ActiveSupport::SafeBuffer #:nodoc:
  20. def initialize(*)
  21. super
  22. encode!
  23. end
  24. def <<(value)
  25. return self if value.nil?
  26. super(value.to_s)
  27. end
  28. alias :append= :<<
  29. def safe_expr_append=(val)
  30. return self if val.nil?
  31. safe_concat val.to_s
  32. end
  33. alias :safe_append= :safe_concat
  34. end
  35. class StreamingBuffer #:nodoc:
  36. def initialize(block)
  37. @block = block
  38. end
  39. def <<(value)
  40. value = value.to_s
  41. value = ERB::Util.h(value) unless value.html_safe?
  42. @block.call(value)
  43. end
  44. alias :concat :<<
  45. alias :append= :<<
  46. def safe_concat(value)
  47. @block.call(value.to_s)
  48. end
  49. alias :safe_append= :safe_concat
  50. def html_safe?
  51. true
  52. end
  53. def html_safe
  54. self
  55. end
  56. end
  57. end

lib/action_view/cache_expiry.rb

0.0% lines covered

43 relevant lines. 0 lines covered and 43 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView
  3. class CacheExpiry
  4. class Executor
  5. def initialize(watcher:)
  6. @cache_expiry = CacheExpiry.new(watcher: watcher)
  7. end
  8. def before(target)
  9. @cache_expiry.clear_cache_if_necessary
  10. end
  11. end
  12. def initialize(watcher:)
  13. @watched_dirs = nil
  14. @watcher_class = watcher
  15. @watcher = nil
  16. @mutex = Mutex.new
  17. end
  18. def clear_cache_if_necessary
  19. @mutex.synchronize do
  20. watched_dirs = dirs_to_watch
  21. return if watched_dirs.empty?
  22. if watched_dirs != @watched_dirs
  23. @watched_dirs = watched_dirs
  24. @watcher = @watcher_class.new([], watched_dirs) do
  25. clear_cache
  26. end
  27. @watcher.execute
  28. else
  29. @watcher.execute_if_updated
  30. end
  31. end
  32. end
  33. def clear_cache
  34. ActionView::LookupContext::DetailsKey.clear
  35. end
  36. private
  37. def dirs_to_watch
  38. all_view_paths.grep(FileSystemResolver).map!(&:path).tap(&:uniq!).sort!
  39. end
  40. def all_view_paths
  41. ActionView::ViewPaths.all_view_paths.flat_map(&:paths)
  42. end
  43. end
  44. end

lib/action_view/context.rb

55.56% lines covered

9 relevant lines. 5 lines covered and 4 lines missed.
    
  1. # frozen_string_literal: true
  2. 6 module ActionView
  3. # = Action View Context
  4. #
  5. # Action View contexts are supplied to Action Controller to render a template.
  6. # The default Action View context is ActionView::Base.
  7. #
  8. # In order to work with Action Controller, a Context must just include this
  9. # module. The initialization of the variables used by the context
  10. # (@output_buffer, @view_flow, and @virtual_path) is responsibility of the
  11. # object that includes this module (although you can call _prepare_context
  12. # defined below).
  13. 6 module Context
  14. 6 attr_accessor :output_buffer, :view_flow
  15. # Prepares the context by setting the appropriate instance variables.
  16. 6 def _prepare_context
  17. @view_flow = OutputFlow.new
  18. @output_buffer = nil
  19. end
  20. # Encapsulates the interaction with the view flow so it
  21. # returns the correct buffer on +yield+. This is usually
  22. # overwritten by helpers to add more behavior.
  23. 6 def _layout_for(name = nil)
  24. name ||= :layout
  25. view_flow.get(name).html_safe
  26. end
  27. end
  28. end

lib/action_view/dependency_tracker.rb

84.93% lines covered

73 relevant lines. 62 lines covered and 11 lines missed.
    
  1. # frozen_string_literal: true
  2. 3 require "concurrent/map"
  3. 3 require "action_view/path_set"
  4. 3 module ActionView
  5. 3 class DependencyTracker # :nodoc:
  6. 3 @trackers = Concurrent::Map.new
  7. 3 def self.find_dependencies(name, template, view_paths = nil)
  8. tracker = @trackers[template.handler]
  9. return [] unless tracker
  10. tracker.call(name, template, view_paths)
  11. end
  12. 3 def self.register_tracker(extension, tracker)
  13. 3 handler = Template.handler_for_extension(extension)
  14. 3 if tracker.respond_to?(:supports_view_paths?)
  15. 3 @trackers[handler] = tracker
  16. else
  17. @trackers[handler] = lambda { |name, template, _|
  18. tracker.call(name, template)
  19. }
  20. end
  21. end
  22. 3 def self.remove_tracker(handler)
  23. @trackers.delete(handler)
  24. end
  25. 3 class ERBTracker # :nodoc:
  26. 3 EXPLICIT_DEPENDENCY = /# Template Dependency: (\S+)/
  27. # A valid ruby identifier - suitable for class, method and specially variable names
  28. 3 IDENTIFIER = /
  29. [[:alpha:]_] # at least one uppercase letter, lowercase letter or underscore
  30. [[:word:]]* # followed by optional letters, numbers or underscores
  31. /x
  32. # Any kind of variable name. e.g. @instance, @@class, $global or local.
  33. # Possibly following a method call chain
  34. 3 VARIABLE_OR_METHOD_CHAIN = /
  35. (?:\$|@{1,2})? # optional global, instance or class variable indicator
  36. (?:#{IDENTIFIER}\.)* # followed by an optional chain of zero-argument method calls
  37. (?<dynamic>#{IDENTIFIER}) # and a final valid identifier, captured as DYNAMIC
  38. /x
  39. # A simple string literal. e.g. "School's out!"
  40. 3 STRING = /
  41. (?<quote>['"]) # an opening quote
  42. (?<static>.*?) # with anything inside, captured as STATIC
  43. \k<quote> # and a matching closing quote
  44. /x
  45. # Part of any hash containing the :partial key
  46. 3 PARTIAL_HASH_KEY = /
  47. (?:\bpartial:|:partial\s*=>) # partial key in either old or new style hash syntax
  48. \s* # followed by optional spaces
  49. /x
  50. # Part of any hash containing the :layout key
  51. 3 LAYOUT_HASH_KEY = /
  52. (?:\blayout:|:layout\s*=>) # layout key in either old or new style hash syntax
  53. \s* # followed by optional spaces
  54. /x
  55. # Matches:
  56. # partial: "comments/comment", collection: @all_comments => "comments/comment"
  57. # (object: @single_comment, partial: "comments/comment") => "comments/comment"
  58. #
  59. # "comments/comments"
  60. # 'comments/comments'
  61. # ('comments/comments')
  62. #
  63. # (@topic) => "topics/topic"
  64. # topics => "topics/topic"
  65. # (message.topics) => "topics/topic"
  66. 3 RENDER_ARGUMENTS = /\A
  67. (?:\s*\(?\s*) # optional opening paren surrounded by spaces
  68. (?:.*?#{PARTIAL_HASH_KEY}|#{LAYOUT_HASH_KEY})? # optional hash, up to the partial or layout key declaration
  69. (?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest
  70. /xm
  71. 3 LAYOUT_DEPENDENCY = /\A
  72. (?:\s*\(?\s*) # optional opening paren surrounded by spaces
  73. (?:.*?#{LAYOUT_HASH_KEY}) # check if the line has layout key declaration
  74. (?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest
  75. /xm
  76. 3 def self.supports_view_paths? # :nodoc:
  77. true
  78. end
  79. 3 def self.call(name, template, view_paths = nil)
  80. new(name, template, view_paths).dependencies
  81. end
  82. 3 def initialize(name, template, view_paths = nil)
  83. 48 @name, @template, @view_paths = name, template, view_paths
  84. end
  85. 3 def dependencies
  86. 48 render_dependencies + explicit_dependencies
  87. end
  88. 3 attr_reader :name, :template
  89. 3 private :name, :template
  90. 3 private
  91. 3 def source
  92. 96 template.source
  93. end
  94. 3 def directory
  95. 12 name.split("/")[0..-2].join("/")
  96. end
  97. 3 def render_dependencies
  98. 48 render_dependencies = []
  99. 48 render_calls = source.split(/\brender\b/).drop(1)
  100. 48 render_calls.each do |arguments|
  101. 69 add_dependencies(render_dependencies, arguments, LAYOUT_DEPENDENCY)
  102. 69 add_dependencies(render_dependencies, arguments, RENDER_ARGUMENTS)
  103. end
  104. 48 render_dependencies.uniq
  105. end
  106. 3 def add_dependencies(render_dependencies, arguments, pattern)
  107. 138 arguments.scan(pattern) do
  108. 75 match = Regexp.last_match
  109. 75 add_dynamic_dependency(render_dependencies, match[:dynamic])
  110. 75 add_static_dependency(render_dependencies, match[:static], match[:quote])
  111. end
  112. end
  113. 3 def add_dynamic_dependency(dependencies, dependency)
  114. 75 if dependency
  115. 27 dependencies << "#{dependency.pluralize}/#{dependency.singularize}"
  116. end
  117. end
  118. 3 def add_static_dependency(dependencies, dependency, quote_type)
  119. 75 if quote_type == '"'
  120. # Ignore if there is interpolation
  121. 15 return if dependency.include?('#{')
  122. end
  123. 72 if dependency
  124. 45 if dependency.include?("/")
  125. 33 dependencies << dependency
  126. else
  127. 12 dependencies << "#{directory}/#{dependency}"
  128. end
  129. end
  130. end
  131. 3 def resolve_directories(wildcard_dependencies)
  132. 48 return [] unless @view_paths
  133. wildcard_dependencies.flat_map { |query, templates|
  134. @view_paths.find_all_with_query(query).map do |template|
  135. "#{File.dirname(query)}/#{File.basename(template).split('.').first}"
  136. end
  137. }.sort
  138. end
  139. 3 def explicit_dependencies
  140. 48 dependencies = source.scan(EXPLICIT_DEPENDENCY).flatten.uniq
  141. 48 wildcards, explicits = dependencies.partition { |dependency| dependency.end_with?("*") }
  142. 48 (explicits + resolve_directories(wildcards)).uniq
  143. end
  144. end
  145. 3 register_tracker :erb, ERBTracker
  146. end
  147. end

lib/action_view/digestor.rb

38.46% lines covered

65 relevant lines. 25 lines covered and 40 lines missed.
    
  1. # frozen_string_literal: true
  2. 3 require "action_view/dependency_tracker"
  3. 3 module ActionView
  4. 3 class Digestor
  5. 3 @@digest_mutex = Mutex.new
  6. 3 class << self
  7. # Supported options:
  8. #
  9. # * <tt>name</tt> - Template name
  10. # * <tt>format</tt> - Template format
  11. # * <tt>finder</tt> - An instance of <tt>ActionView::LookupContext</tt>
  12. # * <tt>dependencies</tt> - An array of dependent views
  13. 3 def digest(name:, format: nil, finder:, dependencies: nil)
  14. if dependencies.nil? || dependencies.empty?
  15. cache_key = "#{name}.#{format}"
  16. else
  17. cache_key = [ name, format, dependencies ].flatten.compact.join(".")
  18. end
  19. # this is a correctly done double-checked locking idiom
  20. # (Concurrent::Map's lookups have volatile semantics)
  21. finder.digest_cache[cache_key] || @@digest_mutex.synchronize do
  22. finder.digest_cache.fetch(cache_key) do # re-check under lock
  23. partial = name.include?("/_")
  24. root = tree(name, finder, partial)
  25. dependencies.each do |injected_dep|
  26. root.children << Injected.new(injected_dep, nil, nil)
  27. end if dependencies
  28. finder.digest_cache[cache_key] = root.digest(finder)
  29. end
  30. end
  31. end
  32. 3 def logger
  33. ActionView::Base.logger || NullLogger
  34. end
  35. # Create a dependency tree for template named +name+.
  36. 3 def tree(name, finder, partial = false, seen = {})
  37. logical_name = name.gsub(%r|/_|, "/")
  38. interpolated = name.include?("#")
  39. if !interpolated && (template = find_template(finder, logical_name, [], partial, []))
  40. if node = seen[template.identifier] # handle cycles in the tree
  41. node
  42. else
  43. node = seen[template.identifier] = Node.create(name, logical_name, template, partial)
  44. deps = DependencyTracker.find_dependencies(name, template, finder.view_paths)
  45. deps.uniq { |n| n.gsub(%r|/_|, "/") }.each do |dep_file|
  46. node.children << tree(dep_file, finder, true, seen)
  47. end
  48. node
  49. end
  50. else
  51. unless interpolated # Dynamic template partial names can never be tracked
  52. logger.error " Couldn't find template for digesting: #{name}"
  53. end
  54. seen[name] ||= Missing.new(name, logical_name, nil)
  55. end
  56. end
  57. 3 private
  58. 3 def find_template(finder, name, prefixes, partial, keys)
  59. finder.disable_cache do
  60. finder.find_all(name, prefixes, partial, keys).first
  61. end
  62. end
  63. end
  64. 3 class Node
  65. 3 attr_reader :name, :logical_name, :template, :children
  66. 3 def self.create(name, logical_name, template, partial)
  67. klass = partial ? Partial : Node
  68. klass.new(name, logical_name, template, [])
  69. end
  70. 3 def initialize(name, logical_name, template, children = [])
  71. @name = name
  72. @logical_name = logical_name
  73. @template = template
  74. @children = children
  75. end
  76. 3 def digest(finder, stack = [])
  77. ActiveSupport::Digest.hexdigest("#{template.source}-#{dependency_digest(finder, stack)}")
  78. end
  79. 3 def dependency_digest(finder, stack)
  80. children.map do |node|
  81. if stack.include?(node)
  82. false
  83. else
  84. finder.digest_cache[node.name] ||= begin
  85. stack.push node
  86. node.digest(finder, stack).tap { stack.pop }
  87. end
  88. end
  89. end.join("-")
  90. end
  91. 3 def to_dep_map
  92. children.any? ? { name => children.map(&:to_dep_map) } : name
  93. end
  94. end
  95. 3 class Partial < Node; end
  96. 3 class Missing < Node
  97. 3 def digest(finder, _ = []) "" end
  98. end
  99. 3 class Injected < Node
  100. 3 def digest(finder, _ = []) name end
  101. end
  102. 3 class NullLogger
  103. 3 def self.debug(_); end
  104. 3 def self.error(_); end
  105. end
  106. end
  107. end

lib/action_view/flows.rb

0.0% lines covered

52 relevant lines. 0 lines covered and 52 lines missed.
    
  1. # frozen_string_literal: true
  2. require "active_support/core_ext/string/output_safety"
  3. module ActionView
  4. class OutputFlow #:nodoc:
  5. attr_reader :content
  6. def initialize
  7. @content = Hash.new { |h, k| h[k] = ActiveSupport::SafeBuffer.new }
  8. end
  9. # Called by _layout_for to read stored values.
  10. def get(key)
  11. @content[key]
  12. end
  13. # Called by each renderer object to set the layout contents.
  14. def set(key, value)
  15. @content[key] = ActiveSupport::SafeBuffer.new(value)
  16. end
  17. # Called by content_for
  18. def append(key, value)
  19. @content[key] << value
  20. end
  21. alias_method :append!, :append
  22. end
  23. class StreamingFlow < OutputFlow #:nodoc:
  24. def initialize(view, fiber)
  25. @view = view
  26. @parent = nil
  27. @child = view.output_buffer
  28. @content = view.view_flow.content
  29. @fiber = fiber
  30. @root = Fiber.current.object_id
  31. end
  32. # Try to get stored content. If the content
  33. # is not available and we're inside the layout fiber,
  34. # then it will begin waiting for the given key and yield.
  35. def get(key)
  36. return super if @content.key?(key)
  37. if inside_fiber?
  38. view = @view
  39. begin
  40. @waiting_for = key
  41. view.output_buffer, @parent = @child, view.output_buffer
  42. Fiber.yield
  43. ensure
  44. @waiting_for = nil
  45. view.output_buffer, @child = @parent, view.output_buffer
  46. end
  47. end
  48. super
  49. end
  50. # Appends the contents for the given key. This is called
  51. # by providing and resuming back to the fiber,
  52. # if that's the key it's waiting for.
  53. def append!(key, value)
  54. super
  55. @fiber.resume if @waiting_for == key
  56. end
  57. private
  58. def inside_fiber?
  59. Fiber.current.object_id != @root
  60. end
  61. end
  62. end

lib/action_view/gem_version.rb

88.89% lines covered

9 relevant lines. 8 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 module ActionView
  3. # Returns the version of the currently loaded Action View as a <tt>Gem::Version</tt>
  4. 9 def self.gem_version
  5. Gem::Version.new VERSION::STRING
  6. end
  7. 9 module VERSION
  8. 9 MAJOR = 6
  9. 9 MINOR = 1
  10. 9 TINY = 0
  11. 9 PRE = "alpha"
  12. 9 STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
  13. end
  14. end

lib/action_view/helpers.rb

96.43% lines covered

56 relevant lines. 54 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 require "active_support/benchmarkable"
  3. 9 module ActionView #:nodoc:
  4. 9 module Helpers #:nodoc:
  5. 9 extend ActiveSupport::Autoload
  6. 9 autoload :ActiveModelHelper
  7. 9 autoload :AssetTagHelper
  8. 9 autoload :AssetUrlHelper
  9. 9 autoload :AtomFeedHelper
  10. 9 autoload :CacheHelper
  11. 9 autoload :CaptureHelper
  12. 9 autoload :ControllerHelper
  13. 9 autoload :CspHelper
  14. 9 autoload :CsrfHelper
  15. 9 autoload :DateHelper
  16. 9 autoload :DebugHelper
  17. 9 autoload :FormHelper
  18. 9 autoload :FormOptionsHelper
  19. 9 autoload :FormTagHelper
  20. 9 autoload :JavaScriptHelper, "action_view/helpers/javascript_helper"
  21. 9 autoload :NumberHelper
  22. 9 autoload :OutputSafetyHelper
  23. 9 autoload :RenderingHelper
  24. 9 autoload :SanitizeHelper
  25. 9 autoload :TagHelper
  26. 9 autoload :TextHelper
  27. 9 autoload :TranslationHelper
  28. 9 autoload :UrlHelper
  29. 9 autoload :Tags
  30. 9 def self.eager_load!
  31. super
  32. Tags.eager_load!
  33. end
  34. 9 extend ActiveSupport::Concern
  35. 9 include ActiveSupport::Benchmarkable
  36. 9 include ActiveModelHelper
  37. 9 include AssetTagHelper
  38. 9 include AssetUrlHelper
  39. 9 include AtomFeedHelper
  40. 9 include CacheHelper
  41. 9 include CaptureHelper
  42. 9 include ControllerHelper
  43. 9 include CspHelper
  44. 9 include CsrfHelper
  45. 9 include DateHelper
  46. 9 include DebugHelper
  47. 9 include FormHelper
  48. 9 include FormOptionsHelper
  49. 9 include FormTagHelper
  50. 9 include JavaScriptHelper
  51. 9 include NumberHelper
  52. 9 include OutputSafetyHelper
  53. 9 include RenderingHelper
  54. 9 include SanitizeHelper
  55. 9 include TagHelper
  56. 9 include TextHelper
  57. 9 include TranslationHelper
  58. 9 include UrlHelper
  59. end
  60. end

lib/action_view/helpers/active_model_helper.rb

55.56% lines covered

27 relevant lines. 15 lines covered and 12 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 require "active_support/core_ext/module/attribute_accessors"
  3. 9 require "active_support/core_ext/enumerable"
  4. 9 module ActionView
  5. # = Active Model Helpers
  6. 9 module Helpers #:nodoc:
  7. 9 module ActiveModelHelper
  8. end
  9. 9 module ActiveModelInstanceTag
  10. 9 def object
  11. @active_model_object ||= begin
  12. object = super
  13. object.respond_to?(:to_model) ? object.to_model : object
  14. end
  15. end
  16. 9 def content_tag(type, options, *)
  17. select_markup_helper?(type) ? super : error_wrapping(super)
  18. end
  19. 9 def tag(type, options, *)
  20. tag_generate_errors?(options) ? error_wrapping(super) : super
  21. end
  22. 9 def error_wrapping(html_tag)
  23. if object_has_errors?
  24. Base.field_error_proc.call(html_tag, self)
  25. else
  26. html_tag
  27. end
  28. end
  29. 9 def error_message
  30. object.errors[@method_name]
  31. end
  32. 9 private
  33. 9 def object_has_errors?
  34. object.respond_to?(:errors) && object.errors.respond_to?(:[]) && error_message.present?
  35. end
  36. 9 def select_markup_helper?(type)
  37. ["optgroup", "option"].include?(type)
  38. end
  39. 9 def tag_generate_errors?(options)
  40. options["type"] != "hidden"
  41. end
  42. end
  43. end
  44. end

lib/action_view/helpers/asset_tag_helper.rb

22.41% lines covered

116 relevant lines. 26 lines covered and 90 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 require "active_support/core_ext/array/extract_options"
  3. 9 require "active_support/core_ext/hash/keys"
  4. 9 require "active_support/core_ext/object/inclusion"
  5. 9 require "action_view/helpers/asset_url_helper"
  6. 9 require "action_view/helpers/tag_helper"
  7. 9 module ActionView
  8. # = Action View Asset Tag Helpers
  9. 9 module Helpers #:nodoc:
  10. # This module provides methods for generating HTML that links views to assets such
  11. # as images, JavaScripts, stylesheets, and feeds. These methods do not verify
  12. # the assets exist before linking to them:
  13. #
  14. # image_tag("rails.png")
  15. # # => <img src="/assets/rails.png" />
  16. # stylesheet_link_tag("application")
  17. # # => <link href="/assets/application.css?body=1" media="screen" rel="stylesheet" />
  18. 9 module AssetTagHelper
  19. 9 extend ActiveSupport::Concern
  20. 9 include AssetUrlHelper
  21. 9 include TagHelper
  22. # Returns an HTML script tag for each of the +sources+ provided.
  23. #
  24. # Sources may be paths to JavaScript files. Relative paths are assumed to be relative
  25. # to <tt>assets/javascripts</tt>, full paths are assumed to be relative to the document
  26. # root. Relative paths are idiomatic, use absolute paths only when needed.
  27. #
  28. # When passing paths, the ".js" extension is optional. If you do not want ".js"
  29. # appended to the path <tt>extname: false</tt> can be set on the options.
  30. #
  31. # You can modify the HTML attributes of the script tag by passing a hash as the
  32. # last argument.
  33. #
  34. # When the Asset Pipeline is enabled, you can pass the name of your manifest as
  35. # source, and include other JavaScript or CoffeeScript files inside the manifest.
  36. #
  37. # If the server supports Early Hints header links for these assets will be
  38. # automatically pushed.
  39. #
  40. # ==== Options
  41. #
  42. # When the last parameter is a hash you can add HTML attributes using that
  43. # parameter. The following options are supported:
  44. #
  45. # * <tt>:extname</tt> - Append an extension to the generated URL unless the extension
  46. # already exists. This only applies for relative URLs.
  47. # * <tt>:protocol</tt> - Sets the protocol of the generated URL. This option only
  48. # applies when a relative URL and +host+ options are provided.
  49. # * <tt>:host</tt> - When a relative URL is provided the host is added to the
  50. # that path.
  51. # * <tt>:skip_pipeline</tt> - This option is used to bypass the asset pipeline
  52. # when it is set to true.
  53. # * <tt>:nonce</tt> - When set to true, adds an automatic nonce value if
  54. # you have Content Security Policy enabled.
  55. #
  56. # ==== Examples
  57. #
  58. # javascript_include_tag "xmlhr"
  59. # # => <script src="/assets/xmlhr.debug-1284139606.js"></script>
  60. #
  61. # javascript_include_tag "xmlhr", host: "localhost", protocol: "https"
  62. # # => <script src="https://localhost/assets/xmlhr.debug-1284139606.js"></script>
  63. #
  64. # javascript_include_tag "template.jst", extname: false
  65. # # => <script src="/assets/template.debug-1284139606.jst"></script>
  66. #
  67. # javascript_include_tag "xmlhr.js"
  68. # # => <script src="/assets/xmlhr.debug-1284139606.js"></script>
  69. #
  70. # javascript_include_tag "common.javascript", "/elsewhere/cools"
  71. # # => <script src="/assets/common.javascript.debug-1284139606.js"></script>
  72. # # <script src="/elsewhere/cools.debug-1284139606.js"></script>
  73. #
  74. # javascript_include_tag "http://www.example.com/xmlhr"
  75. # # => <script src="http://www.example.com/xmlhr"></script>
  76. #
  77. # javascript_include_tag "http://www.example.com/xmlhr.js"
  78. # # => <script src="http://www.example.com/xmlhr.js"></script>
  79. #
  80. # javascript_include_tag "http://www.example.com/xmlhr.js", nonce: true
  81. # # => <script src="http://www.example.com/xmlhr.js" nonce="..."></script>
  82. 9 def javascript_include_tag(*sources)
  83. options = sources.extract_options!.stringify_keys
  84. path_options = options.extract!("protocol", "extname", "host", "skip_pipeline").symbolize_keys
  85. preload_links = []
  86. sources_tags = sources.uniq.map { |source|
  87. href = path_to_javascript(source, path_options)
  88. preload_links << "<#{href}>; rel=preload; as=script"
  89. tag_options = {
  90. "src" => href
  91. }.merge!(options)
  92. if tag_options["nonce"] == true
  93. tag_options["nonce"] = content_security_policy_nonce
  94. end
  95. content_tag("script", "", tag_options)
  96. }.join("\n").html_safe
  97. send_preload_links_header(preload_links)
  98. sources_tags
  99. end
  100. # Returns a stylesheet link tag for the sources specified as arguments. If
  101. # you don't specify an extension, <tt>.css</tt> will be appended automatically.
  102. # You can modify the link attributes by passing a hash as the last argument.
  103. # For historical reasons, the 'media' attribute will always be present and defaults
  104. # to "screen", so you must explicitly set it to "all" for the stylesheet(s) to
  105. # apply to all media types.
  106. #
  107. # If the server supports Early Hints header links for these assets will be
  108. # automatically pushed.
  109. #
  110. # stylesheet_link_tag "style"
  111. # # => <link href="/assets/style.css" media="screen" rel="stylesheet" />
  112. #
  113. # stylesheet_link_tag "style.css"
  114. # # => <link href="/assets/style.css" media="screen" rel="stylesheet" />
  115. #
  116. # stylesheet_link_tag "http://www.example.com/style.css"
  117. # # => <link href="http://www.example.com/style.css" media="screen" rel="stylesheet" />
  118. #
  119. # stylesheet_link_tag "style", media: "all"
  120. # # => <link href="/assets/style.css" media="all" rel="stylesheet" />
  121. #
  122. # stylesheet_link_tag "style", media: "print"
  123. # # => <link href="/assets/style.css" media="print" rel="stylesheet" />
  124. #
  125. # stylesheet_link_tag "random.styles", "/css/stylish"
  126. # # => <link href="/assets/random.styles" media="screen" rel="stylesheet" />
  127. # # <link href="/css/stylish.css" media="screen" rel="stylesheet" />
  128. 9 def stylesheet_link_tag(*sources)
  129. options = sources.extract_options!.stringify_keys
  130. path_options = options.extract!("protocol", "host", "skip_pipeline").symbolize_keys
  131. preload_links = []
  132. sources_tags = sources.uniq.map { |source|
  133. href = path_to_stylesheet(source, path_options)
  134. preload_links << "<#{href}>; rel=preload; as=style"
  135. tag_options = {
  136. "rel" => "stylesheet",
  137. "media" => "screen",
  138. "href" => href
  139. }.merge!(options)
  140. tag(:link, tag_options)
  141. }.join("\n").html_safe
  142. send_preload_links_header(preload_links)
  143. sources_tags
  144. end
  145. # Returns a link tag that browsers and feed readers can use to auto-detect
  146. # an RSS, Atom, or JSON feed. The +type+ can be <tt>:rss</tt> (default),
  147. # <tt>:atom</tt>, or <tt>:json</tt>. Control the link options in url_for format
  148. # using the +url_options+. You can modify the LINK tag itself in +tag_options+.
  149. #
  150. # ==== Options
  151. #
  152. # * <tt>:rel</tt> - Specify the relation of this link, defaults to "alternate"
  153. # * <tt>:type</tt> - Override the auto-generated mime type
  154. # * <tt>:title</tt> - Specify the title of the link, defaults to the +type+
  155. #
  156. # ==== Examples
  157. #
  158. # auto_discovery_link_tag
  159. # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/action" />
  160. # auto_discovery_link_tag(:atom)
  161. # # => <link rel="alternate" type="application/atom+xml" title="ATOM" href="http://www.currenthost.com/controller/action" />
  162. # auto_discovery_link_tag(:json)
  163. # # => <link rel="alternate" type="application/json" title="JSON" href="http://www.currenthost.com/controller/action" />
  164. # auto_discovery_link_tag(:rss, {action: "feed"})
  165. # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/feed" />
  166. # auto_discovery_link_tag(:rss, {action: "feed"}, {title: "My RSS"})
  167. # # => <link rel="alternate" type="application/rss+xml" title="My RSS" href="http://www.currenthost.com/controller/feed" />
  168. # auto_discovery_link_tag(:rss, {controller: "news", action: "feed"})
  169. # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/news/feed" />
  170. # auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", {title: "Example RSS"})
  171. # # => <link rel="alternate" type="application/rss+xml" title="Example RSS" href="http://www.example.com/feed.rss" />
  172. 9 def auto_discovery_link_tag(type = :rss, url_options = {}, tag_options = {})
  173. if !(type == :rss || type == :atom || type == :json) && tag_options[:type].blank?
  174. raise ArgumentError.new("You should pass :type tag_option key explicitly, because you have passed #{type} type other than :rss, :atom, or :json.")
  175. end
  176. tag(
  177. "link",
  178. "rel" => tag_options[:rel] || "alternate",
  179. "type" => tag_options[:type] || Template::Types[type].to_s,
  180. "title" => tag_options[:title] || type.to_s.upcase,
  181. "href" => url_options.is_a?(Hash) ? url_for(url_options.merge(only_path: false)) : url_options
  182. )
  183. end
  184. # Returns a link tag for a favicon managed by the asset pipeline.
  185. #
  186. # If a page has no link like the one generated by this helper, browsers
  187. # ask for <tt>/favicon.ico</tt> automatically, and cache the file if the
  188. # request succeeds. If the favicon changes it is hard to get it updated.
  189. #
  190. # To have better control applications may let the asset pipeline manage
  191. # their favicon storing the file under <tt>app/assets/images</tt>, and
  192. # using this helper to generate its corresponding link tag.
  193. #
  194. # The helper gets the name of the favicon file as first argument, which
  195. # defaults to "favicon.ico", and also supports +:rel+ and +:type+ options
  196. # to override their defaults, "shortcut icon" and "image/x-icon"
  197. # respectively:
  198. #
  199. # favicon_link_tag
  200. # # => <link href="/assets/favicon.ico" rel="shortcut icon" type="image/x-icon" />
  201. #
  202. # favicon_link_tag 'myicon.ico'
  203. # # => <link href="/assets/myicon.ico" rel="shortcut icon" type="image/x-icon" />
  204. #
  205. # Mobile Safari looks for a different link tag, pointing to an image that
  206. # will be used if you add the page to the home screen of an iOS device.
  207. # The following call would generate such a tag:
  208. #
  209. # favicon_link_tag 'mb-icon.png', rel: 'apple-touch-icon', type: 'image/png'
  210. # # => <link href="/assets/mb-icon.png" rel="apple-touch-icon" type="image/png" />
  211. 9 def favicon_link_tag(source = "favicon.ico", options = {})
  212. tag("link", {
  213. rel: "shortcut icon",
  214. type: "image/x-icon",
  215. href: path_to_image(source, skip_pipeline: options.delete(:skip_pipeline))
  216. }.merge!(options.symbolize_keys))
  217. end
  218. # Returns a link tag that browsers can use to preload the +source+.
  219. # The +source+ can be the path of a resource managed by asset pipeline,
  220. # a full path, or an URI.
  221. #
  222. # ==== Options
  223. #
  224. # * <tt>:type</tt> - Override the auto-generated mime type, defaults to the mime type for +source+ extension.
  225. # * <tt>:as</tt> - Override the auto-generated value for as attribute, calculated using +source+ extension and mime type.
  226. # * <tt>:crossorigin</tt> - Specify the crossorigin attribute, required to load cross-origin resources.
  227. # * <tt>:nopush</tt> - Specify if the use of server push is not desired for the resource. Defaults to +false+.
  228. #
  229. # ==== Examples
  230. #
  231. # preload_link_tag("custom_theme.css")
  232. # # => <link rel="preload" href="/assets/custom_theme.css" as="style" type="text/css" />
  233. #
  234. # preload_link_tag("/videos/video.webm")
  235. # # => <link rel="preload" href="/videos/video.mp4" as="video" type="video/webm" />
  236. #
  237. # preload_link_tag(post_path(format: :json), as: "fetch")
  238. # # => <link rel="preload" href="/posts.json" as="fetch" type="application/json" />
  239. #
  240. # preload_link_tag("worker.js", as: "worker")
  241. # # => <link rel="preload" href="/assets/worker.js" as="worker" type="text/javascript" />
  242. #
  243. # preload_link_tag("//example.com/font.woff2")
  244. # # => <link rel="preload" href="//example.com/font.woff2" as="font" type="font/woff2" crossorigin="anonymous"/>
  245. #
  246. # preload_link_tag("//example.com/font.woff2", crossorigin: "use-credentials")
  247. # # => <link rel="preload" href="//example.com/font.woff2" as="font" type="font/woff2" crossorigin="use-credentials" />
  248. #
  249. # preload_link_tag("/media/audio.ogg", nopush: true)
  250. # # => <link rel="preload" href="/media/audio.ogg" as="audio" type="audio/ogg" />
  251. #
  252. 9 def preload_link_tag(source, options = {})
  253. href = asset_path(source, skip_pipeline: options.delete(:skip_pipeline))
  254. extname = File.extname(source).downcase.delete(".")
  255. mime_type = options.delete(:type) || Template::Types[extname]&.to_s
  256. as_type = options.delete(:as) || resolve_link_as(extname, mime_type)
  257. crossorigin = options.delete(:crossorigin)
  258. crossorigin = "anonymous" if crossorigin == true || (crossorigin.blank? && as_type == "font")
  259. nopush = options.delete(:nopush) || false
  260. link_tag = tag.link(**{
  261. rel: "preload",
  262. href: href,
  263. as: as_type,
  264. type: mime_type,
  265. crossorigin: crossorigin
  266. }.merge!(options.symbolize_keys))
  267. preload_link = "<#{href}>; rel=preload; as=#{as_type}"
  268. preload_link += "; type=#{mime_type}" if mime_type
  269. preload_link += "; crossorigin=#{crossorigin}" if crossorigin
  270. preload_link += "; nopush" if nopush
  271. send_preload_links_header([preload_link])
  272. link_tag
  273. end
  274. # Returns an HTML image tag for the +source+. The +source+ can be a full
  275. # path, a file, or an Active Storage attachment.
  276. #
  277. # ==== Options
  278. #
  279. # You can add HTML attributes using the +options+. The +options+ supports
  280. # additional keys for convenience and conformance:
  281. #
  282. # * <tt>:size</tt> - Supplied as "{Width}x{Height}" or "{Number}", so "30x45" becomes
  283. # width="30" and height="45", and "50" becomes width="50" and height="50".
  284. # <tt>:size</tt> will be ignored if the value is not in the correct format.
  285. # * <tt>:srcset</tt> - If supplied as a hash or array of <tt>[source, descriptor]</tt>
  286. # pairs, each image path will be expanded before the list is formatted as a string.
  287. #
  288. # ==== Examples
  289. #
  290. # Assets (images that are part of your app):
  291. #
  292. # image_tag("icon")
  293. # # => <img src="/assets/icon" />
  294. # image_tag("icon.png")
  295. # # => <img src="/assets/icon.png" />
  296. # image_tag("icon.png", size: "16x10", alt: "Edit Entry")
  297. # # => <img src="/assets/icon.png" width="16" height="10" alt="Edit Entry" />
  298. # image_tag("/icons/icon.gif", size: "16")
  299. # # => <img src="/icons/icon.gif" width="16" height="16" />
  300. # image_tag("/icons/icon.gif", height: '32', width: '32')
  301. # # => <img height="32" src="/icons/icon.gif" width="32" />
  302. # image_tag("/icons/icon.gif", class: "menu_icon")
  303. # # => <img class="menu_icon" src="/icons/icon.gif" />
  304. # image_tag("/icons/icon.gif", data: { title: 'Rails Application' })
  305. # # => <img data-title="Rails Application" src="/icons/icon.gif" />
  306. # image_tag("icon.png", srcset: { "icon_2x.png" => "2x", "icon_4x.png" => "4x" })
  307. # # => <img src="/assets/icon.png" srcset="/assets/icon_2x.png 2x, /assets/icon_4x.png 4x">
  308. # image_tag("pic.jpg", srcset: [["pic_1024.jpg", "1024w"], ["pic_1980.jpg", "1980w"]], sizes: "100vw")
  309. # # => <img src="/assets/pic.jpg" srcset="/assets/pic_1024.jpg 1024w, /assets/pic_1980.jpg 1980w" sizes="100vw">
  310. #
  311. # Active Storage blobs (images that are uploaded by the users of your app):
  312. #
  313. # image_tag(user.avatar)
  314. # # => <img src="/rails/active_storage/blobs/.../tiger.jpg" />
  315. # image_tag(user.avatar.variant(resize_to_limit: [100, 100]))
  316. # # => <img src="/rails/active_storage/representations/.../tiger.jpg" />
  317. # image_tag(user.avatar.variant(resize_to_limit: [100, 100]), size: '100')
  318. # # => <img width="100" height="100" src="/rails/active_storage/representations/.../tiger.jpg" />
  319. 9 def image_tag(source, options = {})
  320. options = options.symbolize_keys
  321. check_for_image_tag_errors(options)
  322. skip_pipeline = options.delete(:skip_pipeline)
  323. options[:src] = resolve_image_source(source, skip_pipeline)
  324. if options[:srcset] && !options[:srcset].is_a?(String)
  325. options[:srcset] = options[:srcset].map do |src_path, size|
  326. src_path = path_to_image(src_path, skip_pipeline: skip_pipeline)
  327. "#{src_path} #{size}"
  328. end.join(", ")
  329. end
  330. options[:width], options[:height] = extract_dimensions(options.delete(:size)) if options[:size]
  331. tag("img", options)
  332. end
  333. # Returns an HTML video tag for the +sources+. If +sources+ is a string,
  334. # a single video tag will be returned. If +sources+ is an array, a video
  335. # tag with nested source tags for each source will be returned. The
  336. # +sources+ can be full paths or files that exist in your public videos
  337. # directory.
  338. #
  339. # ==== Options
  340. #
  341. # When the last parameter is a hash you can add HTML attributes using that
  342. # parameter. The following options are supported:
  343. #
  344. # * <tt>:poster</tt> - Set an image (like a screenshot) to be shown
  345. # before the video loads. The path is calculated like the +src+ of +image_tag+.
  346. # * <tt>:size</tt> - Supplied as "{Width}x{Height}" or "{Number}", so "30x45" becomes
  347. # width="30" and height="45", and "50" becomes width="50" and height="50".
  348. # <tt>:size</tt> will be ignored if the value is not in the correct format.
  349. # * <tt>:poster_skip_pipeline</tt> will bypass the asset pipeline when using
  350. # the <tt>:poster</tt> option instead using an asset in the public folder.
  351. #
  352. # ==== Examples
  353. #
  354. # video_tag("trailer")
  355. # # => <video src="/videos/trailer"></video>
  356. # video_tag("trailer.ogg")
  357. # # => <video src="/videos/trailer.ogg"></video>
  358. # video_tag("trailer.ogg", controls: true, preload: 'none')
  359. # # => <video preload="none" controls="controls" src="/videos/trailer.ogg"></video>
  360. # video_tag("trailer.m4v", size: "16x10", poster: "screenshot.png")
  361. # # => <video src="/videos/trailer.m4v" width="16" height="10" poster="/assets/screenshot.png"></video>
  362. # video_tag("trailer.m4v", size: "16x10", poster: "screenshot.png", poster_skip_pipeline: true)
  363. # # => <video src="/videos/trailer.m4v" width="16" height="10" poster="screenshot.png"></video>
  364. # video_tag("/trailers/hd.avi", size: "16x16")
  365. # # => <video src="/trailers/hd.avi" width="16" height="16"></video>
  366. # video_tag("/trailers/hd.avi", size: "16")
  367. # # => <video height="16" src="/trailers/hd.avi" width="16"></video>
  368. # video_tag("/trailers/hd.avi", height: '32', width: '32')
  369. # # => <video height="32" src="/trailers/hd.avi" width="32"></video>
  370. # video_tag("trailer.ogg", "trailer.flv")
  371. # # => <video><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video>
  372. # video_tag(["trailer.ogg", "trailer.flv"])
  373. # # => <video><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video>
  374. # video_tag(["trailer.ogg", "trailer.flv"], size: "160x120")
  375. # # => <video height="120" width="160"><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video>
  376. 9 def video_tag(*sources)
  377. options = sources.extract_options!.symbolize_keys
  378. public_poster_folder = options.delete(:poster_skip_pipeline)
  379. sources << options
  380. multiple_sources_tag_builder("video", sources) do |tag_options|
  381. tag_options[:poster] = path_to_image(tag_options[:poster], skip_pipeline: public_poster_folder) if tag_options[:poster]
  382. tag_options[:width], tag_options[:height] = extract_dimensions(tag_options.delete(:size)) if tag_options[:size]
  383. end
  384. end
  385. # Returns an HTML audio tag for the +sources+. If +sources+ is a string,
  386. # a single audio tag will be returned. If +sources+ is an array, an audio
  387. # tag with nested source tags for each source will be returned. The
  388. # +sources+ can be full paths or files that exist in your public audios
  389. # directory.
  390. #
  391. # When the last parameter is a hash you can add HTML attributes using that
  392. # parameter.
  393. #
  394. # audio_tag("sound")
  395. # # => <audio src="/audios/sound"></audio>
  396. # audio_tag("sound.wav")
  397. # # => <audio src="/audios/sound.wav"></audio>
  398. # audio_tag("sound.wav", autoplay: true, controls: true)
  399. # # => <audio autoplay="autoplay" controls="controls" src="/audios/sound.wav"></audio>
  400. # audio_tag("sound.wav", "sound.mid")
  401. # # => <audio><source src="/audios/sound.wav" /><source src="/audios/sound.mid" /></audio>
  402. 9 def audio_tag(*sources)
  403. multiple_sources_tag_builder("audio", sources)
  404. end
  405. 9 private
  406. 9 def multiple_sources_tag_builder(type, sources)
  407. options = sources.extract_options!.symbolize_keys
  408. skip_pipeline = options.delete(:skip_pipeline)
  409. sources.flatten!
  410. yield options if block_given?
  411. if sources.size > 1
  412. content_tag(type, options) do
  413. safe_join sources.map { |source| tag("source", src: send("path_to_#{type}", source, skip_pipeline: skip_pipeline)) }
  414. end
  415. else
  416. options[:src] = send("path_to_#{type}", sources.first, skip_pipeline: skip_pipeline)
  417. content_tag(type, nil, options)
  418. end
  419. end
  420. 9 def resolve_image_source(source, skip_pipeline)
  421. if source.is_a?(Symbol) || source.is_a?(String)
  422. path_to_image(source, skip_pipeline: skip_pipeline)
  423. else
  424. polymorphic_url(source)
  425. end
  426. rescue NoMethodError => e
  427. raise ArgumentError, "Can't resolve image into URL: #{e}"
  428. end
  429. 9 def extract_dimensions(size)
  430. size = size.to_s
  431. if /\A\d+x\d+\z/.match?(size)
  432. size.split("x")
  433. elsif /\A\d+\z/.match?(size)
  434. [size, size]
  435. end
  436. end
  437. 9 def check_for_image_tag_errors(options)
  438. if options[:size] && (options[:height] || options[:width])
  439. raise ArgumentError, "Cannot pass a :size option with a :height or :width option"
  440. end
  441. end
  442. 9 def resolve_link_as(extname, mime_type)
  443. if extname == "js"
  444. "script"
  445. elsif extname == "css"
  446. "style"
  447. elsif extname == "vtt"
  448. "track"
  449. elsif (type = mime_type.to_s.split("/")[0]) && type.in?(%w(audio video font))
  450. type
  451. end
  452. end
  453. 9 def send_preload_links_header(preload_links)
  454. if respond_to?(:request) && request
  455. request.send_early_hints("Link" => preload_links.join("\n"))
  456. end
  457. if respond_to?(:response) && response
  458. response.headers["Link"] = [response.headers["Link"].presence, *preload_links].compact.join(",")
  459. end
  460. end
  461. end
  462. end
  463. end

lib/action_view/helpers/asset_url_helper.rb

41.05% lines covered

95 relevant lines. 39 lines covered and 56 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 require "zlib"
  3. 9 module ActionView
  4. # = Action View Asset URL Helpers
  5. 9 module Helpers #:nodoc:
  6. # This module provides methods for generating asset paths and
  7. # URLs.
  8. #
  9. # image_path("rails.png")
  10. # # => "/assets/rails.png"
  11. #
  12. # image_url("rails.png")
  13. # # => "http://www.example.com/assets/rails.png"
  14. #
  15. # === Using asset hosts
  16. #
  17. # By default, Rails links to these assets on the current host in the public
  18. # folder, but you can direct Rails to link to assets from a dedicated asset
  19. # server by setting <tt>ActionController::Base.asset_host</tt> in the application
  20. # configuration, typically in <tt>config/environments/production.rb</tt>.
  21. # For example, you'd define <tt>assets.example.com</tt> to be your asset
  22. # host this way, inside the <tt>configure</tt> block of your environment-specific
  23. # configuration files or <tt>config/application.rb</tt>:
  24. #
  25. # config.action_controller.asset_host = "assets.example.com"
  26. #
  27. # Helpers take that into account:
  28. #
  29. # image_tag("rails.png")
  30. # # => <img src="http://assets.example.com/assets/rails.png" />
  31. # stylesheet_link_tag("application")
  32. # # => <link href="http://assets.example.com/assets/application.css" media="screen" rel="stylesheet" />
  33. #
  34. # Browsers open a limited number of simultaneous connections to a single
  35. # host. The exact number varies by browser and version. This limit may cause
  36. # some asset downloads to wait for previous assets to finish before they can
  37. # begin. You can use the <tt>%d</tt> wildcard in the +asset_host+ to
  38. # distribute the requests over four hosts. For example,
  39. # <tt>assets%d.example.com</tt> will spread the asset requests over
  40. # "assets0.example.com", ..., "assets3.example.com".
  41. #
  42. # image_tag("rails.png")
  43. # # => <img src="http://assets0.example.com/assets/rails.png" />
  44. # stylesheet_link_tag("application")
  45. # # => <link href="http://assets2.example.com/assets/application.css" media="screen" rel="stylesheet" />
  46. #
  47. # This may improve the asset loading performance of your application.
  48. # It is also possible the combination of additional connection overhead
  49. # (DNS, SSL) and the overall browser connection limits may result in this
  50. # solution being slower. You should be sure to measure your actual
  51. # performance across targeted browsers both before and after this change.
  52. #
  53. # To implement the corresponding hosts you can either set up four actual
  54. # hosts or use wildcard DNS to CNAME the wildcard to a single asset host.
  55. # You can read more about setting up your DNS CNAME records from your ISP.
  56. #
  57. # Note: This is purely a browser performance optimization and is not meant
  58. # for server load balancing. See https://www.die.net/musings/page_load_time/
  59. # for background and https://www.browserscope.org/?category=network for
  60. # connection limit data.
  61. #
  62. # Alternatively, you can exert more control over the asset host by setting
  63. # +asset_host+ to a proc like this:
  64. #
  65. # ActionController::Base.asset_host = Proc.new { |source|
  66. # "http://assets#{Digest::MD5.hexdigest(source).to_i(16) % 2 + 1}.example.com"
  67. # }
  68. # image_tag("rails.png")
  69. # # => <img src="http://assets1.example.com/assets/rails.png" />
  70. # stylesheet_link_tag("application")
  71. # # => <link href="http://assets2.example.com/assets/application.css" media="screen" rel="stylesheet" />
  72. #
  73. # The example above generates "http://assets1.example.com" and
  74. # "http://assets2.example.com". This option is useful for example if
  75. # you need fewer/more than four hosts, custom host names, etc.
  76. #
  77. # As you see the proc takes a +source+ parameter. That's a string with the
  78. # absolute path of the asset, for example "/assets/rails.png".
  79. #
  80. # ActionController::Base.asset_host = Proc.new { |source|
  81. # if source.end_with?('.css')
  82. # "http://stylesheets.example.com"
  83. # else
  84. # "http://assets.example.com"
  85. # end
  86. # }
  87. # image_tag("rails.png")
  88. # # => <img src="http://assets.example.com/assets/rails.png" />
  89. # stylesheet_link_tag("application")
  90. # # => <link href="http://stylesheets.example.com/assets/application.css" media="screen" rel="stylesheet" />
  91. #
  92. # Alternatively you may ask for a second parameter +request+. That one is
  93. # particularly useful for serving assets from an SSL-protected page. The
  94. # example proc below disables asset hosting for HTTPS connections, while
  95. # still sending assets for plain HTTP requests from asset hosts. If you don't
  96. # have SSL certificates for each of the asset hosts this technique allows you
  97. # to avoid warnings in the client about mixed media.
  98. # Note that the +request+ parameter might not be supplied, e.g. when the assets
  99. # are precompiled with the command `bin/rails assets:precompile`. Make sure to use a
  100. # +Proc+ instead of a lambda, since a +Proc+ allows missing parameters and sets them
  101. # to +nil+.
  102. #
  103. # config.action_controller.asset_host = Proc.new { |source, request|
  104. # if request && request.ssl?
  105. # "#{request.protocol}#{request.host_with_port}"
  106. # else
  107. # "#{request.protocol}assets.example.com"
  108. # end
  109. # }
  110. #
  111. # You can also implement a custom asset host object that responds to +call+
  112. # and takes either one or two parameters just like the proc.
  113. #
  114. # config.action_controller.asset_host = AssetHostingWithMinimumSsl.new(
  115. # "http://asset%d.example.com", "https://asset1.example.com"
  116. # )
  117. #
  118. 9 module AssetUrlHelper
  119. 9 URI_REGEXP = %r{^[-a-z]+://|^(?:cid|data):|^//}i
  120. # This is the entry point for all assets.
  121. # When using the asset pipeline (i.e. sprockets and sprockets-rails), the
  122. # behavior is "enhanced". You can bypass the asset pipeline by passing in
  123. # <tt>skip_pipeline: true</tt> to the options.
  124. #
  125. # All other asset *_path helpers delegate through this method.
  126. #
  127. # === With the asset pipeline
  128. #
  129. # All options passed to +asset_path+ will be passed to +compute_asset_path+
  130. # which is implemented by sprockets-rails.
  131. #
  132. # asset_path("application.js") # => "/assets/application-60aa4fdc5cea14baf5400fba1abf4f2a46a5166bad4772b1effe341570f07de9.js"
  133. # asset_path('application.js', host: 'example.com') # => "//example.com/assets/application.js"
  134. # asset_path("application.js", host: 'example.com', protocol: 'https') # => "https://example.com/assets/application.js"
  135. #
  136. # === Without the asset pipeline (<tt>skip_pipeline: true</tt>)
  137. #
  138. # Accepts a <tt>type</tt> option that can specify the asset's extension. No error
  139. # checking is done to verify the source passed into +asset_path+ is valid
  140. # and that the file exists on disk.
  141. #
  142. # asset_path("application.js", skip_pipeline: true) # => "application.js"
  143. # asset_path("filedoesnotexist.png", skip_pipeline: true) # => "filedoesnotexist.png"
  144. # asset_path("application", type: :javascript, skip_pipeline: true) # => "/javascripts/application.js"
  145. # asset_path("application", type: :stylesheet, skip_pipeline: true) # => "/stylesheets/application.css"
  146. #
  147. # === Options applying to all assets
  148. #
  149. # Below lists scenarios that apply to +asset_path+ whether or not you're
  150. # using the asset pipeline.
  151. #
  152. # - All fully qualified URLs are returned immediately. This bypasses the
  153. # asset pipeline and all other behavior described.
  154. #
  155. # asset_path("http://www.example.com/js/xmlhr.js") # => "http://www.example.com/js/xmlhr.js"
  156. #
  157. # - All assets that begin with a forward slash are assumed to be full
  158. # URLs and will not be expanded. This will bypass the asset pipeline.
  159. #
  160. # asset_path("/foo.png") # => "/foo.png"
  161. #
  162. # - All blank strings will be returned immediately. This bypasses the
  163. # asset pipeline and all other behavior described.
  164. #
  165. # asset_path("") # => ""
  166. #
  167. # - If <tt>config.relative_url_root</tt> is specified, all assets will have that
  168. # root prepended.
  169. #
  170. # Rails.application.config.relative_url_root = "bar"
  171. # asset_path("foo.js", skip_pipeline: true) # => "bar/foo.js"
  172. #
  173. # - A different asset host can be specified via <tt>config.action_controller.asset_host</tt>
  174. # this is commonly used in conjunction with a CDN.
  175. #
  176. # Rails.application.config.action_controller.asset_host = "assets.example.com"
  177. # asset_path("foo.js", skip_pipeline: true) # => "http://assets.example.com/foo.js"
  178. #
  179. # - An extension name can be specified manually with <tt>extname</tt>.
  180. #
  181. # asset_path("foo", skip_pipeline: true, extname: ".js") # => "/foo.js"
  182. # asset_path("foo.css", skip_pipeline: true, extname: ".js") # => "/foo.css.js"
  183. 9 def asset_path(source, options = {})
  184. raise ArgumentError, "nil is not a valid asset source" if source.nil?
  185. source = source.to_s
  186. return "" if source.blank?
  187. return source if URI_REGEXP.match?(source)
  188. tail, source = source[/([\?#].+)$/], source.sub(/([\?#].+)$/, "")
  189. if extname = compute_asset_extname(source, options)
  190. source = "#{source}#{extname}"
  191. end
  192. if source[0] != ?/
  193. if options[:skip_pipeline]
  194. source = public_compute_asset_path(source, options)
  195. else
  196. source = compute_asset_path(source, options)
  197. end
  198. end
  199. relative_url_root = defined?(config.relative_url_root) && config.relative_url_root
  200. if relative_url_root
  201. source = File.join(relative_url_root, source) unless source.start_with?("#{relative_url_root}/")
  202. end
  203. if host = compute_asset_host(source, options)
  204. source = File.join(host, source)
  205. end
  206. "#{source}#{tail}"
  207. end
  208. 9 alias_method :path_to_asset, :asset_path # aliased to avoid conflicts with an asset_path named route
  209. # Computes the full URL to an asset in the public directory. This
  210. # will use +asset_path+ internally, so most of their behaviors
  211. # will be the same. If :host options is set, it overwrites global
  212. # +config.action_controller.asset_host+ setting.
  213. #
  214. # All other options provided are forwarded to +asset_path+ call.
  215. #
  216. # asset_url "application.js" # => http://example.com/assets/application.js
  217. # asset_url "application.js", host: "http://cdn.example.com" # => http://cdn.example.com/assets/application.js
  218. #
  219. 9 def asset_url(source, options = {})
  220. path_to_asset(source, options.merge(protocol: :request))
  221. end
  222. 9 alias_method :url_to_asset, :asset_url # aliased to avoid conflicts with an asset_url named route
  223. 9 ASSET_EXTENSIONS = {
  224. javascript: ".js",
  225. stylesheet: ".css"
  226. }
  227. # Compute extname to append to asset path. Returns +nil+ if
  228. # nothing should be added.
  229. 9 def compute_asset_extname(source, options = {})
  230. return if options[:extname] == false
  231. extname = options[:extname] || ASSET_EXTENSIONS[options[:type]]
  232. if extname && File.extname(source) != extname
  233. extname
  234. else
  235. nil
  236. end
  237. end
  238. # Maps asset types to public directory.
  239. 9 ASSET_PUBLIC_DIRECTORIES = {
  240. audio: "/audios",
  241. font: "/fonts",
  242. image: "/images",
  243. javascript: "/javascripts",
  244. stylesheet: "/stylesheets",
  245. video: "/videos"
  246. }
  247. # Computes asset path to public directory. Plugins and
  248. # extensions can override this method to point to custom assets
  249. # or generate digested paths or query strings.
  250. 9 def compute_asset_path(source, options = {})
  251. dir = ASSET_PUBLIC_DIRECTORIES[options[:type]] || ""
  252. File.join(dir, source)
  253. end
  254. 9 alias :public_compute_asset_path :compute_asset_path
  255. # Pick an asset host for this source. Returns +nil+ if no host is set,
  256. # the host if no wildcard is set, the host interpolated with the
  257. # numbers 0-3 if it contains <tt>%d</tt> (the number is the source hash mod 4),
  258. # or the value returned from invoking call on an object responding to call
  259. # (proc or otherwise).
  260. 9 def compute_asset_host(source = "", options = {})
  261. request = self.request if respond_to?(:request)
  262. host = options[:host]
  263. host ||= config.asset_host if defined? config.asset_host
  264. if host
  265. if host.respond_to?(:call)
  266. arity = host.respond_to?(:arity) ? host.arity : host.method(:call).arity
  267. args = [source]
  268. args << request if request && (arity > 1 || arity < 0)
  269. host = host.call(*args)
  270. elsif host.include?("%d")
  271. host = host % (Zlib.crc32(source) % 4)
  272. end
  273. end
  274. host ||= request.base_url if request && options[:protocol] == :request
  275. return unless host
  276. if URI_REGEXP.match?(host)
  277. host
  278. else
  279. protocol = options[:protocol] || config.default_asset_host_protocol || (request ? :request : :relative)
  280. case protocol
  281. when :relative
  282. "//#{host}"
  283. when :request
  284. "#{request.protocol}#{host}"
  285. else
  286. "#{protocol}://#{host}"
  287. end
  288. end
  289. end
  290. # Computes the path to a JavaScript asset in the public javascripts directory.
  291. # If the +source+ filename has no extension, .js will be appended (except for explicit URIs)
  292. # Full paths from the document root will be passed through.
  293. # Used internally by +javascript_include_tag+ to build the script path.
  294. #
  295. # javascript_path "xmlhr" # => /assets/xmlhr.js
  296. # javascript_path "dir/xmlhr.js" # => /assets/dir/xmlhr.js
  297. # javascript_path "/dir/xmlhr" # => /dir/xmlhr.js
  298. # javascript_path "http://www.example.com/js/xmlhr" # => http://www.example.com/js/xmlhr
  299. # javascript_path "http://www.example.com/js/xmlhr.js" # => http://www.example.com/js/xmlhr.js
  300. 9 def javascript_path(source, options = {})
  301. path_to_asset(source, { type: :javascript }.merge!(options))
  302. end
  303. 9 alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with a javascript_path named route
  304. # Computes the full URL to a JavaScript asset in the public javascripts directory.
  305. # This will use +javascript_path+ internally, so most of their behaviors will be the same.
  306. # Since +javascript_url+ is based on +asset_url+ method you can set :host options. If :host
  307. # options is set, it overwrites global +config.action_controller.asset_host+ setting.
  308. #
  309. # javascript_url "js/xmlhr.js", host: "http://stage.example.com" # => http://stage.example.com/assets/js/xmlhr.js
  310. #
  311. 9 def javascript_url(source, options = {})
  312. url_to_asset(source, { type: :javascript }.merge!(options))
  313. end
  314. 9 alias_method :url_to_javascript, :javascript_url # aliased to avoid conflicts with a javascript_url named route
  315. # Computes the path to a stylesheet asset in the public stylesheets directory.
  316. # If the +source+ filename has no extension, .css will be appended (except for explicit URIs).
  317. # Full paths from the document root will be passed through.
  318. # Used internally by +stylesheet_link_tag+ to build the stylesheet path.
  319. #
  320. # stylesheet_path "style" # => /assets/style.css
  321. # stylesheet_path "dir/style.css" # => /assets/dir/style.css
  322. # stylesheet_path "/dir/style.css" # => /dir/style.css
  323. # stylesheet_path "http://www.example.com/css/style" # => http://www.example.com/css/style
  324. # stylesheet_path "http://www.example.com/css/style.css" # => http://www.example.com/css/style.css
  325. 9 def stylesheet_path(source, options = {})
  326. path_to_asset(source, { type: :stylesheet }.merge!(options))
  327. end
  328. 9 alias_method :path_to_stylesheet, :stylesheet_path # aliased to avoid conflicts with a stylesheet_path named route
  329. # Computes the full URL to a stylesheet asset in the public stylesheets directory.
  330. # This will use +stylesheet_path+ internally, so most of their behaviors will be the same.
  331. # Since +stylesheet_url+ is based on +asset_url+ method you can set :host options. If :host
  332. # options is set, it overwrites global +config.action_controller.asset_host+ setting.
  333. #
  334. # stylesheet_url "css/style.css", host: "http://stage.example.com" # => http://stage.example.com/assets/css/style.css
  335. #
  336. 9 def stylesheet_url(source, options = {})
  337. url_to_asset(source, { type: :stylesheet }.merge!(options))
  338. end
  339. 9 alias_method :url_to_stylesheet, :stylesheet_url # aliased to avoid conflicts with a stylesheet_url named route
  340. # Computes the path to an image asset.
  341. # Full paths from the document root will be passed through.
  342. # Used internally by +image_tag+ to build the image path:
  343. #
  344. # image_path("edit") # => "/assets/edit"
  345. # image_path("edit.png") # => "/assets/edit.png"
  346. # image_path("icons/edit.png") # => "/assets/icons/edit.png"
  347. # image_path("/icons/edit.png") # => "/icons/edit.png"
  348. # image_path("http://www.example.com/img/edit.png") # => "http://www.example.com/img/edit.png"
  349. #
  350. # If you have images as application resources this method may conflict with their named routes.
  351. # The alias +path_to_image+ is provided to avoid that. Rails uses the alias internally, and
  352. # plugin authors are encouraged to do so.
  353. 9 def image_path(source, options = {})
  354. path_to_asset(source, { type: :image }.merge!(options))
  355. end
  356. 9 alias_method :path_to_image, :image_path # aliased to avoid conflicts with an image_path named route
  357. # Computes the full URL to an image asset.
  358. # This will use +image_path+ internally, so most of their behaviors will be the same.
  359. # Since +image_url+ is based on +asset_url+ method you can set :host options. If :host
  360. # options is set, it overwrites global +config.action_controller.asset_host+ setting.
  361. #
  362. # image_url "edit.png", host: "http://stage.example.com" # => http://stage.example.com/assets/edit.png
  363. #
  364. 9 def image_url(source, options = {})
  365. url_to_asset(source, { type: :image }.merge!(options))
  366. end
  367. 9 alias_method :url_to_image, :image_url # aliased to avoid conflicts with an image_url named route
  368. # Computes the path to a video asset in the public videos directory.
  369. # Full paths from the document root will be passed through.
  370. # Used internally by +video_tag+ to build the video path.
  371. #
  372. # video_path("hd") # => /videos/hd
  373. # video_path("hd.avi") # => /videos/hd.avi
  374. # video_path("trailers/hd.avi") # => /videos/trailers/hd.avi
  375. # video_path("/trailers/hd.avi") # => /trailers/hd.avi
  376. # video_path("http://www.example.com/vid/hd.avi") # => http://www.example.com/vid/hd.avi
  377. 9 def video_path(source, options = {})
  378. path_to_asset(source, { type: :video }.merge!(options))
  379. end
  380. 9 alias_method :path_to_video, :video_path # aliased to avoid conflicts with a video_path named route
  381. # Computes the full URL to a video asset in the public videos directory.
  382. # This will use +video_path+ internally, so most of their behaviors will be the same.
  383. # Since +video_url+ is based on +asset_url+ method you can set :host options. If :host
  384. # options is set, it overwrites global +config.action_controller.asset_host+ setting.
  385. #
  386. # video_url "hd.avi", host: "http://stage.example.com" # => http://stage.example.com/videos/hd.avi
  387. #
  388. 9 def video_url(source, options = {})
  389. url_to_asset(source, { type: :video }.merge!(options))
  390. end
  391. 9 alias_method :url_to_video, :video_url # aliased to avoid conflicts with a video_url named route
  392. # Computes the path to an audio asset in the public audios directory.
  393. # Full paths from the document root will be passed through.
  394. # Used internally by +audio_tag+ to build the audio path.
  395. #
  396. # audio_path("horse") # => /audios/horse
  397. # audio_path("horse.wav") # => /audios/horse.wav
  398. # audio_path("sounds/horse.wav") # => /audios/sounds/horse.wav
  399. # audio_path("/sounds/horse.wav") # => /sounds/horse.wav
  400. # audio_path("http://www.example.com/sounds/horse.wav") # => http://www.example.com/sounds/horse.wav
  401. 9 def audio_path(source, options = {})
  402. path_to_asset(source, { type: :audio }.merge!(options))
  403. end
  404. 9 alias_method :path_to_audio, :audio_path # aliased to avoid conflicts with an audio_path named route
  405. # Computes the full URL to an audio asset in the public audios directory.
  406. # This will use +audio_path+ internally, so most of their behaviors will be the same.
  407. # Since +audio_url+ is based on +asset_url+ method you can set :host options. If :host
  408. # options is set, it overwrites global +config.action_controller.asset_host+ setting.
  409. #
  410. # audio_url "horse.wav", host: "http://stage.example.com" # => http://stage.example.com/audios/horse.wav
  411. #
  412. 9 def audio_url(source, options = {})
  413. url_to_asset(source, { type: :audio }.merge!(options))
  414. end
  415. 9 alias_method :url_to_audio, :audio_url # aliased to avoid conflicts with an audio_url named route
  416. # Computes the path to a font asset.
  417. # Full paths from the document root will be passed through.
  418. #
  419. # font_path("font") # => /fonts/font
  420. # font_path("font.ttf") # => /fonts/font.ttf
  421. # font_path("dir/font.ttf") # => /fonts/dir/font.ttf
  422. # font_path("/dir/font.ttf") # => /dir/font.ttf
  423. # font_path("http://www.example.com/dir/font.ttf") # => http://www.example.com/dir/font.ttf
  424. 9 def font_path(source, options = {})
  425. path_to_asset(source, { type: :font }.merge!(options))
  426. end
  427. 9 alias_method :path_to_font, :font_path # aliased to avoid conflicts with a font_path named route
  428. # Computes the full URL to a font asset.
  429. # This will use +font_path+ internally, so most of their behaviors will be the same.
  430. # Since +font_url+ is based on +asset_url+ method you can set :host options. If :host
  431. # options is set, it overwrites global +config.action_controller.asset_host+ setting.
  432. #
  433. # font_url "font.ttf", host: "http://stage.example.com" # => http://stage.example.com/fonts/font.ttf
  434. #
  435. 9 def font_url(source, options = {})
  436. url_to_asset(source, { type: :font }.merge!(options))
  437. end
  438. 9 alias_method :url_to_font, :font_url # aliased to avoid conflicts with a font_url named route
  439. end
  440. end
  441. end

lib/action_view/helpers/atom_feed_helper.rb

29.09% lines covered

55 relevant lines. 16 lines covered and 39 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 require "set"
  3. 9 require "active_support/core_ext/symbol/starts_ends_with"
  4. 9 module ActionView
  5. # = Action View Atom Feed Helpers
  6. 9 module Helpers #:nodoc:
  7. 9 module AtomFeedHelper
  8. # Adds easy defaults to writing Atom feeds with the Builder template engine (this does not work on ERB or any other
  9. # template languages).
  10. #
  11. # Full usage example:
  12. #
  13. # config/routes.rb:
  14. # Rails.application.routes.draw do
  15. # resources :posts
  16. # root to: "posts#index"
  17. # end
  18. #
  19. # app/controllers/posts_controller.rb:
  20. # class PostsController < ApplicationController
  21. # # GET /posts.html
  22. # # GET /posts.atom
  23. # def index
  24. # @posts = Post.all
  25. #
  26. # respond_to do |format|
  27. # format.html
  28. # format.atom
  29. # end
  30. # end
  31. # end
  32. #
  33. # app/views/posts/index.atom.builder:
  34. # atom_feed do |feed|
  35. # feed.title("My great blog!")
  36. # feed.updated(@posts[0].created_at) if @posts.length > 0
  37. #
  38. # @posts.each do |post|
  39. # feed.entry(post) do |entry|
  40. # entry.title(post.title)
  41. # entry.content(post.body, type: 'html')
  42. #
  43. # entry.author do |author|
  44. # author.name("DHH")
  45. # end
  46. # end
  47. # end
  48. # end
  49. #
  50. # The options for atom_feed are:
  51. #
  52. # * <tt>:language</tt>: Defaults to "en-US".
  53. # * <tt>:root_url</tt>: The HTML alternative that this feed is doubling for. Defaults to / on the current host.
  54. # * <tt>:url</tt>: The URL for this feed. Defaults to the current URL.
  55. # * <tt>:id</tt>: The id for this feed. Defaults to "tag:localhost,2005:/posts", in this case.
  56. # * <tt>:schema_date</tt>: The date at which the tag scheme for the feed was first used. A good default is the year you
  57. # created the feed. See http://feedvalidator.org/docs/error/InvalidTAG.html for more information. If not specified,
  58. # 2005 is used (as an "I don't care" value).
  59. # * <tt>:instruct</tt>: Hash of XML processing instructions in the form {target => {attribute => value, }} or {target => [{attribute => value, }, ]}
  60. #
  61. # Other namespaces can be added to the root element:
  62. #
  63. # app/views/posts/index.atom.builder:
  64. # atom_feed({'xmlns:app' => 'http://www.w3.org/2007/app',
  65. # 'xmlns:openSearch' => 'http://a9.com/-/spec/opensearch/1.1/'}) do |feed|
  66. # feed.title("My great blog!")
  67. # feed.updated((@posts.first.created_at))
  68. # feed.tag!('openSearch:totalResults', 10)
  69. #
  70. # @posts.each do |post|
  71. # feed.entry(post) do |entry|
  72. # entry.title(post.title)
  73. # entry.content(post.body, type: 'html')
  74. # entry.tag!('app:edited', Time.now)
  75. #
  76. # entry.author do |author|
  77. # author.name("DHH")
  78. # end
  79. # end
  80. # end
  81. # end
  82. #
  83. # The Atom spec defines five elements (content rights title subtitle
  84. # summary) which may directly contain xhtml content if type: 'xhtml'
  85. # is specified as an attribute. If so, this helper will take care of
  86. # the enclosing div and xhtml namespace declaration. Example usage:
  87. #
  88. # entry.summary type: 'xhtml' do |xhtml|
  89. # xhtml.p pluralize(order.line_items.count, "line item")
  90. # xhtml.p "Shipped to #{order.address}"
  91. # xhtml.p "Paid by #{order.pay_type}"
  92. # end
  93. #
  94. #
  95. # <tt>atom_feed</tt> yields an +AtomFeedBuilder+ instance. Nested elements yield
  96. # an +AtomBuilder+ instance.
  97. 9 def atom_feed(options = {}, &block)
  98. if options[:schema_date]
  99. options[:schema_date] = options[:schema_date].strftime("%Y-%m-%d") if options[:schema_date].respond_to?(:strftime)
  100. else
  101. options[:schema_date] = "2005" # The Atom spec copyright date
  102. end
  103. xml = options.delete(:xml) || eval("xml", block.binding)
  104. xml.instruct!
  105. if options[:instruct]
  106. options[:instruct].each do |target, attrs|
  107. if attrs.respond_to?(:keys)
  108. xml.instruct!(target, attrs)
  109. elsif attrs.respond_to?(:each)
  110. attrs.each { |attr_group| xml.instruct!(target, attr_group) }
  111. end
  112. end
  113. end
  114. feed_opts = { "xml:lang" => options[:language] || "en-US", "xmlns" => "http://www.w3.org/2005/Atom" }
  115. feed_opts.merge!(options).select! { |k, _| k.start_with?("xml") }
  116. xml.feed(feed_opts) do
  117. xml.id(options[:id] || "tag:#{request.host},#{options[:schema_date]}:#{request.fullpath.split(".")[0]}")
  118. xml.link(rel: "alternate", type: "text/html", href: options[:root_url] || (request.protocol + request.host_with_port))
  119. xml.link(rel: "self", type: "application/atom+xml", href: options[:url] || request.url)
  120. yield AtomFeedBuilder.new(xml, self, options)
  121. end
  122. end
  123. 9 class AtomBuilder #:nodoc:
  124. 9 XHTML_TAG_NAMES = %w(content rights title subtitle summary).to_set
  125. 9 def initialize(xml)
  126. @xml = xml
  127. end
  128. 9 private
  129. # Delegate to xml builder, first wrapping the element in an xhtml
  130. # namespaced div element if the method and arguments indicate
  131. # that an xhtml_block? is desired.
  132. 9 def method_missing(method, *arguments, &block)
  133. if xhtml_block?(method, arguments)
  134. @xml.__send__(method, *arguments) do
  135. @xml.div(xmlns: "http://www.w3.org/1999/xhtml") do |xhtml|
  136. block.call(xhtml)
  137. end
  138. end
  139. else
  140. @xml.__send__(method, *arguments, &block)
  141. end
  142. end
  143. # True if the method name matches one of the five elements defined
  144. # in the Atom spec as potentially containing XHTML content and
  145. # if type: 'xhtml' is, in fact, specified.
  146. 9 def xhtml_block?(method, arguments)
  147. if XHTML_TAG_NAMES.include?(method.to_s)
  148. last = arguments.last
  149. last.is_a?(Hash) && last[:type].to_s == "xhtml"
  150. end
  151. end
  152. end
  153. 9 class AtomFeedBuilder < AtomBuilder #:nodoc:
  154. 9 def initialize(xml, view, feed_options = {})
  155. @xml, @view, @feed_options = xml, view, feed_options
  156. end
  157. # Accepts a Date or Time object and inserts it in the proper format. If +nil+ is passed, current time in UTC is used.
  158. 9 def updated(date_or_time = nil)
  159. @xml.updated((date_or_time || Time.now.utc).xmlschema)
  160. end
  161. # Creates an entry tag for a specific record and prefills the id using class and id.
  162. #
  163. # Options:
  164. #
  165. # * <tt>:published</tt>: Time first published. Defaults to the created_at attribute on the record if one such exists.
  166. # * <tt>:updated</tt>: Time of update. Defaults to the updated_at attribute on the record if one such exists.
  167. # * <tt>:url</tt>: The URL for this entry or +false+ or +nil+ for not having a link tag. Defaults to the +polymorphic_url+ for the record.
  168. # * <tt>:id</tt>: The ID for this entry. Defaults to "tag:#{@view.request.host},#{@feed_options[:schema_date]}:#{record.class}/#{record.id}"
  169. # * <tt>:type</tt>: The TYPE for this entry. Defaults to "text/html".
  170. 9 def entry(record, options = {})
  171. @xml.entry do
  172. @xml.id(options[:id] || "tag:#{@view.request.host},#{@feed_options[:schema_date]}:#{record.class}/#{record.id}")
  173. if options[:published] || (record.respond_to?(:created_at) && record.created_at)
  174. @xml.published((options[:published] || record.created_at).xmlschema)
  175. end
  176. if options[:updated] || (record.respond_to?(:updated_at) && record.updated_at)
  177. @xml.updated((options[:updated] || record.updated_at).xmlschema)
  178. end
  179. type = options.fetch(:type, "text/html")
  180. url = options.fetch(:url) { @view.polymorphic_url(record) }
  181. @xml.link(rel: "alternate", type: type, href: url) if url
  182. yield AtomBuilder.new(@xml)
  183. end
  184. end
  185. end
  186. end
  187. end
  188. end

lib/action_view/helpers/cache_helper.rb

28.26% lines covered

46 relevant lines. 13 lines covered and 33 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 module ActionView
  3. # = Action View Cache Helper
  4. 9 module Helpers #:nodoc:
  5. 9 module CacheHelper
  6. # This helper exposes a method for caching fragments of a view
  7. # rather than an entire action or page. This technique is useful
  8. # caching pieces like menus, lists of new topics, static HTML
  9. # fragments, and so on. This method takes a block that contains
  10. # the content you wish to cache.
  11. #
  12. # The best way to use this is by doing recyclable key-based cache expiration
  13. # on top of a cache store like Memcached or Redis that'll automatically
  14. # kick out old entries.
  15. #
  16. # When using this method, you list the cache dependency as the name of the cache, like so:
  17. #
  18. # <% cache project do %>
  19. # <b>All the topics on this project</b>
  20. # <%= render project.topics %>
  21. # <% end %>
  22. #
  23. # This approach will assume that when a new topic is added, you'll touch
  24. # the project. The cache key generated from this call will be something like:
  25. #
  26. # views/template/action:7a1156131a6928cb0026877f8b749ac9/projects/123
  27. # ^template path ^template tree digest ^class ^id
  28. #
  29. # This cache key is stable, but it's combined with a cache version derived from the project
  30. # record. When the project updated_at is touched, the #cache_version changes, even
  31. # if the key stays stable. This means that unlike a traditional key-based cache expiration
  32. # approach, you won't be generating cache trash, unused keys, simply because the dependent
  33. # record is updated.
  34. #
  35. # If your template cache depends on multiple sources (try to avoid this to keep things simple),
  36. # you can name all these dependencies as part of an array:
  37. #
  38. # <% cache [ project, current_user ] do %>
  39. # <b>All the topics on this project</b>
  40. # <%= render project.topics %>
  41. # <% end %>
  42. #
  43. # This will include both records as part of the cache key and updating either of them will
  44. # expire the cache.
  45. #
  46. # ==== \Template digest
  47. #
  48. # The template digest that's added to the cache key is computed by taking an MD5 of the
  49. # contents of the entire template file. This ensures that your caches will automatically
  50. # expire when you change the template file.
  51. #
  52. # Note that the MD5 is taken of the entire template file, not just what's within the
  53. # cache do/end call. So it's possible that changing something outside of that call will
  54. # still expire the cache.
  55. #
  56. # Additionally, the digestor will automatically look through your template file for
  57. # explicit and implicit dependencies, and include those as part of the digest.
  58. #
  59. # The digestor can be bypassed by passing skip_digest: true as an option to the cache call:
  60. #
  61. # <% cache project, skip_digest: true do %>
  62. # <b>All the topics on this project</b>
  63. # <%= render project.topics %>
  64. # <% end %>
  65. #
  66. # ==== Implicit dependencies
  67. #
  68. # Most template dependencies can be derived from calls to render in the template itself.
  69. # Here are some examples of render calls that Cache Digests knows how to decode:
  70. #
  71. # render partial: "comments/comment", collection: commentable.comments
  72. # render "comments/comments"
  73. # render 'comments/comments'
  74. # render('comments/comments')
  75. #
  76. # render "header" translates to render("comments/header")
  77. #
  78. # render(@topic) translates to render("topics/topic")
  79. # render(topics) translates to render("topics/topic")
  80. # render(message.topics) translates to render("topics/topic")
  81. #
  82. # It's not possible to derive all render calls like that, though.
  83. # Here are a few examples of things that can't be derived:
  84. #
  85. # render group_of_attachments
  86. # render @project.documents.where(published: true).order('created_at')
  87. #
  88. # You will have to rewrite those to the explicit form:
  89. #
  90. # render partial: 'attachments/attachment', collection: group_of_attachments
  91. # render partial: 'documents/document', collection: @project.documents.where(published: true).order('created_at')
  92. #
  93. # === Explicit dependencies
  94. #
  95. # Sometimes you'll have template dependencies that can't be derived at all. This is typically
  96. # the case when you have template rendering that happens in helpers. Here's an example:
  97. #
  98. # <%= render_sortable_todolists @project.todolists %>
  99. #
  100. # You'll need to use a special comment format to call those out:
  101. #
  102. # <%# Template Dependency: todolists/todolist %>
  103. # <%= render_sortable_todolists @project.todolists %>
  104. #
  105. # In some cases, like a single table inheritance setup, you might have
  106. # a bunch of explicit dependencies. Instead of writing every template out,
  107. # you can use a wildcard to match any template in a directory:
  108. #
  109. # <%# Template Dependency: events/* %>
  110. # <%= render_categorizable_events @person.events %>
  111. #
  112. # This marks every template in the directory as a dependency. To find those
  113. # templates, the wildcard path must be absolutely defined from <tt>app/views</tt> or paths
  114. # otherwise added with +prepend_view_path+ or +append_view_path+.
  115. # This way the wildcard for <tt>app/views/recordings/events</tt> would be <tt>recordings/events/*</tt> etc.
  116. #
  117. # The pattern used to match explicit dependencies is <tt>/# Template Dependency: (\S+)/</tt>,
  118. # so it's important that you type it out just so.
  119. # You can only declare one template dependency per line.
  120. #
  121. # === External dependencies
  122. #
  123. # If you use a helper method, for example, inside a cached block and
  124. # you then update that helper, you'll have to bump the cache as well.
  125. # It doesn't really matter how you do it, but the MD5 of the template file
  126. # must change. One recommendation is to simply be explicit in a comment, like:
  127. #
  128. # <%# Helper Dependency Updated: May 6, 2012 at 6pm %>
  129. # <%= some_helper_method(person) %>
  130. #
  131. # Now all you have to do is change that timestamp when the helper method changes.
  132. #
  133. # === Collection Caching
  134. #
  135. # When rendering a collection of objects that each use the same partial, a <tt>:cached</tt>
  136. # option can be passed.
  137. #
  138. # For collections rendered such:
  139. #
  140. # <%= render partial: 'projects/project', collection: @projects, cached: true %>
  141. #
  142. # The <tt>cached: true</tt> will make Action View's rendering read several templates
  143. # from cache at once instead of one call per template.
  144. #
  145. # Templates in the collection not already cached are written to cache.
  146. #
  147. # Works great alongside individual template fragment caching.
  148. # For instance if the template the collection renders is cached like:
  149. #
  150. # # projects/_project.html.erb
  151. # <% cache project do %>
  152. # <%# ... %>
  153. # <% end %>
  154. #
  155. # Any collection renders will find those cached templates when attempting
  156. # to read multiple templates at once.
  157. #
  158. # If your collection cache depends on multiple sources (try to avoid this to keep things simple),
  159. # you can name all these dependencies as part of a block that returns an array:
  160. #
  161. # <%= render partial: 'projects/project', collection: @projects, cached: -> project { [ project, current_user ] } %>
  162. #
  163. # This will include both records as part of the cache key and updating either of them will
  164. # expire the cache.
  165. 9 def cache(name = {}, options = {}, &block)
  166. if controller.respond_to?(:perform_caching) && controller.perform_caching
  167. name_options = options.slice(:skip_digest)
  168. safe_concat(fragment_for(cache_fragment_name(name, **name_options), options, &block))
  169. else
  170. yield
  171. end
  172. nil
  173. end
  174. # Cache fragments of a view if +condition+ is true
  175. #
  176. # <% cache_if admin?, project do %>
  177. # <b>All the topics on this project</b>
  178. # <%= render project.topics %>
  179. # <% end %>
  180. 9 def cache_if(condition, name = {}, options = {}, &block)
  181. if condition
  182. cache(name, options, &block)
  183. else
  184. yield
  185. end
  186. nil
  187. end
  188. # Cache fragments of a view unless +condition+ is true
  189. #
  190. # <% cache_unless admin?, project do %>
  191. # <b>All the topics on this project</b>
  192. # <%= render project.topics %>
  193. # <% end %>
  194. 9 def cache_unless(condition, name = {}, options = {}, &block)
  195. cache_if !condition, name, options, &block
  196. end
  197. # This helper returns the name of a cache key for a given fragment cache
  198. # call. By supplying <tt>skip_digest: true</tt> to cache, the digestion of cache
  199. # fragments can be manually bypassed. This is useful when cache fragments
  200. # cannot be manually expired unless you know the exact key which is the
  201. # case when using memcached.
  202. 9 def cache_fragment_name(name = {}, skip_digest: nil, digest_path: nil)
  203. if skip_digest
  204. name
  205. else
  206. fragment_name_with_digest(name, digest_path)
  207. end
  208. end
  209. 9 def digest_path_from_template(template) # :nodoc:
  210. digest = Digestor.digest(name: template.virtual_path, format: template.format, finder: lookup_context, dependencies: view_cache_dependencies)
  211. if digest.present?
  212. "#{template.virtual_path}:#{digest}"
  213. else
  214. template.virtual_path
  215. end
  216. end
  217. 9 private
  218. 9 def fragment_name_with_digest(name, digest_path)
  219. name = controller.url_for(name).split("://").last if name.is_a?(Hash)
  220. if @current_template&.virtual_path || digest_path
  221. digest_path ||= digest_path_from_template(@current_template)
  222. [ digest_path, name ]
  223. else
  224. name
  225. end
  226. end
  227. 9 def fragment_for(name = {}, options = nil, &block)
  228. if content = read_fragment_for(name, options)
  229. @view_renderer.cache_hits[@current_template&.virtual_path] = :hit if defined?(@view_renderer)
  230. content
  231. else
  232. @view_renderer.cache_hits[@current_template&.virtual_path] = :miss if defined?(@view_renderer)
  233. write_fragment_for(name, options, &block)
  234. end
  235. end
  236. 9 def read_fragment_for(name, options)
  237. controller.read_fragment(name, options)
  238. end
  239. 9 def write_fragment_for(name, options)
  240. pos = output_buffer.length
  241. yield
  242. output_safe = output_buffer.html_safe?
  243. fragment = output_buffer.slice!(pos..-1)
  244. if output_safe
  245. self.output_buffer = output_buffer.class.new(output_buffer)
  246. end
  247. controller.write_fragment(name, fragment, options)
  248. end
  249. end
  250. end
  251. end

lib/action_view/helpers/capture_helper.rb

28.13% lines covered

32 relevant lines. 9 lines covered and 23 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 require "active_support/core_ext/string/output_safety"
  3. 9 module ActionView
  4. # = Action View Capture Helper
  5. 9 module Helpers #:nodoc:
  6. # CaptureHelper exposes methods to let you extract generated markup which
  7. # can be used in other parts of a template or layout file.
  8. #
  9. # It provides a method to capture blocks into variables through capture and
  10. # a way to capture a block of markup for use in a layout through {content_for}[rdoc-ref:ActionView::Helpers::CaptureHelper#content_for].
  11. 9 module CaptureHelper
  12. # The capture method extracts part of a template as a String object.
  13. # You can then use this object anywhere in your templates, layout, or helpers.
  14. #
  15. # The capture method can be used in ERB templates...
  16. #
  17. # <% @greeting = capture do %>
  18. # Welcome to my shiny new web page! The date and time is
  19. # <%= Time.now %>
  20. # <% end %>
  21. #
  22. # ...and Builder (RXML) templates.
  23. #
  24. # @timestamp = capture do
  25. # "The current timestamp is #{Time.now}."
  26. # end
  27. #
  28. # You can then use that variable anywhere else. For example:
  29. #
  30. # <html>
  31. # <head><title><%= @greeting %></title></head>
  32. # <body>
  33. # <b><%= @greeting %></b>
  34. # </body>
  35. # </html>
  36. #
  37. # The return of capture is the string generated by the block. For Example:
  38. #
  39. # @greeting # => "Welcome to my shiny new web page! The date and time is 2018-09-06 11:09:16 -0500"
  40. #
  41. 9 def capture(*args)
  42. value = nil
  43. buffer = with_output_buffer { value = yield(*args) }
  44. if (string = buffer.presence || value) && string.is_a?(String)
  45. ERB::Util.html_escape string
  46. end
  47. end
  48. # Calling <tt>content_for</tt> stores a block of markup in an identifier for later use.
  49. # In order to access this stored content in other templates, helper modules
  50. # or the layout, you would pass the identifier as an argument to <tt>content_for</tt>.
  51. #
  52. # Note: <tt>yield</tt> can still be used to retrieve the stored content, but calling
  53. # <tt>yield</tt> doesn't work in helper modules, while <tt>content_for</tt> does.
  54. #
  55. # <% content_for :not_authorized do %>
  56. # alert('You are not authorized to do that!')
  57. # <% end %>
  58. #
  59. # You can then use <tt>content_for :not_authorized</tt> anywhere in your templates.
  60. #
  61. # <%= content_for :not_authorized if current_user.nil? %>
  62. #
  63. # This is equivalent to:
  64. #
  65. # <%= yield :not_authorized if current_user.nil? %>
  66. #
  67. # <tt>content_for</tt>, however, can also be used in helper modules.
  68. #
  69. # module StorageHelper
  70. # def stored_content
  71. # content_for(:storage) || "Your storage is empty"
  72. # end
  73. # end
  74. #
  75. # This helper works just like normal helpers.
  76. #
  77. # <%= stored_content %>
  78. #
  79. # You can also use the <tt>yield</tt> syntax alongside an existing call to
  80. # <tt>yield</tt> in a layout. For example:
  81. #
  82. # <%# This is the layout %>
  83. # <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  84. # <head>
  85. # <title>My Website</title>
  86. # <%= yield :script %>
  87. # </head>
  88. # <body>
  89. # <%= yield %>
  90. # </body>
  91. # </html>
  92. #
  93. # And now, we'll create a view that has a <tt>content_for</tt> call that
  94. # creates the <tt>script</tt> identifier.
  95. #
  96. # <%# This is our view %>
  97. # Please login!
  98. #
  99. # <% content_for :script do %>
  100. # <script>alert('You are not authorized to view this page!')</script>
  101. # <% end %>
  102. #
  103. # Then, in another view, you could to do something like this:
  104. #
  105. # <%= link_to 'Logout', action: 'logout', remote: true %>
  106. #
  107. # <% content_for :script do %>
  108. # <%= javascript_include_tag :defaults %>
  109. # <% end %>
  110. #
  111. # That will place +script+ tags for your default set of JavaScript files on the page;
  112. # this technique is useful if you'll only be using these scripts in a few views.
  113. #
  114. # Note that <tt>content_for</tt> concatenates (default) the blocks it is given for a particular
  115. # identifier in order. For example:
  116. #
  117. # <% content_for :navigation do %>
  118. # <li><%= link_to 'Home', action: 'index' %></li>
  119. # <% end %>
  120. #
  121. # And in another place:
  122. #
  123. # <% content_for :navigation do %>
  124. # <li><%= link_to 'Login', action: 'login' %></li>
  125. # <% end %>
  126. #
  127. # Then, in another template or layout, this code would render both links in order:
  128. #
  129. # <ul><%= content_for :navigation %></ul>
  130. #
  131. # If the flush parameter is +true+ <tt>content_for</tt> replaces the blocks it is given. For example:
  132. #
  133. # <% content_for :navigation do %>
  134. # <li><%= link_to 'Home', action: 'index' %></li>
  135. # <% end %>
  136. #
  137. # <%# Add some other content, or use a different template: %>
  138. #
  139. # <% content_for :navigation, flush: true do %>
  140. # <li><%= link_to 'Login', action: 'login' %></li>
  141. # <% end %>
  142. #
  143. # Then, in another template or layout, this code would render only the last link:
  144. #
  145. # <ul><%= content_for :navigation %></ul>
  146. #
  147. # Lastly, simple content can be passed as a parameter:
  148. #
  149. # <% content_for :script, javascript_include_tag(:defaults) %>
  150. #
  151. # WARNING: <tt>content_for</tt> is ignored in caches. So you shouldn't use it for elements that will be fragment cached.
  152. 9 def content_for(name, content = nil, options = {}, &block)
  153. if content || block_given?
  154. if block_given?
  155. options = content if content
  156. content = capture(&block)
  157. end
  158. if content
  159. options[:flush] ? @view_flow.set(name, content) : @view_flow.append(name, content)
  160. end
  161. nil
  162. else
  163. @view_flow.get(name).presence
  164. end
  165. end
  166. # The same as +content_for+ but when used with streaming flushes
  167. # straight back to the layout. In other words, if you want to
  168. # concatenate several times to the same buffer when rendering a given
  169. # template, you should use +content_for+, if not, use +provide+ to tell
  170. # the layout to stop looking for more contents.
  171. 9 def provide(name, content = nil, &block)
  172. content = capture(&block) if block_given?
  173. result = @view_flow.append!(name, content) if content
  174. result unless content
  175. end
  176. # <tt>content_for?</tt> checks whether any content has been captured yet using <tt>content_for</tt>.
  177. # Useful to render parts of your layout differently based on what is in your views.
  178. #
  179. # <%# This is the layout %>
  180. # <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  181. # <head>
  182. # <title>My Website</title>
  183. # <%= yield :script %>
  184. # </head>
  185. # <body class="<%= content_for?(:right_col) ? 'two-column' : 'one-column' %>">
  186. # <%= yield %>
  187. # <%= yield :right_col %>
  188. # </body>
  189. # </html>
  190. 9 def content_for?(name)
  191. @view_flow.get(name).present?
  192. end
  193. # Use an alternate output buffer for the duration of the block.
  194. # Defaults to a new empty string.
  195. 9 def with_output_buffer(buf = nil) #:nodoc:
  196. unless buf
  197. buf = ActionView::OutputBuffer.new
  198. if output_buffer && output_buffer.respond_to?(:encoding)
  199. buf.force_encoding(output_buffer.encoding)
  200. end
  201. end
  202. self.output_buffer, old_buffer = buf, output_buffer
  203. yield
  204. output_buffer
  205. ensure
  206. self.output_buffer = old_buffer
  207. end
  208. end
  209. end
  210. end

lib/action_view/helpers/controller_helper.rb

58.82% lines covered

17 relevant lines. 10 lines covered and 7 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 require "active_support/core_ext/module/attr_internal"
  3. 9 module ActionView
  4. 9 module Helpers #:nodoc:
  5. # This module keeps all methods and behavior in ActionView
  6. # that simply delegates to the controller.
  7. 9 module ControllerHelper #:nodoc:
  8. 9 attr_internal :controller, :request
  9. 9 CONTROLLER_DELEGATES = [:request_forgery_protection_token, :params,
  10. :session, :cookies, :response, :headers, :flash, :action_name,
  11. :controller_name, :controller_path]
  12. 9 delegate(*CONTROLLER_DELEGATES, to: :controller)
  13. 9 def assign_controller(controller)
  14. if @_controller = controller
  15. @_request = controller.request if controller.respond_to?(:request)
  16. @_config = controller.config.inheritable_copy if controller.respond_to?(:config)
  17. @_default_form_builder = controller.default_form_builder if controller.respond_to?(:default_form_builder)
  18. end
  19. end
  20. 9 def logger
  21. controller.logger if controller.respond_to?(:logger)
  22. end
  23. 9 def respond_to?(method_name, include_private = false)
  24. return controller.respond_to?(method_name) if CONTROLLER_DELEGATES.include?(method_name.to_sym)
  25. super
  26. end
  27. end
  28. end
  29. end

lib/action_view/helpers/csp_helper.rb

50.0% lines covered

8 relevant lines. 4 lines covered and 4 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 module ActionView
  3. # = Action View CSP Helper
  4. 9 module Helpers #:nodoc:
  5. 9 module CspHelper
  6. # Returns a meta tag "csp-nonce" with the per-session nonce value
  7. # for allowing inline <script> tags.
  8. #
  9. # <head>
  10. # <%= csp_meta_tag %>
  11. # </head>
  12. #
  13. # This is used by the Rails UJS helper to create dynamically
  14. # loaded inline <script> elements.
  15. #
  16. 9 def csp_meta_tag(**options)
  17. if content_security_policy?
  18. options[:name] = "csp-nonce"
  19. options[:content] = content_security_policy_nonce
  20. tag("meta", options)
  21. end
  22. end
  23. end
  24. end
  25. end

lib/action_view/helpers/csrf_helper.rb

71.43% lines covered

7 relevant lines. 5 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 module ActionView
  3. # = Action View CSRF Helper
  4. 9 module Helpers #:nodoc:
  5. 9 module CsrfHelper
  6. # Returns meta tags "csrf-param" and "csrf-token" with the name of the cross-site
  7. # request forgery protection parameter and token, respectively.
  8. #
  9. # <head>
  10. # <%= csrf_meta_tags %>
  11. # </head>
  12. #
  13. # These are used to generate the dynamic forms that implement non-remote links with
  14. # <tt>:method</tt>.
  15. #
  16. # You don't need to use these tags for regular forms as they generate their own hidden fields.
  17. #
  18. # For AJAX requests other than GETs, extract the "csrf-token" from the meta-tag and send as the
  19. # "X-CSRF-Token" HTTP header. If you are using rails-ujs this happens automatically.
  20. #
  21. 9 def csrf_meta_tags
  22. if defined?(protect_against_forgery?) && protect_against_forgery?
  23. [
  24. tag("meta", name: "csrf-param", content: request_forgery_protection_token),
  25. tag("meta", name: "csrf-token", content: form_authenticity_token)
  26. ].join("\n").html_safe
  27. end
  28. end
  29. # For backwards compatibility.
  30. 9 alias csrf_meta_tag csrf_meta_tags
  31. end
  32. end
  33. end

lib/action_view/helpers/date_helper.rb

23.05% lines covered

308 relevant lines. 71 lines covered and 237 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 require "date"
  3. 9 require "action_view/helpers/tag_helper"
  4. 9 require "active_support/core_ext/array/extract_options"
  5. 9 require "active_support/core_ext/date/conversions"
  6. 9 require "active_support/core_ext/hash/slice"
  7. 9 require "active_support/core_ext/object/acts_like"
  8. 9 require "active_support/core_ext/object/with_options"
  9. 9 module ActionView
  10. 9 module Helpers #:nodoc:
  11. # = Action View Date Helpers
  12. #
  13. # The Date Helper primarily creates select/option tags for different kinds of dates and times or date and time
  14. # elements. All of the select-type methods share a number of common options that are as follows:
  15. #
  16. # * <tt>:prefix</tt> - overwrites the default prefix of "date" used for the select names. So specifying "birthday"
  17. # would give \birthday[month] instead of \date[month] if passed to the <tt>select_month</tt> method.
  18. # * <tt>:include_blank</tt> - set to true if it should be possible to set an empty date.
  19. # * <tt>:discard_type</tt> - set to true if you want to discard the type part of the select name. If set to true,
  20. # the <tt>select_month</tt> method would use simply "date" (which can be overwritten using <tt>:prefix</tt>) instead
  21. # of \date[month].
  22. 9 module DateHelper
  23. 9 MINUTES_IN_YEAR = 525600
  24. 9 MINUTES_IN_QUARTER_YEAR = 131400
  25. 9 MINUTES_IN_THREE_QUARTERS_YEAR = 394200
  26. # Reports the approximate distance in time between two Time, Date or DateTime objects or integers as seconds.
  27. # Pass <tt>include_seconds: true</tt> if you want more detailed approximations when distance < 1 min, 29 secs.
  28. # Distances are reported based on the following table:
  29. #
  30. # 0 <-> 29 secs # => less than a minute
  31. # 30 secs <-> 1 min, 29 secs # => 1 minute
  32. # 1 min, 30 secs <-> 44 mins, 29 secs # => [2..44] minutes
  33. # 44 mins, 30 secs <-> 89 mins, 29 secs # => about 1 hour
  34. # 89 mins, 30 secs <-> 23 hrs, 59 mins, 29 secs # => about [2..24] hours
  35. # 23 hrs, 59 mins, 30 secs <-> 41 hrs, 59 mins, 29 secs # => 1 day
  36. # 41 hrs, 59 mins, 30 secs <-> 29 days, 23 hrs, 59 mins, 29 secs # => [2..29] days
  37. # 29 days, 23 hrs, 59 mins, 30 secs <-> 44 days, 23 hrs, 59 mins, 29 secs # => about 1 month
  38. # 44 days, 23 hrs, 59 mins, 30 secs <-> 59 days, 23 hrs, 59 mins, 29 secs # => about 2 months
  39. # 59 days, 23 hrs, 59 mins, 30 secs <-> 1 yr minus 1 sec # => [2..12] months
  40. # 1 yr <-> 1 yr, 3 months # => about 1 year
  41. # 1 yr, 3 months <-> 1 yr, 9 months # => over 1 year
  42. # 1 yr, 9 months <-> 2 yr minus 1 sec # => almost 2 years
  43. # 2 yrs <-> max time or date # => (same rules as 1 yr)
  44. #
  45. # With <tt>include_seconds: true</tt> and the difference < 1 minute 29 seconds:
  46. # 0-4 secs # => less than 5 seconds
  47. # 5-9 secs # => less than 10 seconds
  48. # 10-19 secs # => less than 20 seconds
  49. # 20-39 secs # => half a minute
  50. # 40-59 secs # => less than a minute
  51. # 60-89 secs # => 1 minute
  52. #
  53. # from_time = Time.now
  54. # distance_of_time_in_words(from_time, from_time + 50.minutes) # => about 1 hour
  55. # distance_of_time_in_words(from_time, 50.minutes.from_now) # => about 1 hour
  56. # distance_of_time_in_words(from_time, from_time + 15.seconds) # => less than a minute
  57. # distance_of_time_in_words(from_time, from_time + 15.seconds, include_seconds: true) # => less than 20 seconds
  58. # distance_of_time_in_words(from_time, 3.years.from_now) # => about 3 years
  59. # distance_of_time_in_words(from_time, from_time + 60.hours) # => 3 days
  60. # distance_of_time_in_words(from_time, from_time + 45.seconds, include_seconds: true) # => less than a minute
  61. # distance_of_time_in_words(from_time, from_time - 45.seconds, include_seconds: true) # => less than a minute
  62. # distance_of_time_in_words(from_time, 76.seconds.from_now) # => 1 minute
  63. # distance_of_time_in_words(from_time, from_time + 1.year + 3.days) # => about 1 year
  64. # distance_of_time_in_words(from_time, from_time + 3.years + 6.months) # => over 3 years
  65. # distance_of_time_in_words(from_time, from_time + 4.years + 9.days + 30.minutes + 5.seconds) # => about 4 years
  66. #
  67. # to_time = Time.now + 6.years + 19.days
  68. # distance_of_time_in_words(from_time, to_time, include_seconds: true) # => about 6 years
  69. # distance_of_time_in_words(to_time, from_time, include_seconds: true) # => about 6 years
  70. # distance_of_time_in_words(Time.now, Time.now) # => less than a minute
  71. #
  72. # With the <tt>scope</tt> option, you can define a custom scope for Rails
  73. # to look up the translation.
  74. #
  75. # For example you can define the following in your locale (e.g. en.yml).
  76. #
  77. # datetime:
  78. # distance_in_words:
  79. # short:
  80. # about_x_hours:
  81. # one: 'an hour'
  82. # other: '%{count} hours'
  83. #
  84. # See https://github.com/svenfuchs/rails-i18n/blob/master/rails/locale/en.yml
  85. # for more examples.
  86. #
  87. # Which will then result in the following:
  88. #
  89. # from_time = Time.now
  90. # distance_of_time_in_words(from_time, from_time + 50.minutes, scope: 'datetime.distance_in_words.short') # => "an hour"
  91. # distance_of_time_in_words(from_time, from_time + 3.hours, scope: 'datetime.distance_in_words.short') # => "3 hours"
  92. 9 def distance_of_time_in_words(from_time, to_time = 0, options = {})
  93. options = {
  94. scope: :'datetime.distance_in_words'
  95. }.merge!(options)
  96. from_time = normalize_distance_of_time_argument_to_time(from_time)
  97. to_time = normalize_distance_of_time_argument_to_time(to_time)
  98. from_time, to_time = to_time, from_time if from_time > to_time
  99. distance_in_minutes = ((to_time - from_time) / 60.0).round
  100. distance_in_seconds = (to_time - from_time).round
  101. I18n.with_options locale: options[:locale], scope: options[:scope] do |locale|
  102. case distance_in_minutes
  103. when 0..1
  104. return distance_in_minutes == 0 ?
  105. locale.t(:less_than_x_minutes, count: 1) :
  106. locale.t(:x_minutes, count: distance_in_minutes) unless options[:include_seconds]
  107. case distance_in_seconds
  108. when 0..4 then locale.t :less_than_x_seconds, count: 5
  109. when 5..9 then locale.t :less_than_x_seconds, count: 10
  110. when 10..19 then locale.t :less_than_x_seconds, count: 20
  111. when 20..39 then locale.t :half_a_minute
  112. when 40..59 then locale.t :less_than_x_minutes, count: 1
  113. else locale.t :x_minutes, count: 1
  114. end
  115. when 2...45 then locale.t :x_minutes, count: distance_in_minutes
  116. when 45...90 then locale.t :about_x_hours, count: 1
  117. # 90 mins up to 24 hours
  118. when 90...1440 then locale.t :about_x_hours, count: (distance_in_minutes.to_f / 60.0).round
  119. # 24 hours up to 42 hours
  120. when 1440...2520 then locale.t :x_days, count: 1
  121. # 42 hours up to 30 days
  122. when 2520...43200 then locale.t :x_days, count: (distance_in_minutes.to_f / 1440.0).round
  123. # 30 days up to 60 days
  124. when 43200...86400 then locale.t :about_x_months, count: (distance_in_minutes.to_f / 43200.0).round
  125. # 60 days up to 365 days
  126. when 86400...525600 then locale.t :x_months, count: (distance_in_minutes.to_f / 43200.0).round
  127. else
  128. from_year = from_time.year
  129. from_year += 1 if from_time.month >= 3
  130. to_year = to_time.year
  131. to_year -= 1 if to_time.month < 3
  132. leap_years = (from_year > to_year) ? 0 : (from_year..to_year).count { |x| Date.leap?(x) }
  133. minute_offset_for_leap_year = leap_years * 1440
  134. # Discount the leap year days when calculating year distance.
  135. # e.g. if there are 20 leap year days between 2 dates having the same day
  136. # and month then the based on 365 days calculation
  137. # the distance in years will come out to over 80 years when in written
  138. # English it would read better as about 80 years.
  139. minutes_with_offset = distance_in_minutes - minute_offset_for_leap_year
  140. remainder = (minutes_with_offset % MINUTES_IN_YEAR)
  141. distance_in_years = (minutes_with_offset.div MINUTES_IN_YEAR)
  142. if remainder < MINUTES_IN_QUARTER_YEAR
  143. locale.t(:about_x_years, count: distance_in_years)
  144. elsif remainder < MINUTES_IN_THREE_QUARTERS_YEAR
  145. locale.t(:over_x_years, count: distance_in_years)
  146. else
  147. locale.t(:almost_x_years, count: distance_in_years + 1)
  148. end
  149. end
  150. end
  151. end
  152. # Like <tt>distance_of_time_in_words</tt>, but where <tt>to_time</tt> is fixed to <tt>Time.now</tt>.
  153. #
  154. # time_ago_in_words(3.minutes.from_now) # => 3 minutes
  155. # time_ago_in_words(3.minutes.ago) # => 3 minutes
  156. # time_ago_in_words(Time.now - 15.hours) # => about 15 hours
  157. # time_ago_in_words(Time.now) # => less than a minute
  158. # time_ago_in_words(Time.now, include_seconds: true) # => less than 5 seconds
  159. #
  160. # from_time = Time.now - 3.days - 14.minutes - 25.seconds
  161. # time_ago_in_words(from_time) # => 3 days
  162. #
  163. # from_time = (3.days + 14.minutes + 25.seconds).ago
  164. # time_ago_in_words(from_time) # => 3 days
  165. #
  166. # Note that you cannot pass a <tt>Numeric</tt> value to <tt>time_ago_in_words</tt>.
  167. #
  168. 9 def time_ago_in_words(from_time, options = {})
  169. distance_of_time_in_words(from_time, Time.now, options)
  170. end
  171. 9 alias_method :distance_of_time_in_words_to_now, :time_ago_in_words
  172. # Returns a set of select tags (one for year, month, and day) pre-selected for accessing a specified date-based
  173. # attribute (identified by +method+) on an object assigned to the template (identified by +object+).
  174. #
  175. # ==== Options
  176. # * <tt>:use_month_numbers</tt> - Set to true if you want to use month numbers rather than month names (e.g.
  177. # "2" instead of "February").
  178. # * <tt>:use_two_digit_numbers</tt> - Set to true if you want to display two digit month and day numbers (e.g.
  179. # "02" instead of "February" and "08" instead of "8").
  180. # * <tt>:use_short_month</tt> - Set to true if you want to use abbreviated month names instead of full
  181. # month names (e.g. "Feb" instead of "February").
  182. # * <tt>:add_month_numbers</tt> - Set to true if you want to use both month numbers and month names (e.g.
  183. # "2 - February" instead of "February").
  184. # * <tt>:use_month_names</tt> - Set to an array with 12 month names if you want to customize month names.
  185. # Note: You can also use Rails' i18n functionality for this.
  186. # * <tt>:month_format_string</tt> - Set to a format string. The string gets passed keys +:number+ (integer)
  187. # and +:name+ (string). A format string would be something like "%{name} (%<number>02d)" for example.
  188. # See <tt>Kernel.sprintf</tt> for documentation on format sequences.
  189. # * <tt>:date_separator</tt> - Specifies a string to separate the date fields. Default is "" (i.e. nothing).
  190. # * <tt>:time_separator</tt> - Specifies a string to separate the time fields. Default is " : ".
  191. # * <tt>:datetime_separator</tt>- Specifies a string to separate the date and time fields. Default is " &mdash; ".
  192. # * <tt>:start_year</tt> - Set the start year for the year select. Default is <tt>Date.today.year - 5</tt> if
  193. # you are creating new record. While editing existing record, <tt>:start_year</tt> defaults to
  194. # the current selected year minus 5.
  195. # * <tt>:end_year</tt> - Set the end year for the year select. Default is <tt>Date.today.year + 5</tt> if
  196. # you are creating new record. While editing existing record, <tt>:end_year</tt> defaults to
  197. # the current selected year plus 5.
  198. # * <tt>:year_format</tt> - Set format of years for year select. Lambda should be passed.
  199. # * <tt>:discard_day</tt> - Set to true if you don't want to show a day select. This includes the day
  200. # as a hidden field instead of showing a select field. Also note that this implicitly sets the day to be the
  201. # first of the given month in order to not create invalid dates like 31 February.
  202. # * <tt>:discard_month</tt> - Set to true if you don't want to show a month select. This includes the month
  203. # as a hidden field instead of showing a select field. Also note that this implicitly sets :discard_day to true.
  204. # * <tt>:discard_year</tt> - Set to true if you don't want to show a year select. This includes the year
  205. # as a hidden field instead of showing a select field.
  206. # * <tt>:order</tt> - Set to an array containing <tt>:day</tt>, <tt>:month</tt> and <tt>:year</tt> to
  207. # customize the order in which the select fields are shown. If you leave out any of the symbols, the respective
  208. # select will not be shown (like when you set <tt>discard_xxx: true</tt>. Defaults to the order defined in
  209. # the respective locale (e.g. [:year, :month, :day] in the en locale that ships with Rails).
  210. # * <tt>:include_blank</tt> - Include a blank option in every select field so it's possible to set empty
  211. # dates.
  212. # * <tt>:default</tt> - Set a default date if the affected date isn't set or is +nil+.
  213. # * <tt>:selected</tt> - Set a date that overrides the actual value.
  214. # * <tt>:disabled</tt> - Set to true if you want show the select fields as disabled.
  215. # * <tt>:prompt</tt> - Set to true (for a generic prompt), a prompt string or a hash of prompt strings
  216. # for <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>, <tt>:hour</tt>, <tt>:minute</tt> and <tt>:second</tt>.
  217. # Setting this option prepends a select option with a generic prompt (Day, Month, Year, Hour, Minute, Seconds)
  218. # or the given prompt string.
  219. # * <tt>:with_css_classes</tt> - Set to true or a hash of strings. Use true if you want to assign generic styles for
  220. # select tags. This automatically set classes 'year', 'month', 'day', 'hour', 'minute' and 'second'. A hash of
  221. # strings for <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>, <tt>:hour</tt>, <tt>:minute</tt>, <tt>:second</tt>
  222. # will extend the select type with the given value. Use +html_options+ to modify every select tag in the set.
  223. # * <tt>:use_hidden</tt> - Set to true if you only want to generate hidden input tags.
  224. #
  225. # If anything is passed in the +html_options+ hash it will be applied to every select tag in the set.
  226. #
  227. # NOTE: Discarded selects will default to 1. So if no month select is available, January will be assumed.
  228. #
  229. # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute.
  230. # date_select("article", "written_on")
  231. #
  232. # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute,
  233. # # with the year in the year drop down box starting at 1995.
  234. # date_select("article", "written_on", start_year: 1995)
  235. #
  236. # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute,
  237. # # with the year in the year drop down box starting at 1995, numbers used for months instead of words,
  238. # # and without a day select box.
  239. # date_select("article", "written_on", start_year: 1995, use_month_numbers: true,
  240. # discard_day: true, include_blank: true)
  241. #
  242. # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute,
  243. # # with two digit numbers used for months and days.
  244. # date_select("article", "written_on", use_two_digit_numbers: true)
  245. #
  246. # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute
  247. # # with the fields ordered as day, month, year rather than month, day, year.
  248. # date_select("article", "written_on", order: [:day, :month, :year])
  249. #
  250. # # Generates a date select that when POSTed is stored in the user variable, in the birthday attribute
  251. # # lacking a year field.
  252. # date_select("user", "birthday", order: [:month, :day])
  253. #
  254. # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute
  255. # # which is initially set to the date 3 days from the current date
  256. # date_select("article", "written_on", default: 3.days.from_now)
  257. #
  258. # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute
  259. # # which is set in the form with today's date, regardless of the value in the Active Record object.
  260. # date_select("article", "written_on", selected: Date.today)
  261. #
  262. # # Generates a date select that when POSTed is stored in the credit_card variable, in the bill_due attribute
  263. # # that will have a default day of 20.
  264. # date_select("credit_card", "bill_due", default: { day: 20 })
  265. #
  266. # # Generates a date select with custom prompts.
  267. # date_select("article", "written_on", prompt: { day: 'Select day', month: 'Select month', year: 'Select year' })
  268. #
  269. # # Generates a date select with custom year format.
  270. # date_select("article", "written_on", year_format: ->(year) { "Heisei #{year - 1988}" })
  271. #
  272. # The selects are prepared for multi-parameter assignment to an Active Record object.
  273. #
  274. # Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that
  275. # all month choices are valid.
  276. 9 def date_select(object_name, method, options = {}, html_options = {})
  277. Tags::DateSelect.new(object_name, method, self, options, html_options).render
  278. end
  279. # Returns a set of select tags (one for hour, minute and optionally second) pre-selected for accessing a
  280. # specified time-based attribute (identified by +method+) on an object assigned to the template (identified by
  281. # +object+). You can include the seconds with <tt>:include_seconds</tt>. You can get hours in the AM/PM format
  282. # with <tt>:ampm</tt> option.
  283. #
  284. # This method will also generate 3 input hidden tags, for the actual year, month and day unless the option
  285. # <tt>:ignore_date</tt> is set to +true+. If you set the <tt>:ignore_date</tt> to +true+, you must have a
  286. # +date_select+ on the same method within the form otherwise an exception will be raised.
  287. #
  288. # If anything is passed in the html_options hash it will be applied to every select tag in the set.
  289. #
  290. # # Creates a time select tag that, when POSTed, will be stored in the article variable in the sunrise attribute.
  291. # time_select("article", "sunrise")
  292. #
  293. # # Creates a time select tag with a seconds field that, when POSTed, will be stored in the article variables in
  294. # # the sunrise attribute.
  295. # time_select("article", "start_time", include_seconds: true)
  296. #
  297. # # You can set the <tt>:minute_step</tt> to 15 which will give you: 00, 15, 30, and 45.
  298. # time_select 'game', 'game_time', { minute_step: 15 }
  299. #
  300. # # Creates a time select tag with a custom prompt. Use <tt>prompt: true</tt> for generic prompts.
  301. # time_select("article", "written_on", prompt: { hour: 'Choose hour', minute: 'Choose minute', second: 'Choose seconds' })
  302. # time_select("article", "written_on", prompt: { hour: true }) # generic prompt for hours
  303. # time_select("article", "written_on", prompt: true) # generic prompts for all
  304. #
  305. # # You can set :ampm option to true which will show the hours as: 12 PM, 01 AM .. 11 PM.
  306. # time_select 'game', 'game_time', { ampm: true }
  307. #
  308. # The selects are prepared for multi-parameter assignment to an Active Record object.
  309. #
  310. # Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that
  311. # all month choices are valid.
  312. 9 def time_select(object_name, method, options = {}, html_options = {})
  313. Tags::TimeSelect.new(object_name, method, self, options, html_options).render
  314. end
  315. # Returns a set of select tags (one for year, month, day, hour, and minute) pre-selected for accessing a
  316. # specified datetime-based attribute (identified by +method+) on an object assigned to the template (identified
  317. # by +object+).
  318. #
  319. # If anything is passed in the html_options hash it will be applied to every select tag in the set.
  320. #
  321. # # Generates a datetime select that, when POSTed, will be stored in the article variable in the written_on
  322. # # attribute.
  323. # datetime_select("article", "written_on")
  324. #
  325. # # Generates a datetime select with a year select that starts at 1995 that, when POSTed, will be stored in the
  326. # # article variable in the written_on attribute.
  327. # datetime_select("article", "written_on", start_year: 1995)
  328. #
  329. # # Generates a datetime select with a default value of 3 days from the current time that, when POSTed, will
  330. # # be stored in the trip variable in the departing attribute.
  331. # datetime_select("trip", "departing", default: 3.days.from_now)
  332. #
  333. # # Generate a datetime select with hours in the AM/PM format
  334. # datetime_select("article", "written_on", ampm: true)
  335. #
  336. # # Generates a datetime select that discards the type that, when POSTed, will be stored in the article variable
  337. # # as the written_on attribute.
  338. # datetime_select("article", "written_on", discard_type: true)
  339. #
  340. # # Generates a datetime select with a custom prompt. Use <tt>prompt: true</tt> for generic prompts.
  341. # datetime_select("article", "written_on", prompt: { day: 'Choose day', month: 'Choose month', year: 'Choose year' })
  342. # datetime_select("article", "written_on", prompt: { hour: true }) # generic prompt for hours
  343. # datetime_select("article", "written_on", prompt: true) # generic prompts for all
  344. #
  345. # The selects are prepared for multi-parameter assignment to an Active Record object.
  346. 9 def datetime_select(object_name, method, options = {}, html_options = {})
  347. Tags::DatetimeSelect.new(object_name, method, self, options, html_options).render
  348. end
  349. # Returns a set of HTML select-tags (one for year, month, day, hour, minute, and second) pre-selected with the
  350. # +datetime+. It's also possible to explicitly set the order of the tags using the <tt>:order</tt> option with
  351. # an array of symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. If you do not
  352. # supply a Symbol, it will be appended onto the <tt>:order</tt> passed in. You can also add
  353. # <tt>:date_separator</tt>, <tt>:datetime_separator</tt> and <tt>:time_separator</tt> keys to the +options+ to
  354. # control visual display of the elements.
  355. #
  356. # If anything is passed in the html_options hash it will be applied to every select tag in the set.
  357. #
  358. # my_date_time = Time.now + 4.days
  359. #
  360. # # Generates a datetime select that defaults to the datetime in my_date_time (four days after today).
  361. # select_datetime(my_date_time)
  362. #
  363. # # Generates a datetime select that defaults to today (no specified datetime)
  364. # select_datetime()
  365. #
  366. # # Generates a datetime select that defaults to the datetime in my_date_time (four days after today)
  367. # # with the fields ordered year, month, day rather than month, day, year.
  368. # select_datetime(my_date_time, order: [:year, :month, :day])
  369. #
  370. # # Generates a datetime select that defaults to the datetime in my_date_time (four days after today)
  371. # # with a '/' between each date field.
  372. # select_datetime(my_date_time, date_separator: '/')
  373. #
  374. # # Generates a datetime select that defaults to the datetime in my_date_time (four days after today)
  375. # # with a date fields separated by '/', time fields separated by '' and the date and time fields
  376. # # separated by a comma (',').
  377. # select_datetime(my_date_time, date_separator: '/', time_separator: '', datetime_separator: ',')
  378. #
  379. # # Generates a datetime select that discards the type of the field and defaults to the datetime in
  380. # # my_date_time (four days after today)
  381. # select_datetime(my_date_time, discard_type: true)
  382. #
  383. # # Generate a datetime field with hours in the AM/PM format
  384. # select_datetime(my_date_time, ampm: true)
  385. #
  386. # # Generates a datetime select that defaults to the datetime in my_date_time (four days after today)
  387. # # prefixed with 'payday' rather than 'date'
  388. # select_datetime(my_date_time, prefix: 'payday')
  389. #
  390. # # Generates a datetime select with a custom prompt. Use <tt>prompt: true</tt> for generic prompts.
  391. # select_datetime(my_date_time, prompt: { day: 'Choose day', month: 'Choose month', year: 'Choose year' })
  392. # select_datetime(my_date_time, prompt: { hour: true }) # generic prompt for hours
  393. # select_datetime(my_date_time, prompt: true) # generic prompts for all
  394. 9 def select_datetime(datetime = Time.current, options = {}, html_options = {})
  395. DateTimeSelector.new(datetime, options, html_options).select_datetime
  396. end
  397. # Returns a set of HTML select-tags (one for year, month, and day) pre-selected with the +date+.
  398. # It's possible to explicitly set the order of the tags using the <tt>:order</tt> option with an array of
  399. # symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order.
  400. # If the array passed to the <tt>:order</tt> option does not contain all the three symbols, all tags will be hidden.
  401. #
  402. # If anything is passed in the html_options hash it will be applied to every select tag in the set.
  403. #
  404. # my_date = Time.now + 6.days
  405. #
  406. # # Generates a date select that defaults to the date in my_date (six days after today).
  407. # select_date(my_date)
  408. #
  409. # # Generates a date select that defaults to today (no specified date).
  410. # select_date()
  411. #
  412. # # Generates a date select that defaults to the date in my_date (six days after today)
  413. # # with the fields ordered year, month, day rather than month, day, year.
  414. # select_date(my_date, order: [:year, :month, :day])
  415. #
  416. # # Generates a date select that discards the type of the field and defaults to the date in
  417. # # my_date (six days after today).
  418. # select_date(my_date, discard_type: true)
  419. #
  420. # # Generates a date select that defaults to the date in my_date,
  421. # # which has fields separated by '/'.
  422. # select_date(my_date, date_separator: '/')
  423. #
  424. # # Generates a date select that defaults to the datetime in my_date (six days after today)
  425. # # prefixed with 'payday' rather than 'date'.
  426. # select_date(my_date, prefix: 'payday')
  427. #
  428. # # Generates a date select with a custom prompt. Use <tt>prompt: true</tt> for generic prompts.
  429. # select_date(my_date, prompt: { day: 'Choose day', month: 'Choose month', year: 'Choose year' })
  430. # select_date(my_date, prompt: { hour: true }) # generic prompt for hours
  431. # select_date(my_date, prompt: true) # generic prompts for all
  432. 9 def select_date(date = Date.current, options = {}, html_options = {})
  433. DateTimeSelector.new(date, options, html_options).select_date
  434. end
  435. # Returns a set of HTML select-tags (one for hour and minute).
  436. # You can set <tt>:time_separator</tt> key to format the output, and
  437. # the <tt>:include_seconds</tt> option to include an input for seconds.
  438. #
  439. # If anything is passed in the html_options hash it will be applied to every select tag in the set.
  440. #
  441. # my_time = Time.now + 5.days + 7.hours + 3.minutes + 14.seconds
  442. #
  443. # # Generates a time select that defaults to the time in my_time.
  444. # select_time(my_time)
  445. #
  446. # # Generates a time select that defaults to the current time (no specified time).
  447. # select_time()
  448. #
  449. # # Generates a time select that defaults to the time in my_time,
  450. # # which has fields separated by ':'.
  451. # select_time(my_time, time_separator: ':')
  452. #
  453. # # Generates a time select that defaults to the time in my_time,
  454. # # that also includes an input for seconds.
  455. # select_time(my_time, include_seconds: true)
  456. #
  457. # # Generates a time select that defaults to the time in my_time, that has fields
  458. # # separated by ':' and includes an input for seconds.
  459. # select_time(my_time, time_separator: ':', include_seconds: true)
  460. #
  461. # # Generate a time select field with hours in the AM/PM format
  462. # select_time(my_time, ampm: true)
  463. #
  464. # # Generates a time select field with hours that range from 2 to 14
  465. # select_time(my_time, start_hour: 2, end_hour: 14)
  466. #
  467. # # Generates a time select with a custom prompt. Use <tt>:prompt</tt> to true for generic prompts.
  468. # select_time(my_time, prompt: { day: 'Choose day', month: 'Choose month', year: 'Choose year' })
  469. # select_time(my_time, prompt: { hour: true }) # generic prompt for hours
  470. # select_time(my_time, prompt: true) # generic prompts for all
  471. 9 def select_time(datetime = Time.current, options = {}, html_options = {})
  472. DateTimeSelector.new(datetime, options, html_options).select_time
  473. end
  474. # Returns a select tag with options for each of the seconds 0 through 59 with the current second selected.
  475. # The <tt>datetime</tt> can be either a +Time+ or +DateTime+ object or an integer.
  476. # Override the field name using the <tt>:field_name</tt> option, 'second' by default.
  477. #
  478. # my_time = Time.now + 16.seconds
  479. #
  480. # # Generates a select field for seconds that defaults to the seconds for the time in my_time.
  481. # select_second(my_time)
  482. #
  483. # # Generates a select field for seconds that defaults to the number given.
  484. # select_second(33)
  485. #
  486. # # Generates a select field for seconds that defaults to the seconds for the time in my_time
  487. # # that is named 'interval' rather than 'second'.
  488. # select_second(my_time, field_name: 'interval')
  489. #
  490. # # Generates a select field for seconds with a custom prompt. Use <tt>prompt: true</tt> for a
  491. # # generic prompt.
  492. # select_second(14, prompt: 'Choose seconds')
  493. 9 def select_second(datetime, options = {}, html_options = {})
  494. DateTimeSelector.new(datetime, options, html_options).select_second
  495. end
  496. # Returns a select tag with options for each of the minutes 0 through 59 with the current minute selected.
  497. # Also can return a select tag with options by <tt>minute_step</tt> from 0 through 59 with the 00 minute
  498. # selected. The <tt>datetime</tt> can be either a +Time+ or +DateTime+ object or an integer.
  499. # Override the field name using the <tt>:field_name</tt> option, 'minute' by default.
  500. #
  501. # my_time = Time.now + 10.minutes
  502. #
  503. # # Generates a select field for minutes that defaults to the minutes for the time in my_time.
  504. # select_minute(my_time)
  505. #
  506. # # Generates a select field for minutes that defaults to the number given.
  507. # select_minute(14)
  508. #
  509. # # Generates a select field for minutes that defaults to the minutes for the time in my_time
  510. # # that is named 'moment' rather than 'minute'.
  511. # select_minute(my_time, field_name: 'moment')
  512. #
  513. # # Generates a select field for minutes with a custom prompt. Use <tt>prompt: true</tt> for a
  514. # # generic prompt.
  515. # select_minute(14, prompt: 'Choose minutes')
  516. 9 def select_minute(datetime, options = {}, html_options = {})
  517. DateTimeSelector.new(datetime, options, html_options).select_minute
  518. end
  519. # Returns a select tag with options for each of the hours 0 through 23 with the current hour selected.
  520. # The <tt>datetime</tt> can be either a +Time+ or +DateTime+ object or an integer.
  521. # Override the field name using the <tt>:field_name</tt> option, 'hour' by default.
  522. #
  523. # my_time = Time.now + 6.hours
  524. #
  525. # # Generates a select field for hours that defaults to the hour for the time in my_time.
  526. # select_hour(my_time)
  527. #
  528. # # Generates a select field for hours that defaults to the number given.
  529. # select_hour(13)
  530. #
  531. # # Generates a select field for hours that defaults to the hour for the time in my_time
  532. # # that is named 'stride' rather than 'hour'.
  533. # select_hour(my_time, field_name: 'stride')
  534. #
  535. # # Generates a select field for hours with a custom prompt. Use <tt>prompt: true</tt> for a
  536. # # generic prompt.
  537. # select_hour(13, prompt: 'Choose hour')
  538. #
  539. # # Generate a select field for hours in the AM/PM format
  540. # select_hour(my_time, ampm: true)
  541. #
  542. # # Generates a select field that includes options for hours from 2 to 14.
  543. # select_hour(my_time, start_hour: 2, end_hour: 14)
  544. 9 def select_hour(datetime, options = {}, html_options = {})
  545. DateTimeSelector.new(datetime, options, html_options).select_hour
  546. end
  547. # Returns a select tag with options for each of the days 1 through 31 with the current day selected.
  548. # The <tt>date</tt> can also be substituted for a day number.
  549. # If you want to display days with a leading zero set the <tt>:use_two_digit_numbers</tt> key in +options+ to true.
  550. # Override the field name using the <tt>:field_name</tt> option, 'day' by default.
  551. #
  552. # my_date = Time.now + 2.days
  553. #
  554. # # Generates a select field for days that defaults to the day for the date in my_date.
  555. # select_day(my_date)
  556. #
  557. # # Generates a select field for days that defaults to the number given.
  558. # select_day(5)
  559. #
  560. # # Generates a select field for days that defaults to the number given, but displays it with two digits.
  561. # select_day(5, use_two_digit_numbers: true)
  562. #
  563. # # Generates a select field for days that defaults to the day for the date in my_date
  564. # # that is named 'due' rather than 'day'.
  565. # select_day(my_date, field_name: 'due')
  566. #
  567. # # Generates a select field for days with a custom prompt. Use <tt>prompt: true</tt> for a
  568. # # generic prompt.
  569. # select_day(5, prompt: 'Choose day')
  570. 9 def select_day(date, options = {}, html_options = {})
  571. DateTimeSelector.new(date, options, html_options).select_day
  572. end
  573. # Returns a select tag with options for each of the months January through December with the current month
  574. # selected. The month names are presented as keys (what's shown to the user) and the month numbers (1-12) are
  575. # used as values (what's submitted to the server). It's also possible to use month numbers for the presentation
  576. # instead of names -- set the <tt>:use_month_numbers</tt> key in +options+ to true for this to happen. If you
  577. # want both numbers and names, set the <tt>:add_month_numbers</tt> key in +options+ to true. If you would prefer
  578. # to show month names as abbreviations, set the <tt>:use_short_month</tt> key in +options+ to true. If you want
  579. # to use your own month names, set the <tt>:use_month_names</tt> key in +options+ to an array of 12 month names.
  580. # If you want to display months with a leading zero set the <tt>:use_two_digit_numbers</tt> key in +options+ to true.
  581. # Override the field name using the <tt>:field_name</tt> option, 'month' by default.
  582. #
  583. # # Generates a select field for months that defaults to the current month that
  584. # # will use keys like "January", "March".
  585. # select_month(Date.today)
  586. #
  587. # # Generates a select field for months that defaults to the current month that
  588. # # is named "start" rather than "month".
  589. # select_month(Date.today, field_name: 'start')
  590. #
  591. # # Generates a select field for months that defaults to the current month that
  592. # # will use keys like "1", "3".
  593. # select_month(Date.today, use_month_numbers: true)
  594. #
  595. # # Generates a select field for months that defaults to the current month that
  596. # # will use keys like "1 - January", "3 - March".
  597. # select_month(Date.today, add_month_numbers: true)
  598. #
  599. # # Generates a select field for months that defaults to the current month that
  600. # # will use keys like "Jan", "Mar".
  601. # select_month(Date.today, use_short_month: true)
  602. #
  603. # # Generates a select field for months that defaults to the current month that
  604. # # will use keys like "Januar", "Marts."
  605. # select_month(Date.today, use_month_names: %w(Januar Februar Marts ...))
  606. #
  607. # # Generates a select field for months that defaults to the current month that
  608. # # will use keys with two digit numbers like "01", "03".
  609. # select_month(Date.today, use_two_digit_numbers: true)
  610. #
  611. # # Generates a select field for months with a custom prompt. Use <tt>prompt: true</tt> for a
  612. # # generic prompt.
  613. # select_month(14, prompt: 'Choose month')
  614. 9 def select_month(date, options = {}, html_options = {})
  615. DateTimeSelector.new(date, options, html_options).select_month
  616. end
  617. # Returns a select tag with options for each of the five years on each side of the current, which is selected.
  618. # The five year radius can be changed using the <tt>:start_year</tt> and <tt>:end_year</tt> keys in the
  619. # +options+. Both ascending and descending year lists are supported by making <tt>:start_year</tt> less than or
  620. # greater than <tt>:end_year</tt>. The <tt>date</tt> can also be substituted for a year given as a number.
  621. # Override the field name using the <tt>:field_name</tt> option, 'year' by default.
  622. #
  623. # # Generates a select field for years that defaults to the current year that
  624. # # has ascending year values.
  625. # select_year(Date.today, start_year: 1992, end_year: 2007)
  626. #
  627. # # Generates a select field for years that defaults to the current year that
  628. # # is named 'birth' rather than 'year'.
  629. # select_year(Date.today, field_name: 'birth')
  630. #
  631. # # Generates a select field for years that defaults to the current year that
  632. # # has descending year values.
  633. # select_year(Date.today, start_year: 2005, end_year: 1900)
  634. #
  635. # # Generates a select field for years that defaults to the year 2006 that
  636. # # has ascending year values.
  637. # select_year(2006, start_year: 2000, end_year: 2010)
  638. #
  639. # # Generates a select field for years with a custom prompt. Use <tt>prompt: true</tt> for a
  640. # # generic prompt.
  641. # select_year(14, prompt: 'Choose year')
  642. 9 def select_year(date, options = {}, html_options = {})
  643. DateTimeSelector.new(date, options, html_options).select_year
  644. end
  645. # Returns an HTML time tag for the given date or time.
  646. #
  647. # time_tag Date.today # =>
  648. # <time datetime="2010-11-04">November 04, 2010</time>
  649. # time_tag Time.now # =>
  650. # <time datetime="2010-11-04T17:55:45+01:00">November 04, 2010 17:55</time>
  651. # time_tag Date.yesterday, 'Yesterday' # =>
  652. # <time datetime="2010-11-03">Yesterday</time>
  653. # time_tag Date.today, datetime: Date.today.strftime('%G-W%V') # =>
  654. # <time datetime="2010-W44">November 04, 2010</time>
  655. #
  656. # <%= time_tag Time.now do %>
  657. # <span>Right now</span>
  658. # <% end %>
  659. # # => <time datetime="2010-11-04T17:55:45+01:00"><span>Right now</span></time>
  660. 9 def time_tag(date_or_time, *args, &block)
  661. options = args.extract_options!
  662. format = options.delete(:format) || :long
  663. content = args.first || I18n.l(date_or_time, format: format)
  664. content_tag("time", content, options.reverse_merge(datetime: date_or_time.iso8601), &block)
  665. end
  666. 9 private
  667. 9 def normalize_distance_of_time_argument_to_time(value)
  668. if value.is_a?(Numeric)
  669. Time.at(value)
  670. elsif value.respond_to?(:to_time)
  671. value.to_time
  672. else
  673. raise ArgumentError, "#{value.inspect} can't be converted to a Time value"
  674. end
  675. end
  676. end
  677. 9 class DateTimeSelector #:nodoc:
  678. 9 include ActionView::Helpers::TagHelper
  679. 9 DEFAULT_PREFIX = "date"
  680. 9 POSITION = {
  681. year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6
  682. }.freeze
  683. 9 AMPM_TRANSLATION = Hash[
  684. [[0, "12 AM"], [1, "01 AM"], [2, "02 AM"], [3, "03 AM"],
  685. [4, "04 AM"], [5, "05 AM"], [6, "06 AM"], [7, "07 AM"],
  686. [8, "08 AM"], [9, "09 AM"], [10, "10 AM"], [11, "11 AM"],
  687. [12, "12 PM"], [13, "01 PM"], [14, "02 PM"], [15, "03 PM"],
  688. [16, "04 PM"], [17, "05 PM"], [18, "06 PM"], [19, "07 PM"],
  689. [20, "08 PM"], [21, "09 PM"], [22, "10 PM"], [23, "11 PM"]]
  690. ].freeze
  691. 9 def initialize(datetime, options = {}, html_options = {})
  692. @options = options.dup
  693. @html_options = html_options.dup
  694. @datetime = datetime
  695. @options[:datetime_separator] ||= " &mdash; "
  696. @options[:time_separator] ||= " : "
  697. end
  698. 9 def select_datetime
  699. order = date_order.dup
  700. order -= [:hour, :minute, :second]
  701. @options[:discard_year] ||= true unless order.include?(:year)
  702. @options[:discard_month] ||= true unless order.include?(:month)
  703. @options[:discard_day] ||= true if @options[:discard_month] || !order.include?(:day)
  704. @options[:discard_minute] ||= true if @options[:discard_hour]
  705. @options[:discard_second] ||= true unless @options[:include_seconds] && !@options[:discard_minute]
  706. set_day_if_discarded
  707. if @options[:tag] && @options[:ignore_date]
  708. select_time
  709. else
  710. [:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) }
  711. order += [:hour, :minute, :second] unless @options[:discard_hour]
  712. build_selects_from_types(order)
  713. end
  714. end
  715. 9 def select_date
  716. order = date_order.dup
  717. @options[:discard_hour] = true
  718. @options[:discard_minute] = true
  719. @options[:discard_second] = true
  720. @options[:discard_year] ||= true unless order.include?(:year)
  721. @options[:discard_month] ||= true unless order.include?(:month)
  722. @options[:discard_day] ||= true if @options[:discard_month] || !order.include?(:day)
  723. set_day_if_discarded
  724. [:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) }
  725. build_selects_from_types(order)
  726. end
  727. 9 def select_time
  728. order = []
  729. @options[:discard_month] = true
  730. @options[:discard_year] = true
  731. @options[:discard_day] = true
  732. @options[:discard_second] ||= true unless @options[:include_seconds]
  733. order += [:year, :month, :day] unless @options[:ignore_date]
  734. order += [:hour, :minute]
  735. order << :second if @options[:include_seconds]
  736. build_selects_from_types(order)
  737. end
  738. 9 def select_second
  739. if @options[:use_hidden] || @options[:discard_second]
  740. build_hidden(:second, sec) if @options[:include_seconds]
  741. else
  742. build_options_and_select(:second, sec)
  743. end
  744. end
  745. 9 def select_minute
  746. if @options[:use_hidden] || @options[:discard_minute]
  747. build_hidden(:minute, min)
  748. else
  749. build_options_and_select(:minute, min, step: @options[:minute_step])
  750. end
  751. end
  752. 9 def select_hour
  753. if @options[:use_hidden] || @options[:discard_hour]
  754. build_hidden(:hour, hour)
  755. else
  756. options = {}
  757. options[:ampm] = @options[:ampm] || false
  758. options[:start] = @options[:start_hour] || 0
  759. options[:end] = @options[:end_hour] || 23
  760. build_options_and_select(:hour, hour, options)
  761. end
  762. end
  763. 9 def select_day
  764. if @options[:use_hidden] || @options[:discard_day]
  765. build_hidden(:day, day || 1)
  766. else
  767. build_options_and_select(:day, day, start: 1, end: 31, leading_zeros: false, use_two_digit_numbers: @options[:use_two_digit_numbers])
  768. end
  769. end
  770. 9 def select_month
  771. if @options[:use_hidden] || @options[:discard_month]
  772. build_hidden(:month, month || 1)
  773. else
  774. month_options = []
  775. 1.upto(12) do |month_number|
  776. options = { value: month_number }
  777. options[:selected] = "selected" if month == month_number
  778. month_options << content_tag("option", month_name(month_number), options) + "\n"
  779. end
  780. build_select(:month, month_options.join)
  781. end
  782. end
  783. 9 def select_year
  784. if !year || @datetime == 0
  785. val = "1"
  786. middle_year = Date.today.year
  787. else
  788. val = middle_year = year
  789. end
  790. if @options[:use_hidden] || @options[:discard_year]
  791. build_hidden(:year, val)
  792. else
  793. options = {}
  794. options[:start] = @options[:start_year] || middle_year - 5
  795. options[:end] = @options[:end_year] || middle_year + 5
  796. options[:step] = options[:start] < options[:end] ? 1 : -1
  797. options[:leading_zeros] = false
  798. options[:max_years_allowed] = @options[:max_years_allowed] || 1000
  799. if (options[:end] - options[:start]).abs > options[:max_years_allowed]
  800. raise ArgumentError, "There are too many years options to be built. Are you sure you haven't mistyped something? You can provide the :max_years_allowed parameter."
  801. end
  802. build_select(:year, build_year_options(val, options))
  803. end
  804. end
  805. 9 private
  806. 9 %w( sec min hour day month year ).each do |method|
  807. 54 define_method(method) do
  808. case @datetime
  809. when Hash then @datetime[method.to_sym]
  810. when Numeric then @datetime
  811. when nil then nil
  812. else @datetime.send(method)
  813. end
  814. end
  815. end
  816. # If the day is hidden, the day should be set to the 1st so all month and year choices are
  817. # valid. Otherwise, February 31st or February 29th, 2011 can be selected, which are invalid.
  818. 9 def set_day_if_discarded
  819. if @datetime && @options[:discard_day]
  820. @datetime = @datetime.change(day: 1)
  821. end
  822. end
  823. # Returns translated month names, but also ensures that a custom month
  824. # name array has a leading +nil+ element.
  825. 9 def month_names
  826. @month_names ||= begin
  827. month_names = @options[:use_month_names] || translated_month_names
  828. month_names.unshift(nil) if month_names.size < 13
  829. month_names
  830. end
  831. end
  832. # Returns translated month names.
  833. # => [nil, "January", "February", "March",
  834. # "April", "May", "June", "July",
  835. # "August", "September", "October",
  836. # "November", "December"]
  837. #
  838. # If <tt>:use_short_month</tt> option is set
  839. # => [nil, "Jan", "Feb", "Mar", "Apr", "May", "Jun",
  840. # "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
  841. 9 def translated_month_names
  842. key = @options[:use_short_month] ? :'date.abbr_month_names' : :'date.month_names'
  843. I18n.translate(key, locale: @options[:locale])
  844. end
  845. # Looks up month names by number (1-based):
  846. #
  847. # month_name(1) # => "January"
  848. #
  849. # If the <tt>:use_month_numbers</tt> option is passed:
  850. #
  851. # month_name(1) # => 1
  852. #
  853. # If the <tt>:use_two_month_numbers</tt> option is passed:
  854. #
  855. # month_name(1) # => '01'
  856. #
  857. # If the <tt>:add_month_numbers</tt> option is passed:
  858. #
  859. # month_name(1) # => "1 - January"
  860. #
  861. # If the <tt>:month_format_string</tt> option is passed:
  862. #
  863. # month_name(1) # => "January (01)"
  864. #
  865. # depending on the format string.
  866. 9 def month_name(number)
  867. if @options[:use_month_numbers]
  868. number
  869. elsif @options[:use_two_digit_numbers]
  870. "%02d" % number
  871. elsif @options[:add_month_numbers]
  872. "#{number} - #{month_names[number]}"
  873. elsif format_string = @options[:month_format_string]
  874. format_string % { number: number, name: month_names[number] }
  875. else
  876. month_names[number]
  877. end
  878. end
  879. # Looks up year names by number.
  880. #
  881. # year_name(1998) # => 1998
  882. #
  883. # If the <tt>:year_format</tt> option is passed:
  884. #
  885. # year_name(1998) # => "Heisei 10"
  886. 9 def year_name(number)
  887. if year_format_lambda = @options[:year_format]
  888. year_format_lambda.call(number)
  889. else
  890. number
  891. end
  892. end
  893. 9 def date_order
  894. @date_order ||= @options[:order] || translated_date_order
  895. end
  896. 9 def translated_date_order
  897. date_order = I18n.translate(:'date.order', locale: @options[:locale], default: [])
  898. date_order = date_order.map(&:to_sym)
  899. forbidden_elements = date_order - [:year, :month, :day]
  900. if forbidden_elements.any?
  901. raise StandardError,
  902. "#{@options[:locale]}.date.order only accepts :year, :month and :day"
  903. end
  904. date_order
  905. end
  906. # Build full select tag from date type and options.
  907. 9 def build_options_and_select(type, selected, options = {})
  908. build_select(type, build_options(selected, options))
  909. end
  910. # Build select option HTML from date value and options.
  911. # build_options(15, start: 1, end: 31)
  912. # => "<option value="1">1</option>
  913. # <option value="2">2</option>
  914. # <option value="3">3</option>..."
  915. #
  916. # If <tt>use_two_digit_numbers: true</tt> option is passed
  917. # build_options(15, start: 1, end: 31, use_two_digit_numbers: true)
  918. # => "<option value="1">01</option>
  919. # <option value="2">02</option>
  920. # <option value="3">03</option>..."
  921. #
  922. # If <tt>:step</tt> options is passed
  923. # build_options(15, start: 1, end: 31, step: 2)
  924. # => "<option value="1">1</option>
  925. # <option value="3">3</option>
  926. # <option value="5">5</option>..."
  927. 9 def build_options(selected, options = {})
  928. options = {
  929. leading_zeros: true, ampm: false, use_two_digit_numbers: false
  930. }.merge!(options)
  931. start = options.delete(:start) || 0
  932. stop = options.delete(:end) || 59
  933. step = options.delete(:step) || 1
  934. leading_zeros = options.delete(:leading_zeros)
  935. select_options = []
  936. start.step(stop, step) do |i|
  937. value = leading_zeros ? sprintf("%02d", i) : i
  938. tag_options = { value: value }
  939. tag_options[:selected] = "selected" if selected == i
  940. text = options[:use_two_digit_numbers] ? sprintf("%02d", i) : value
  941. text = options[:ampm] ? AMPM_TRANSLATION[i] : text
  942. select_options << content_tag("option", text, tag_options)
  943. end
  944. (select_options.join("\n") + "\n").html_safe
  945. end
  946. # Build select option HTML for year.
  947. # If <tt>year_format</tt> option is not passed
  948. # build_year_options(1998, start: 1998, end: 2000)
  949. # => "<option value="1998" selected="selected">1998</option>
  950. # <option value="1999">1999</option>
  951. # <option value="2000">2000</option>"
  952. #
  953. # If <tt>year_format</tt> option is passed
  954. # build_year_options(1998, start: 1998, end: 2000, year_format: ->year { "Heisei #{ year - 1988 }" })
  955. # => "<option value="1998" selected="selected">Heisei 10</option>
  956. # <option value="1999">Heisei 11</option>
  957. # <option value="2000">Heisei 12</option>"
  958. 9 def build_year_options(selected, options = {})
  959. start = options.delete(:start)
  960. stop = options.delete(:end)
  961. step = options.delete(:step)
  962. select_options = []
  963. start.step(stop, step) do |value|
  964. tag_options = { value: value }
  965. tag_options[:selected] = "selected" if selected == value
  966. text = year_name(value)
  967. select_options << content_tag("option", text, tag_options)
  968. end
  969. (select_options.join("\n") + "\n").html_safe
  970. end
  971. # Builds select tag from date type and HTML select options.
  972. # build_select(:month, "<option value="1">January</option>...")
  973. # => "<select id="post_written_on_2i" name="post[written_on(2i)]">
  974. # <option value="1">January</option>...
  975. # </select>"
  976. 9 def build_select(type, select_options_as_html)
  977. select_options = {
  978. id: input_id_from_type(type),
  979. name: input_name_from_type(type)
  980. }.merge!(@html_options)
  981. select_options[:disabled] = "disabled" if @options[:disabled]
  982. select_options[:class] = css_class_attribute(type, select_options[:class], @options[:with_css_classes]) if @options[:with_css_classes]
  983. select_html = +"\n"
  984. select_html << content_tag("option", "", value: "", label: " ") + "\n" if @options[:include_blank]
  985. select_html << prompt_option_tag(type, @options[:prompt]) + "\n" if @options[:prompt]
  986. select_html << select_options_as_html
  987. (content_tag("select", select_html.html_safe, select_options) + "\n").html_safe
  988. end
  989. # Builds the css class value for the select element
  990. # css_class_attribute(:year, 'date optional', { year: 'my-year' })
  991. # => "date optional my-year"
  992. 9 def css_class_attribute(type, html_options_class, options) # :nodoc:
  993. css_class = \
  994. case options
  995. when Hash
  996. options[type.to_sym]
  997. else
  998. type
  999. end
  1000. [html_options_class, css_class].compact.join(" ")
  1001. end
  1002. # Builds a prompt option tag with supplied options or from default options.
  1003. # prompt_option_tag(:month, prompt: 'Select month')
  1004. # => "<option value="">Select month</option>"
  1005. 9 def prompt_option_tag(type, options)
  1006. prompt = \
  1007. case options
  1008. when Hash
  1009. default_options = { year: false, month: false, day: false, hour: false, minute: false, second: false }
  1010. default_options.merge!(options)[type.to_sym]
  1011. when String
  1012. options
  1013. else
  1014. I18n.translate(:"datetime.prompts.#{type}", locale: @options[:locale])
  1015. end
  1016. prompt ? content_tag("option", prompt, value: "") : ""
  1017. end
  1018. # Builds hidden input tag for date part and value.
  1019. # build_hidden(:year, 2008)
  1020. # => "<input id="post_written_on_1i" name="post[written_on(1i)]" type="hidden" value="2008" />"
  1021. 9 def build_hidden(type, value)
  1022. select_options = {
  1023. type: "hidden",
  1024. id: input_id_from_type(type),
  1025. name: input_name_from_type(type),
  1026. value: value
  1027. }.merge!(@html_options.slice(:disabled))
  1028. select_options[:disabled] = "disabled" if @options[:disabled]
  1029. tag(:input, select_options) + "\n".html_safe
  1030. end
  1031. # Returns the name attribute for the input tag.
  1032. # => post[written_on(1i)]
  1033. 9 def input_name_from_type(type)
  1034. prefix = @options[:prefix] || ActionView::Helpers::DateTimeSelector::DEFAULT_PREFIX
  1035. prefix += "[#{@options[:index]}]" if @options.has_key?(:index)
  1036. field_name = @options[:field_name] || type.to_s
  1037. if @options[:include_position]
  1038. field_name += "(#{ActionView::Helpers::DateTimeSelector::POSITION[type]}i)"
  1039. end
  1040. @options[:discard_type] ? prefix : "#{prefix}[#{field_name}]"
  1041. end
  1042. # Returns the id attribute for the input tag.
  1043. # => "post_written_on_1i"
  1044. 9 def input_id_from_type(type)
  1045. id = input_name_from_type(type).gsub(/([\[\(])|(\]\[)/, "_").gsub(/[\]\)]/, "")
  1046. id = @options[:namespace] + "_" + id if @options[:namespace]
  1047. id
  1048. end
  1049. # Given an ordering of datetime components, create the selection HTML
  1050. # and join them with their appropriate separators.
  1051. 9 def build_selects_from_types(order)
  1052. select = +""
  1053. first_visible = order.find { |type| !@options[:"discard_#{type}"] }
  1054. order.reverse_each do |type|
  1055. separator = separator(type) unless type == first_visible # don't add before first visible field
  1056. select.insert(0, separator.to_s + send("select_#{type}").to_s)
  1057. end
  1058. select.html_safe
  1059. end
  1060. # Returns the separator for a given datetime component.
  1061. 9 def separator(type)
  1062. return "" if @options[:use_hidden]
  1063. case type
  1064. when :year, :month, :day
  1065. @options[:"discard_#{type}"] ? "" : @options[:date_separator]
  1066. when :hour
  1067. (@options[:discard_year] && @options[:discard_day]) ? "" : @options[:datetime_separator]
  1068. when :minute, :second
  1069. @options[:"discard_#{type}"] ? "" : @options[:time_separator]
  1070. end
  1071. end
  1072. end
  1073. 9 class FormBuilder
  1074. # Wraps ActionView::Helpers::DateHelper#date_select for form builders:
  1075. #
  1076. # <%= form_for @person do |f| %>
  1077. # <%= f.date_select :birth_date %>
  1078. # <%= f.submit %>
  1079. # <% end %>
  1080. #
  1081. # Please refer to the documentation of the base helper for details.
  1082. 9 def date_select(method, options = {}, html_options = {})
  1083. @template.date_select(@object_name, method, objectify_options(options), html_options)
  1084. end
  1085. # Wraps ActionView::Helpers::DateHelper#time_select for form builders:
  1086. #
  1087. # <%= form_for @race do |f| %>
  1088. # <%= f.time_select :average_lap %>
  1089. # <%= f.submit %>
  1090. # <% end %>
  1091. #
  1092. # Please refer to the documentation of the base helper for details.
  1093. 9 def time_select(method, options = {}, html_options = {})
  1094. @template.time_select(@object_name, method, objectify_options(options), html_options)
  1095. end
  1096. # Wraps ActionView::Helpers::DateHelper#datetime_select for form builders:
  1097. #
  1098. # <%= form_for @person do |f| %>
  1099. # <%= f.datetime_select :last_request_at %>
  1100. # <%= f.submit %>
  1101. # <% end %>
  1102. #
  1103. # Please refer to the documentation of the base helper for details.
  1104. 9 def datetime_select(method, options = {}, html_options = {})
  1105. @template.datetime_select(@object_name, method, objectify_options(options), html_options)
  1106. end
  1107. end
  1108. end
  1109. end

lib/action_view/helpers/debug_helper.rb

55.56% lines covered

9 relevant lines. 5 lines covered and 4 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 module ActionView
  3. # = Action View Debug Helper
  4. #
  5. # Provides a set of methods for making it easier to debug Rails objects.
  6. 9 module Helpers #:nodoc:
  7. 9 module DebugHelper
  8. 9 include TagHelper
  9. # Returns a YAML representation of +object+ wrapped with <pre> and </pre>.
  10. # If the object cannot be converted to YAML using +to_yaml+, +inspect+ will be called instead.
  11. # Useful for inspecting an object at the time of rendering.
  12. #
  13. # @user = User.new({ username: 'testing', password: 'xyz', age: 42})
  14. # debug(@user)
  15. # # =>
  16. # <pre class='debug_dump'>--- !ruby/object:User
  17. # attributes:
  18. # updated_at:
  19. # username: testing
  20. # age: 42
  21. # password: xyz
  22. # created_at:
  23. # </pre>
  24. 9 def debug(object)
  25. Marshal.dump(object)
  26. object = ERB::Util.html_escape(object.to_yaml)
  27. content_tag(:pre, object, class: "debug_dump")
  28. rescue # errors from Marshal or YAML
  29. # Object couldn't be dumped, perhaps because of singleton methods -- this is the fallback
  30. content_tag(:code, object.inspect, class: "debug_dump")
  31. end
  32. end
  33. end
  34. end

lib/action_view/helpers/form_helper.rb

31.21% lines covered

282 relevant lines. 88 lines covered and 194 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 require "cgi"
  3. 9 require "action_view/helpers/date_helper"
  4. 9 require "action_view/helpers/tag_helper"
  5. 9 require "action_view/helpers/form_tag_helper"
  6. 9 require "action_view/helpers/active_model_helper"
  7. 9 require "action_view/model_naming"
  8. 9 require "action_view/record_identifier"
  9. 9 require "active_support/core_ext/module/attribute_accessors"
  10. 9 require "active_support/core_ext/hash/slice"
  11. 9 require "active_support/core_ext/string/output_safety"
  12. 9 require "active_support/core_ext/string/inflections"
  13. 9 require "active_support/core_ext/symbol/starts_ends_with"
  14. 9 module ActionView
  15. # = Action View Form Helpers
  16. 9 module Helpers #:nodoc:
  17. # Form helpers are designed to make working with resources much easier
  18. # compared to using vanilla HTML.
  19. #
  20. # Typically, a form designed to create or update a resource reflects the
  21. # identity of the resource in several ways: (i) the URL that the form is
  22. # sent to (the form element's +action+ attribute) should result in a request
  23. # being routed to the appropriate controller action (with the appropriate <tt>:id</tt>
  24. # parameter in the case of an existing resource), (ii) input fields should
  25. # be named in such a way that in the controller their values appear in the
  26. # appropriate places within the +params+ hash, and (iii) for an existing record,
  27. # when the form is initially displayed, input fields corresponding to attributes
  28. # of the resource should show the current values of those attributes.
  29. #
  30. # In Rails, this is usually achieved by creating the form using +form_for+ and
  31. # a number of related helper methods. +form_for+ generates an appropriate <tt>form</tt>
  32. # tag and yields a form builder object that knows the model the form is about.
  33. # Input fields are created by calling methods defined on the form builder, which
  34. # means they are able to generate the appropriate names and default values
  35. # corresponding to the model attributes, as well as convenient IDs, etc.
  36. # Conventions in the generated field names allow controllers to receive form data
  37. # nicely structured in +params+ with no effort on your side.
  38. #
  39. # For example, to create a new person you typically set up a new instance of
  40. # +Person+ in the <tt>PeopleController#new</tt> action, <tt>@person</tt>, and
  41. # in the view template pass that object to +form_for+:
  42. #
  43. # <%= form_for @person do |f| %>
  44. # <%= f.label :first_name %>:
  45. # <%= f.text_field :first_name %><br />
  46. #
  47. # <%= f.label :last_name %>:
  48. # <%= f.text_field :last_name %><br />
  49. #
  50. # <%= f.submit %>
  51. # <% end %>
  52. #
  53. # The HTML generated for this would be (modulus formatting):
  54. #
  55. # <form action="/people" class="new_person" id="new_person" method="post">
  56. # <input name="authenticity_token" type="hidden" value="NrOp5bsjoLRuK8IW5+dQEYjKGUJDe7TQoZVvq95Wteg=" />
  57. # <label for="person_first_name">First name</label>:
  58. # <input id="person_first_name" name="person[first_name]" type="text" /><br />
  59. #
  60. # <label for="person_last_name">Last name</label>:
  61. # <input id="person_last_name" name="person[last_name]" type="text" /><br />
  62. #
  63. # <input name="commit" type="submit" value="Create Person" />
  64. # </form>
  65. #
  66. # As you see, the HTML reflects knowledge about the resource in several spots,
  67. # like the path the form should be submitted to, or the names of the input fields.
  68. #
  69. # In particular, thanks to the conventions followed in the generated field names, the
  70. # controller gets a nested hash <tt>params[:person]</tt> with the person attributes
  71. # set in the form. That hash is ready to be passed to <tt>Person.new</tt>:
  72. #
  73. # @person = Person.new(params[:person])
  74. # if @person.save
  75. # # success
  76. # else
  77. # # error handling
  78. # end
  79. #
  80. # Interestingly, the exact same view code in the previous example can be used to edit
  81. # a person. If <tt>@person</tt> is an existing record with name "John Smith" and ID 256,
  82. # the code above as is would yield instead:
  83. #
  84. # <form action="/people/256" class="edit_person" id="edit_person_256" method="post">
  85. # <input name="_method" type="hidden" value="patch" />
  86. # <input name="authenticity_token" type="hidden" value="NrOp5bsjoLRuK8IW5+dQEYjKGUJDe7TQoZVvq95Wteg=" />
  87. # <label for="person_first_name">First name</label>:
  88. # <input id="person_first_name" name="person[first_name]" type="text" value="John" /><br />
  89. #
  90. # <label for="person_last_name">Last name</label>:
  91. # <input id="person_last_name" name="person[last_name]" type="text" value="Smith" /><br />
  92. #
  93. # <input name="commit" type="submit" value="Update Person" />
  94. # </form>
  95. #
  96. # Note that the endpoint, default values, and submit button label are tailored for <tt>@person</tt>.
  97. # That works that way because the involved helpers know whether the resource is a new record or not,
  98. # and generate HTML accordingly.
  99. #
  100. # The controller would receive the form data again in <tt>params[:person]</tt>, ready to be
  101. # passed to <tt>Person#update</tt>:
  102. #
  103. # if @person.update(params[:person])
  104. # # success
  105. # else
  106. # # error handling
  107. # end
  108. #
  109. # That's how you typically work with resources.
  110. 9 module FormHelper
  111. 9 extend ActiveSupport::Concern
  112. 9 include FormTagHelper
  113. 9 include UrlHelper
  114. 9 include ModelNaming
  115. 9 include RecordIdentifier
  116. 9 attr_internal :default_form_builder
  117. # Creates a form that allows the user to create or update the attributes
  118. # of a specific model object.
  119. #
  120. # The method can be used in several slightly different ways, depending on
  121. # how much you wish to rely on Rails to infer automatically from the model
  122. # how the form should be constructed. For a generic model object, a form
  123. # can be created by passing +form_for+ a string or symbol representing
  124. # the object we are concerned with:
  125. #
  126. # <%= form_for :person do |f| %>
  127. # First name: <%= f.text_field :first_name %><br />
  128. # Last name : <%= f.text_field :last_name %><br />
  129. # Biography : <%= f.text_area :biography %><br />
  130. # Admin? : <%= f.check_box :admin %><br />
  131. # <%= f.submit %>
  132. # <% end %>
  133. #
  134. # The variable +f+ yielded to the block is a FormBuilder object that
  135. # incorporates the knowledge about the model object represented by
  136. # <tt>:person</tt> passed to +form_for+. Methods defined on the FormBuilder
  137. # are used to generate fields bound to this model. Thus, for example,
  138. #
  139. # <%= f.text_field :first_name %>
  140. #
  141. # will get expanded to
  142. #
  143. # <%= text_field :person, :first_name %>
  144. #
  145. # which results in an HTML <tt><input></tt> tag whose +name+ attribute is
  146. # <tt>person[first_name]</tt>. This means that when the form is submitted,
  147. # the value entered by the user will be available in the controller as
  148. # <tt>params[:person][:first_name]</tt>.
  149. #
  150. # For fields generated in this way using the FormBuilder,
  151. # if <tt>:person</tt> also happens to be the name of an instance variable
  152. # <tt>@person</tt>, the default value of the field shown when the form is
  153. # initially displayed (e.g. in the situation where you are editing an
  154. # existing record) will be the value of the corresponding attribute of
  155. # <tt>@person</tt>.
  156. #
  157. # The rightmost argument to +form_for+ is an
  158. # optional hash of options -
  159. #
  160. # * <tt>:url</tt> - The URL the form is to be submitted to. This may be
  161. # represented in the same way as values passed to +url_for+ or +link_to+.
  162. # So for example you may use a named route directly. When the model is
  163. # represented by a string or symbol, as in the example above, if the
  164. # <tt>:url</tt> option is not specified, by default the form will be
  165. # sent back to the current URL (We will describe below an alternative
  166. # resource-oriented usage of +form_for+ in which the URL does not need
  167. # to be specified explicitly).
  168. # * <tt>:namespace</tt> - A namespace for your form to ensure uniqueness of
  169. # id attributes on form elements. The namespace attribute will be prefixed
  170. # with underscore on the generated HTML id.
  171. # * <tt>:method</tt> - The method to use when submitting the form, usually
  172. # either "get" or "post". If "patch", "put", "delete", or another verb
  173. # is used, a hidden input with name <tt>_method</tt> is added to
  174. # simulate the verb over post.
  175. # * <tt>:authenticity_token</tt> - Authenticity token to use in the form.
  176. # Use only if you need to pass custom authenticity token string, or to
  177. # not add authenticity_token field at all (by passing <tt>false</tt>).
  178. # Remote forms may omit the embedded authenticity token by setting
  179. # <tt>config.action_view.embed_authenticity_token_in_remote_forms = false</tt>.
  180. # This is helpful when you're fragment-caching the form. Remote forms
  181. # get the authenticity token from the <tt>meta</tt> tag, so embedding is
  182. # unnecessary unless you support browsers without JavaScript.
  183. # * <tt>:remote</tt> - If set to true, will allow the Unobtrusive
  184. # JavaScript drivers to control the submit behavior. By default this
  185. # behavior is an ajax submit.
  186. # * <tt>:enforce_utf8</tt> - If set to false, a hidden input with name
  187. # utf8 is not output.
  188. # * <tt>:html</tt> - Optional HTML attributes for the form tag.
  189. #
  190. # Also note that +form_for+ doesn't create an exclusive scope. It's still
  191. # possible to use both the stand-alone FormHelper methods and methods
  192. # from FormTagHelper. For example:
  193. #
  194. # <%= form_for :person do |f| %>
  195. # First name: <%= f.text_field :first_name %>
  196. # Last name : <%= f.text_field :last_name %>
  197. # Biography : <%= text_area :person, :biography %>
  198. # Admin? : <%= check_box_tag "person[admin]", "1", @person.company.admin? %>
  199. # <%= f.submit %>
  200. # <% end %>
  201. #
  202. # This also works for the methods in FormOptionsHelper and DateHelper that
  203. # are designed to work with an object as base, like
  204. # FormOptionsHelper#collection_select and DateHelper#datetime_select.
  205. #
  206. # === #form_for with a model object
  207. #
  208. # In the examples above, the object to be created or edited was
  209. # represented by a symbol passed to +form_for+, and we noted that
  210. # a string can also be used equivalently. It is also possible, however,
  211. # to pass a model object itself to +form_for+. For example, if <tt>@post</tt>
  212. # is an existing record you wish to edit, you can create the form using
  213. #
  214. # <%= form_for @post do |f| %>
  215. # ...
  216. # <% end %>
  217. #
  218. # This behaves in almost the same way as outlined previously, with a
  219. # couple of small exceptions. First, the prefix used to name the input
  220. # elements within the form (hence the key that denotes them in the +params+
  221. # hash) is actually derived from the object's _class_, e.g. <tt>params[:post]</tt>
  222. # if the object's class is +Post+. However, this can be overwritten using
  223. # the <tt>:as</tt> option, e.g. -
  224. #
  225. # <%= form_for(@person, as: :client) do |f| %>
  226. # ...
  227. # <% end %>
  228. #
  229. # would result in <tt>params[:client]</tt>.
  230. #
  231. # Secondly, the field values shown when the form is initially displayed
  232. # are taken from the attributes of the object passed to +form_for+,
  233. # regardless of whether the object is an instance
  234. # variable. So, for example, if we had a _local_ variable +post+
  235. # representing an existing record,
  236. #
  237. # <%= form_for post do |f| %>
  238. # ...
  239. # <% end %>
  240. #
  241. # would produce a form with fields whose initial state reflect the current
  242. # values of the attributes of +post+.
  243. #
  244. # === Resource-oriented style
  245. #
  246. # In the examples just shown, although not indicated explicitly, we still
  247. # need to use the <tt>:url</tt> option in order to specify where the
  248. # form is going to be sent. However, further simplification is possible
  249. # if the record passed to +form_for+ is a _resource_, i.e. it corresponds
  250. # to a set of RESTful routes, e.g. defined using the +resources+ method
  251. # in <tt>config/routes.rb</tt>. In this case Rails will simply infer the
  252. # appropriate URL from the record itself. For example,
  253. #
  254. # <%= form_for @post do |f| %>
  255. # ...
  256. # <% end %>
  257. #
  258. # is then equivalent to something like:
  259. #
  260. # <%= form_for @post, as: :post, url: post_path(@post), method: :patch, html: { class: "edit_post", id: "edit_post_45" } do |f| %>
  261. # ...
  262. # <% end %>
  263. #
  264. # And for a new record
  265. #
  266. # <%= form_for(Post.new) do |f| %>
  267. # ...
  268. # <% end %>
  269. #
  270. # is equivalent to something like:
  271. #
  272. # <%= form_for @post, as: :post, url: posts_path, html: { class: "new_post", id: "new_post" } do |f| %>
  273. # ...
  274. # <% end %>
  275. #
  276. # However you can still overwrite individual conventions, such as:
  277. #
  278. # <%= form_for(@post, url: super_posts_path) do |f| %>
  279. # ...
  280. # <% end %>
  281. #
  282. # You can also set the answer format, like this:
  283. #
  284. # <%= form_for(@post, format: :json) do |f| %>
  285. # ...
  286. # <% end %>
  287. #
  288. # For namespaced routes, like +admin_post_url+:
  289. #
  290. # <%= form_for([:admin, @post]) do |f| %>
  291. # ...
  292. # <% end %>
  293. #
  294. # If your resource has associations defined, for example, you want to add comments
  295. # to the document given that the routes are set correctly:
  296. #
  297. # <%= form_for([@document, @comment]) do |f| %>
  298. # ...
  299. # <% end %>
  300. #
  301. # Where <tt>@document = Document.find(params[:id])</tt> and
  302. # <tt>@comment = Comment.new</tt>.
  303. #
  304. # === Setting the method
  305. #
  306. # You can force the form to use the full array of HTTP verbs by setting
  307. #
  308. # method: (:get|:post|:patch|:put|:delete)
  309. #
  310. # in the options hash. If the verb is not GET or POST, which are natively
  311. # supported by HTML forms, the form will be set to POST and a hidden input
  312. # called _method will carry the intended verb for the server to interpret.
  313. #
  314. # === Unobtrusive JavaScript
  315. #
  316. # Specifying:
  317. #
  318. # remote: true
  319. #
  320. # in the options hash creates a form that will allow the unobtrusive JavaScript drivers to modify its
  321. # behavior. The expected default behavior is an XMLHttpRequest in the background instead of the regular
  322. # POST arrangement, but ultimately the behavior is the choice of the JavaScript driver implementor.
  323. # Even though it's using JavaScript to serialize the form elements, the form submission will work just like
  324. # a regular submission as viewed by the receiving side (all elements available in <tt>params</tt>).
  325. #
  326. # Example:
  327. #
  328. # <%= form_for(@post, remote: true) do |f| %>
  329. # ...
  330. # <% end %>
  331. #
  332. # The HTML generated for this would be:
  333. #
  334. # <form action='http://www.example.com' method='post' data-remote='true'>
  335. # <input name='_method' type='hidden' value='patch' />
  336. # ...
  337. # </form>
  338. #
  339. # === Setting HTML options
  340. #
  341. # You can set data attributes directly by passing in a data hash, but all other HTML options must be wrapped in
  342. # the HTML key. Example:
  343. #
  344. # <%= form_for(@post, data: { behavior: "autosave" }, html: { name: "go" }) do |f| %>
  345. # ...
  346. # <% end %>
  347. #
  348. # The HTML generated for this would be:
  349. #
  350. # <form action='http://www.example.com' method='post' data-behavior='autosave' name='go'>
  351. # <input name='_method' type='hidden' value='patch' />
  352. # ...
  353. # </form>
  354. #
  355. # === Removing hidden model id's
  356. #
  357. # The form_for method automatically includes the model id as a hidden field in the form.
  358. # This is used to maintain the correlation between the form data and its associated model.
  359. # Some ORM systems do not use IDs on nested models so in this case you want to be able
  360. # to disable the hidden id.
  361. #
  362. # In the following example the Post model has many Comments stored within it in a NoSQL database,
  363. # thus there is no primary key for comments.
  364. #
  365. # Example:
  366. #
  367. # <%= form_for(@post) do |f| %>
  368. # <%= f.fields_for(:comments, include_id: false) do |cf| %>
  369. # ...
  370. # <% end %>
  371. # <% end %>
  372. #
  373. # === Customized form builders
  374. #
  375. # You can also build forms using a customized FormBuilder class. Subclass
  376. # FormBuilder and override or define some more helpers, then use your
  377. # custom builder. For example, let's say you made a helper to
  378. # automatically add labels to form inputs.
  379. #
  380. # <%= form_for @person, url: { action: "create" }, builder: LabellingFormBuilder do |f| %>
  381. # <%= f.text_field :first_name %>
  382. # <%= f.text_field :last_name %>
  383. # <%= f.text_area :biography %>
  384. # <%= f.check_box :admin %>
  385. # <%= f.submit %>
  386. # <% end %>
  387. #
  388. # In this case, if you use this:
  389. #
  390. # <%= render f %>
  391. #
  392. # The rendered template is <tt>people/_labelling_form</tt> and the local
  393. # variable referencing the form builder is called
  394. # <tt>labelling_form</tt>.
  395. #
  396. # The custom FormBuilder class is automatically merged with the options
  397. # of a nested fields_for call, unless it's explicitly set.
  398. #
  399. # In many cases you will want to wrap the above in another helper, so you
  400. # could do something like the following:
  401. #
  402. # def labelled_form_for(record_or_name_or_array, *args, &block)
  403. # options = args.extract_options!
  404. # form_for(record_or_name_or_array, *(args << options.merge(builder: LabellingFormBuilder)), &block)
  405. # end
  406. #
  407. # If you don't need to attach a form to a model instance, then check out
  408. # FormTagHelper#form_tag.
  409. #
  410. # === Form to external resources
  411. #
  412. # When you build forms to external resources sometimes you need to set an authenticity token or just render a form
  413. # without it, for example when you submit data to a payment gateway number and types of fields could be limited.
  414. #
  415. # To set an authenticity token you need to pass an <tt>:authenticity_token</tt> parameter
  416. #
  417. # <%= form_for @invoice, url: external_url, authenticity_token: 'external_token' do |f| %>
  418. # ...
  419. # <% end %>
  420. #
  421. # If you don't want to an authenticity token field be rendered at all just pass <tt>false</tt>:
  422. #
  423. # <%= form_for @invoice, url: external_url, authenticity_token: false do |f| %>
  424. # ...
  425. # <% end %>
  426. 9 def form_for(record, options = {}, &block)
  427. raise ArgumentError, "Missing block" unless block_given?
  428. html_options = options[:html] ||= {}
  429. case record
  430. when String, Symbol
  431. object_name = record
  432. object = nil
  433. else
  434. object = record.is_a?(Array) ? record.last : record
  435. raise ArgumentError, "First argument in form cannot contain nil or be empty" unless object
  436. object_name = options[:as] || model_name_from_record_or_class(object).param_key
  437. apply_form_for_options!(record, object, options)
  438. end
  439. html_options[:data] = options.delete(:data) if options.has_key?(:data)
  440. html_options[:remote] = options.delete(:remote) if options.has_key?(:remote)
  441. html_options[:method] = options.delete(:method) if options.has_key?(:method)
  442. html_options[:enforce_utf8] = options.delete(:enforce_utf8) if options.has_key?(:enforce_utf8)
  443. html_options[:authenticity_token] = options.delete(:authenticity_token)
  444. builder = instantiate_builder(object_name, object, options)
  445. output = capture(builder, &block)
  446. html_options[:multipart] ||= builder.multipart?
  447. html_options = html_options_for_form(options[:url] || {}, html_options)
  448. form_tag_with_body(html_options, output)
  449. end
  450. 9 def apply_form_for_options!(record, object, options) #:nodoc:
  451. object = convert_to_model(object)
  452. as = options[:as]
  453. namespace = options[:namespace]
  454. action, method = object.respond_to?(:persisted?) && object.persisted? ? [:edit, :patch] : [:new, :post]
  455. options[:html].reverse_merge!(
  456. class: as ? "#{action}_#{as}" : dom_class(object, action),
  457. id: (as ? [namespace, action, as] : [namespace, dom_id(object, action)]).compact.join("_").presence,
  458. method: method
  459. )
  460. options[:url] ||= if options.key?(:format)
  461. polymorphic_path(record, format: options.delete(:format))
  462. else
  463. polymorphic_path(record, {})
  464. end
  465. end
  466. 9 private :apply_form_for_options!
  467. 9 mattr_accessor :form_with_generates_remote_forms, default: true
  468. 9 mattr_accessor :form_with_generates_ids, default: false
  469. # Creates a form tag based on mixing URLs, scopes, or models.
  470. #
  471. # # Using just a URL:
  472. # <%= form_with url: posts_path do |form| %>
  473. # <%= form.text_field :title %>
  474. # <% end %>
  475. # # =>
  476. # <form action="/posts" method="post" data-remote="true">
  477. # <input type="text" name="title">
  478. # </form>
  479. #
  480. # # Adding a scope prefixes the input field names:
  481. # <%= form_with scope: :post, url: posts_path do |form| %>
  482. # <%= form.text_field :title %>
  483. # <% end %>
  484. # # =>
  485. # <form action="/posts" method="post" data-remote="true">
  486. # <input type="text" name="post[title]">
  487. # </form>
  488. #
  489. # # Using a model infers both the URL and scope:
  490. # <%= form_with model: Post.new do |form| %>
  491. # <%= form.text_field :title %>
  492. # <% end %>
  493. # # =>
  494. # <form action="/posts" method="post" data-remote="true">
  495. # <input type="text" name="post[title]">
  496. # </form>
  497. #
  498. # # An existing model makes an update form and fills out field values:
  499. # <%= form_with model: Post.first do |form| %>
  500. # <%= form.text_field :title %>
  501. # <% end %>
  502. # # =>
  503. # <form action="/posts/1" method="post" data-remote="true">
  504. # <input type="hidden" name="_method" value="patch">
  505. # <input type="text" name="post[title]" value="<the title of the post>">
  506. # </form>
  507. #
  508. # # Though the fields don't have to correspond to model attributes:
  509. # <%= form_with model: Cat.new do |form| %>
  510. # <%= form.text_field :cats_dont_have_gills %>
  511. # <%= form.text_field :but_in_forms_they_can %>
  512. # <% end %>
  513. # # =>
  514. # <form action="/cats" method="post" data-remote="true">
  515. # <input type="text" name="cat[cats_dont_have_gills]">
  516. # <input type="text" name="cat[but_in_forms_they_can]">
  517. # </form>
  518. #
  519. # The parameters in the forms are accessible in controllers according to
  520. # their name nesting. So inputs named +title+ and <tt>post[title]</tt> are
  521. # accessible as <tt>params[:title]</tt> and <tt>params[:post][:title]</tt>
  522. # respectively.
  523. #
  524. # By default +form_with+ attaches the <tt>data-remote</tt> attribute
  525. # submitting the form via an XMLHTTPRequest in the background if an
  526. # Unobtrusive JavaScript driver, like rails-ujs, is used. See the
  527. # <tt>:local</tt> option for more.
  528. #
  529. # For ease of comparison the examples above left out the submit button,
  530. # as well as the auto generated hidden fields that enable UTF-8 support
  531. # and adds an authenticity token needed for cross site request forgery
  532. # protection.
  533. #
  534. # === Resource-oriented style
  535. #
  536. # In many of the examples just shown, the +:model+ passed to +form_with+
  537. # is a _resource_. It corresponds to a set of RESTful routes, most likely
  538. # defined via +resources+ in <tt>config/routes.rb</tt>.
  539. #
  540. # So when passing such a model record, Rails infers the URL and method.
  541. #
  542. # <%= form_with model: @post do |form| %>
  543. # ...
  544. # <% end %>
  545. #
  546. # is then equivalent to something like:
  547. #
  548. # <%= form_with scope: :post, url: post_path(@post), method: :patch do |form| %>
  549. # ...
  550. # <% end %>
  551. #
  552. # And for a new record
  553. #
  554. # <%= form_with model: Post.new do |form| %>
  555. # ...
  556. # <% end %>
  557. #
  558. # is equivalent to something like:
  559. #
  560. # <%= form_with scope: :post, url: posts_path do |form| %>
  561. # ...
  562. # <% end %>
  563. #
  564. # ==== +form_with+ options
  565. #
  566. # * <tt>:url</tt> - The URL the form submits to. Akin to values passed to
  567. # +url_for+ or +link_to+. For example, you may use a named route
  568. # directly. When a <tt>:scope</tt> is passed without a <tt>:url</tt> the
  569. # form just submits to the current URL.
  570. # * <tt>:method</tt> - The method to use when submitting the form, usually
  571. # either "get" or "post". If "patch", "put", "delete", or another verb
  572. # is used, a hidden input named <tt>_method</tt> is added to
  573. # simulate the verb over post.
  574. # * <tt>:format</tt> - The format of the route the form submits to.
  575. # Useful when submitting to another resource type, like <tt>:json</tt>.
  576. # Skipped if a <tt>:url</tt> is passed.
  577. # * <tt>:scope</tt> - The scope to prefix input field names with and
  578. # thereby how the submitted parameters are grouped in controllers.
  579. # * <tt>:namespace</tt> - A namespace for your form to ensure uniqueness of
  580. # id attributes on form elements. The namespace attribute will be prefixed
  581. # with underscore on the generated HTML id.
  582. # * <tt>:model</tt> - A model object to infer the <tt>:url</tt> and
  583. # <tt>:scope</tt> by, plus fill out input field values.
  584. # So if a +title+ attribute is set to "Ahoy!" then a +title+ input
  585. # field's value would be "Ahoy!".
  586. # If the model is a new record a create form is generated, if an
  587. # existing record, however, an update form is generated.
  588. # Pass <tt>:scope</tt> or <tt>:url</tt> to override the defaults.
  589. # E.g. turn <tt>params[:post]</tt> into <tt>params[:article]</tt>.
  590. # * <tt>:authenticity_token</tt> - Authenticity token to use in the form.
  591. # Override with a custom authenticity token or pass <tt>false</tt> to
  592. # skip the authenticity token field altogether.
  593. # Useful when submitting to an external resource like a payment gateway
  594. # that might limit the valid fields.
  595. # Remote forms may omit the embedded authenticity token by setting
  596. # <tt>config.action_view.embed_authenticity_token_in_remote_forms = false</tt>.
  597. # This is helpful when fragment-caching the form. Remote forms
  598. # get the authenticity token from the <tt>meta</tt> tag, so embedding is
  599. # unnecessary unless you support browsers without JavaScript.
  600. # * <tt>:local</tt> - By default form submits are remote and unobtrusive XHRs.
  601. # Disable remote submits with <tt>local: true</tt>.
  602. # * <tt>:skip_enforcing_utf8</tt> - If set to true, a hidden input with name
  603. # utf8 is not output.
  604. # * <tt>:builder</tt> - Override the object used to build the form.
  605. # * <tt>:id</tt> - Optional HTML id attribute.
  606. # * <tt>:class</tt> - Optional HTML class attribute.
  607. # * <tt>:data</tt> - Optional HTML data attributes.
  608. # * <tt>:html</tt> - Other optional HTML attributes for the form tag.
  609. #
  610. # === Examples
  611. #
  612. # When not passing a block, +form_with+ just generates an opening form tag.
  613. #
  614. # <%= form_with(model: @post, url: super_posts_path) %>
  615. # <%= form_with(model: @post, scope: :article) %>
  616. # <%= form_with(model: @post, format: :json) %>
  617. # <%= form_with(model: @post, authenticity_token: false) %> # Disables the token.
  618. #
  619. # For namespaced routes, like +admin_post_url+:
  620. #
  621. # <%= form_with(model: [ :admin, @post ]) do |form| %>
  622. # ...
  623. # <% end %>
  624. #
  625. # If your resource has associations defined, for example, you want to add comments
  626. # to the document given that the routes are set correctly:
  627. #
  628. # <%= form_with(model: [ @document, Comment.new ]) do |form| %>
  629. # ...
  630. # <% end %>
  631. #
  632. # Where <tt>@document = Document.find(params[:id])</tt>.
  633. #
  634. # === Mixing with other form helpers
  635. #
  636. # While +form_with+ uses a FormBuilder object it's possible to mix and
  637. # match the stand-alone FormHelper methods and methods
  638. # from FormTagHelper:
  639. #
  640. # <%= form_with scope: :person do |form| %>
  641. # <%= form.text_field :first_name %>
  642. # <%= form.text_field :last_name %>
  643. #
  644. # <%= text_area :person, :biography %>
  645. # <%= check_box_tag "person[admin]", "1", @person.company.admin? %>
  646. #
  647. # <%= form.submit %>
  648. # <% end %>
  649. #
  650. # Same goes for the methods in FormOptionsHelper and DateHelper designed
  651. # to work with an object as a base, like
  652. # FormOptionsHelper#collection_select and DateHelper#datetime_select.
  653. #
  654. # === Setting the method
  655. #
  656. # You can force the form to use the full array of HTTP verbs by setting
  657. #
  658. # method: (:get|:post|:patch|:put|:delete)
  659. #
  660. # in the options hash. If the verb is not GET or POST, which are natively
  661. # supported by HTML forms, the form will be set to POST and a hidden input
  662. # called _method will carry the intended verb for the server to interpret.
  663. #
  664. # === Setting HTML options
  665. #
  666. # You can set data attributes directly in a data hash, but HTML options
  667. # besides id and class must be wrapped in an HTML key:
  668. #
  669. # <%= form_with(model: @post, data: { behavior: "autosave" }, html: { name: "go" }) do |form| %>
  670. # ...
  671. # <% end %>
  672. #
  673. # generates
  674. #
  675. # <form action="/posts/123" method="post" data-behavior="autosave" name="go">
  676. # <input name="_method" type="hidden" value="patch" />
  677. # ...
  678. # </form>
  679. #
  680. # === Removing hidden model id's
  681. #
  682. # The +form_with+ method automatically includes the model id as a hidden field in the form.
  683. # This is used to maintain the correlation between the form data and its associated model.
  684. # Some ORM systems do not use IDs on nested models so in this case you want to be able
  685. # to disable the hidden id.
  686. #
  687. # In the following example the Post model has many Comments stored within it in a NoSQL database,
  688. # thus there is no primary key for comments.
  689. #
  690. # <%= form_with(model: @post) do |form| %>
  691. # <%= form.fields(:comments, skip_id: true) do |fields| %>
  692. # ...
  693. # <% end %>
  694. # <% end %>
  695. #
  696. # === Customized form builders
  697. #
  698. # You can also build forms using a customized FormBuilder class. Subclass
  699. # FormBuilder and override or define some more helpers, then use your
  700. # custom builder. For example, let's say you made a helper to
  701. # automatically add labels to form inputs.
  702. #
  703. # <%= form_with model: @person, url: { action: "create" }, builder: LabellingFormBuilder do |form| %>
  704. # <%= form.text_field :first_name %>
  705. # <%= form.text_field :last_name %>
  706. # <%= form.text_area :biography %>
  707. # <%= form.check_box :admin %>
  708. # <%= form.submit %>
  709. # <% end %>
  710. #
  711. # In this case, if you use:
  712. #
  713. # <%= render form %>
  714. #
  715. # The rendered template is <tt>people/_labelling_form</tt> and the local
  716. # variable referencing the form builder is called
  717. # <tt>labelling_form</tt>.
  718. #
  719. # The custom FormBuilder class is automatically merged with the options
  720. # of a nested +fields+ call, unless it's explicitly set.
  721. #
  722. # In many cases you will want to wrap the above in another helper, so you
  723. # could do something like the following:
  724. #
  725. # def labelled_form_with(**options, &block)
  726. # form_with(**options.merge(builder: LabellingFormBuilder), &block)
  727. # end
  728. 9 def form_with(model: nil, scope: nil, url: nil, format: nil, **options, &block)
  729. options[:allow_method_names_outside_object] = true
  730. options[:skip_default_ids] = !form_with_generates_ids
  731. if model
  732. url ||= polymorphic_path(model, format: format)
  733. model = model.last if model.is_a?(Array)
  734. scope ||= model_name_from_record_or_class(model).param_key
  735. end
  736. if block_given?
  737. builder = instantiate_builder(scope, model, options)
  738. output = capture(builder, &block)
  739. options[:multipart] ||= builder.multipart?
  740. html_options = html_options_for_form_with(url, model, **options)
  741. form_tag_with_body(html_options, output)
  742. else
  743. html_options = html_options_for_form_with(url, model, **options)
  744. form_tag_html(html_options)
  745. end
  746. end
  747. # Creates a scope around a specific model object like form_for, but
  748. # doesn't create the form tags themselves. This makes fields_for suitable
  749. # for specifying additional model objects in the same form.
  750. #
  751. # Although the usage and purpose of +fields_for+ is similar to +form_for+'s,
  752. # its method signature is slightly different. Like +form_for+, it yields
  753. # a FormBuilder object associated with a particular model object to a block,
  754. # and within the block allows methods to be called on the builder to
  755. # generate fields associated with the model object. Fields may reflect
  756. # a model object in two ways - how they are named (hence how submitted
  757. # values appear within the +params+ hash in the controller) and what
  758. # default values are shown when the form the fields appear in is first
  759. # displayed. In order for both of these features to be specified independently,
  760. # both an object name (represented by either a symbol or string) and the
  761. # object itself can be passed to the method separately -
  762. #
  763. # <%= form_for @person do |person_form| %>
  764. # First name: <%= person_form.text_field :first_name %>
  765. # Last name : <%= person_form.text_field :last_name %>
  766. #
  767. # <%= fields_for :permission, @person.permission do |permission_fields| %>
  768. # Admin? : <%= permission_fields.check_box :admin %>
  769. # <% end %>
  770. #
  771. # <%= person_form.submit %>
  772. # <% end %>
  773. #
  774. # In this case, the checkbox field will be represented by an HTML +input+
  775. # tag with the +name+ attribute <tt>permission[admin]</tt>, and the submitted
  776. # value will appear in the controller as <tt>params[:permission][:admin]</tt>.
  777. # If <tt>@person.permission</tt> is an existing record with an attribute
  778. # +admin+, the initial state of the checkbox when first displayed will
  779. # reflect the value of <tt>@person.permission.admin</tt>.
  780. #
  781. # Often this can be simplified by passing just the name of the model
  782. # object to +fields_for+ -
  783. #
  784. # <%= fields_for :permission do |permission_fields| %>
  785. # Admin?: <%= permission_fields.check_box :admin %>
  786. # <% end %>
  787. #
  788. # ...in which case, if <tt>:permission</tt> also happens to be the name of an
  789. # instance variable <tt>@permission</tt>, the initial state of the input
  790. # field will reflect the value of that variable's attribute <tt>@permission.admin</tt>.
  791. #
  792. # Alternatively, you can pass just the model object itself (if the first
  793. # argument isn't a string or symbol +fields_for+ will realize that the
  794. # name has been omitted) -
  795. #
  796. # <%= fields_for @person.permission do |permission_fields| %>
  797. # Admin?: <%= permission_fields.check_box :admin %>
  798. # <% end %>
  799. #
  800. # and +fields_for+ will derive the required name of the field from the
  801. # _class_ of the model object, e.g. if <tt>@person.permission</tt>, is
  802. # of class +Permission+, the field will still be named <tt>permission[admin]</tt>.
  803. #
  804. # Note: This also works for the methods in FormOptionsHelper and
  805. # DateHelper that are designed to work with an object as base, like
  806. # FormOptionsHelper#collection_select and DateHelper#datetime_select.
  807. #
  808. # === Nested Attributes Examples
  809. #
  810. # When the object belonging to the current scope has a nested attribute
  811. # writer for a certain attribute, fields_for will yield a new scope
  812. # for that attribute. This allows you to create forms that set or change
  813. # the attributes of a parent object and its associations in one go.
  814. #
  815. # Nested attribute writers are normal setter methods named after an
  816. # association. The most common way of defining these writers is either
  817. # with +accepts_nested_attributes_for+ in a model definition or by
  818. # defining a method with the proper name. For example: the attribute
  819. # writer for the association <tt>:address</tt> is called
  820. # <tt>address_attributes=</tt>.
  821. #
  822. # Whether a one-to-one or one-to-many style form builder will be yielded
  823. # depends on whether the normal reader method returns a _single_ object
  824. # or an _array_ of objects.
  825. #
  826. # ==== One-to-one
  827. #
  828. # Consider a Person class which returns a _single_ Address from the
  829. # <tt>address</tt> reader method and responds to the
  830. # <tt>address_attributes=</tt> writer method:
  831. #
  832. # class Person
  833. # def address
  834. # @address
  835. # end
  836. #
  837. # def address_attributes=(attributes)
  838. # # Process the attributes hash
  839. # end
  840. # end
  841. #
  842. # This model can now be used with a nested fields_for, like so:
  843. #
  844. # <%= form_for @person do |person_form| %>
  845. # ...
  846. # <%= person_form.fields_for :address do |address_fields| %>
  847. # Street : <%= address_fields.text_field :street %>
  848. # Zip code: <%= address_fields.text_field :zip_code %>
  849. # <% end %>
  850. # ...
  851. # <% end %>
  852. #
  853. # When address is already an association on a Person you can use
  854. # +accepts_nested_attributes_for+ to define the writer method for you:
  855. #
  856. # class Person < ActiveRecord::Base
  857. # has_one :address
  858. # accepts_nested_attributes_for :address
  859. # end
  860. #
  861. # If you want to destroy the associated model through the form, you have
  862. # to enable it first using the <tt>:allow_destroy</tt> option for
  863. # +accepts_nested_attributes_for+:
  864. #
  865. # class Person < ActiveRecord::Base
  866. # has_one :address
  867. # accepts_nested_attributes_for :address, allow_destroy: true
  868. # end
  869. #
  870. # Now, when you use a form element with the <tt>_destroy</tt> parameter,
  871. # with a value that evaluates to +true+, you will destroy the associated
  872. # model (e.g. 1, '1', true, or 'true'):
  873. #
  874. # <%= form_for @person do |person_form| %>
  875. # ...
  876. # <%= person_form.fields_for :address do |address_fields| %>
  877. # ...
  878. # Delete: <%= address_fields.check_box :_destroy %>
  879. # <% end %>
  880. # ...
  881. # <% end %>
  882. #
  883. # ==== One-to-many
  884. #
  885. # Consider a Person class which returns an _array_ of Project instances
  886. # from the <tt>projects</tt> reader method and responds to the
  887. # <tt>projects_attributes=</tt> writer method:
  888. #
  889. # class Person
  890. # def projects
  891. # [@project1, @project2]
  892. # end
  893. #
  894. # def projects_attributes=(attributes)
  895. # # Process the attributes hash
  896. # end
  897. # end
  898. #
  899. # Note that the <tt>projects_attributes=</tt> writer method is in fact
  900. # required for fields_for to correctly identify <tt>:projects</tt> as a
  901. # collection, and the correct indices to be set in the form markup.
  902. #
  903. # When projects is already an association on Person you can use
  904. # +accepts_nested_attributes_for+ to define the writer method for you:
  905. #
  906. # class Person < ActiveRecord::Base
  907. # has_many :projects
  908. # accepts_nested_attributes_for :projects
  909. # end
  910. #
  911. # This model can now be used with a nested fields_for. The block given to
  912. # the nested fields_for call will be repeated for each instance in the
  913. # collection:
  914. #
  915. # <%= form_for @person do |person_form| %>
  916. # ...
  917. # <%= person_form.fields_for :projects do |project_fields| %>
  918. # <% if project_fields.object.active? %>
  919. # Name: <%= project_fields.text_field :name %>
  920. # <% end %>
  921. # <% end %>
  922. # ...
  923. # <% end %>
  924. #
  925. # It's also possible to specify the instance to be used:
  926. #
  927. # <%= form_for @person do |person_form| %>
  928. # ...
  929. # <% @person.projects.each do |project| %>
  930. # <% if project.active? %>
  931. # <%= person_form.fields_for :projects, project do |project_fields| %>
  932. # Name: <%= project_fields.text_field :name %>
  933. # <% end %>
  934. # <% end %>
  935. # <% end %>
  936. # ...
  937. # <% end %>
  938. #
  939. # Or a collection to be used:
  940. #
  941. # <%= form_for @person do |person_form| %>
  942. # ...
  943. # <%= person_form.fields_for :projects, @active_projects do |project_fields| %>
  944. # Name: <%= project_fields.text_field :name %>
  945. # <% end %>
  946. # ...
  947. # <% end %>
  948. #
  949. # If you want to destroy any of the associated models through the
  950. # form, you have to enable it first using the <tt>:allow_destroy</tt>
  951. # option for +accepts_nested_attributes_for+:
  952. #
  953. # class Person < ActiveRecord::Base
  954. # has_many :projects
  955. # accepts_nested_attributes_for :projects, allow_destroy: true
  956. # end
  957. #
  958. # This will allow you to specify which models to destroy in the
  959. # attributes hash by adding a form element for the <tt>_destroy</tt>
  960. # parameter with a value that evaluates to +true+
  961. # (e.g. 1, '1', true, or 'true'):
  962. #
  963. # <%= form_for @person do |person_form| %>
  964. # ...
  965. # <%= person_form.fields_for :projects do |project_fields| %>
  966. # Delete: <%= project_fields.check_box :_destroy %>
  967. # <% end %>
  968. # ...
  969. # <% end %>
  970. #
  971. # When a collection is used you might want to know the index of each
  972. # object into the array. For this purpose, the <tt>index</tt> method
  973. # is available in the FormBuilder object.
  974. #
  975. # <%= form_for @person do |person_form| %>
  976. # ...
  977. # <%= person_form.fields_for :projects do |project_fields| %>
  978. # Project #<%= project_fields.index %>
  979. # ...
  980. # <% end %>
  981. # ...
  982. # <% end %>
  983. #
  984. # Note that fields_for will automatically generate a hidden field
  985. # to store the ID of the record. There are circumstances where this
  986. # hidden field is not needed and you can pass <tt>include_id: false</tt>
  987. # to prevent fields_for from rendering it automatically.
  988. 9 def fields_for(record_name, record_object = nil, options = {}, &block)
  989. builder = instantiate_builder(record_name, record_object, options)
  990. capture(builder, &block)
  991. end
  992. # Scopes input fields with either an explicit scope or model.
  993. # Like +form_with+ does with <tt>:scope</tt> or <tt>:model</tt>,
  994. # except it doesn't output the form tags.
  995. #
  996. # # Using a scope prefixes the input field names:
  997. # <%= fields :comment do |fields| %>
  998. # <%= fields.text_field :body %>
  999. # <% end %>
  1000. # # => <input type="text" name="comment[body]">
  1001. #
  1002. # # Using a model infers the scope and assigns field values:
  1003. # <%= fields model: Comment.new(body: "full bodied") do |fields| %>
  1004. # <%= fields.text_field :body %>
  1005. # <% end %>
  1006. # # => <input type="text" name="comment[body]" value="full bodied">
  1007. #
  1008. # # Using +fields+ with +form_with+:
  1009. # <%= form_with model: @post do |form| %>
  1010. # <%= form.text_field :title %>
  1011. #
  1012. # <%= form.fields :comment do |fields| %>
  1013. # <%= fields.text_field :body %>
  1014. # <% end %>
  1015. # <% end %>
  1016. #
  1017. # Much like +form_with+ a FormBuilder instance associated with the scope
  1018. # or model is yielded, so any generated field names are prefixed with
  1019. # either the passed scope or the scope inferred from the <tt>:model</tt>.
  1020. #
  1021. # === Mixing with other form helpers
  1022. #
  1023. # While +form_with+ uses a FormBuilder object it's possible to mix and
  1024. # match the stand-alone FormHelper methods and methods
  1025. # from FormTagHelper:
  1026. #
  1027. # <%= fields model: @comment do |fields| %>
  1028. # <%= fields.text_field :body %>
  1029. #
  1030. # <%= text_area :commenter, :biography %>
  1031. # <%= check_box_tag "comment[all_caps]", "1", @comment.commenter.hulk_mode? %>
  1032. # <% end %>
  1033. #
  1034. # Same goes for the methods in FormOptionsHelper and DateHelper designed
  1035. # to work with an object as a base, like
  1036. # FormOptionsHelper#collection_select and DateHelper#datetime_select.
  1037. 9 def fields(scope = nil, model: nil, **options, &block)
  1038. options[:allow_method_names_outside_object] = true
  1039. options[:skip_default_ids] = !form_with_generates_ids
  1040. if model
  1041. scope ||= model_name_from_record_or_class(model).param_key
  1042. end
  1043. builder = instantiate_builder(scope, model, options)
  1044. capture(builder, &block)
  1045. end
  1046. # Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object
  1047. # assigned to the template (identified by +object+). The text of label will default to the attribute name unless a translation
  1048. # is found in the current I18n locale (through helpers.label.<modelname>.<attribute>) or you specify it explicitly.
  1049. # Additional options on the label tag can be passed as a hash with +options+. These options will be tagged
  1050. # onto the HTML as an HTML element attribute as in the example shown, except for the <tt>:value</tt> option, which is designed to
  1051. # target labels for radio_button tags (where the value is used in the ID of the input tag).
  1052. #
  1053. # ==== Examples
  1054. # label(:post, :title)
  1055. # # => <label for="post_title">Title</label>
  1056. #
  1057. # You can localize your labels based on model and attribute names.
  1058. # For example you can define the following in your locale (e.g. en.yml)
  1059. #
  1060. # helpers:
  1061. # label:
  1062. # post:
  1063. # body: "Write your entire text here"
  1064. #
  1065. # Which then will result in
  1066. #
  1067. # label(:post, :body)
  1068. # # => <label for="post_body">Write your entire text here</label>
  1069. #
  1070. # Localization can also be based purely on the translation of the attribute-name
  1071. # (if you are using ActiveRecord):
  1072. #
  1073. # activerecord:
  1074. # attributes:
  1075. # post:
  1076. # cost: "Total cost"
  1077. #
  1078. # label(:post, :cost)
  1079. # # => <label for="post_cost">Total cost</label>
  1080. #
  1081. # label(:post, :title, "A short title")
  1082. # # => <label for="post_title">A short title</label>
  1083. #
  1084. # label(:post, :title, "A short title", class: "title_label")
  1085. # # => <label for="post_title" class="title_label">A short title</label>
  1086. #
  1087. # label(:post, :privacy, "Public Post", value: "public")
  1088. # # => <label for="post_privacy_public">Public Post</label>
  1089. #
  1090. # label(:post, :terms) do
  1091. # raw('Accept <a href="/terms">Terms</a>.')
  1092. # end
  1093. # # => <label for="post_terms">Accept <a href="/terms">Terms</a>.</label>
  1094. 9 def label(object_name, method, content_or_options = nil, options = nil, &block)
  1095. Tags::Label.new(object_name, method, self, content_or_options, options).render(&block)
  1096. end
  1097. # Returns an input tag of the "text" type tailored for accessing a specified attribute (identified by +method+) on an object
  1098. # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
  1099. # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
  1100. # shown.
  1101. #
  1102. # ==== Examples
  1103. # text_field(:post, :title, size: 20)
  1104. # # => <input type="text" id="post_title" name="post[title]" size="20" value="#{@post.title}" />
  1105. #
  1106. # text_field(:post, :title, class: "create_input")
  1107. # # => <input type="text" id="post_title" name="post[title]" value="#{@post.title}" class="create_input" />
  1108. #
  1109. # text_field(:post, :title, maxlength: 30, class: "title_input")
  1110. # # => <input type="text" id="post_title" name="post[title]" maxlength="30" size="30" value="#{@post.title}" class="title_input" />
  1111. #
  1112. # text_field(:session, :user, onchange: "if ($('#session_user').val() === 'admin') { alert('Your login cannot be admin!'); }")
  1113. # # => <input type="text" id="session_user" name="session[user]" value="#{@session.user}" onchange="if ($('#session_user').val() === 'admin') { alert('Your login cannot be admin!'); }"/>
  1114. #
  1115. # text_field(:snippet, :code, size: 20, class: 'code_input')
  1116. # # => <input type="text" id="snippet_code" name="snippet[code]" size="20" value="#{@snippet.code}" class="code_input" />
  1117. 9 def text_field(object_name, method, options = {})
  1118. Tags::TextField.new(object_name, method, self, options).render
  1119. end
  1120. # Returns an input tag of the "password" type tailored for accessing a specified attribute (identified by +method+) on an object
  1121. # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
  1122. # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
  1123. # shown. For security reasons this field is blank by default; pass in a value via +options+ if this is not desired.
  1124. #
  1125. # ==== Examples
  1126. # password_field(:login, :pass, size: 20)
  1127. # # => <input type="password" id="login_pass" name="login[pass]" size="20" />
  1128. #
  1129. # password_field(:account, :secret, class: "form_input", value: @account.secret)
  1130. # # => <input type="password" id="account_secret" name="account[secret]" value="#{@account.secret}" class="form_input" />
  1131. #
  1132. # password_field(:user, :password, onchange: "if ($('#user_password').val().length > 30) { alert('Your password needs to be shorter!'); }")
  1133. # # => <input type="password" id="user_password" name="user[password]" onchange="if ($('#user_password').val().length > 30) { alert('Your password needs to be shorter!'); }"/>
  1134. #
  1135. # password_field(:account, :pin, size: 20, class: 'form_input')
  1136. # # => <input type="password" id="account_pin" name="account[pin]" size="20" class="form_input" />
  1137. 9 def password_field(object_name, method, options = {})
  1138. Tags::PasswordField.new(object_name, method, self, options).render
  1139. end
  1140. # Returns a hidden input tag tailored for accessing a specified attribute (identified by +method+) on an object
  1141. # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
  1142. # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
  1143. # shown.
  1144. #
  1145. # ==== Examples
  1146. # hidden_field(:signup, :pass_confirm)
  1147. # # => <input type="hidden" id="signup_pass_confirm" name="signup[pass_confirm]" value="#{@signup.pass_confirm}" />
  1148. #
  1149. # hidden_field(:post, :tag_list)
  1150. # # => <input type="hidden" id="post_tag_list" name="post[tag_list]" value="#{@post.tag_list}" />
  1151. #
  1152. # hidden_field(:user, :token)
  1153. # # => <input type="hidden" id="user_token" name="user[token]" value="#{@user.token}" />
  1154. 9 def hidden_field(object_name, method, options = {})
  1155. Tags::HiddenField.new(object_name, method, self, options).render
  1156. end
  1157. # Returns a file upload input tag tailored for accessing a specified attribute (identified by +method+) on an object
  1158. # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
  1159. # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
  1160. # shown.
  1161. #
  1162. # Using this method inside a +form_for+ block will set the enclosing form's encoding to <tt>multipart/form-data</tt>.
  1163. #
  1164. # ==== Options
  1165. # * Creates standard HTML attributes for the tag.
  1166. # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
  1167. # * <tt>:multiple</tt> - If set to true, *in most updated browsers* the user will be allowed to select multiple files.
  1168. # * <tt>:accept</tt> - If set to one or multiple mime-types, the user will be suggested a filter when choosing a file. You still need to set up model validations.
  1169. #
  1170. # ==== Examples
  1171. # file_field(:user, :avatar)
  1172. # # => <input type="file" id="user_avatar" name="user[avatar]" />
  1173. #
  1174. # file_field(:post, :image, multiple: true)
  1175. # # => <input type="file" id="post_image" name="post[image][]" multiple="multiple" />
  1176. #
  1177. # file_field(:post, :attached, accept: 'text/html')
  1178. # # => <input accept="text/html" type="file" id="post_attached" name="post[attached]" />
  1179. #
  1180. # file_field(:post, :image, accept: 'image/png,image/gif,image/jpeg')
  1181. # # => <input type="file" id="post_image" name="post[image]" accept="image/png,image/gif,image/jpeg" />
  1182. #
  1183. # file_field(:attachment, :file, class: 'file_input')
  1184. # # => <input type="file" id="attachment_file" name="attachment[file]" class="file_input" />
  1185. 9 def file_field(object_name, method, options = {})
  1186. Tags::FileField.new(object_name, method, self, convert_direct_upload_option_to_url(options.dup)).render
  1187. end
  1188. # Returns a textarea opening and closing tag set tailored for accessing a specified attribute (identified by +method+)
  1189. # on an object assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
  1190. # hash with +options+.
  1191. #
  1192. # ==== Examples
  1193. # text_area(:post, :body, cols: 20, rows: 40)
  1194. # # => <textarea cols="20" rows="40" id="post_body" name="post[body]">
  1195. # # #{@post.body}
  1196. # # </textarea>
  1197. #
  1198. # text_area(:comment, :text, size: "20x30")
  1199. # # => <textarea cols="20" rows="30" id="comment_text" name="comment[text]">
  1200. # # #{@comment.text}
  1201. # # </textarea>
  1202. #
  1203. # text_area(:application, :notes, cols: 40, rows: 15, class: 'app_input')
  1204. # # => <textarea cols="40" rows="15" id="application_notes" name="application[notes]" class="app_input">
  1205. # # #{@application.notes}
  1206. # # </textarea>
  1207. #
  1208. # text_area(:entry, :body, size: "20x20", disabled: 'disabled')
  1209. # # => <textarea cols="20" rows="20" id="entry_body" name="entry[body]" disabled="disabled">
  1210. # # #{@entry.body}
  1211. # # </textarea>
  1212. 9 def text_area(object_name, method, options = {})
  1213. Tags::TextArea.new(object_name, method, self, options).render
  1214. end
  1215. # Returns a checkbox tag tailored for accessing a specified attribute (identified by +method+) on an object
  1216. # assigned to the template (identified by +object+). This object must be an instance object (@object) and not a local object.
  1217. # It's intended that +method+ returns an integer and if that integer is above zero, then the checkbox is checked.
  1218. # Additional options on the input tag can be passed as a hash with +options+. The +checked_value+ defaults to 1
  1219. # while the default +unchecked_value+ is set to 0 which is convenient for boolean values.
  1220. #
  1221. # ==== Gotcha
  1222. #
  1223. # The HTML specification says unchecked check boxes are not successful, and
  1224. # thus web browsers do not send them. Unfortunately this introduces a gotcha:
  1225. # if an +Invoice+ model has a +paid+ flag, and in the form that edits a paid
  1226. # invoice the user unchecks its check box, no +paid+ parameter is sent. So,
  1227. # any mass-assignment idiom like
  1228. #
  1229. # @invoice.update(params[:invoice])
  1230. #
  1231. # wouldn't update the flag.
  1232. #
  1233. # To prevent this the helper generates an auxiliary hidden field before
  1234. # the very check box. The hidden field has the same name and its
  1235. # attributes mimic an unchecked check box.
  1236. #
  1237. # This way, the client either sends only the hidden field (representing
  1238. # the check box is unchecked), or both fields. Since the HTML specification
  1239. # says key/value pairs have to be sent in the same order they appear in the
  1240. # form, and parameters extraction gets the last occurrence of any repeated
  1241. # key in the query string, that works for ordinary forms.
  1242. #
  1243. # Unfortunately that workaround does not work when the check box goes
  1244. # within an array-like parameter, as in
  1245. #
  1246. # <%= fields_for "project[invoice_attributes][]", invoice, index: nil do |form| %>
  1247. # <%= form.check_box :paid %>
  1248. # ...
  1249. # <% end %>
  1250. #
  1251. # because parameter name repetition is precisely what Rails seeks to distinguish
  1252. # the elements of the array. For each item with a checked check box you
  1253. # get an extra ghost item with only that attribute, assigned to "0".
  1254. #
  1255. # In that case it is preferable to either use +check_box_tag+ or to use
  1256. # hashes instead of arrays.
  1257. #
  1258. # # Let's say that @post.validated? is 1:
  1259. # check_box("post", "validated")
  1260. # # => <input name="post[validated]" type="hidden" value="0" />
  1261. # # <input checked="checked" type="checkbox" id="post_validated" name="post[validated]" value="1" />
  1262. #
  1263. # # Let's say that @puppy.gooddog is "no":
  1264. # check_box("puppy", "gooddog", {}, "yes", "no")
  1265. # # => <input name="puppy[gooddog]" type="hidden" value="no" />
  1266. # # <input type="checkbox" id="puppy_gooddog" name="puppy[gooddog]" value="yes" />
  1267. #
  1268. # check_box("eula", "accepted", { class: 'eula_check' }, "yes", "no")
  1269. # # => <input name="eula[accepted]" type="hidden" value="no" />
  1270. # # <input type="checkbox" class="eula_check" id="eula_accepted" name="eula[accepted]" value="yes" />
  1271. 9 def check_box(object_name, method, options = {}, checked_value = "1", unchecked_value = "0")
  1272. Tags::CheckBox.new(object_name, method, self, checked_value, unchecked_value, options).render
  1273. end
  1274. # Returns a radio button tag for accessing a specified attribute (identified by +method+) on an object
  1275. # assigned to the template (identified by +object+). If the current value of +method+ is +tag_value+ the
  1276. # radio button will be checked.
  1277. #
  1278. # To force the radio button to be checked pass <tt>checked: true</tt> in the
  1279. # +options+ hash. You may pass HTML options there as well.
  1280. #
  1281. # # Let's say that @post.category returns "rails":
  1282. # radio_button("post", "category", "rails")
  1283. # radio_button("post", "category", "java")
  1284. # # => <input type="radio" id="post_category_rails" name="post[category]" value="rails" checked="checked" />
  1285. # # <input type="radio" id="post_category_java" name="post[category]" value="java" />
  1286. #
  1287. # # Let's say that @user.receive_newsletter returns "no":
  1288. # radio_button("user", "receive_newsletter", "yes")
  1289. # radio_button("user", "receive_newsletter", "no")
  1290. # # => <input type="radio" id="user_receive_newsletter_yes" name="user[receive_newsletter]" value="yes" />
  1291. # # <input type="radio" id="user_receive_newsletter_no" name="user[receive_newsletter]" value="no" checked="checked" />
  1292. 9 def radio_button(object_name, method, tag_value, options = {})
  1293. Tags::RadioButton.new(object_name, method, self, tag_value, options).render
  1294. end
  1295. # Returns a text_field of type "color".
  1296. #
  1297. # color_field("car", "color")
  1298. # # => <input id="car_color" name="car[color]" type="color" value="#000000" />
  1299. 9 def color_field(object_name, method, options = {})
  1300. Tags::ColorField.new(object_name, method, self, options).render
  1301. end
  1302. # Returns an input of type "search" for accessing a specified attribute (identified by +method+) on an object
  1303. # assigned to the template (identified by +object_name+). Inputs of type "search" may be styled differently by
  1304. # some browsers.
  1305. #
  1306. # search_field(:user, :name)
  1307. # # => <input id="user_name" name="user[name]" type="search" />
  1308. # search_field(:user, :name, autosave: false)
  1309. # # => <input autosave="false" id="user_name" name="user[name]" type="search" />
  1310. # search_field(:user, :name, results: 3)
  1311. # # => <input id="user_name" name="user[name]" results="3" type="search" />
  1312. # # Assume request.host returns "www.example.com"
  1313. # search_field(:user, :name, autosave: true)
  1314. # # => <input autosave="com.example.www" id="user_name" name="user[name]" results="10" type="search" />
  1315. # search_field(:user, :name, onsearch: true)
  1316. # # => <input id="user_name" incremental="true" name="user[name]" onsearch="true" type="search" />
  1317. # search_field(:user, :name, autosave: false, onsearch: true)
  1318. # # => <input autosave="false" id="user_name" incremental="true" name="user[name]" onsearch="true" type="search" />
  1319. # search_field(:user, :name, autosave: true, onsearch: true)
  1320. # # => <input autosave="com.example.www" id="user_name" incremental="true" name="user[name]" onsearch="true" results="10" type="search" />
  1321. 9 def search_field(object_name, method, options = {})
  1322. Tags::SearchField.new(object_name, method, self, options).render
  1323. end
  1324. # Returns a text_field of type "tel".
  1325. #
  1326. # telephone_field("user", "phone")
  1327. # # => <input id="user_phone" name="user[phone]" type="tel" />
  1328. #
  1329. 9 def telephone_field(object_name, method, options = {})
  1330. Tags::TelField.new(object_name, method, self, options).render
  1331. end
  1332. # aliases telephone_field
  1333. 9 alias phone_field telephone_field
  1334. # Returns a text_field of type "date".
  1335. #
  1336. # date_field("user", "born_on")
  1337. # # => <input id="user_born_on" name="user[born_on]" type="date" />
  1338. #
  1339. # The default value is generated by trying to call +strftime+ with "%Y-%m-%d"
  1340. # on the object's value, which makes it behave as expected for instances
  1341. # of DateTime and ActiveSupport::TimeWithZone. You can still override that
  1342. # by passing the "value" option explicitly, e.g.
  1343. #
  1344. # @user.born_on = Date.new(1984, 1, 27)
  1345. # date_field("user", "born_on", value: "1984-05-12")
  1346. # # => <input id="user_born_on" name="user[born_on]" type="date" value="1984-05-12" />
  1347. #
  1348. # You can create values for the "min" and "max" attributes by passing
  1349. # instances of Date or Time to the options hash.
  1350. #
  1351. # date_field("user", "born_on", min: Date.today)
  1352. # # => <input id="user_born_on" name="user[born_on]" type="date" min="2014-05-20" />
  1353. #
  1354. # Alternatively, you can pass a String formatted as an ISO8601 date as the
  1355. # values for "min" and "max."
  1356. #
  1357. # date_field("user", "born_on", min: "2014-05-20")
  1358. # # => <input id="user_born_on" name="user[born_on]" type="date" min="2014-05-20" />
  1359. #
  1360. 9 def date_field(object_name, method, options = {})
  1361. Tags::DateField.new(object_name, method, self, options).render
  1362. end
  1363. # Returns a text_field of type "time".
  1364. #
  1365. # The default value is generated by trying to call +strftime+ with "%T.%L"
  1366. # on the object's value. It is still possible to override that
  1367. # by passing the "value" option.
  1368. #
  1369. # === Options
  1370. # * Accepts same options as time_field_tag
  1371. #
  1372. # === Example
  1373. # time_field("task", "started_at")
  1374. # # => <input id="task_started_at" name="task[started_at]" type="time" />
  1375. #
  1376. # You can create values for the "min" and "max" attributes by passing
  1377. # instances of Date or Time to the options hash.
  1378. #
  1379. # time_field("task", "started_at", min: Time.now)
  1380. # # => <input id="task_started_at" name="task[started_at]" type="time" min="01:00:00.000" />
  1381. #
  1382. # Alternatively, you can pass a String formatted as an ISO8601 time as the
  1383. # values for "min" and "max."
  1384. #
  1385. # time_field("task", "started_at", min: "01:00:00")
  1386. # # => <input id="task_started_at" name="task[started_at]" type="time" min="01:00:00.000" />
  1387. #
  1388. 9 def time_field(object_name, method, options = {})
  1389. Tags::TimeField.new(object_name, method, self, options).render
  1390. end
  1391. # Returns a text_field of type "datetime-local".
  1392. #
  1393. # datetime_field("user", "born_on")
  1394. # # => <input id="user_born_on" name="user[born_on]" type="datetime-local" />
  1395. #
  1396. # The default value is generated by trying to call +strftime+ with "%Y-%m-%dT%T"
  1397. # on the object's value, which makes it behave as expected for instances
  1398. # of DateTime and ActiveSupport::TimeWithZone.
  1399. #
  1400. # @user.born_on = Date.new(1984, 1, 12)
  1401. # datetime_field("user", "born_on")
  1402. # # => <input id="user_born_on" name="user[born_on]" type="datetime-local" value="1984-01-12T00:00:00" />
  1403. #
  1404. # You can create values for the "min" and "max" attributes by passing
  1405. # instances of Date or Time to the options hash.
  1406. #
  1407. # datetime_field("user", "born_on", min: Date.today)
  1408. # # => <input id="user_born_on" name="user[born_on]" type="datetime-local" min="2014-05-20T00:00:00.000" />
  1409. #
  1410. # Alternatively, you can pass a String formatted as an ISO8601 datetime as
  1411. # the values for "min" and "max."
  1412. #
  1413. # datetime_field("user", "born_on", min: "2014-05-20T00:00:00")
  1414. # # => <input id="user_born_on" name="user[born_on]" type="datetime-local" min="2014-05-20T00:00:00.000" />
  1415. #
  1416. 9 def datetime_field(object_name, method, options = {})
  1417. Tags::DatetimeLocalField.new(object_name, method, self, options).render
  1418. end
  1419. 9 alias datetime_local_field datetime_field
  1420. # Returns a text_field of type "month".
  1421. #
  1422. # month_field("user", "born_on")
  1423. # # => <input id="user_born_on" name="user[born_on]" type="month" />
  1424. #
  1425. # The default value is generated by trying to call +strftime+ with "%Y-%m"
  1426. # on the object's value, which makes it behave as expected for instances
  1427. # of DateTime and ActiveSupport::TimeWithZone.
  1428. #
  1429. # @user.born_on = Date.new(1984, 1, 27)
  1430. # month_field("user", "born_on")
  1431. # # => <input id="user_born_on" name="user[born_on]" type="date" value="1984-01" />
  1432. #
  1433. 9 def month_field(object_name, method, options = {})
  1434. Tags::MonthField.new(object_name, method, self, options).render
  1435. end
  1436. # Returns a text_field of type "week".
  1437. #
  1438. # week_field("user", "born_on")
  1439. # # => <input id="user_born_on" name="user[born_on]" type="week" />
  1440. #
  1441. # The default value is generated by trying to call +strftime+ with "%Y-W%W"
  1442. # on the object's value, which makes it behave as expected for instances
  1443. # of DateTime and ActiveSupport::TimeWithZone.
  1444. #
  1445. # @user.born_on = Date.new(1984, 5, 12)
  1446. # week_field("user", "born_on")
  1447. # # => <input id="user_born_on" name="user[born_on]" type="date" value="1984-W19" />
  1448. #
  1449. 9 def week_field(object_name, method, options = {})
  1450. Tags::WeekField.new(object_name, method, self, options).render
  1451. end
  1452. # Returns a text_field of type "url".
  1453. #
  1454. # url_field("user", "homepage")
  1455. # # => <input id="user_homepage" name="user[homepage]" type="url" />
  1456. #
  1457. 9 def url_field(object_name, method, options = {})
  1458. Tags::UrlField.new(object_name, method, self, options).render
  1459. end
  1460. # Returns a text_field of type "email".
  1461. #
  1462. # email_field("user", "address")
  1463. # # => <input id="user_address" name="user[address]" type="email" />
  1464. #
  1465. 9 def email_field(object_name, method, options = {})
  1466. Tags::EmailField.new(object_name, method, self, options).render
  1467. end
  1468. # Returns an input tag of type "number".
  1469. #
  1470. # ==== Options
  1471. # * Accepts same options as number_field_tag
  1472. 9 def number_field(object_name, method, options = {})
  1473. Tags::NumberField.new(object_name, method, self, options).render
  1474. end
  1475. # Returns an input tag of type "range".
  1476. #
  1477. # ==== Options
  1478. # * Accepts same options as range_field_tag
  1479. 9 def range_field(object_name, method, options = {})
  1480. Tags::RangeField.new(object_name, method, self, options).render
  1481. end
  1482. 9 private
  1483. 9 def html_options_for_form_with(url_for_options = nil, model = nil, html: {}, local: !form_with_generates_remote_forms,
  1484. skip_enforcing_utf8: nil, **options)
  1485. html_options = options.slice(:id, :class, :multipart, :method, :data).merge(html)
  1486. html_options[:method] ||= :patch if model.respond_to?(:persisted?) && model.persisted?
  1487. html_options[:enforce_utf8] = !skip_enforcing_utf8 unless skip_enforcing_utf8.nil?
  1488. html_options[:enctype] = "multipart/form-data" if html_options.delete(:multipart)
  1489. # The following URL is unescaped, this is just a hash of options, and it is the
  1490. # responsibility of the caller to escape all the values.
  1491. html_options[:action] = url_for(url_for_options || {})
  1492. html_options[:"accept-charset"] = "UTF-8"
  1493. html_options[:"data-remote"] = true unless local
  1494. html_options[:authenticity_token] = options.delete(:authenticity_token)
  1495. if !local && html_options[:authenticity_token].blank?
  1496. html_options[:authenticity_token] = embed_authenticity_token_in_remote_forms
  1497. end
  1498. if html_options[:authenticity_token] == true
  1499. # Include the default authenticity_token, which is only generated when it's set to nil,
  1500. # but we needed the true value to override the default of no authenticity_token on data-remote.
  1501. html_options[:authenticity_token] = nil
  1502. end
  1503. html_options.stringify_keys!
  1504. end
  1505. 9 def instantiate_builder(record_name, record_object, options)
  1506. case record_name
  1507. when String, Symbol
  1508. object = record_object
  1509. object_name = record_name
  1510. else
  1511. object = record_name
  1512. object_name = model_name_from_record_or_class(object).param_key if object
  1513. end
  1514. builder = options[:builder] || default_form_builder_class
  1515. builder.new(object_name, object, self, options)
  1516. end
  1517. 9 def default_form_builder_class
  1518. builder = default_form_builder || ActionView::Base.default_form_builder
  1519. builder.respond_to?(:constantize) ? builder.constantize : builder
  1520. end
  1521. end
  1522. # A +FormBuilder+ object is associated with a particular model object and
  1523. # allows you to generate fields associated with the model object. The
  1524. # +FormBuilder+ object is yielded when using +form_for+ or +fields_for+.
  1525. # For example:
  1526. #
  1527. # <%= form_for @person do |person_form| %>
  1528. # Name: <%= person_form.text_field :name %>
  1529. # Admin: <%= person_form.check_box :admin %>
  1530. # <% end %>
  1531. #
  1532. # In the above block, a +FormBuilder+ object is yielded as the
  1533. # +person_form+ variable. This allows you to generate the +text_field+
  1534. # and +check_box+ fields by specifying their eponymous methods, which
  1535. # modify the underlying template and associates the <tt>@person</tt> model object
  1536. # with the form.
  1537. #
  1538. # The +FormBuilder+ object can be thought of as serving as a proxy for the
  1539. # methods in the +FormHelper+ module. This class, however, allows you to
  1540. # call methods with the model object you are building the form for.
  1541. #
  1542. # You can create your own custom FormBuilder templates by subclassing this
  1543. # class. For example:
  1544. #
  1545. # class MyFormBuilder < ActionView::Helpers::FormBuilder
  1546. # def div_radio_button(method, tag_value, options = {})
  1547. # @template.content_tag(:div,
  1548. # @template.radio_button(
  1549. # @object_name, method, tag_value, objectify_options(options)
  1550. # )
  1551. # )
  1552. # end
  1553. # end
  1554. #
  1555. # The above code creates a new method +div_radio_button+ which wraps a div
  1556. # around the new radio button. Note that when options are passed in, you
  1557. # must call +objectify_options+ in order for the model object to get
  1558. # correctly passed to the method. If +objectify_options+ is not called,
  1559. # then the newly created helper will not be linked back to the model.
  1560. #
  1561. # The +div_radio_button+ code from above can now be used as follows:
  1562. #
  1563. # <%= form_for @person, :builder => MyFormBuilder do |f| %>
  1564. # I am a child: <%= f.div_radio_button(:admin, "child") %>
  1565. # I am an adult: <%= f.div_radio_button(:admin, "adult") %>
  1566. # <% end -%>
  1567. #
  1568. # The standard set of helper methods for form building are located in the
  1569. # +field_helpers+ class attribute.
  1570. 9 class FormBuilder
  1571. 9 include ModelNaming
  1572. # The methods which wrap a form helper call.
  1573. 9 class_attribute :field_helpers, default: [
  1574. :fields_for, :fields, :label, :text_field, :password_field,
  1575. :hidden_field, :file_field, :text_area, :check_box,
  1576. :radio_button, :color_field, :search_field,
  1577. :telephone_field, :phone_field, :date_field,
  1578. :time_field, :datetime_field, :datetime_local_field,
  1579. :month_field, :week_field, :url_field, :email_field,
  1580. :number_field, :range_field
  1581. ]
  1582. 9 attr_accessor :object_name, :object, :options
  1583. 9 attr_reader :multipart, :index
  1584. 9 alias :multipart? :multipart
  1585. 9 def multipart=(multipart)
  1586. @multipart = multipart
  1587. if parent_builder = @options[:parent_builder]
  1588. parent_builder.multipart = multipart
  1589. end
  1590. end
  1591. 9 def self._to_partial_path
  1592. @_to_partial_path ||= name.demodulize.underscore.sub!(/_builder$/, "")
  1593. end
  1594. 9 def to_partial_path
  1595. self.class._to_partial_path
  1596. end
  1597. 9 def to_model
  1598. self
  1599. end
  1600. 9 def initialize(object_name, object, template, options)
  1601. @nested_child_index = {}
  1602. @object_name, @object, @template, @options = object_name, object, template, options
  1603. @default_options = @options ? @options.slice(:index, :namespace, :skip_default_ids, :allow_method_names_outside_object) : {}
  1604. @default_html_options = @default_options.except(:skip_default_ids, :allow_method_names_outside_object)
  1605. convert_to_legacy_options(@options)
  1606. if @object_name&.end_with?("[]")
  1607. if (object ||= @template.instance_variable_get("@#{@object_name[0..-3]}")) && object.respond_to?(:to_param)
  1608. @auto_index = object.to_param
  1609. else
  1610. raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
  1611. end
  1612. end
  1613. @multipart = nil
  1614. @index = options[:index] || options[:child_index]
  1615. end
  1616. ##
  1617. # :method: text_field
  1618. #
  1619. # :call-seq: text_field(method, options = {})
  1620. #
  1621. # Wraps ActionView::Helpers::FormHelper#text_field for form builders:
  1622. #
  1623. # <%= form_with model: @user do |f| %>
  1624. # <%= f.text_field :name %>
  1625. # <% end %>
  1626. #
  1627. # Please refer to the documentation of the base helper for details.
  1628. ##
  1629. # :method: password_field
  1630. #
  1631. # :call-seq: password_field(method, options = {})
  1632. #
  1633. # Wraps ActionView::Helpers::FormHelper#password_field for form builders:
  1634. #
  1635. # <%= form_with model: @user do |f| %>
  1636. # <%= f.password_field :password %>
  1637. # <% end %>
  1638. #
  1639. # Please refer to the documentation of the base helper for details.
  1640. ##
  1641. # :method: text_area
  1642. #
  1643. # :call-seq: text_area(method, options = {})
  1644. #
  1645. # Wraps ActionView::Helpers::FormHelper#text_area for form builders:
  1646. #
  1647. # <%= form_with model: @user do |f| %>
  1648. # <%= f.text_area :detail %>
  1649. # <% end %>
  1650. #
  1651. # Please refer to the documentation of the base helper for details.
  1652. ##
  1653. # :method: color_field
  1654. #
  1655. # :call-seq: color_field(method, options = {})
  1656. #
  1657. # Wraps ActionView::Helpers::FormHelper#color_field for form builders:
  1658. #
  1659. # <%= form_with model: @user do |f| %>
  1660. # <%= f.color_field :favorite_color %>
  1661. # <% end %>
  1662. #
  1663. # Please refer to the documentation of the base helper for details.
  1664. ##
  1665. # :method: search_field
  1666. #
  1667. # :call-seq: search_field(method, options = {})
  1668. #
  1669. # Wraps ActionView::Helpers::FormHelper#search_field for form builders:
  1670. #
  1671. # <%= form_with model: @user do |f| %>
  1672. # <%= f.search_field :name %>
  1673. # <% end %>
  1674. #
  1675. # Please refer to the documentation of the base helper for details.
  1676. ##
  1677. # :method: telephone_field
  1678. #
  1679. # :call-seq: telephone_field(method, options = {})
  1680. #
  1681. # Wraps ActionView::Helpers::FormHelper#telephone_field for form builders:
  1682. #
  1683. # <%= form_with model: @user do |f| %>
  1684. # <%= f.telephone_field :phone %>
  1685. # <% end %>
  1686. #
  1687. # Please refer to the documentation of the base helper for details.
  1688. ##
  1689. # :method: phone_field
  1690. #
  1691. # :call-seq: phone_field(method, options = {})
  1692. #
  1693. # Wraps ActionView::Helpers::FormHelper#phone_field for form builders:
  1694. #
  1695. # <%= form_with model: @user do |f| %>
  1696. # <%= f.phone_field :phone %>
  1697. # <% end %>
  1698. #
  1699. # Please refer to the documentation of the base helper for details.
  1700. ##
  1701. # :method: date_field
  1702. #
  1703. # :call-seq: date_field(method, options = {})
  1704. #
  1705. # Wraps ActionView::Helpers::FormHelper#date_field for form builders:
  1706. #
  1707. # <%= form_with model: @user do |f| %>
  1708. # <%= f.date_field :born_on %>
  1709. # <% end %>
  1710. #
  1711. # Please refer to the documentation of the base helper for details.
  1712. ##
  1713. # :method: time_field
  1714. #
  1715. # :call-seq: time_field(method, options = {})
  1716. #
  1717. # Wraps ActionView::Helpers::FormHelper#time_field for form builders:
  1718. #
  1719. # <%= form_with model: @user do |f| %>
  1720. # <%= f.time_field :born_at %>
  1721. # <% end %>
  1722. #
  1723. # Please refer to the documentation of the base helper for details.
  1724. ##
  1725. # :method: datetime_field
  1726. #
  1727. # :call-seq: datetime_field(method, options = {})
  1728. #
  1729. # Wraps ActionView::Helpers::FormHelper#datetime_field for form builders:
  1730. #
  1731. # <%= form_with model: @user do |f| %>
  1732. # <%= f.datetime_field :graduation_day %>
  1733. # <% end %>
  1734. #
  1735. # Please refer to the documentation of the base helper for details.
  1736. ##
  1737. # :method: datetime_local_field
  1738. #
  1739. # :call-seq: datetime_local_field(method, options = {})
  1740. #
  1741. # Wraps ActionView::Helpers::FormHelper#datetime_local_field for form builders:
  1742. #
  1743. # <%= form_with model: @user do |f| %>
  1744. # <%= f.datetime_local_field :graduation_day %>
  1745. # <% end %>
  1746. #
  1747. # Please refer to the documentation of the base helper for details.
  1748. ##
  1749. # :method: month_field
  1750. #
  1751. # :call-seq: month_field(method, options = {})
  1752. #
  1753. # Wraps ActionView::Helpers::FormHelper#month_field for form builders:
  1754. #
  1755. # <%= form_with model: @user do |f| %>
  1756. # <%= f.month_field :birthday_month %>
  1757. # <% end %>
  1758. #
  1759. # Please refer to the documentation of the base helper for details.
  1760. ##
  1761. # :method: week_field
  1762. #
  1763. # :call-seq: week_field(method, options = {})
  1764. #
  1765. # Wraps ActionView::Helpers::FormHelper#week_field for form builders:
  1766. #
  1767. # <%= form_with model: @user do |f| %>
  1768. # <%= f.week_field :birthday_week %>
  1769. # <% end %>
  1770. #
  1771. # Please refer to the documentation of the base helper for details.
  1772. ##
  1773. # :method: url_field
  1774. #
  1775. # :call-seq: url_field(method, options = {})
  1776. #
  1777. # Wraps ActionView::Helpers::FormHelper#url_field for form builders:
  1778. #
  1779. # <%= form_with model: @user do |f| %>
  1780. # <%= f.url_field :homepage %>
  1781. # <% end %>
  1782. #
  1783. # Please refer to the documentation of the base helper for details.
  1784. ##
  1785. # :method: email_field
  1786. #
  1787. # :call-seq: email_field(method, options = {})
  1788. #
  1789. # Wraps ActionView::Helpers::FormHelper#email_field for form builders:
  1790. #
  1791. # <%= form_with model: @user do |f| %>
  1792. # <%= f.email_field :address %>
  1793. # <% end %>
  1794. #
  1795. # Please refer to the documentation of the base helper for details.
  1796. ##
  1797. # :method: number_field
  1798. #
  1799. # :call-seq: number_field(method, options = {})
  1800. #
  1801. # Wraps ActionView::Helpers::FormHelper#number_field for form builders:
  1802. #
  1803. # <%= form_with model: @user do |f| %>
  1804. # <%= f.number_field :age %>
  1805. # <% end %>
  1806. #
  1807. # Please refer to the documentation of the base helper for details.
  1808. ##
  1809. # :method: range_field
  1810. #
  1811. # :call-seq: range_field(method, options = {})
  1812. #
  1813. # Wraps ActionView::Helpers::FormHelper#range_field for form builders:
  1814. #
  1815. # <%= form_with model: @user do |f| %>
  1816. # <%= f.range_field :age %>
  1817. # <% end %>
  1818. #
  1819. # Please refer to the documentation of the base helper for details.
  1820. 9 (field_helpers - [:label, :check_box, :radio_button, :fields_for, :fields, :hidden_field, :file_field]).each do |selector|
  1821. 153 class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
  1822. def #{selector}(method, options = {}) # def text_field(method, options = {})
  1823. @template.send( # @template.send(
  1824. #{selector.inspect}, # :text_field,
  1825. @object_name, # @object_name,
  1826. method, # method,
  1827. objectify_options(options)) # objectify_options(options))
  1828. end # end
  1829. RUBY_EVAL
  1830. end
  1831. # Creates a scope around a specific model object like form_for, but
  1832. # doesn't create the form tags themselves. This makes fields_for suitable
  1833. # for specifying additional model objects in the same form.
  1834. #
  1835. # Although the usage and purpose of +fields_for+ is similar to +form_for+'s,
  1836. # its method signature is slightly different. Like +form_for+, it yields
  1837. # a FormBuilder object associated with a particular model object to a block,
  1838. # and within the block allows methods to be called on the builder to
  1839. # generate fields associated with the model object. Fields may reflect
  1840. # a model object in two ways - how they are named (hence how submitted
  1841. # values appear within the +params+ hash in the controller) and what
  1842. # default values are shown when the form the fields appear in is first
  1843. # displayed. In order for both of these features to be specified independently,
  1844. # both an object name (represented by either a symbol or string) and the
  1845. # object itself can be passed to the method separately -
  1846. #
  1847. # <%= form_for @person do |person_form| %>
  1848. # First name: <%= person_form.text_field :first_name %>
  1849. # Last name : <%= person_form.text_field :last_name %>
  1850. #
  1851. # <%= fields_for :permission, @person.permission do |permission_fields| %>
  1852. # Admin? : <%= permission_fields.check_box :admin %>
  1853. # <% end %>
  1854. #
  1855. # <%= person_form.submit %>
  1856. # <% end %>
  1857. #
  1858. # In this case, the checkbox field will be represented by an HTML +input+
  1859. # tag with the +name+ attribute <tt>permission[admin]</tt>, and the submitted
  1860. # value will appear in the controller as <tt>params[:permission][:admin]</tt>.
  1861. # If <tt>@person.permission</tt> is an existing record with an attribute
  1862. # +admin+, the initial state of the checkbox when first displayed will
  1863. # reflect the value of <tt>@person.permission.admin</tt>.
  1864. #
  1865. # Often this can be simplified by passing just the name of the model
  1866. # object to +fields_for+ -
  1867. #
  1868. # <%= fields_for :permission do |permission_fields| %>
  1869. # Admin?: <%= permission_fields.check_box :admin %>
  1870. # <% end %>
  1871. #
  1872. # ...in which case, if <tt>:permission</tt> also happens to be the name of an
  1873. # instance variable <tt>@permission</tt>, the initial state of the input
  1874. # field will reflect the value of that variable's attribute <tt>@permission.admin</tt>.
  1875. #
  1876. # Alternatively, you can pass just the model object itself (if the first
  1877. # argument isn't a string or symbol +fields_for+ will realize that the
  1878. # name has been omitted) -
  1879. #
  1880. # <%= fields_for @person.permission do |permission_fields| %>
  1881. # Admin?: <%= permission_fields.check_box :admin %>
  1882. # <% end %>
  1883. #
  1884. # and +fields_for+ will derive the required name of the field from the
  1885. # _class_ of the model object, e.g. if <tt>@person.permission</tt>, is
  1886. # of class +Permission+, the field will still be named <tt>permission[admin]</tt>.
  1887. #
  1888. # Note: This also works for the methods in FormOptionsHelper and
  1889. # DateHelper that are designed to work with an object as base, like
  1890. # FormOptionsHelper#collection_select and DateHelper#datetime_select.
  1891. #
  1892. # === Nested Attributes Examples
  1893. #
  1894. # When the object belonging to the current scope has a nested attribute
  1895. # writer for a certain attribute, fields_for will yield a new scope
  1896. # for that attribute. This allows you to create forms that set or change
  1897. # the attributes of a parent object and its associations in one go.
  1898. #
  1899. # Nested attribute writers are normal setter methods named after an
  1900. # association. The most common way of defining these writers is either
  1901. # with +accepts_nested_attributes_for+ in a model definition or by
  1902. # defining a method with the proper name. For example: the attribute
  1903. # writer for the association <tt>:address</tt> is called
  1904. # <tt>address_attributes=</tt>.
  1905. #
  1906. # Whether a one-to-one or one-to-many style form builder will be yielded
  1907. # depends on whether the normal reader method returns a _single_ object
  1908. # or an _array_ of objects.
  1909. #
  1910. # ==== One-to-one
  1911. #
  1912. # Consider a Person class which returns a _single_ Address from the
  1913. # <tt>address</tt> reader method and responds to the
  1914. # <tt>address_attributes=</tt> writer method:
  1915. #
  1916. # class Person
  1917. # def address
  1918. # @address
  1919. # end
  1920. #
  1921. # def address_attributes=(attributes)
  1922. # # Process the attributes hash
  1923. # end
  1924. # end
  1925. #
  1926. # This model can now be used with a nested fields_for, like so:
  1927. #
  1928. # <%= form_for @person do |person_form| %>
  1929. # ...
  1930. # <%= person_form.fields_for :address do |address_fields| %>
  1931. # Street : <%= address_fields.text_field :street %>
  1932. # Zip code: <%= address_fields.text_field :zip_code %>
  1933. # <% end %>
  1934. # ...
  1935. # <% end %>
  1936. #
  1937. # When address is already an association on a Person you can use
  1938. # +accepts_nested_attributes_for+ to define the writer method for you:
  1939. #
  1940. # class Person < ActiveRecord::Base
  1941. # has_one :address
  1942. # accepts_nested_attributes_for :address
  1943. # end
  1944. #
  1945. # If you want to destroy the associated model through the form, you have
  1946. # to enable it first using the <tt>:allow_destroy</tt> option for
  1947. # +accepts_nested_attributes_for+:
  1948. #
  1949. # class Person < ActiveRecord::Base
  1950. # has_one :address
  1951. # accepts_nested_attributes_for :address, allow_destroy: true
  1952. # end
  1953. #
  1954. # Now, when you use a form element with the <tt>_destroy</tt> parameter,
  1955. # with a value that evaluates to +true+, you will destroy the associated
  1956. # model (e.g. 1, '1', true, or 'true'):
  1957. #
  1958. # <%= form_for @person do |person_form| %>
  1959. # ...
  1960. # <%= person_form.fields_for :address do |address_fields| %>
  1961. # ...
  1962. # Delete: <%= address_fields.check_box :_destroy %>
  1963. # <% end %>
  1964. # ...
  1965. # <% end %>
  1966. #
  1967. # ==== One-to-many
  1968. #
  1969. # Consider a Person class which returns an _array_ of Project instances
  1970. # from the <tt>projects</tt> reader method and responds to the
  1971. # <tt>projects_attributes=</tt> writer method:
  1972. #
  1973. # class Person
  1974. # def projects
  1975. # [@project1, @project2]
  1976. # end
  1977. #
  1978. # def projects_attributes=(attributes)
  1979. # # Process the attributes hash
  1980. # end
  1981. # end
  1982. #
  1983. # Note that the <tt>projects_attributes=</tt> writer method is in fact
  1984. # required for fields_for to correctly identify <tt>:projects</tt> as a
  1985. # collection, and the correct indices to be set in the form markup.
  1986. #
  1987. # When projects is already an association on Person you can use
  1988. # +accepts_nested_attributes_for+ to define the writer method for you:
  1989. #
  1990. # class Person < ActiveRecord::Base
  1991. # has_many :projects
  1992. # accepts_nested_attributes_for :projects
  1993. # end
  1994. #
  1995. # This model can now be used with a nested fields_for. The block given to
  1996. # the nested fields_for call will be repeated for each instance in the
  1997. # collection:
  1998. #
  1999. # <%= form_for @person do |person_form| %>
  2000. # ...
  2001. # <%= person_form.fields_for :projects do |project_fields| %>
  2002. # <% if project_fields.object.active? %>
  2003. # Name: <%= project_fields.text_field :name %>
  2004. # <% end %>
  2005. # <% end %>
  2006. # ...
  2007. # <% end %>
  2008. #
  2009. # It's also possible to specify the instance to be used:
  2010. #
  2011. # <%= form_for @person do |person_form| %>
  2012. # ...
  2013. # <% @person.projects.each do |project| %>
  2014. # <% if project.active? %>
  2015. # <%= person_form.fields_for :projects, project do |project_fields| %>
  2016. # Name: <%= project_fields.text_field :name %>
  2017. # <% end %>
  2018. # <% end %>
  2019. # <% end %>
  2020. # ...
  2021. # <% end %>
  2022. #
  2023. # Or a collection to be used:
  2024. #
  2025. # <%= form_for @person do |person_form| %>
  2026. # ...
  2027. # <%= person_form.fields_for :projects, @active_projects do |project_fields| %>
  2028. # Name: <%= project_fields.text_field :name %>
  2029. # <% end %>
  2030. # ...
  2031. # <% end %>
  2032. #
  2033. # If you want to destroy any of the associated models through the
  2034. # form, you have to enable it first using the <tt>:allow_destroy</tt>
  2035. # option for +accepts_nested_attributes_for+:
  2036. #
  2037. # class Person < ActiveRecord::Base
  2038. # has_many :projects
  2039. # accepts_nested_attributes_for :projects, allow_destroy: true
  2040. # end
  2041. #
  2042. # This will allow you to specify which models to destroy in the
  2043. # attributes hash by adding a form element for the <tt>_destroy</tt>
  2044. # parameter with a value that evaluates to +true+
  2045. # (e.g. 1, '1', true, or 'true'):
  2046. #
  2047. # <%= form_for @person do |person_form| %>
  2048. # ...
  2049. # <%= person_form.fields_for :projects do |project_fields| %>
  2050. # Delete: <%= project_fields.check_box :_destroy %>
  2051. # <% end %>
  2052. # ...
  2053. # <% end %>
  2054. #
  2055. # When a collection is used you might want to know the index of each
  2056. # object into the array. For this purpose, the <tt>index</tt> method
  2057. # is available in the FormBuilder object.
  2058. #
  2059. # <%= form_for @person do |person_form| %>
  2060. # ...
  2061. # <%= person_form.fields_for :projects do |project_fields| %>
  2062. # Project #<%= project_fields.index %>
  2063. # ...
  2064. # <% end %>
  2065. # ...
  2066. # <% end %>
  2067. #
  2068. # Note that fields_for will automatically generate a hidden field
  2069. # to store the ID of the record. There are circumstances where this
  2070. # hidden field is not needed and you can pass <tt>include_id: false</tt>
  2071. # to prevent fields_for from rendering it automatically.
  2072. 9 def fields_for(record_name, record_object = nil, fields_options = {}, &block)
  2073. fields_options, record_object = record_object, nil if record_object.is_a?(Hash) && record_object.extractable_options?
  2074. fields_options[:builder] ||= options[:builder]
  2075. fields_options[:namespace] = options[:namespace]
  2076. fields_options[:parent_builder] = self
  2077. case record_name
  2078. when String, Symbol
  2079. if nested_attributes_association?(record_name)
  2080. return fields_for_with_nested_attributes(record_name, record_object, fields_options, block)
  2081. end
  2082. else
  2083. record_object = record_name.is_a?(Array) ? record_name.last : record_name
  2084. record_name = model_name_from_record_or_class(record_object).param_key
  2085. end
  2086. object_name = @object_name
  2087. index = if options.has_key?(:index)
  2088. options[:index]
  2089. elsif defined?(@auto_index)
  2090. object_name = object_name.to_s.delete_suffix("[]")
  2091. @auto_index
  2092. end
  2093. record_name = if index
  2094. "#{object_name}[#{index}][#{record_name}]"
  2095. elsif record_name.end_with?("[]")
  2096. "#{object_name}[#{record_name[0..-3]}][#{record_object.id}]"
  2097. else
  2098. "#{object_name}[#{record_name}]"
  2099. end
  2100. fields_options[:child_index] = index
  2101. @template.fields_for(record_name, record_object, fields_options, &block)
  2102. end
  2103. # See the docs for the <tt>ActionView::FormHelper.fields</tt> helper method.
  2104. 9 def fields(scope = nil, model: nil, **options, &block)
  2105. options[:allow_method_names_outside_object] = true
  2106. options[:skip_default_ids] = !FormHelper.form_with_generates_ids
  2107. convert_to_legacy_options(options)
  2108. fields_for(scope || model, model, options, &block)
  2109. end
  2110. # Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object
  2111. # assigned to the template (identified by +object+). The text of label will default to the attribute name unless a translation
  2112. # is found in the current I18n locale (through helpers.label.<modelname>.<attribute>) or you specify it explicitly.
  2113. # Additional options on the label tag can be passed as a hash with +options+. These options will be tagged
  2114. # onto the HTML as an HTML element attribute as in the example shown, except for the <tt>:value</tt> option, which is designed to
  2115. # target labels for radio_button tags (where the value is used in the ID of the input tag).
  2116. #
  2117. # ==== Examples
  2118. # label(:title)
  2119. # # => <label for="post_title">Title</label>
  2120. #
  2121. # You can localize your labels based on model and attribute names.
  2122. # For example you can define the following in your locale (e.g. en.yml)
  2123. #
  2124. # helpers:
  2125. # label:
  2126. # post:
  2127. # body: "Write your entire text here"
  2128. #
  2129. # Which then will result in
  2130. #
  2131. # label(:body)
  2132. # # => <label for="post_body">Write your entire text here</label>
  2133. #
  2134. # Localization can also be based purely on the translation of the attribute-name
  2135. # (if you are using ActiveRecord):
  2136. #
  2137. # activerecord:
  2138. # attributes:
  2139. # post:
  2140. # cost: "Total cost"
  2141. #
  2142. # label(:cost)
  2143. # # => <label for="post_cost">Total cost</label>
  2144. #
  2145. # label(:title, "A short title")
  2146. # # => <label for="post_title">A short title</label>
  2147. #
  2148. # label(:title, "A short title", class: "title_label")
  2149. # # => <label for="post_title" class="title_label">A short title</label>
  2150. #
  2151. # label(:privacy, "Public Post", value: "public")
  2152. # # => <label for="post_privacy_public">Public Post</label>
  2153. #
  2154. # label(:terms) do
  2155. # raw('Accept <a href="/terms">Terms</a>.')
  2156. # end
  2157. # # => <label for="post_terms">Accept <a href="/terms">Terms</a>.</label>
  2158. 9 def label(method, text = nil, options = {}, &block)
  2159. @template.label(@object_name, method, text, objectify_options(options), &block)
  2160. end
  2161. # Returns a checkbox tag tailored for accessing a specified attribute (identified by +method+) on an object
  2162. # assigned to the template (identified by +object+). This object must be an instance object (@object) and not a local object.
  2163. # It's intended that +method+ returns an integer and if that integer is above zero, then the checkbox is checked.
  2164. # Additional options on the input tag can be passed as a hash with +options+. The +checked_value+ defaults to 1
  2165. # while the default +unchecked_value+ is set to 0 which is convenient for boolean values.
  2166. #
  2167. # ==== Gotcha
  2168. #
  2169. # The HTML specification says unchecked check boxes are not successful, and
  2170. # thus web browsers do not send them. Unfortunately this introduces a gotcha:
  2171. # if an +Invoice+ model has a +paid+ flag, and in the form that edits a paid
  2172. # invoice the user unchecks its check box, no +paid+ parameter is sent. So,
  2173. # any mass-assignment idiom like
  2174. #
  2175. # @invoice.update(params[:invoice])
  2176. #
  2177. # wouldn't update the flag.
  2178. #
  2179. # To prevent this the helper generates an auxiliary hidden field before
  2180. # the very check box. The hidden field has the same name and its
  2181. # attributes mimic an unchecked check box.
  2182. #
  2183. # This way, the client either sends only the hidden field (representing
  2184. # the check box is unchecked), or both fields. Since the HTML specification
  2185. # says key/value pairs have to be sent in the same order they appear in the
  2186. # form, and parameters extraction gets the last occurrence of any repeated
  2187. # key in the query string, that works for ordinary forms.
  2188. #
  2189. # Unfortunately that workaround does not work when the check box goes
  2190. # within an array-like parameter, as in
  2191. #
  2192. # <%= fields_for "project[invoice_attributes][]", invoice, index: nil do |form| %>
  2193. # <%= form.check_box :paid %>
  2194. # ...
  2195. # <% end %>
  2196. #
  2197. # because parameter name repetition is precisely what Rails seeks to distinguish
  2198. # the elements of the array. For each item with a checked check box you
  2199. # get an extra ghost item with only that attribute, assigned to "0".
  2200. #
  2201. # In that case it is preferable to either use +check_box_tag+ or to use
  2202. # hashes instead of arrays.
  2203. #
  2204. # # Let's say that @post.validated? is 1:
  2205. # check_box("validated")
  2206. # # => <input name="post[validated]" type="hidden" value="0" />
  2207. # # <input checked="checked" type="checkbox" id="post_validated" name="post[validated]" value="1" />
  2208. #
  2209. # # Let's say that @puppy.gooddog is "no":
  2210. # check_box("gooddog", {}, "yes", "no")
  2211. # # => <input name="puppy[gooddog]" type="hidden" value="no" />
  2212. # # <input type="checkbox" id="puppy_gooddog" name="puppy[gooddog]" value="yes" />
  2213. #
  2214. # # Let's say that @eula.accepted is "no":
  2215. # check_box("accepted", { class: 'eula_check' }, "yes", "no")
  2216. # # => <input name="eula[accepted]" type="hidden" value="no" />
  2217. # # <input type="checkbox" class="eula_check" id="eula_accepted" name="eula[accepted]" value="yes" />
  2218. 9 def check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
  2219. @template.check_box(@object_name, method, objectify_options(options), checked_value, unchecked_value)
  2220. end
  2221. # Returns a radio button tag for accessing a specified attribute (identified by +method+) on an object
  2222. # assigned to the template (identified by +object+). If the current value of +method+ is +tag_value+ the
  2223. # radio button will be checked.
  2224. #
  2225. # To force the radio button to be checked pass <tt>checked: true</tt> in the
  2226. # +options+ hash. You may pass HTML options there as well.
  2227. #
  2228. # # Let's say that @post.category returns "rails":
  2229. # radio_button("category", "rails")
  2230. # radio_button("category", "java")
  2231. # # => <input type="radio" id="post_category_rails" name="post[category]" value="rails" checked="checked" />
  2232. # # <input type="radio" id="post_category_java" name="post[category]" value="java" />
  2233. #
  2234. # # Let's say that @user.receive_newsletter returns "no":
  2235. # radio_button("receive_newsletter", "yes")
  2236. # radio_button("receive_newsletter", "no")
  2237. # # => <input type="radio" id="user_receive_newsletter_yes" name="user[receive_newsletter]" value="yes" />
  2238. # # <input type="radio" id="user_receive_newsletter_no" name="user[receive_newsletter]" value="no" checked="checked" />
  2239. 9 def radio_button(method, tag_value, options = {})
  2240. @template.radio_button(@object_name, method, tag_value, objectify_options(options))
  2241. end
  2242. # Returns a hidden input tag tailored for accessing a specified attribute (identified by +method+) on an object
  2243. # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
  2244. # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
  2245. # shown.
  2246. #
  2247. # ==== Examples
  2248. # # Let's say that @signup.pass_confirm returns true:
  2249. # hidden_field(:pass_confirm)
  2250. # # => <input type="hidden" id="signup_pass_confirm" name="signup[pass_confirm]" value="true" />
  2251. #
  2252. # # Let's say that @post.tag_list returns "blog, ruby":
  2253. # hidden_field(:tag_list)
  2254. # # => <input type="hidden" id="post_tag_list" name="post[tag_list]" value="blog, ruby" />
  2255. #
  2256. # # Let's say that @user.token returns "abcde":
  2257. # hidden_field(:token)
  2258. # # => <input type="hidden" id="user_token" name="user[token]" value="abcde" />
  2259. #
  2260. 9 def hidden_field(method, options = {})
  2261. @emitted_hidden_id = true if method == :id
  2262. @template.hidden_field(@object_name, method, objectify_options(options))
  2263. end
  2264. # Returns a file upload input tag tailored for accessing a specified attribute (identified by +method+) on an object
  2265. # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
  2266. # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
  2267. # shown.
  2268. #
  2269. # Using this method inside a +form_for+ block will set the enclosing form's encoding to <tt>multipart/form-data</tt>.
  2270. #
  2271. # ==== Options
  2272. # * Creates standard HTML attributes for the tag.
  2273. # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
  2274. # * <tt>:multiple</tt> - If set to true, *in most updated browsers* the user will be allowed to select multiple files.
  2275. # * <tt>:accept</tt> - If set to one or multiple mime-types, the user will be suggested a filter when choosing a file. You still need to set up model validations.
  2276. #
  2277. # ==== Examples
  2278. # # Let's say that @user has avatar:
  2279. # file_field(:avatar)
  2280. # # => <input type="file" id="user_avatar" name="user[avatar]" />
  2281. #
  2282. # # Let's say that @post has image:
  2283. # file_field(:image, :multiple => true)
  2284. # # => <input type="file" id="post_image" name="post[image][]" multiple="multiple" />
  2285. #
  2286. # # Let's say that @post has attached:
  2287. # file_field(:attached, accept: 'text/html')
  2288. # # => <input accept="text/html" type="file" id="post_attached" name="post[attached]" />
  2289. #
  2290. # # Let's say that @post has image:
  2291. # file_field(:image, accept: 'image/png,image/gif,image/jpeg')
  2292. # # => <input type="file" id="post_image" name="post[image]" accept="image/png,image/gif,image/jpeg" />
  2293. #
  2294. # # Let's say that @attachment has file:
  2295. # file_field(:file, class: 'file_input')
  2296. # # => <input type="file" id="attachment_file" name="attachment[file]" class="file_input" />
  2297. 9 def file_field(method, options = {})
  2298. self.multipart = true
  2299. @template.file_field(@object_name, method, objectify_options(options))
  2300. end
  2301. # Add the submit button for the given form. When no value is given, it checks
  2302. # if the object is a new resource or not to create the proper label:
  2303. #
  2304. # <%= form_for @post do |f| %>
  2305. # <%= f.submit %>
  2306. # <% end %>
  2307. #
  2308. # In the example above, if <tt>@post</tt> is a new record, it will use "Create Post" as
  2309. # submit button label; otherwise, it uses "Update Post".
  2310. #
  2311. # Those labels can be customized using I18n under the +helpers.submit+ key and using
  2312. # <tt>%{model}</tt> for translation interpolation:
  2313. #
  2314. # en:
  2315. # helpers:
  2316. # submit:
  2317. # create: "Create a %{model}"
  2318. # update: "Confirm changes to %{model}"
  2319. #
  2320. # It also searches for a key specific to the given object:
  2321. #
  2322. # en:
  2323. # helpers:
  2324. # submit:
  2325. # post:
  2326. # create: "Add %{model}"
  2327. #
  2328. 9 def submit(value = nil, options = {})
  2329. value, options = nil, value if value.is_a?(Hash)
  2330. value ||= submit_default_value
  2331. @template.submit_tag(value, options)
  2332. end
  2333. # Add the submit button for the given form. When no value is given, it checks
  2334. # if the object is a new resource or not to create the proper label:
  2335. #
  2336. # <%= form_for @post do |f| %>
  2337. # <%= f.button %>
  2338. # <% end %>
  2339. #
  2340. # In the example above, if <tt>@post</tt> is a new record, it will use "Create Post" as
  2341. # button label; otherwise, it uses "Update Post".
  2342. #
  2343. # Those labels can be customized using I18n under the +helpers.submit+ key
  2344. # (the same as submit helper) and using <tt>%{model}</tt> for translation interpolation:
  2345. #
  2346. # en:
  2347. # helpers:
  2348. # submit:
  2349. # create: "Create a %{model}"
  2350. # update: "Confirm changes to %{model}"
  2351. #
  2352. # It also searches for a key specific to the given object:
  2353. #
  2354. # en:
  2355. # helpers:
  2356. # submit:
  2357. # post:
  2358. # create: "Add %{model}"
  2359. #
  2360. # ==== Examples
  2361. # button("Create post")
  2362. # # => <button name='button' type='submit'>Create post</button>
  2363. #
  2364. # button do
  2365. # content_tag(:strong, 'Ask me!')
  2366. # end
  2367. # # => <button name='button' type='submit'>
  2368. # # <strong>Ask me!</strong>
  2369. # # </button>
  2370. #
  2371. 9 def button(value = nil, options = {}, &block)
  2372. value, options = nil, value if value.is_a?(Hash)
  2373. value ||= submit_default_value
  2374. @template.button_tag(value, options, &block)
  2375. end
  2376. 9 def emitted_hidden_id? # :nodoc:
  2377. @emitted_hidden_id ||= nil
  2378. end
  2379. 9 private
  2380. 9 def objectify_options(options)
  2381. result = @default_options.merge(options)
  2382. result[:object] = @object
  2383. result
  2384. end
  2385. 9 def submit_default_value
  2386. object = convert_to_model(@object)
  2387. key = object ? (object.persisted? ? :update : :create) : :submit
  2388. model = if object.respond_to?(:model_name)
  2389. object.model_name.human
  2390. else
  2391. @object_name.to_s.humanize
  2392. end
  2393. defaults = []
  2394. # Object is a model and it is not overwritten by as and scope option.
  2395. if object.respond_to?(:model_name) && object_name.to_s == model.downcase
  2396. defaults << :"helpers.submit.#{object.model_name.i18n_key}.#{key}"
  2397. else
  2398. defaults << :"helpers.submit.#{object_name}.#{key}"
  2399. end
  2400. defaults << :"helpers.submit.#{key}"
  2401. defaults << "#{key.to_s.humanize} #{model}"
  2402. I18n.t(defaults.shift, model: model, default: defaults)
  2403. end
  2404. 9 def nested_attributes_association?(association_name)
  2405. @object.respond_to?("#{association_name}_attributes=")
  2406. end
  2407. 9 def fields_for_with_nested_attributes(association_name, association, options, block)
  2408. name = "#{object_name}[#{association_name}_attributes]"
  2409. association = convert_to_model(association)
  2410. if association.respond_to?(:persisted?)
  2411. association = [association] if @object.send(association_name).respond_to?(:to_ary)
  2412. elsif !association.respond_to?(:to_ary)
  2413. association = @object.send(association_name)
  2414. end
  2415. if association.respond_to?(:to_ary)
  2416. explicit_child_index = options[:child_index]
  2417. output = ActiveSupport::SafeBuffer.new
  2418. association.each do |child|
  2419. if explicit_child_index
  2420. options[:child_index] = explicit_child_index.call if explicit_child_index.respond_to?(:call)
  2421. else
  2422. options[:child_index] = nested_child_index(name)
  2423. end
  2424. output << fields_for_nested_model("#{name}[#{options[:child_index]}]", child, options, block)
  2425. end
  2426. output
  2427. elsif association
  2428. fields_for_nested_model(name, association, options, block)
  2429. end
  2430. end
  2431. 9 def fields_for_nested_model(name, object, fields_options, block)
  2432. object = convert_to_model(object)
  2433. emit_hidden_id = object.persisted? && fields_options.fetch(:include_id) {
  2434. options.fetch(:include_id, true)
  2435. }
  2436. @template.fields_for(name, object, fields_options) do |f|
  2437. output = @template.capture(f, &block)
  2438. output.concat f.hidden_field(:id) if output && emit_hidden_id && !f.emitted_hidden_id?
  2439. output
  2440. end
  2441. end
  2442. 9 def nested_child_index(name)
  2443. @nested_child_index[name] ||= -1
  2444. @nested_child_index[name] += 1
  2445. end
  2446. 9 def convert_to_legacy_options(options)
  2447. if options.key?(:skip_id)
  2448. options[:include_id] = !options.delete(:skip_id)
  2449. end
  2450. end
  2451. end
  2452. end
  2453. 9 ActiveSupport.on_load(:action_view) do
  2454. 3 cattr_accessor :default_form_builder, instance_writer: false, instance_reader: false, default: ::ActionView::Helpers::FormBuilder
  2455. end
  2456. end

lib/action_view/helpers/form_options_helper.rb

32.17% lines covered

115 relevant lines. 37 lines covered and 78 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 require "cgi"
  3. 9 require "erb"
  4. 9 require "action_view/helpers/form_helper"
  5. 9 require "active_support/core_ext/string/output_safety"
  6. 9 require "active_support/core_ext/array/extract_options"
  7. 9 require "active_support/core_ext/array/wrap"
  8. 9 module ActionView
  9. # = Action View Form Option Helpers
  10. 9 module Helpers #:nodoc:
  11. # Provides a number of methods for turning different kinds of containers into a set of option tags.
  12. #
  13. # The <tt>collection_select</tt>, <tt>select</tt> and <tt>time_zone_select</tt> methods take an <tt>options</tt> parameter, a hash:
  14. #
  15. # * <tt>:include_blank</tt> - set to true or a prompt string if the first option element of the select element is a blank. Useful if there is not a default value required for the select element.
  16. #
  17. # select("post", "category", Post::CATEGORIES, { include_blank: true })
  18. #
  19. # could become:
  20. #
  21. # <select name="post[category]" id="post_category">
  22. # <option value="" label=" "></option>
  23. # <option value="joke">joke</option>
  24. # <option value="poem">poem</option>
  25. # </select>
  26. #
  27. # Another common case is a select tag for a <tt>belongs_to</tt>-associated object.
  28. #
  29. # Example with <tt>@post.person_id => 2</tt>:
  30. #
  31. # select("post", "person_id", Person.all.collect { |p| [ p.name, p.id ] }, { include_blank: 'None' })
  32. #
  33. # could become:
  34. #
  35. # <select name="post[person_id]" id="post_person_id">
  36. # <option value="">None</option>
  37. # <option value="1">David</option>
  38. # <option value="2" selected="selected">Eileen</option>
  39. # <option value="3">Rafael</option>
  40. # </select>
  41. #
  42. # * <tt>:prompt</tt> - set to true or a prompt string. When the select element doesn't have a value yet, this prepends an option with a generic prompt -- "Please select" -- or the given prompt string.
  43. #
  44. # select("post", "person_id", Person.all.collect { |p| [ p.name, p.id ] }, { prompt: 'Select Person' })
  45. #
  46. # could become:
  47. #
  48. # <select name="post[person_id]" id="post_person_id">
  49. # <option value="">Select Person</option>
  50. # <option value="1">David</option>
  51. # <option value="2">Eileen</option>
  52. # <option value="3">Rafael</option>
  53. # </select>
  54. #
  55. # * <tt>:index</tt> - like the other form helpers, +select+ can accept an <tt>:index</tt> option to manually set the ID used in the resulting output. Unlike other helpers, +select+ expects this
  56. # option to be in the +html_options+ parameter.
  57. #
  58. # select("album[]", "genre", %w[rap rock country], {}, { index: nil })
  59. #
  60. # becomes:
  61. #
  62. # <select name="album[][genre]" id="album__genre">
  63. # <option value="rap">rap</option>
  64. # <option value="rock">rock</option>
  65. # <option value="country">country</option>
  66. # </select>
  67. #
  68. # * <tt>:disabled</tt> - can be a single value or an array of values that will be disabled options in the final output.
  69. #
  70. # select("post", "category", Post::CATEGORIES, { disabled: 'restricted' })
  71. #
  72. # could become:
  73. #
  74. # <select name="post[category]" id="post_category">
  75. # <option value="joke">joke</option>
  76. # <option value="poem">poem</option>
  77. # <option disabled="disabled" value="restricted">restricted</option>
  78. # </select>
  79. #
  80. # When used with the <tt>collection_select</tt> helper, <tt>:disabled</tt> can also be a Proc that identifies those options that should be disabled.
  81. #
  82. # collection_select(:post, :category_id, Category.all, :id, :name, { disabled: -> (category) { category.archived? } })
  83. #
  84. # If the categories "2008 stuff" and "Christmas" return true when the method <tt>archived?</tt> is called, this would return:
  85. # <select name="post[category_id]" id="post_category_id">
  86. # <option value="1" disabled="disabled">2008 stuff</option>
  87. # <option value="2" disabled="disabled">Christmas</option>
  88. # <option value="3">Jokes</option>
  89. # <option value="4">Poems</option>
  90. # </select>
  91. #
  92. 9 module FormOptionsHelper
  93. # ERB::Util can mask some helpers like textilize. Make sure to include them.
  94. 9 include TextHelper
  95. # Create a select tag and a series of contained option tags for the provided object and method.
  96. # The option currently held by the object will be selected, provided that the object is available.
  97. #
  98. # There are two possible formats for the +choices+ parameter, corresponding to other helpers' output:
  99. #
  100. # * A flat collection (see +options_for_select+).
  101. #
  102. # * A nested collection (see +grouped_options_for_select+).
  103. #
  104. # For example:
  105. #
  106. # select("post", "person_id", Person.all.collect { |p| [ p.name, p.id ] }, { include_blank: true })
  107. #
  108. # would become:
  109. #
  110. # <select name="post[person_id]" id="post_person_id">
  111. # <option value="" label=" "></option>
  112. # <option value="1" selected="selected">David</option>
  113. # <option value="2">Eileen</option>
  114. # <option value="3">Rafael</option>
  115. # </select>
  116. #
  117. # assuming the associated person has ID 1.
  118. #
  119. # This can be used to provide a default set of options in the standard way: before rendering the create form, a
  120. # new model instance is assigned the default options and bound to @model_name. Usually this model is not saved
  121. # to the database. Instead, a second model object is created when the create request is received.
  122. # This allows the user to submit a form page more than once with the expected results of creating multiple records.
  123. # In addition, this allows a single partial to be used to generate form inputs for both edit and create forms.
  124. #
  125. # By default, <tt>post.person_id</tt> is the selected option. Specify <tt>selected: value</tt> to use a different selection
  126. # or <tt>selected: nil</tt> to leave all options unselected. Similarly, you can specify values to be disabled in the option
  127. # tags by specifying the <tt>:disabled</tt> option. This can either be a single value or an array of values to be disabled.
  128. #
  129. # A block can be passed to +select+ to customize how the options tags will be rendered. This
  130. # is useful when the options tag has complex attributes.
  131. #
  132. # select(report, "campaign_ids") do
  133. # available_campaigns.each do |c|
  134. # content_tag(:option, c.name, value: c.id, data: { tags: c.tags.to_json })
  135. # end
  136. # end
  137. #
  138. # ==== Gotcha
  139. #
  140. # The HTML specification says when +multiple+ parameter passed to select and all options got deselected
  141. # web browsers do not send any value to server. Unfortunately this introduces a gotcha:
  142. # if a +User+ model has many +roles+ and have +role_ids+ accessor, and in the form that edits roles of the user
  143. # the user deselects all roles from +role_ids+ multiple select box, no +role_ids+ parameter is sent. So,
  144. # any mass-assignment idiom like
  145. #
  146. # @user.update(params[:user])
  147. #
  148. # wouldn't update roles.
  149. #
  150. # To prevent this the helper generates an auxiliary hidden field before
  151. # every multiple select. The hidden field has the same name as multiple select and blank value.
  152. #
  153. # <b>Note:</b> The client either sends only the hidden field (representing
  154. # the deselected multiple select box), or both fields. This means that the resulting array
  155. # always contains a blank string.
  156. #
  157. # In case if you don't want the helper to generate this hidden field you can specify
  158. # <tt>include_hidden: false</tt> option.
  159. #
  160. 9 def select(object, method, choices = nil, options = {}, html_options = {}, &block)
  161. Tags::Select.new(object, method, self, choices, options, html_options, &block).render
  162. end
  163. # Returns <tt><select></tt> and <tt><option></tt> tags for the collection of existing return values of
  164. # +method+ for +object+'s class. The value returned from calling +method+ on the instance +object+ will
  165. # be selected. If calling +method+ returns +nil+, no selection is made without including <tt>:prompt</tt>
  166. # or <tt>:include_blank</tt> in the +options+ hash.
  167. #
  168. # The <tt>:value_method</tt> and <tt>:text_method</tt> parameters are methods to be called on each member
  169. # of +collection+. The return values are used as the +value+ attribute and contents of each
  170. # <tt><option></tt> tag, respectively. They can also be any object that responds to +call+, such
  171. # as a +proc+, that will be called for each member of the +collection+ to
  172. # retrieve the value/text.
  173. #
  174. # Example object structure for use with this method:
  175. #
  176. # class Post < ActiveRecord::Base
  177. # belongs_to :author
  178. # end
  179. #
  180. # class Author < ActiveRecord::Base
  181. # has_many :posts
  182. # def name_with_initial
  183. # "#{first_name.first}. #{last_name}"
  184. # end
  185. # end
  186. #
  187. # Sample usage (selecting the associated Author for an instance of Post, <tt>@post</tt>):
  188. #
  189. # collection_select(:post, :author_id, Author.all, :id, :name_with_initial, prompt: true)
  190. #
  191. # If <tt>@post.author_id</tt> is already <tt>1</tt>, this would return:
  192. # <select name="post[author_id]" id="post_author_id">
  193. # <option value="">Please select</option>
  194. # <option value="1" selected="selected">D. Heinemeier Hansson</option>
  195. # <option value="2">D. Thomas</option>
  196. # <option value="3">M. Clark</option>
  197. # </select>
  198. 9 def collection_select(object, method, collection, value_method, text_method, options = {}, html_options = {})
  199. Tags::CollectionSelect.new(object, method, self, collection, value_method, text_method, options, html_options).render
  200. end
  201. # Returns <tt><select></tt>, <tt><optgroup></tt> and <tt><option></tt> tags for the collection of existing return values of
  202. # +method+ for +object+'s class. The value returned from calling +method+ on the instance +object+ will
  203. # be selected. If calling +method+ returns +nil+, no selection is made without including <tt>:prompt</tt>
  204. # or <tt>:include_blank</tt> in the +options+ hash.
  205. #
  206. # Parameters:
  207. # * +object+ - The instance of the class to be used for the select tag
  208. # * +method+ - The attribute of +object+ corresponding to the select tag
  209. # * +collection+ - An array of objects representing the <tt><optgroup></tt> tags.
  210. # * +group_method+ - The name of a method which, when called on a member of +collection+, returns an
  211. # array of child objects representing the <tt><option></tt> tags. It can also be any object that responds
  212. # to +call+, such as a +proc+, that will be called for each member of the +collection+ to retrieve the
  213. # value.
  214. # * +group_label_method+ - The name of a method which, when called on a member of +collection+, returns a
  215. # string to be used as the +label+ attribute for its <tt><optgroup></tt> tag. It can also be any object
  216. # that responds to +call+, such as a +proc+, that will be called for each member of the +collection+ to
  217. # retrieve the label.
  218. # * +option_key_method+ - The name of a method which, when called on a child object of a member of
  219. # +collection+, returns a value to be used as the +value+ attribute for its <tt><option></tt> tag.
  220. # * +option_value_method+ - The name of a method which, when called on a child object of a member of
  221. # +collection+, returns a value to be used as the contents of its <tt><option></tt> tag.
  222. #
  223. # Example object structure for use with this method:
  224. #
  225. # class Continent < ActiveRecord::Base
  226. # has_many :countries
  227. # # attribs: id, name
  228. # end
  229. #
  230. # class Country < ActiveRecord::Base
  231. # belongs_to :continent
  232. # # attribs: id, name, continent_id
  233. # end
  234. #
  235. # class City < ActiveRecord::Base
  236. # belongs_to :country
  237. # # attribs: id, name, country_id
  238. # end
  239. #
  240. # Sample usage:
  241. #
  242. # grouped_collection_select(:city, :country_id, @continents, :countries, :name, :id, :name)
  243. #
  244. # Possible output:
  245. #
  246. # <select name="city[country_id]" id="city_country_id">
  247. # <optgroup label="Africa">
  248. # <option value="1">South Africa</option>
  249. # <option value="3">Somalia</option>
  250. # </optgroup>
  251. # <optgroup label="Europe">
  252. # <option value="7" selected="selected">Denmark</option>
  253. # <option value="2">Ireland</option>
  254. # </optgroup>
  255. # </select>
  256. #
  257. 9 def grouped_collection_select(object, method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {})
  258. Tags::GroupedCollectionSelect.new(object, method, self, collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options).render
  259. end
  260. # Returns select and option tags for the given object and method, using
  261. # #time_zone_options_for_select to generate the list of option tags.
  262. #
  263. # In addition to the <tt>:include_blank</tt> option documented above,
  264. # this method also supports a <tt>:model</tt> option, which defaults
  265. # to ActiveSupport::TimeZone. This may be used by users to specify a
  266. # different time zone model object. (See +time_zone_options_for_select+
  267. # for more information.)
  268. #
  269. # You can also supply an array of ActiveSupport::TimeZone objects
  270. # as +priority_zones+ so that they will be listed above the rest of the
  271. # (long) list. You can use ActiveSupport::TimeZone.us_zones for a list
  272. # of US time zones, ActiveSupport::TimeZone.country_zones(country_code)
  273. # for another country's time zones, or a Regexp to select the zones of
  274. # your choice.
  275. #
  276. # Finally, this method supports a <tt>:default</tt> option, which selects
  277. # a default ActiveSupport::TimeZone if the object's time zone is +nil+.
  278. #
  279. # time_zone_select("user", "time_zone", nil, include_blank: true)
  280. #
  281. # time_zone_select("user", "time_zone", nil, default: "Pacific Time (US & Canada)")
  282. #
  283. # time_zone_select("user", 'time_zone', ActiveSupport::TimeZone.us_zones, default: "Pacific Time (US & Canada)")
  284. #
  285. # time_zone_select("user", 'time_zone', [ ActiveSupport::TimeZone['Alaska'], ActiveSupport::TimeZone['Hawaii'] ])
  286. #
  287. # time_zone_select("user", 'time_zone', /Australia/)
  288. #
  289. # time_zone_select("user", "time_zone", ActiveSupport::TimeZone.all.sort, model: ActiveSupport::TimeZone)
  290. 9 def time_zone_select(object, method, priority_zones = nil, options = {}, html_options = {})
  291. Tags::TimeZoneSelect.new(object, method, self, priority_zones, options, html_options).render
  292. end
  293. # Accepts a container (hash, array, enumerable, your type) and returns a string of option tags. Given a container
  294. # where the elements respond to first and last (such as a two-element array), the "lasts" serve as option values and
  295. # the "firsts" as option text. Hashes are turned into this form automatically, so the keys become "firsts" and values
  296. # become lasts. If +selected+ is specified, the matching "last" or element will get the selected option-tag. +selected+
  297. # may also be an array of values to be selected when using a multiple select.
  298. #
  299. # options_for_select([["Dollar", "$"], ["Kroner", "DKK"]])
  300. # # => <option value="$">Dollar</option>
  301. # # => <option value="DKK">Kroner</option>
  302. #
  303. # options_for_select([ "VISA", "MasterCard" ], "MasterCard")
  304. # # => <option value="VISA">VISA</option>
  305. # # => <option selected="selected" value="MasterCard">MasterCard</option>
  306. #
  307. # options_for_select({ "Basic" => "$20", "Plus" => "$40" }, "$40")
  308. # # => <option value="$20">Basic</option>
  309. # # => <option value="$40" selected="selected">Plus</option>
  310. #
  311. # options_for_select([ "VISA", "MasterCard", "Discover" ], ["VISA", "Discover"])
  312. # # => <option selected="selected" value="VISA">VISA</option>
  313. # # => <option value="MasterCard">MasterCard</option>
  314. # # => <option selected="selected" value="Discover">Discover</option>
  315. #
  316. # You can optionally provide HTML attributes as the last element of the array.
  317. #
  318. # options_for_select([ "Denmark", ["USA", { class: 'bold' }], "Sweden" ], ["USA", "Sweden"])
  319. # # => <option value="Denmark">Denmark</option>
  320. # # => <option value="USA" class="bold" selected="selected">USA</option>
  321. # # => <option value="Sweden" selected="selected">Sweden</option>
  322. #
  323. # options_for_select([["Dollar", "$", { class: "bold" }], ["Kroner", "DKK", { onclick: "alert('HI');" }]])
  324. # # => <option value="$" class="bold">Dollar</option>
  325. # # => <option value="DKK" onclick="alert('HI');">Kroner</option>
  326. #
  327. # If you wish to specify disabled option tags, set +selected+ to be a hash, with <tt>:disabled</tt> being either a value
  328. # or array of values to be disabled. In this case, you can use <tt>:selected</tt> to specify selected option tags.
  329. #
  330. # options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], disabled: "Super Platinum")
  331. # # => <option value="Free">Free</option>
  332. # # => <option value="Basic">Basic</option>
  333. # # => <option value="Advanced">Advanced</option>
  334. # # => <option value="Super Platinum" disabled="disabled">Super Platinum</option>
  335. #
  336. # options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], disabled: ["Advanced", "Super Platinum"])
  337. # # => <option value="Free">Free</option>
  338. # # => <option value="Basic">Basic</option>
  339. # # => <option value="Advanced" disabled="disabled">Advanced</option>
  340. # # => <option value="Super Platinum" disabled="disabled">Super Platinum</option>
  341. #
  342. # options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], selected: "Free", disabled: "Super Platinum")
  343. # # => <option value="Free" selected="selected">Free</option>
  344. # # => <option value="Basic">Basic</option>
  345. # # => <option value="Advanced">Advanced</option>
  346. # # => <option value="Super Platinum" disabled="disabled">Super Platinum</option>
  347. #
  348. # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag.
  349. 9 def options_for_select(container, selected = nil)
  350. return container if String === container
  351. selected, disabled = extract_selected_and_disabled(selected).map do |r|
  352. Array(r).map(&:to_s)
  353. end
  354. container.map do |element|
  355. html_attributes = option_html_attributes(element)
  356. text, value = option_text_and_value(element).map(&:to_s)
  357. html_attributes[:selected] ||= option_value_selected?(value, selected)
  358. html_attributes[:disabled] ||= disabled && option_value_selected?(value, disabled)
  359. html_attributes[:value] = value
  360. tag_builder.content_tag_string(:option, text, html_attributes)
  361. end.join("\n").html_safe
  362. end
  363. # Returns a string of option tags that have been compiled by iterating over the +collection+ and assigning
  364. # the result of a call to the +value_method+ as the option value and the +text_method+ as the option text.
  365. #
  366. # options_from_collection_for_select(@people, 'id', 'name')
  367. # # => <option value="#{person.id}">#{person.name}</option>
  368. #
  369. # This is more often than not used inside a #select_tag like this example:
  370. #
  371. # select_tag 'person', options_from_collection_for_select(@people, 'id', 'name')
  372. #
  373. # If +selected+ is specified as a value or array of values, the element(s) returning a match on +value_method+
  374. # will be selected option tag(s).
  375. #
  376. # If +selected+ is specified as a Proc, those members of the collection that return true for the anonymous
  377. # function are the selected values.
  378. #
  379. # +selected+ can also be a hash, specifying both <tt>:selected</tt> and/or <tt>:disabled</tt> values as required.
  380. #
  381. # Be sure to specify the same class as the +value_method+ when specifying selected or disabled options.
  382. # Failure to do this will produce undesired results. Example:
  383. # options_from_collection_for_select(@people, 'id', 'name', '1')
  384. # Will not select a person with the id of 1 because 1 (an Integer) is not the same as '1' (a string)
  385. # options_from_collection_for_select(@people, 'id', 'name', 1)
  386. # should produce the desired results.
  387. 9 def options_from_collection_for_select(collection, value_method, text_method, selected = nil)
  388. options = collection.map do |element|
  389. [value_for_collection(element, text_method), value_for_collection(element, value_method), option_html_attributes(element)]
  390. end
  391. selected, disabled = extract_selected_and_disabled(selected)
  392. select_deselect = {
  393. selected: extract_values_from_collection(collection, value_method, selected),
  394. disabled: extract_values_from_collection(collection, value_method, disabled)
  395. }
  396. options_for_select(options, select_deselect)
  397. end
  398. # Returns a string of <tt><option></tt> tags, like <tt>options_from_collection_for_select</tt>, but
  399. # groups them by <tt><optgroup></tt> tags based on the object relationships of the arguments.
  400. #
  401. # Parameters:
  402. # * +collection+ - An array of objects representing the <tt><optgroup></tt> tags.
  403. # * +group_method+ - The name of a method which, when called on a member of +collection+, returns an
  404. # array of child objects representing the <tt><option></tt> tags.
  405. # * +group_label_method+ - The name of a method which, when called on a member of +collection+, returns a
  406. # string to be used as the +label+ attribute for its <tt><optgroup></tt> tag.
  407. # * +option_key_method+ - The name of a method which, when called on a child object of a member of
  408. # +collection+, returns a value to be used as the +value+ attribute for its <tt><option></tt> tag.
  409. # * +option_value_method+ - The name of a method which, when called on a child object of a member of
  410. # +collection+, returns a value to be used as the contents of its <tt><option></tt> tag.
  411. # * +selected_key+ - A value equal to the +value+ attribute for one of the <tt><option></tt> tags,
  412. # which will have the +selected+ attribute set. Corresponds to the return value of one of the calls
  413. # to +option_key_method+. If +nil+, no selection is made. Can also be a hash if disabled values are
  414. # to be specified.
  415. #
  416. # Example object structure for use with this method:
  417. #
  418. # class Continent < ActiveRecord::Base
  419. # has_many :countries
  420. # # attribs: id, name
  421. # end
  422. #
  423. # class Country < ActiveRecord::Base
  424. # belongs_to :continent
  425. # # attribs: id, name, continent_id
  426. # end
  427. #
  428. # Sample usage:
  429. # option_groups_from_collection_for_select(@continents, :countries, :name, :id, :name, 3)
  430. #
  431. # Possible output:
  432. # <optgroup label="Africa">
  433. # <option value="1">Egypt</option>
  434. # <option value="4">Rwanda</option>
  435. # ...
  436. # </optgroup>
  437. # <optgroup label="Asia">
  438. # <option value="3" selected="selected">China</option>
  439. # <option value="12">India</option>
  440. # <option value="5">Japan</option>
  441. # ...
  442. # </optgroup>
  443. #
  444. # <b>Note:</b> Only the <tt><optgroup></tt> and <tt><option></tt> tags are returned, so you still have to
  445. # wrap the output in an appropriate <tt><select></tt> tag.
  446. 9 def option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key = nil)
  447. collection.map do |group|
  448. option_tags = options_from_collection_for_select(
  449. value_for_collection(group, group_method), option_key_method, option_value_method, selected_key)
  450. content_tag("optgroup", option_tags, label: value_for_collection(group, group_label_method))
  451. end.join.html_safe
  452. end
  453. # Returns a string of <tt><option></tt> tags, like <tt>options_for_select</tt>, but
  454. # wraps them with <tt><optgroup></tt> tags:
  455. #
  456. # grouped_options = [
  457. # ['North America',
  458. # [['United States','US'],'Canada']],
  459. # ['Europe',
  460. # ['Denmark','Germany','France']]
  461. # ]
  462. # grouped_options_for_select(grouped_options)
  463. #
  464. # grouped_options = {
  465. # 'North America' => [['United States','US'], 'Canada'],
  466. # 'Europe' => ['Denmark','Germany','France']
  467. # }
  468. # grouped_options_for_select(grouped_options)
  469. #
  470. # Possible output:
  471. # <optgroup label="North America">
  472. # <option value="US">United States</option>
  473. # <option value="Canada">Canada</option>
  474. # </optgroup>
  475. # <optgroup label="Europe">
  476. # <option value="Denmark">Denmark</option>
  477. # <option value="Germany">Germany</option>
  478. # <option value="France">France</option>
  479. # </optgroup>
  480. #
  481. # Parameters:
  482. # * +grouped_options+ - Accepts a nested array or hash of strings. The first value serves as the
  483. # <tt><optgroup></tt> label while the second value must be an array of options. The second value can be a
  484. # nested array of text-value pairs. See <tt>options_for_select</tt> for more info.
  485. # Ex. ["North America",[["United States","US"],["Canada","CA"]]]
  486. # * +selected_key+ - A value equal to the +value+ attribute for one of the <tt><option></tt> tags,
  487. # which will have the +selected+ attribute set. Note: It is possible for this value to match multiple options
  488. # as you might have the same option in multiple groups. Each will then get <tt>selected="selected"</tt>.
  489. #
  490. # Options:
  491. # * <tt>:prompt</tt> - set to true or a prompt string. When the select element doesn't have a value yet, this
  492. # prepends an option with a generic prompt - "Please select" - or the given prompt string.
  493. # * <tt>:divider</tt> - the divider for the options groups.
  494. #
  495. # grouped_options = [
  496. # [['United States','US'], 'Canada'],
  497. # ['Denmark','Germany','France']
  498. # ]
  499. # grouped_options_for_select(grouped_options, nil, divider: '---------')
  500. #
  501. # Possible output:
  502. # <optgroup label="---------">
  503. # <option value="US">United States</option>
  504. # <option value="Canada">Canada</option>
  505. # </optgroup>
  506. # <optgroup label="---------">
  507. # <option value="Denmark">Denmark</option>
  508. # <option value="Germany">Germany</option>
  509. # <option value="France">France</option>
  510. # </optgroup>
  511. #
  512. # <b>Note:</b> Only the <tt><optgroup></tt> and <tt><option></tt> tags are returned, so you still have to
  513. # wrap the output in an appropriate <tt><select></tt> tag.
  514. 9 def grouped_options_for_select(grouped_options, selected_key = nil, options = {})
  515. prompt = options[:prompt]
  516. divider = options[:divider]
  517. body = "".html_safe
  518. if prompt
  519. body.safe_concat content_tag("option", prompt_text(prompt), value: "")
  520. end
  521. grouped_options.each do |container|
  522. html_attributes = option_html_attributes(container)
  523. if divider
  524. label = divider
  525. else
  526. label, container = container
  527. end
  528. html_attributes = { label: label }.merge!(html_attributes)
  529. body.safe_concat content_tag("optgroup", options_for_select(container, selected_key), html_attributes)
  530. end
  531. body
  532. end
  533. # Returns a string of option tags for pretty much any time zone in the
  534. # world. Supply an ActiveSupport::TimeZone name as +selected+ to have it
  535. # marked as the selected option tag. You can also supply an array of
  536. # ActiveSupport::TimeZone objects as +priority_zones+, so that they will
  537. # be listed above the rest of the (long) list. (You can use
  538. # ActiveSupport::TimeZone.us_zones as a convenience for obtaining a list
  539. # of the US time zones, or a Regexp to select the zones of your choice)
  540. #
  541. # The +selected+ parameter must be either +nil+, or a string that names
  542. # an ActiveSupport::TimeZone.
  543. #
  544. # By default, +model+ is the ActiveSupport::TimeZone constant (which can
  545. # be obtained in Active Record as a value object). The +model+ parameter
  546. # must respond to +all+ and return an array of objects that represent time
  547. # zones; each object must respond to +name+. If a Regexp is given it will
  548. # attempt to match the zones using <code>match?</code> method.
  549. #
  550. # NOTE: Only the option tags are returned, you have to wrap this call in
  551. # a regular HTML select tag.
  552. 9 def time_zone_options_for_select(selected = nil, priority_zones = nil, model = ::ActiveSupport::TimeZone)
  553. zone_options = "".html_safe
  554. zones = model.all
  555. convert_zones = lambda { |list| list.map { |z| [ z.to_s, z.name ] } }
  556. if priority_zones
  557. if priority_zones.is_a?(Regexp)
  558. priority_zones = zones.select { |z| z.match?(priority_zones) }
  559. end
  560. zone_options.safe_concat options_for_select(convert_zones[priority_zones], selected)
  561. zone_options.safe_concat content_tag("option", "-------------", value: "", disabled: true)
  562. zone_options.safe_concat "\n"
  563. zones = zones - priority_zones
  564. end
  565. zone_options.safe_concat options_for_select(convert_zones[zones], selected)
  566. end
  567. # Returns radio button tags for the collection of existing return values
  568. # of +method+ for +object+'s class. The value returned from calling
  569. # +method+ on the instance +object+ will be selected. If calling +method+
  570. # returns +nil+, no selection is made.
  571. #
  572. # The <tt>:value_method</tt> and <tt>:text_method</tt> parameters are
  573. # methods to be called on each member of +collection+. The return values
  574. # are used as the +value+ attribute and contents of each radio button tag,
  575. # respectively. They can also be any object that responds to +call+, such
  576. # as a +proc+, that will be called for each member of the +collection+ to
  577. # retrieve the value/text.
  578. #
  579. # Example object structure for use with this method:
  580. # class Post < ActiveRecord::Base
  581. # belongs_to :author
  582. # end
  583. # class Author < ActiveRecord::Base
  584. # has_many :posts
  585. # def name_with_initial
  586. # "#{first_name.first}. #{last_name}"
  587. # end
  588. # end
  589. #
  590. # Sample usage (selecting the associated Author for an instance of Post, <tt>@post</tt>):
  591. # collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial)
  592. #
  593. # If <tt>@post.author_id</tt> is already <tt>1</tt>, this would return:
  594. # <input id="post_author_id_1" name="post[author_id]" type="radio" value="1" checked="checked" />
  595. # <label for="post_author_id_1">D. Heinemeier Hansson</label>
  596. # <input id="post_author_id_2" name="post[author_id]" type="radio" value="2" />
  597. # <label for="post_author_id_2">D. Thomas</label>
  598. # <input id="post_author_id_3" name="post[author_id]" type="radio" value="3" />
  599. # <label for="post_author_id_3">M. Clark</label>
  600. #
  601. # It is also possible to customize the way the elements will be shown by
  602. # giving a block to the method:
  603. # collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) do |b|
  604. # b.label { b.radio_button }
  605. # end
  606. #
  607. # The argument passed to the block is a special kind of builder for this
  608. # collection, which has the ability to generate the label and radio button
  609. # for the current item in the collection, with proper text and value.
  610. # Using it, you can change the label and radio button display order or
  611. # even use the label as wrapper, as in the example above.
  612. #
  613. # The builder methods <tt>label</tt> and <tt>radio_button</tt> also accept
  614. # extra HTML options:
  615. # collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) do |b|
  616. # b.label(class: "radio_button") { b.radio_button(class: "radio_button") }
  617. # end
  618. #
  619. # There are also three special methods available: <tt>object</tt>, <tt>text</tt> and
  620. # <tt>value</tt>, which are the current item being rendered, its text and value methods,
  621. # respectively. You can use them like this:
  622. # collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) do |b|
  623. # b.label(:"data-value" => b.value) { b.radio_button + b.text }
  624. # end
  625. #
  626. # ==== Gotcha
  627. #
  628. # The HTML specification says when nothing is selected on a collection of radio buttons
  629. # web browsers do not send any value to server.
  630. # Unfortunately this introduces a gotcha:
  631. # if a +User+ model has a +category_id+ field and in the form no category is selected, no +category_id+ parameter is sent. So,
  632. # any strong parameters idiom like:
  633. #
  634. # params.require(:user).permit(...)
  635. #
  636. # will raise an error since no <tt>{user: ...}</tt> will be present.
  637. #
  638. # To prevent this the helper generates an auxiliary hidden field before
  639. # every collection of radio buttons. The hidden field has the same name as collection radio button and blank value.
  640. #
  641. # In case if you don't want the helper to generate this hidden field you can specify
  642. # <tt>include_hidden: false</tt> option.
  643. 9 def collection_radio_buttons(object, method, collection, value_method, text_method, options = {}, html_options = {}, &block)
  644. Tags::CollectionRadioButtons.new(object, method, self, collection, value_method, text_method, options, html_options).render(&block)
  645. end
  646. # Returns check box tags for the collection of existing return values of
  647. # +method+ for +object+'s class. The value returned from calling +method+
  648. # on the instance +object+ will be selected. If calling +method+ returns
  649. # +nil+, no selection is made.
  650. #
  651. # The <tt>:value_method</tt> and <tt>:text_method</tt> parameters are
  652. # methods to be called on each member of +collection+. The return values
  653. # are used as the +value+ attribute and contents of each check box tag,
  654. # respectively. They can also be any object that responds to +call+, such
  655. # as a +proc+, that will be called for each member of the +collection+ to
  656. # retrieve the value/text.
  657. #
  658. # Example object structure for use with this method:
  659. # class Post < ActiveRecord::Base
  660. # has_and_belongs_to_many :authors
  661. # end
  662. # class Author < ActiveRecord::Base
  663. # has_and_belongs_to_many :posts
  664. # def name_with_initial
  665. # "#{first_name.first}. #{last_name}"
  666. # end
  667. # end
  668. #
  669. # Sample usage (selecting the associated Author for an instance of Post, <tt>@post</tt>):
  670. # collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial)
  671. #
  672. # If <tt>@post.author_ids</tt> is already <tt>[1]</tt>, this would return:
  673. # <input id="post_author_ids_1" name="post[author_ids][]" type="checkbox" value="1" checked="checked" />
  674. # <label for="post_author_ids_1">D. Heinemeier Hansson</label>
  675. # <input id="post_author_ids_2" name="post[author_ids][]" type="checkbox" value="2" />
  676. # <label for="post_author_ids_2">D. Thomas</label>
  677. # <input id="post_author_ids_3" name="post[author_ids][]" type="checkbox" value="3" />
  678. # <label for="post_author_ids_3">M. Clark</label>
  679. # <input name="post[author_ids][]" type="hidden" value="" />
  680. #
  681. # It is also possible to customize the way the elements will be shown by
  682. # giving a block to the method:
  683. # collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) do |b|
  684. # b.label { b.check_box }
  685. # end
  686. #
  687. # The argument passed to the block is a special kind of builder for this
  688. # collection, which has the ability to generate the label and check box
  689. # for the current item in the collection, with proper text and value.
  690. # Using it, you can change the label and check box display order or even
  691. # use the label as wrapper, as in the example above.
  692. #
  693. # The builder methods <tt>label</tt> and <tt>check_box</tt> also accept
  694. # extra HTML options:
  695. # collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) do |b|
  696. # b.label(class: "check_box") { b.check_box(class: "check_box") }
  697. # end
  698. #
  699. # There are also three special methods available: <tt>object</tt>, <tt>text</tt> and
  700. # <tt>value</tt>, which are the current item being rendered, its text and value methods,
  701. # respectively. You can use them like this:
  702. # collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) do |b|
  703. # b.label(:"data-value" => b.value) { b.check_box + b.text }
  704. # end
  705. #
  706. # ==== Gotcha
  707. #
  708. # When no selection is made for a collection of checkboxes most
  709. # web browsers will not send any value.
  710. #
  711. # For example, if we have a +User+ model with +category_ids+ field and we
  712. # have the following code in our update action:
  713. #
  714. # @user.update(params[:user])
  715. #
  716. # If no +category_ids+ are selected then we can safely assume this field
  717. # will not be updated.
  718. #
  719. # This is possible thanks to a hidden field generated by the helper method
  720. # for every collection of checkboxes.
  721. # This hidden field is given the same field name as the checkboxes with a
  722. # blank value.
  723. #
  724. # In the rare case you don't want this hidden field, you can pass the
  725. # <tt>include_hidden: false</tt> option to the helper method.
  726. 9 def collection_check_boxes(object, method, collection, value_method, text_method, options = {}, html_options = {}, &block)
  727. Tags::CollectionCheckBoxes.new(object, method, self, collection, value_method, text_method, options, html_options).render(&block)
  728. end
  729. 9 private
  730. 9 def option_html_attributes(element)
  731. if Array === element
  732. element.select { |e| Hash === e }.reduce({}, :merge!)
  733. else
  734. {}
  735. end
  736. end
  737. 9 def option_text_and_value(option)
  738. # Options are [text, value] pairs or strings used for both.
  739. if !option.is_a?(String) && option.respond_to?(:first) && option.respond_to?(:last)
  740. option = option.reject { |e| Hash === e } if Array === option
  741. [option.first, option.last]
  742. else
  743. [option, option]
  744. end
  745. end
  746. 9 def option_value_selected?(value, selected)
  747. Array(selected).include? value
  748. end
  749. 9 def extract_selected_and_disabled(selected)
  750. if selected.is_a?(Proc)
  751. [selected, nil]
  752. else
  753. selected = Array.wrap(selected)
  754. options = selected.extract_options!.symbolize_keys
  755. selected_items = options.fetch(:selected, selected)
  756. [selected_items, options[:disabled]]
  757. end
  758. end
  759. 9 def extract_values_from_collection(collection, value_method, selected)
  760. if selected.is_a?(Proc)
  761. collection.map do |element|
  762. public_or_deprecated_send(element, value_method) if selected.call(element)
  763. end.compact
  764. else
  765. selected
  766. end
  767. end
  768. 9 def value_for_collection(item, value)
  769. value.respond_to?(:call) ? value.call(item) : public_or_deprecated_send(item, value)
  770. end
  771. 9 def public_or_deprecated_send(item, value)
  772. item.public_send(value)
  773. rescue NoMethodError
  774. raise unless item.respond_to?(value, true) && !item.respond_to?(value)
  775. ActiveSupport::Deprecation.warn "Using private methods from view helpers is deprecated (calling private #{item.class}##{value})"
  776. item.send(value)
  777. end
  778. 9 def prompt_text(prompt)
  779. prompt.kind_of?(String) ? prompt : I18n.translate("helpers.select.prompt", default: "Please select")
  780. end
  781. end
  782. 9 class FormBuilder
  783. # Wraps ActionView::Helpers::FormOptionsHelper#select for form builders:
  784. #
  785. # <%= form_for @post do |f| %>
  786. # <%= f.select :person_id, Person.all.collect { |p| [ p.name, p.id ] }, include_blank: true %>
  787. # <%= f.submit %>
  788. # <% end %>
  789. #
  790. # Please refer to the documentation of the base helper for details.
  791. 9 def select(method, choices = nil, options = {}, html_options = {}, &block)
  792. @template.select(@object_name, method, choices, objectify_options(options), @default_html_options.merge(html_options), &block)
  793. end
  794. # Wraps ActionView::Helpers::FormOptionsHelper#collection_select for form builders:
  795. #
  796. # <%= form_for @post do |f| %>
  797. # <%= f.collection_select :person_id, Author.all, :id, :name_with_initial, prompt: true %>
  798. # <%= f.submit %>
  799. # <% end %>
  800. #
  801. # Please refer to the documentation of the base helper for details.
  802. 9 def collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
  803. @template.collection_select(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_html_options.merge(html_options))
  804. end
  805. # Wraps ActionView::Helpers::FormOptionsHelper#grouped_collection_select for form builders:
  806. #
  807. # <%= form_for @city do |f| %>
  808. # <%= f.grouped_collection_select :country_id, @continents, :countries, :name, :id, :name %>
  809. # <%= f.submit %>
  810. # <% end %>
  811. #
  812. # Please refer to the documentation of the base helper for details.
  813. 9 def grouped_collection_select(method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {})
  814. @template.grouped_collection_select(@object_name, method, collection, group_method, group_label_method, option_key_method, option_value_method, objectify_options(options), @default_html_options.merge(html_options))
  815. end
  816. # Wraps ActionView::Helpers::FormOptionsHelper#time_zone_select for form builders:
  817. #
  818. # <%= form_for @user do |f| %>
  819. # <%= f.time_zone_select :time_zone, nil, include_blank: true %>
  820. # <%= f.submit %>
  821. # <% end %>
  822. #
  823. # Please refer to the documentation of the base helper for details.
  824. 9 def time_zone_select(method, priority_zones = nil, options = {}, html_options = {})
  825. @template.time_zone_select(@object_name, method, priority_zones, objectify_options(options), @default_html_options.merge(html_options))
  826. end
  827. # Wraps ActionView::Helpers::FormOptionsHelper#collection_check_boxes for form builders:
  828. #
  829. # <%= form_for @post do |f| %>
  830. # <%= f.collection_check_boxes :author_ids, Author.all, :id, :name_with_initial %>
  831. # <%= f.submit %>
  832. # <% end %>
  833. #
  834. # Please refer to the documentation of the base helper for details.
  835. 9 def collection_check_boxes(method, collection, value_method, text_method, options = {}, html_options = {}, &block)
  836. @template.collection_check_boxes(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_html_options.merge(html_options), &block)
  837. end
  838. # Wraps ActionView::Helpers::FormOptionsHelper#collection_radio_buttons for form builders:
  839. #
  840. # <%= form_for @post do |f| %>
  841. # <%= f.collection_radio_buttons :author_id, Author.all, :id, :name_with_initial %>
  842. # <%= f.submit %>
  843. # <% end %>
  844. #
  845. # Please refer to the documentation of the base helper for details.
  846. 9 def collection_radio_buttons(method, collection, value_method, text_method, options = {}, html_options = {}, &block)
  847. @template.collection_radio_buttons(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_html_options.merge(html_options), &block)
  848. end
  849. end
  850. end
  851. end

lib/action_view/helpers/form_tag_helper.rb

30.91% lines covered

165 relevant lines. 51 lines covered and 114 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 require "cgi"
  3. 9 require "action_view/helpers/tag_helper"
  4. 9 require "active_support/core_ext/string/output_safety"
  5. 9 require "active_support/core_ext/module/attribute_accessors"
  6. 9 require "active_support/core_ext/symbol/starts_ends_with"
  7. 9 module ActionView
  8. # = Action View Form Tag Helpers
  9. 9 module Helpers #:nodoc:
  10. # Provides a number of methods for creating form tags that don't rely on an Active Record object assigned to the template like
  11. # FormHelper does. Instead, you provide the names and values manually.
  12. #
  13. # NOTE: The HTML options <tt>disabled</tt>, <tt>readonly</tt>, and <tt>multiple</tt> can all be treated as booleans. So specifying
  14. # <tt>disabled: true</tt> will give <tt>disabled="disabled"</tt>.
  15. 9 module FormTagHelper
  16. 9 extend ActiveSupport::Concern
  17. 9 include UrlHelper
  18. 9 include TextHelper
  19. 9 mattr_accessor :embed_authenticity_token_in_remote_forms
  20. 9 self.embed_authenticity_token_in_remote_forms = nil
  21. 9 mattr_accessor :default_enforce_utf8, default: true
  22. # Starts a form tag that points the action to a URL configured with <tt>url_for_options</tt> just like
  23. # ActionController::Base#url_for. The method for the form defaults to POST.
  24. #
  25. # ==== Options
  26. # * <tt>:multipart</tt> - If set to true, the enctype is set to "multipart/form-data".
  27. # * <tt>:method</tt> - The method to use when submitting the form, usually either "get" or "post".
  28. # If "patch", "put", "delete", or another verb is used, a hidden input with name <tt>_method</tt>
  29. # is added to simulate the verb over post.
  30. # * <tt>:authenticity_token</tt> - Authenticity token to use in the form. Use only if you need to
  31. # pass custom authenticity token string, or to not add authenticity_token field at all
  32. # (by passing <tt>false</tt>). Remote forms may omit the embedded authenticity token
  33. # by setting <tt>config.action_view.embed_authenticity_token_in_remote_forms = false</tt>.
  34. # This is helpful when you're fragment-caching the form. Remote forms get the
  35. # authenticity token from the <tt>meta</tt> tag, so embedding is unnecessary unless you
  36. # support browsers without JavaScript.
  37. # * <tt>:remote</tt> - If set to true, will allow the Unobtrusive JavaScript drivers to control the
  38. # submit behavior. By default this behavior is an ajax submit.
  39. # * <tt>:enforce_utf8</tt> - If set to false, a hidden input with name utf8 is not output.
  40. # * Any other key creates standard HTML attributes for the tag.
  41. #
  42. # ==== Examples
  43. # form_tag('/posts')
  44. # # => <form action="/posts" method="post">
  45. #
  46. # form_tag('/posts/1', method: :put)
  47. # # => <form action="/posts/1" method="post"> ... <input name="_method" type="hidden" value="put" /> ...
  48. #
  49. # form_tag('/upload', multipart: true)
  50. # # => <form action="/upload" method="post" enctype="multipart/form-data">
  51. #
  52. # <%= form_tag('/posts') do -%>
  53. # <div><%= submit_tag 'Save' %></div>
  54. # <% end -%>
  55. # # => <form action="/posts" method="post"><div><input type="submit" name="commit" value="Save" /></div></form>
  56. #
  57. # <%= form_tag('/posts', remote: true) %>
  58. # # => <form action="/posts" method="post" data-remote="true">
  59. #
  60. # form_tag('http://far.away.com/form', authenticity_token: false)
  61. # # form without authenticity token
  62. #
  63. # form_tag('http://far.away.com/form', authenticity_token: "cf50faa3fe97702ca1ae")
  64. # # form with custom authenticity token
  65. #
  66. 9 def form_tag(url_for_options = {}, options = {}, &block)
  67. html_options = html_options_for_form(url_for_options, options)
  68. if block_given?
  69. form_tag_with_body(html_options, capture(&block))
  70. else
  71. form_tag_html(html_options)
  72. end
  73. end
  74. # Creates a dropdown selection box, or if the <tt>:multiple</tt> option is set to true, a multiple
  75. # choice selection box.
  76. #
  77. # Helpers::FormOptions can be used to create common select boxes such as countries, time zones, or
  78. # associated records. <tt>option_tags</tt> is a string containing the option tags for the select box.
  79. #
  80. # ==== Options
  81. # * <tt>:multiple</tt> - If set to true, the selection will allow multiple choices.
  82. # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
  83. # * <tt>:include_blank</tt> - If set to true, an empty option will be created. If set to a string, the string will be used as the option's content and the value will be empty.
  84. # * <tt>:prompt</tt> - Create a prompt option with blank value and the text asking user to select something.
  85. # * Any other key creates standard HTML attributes for the tag.
  86. #
  87. # ==== Examples
  88. # select_tag "people", options_from_collection_for_select(@people, "id", "name")
  89. # # <select id="people" name="people"><option value="1">David</option></select>
  90. #
  91. # select_tag "people", options_from_collection_for_select(@people, "id", "name", "1")
  92. # # <select id="people" name="people"><option value="1" selected="selected">David</option></select>
  93. #
  94. # select_tag "people", raw("<option>David</option>")
  95. # # => <select id="people" name="people"><option>David</option></select>
  96. #
  97. # select_tag "count", raw("<option>1</option><option>2</option><option>3</option><option>4</option>")
  98. # # => <select id="count" name="count"><option>1</option><option>2</option>
  99. # # <option>3</option><option>4</option></select>
  100. #
  101. # select_tag "colors", raw("<option>Red</option><option>Green</option><option>Blue</option>"), multiple: true
  102. # # => <select id="colors" multiple="multiple" name="colors[]"><option>Red</option>
  103. # # <option>Green</option><option>Blue</option></select>
  104. #
  105. # select_tag "locations", raw("<option>Home</option><option selected='selected'>Work</option><option>Out</option>")
  106. # # => <select id="locations" name="locations"><option>Home</option><option selected='selected'>Work</option>
  107. # # <option>Out</option></select>
  108. #
  109. # select_tag "access", raw("<option>Read</option><option>Write</option>"), multiple: true, class: 'form_input', id: 'unique_id'
  110. # # => <select class="form_input" id="unique_id" multiple="multiple" name="access[]"><option>Read</option>
  111. # # <option>Write</option></select>
  112. #
  113. # select_tag "people", options_from_collection_for_select(@people, "id", "name"), include_blank: true
  114. # # => <select id="people" name="people"><option value="" label=" "></option><option value="1">David</option></select>
  115. #
  116. # select_tag "people", options_from_collection_for_select(@people, "id", "name"), include_blank: "All"
  117. # # => <select id="people" name="people"><option value="">All</option><option value="1">David</option></select>
  118. #
  119. # select_tag "people", options_from_collection_for_select(@people, "id", "name"), prompt: "Select something"
  120. # # => <select id="people" name="people"><option value="">Select something</option><option value="1">David</option></select>
  121. #
  122. # select_tag "destination", raw("<option>NYC</option><option>Paris</option><option>Rome</option>"), disabled: true
  123. # # => <select disabled="disabled" id="destination" name="destination"><option>NYC</option>
  124. # # <option>Paris</option><option>Rome</option></select>
  125. #
  126. # select_tag "credit_card", options_for_select([ "VISA", "MasterCard" ], "MasterCard")
  127. # # => <select id="credit_card" name="credit_card"><option>VISA</option>
  128. # # <option selected="selected">MasterCard</option></select>
  129. 9 def select_tag(name, option_tags = nil, options = {})
  130. option_tags ||= ""
  131. html_name = (options[:multiple] == true && !name.end_with?("[]")) ? "#{name}[]" : name
  132. if options.include?(:include_blank)
  133. include_blank = options[:include_blank]
  134. options = options.except(:include_blank)
  135. options_for_blank_options_tag = { value: "" }
  136. if include_blank == true
  137. include_blank = ""
  138. options_for_blank_options_tag[:label] = " "
  139. end
  140. if include_blank
  141. option_tags = content_tag("option", include_blank, options_for_blank_options_tag).safe_concat(option_tags)
  142. end
  143. end
  144. if prompt = options.delete(:prompt)
  145. option_tags = content_tag("option", prompt, value: "").safe_concat(option_tags)
  146. end
  147. content_tag "select", option_tags, { "name" => html_name, "id" => sanitize_to_id(name) }.update(options.stringify_keys)
  148. end
  149. # Creates a standard text field; use these text fields to input smaller chunks of text like a username
  150. # or a search query.
  151. #
  152. # ==== Options
  153. # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
  154. # * <tt>:size</tt> - The number of visible characters that will fit in the input.
  155. # * <tt>:maxlength</tt> - The maximum number of characters that the browser will allow the user to enter.
  156. # * <tt>:placeholder</tt> - The text contained in the field by default which is removed when the field receives focus.
  157. # If set to true, use a translation is found in the current I18n locale
  158. # (through helpers.placeholders.<modelname>.<attribute>).
  159. # * Any other key creates standard HTML attributes for the tag.
  160. #
  161. # ==== Examples
  162. # text_field_tag 'name'
  163. # # => <input id="name" name="name" type="text" />
  164. #
  165. # text_field_tag 'query', 'Enter your search query here'
  166. # # => <input id="query" name="query" type="text" value="Enter your search query here" />
  167. #
  168. # text_field_tag 'search', nil, placeholder: 'Enter search term...'
  169. # # => <input id="search" name="search" placeholder="Enter search term..." type="text" />
  170. #
  171. # text_field_tag 'request', nil, class: 'special_input'
  172. # # => <input class="special_input" id="request" name="request" type="text" />
  173. #
  174. # text_field_tag 'address', '', size: 75
  175. # # => <input id="address" name="address" size="75" type="text" value="" />
  176. #
  177. # text_field_tag 'zip', nil, maxlength: 5
  178. # # => <input id="zip" maxlength="5" name="zip" type="text" />
  179. #
  180. # text_field_tag 'payment_amount', '$0.00', disabled: true
  181. # # => <input disabled="disabled" id="payment_amount" name="payment_amount" type="text" value="$0.00" />
  182. #
  183. # text_field_tag 'ip', '0.0.0.0', maxlength: 15, size: 20, class: "ip-input"
  184. # # => <input class="ip-input" id="ip" maxlength="15" name="ip" size="20" type="text" value="0.0.0.0" />
  185. 9 def text_field_tag(name, value = nil, options = {})
  186. tag :input, { "type" => "text", "name" => name, "id" => sanitize_to_id(name), "value" => value }.update(options.stringify_keys)
  187. end
  188. # Creates a label element. Accepts a block.
  189. #
  190. # ==== Options
  191. # * Creates standard HTML attributes for the tag.
  192. #
  193. # ==== Examples
  194. # label_tag 'name'
  195. # # => <label for="name">Name</label>
  196. #
  197. # label_tag 'name', 'Your name'
  198. # # => <label for="name">Your name</label>
  199. #
  200. # label_tag 'name', nil, class: 'small_label'
  201. # # => <label for="name" class="small_label">Name</label>
  202. 9 def label_tag(name = nil, content_or_options = nil, options = nil, &block)
  203. if block_given? && content_or_options.is_a?(Hash)
  204. options = content_or_options = content_or_options.stringify_keys
  205. else
  206. options ||= {}
  207. options = options.stringify_keys
  208. end
  209. options["for"] = sanitize_to_id(name) unless name.blank? || options.has_key?("for")
  210. content_tag :label, content_or_options || name.to_s.humanize, options, &block
  211. end
  212. # Creates a hidden form input field used to transmit data that would be lost due to HTTP's statelessness or
  213. # data that should be hidden from the user.
  214. #
  215. # ==== Options
  216. # * Creates standard HTML attributes for the tag.
  217. #
  218. # ==== Examples
  219. # hidden_field_tag 'tags_list'
  220. # # => <input id="tags_list" name="tags_list" type="hidden" />
  221. #
  222. # hidden_field_tag 'token', 'VUBJKB23UIVI1UU1VOBVI@'
  223. # # => <input id="token" name="token" type="hidden" value="VUBJKB23UIVI1UU1VOBVI@" />
  224. #
  225. # hidden_field_tag 'collected_input', '', onchange: "alert('Input collected!')"
  226. # # => <input id="collected_input" name="collected_input" onchange="alert('Input collected!')"
  227. # # type="hidden" value="" />
  228. 9 def hidden_field_tag(name, value = nil, options = {})
  229. text_field_tag(name, value, options.merge(type: :hidden))
  230. end
  231. # Creates a file upload field. If you are using file uploads then you will also need
  232. # to set the multipart option for the form tag:
  233. #
  234. # <%= form_tag '/upload', multipart: true do %>
  235. # <label for="file">File to Upload</label> <%= file_field_tag "file" %>
  236. # <%= submit_tag %>
  237. # <% end %>
  238. #
  239. # The specified URL will then be passed a File object containing the selected file, or if the field
  240. # was left blank, a StringIO object.
  241. #
  242. # ==== Options
  243. # * Creates standard HTML attributes for the tag.
  244. # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
  245. # * <tt>:multiple</tt> - If set to true, *in most updated browsers* the user will be allowed to select multiple files.
  246. # * <tt>:accept</tt> - If set to one or multiple mime-types, the user will be suggested a filter when choosing a file. You still need to set up model validations.
  247. #
  248. # ==== Examples
  249. # file_field_tag 'attachment'
  250. # # => <input id="attachment" name="attachment" type="file" />
  251. #
  252. # file_field_tag 'avatar', class: 'profile_input'
  253. # # => <input class="profile_input" id="avatar" name="avatar" type="file" />
  254. #
  255. # file_field_tag 'picture', disabled: true
  256. # # => <input disabled="disabled" id="picture" name="picture" type="file" />
  257. #
  258. # file_field_tag 'resume', value: '~/resume.doc'
  259. # # => <input id="resume" name="resume" type="file" value="~/resume.doc" />
  260. #
  261. # file_field_tag 'user_pic', accept: 'image/png,image/gif,image/jpeg'
  262. # # => <input accept="image/png,image/gif,image/jpeg" id="user_pic" name="user_pic" type="file" />
  263. #
  264. # file_field_tag 'file', accept: 'text/html', class: 'upload', value: 'index.html'
  265. # # => <input accept="text/html" class="upload" id="file" name="file" type="file" value="index.html" />
  266. 9 def file_field_tag(name, options = {})
  267. text_field_tag(name, nil, convert_direct_upload_option_to_url(options.merge(type: :file)))
  268. end
  269. # Creates a password field, a masked text field that will hide the users input behind a mask character.
  270. #
  271. # ==== Options
  272. # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
  273. # * <tt>:size</tt> - The number of visible characters that will fit in the input.
  274. # * <tt>:maxlength</tt> - The maximum number of characters that the browser will allow the user to enter.
  275. # * Any other key creates standard HTML attributes for the tag.
  276. #
  277. # ==== Examples
  278. # password_field_tag 'pass'
  279. # # => <input id="pass" name="pass" type="password" />
  280. #
  281. # password_field_tag 'secret', 'Your secret here'
  282. # # => <input id="secret" name="secret" type="password" value="Your secret here" />
  283. #
  284. # password_field_tag 'masked', nil, class: 'masked_input_field'
  285. # # => <input class="masked_input_field" id="masked" name="masked" type="password" />
  286. #
  287. # password_field_tag 'token', '', size: 15
  288. # # => <input id="token" name="token" size="15" type="password" value="" />
  289. #
  290. # password_field_tag 'key', nil, maxlength: 16
  291. # # => <input id="key" maxlength="16" name="key" type="password" />
  292. #
  293. # password_field_tag 'confirm_pass', nil, disabled: true
  294. # # => <input disabled="disabled" id="confirm_pass" name="confirm_pass" type="password" />
  295. #
  296. # password_field_tag 'pin', '1234', maxlength: 4, size: 6, class: "pin_input"
  297. # # => <input class="pin_input" id="pin" maxlength="4" name="pin" size="6" type="password" value="1234" />
  298. 9 def password_field_tag(name = "password", value = nil, options = {})
  299. text_field_tag(name, value, options.merge(type: :password))
  300. end
  301. # Creates a text input area; use a textarea for longer text inputs such as blog posts or descriptions.
  302. #
  303. # ==== Options
  304. # * <tt>:size</tt> - A string specifying the dimensions (columns by rows) of the textarea (e.g., "25x10").
  305. # * <tt>:rows</tt> - Specify the number of rows in the textarea
  306. # * <tt>:cols</tt> - Specify the number of columns in the textarea
  307. # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
  308. # * <tt>:escape</tt> - By default, the contents of the text input are HTML escaped.
  309. # If you need unescaped contents, set this to false.
  310. # * Any other key creates standard HTML attributes for the tag.
  311. #
  312. # ==== Examples
  313. # text_area_tag 'post'
  314. # # => <textarea id="post" name="post"></textarea>
  315. #
  316. # text_area_tag 'bio', @user.bio
  317. # # => <textarea id="bio" name="bio">This is my biography.</textarea>
  318. #
  319. # text_area_tag 'body', nil, rows: 10, cols: 25
  320. # # => <textarea cols="25" id="body" name="body" rows="10"></textarea>
  321. #
  322. # text_area_tag 'body', nil, size: "25x10"
  323. # # => <textarea name="body" id="body" cols="25" rows="10"></textarea>
  324. #
  325. # text_area_tag 'description', "Description goes here.", disabled: true
  326. # # => <textarea disabled="disabled" id="description" name="description">Description goes here.</textarea>
  327. #
  328. # text_area_tag 'comment', nil, class: 'comment_input'
  329. # # => <textarea class="comment_input" id="comment" name="comment"></textarea>
  330. 9 def text_area_tag(name, content = nil, options = {})
  331. options = options.stringify_keys
  332. if size = options.delete("size")
  333. options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split)
  334. end
  335. escape = options.delete("escape") { true }
  336. content = ERB::Util.html_escape(content) if escape
  337. content_tag :textarea, content.to_s.html_safe, { "name" => name, "id" => sanitize_to_id(name) }.update(options)
  338. end
  339. # Creates a check box form input tag.
  340. #
  341. # ==== Options
  342. # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
  343. # * Any other key creates standard HTML options for the tag.
  344. #
  345. # ==== Examples
  346. # check_box_tag 'accept'
  347. # # => <input id="accept" name="accept" type="checkbox" value="1" />
  348. #
  349. # check_box_tag 'rock', 'rock music'
  350. # # => <input id="rock" name="rock" type="checkbox" value="rock music" />
  351. #
  352. # check_box_tag 'receive_email', 'yes', true
  353. # # => <input checked="checked" id="receive_email" name="receive_email" type="checkbox" value="yes" />
  354. #
  355. # check_box_tag 'tos', 'yes', false, class: 'accept_tos'
  356. # # => <input class="accept_tos" id="tos" name="tos" type="checkbox" value="yes" />
  357. #
  358. # check_box_tag 'eula', 'accepted', false, disabled: true
  359. # # => <input disabled="disabled" id="eula" name="eula" type="checkbox" value="accepted" />
  360. 9 def check_box_tag(name, value = "1", checked = false, options = {})
  361. html_options = { "type" => "checkbox", "name" => name, "id" => sanitize_to_id(name), "value" => value }.update(options.stringify_keys)
  362. html_options["checked"] = "checked" if checked
  363. tag :input, html_options
  364. end
  365. # Creates a radio button; use groups of radio buttons named the same to allow users to
  366. # select from a group of options.
  367. #
  368. # ==== Options
  369. # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
  370. # * Any other key creates standard HTML options for the tag.
  371. #
  372. # ==== Examples
  373. # radio_button_tag 'favorite_color', 'maroon'
  374. # # => <input id="favorite_color_maroon" name="favorite_color" type="radio" value="maroon" />
  375. #
  376. # radio_button_tag 'receive_updates', 'no', true
  377. # # => <input checked="checked" id="receive_updates_no" name="receive_updates" type="radio" value="no" />
  378. #
  379. # radio_button_tag 'time_slot', "3:00 p.m.", false, disabled: true
  380. # # => <input disabled="disabled" id="time_slot_3:00_p.m." name="time_slot" type="radio" value="3:00 p.m." />
  381. #
  382. # radio_button_tag 'color', "green", true, class: "color_input"
  383. # # => <input checked="checked" class="color_input" id="color_green" name="color" type="radio" value="green" />
  384. 9 def radio_button_tag(name, value, checked = false, options = {})
  385. html_options = { "type" => "radio", "name" => name, "id" => "#{sanitize_to_id(name)}_#{sanitize_to_id(value)}", "value" => value }.update(options.stringify_keys)
  386. html_options["checked"] = "checked" if checked
  387. tag :input, html_options
  388. end
  389. # Creates a submit button with the text <tt>value</tt> as the caption.
  390. #
  391. # ==== Options
  392. # * <tt>:data</tt> - This option can be used to add custom data attributes.
  393. # * <tt>:disabled</tt> - If true, the user will not be able to use this input.
  394. # * Any other key creates standard HTML options for the tag.
  395. #
  396. # ==== Data attributes
  397. #
  398. # * <tt>confirm: 'question?'</tt> - If present the unobtrusive JavaScript
  399. # drivers will provide a prompt with the question specified. If the user accepts,
  400. # the form is processed normally, otherwise no action is taken.
  401. # * <tt>:disable_with</tt> - Value of this parameter will be used as the value for a
  402. # disabled version of the submit button when the form is submitted. This feature is
  403. # provided by the unobtrusive JavaScript driver. To disable this feature for a single submit tag
  404. # pass <tt>:data => { disable_with: false }</tt> Defaults to value attribute.
  405. #
  406. # ==== Examples
  407. # submit_tag
  408. # # => <input name="commit" data-disable-with="Save changes" type="submit" value="Save changes" />
  409. #
  410. # submit_tag "Edit this article"
  411. # # => <input name="commit" data-disable-with="Edit this article" type="submit" value="Edit this article" />
  412. #
  413. # submit_tag "Save edits", disabled: true
  414. # # => <input disabled="disabled" name="commit" data-disable-with="Save edits" type="submit" value="Save edits" />
  415. #
  416. # submit_tag "Complete sale", data: { disable_with: "Submitting..." }
  417. # # => <input name="commit" data-disable-with="Submitting..." type="submit" value="Complete sale" />
  418. #
  419. # submit_tag nil, class: "form_submit"
  420. # # => <input class="form_submit" name="commit" type="submit" />
  421. #
  422. # submit_tag "Edit", class: "edit_button"
  423. # # => <input class="edit_button" data-disable-with="Edit" name="commit" type="submit" value="Edit" />
  424. #
  425. # submit_tag "Save", data: { confirm: "Are you sure?" }
  426. # # => <input name='commit' type='submit' value='Save' data-disable-with="Save" data-confirm="Are you sure?" />
  427. #
  428. 9 def submit_tag(value = "Save changes", options = {})
  429. options = options.deep_stringify_keys
  430. tag_options = { "type" => "submit", "name" => "commit", "value" => value }.update(options)
  431. set_default_disable_with value, tag_options
  432. tag :input, tag_options
  433. end
  434. # Creates a button element that defines a <tt>submit</tt> button,
  435. # <tt>reset</tt> button or a generic button which can be used in
  436. # JavaScript, for example. You can use the button tag as a regular
  437. # submit tag but it isn't supported in legacy browsers. However,
  438. # the button tag does allow for richer labels such as images and emphasis,
  439. # so this helper will also accept a block. By default, it will create
  440. # a button tag with type <tt>submit</tt>, if type is not given.
  441. #
  442. # ==== Options
  443. # * <tt>:data</tt> - This option can be used to add custom data attributes.
  444. # * <tt>:disabled</tt> - If true, the user will not be able to
  445. # use this input.
  446. # * Any other key creates standard HTML options for the tag.
  447. #
  448. # ==== Data attributes
  449. #
  450. # * <tt>confirm: 'question?'</tt> - If present, the
  451. # unobtrusive JavaScript drivers will provide a prompt with
  452. # the question specified. If the user accepts, the form is
  453. # processed normally, otherwise no action is taken.
  454. # * <tt>:disable_with</tt> - Value of this parameter will be
  455. # used as the value for a disabled version of the submit
  456. # button when the form is submitted. This feature is provided
  457. # by the unobtrusive JavaScript driver.
  458. #
  459. # ==== Examples
  460. # button_tag
  461. # # => <button name="button" type="submit">Button</button>
  462. #
  463. # button_tag 'Reset', type: 'reset'
  464. # # => <button name="button" type="reset">Reset</button>
  465. #
  466. # button_tag 'Button', type: 'button'
  467. # # => <button name="button" type="button">Button</button>
  468. #
  469. # button_tag 'Reset', type: 'reset', disabled: true
  470. # # => <button name="button" type="reset" disabled="disabled">Reset</button>
  471. #
  472. # button_tag(type: 'button') do
  473. # content_tag(:strong, 'Ask me!')
  474. # end
  475. # # => <button name="button" type="button">
  476. # # <strong>Ask me!</strong>
  477. # # </button>
  478. #
  479. # button_tag "Save", data: { confirm: "Are you sure?" }
  480. # # => <button name="button" type="submit" data-confirm="Are you sure?">Save</button>
  481. #
  482. # button_tag "Checkout", data: { disable_with: "Please wait..." }
  483. # # => <button data-disable-with="Please wait..." name="button" type="submit">Checkout</button>
  484. #
  485. 9 def button_tag(content_or_options = nil, options = nil, &block)
  486. if content_or_options.is_a? Hash
  487. options = content_or_options
  488. else
  489. options ||= {}
  490. end
  491. options = { "name" => "button", "type" => "submit" }.merge!(options.stringify_keys)
  492. if block_given?
  493. content_tag :button, options, &block
  494. else
  495. content_tag :button, content_or_options || "Button", options
  496. end
  497. end
  498. # Displays an image which when clicked will submit the form.
  499. #
  500. # <tt>source</tt> is passed to AssetTagHelper#path_to_image
  501. #
  502. # ==== Options
  503. # * <tt>:data</tt> - This option can be used to add custom data attributes.
  504. # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
  505. # * Any other key creates standard HTML options for the tag.
  506. #
  507. # ==== Data attributes
  508. #
  509. # * <tt>confirm: 'question?'</tt> - This will add a JavaScript confirm
  510. # prompt with the question specified. If the user accepts, the form is
  511. # processed normally, otherwise no action is taken.
  512. #
  513. # ==== Examples
  514. # image_submit_tag("login.png")
  515. # # => <input src="/assets/login.png" type="image" />
  516. #
  517. # image_submit_tag("purchase.png", disabled: true)
  518. # # => <input disabled="disabled" src="/assets/purchase.png" type="image" />
  519. #
  520. # image_submit_tag("search.png", class: 'search_button', alt: 'Find')
  521. # # => <input class="search_button" src="/assets/search.png" type="image" />
  522. #
  523. # image_submit_tag("agree.png", disabled: true, class: "agree_disagree_button")
  524. # # => <input class="agree_disagree_button" disabled="disabled" src="/assets/agree.png" type="image" />
  525. #
  526. # image_submit_tag("save.png", data: { confirm: "Are you sure?" })
  527. # # => <input src="/assets/save.png" data-confirm="Are you sure?" type="image" />
  528. 9 def image_submit_tag(source, options = {})
  529. options = options.stringify_keys
  530. src = path_to_image(source, skip_pipeline: options.delete("skip_pipeline"))
  531. tag :input, { "type" => "image", "src" => src }.update(options)
  532. end
  533. # Creates a field set for grouping HTML form elements.
  534. #
  535. # <tt>legend</tt> will become the fieldset's title (optional as per W3C).
  536. # <tt>options</tt> accept the same values as tag.
  537. #
  538. # ==== Examples
  539. # <%= field_set_tag do %>
  540. # <p><%= text_field_tag 'name' %></p>
  541. # <% end %>
  542. # # => <fieldset><p><input id="name" name="name" type="text" /></p></fieldset>
  543. #
  544. # <%= field_set_tag 'Your details' do %>
  545. # <p><%= text_field_tag 'name' %></p>
  546. # <% end %>
  547. # # => <fieldset><legend>Your details</legend><p><input id="name" name="name" type="text" /></p></fieldset>
  548. #
  549. # <%= field_set_tag nil, class: 'format' do %>
  550. # <p><%= text_field_tag 'name' %></p>
  551. # <% end %>
  552. # # => <fieldset class="format"><p><input id="name" name="name" type="text" /></p></fieldset>
  553. 9 def field_set_tag(legend = nil, options = nil, &block)
  554. output = tag(:fieldset, options, true)
  555. output.safe_concat(content_tag("legend", legend)) unless legend.blank?
  556. output.concat(capture(&block)) if block_given?
  557. output.safe_concat("</fieldset>")
  558. end
  559. # Creates a text field of type "color".
  560. #
  561. # ==== Options
  562. # * Accepts the same options as text_field_tag.
  563. #
  564. # ==== Examples
  565. # color_field_tag 'name'
  566. # # => <input id="name" name="name" type="color" />
  567. #
  568. # color_field_tag 'color', '#DEF726'
  569. # # => <input id="color" name="color" type="color" value="#DEF726" />
  570. #
  571. # color_field_tag 'color', nil, class: 'special_input'
  572. # # => <input class="special_input" id="color" name="color" type="color" />
  573. #
  574. # color_field_tag 'color', '#DEF726', class: 'special_input', disabled: true
  575. # # => <input disabled="disabled" class="special_input" id="color" name="color" type="color" value="#DEF726" />
  576. 9 def color_field_tag(name, value = nil, options = {})
  577. text_field_tag(name, value, options.merge(type: :color))
  578. end
  579. # Creates a text field of type "search".
  580. #
  581. # ==== Options
  582. # * Accepts the same options as text_field_tag.
  583. #
  584. # ==== Examples
  585. # search_field_tag 'name'
  586. # # => <input id="name" name="name" type="search" />
  587. #
  588. # search_field_tag 'search', 'Enter your search query here'
  589. # # => <input id="search" name="search" type="search" value="Enter your search query here" />
  590. #
  591. # search_field_tag 'search', nil, class: 'special_input'
  592. # # => <input class="special_input" id="search" name="search" type="search" />
  593. #
  594. # search_field_tag 'search', 'Enter your search query here', class: 'special_input', disabled: true
  595. # # => <input disabled="disabled" class="special_input" id="search" name="search" type="search" value="Enter your search query here" />
  596. 9 def search_field_tag(name, value = nil, options = {})
  597. text_field_tag(name, value, options.merge(type: :search))
  598. end
  599. # Creates a text field of type "tel".
  600. #
  601. # ==== Options
  602. # * Accepts the same options as text_field_tag.
  603. #
  604. # ==== Examples
  605. # telephone_field_tag 'name'
  606. # # => <input id="name" name="name" type="tel" />
  607. #
  608. # telephone_field_tag 'tel', '0123456789'
  609. # # => <input id="tel" name="tel" type="tel" value="0123456789" />
  610. #
  611. # telephone_field_tag 'tel', nil, class: 'special_input'
  612. # # => <input class="special_input" id="tel" name="tel" type="tel" />
  613. #
  614. # telephone_field_tag 'tel', '0123456789', class: 'special_input', disabled: true
  615. # # => <input disabled="disabled" class="special_input" id="tel" name="tel" type="tel" value="0123456789" />
  616. 9 def telephone_field_tag(name, value = nil, options = {})
  617. text_field_tag(name, value, options.merge(type: :tel))
  618. end
  619. 9 alias phone_field_tag telephone_field_tag
  620. # Creates a text field of type "date".
  621. #
  622. # ==== Options
  623. # * Accepts the same options as text_field_tag.
  624. #
  625. # ==== Examples
  626. # date_field_tag 'name'
  627. # # => <input id="name" name="name" type="date" />
  628. #
  629. # date_field_tag 'date', '01/01/2014'
  630. # # => <input id="date" name="date" type="date" value="01/01/2014" />
  631. #
  632. # date_field_tag 'date', nil, class: 'special_input'
  633. # # => <input class="special_input" id="date" name="date" type="date" />
  634. #
  635. # date_field_tag 'date', '01/01/2014', class: 'special_input', disabled: true
  636. # # => <input disabled="disabled" class="special_input" id="date" name="date" type="date" value="01/01/2014" />
  637. 9 def date_field_tag(name, value = nil, options = {})
  638. text_field_tag(name, value, options.merge(type: :date))
  639. end
  640. # Creates a text field of type "time".
  641. #
  642. # === Options
  643. # * <tt>:min</tt> - The minimum acceptable value.
  644. # * <tt>:max</tt> - The maximum acceptable value.
  645. # * <tt>:step</tt> - The acceptable value granularity.
  646. # * Otherwise accepts the same options as text_field_tag.
  647. 9 def time_field_tag(name, value = nil, options = {})
  648. text_field_tag(name, value, options.merge(type: :time))
  649. end
  650. # Creates a text field of type "datetime-local".
  651. #
  652. # === Options
  653. # * <tt>:min</tt> - The minimum acceptable value.
  654. # * <tt>:max</tt> - The maximum acceptable value.
  655. # * <tt>:step</tt> - The acceptable value granularity.
  656. # * Otherwise accepts the same options as text_field_tag.
  657. 9 def datetime_field_tag(name, value = nil, options = {})
  658. text_field_tag(name, value, options.merge(type: "datetime-local"))
  659. end
  660. 9 alias datetime_local_field_tag datetime_field_tag
  661. # Creates a text field of type "month".
  662. #
  663. # === Options
  664. # * <tt>:min</tt> - The minimum acceptable value.
  665. # * <tt>:max</tt> - The maximum acceptable value.
  666. # * <tt>:step</tt> - The acceptable value granularity.
  667. # * Otherwise accepts the same options as text_field_tag.
  668. 9 def month_field_tag(name, value = nil, options = {})
  669. text_field_tag(name, value, options.merge(type: :month))
  670. end
  671. # Creates a text field of type "week".
  672. #
  673. # === Options
  674. # * <tt>:min</tt> - The minimum acceptable value.
  675. # * <tt>:max</tt> - The maximum acceptable value.
  676. # * <tt>:step</tt> - The acceptable value granularity.
  677. # * Otherwise accepts the same options as text_field_tag.
  678. 9 def week_field_tag(name, value = nil, options = {})
  679. text_field_tag(name, value, options.merge(type: :week))
  680. end
  681. # Creates a text field of type "url".
  682. #
  683. # ==== Options
  684. # * Accepts the same options as text_field_tag.
  685. #
  686. # ==== Examples
  687. # url_field_tag 'name'
  688. # # => <input id="name" name="name" type="url" />
  689. #
  690. # url_field_tag 'url', 'http://rubyonrails.org'
  691. # # => <input id="url" name="url" type="url" value="http://rubyonrails.org" />
  692. #
  693. # url_field_tag 'url', nil, class: 'special_input'
  694. # # => <input class="special_input" id="url" name="url" type="url" />
  695. #
  696. # url_field_tag 'url', 'http://rubyonrails.org', class: 'special_input', disabled: true
  697. # # => <input disabled="disabled" class="special_input" id="url" name="url" type="url" value="http://rubyonrails.org" />
  698. 9 def url_field_tag(name, value = nil, options = {})
  699. text_field_tag(name, value, options.merge(type: :url))
  700. end
  701. # Creates a text field of type "email".
  702. #
  703. # ==== Options
  704. # * Accepts the same options as text_field_tag.
  705. #
  706. # ==== Examples
  707. # email_field_tag 'name'
  708. # # => <input id="name" name="name" type="email" />
  709. #
  710. # email_field_tag 'email', 'email@example.com'
  711. # # => <input id="email" name="email" type="email" value="email@example.com" />
  712. #
  713. # email_field_tag 'email', nil, class: 'special_input'
  714. # # => <input class="special_input" id="email" name="email" type="email" />
  715. #
  716. # email_field_tag 'email', 'email@example.com', class: 'special_input', disabled: true
  717. # # => <input disabled="disabled" class="special_input" id="email" name="email" type="email" value="email@example.com" />
  718. 9 def email_field_tag(name, value = nil, options = {})
  719. text_field_tag(name, value, options.merge(type: :email))
  720. end
  721. # Creates a number field.
  722. #
  723. # ==== Options
  724. # * <tt>:min</tt> - The minimum acceptable value.
  725. # * <tt>:max</tt> - The maximum acceptable value.
  726. # * <tt>:in</tt> - A range specifying the <tt>:min</tt> and
  727. # <tt>:max</tt> values.
  728. # * <tt>:within</tt> - Same as <tt>:in</tt>.
  729. # * <tt>:step</tt> - The acceptable value granularity.
  730. # * Otherwise accepts the same options as text_field_tag.
  731. #
  732. # ==== Examples
  733. # number_field_tag 'quantity'
  734. # # => <input id="quantity" name="quantity" type="number" />
  735. #
  736. # number_field_tag 'quantity', '1'
  737. # # => <input id="quantity" name="quantity" type="number" value="1" />
  738. #
  739. # number_field_tag 'quantity', nil, class: 'special_input'
  740. # # => <input class="special_input" id="quantity" name="quantity" type="number" />
  741. #
  742. # number_field_tag 'quantity', nil, min: 1
  743. # # => <input id="quantity" name="quantity" min="1" type="number" />
  744. #
  745. # number_field_tag 'quantity', nil, max: 9
  746. # # => <input id="quantity" name="quantity" max="9" type="number" />
  747. #
  748. # number_field_tag 'quantity', nil, in: 1...10
  749. # # => <input id="quantity" name="quantity" min="1" max="9" type="number" />
  750. #
  751. # number_field_tag 'quantity', nil, within: 1...10
  752. # # => <input id="quantity" name="quantity" min="1" max="9" type="number" />
  753. #
  754. # number_field_tag 'quantity', nil, min: 1, max: 10
  755. # # => <input id="quantity" name="quantity" min="1" max="10" type="number" />
  756. #
  757. # number_field_tag 'quantity', nil, min: 1, max: 10, step: 2
  758. # # => <input id="quantity" name="quantity" min="1" max="10" step="2" type="number" />
  759. #
  760. # number_field_tag 'quantity', '1', class: 'special_input', disabled: true
  761. # # => <input disabled="disabled" class="special_input" id="quantity" name="quantity" type="number" value="1" />
  762. 9 def number_field_tag(name, value = nil, options = {})
  763. options = options.stringify_keys
  764. options["type"] ||= "number"
  765. if range = options.delete("in") || options.delete("within")
  766. options.update("min" => range.min, "max" => range.max)
  767. end
  768. text_field_tag(name, value, options)
  769. end
  770. # Creates a range form element.
  771. #
  772. # ==== Options
  773. # * Accepts the same options as number_field_tag.
  774. 9 def range_field_tag(name, value = nil, options = {})
  775. number_field_tag(name, value, options.merge(type: :range))
  776. end
  777. # Creates the hidden UTF8 enforcer tag. Override this method in a helper
  778. # to customize the tag.
  779. 9 def utf8_enforcer_tag
  780. # Use raw HTML to ensure the value is written as an HTML entity; it
  781. # needs to be the right character regardless of which encoding the
  782. # browser infers.
  783. '<input name="utf8" type="hidden" value="&#x2713;" />'.html_safe
  784. end
  785. 9 private
  786. 9 def html_options_for_form(url_for_options, options)
  787. options.stringify_keys.tap do |html_options|
  788. html_options["enctype"] = "multipart/form-data" if html_options.delete("multipart")
  789. # The following URL is unescaped, this is just a hash of options, and it is the
  790. # responsibility of the caller to escape all the values.
  791. html_options["action"] = url_for(url_for_options)
  792. html_options["accept-charset"] = "UTF-8"
  793. html_options["data-remote"] = true if html_options.delete("remote")
  794. if html_options["data-remote"] &&
  795. !embed_authenticity_token_in_remote_forms &&
  796. html_options["authenticity_token"].blank?
  797. # The authenticity token is taken from the meta tag in this case
  798. html_options["authenticity_token"] = false
  799. elsif html_options["authenticity_token"] == true
  800. # Include the default authenticity_token, which is only generated when its set to nil,
  801. # but we needed the true value to override the default of no authenticity_token on data-remote.
  802. html_options["authenticity_token"] = nil
  803. end
  804. end
  805. end
  806. 9 def extra_tags_for_form(html_options)
  807. authenticity_token = html_options.delete("authenticity_token")
  808. method = html_options.delete("method").to_s.downcase
  809. method_tag = \
  810. case method
  811. when "get"
  812. html_options["method"] = "get"
  813. ""
  814. when "post", ""
  815. html_options["method"] = "post"
  816. token_tag(authenticity_token, form_options: {
  817. action: html_options["action"],
  818. method: "post"
  819. })
  820. else
  821. html_options["method"] = "post"
  822. method_tag(method) + token_tag(authenticity_token, form_options: {
  823. action: html_options["action"],
  824. method: method
  825. })
  826. end
  827. if html_options.delete("enforce_utf8") { default_enforce_utf8 }
  828. utf8_enforcer_tag + method_tag
  829. else
  830. method_tag
  831. end
  832. end
  833. 9 def form_tag_html(html_options)
  834. extra_tags = extra_tags_for_form(html_options)
  835. tag(:form, html_options, true) + extra_tags
  836. end
  837. 9 def form_tag_with_body(html_options, content)
  838. output = form_tag_html(html_options)
  839. output << content
  840. output.safe_concat("</form>")
  841. end
  842. # see http://www.w3.org/TR/html4/types.html#type-name
  843. 9 def sanitize_to_id(name)
  844. name.to_s.delete("]").tr("^-a-zA-Z0-9:.", "_")
  845. end
  846. 9 def set_default_disable_with(value, tag_options)
  847. return unless ActionView::Base.automatically_disable_submit_tag
  848. data = tag_options["data"]
  849. unless tag_options["data-disable-with"] == false || (data && data["disable_with"] == false)
  850. disable_with_text = tag_options["data-disable-with"]
  851. disable_with_text ||= data["disable_with"] if data
  852. disable_with_text ||= value.to_s.clone
  853. tag_options.deep_merge!("data" => { "disable_with" => disable_with_text })
  854. else
  855. data.delete("disable_with") if data
  856. end
  857. tag_options.delete("data-disable-with")
  858. end
  859. 9 def convert_direct_upload_option_to_url(options)
  860. if options.delete(:direct_upload) && respond_to?(:rails_direct_uploads_url)
  861. options["data-direct-upload-url"] = rails_direct_uploads_url
  862. end
  863. options
  864. end
  865. end
  866. end
  867. end

lib/action_view/helpers/javascript_helper.rb

44.0% lines covered

25 relevant lines. 11 lines covered and 14 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 require "action_view/helpers/tag_helper"
  3. 9 module ActionView
  4. 9 module Helpers #:nodoc:
  5. 9 module JavaScriptHelper
  6. 9 JS_ESCAPE_MAP = {
  7. '\\' => '\\\\',
  8. "</" => '<\/',
  9. "\r\n" => '\n',
  10. "\n" => '\n',
  11. "\r" => '\n',
  12. '"' => '\\"',
  13. "'" => "\\'",
  14. "`" => "\\`",
  15. "$" => "\\$"
  16. }
  17. 9 JS_ESCAPE_MAP[(+"\342\200\250").force_encoding(Encoding::UTF_8).encode!] = "&#x2028;"
  18. 9 JS_ESCAPE_MAP[(+"\342\200\251").force_encoding(Encoding::UTF_8).encode!] = "&#x2029;"
  19. # Escapes carriage returns and single and double quotes for JavaScript segments.
  20. #
  21. # Also available through the alias j(). This is particularly helpful in JavaScript
  22. # responses, like:
  23. #
  24. # $('some_element').replaceWith('<%= j render 'some/element_template' %>');
  25. 9 def escape_javascript(javascript)
  26. javascript = javascript.to_s
  27. if javascript.empty?
  28. result = ""
  29. else
  30. result = javascript.gsub(/(\\|<\/|\r\n|\342\200\250|\342\200\251|[\n\r"']|[`]|[$])/u, JS_ESCAPE_MAP)
  31. end
  32. javascript.html_safe? ? result.html_safe : result
  33. end
  34. 9 alias_method :j, :escape_javascript
  35. # Returns a JavaScript tag with the +content+ inside. Example:
  36. # javascript_tag "alert('All is good')"
  37. #
  38. # Returns:
  39. # <script>
  40. # //<![CDATA[
  41. # alert('All is good')
  42. # //]]>
  43. # </script>
  44. #
  45. # +html_options+ may be a hash of attributes for the <tt>\<script></tt>
  46. # tag.
  47. #
  48. # javascript_tag "alert('All is good')", type: 'application/javascript'
  49. #
  50. # Returns:
  51. # <script type="application/javascript">
  52. # //<![CDATA[
  53. # alert('All is good')
  54. # //]]>
  55. # </script>
  56. #
  57. # Instead of passing the content as an argument, you can also use a block
  58. # in which case, you pass your +html_options+ as the first parameter.
  59. #
  60. # <%= javascript_tag type: 'application/javascript' do -%>
  61. # alert('All is good')
  62. # <% end -%>
  63. #
  64. # If you have a content security policy enabled then you can add an automatic
  65. # nonce value by passing <tt>nonce: true</tt> as part of +html_options+. Example:
  66. #
  67. # <%= javascript_tag nonce: true do -%>
  68. # alert('All is good')
  69. # <% end -%>
  70. 9 def javascript_tag(content_or_options_with_block = nil, html_options = {}, &block)
  71. content =
  72. if block_given?
  73. html_options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash)
  74. capture(&block)
  75. else
  76. content_or_options_with_block
  77. end
  78. if html_options[:nonce] == true
  79. html_options[:nonce] = content_security_policy_nonce
  80. end
  81. content_tag("script", javascript_cdata_section(content), html_options)
  82. end
  83. 9 def javascript_cdata_section(content) #:nodoc:
  84. "\n//#{cdata_section("\n#{content}\n//")}\n".html_safe
  85. end
  86. end
  87. end
  88. end

lib/action_view/helpers/number_helper.rb

41.07% lines covered

56 relevant lines. 23 lines covered and 33 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 require "active_support/core_ext/hash/keys"
  3. 9 require "active_support/core_ext/string/output_safety"
  4. 9 require "active_support/number_helper"
  5. 9 module ActionView
  6. # = Action View Number Helpers
  7. 9 module Helpers #:nodoc:
  8. # Provides methods for converting numbers into formatted strings.
  9. # Methods are provided for phone numbers, currency, percentage,
  10. # precision, positional notation, file size and pretty printing.
  11. #
  12. # Most methods expect a +number+ argument, and will return it
  13. # unchanged if can't be converted into a valid number.
  14. 9 module NumberHelper
  15. # Raised when argument +number+ param given to the helpers is invalid and
  16. # the option :raise is set to +true+.
  17. 9 class InvalidNumberError < StandardError
  18. 9 attr_accessor :number
  19. 9 def initialize(number)
  20. @number = number
  21. end
  22. end
  23. # Formats a +number+ into a phone number (US by default e.g., (555)
  24. # 123-9876). You can customize the format in the +options+ hash.
  25. #
  26. # ==== Options
  27. #
  28. # * <tt>:area_code</tt> - Adds parentheses around the area code.
  29. # * <tt>:delimiter</tt> - Specifies the delimiter to use
  30. # (defaults to "-").
  31. # * <tt>:extension</tt> - Specifies an extension to add to the
  32. # end of the generated number.
  33. # * <tt>:country_code</tt> - Sets the country code for the phone
  34. # number.
  35. # * <tt>:pattern</tt> - Specifies how the number is divided into three
  36. # groups with the custom regexp to override the default format.
  37. # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when
  38. # the argument is invalid.
  39. #
  40. # ==== Examples
  41. #
  42. # number_to_phone(5551234) # => 555-1234
  43. # number_to_phone("5551234") # => 555-1234
  44. # number_to_phone(1235551234) # => 123-555-1234
  45. # number_to_phone(1235551234, area_code: true) # => (123) 555-1234
  46. # number_to_phone(1235551234, delimiter: " ") # => 123 555 1234
  47. # number_to_phone(1235551234, area_code: true, extension: 555) # => (123) 555-1234 x 555
  48. # number_to_phone(1235551234, country_code: 1) # => +1-123-555-1234
  49. # number_to_phone("123a456") # => 123a456
  50. # number_to_phone("1234a567", raise: true) # => InvalidNumberError
  51. #
  52. # number_to_phone(1235551234, country_code: 1, extension: 1343, delimiter: ".")
  53. # # => +1.123.555.1234 x 1343
  54. #
  55. # number_to_phone(75561234567, pattern: /(\d{1,4})(\d{4})(\d{4})$/, area_code: true)
  56. # # => "(755) 6123-4567"
  57. # number_to_phone(13312345678, pattern: /(\d{3})(\d{4})(\d{4})$/)
  58. # # => "133-1234-5678"
  59. 9 def number_to_phone(number, options = {})
  60. return unless number
  61. options = options.symbolize_keys
  62. parse_float(number, true) if options.delete(:raise)
  63. ERB::Util.html_escape(ActiveSupport::NumberHelper.number_to_phone(number, options))
  64. end
  65. # Formats a +number+ into a currency string (e.g., $13.65). You
  66. # can customize the format in the +options+ hash.
  67. #
  68. # The currency unit and number formatting of the current locale will be used
  69. # unless otherwise specified in the provided options. No currency conversion
  70. # is performed. If the user is given a way to change their locale, they will
  71. # also be able to change the relative value of the currency displayed with
  72. # this helper. If your application will ever support multiple locales, you
  73. # may want to specify a constant <tt>:locale</tt> option or consider
  74. # using a library capable of currency conversion.
  75. #
  76. # ==== Options
  77. #
  78. # * <tt>:locale</tt> - Sets the locale to be used for formatting
  79. # (defaults to current locale).
  80. # * <tt>:precision</tt> - Sets the level of precision (defaults
  81. # to 2).
  82. # * <tt>:unit</tt> - Sets the denomination of the currency
  83. # (defaults to "$").
  84. # * <tt>:separator</tt> - Sets the separator between the units
  85. # (defaults to ".").
  86. # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
  87. # to ",").
  88. # * <tt>:format</tt> - Sets the format for non-negative numbers
  89. # (defaults to "%u%n"). Fields are <tt>%u</tt> for the
  90. # currency, and <tt>%n</tt> for the number.
  91. # * <tt>:negative_format</tt> - Sets the format for negative
  92. # numbers (defaults to prepending a hyphen to the formatted
  93. # number given by <tt>:format</tt>). Accepts the same fields
  94. # than <tt>:format</tt>, except <tt>%n</tt> is here the
  95. # absolute value of the number.
  96. # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when
  97. # the argument is invalid.
  98. # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes
  99. # insignificant zeros after the decimal separator (defaults to
  100. # +false+).
  101. #
  102. # ==== Examples
  103. #
  104. # number_to_currency(1234567890.50) # => $1,234,567,890.50
  105. # number_to_currency(1234567890.506) # => $1,234,567,890.51
  106. # number_to_currency(1234567890.506, precision: 3) # => $1,234,567,890.506
  107. # number_to_currency(1234567890.506, locale: :fr) # => 1 234 567 890,51 €
  108. # number_to_currency("123a456") # => $123a456
  109. #
  110. # number_to_currency("123a456", raise: true) # => InvalidNumberError
  111. #
  112. # number_to_currency(-0.456789, precision: 0)
  113. # # => "$0"
  114. # number_to_currency(-1234567890.50, negative_format: "(%u%n)")
  115. # # => ($1,234,567,890.50)
  116. # number_to_currency(1234567890.50, unit: "R$", separator: ",", delimiter: "")
  117. # # => R$1234567890,50
  118. # number_to_currency(1234567890.50, unit: "R$", separator: ",", delimiter: "", format: "%n %u")
  119. # # => 1234567890,50 R$
  120. # number_to_currency(1234567890.50, strip_insignificant_zeros: true)
  121. # # => "$1,234,567,890.5"
  122. 9 def number_to_currency(number, options = {})
  123. delegate_number_helper_method(:number_to_currency, number, options)
  124. end
  125. # Formats a +number+ as a percentage string (e.g., 65%). You can
  126. # customize the format in the +options+ hash.
  127. #
  128. # ==== Options
  129. #
  130. # * <tt>:locale</tt> - Sets the locale to be used for formatting
  131. # (defaults to current locale).
  132. # * <tt>:precision</tt> - Sets the precision of the number
  133. # (defaults to 3).
  134. # * <tt>:significant</tt> - If +true+, precision will be the number
  135. # of significant_digits. If +false+, the number of fractional
  136. # digits (defaults to +false+).
  137. # * <tt>:separator</tt> - Sets the separator between the
  138. # fractional and integer digits (defaults to ".").
  139. # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
  140. # to "").
  141. # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes
  142. # insignificant zeros after the decimal separator (defaults to
  143. # +false+).
  144. # * <tt>:format</tt> - Specifies the format of the percentage
  145. # string The number field is <tt>%n</tt> (defaults to "%n%").
  146. # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when
  147. # the argument is invalid.
  148. #
  149. # ==== Examples
  150. #
  151. # number_to_percentage(100) # => 100.000%
  152. # number_to_percentage("98") # => 98.000%
  153. # number_to_percentage(100, precision: 0) # => 100%
  154. # number_to_percentage(1000, delimiter: '.', separator: ',') # => 1.000,000%
  155. # number_to_percentage(302.24398923423, precision: 5) # => 302.24399%
  156. # number_to_percentage(1000, locale: :fr) # => 1 000,000%
  157. # number_to_percentage("98a") # => 98a%
  158. # number_to_percentage(100, format: "%n %") # => 100.000 %
  159. #
  160. # number_to_percentage("98a", raise: true) # => InvalidNumberError
  161. 9 def number_to_percentage(number, options = {})
  162. delegate_number_helper_method(:number_to_percentage, number, options)
  163. end
  164. # Formats a +number+ with grouped thousands using +delimiter+
  165. # (e.g., 12,324). You can customize the format in the +options+
  166. # hash.
  167. #
  168. # ==== Options
  169. #
  170. # * <tt>:locale</tt> - Sets the locale to be used for formatting
  171. # (defaults to current locale).
  172. # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
  173. # to ",").
  174. # * <tt>:separator</tt> - Sets the separator between the
  175. # fractional and integer digits (defaults to ".").
  176. # * <tt>:delimiter_pattern</tt> - Sets a custom regular expression used for
  177. # deriving the placement of delimiter. Helpful when using currency formats
  178. # like INR.
  179. # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when
  180. # the argument is invalid.
  181. #
  182. # ==== Examples
  183. #
  184. # number_with_delimiter(12345678) # => 12,345,678
  185. # number_with_delimiter("123456") # => 123,456
  186. # number_with_delimiter(12345678.05) # => 12,345,678.05
  187. # number_with_delimiter(12345678, delimiter: ".") # => 12.345.678
  188. # number_with_delimiter(12345678, delimiter: ",") # => 12,345,678
  189. # number_with_delimiter(12345678.05, separator: " ") # => 12,345,678 05
  190. # number_with_delimiter(12345678.05, locale: :fr) # => 12 345 678,05
  191. # number_with_delimiter("112a") # => 112a
  192. # number_with_delimiter(98765432.98, delimiter: " ", separator: ",")
  193. # # => 98 765 432,98
  194. #
  195. # number_with_delimiter("123456.78",
  196. # delimiter_pattern: /(\d+?)(?=(\d\d)+(\d)(?!\d))/) # => "1,23,456.78"
  197. #
  198. # number_with_delimiter("112a", raise: true) # => raise InvalidNumberError
  199. 9 def number_with_delimiter(number, options = {})
  200. delegate_number_helper_method(:number_to_delimited, number, options)
  201. end
  202. # Formats a +number+ with the specified level of
  203. # <tt>:precision</tt> (e.g., 112.32 has a precision of 2 if
  204. # +:significant+ is +false+, and 5 if +:significant+ is +true+).
  205. # You can customize the format in the +options+ hash.
  206. #
  207. # ==== Options
  208. #
  209. # * <tt>:locale</tt> - Sets the locale to be used for formatting
  210. # (defaults to current locale).
  211. # * <tt>:precision</tt> - Sets the precision of the number
  212. # (defaults to 3).
  213. # * <tt>:significant</tt> - If +true+, precision will be the number
  214. # of significant_digits. If +false+, the number of fractional
  215. # digits (defaults to +false+).
  216. # * <tt>:separator</tt> - Sets the separator between the
  217. # fractional and integer digits (defaults to ".").
  218. # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
  219. # to "").
  220. # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes
  221. # insignificant zeros after the decimal separator (defaults to
  222. # +false+).
  223. # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when
  224. # the argument is invalid.
  225. #
  226. # ==== Examples
  227. #
  228. # number_with_precision(111.2345) # => 111.235
  229. # number_with_precision(111.2345, precision: 2) # => 111.23
  230. # number_with_precision(13, precision: 5) # => 13.00000
  231. # number_with_precision(389.32314, precision: 0) # => 389
  232. # number_with_precision(111.2345, significant: true) # => 111
  233. # number_with_precision(111.2345, precision: 1, significant: true) # => 100
  234. # number_with_precision(13, precision: 5, significant: true) # => 13.000
  235. # number_with_precision(111.234, locale: :fr) # => 111,234
  236. #
  237. # number_with_precision(13, precision: 5, significant: true, strip_insignificant_zeros: true)
  238. # # => 13
  239. #
  240. # number_with_precision(389.32314, precision: 4, significant: true) # => 389.3
  241. # number_with_precision(1111.2345, precision: 2, separator: ',', delimiter: '.')
  242. # # => 1.111,23
  243. 9 def number_with_precision(number, options = {})
  244. delegate_number_helper_method(:number_to_rounded, number, options)
  245. end
  246. # Formats the bytes in +number+ into a more understandable
  247. # representation (e.g., giving it 1500 yields 1.46 KB). This
  248. # method is useful for reporting file sizes to users. You can
  249. # customize the format in the +options+ hash.
  250. #
  251. # See <tt>number_to_human</tt> if you want to pretty-print a
  252. # generic number.
  253. #
  254. # ==== Options
  255. #
  256. # * <tt>:locale</tt> - Sets the locale to be used for formatting
  257. # (defaults to current locale).
  258. # * <tt>:precision</tt> - Sets the precision of the number
  259. # (defaults to 3).
  260. # * <tt>:significant</tt> - If +true+, precision will be the number
  261. # of significant_digits. If +false+, the number of fractional
  262. # digits (defaults to +true+)
  263. # * <tt>:separator</tt> - Sets the separator between the
  264. # fractional and integer digits (defaults to ".").
  265. # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
  266. # to "").
  267. # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes
  268. # insignificant zeros after the decimal separator (defaults to
  269. # +true+)
  270. # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when
  271. # the argument is invalid.
  272. #
  273. # ==== Examples
  274. #
  275. # number_to_human_size(123) # => 123 Bytes
  276. # number_to_human_size(1234) # => 1.21 KB
  277. # number_to_human_size(12345) # => 12.1 KB
  278. # number_to_human_size(1234567) # => 1.18 MB
  279. # number_to_human_size(1234567890) # => 1.15 GB
  280. # number_to_human_size(1234567890123) # => 1.12 TB
  281. # number_to_human_size(1234567890123456) # => 1.1 PB
  282. # number_to_human_size(1234567890123456789) # => 1.07 EB
  283. # number_to_human_size(1234567, precision: 2) # => 1.2 MB
  284. # number_to_human_size(483989, precision: 2) # => 470 KB
  285. # number_to_human_size(1234567, precision: 2, separator: ',') # => 1,2 MB
  286. # number_to_human_size(1234567890123, precision: 5) # => "1.1228 TB"
  287. # number_to_human_size(524288000, precision: 5) # => "500 MB"
  288. 9 def number_to_human_size(number, options = {})
  289. delegate_number_helper_method(:number_to_human_size, number, options)
  290. end
  291. # Pretty prints (formats and approximates) a number in a way it
  292. # is more readable by humans (e.g.: 1200000000 becomes "1.2
  293. # Billion"). This is useful for numbers that can get very large
  294. # (and too hard to read).
  295. #
  296. # See <tt>number_to_human_size</tt> if you want to print a file
  297. # size.
  298. #
  299. # You can also define your own unit-quantifier names if you want
  300. # to use other decimal units (e.g.: 1500 becomes "1.5
  301. # kilometers", 0.150 becomes "150 milliliters", etc). You may
  302. # define a wide range of unit quantifiers, even fractional ones
  303. # (centi, deci, mili, etc).
  304. #
  305. # ==== Options
  306. #
  307. # * <tt>:locale</tt> - Sets the locale to be used for formatting
  308. # (defaults to current locale).
  309. # * <tt>:precision</tt> - Sets the precision of the number
  310. # (defaults to 3).
  311. # * <tt>:significant</tt> - If +true+, precision will be the number
  312. # of significant_digits. If +false+, the number of fractional
  313. # digits (defaults to +true+)
  314. # * <tt>:separator</tt> - Sets the separator between the
  315. # fractional and integer digits (defaults to ".").
  316. # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
  317. # to "").
  318. # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes
  319. # insignificant zeros after the decimal separator (defaults to
  320. # +true+)
  321. # * <tt>:units</tt> - A Hash of unit quantifier names. Or a
  322. # string containing an i18n scope where to find this hash. It
  323. # might have the following keys:
  324. # * *integers*: <tt>:unit</tt>, <tt>:ten</tt>,
  325. # <tt>:hundred</tt>, <tt>:thousand</tt>, <tt>:million</tt>,
  326. # <tt>:billion</tt>, <tt>:trillion</tt>,
  327. # <tt>:quadrillion</tt>
  328. # * *fractionals*: <tt>:deci</tt>, <tt>:centi</tt>,
  329. # <tt>:mili</tt>, <tt>:micro</tt>, <tt>:nano</tt>,
  330. # <tt>:pico</tt>, <tt>:femto</tt>
  331. # * <tt>:format</tt> - Sets the format of the output string
  332. # (defaults to "%n %u"). The field types are:
  333. # * %u - The quantifier (ex.: 'thousand')
  334. # * %n - The number
  335. # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when
  336. # the argument is invalid.
  337. #
  338. # ==== Examples
  339. #
  340. # number_to_human(123) # => "123"
  341. # number_to_human(1234) # => "1.23 Thousand"
  342. # number_to_human(12345) # => "12.3 Thousand"
  343. # number_to_human(1234567) # => "1.23 Million"
  344. # number_to_human(1234567890) # => "1.23 Billion"
  345. # number_to_human(1234567890123) # => "1.23 Trillion"
  346. # number_to_human(1234567890123456) # => "1.23 Quadrillion"
  347. # number_to_human(1234567890123456789) # => "1230 Quadrillion"
  348. # number_to_human(489939, precision: 2) # => "490 Thousand"
  349. # number_to_human(489939, precision: 4) # => "489.9 Thousand"
  350. # number_to_human(1234567, precision: 4,
  351. # significant: false) # => "1.2346 Million"
  352. # number_to_human(1234567, precision: 1,
  353. # separator: ',',
  354. # significant: false) # => "1,2 Million"
  355. #
  356. # number_to_human(500000000, precision: 5) # => "500 Million"
  357. # number_to_human(12345012345, significant: false) # => "12.345 Billion"
  358. #
  359. # Non-significant zeros after the decimal separator are stripped
  360. # out by default (set <tt>:strip_insignificant_zeros</tt> to
  361. # +false+ to change that):
  362. #
  363. # number_to_human(12.00001) # => "12"
  364. # number_to_human(12.00001, strip_insignificant_zeros: false) # => "12.0"
  365. #
  366. # ==== Custom Unit Quantifiers
  367. #
  368. # You can also use your own custom unit quantifiers:
  369. # number_to_human(500000, units: {unit: "ml", thousand: "lt"}) # => "500 lt"
  370. #
  371. # If in your I18n locale you have:
  372. # distance:
  373. # centi:
  374. # one: "centimeter"
  375. # other: "centimeters"
  376. # unit:
  377. # one: "meter"
  378. # other: "meters"
  379. # thousand:
  380. # one: "kilometer"
  381. # other: "kilometers"
  382. # billion: "gazillion-distance"
  383. #
  384. # Then you could do:
  385. #
  386. # number_to_human(543934, units: :distance) # => "544 kilometers"
  387. # number_to_human(54393498, units: :distance) # => "54400 kilometers"
  388. # number_to_human(54393498000, units: :distance) # => "54.4 gazillion-distance"
  389. # number_to_human(343, units: :distance, precision: 1) # => "300 meters"
  390. # number_to_human(1, units: :distance) # => "1 meter"
  391. # number_to_human(0.34, units: :distance) # => "34 centimeters"
  392. #
  393. 9 def number_to_human(number, options = {})
  394. delegate_number_helper_method(:number_to_human, number, options)
  395. end
  396. 9 private
  397. 9 def delegate_number_helper_method(method, number, options)
  398. return unless number
  399. options = escape_unsafe_options(options.symbolize_keys)
  400. wrap_with_output_safety_handling(number, options.delete(:raise)) {
  401. ActiveSupport::NumberHelper.public_send(method, number, options)
  402. }
  403. end
  404. 9 def escape_unsafe_options(options)
  405. options[:format] = ERB::Util.html_escape(options[:format]) if options[:format]
  406. options[:negative_format] = ERB::Util.html_escape(options[:negative_format]) if options[:negative_format]
  407. options[:separator] = ERB::Util.html_escape(options[:separator]) if options[:separator]
  408. options[:delimiter] = ERB::Util.html_escape(options[:delimiter]) if options[:delimiter]
  409. options[:unit] = ERB::Util.html_escape(options[:unit]) if options[:unit] && !options[:unit].html_safe?
  410. options[:units] = escape_units(options[:units]) if options[:units] && Hash === options[:units]
  411. options
  412. end
  413. 9 def escape_units(units)
  414. units.transform_values do |v|
  415. ERB::Util.html_escape(v)
  416. end
  417. end
  418. 9 def wrap_with_output_safety_handling(number, raise_on_invalid, &block)
  419. valid_float = valid_float?(number)
  420. raise InvalidNumberError, number if raise_on_invalid && !valid_float
  421. formatted_number = yield
  422. if valid_float || number.html_safe?
  423. formatted_number.html_safe
  424. else
  425. formatted_number
  426. end
  427. end
  428. 9 def valid_float?(number)
  429. !parse_float(number, false).nil?
  430. end
  431. 9 def parse_float(number, raise_error)
  432. Float(number)
  433. rescue ArgumentError, TypeError
  434. raise InvalidNumberError, number if raise_error
  435. end
  436. end
  437. end
  438. end

lib/action_view/helpers/output_safety_helper.rb

33.33% lines covered

21 relevant lines. 7 lines covered and 14 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 require "active_support/core_ext/string/output_safety"
  3. 9 module ActionView #:nodoc:
  4. # = Action View Raw Output Helper
  5. 9 module Helpers #:nodoc:
  6. 9 module OutputSafetyHelper
  7. # This method outputs without escaping a string. Since escaping tags is
  8. # now default, this can be used when you don't want Rails to automatically
  9. # escape tags. This is not recommended if the data is coming from the user's
  10. # input.
  11. #
  12. # For example:
  13. #
  14. # raw @user.name
  15. # # => 'Jimmy <alert>Tables</alert>'
  16. 9 def raw(stringish)
  17. stringish.to_s.html_safe
  18. end
  19. # This method returns an HTML safe string similar to what <tt>Array#join</tt>
  20. # would return. The array is flattened, and all items, including
  21. # the supplied separator, are HTML escaped unless they are HTML
  22. # safe, and the returned string is marked as HTML safe.
  23. #
  24. # safe_join([raw("<p>foo</p>"), "<p>bar</p>"], "<br />")
  25. # # => "<p>foo</p>&lt;br /&gt;&lt;p&gt;bar&lt;/p&gt;"
  26. #
  27. # safe_join([raw("<p>foo</p>"), raw("<p>bar</p>")], raw("<br />"))
  28. # # => "<p>foo</p><br /><p>bar</p>"
  29. #
  30. 9 def safe_join(array, sep = $,)
  31. sep = ERB::Util.unwrapped_html_escape(sep)
  32. array.flatten.map! { |i| ERB::Util.unwrapped_html_escape(i) }.join(sep).html_safe
  33. end
  34. # Converts the array to a comma-separated sentence where the last element is
  35. # joined by the connector word. This is the html_safe-aware version of
  36. # ActiveSupport's {Array#to_sentence}[https://api.rubyonrails.org/classes/Array.html#method-i-to_sentence].
  37. #
  38. 9 def to_sentence(array, options = {})
  39. options.assert_valid_keys(:words_connector, :two_words_connector, :last_word_connector, :locale)
  40. default_connectors = {
  41. words_connector: ", ",
  42. two_words_connector: " and ",
  43. last_word_connector: ", and "
  44. }
  45. if defined?(I18n)
  46. i18n_connectors = I18n.translate(:'support.array', locale: options[:locale], default: {})
  47. default_connectors.merge!(i18n_connectors)
  48. end
  49. options = default_connectors.merge!(options)
  50. case array.length
  51. when 0
  52. "".html_safe
  53. when 1
  54. ERB::Util.html_escape(array[0])
  55. when 2
  56. safe_join([array[0], array[1]], options[:two_words_connector])
  57. else
  58. safe_join([safe_join(array[0...-1], options[:words_connector]), options[:last_word_connector], array[-1]], nil)
  59. end
  60. end
  61. end
  62. end
  63. end

lib/action_view/helpers/rendering_helper.rb

29.41% lines covered

17 relevant lines. 5 lines covered and 12 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 module ActionView
  3. 9 module Helpers #:nodoc:
  4. # = Action View Rendering
  5. #
  6. # Implements methods that allow rendering from a view context.
  7. # In order to use this module, all you need is to implement
  8. # view_renderer that returns an ActionView::Renderer object.
  9. 9 module RenderingHelper
  10. # Returns the result of a render that's dictated by the options hash. The primary options are:
  11. #
  12. # * <tt>:partial</tt> - See <tt>ActionView::PartialRenderer</tt>.
  13. # * <tt>:file</tt> - Renders an explicit template file (this used to be the old default), add :locals to pass in those.
  14. # * <tt>:inline</tt> - Renders an inline template similar to how it's done in the controller.
  15. # * <tt>:plain</tt> - Renders the text passed in out. Setting the content
  16. # type as <tt>text/plain</tt>.
  17. # * <tt>:html</tt> - Renders the HTML safe string passed in out, otherwise
  18. # performs HTML escape on the string first. Setting the content type as
  19. # <tt>text/html</tt>.
  20. # * <tt>:body</tt> - Renders the text passed in, and inherits the content
  21. # type of <tt>text/plain</tt> from <tt>ActionDispatch::Response</tt>
  22. # object.
  23. #
  24. # If no <tt>options</tt> hash is passed or if <tt>:update</tt> is specified, then:
  25. #
  26. # If an object responding to `render_in` is passed, `render_in` is called on the object,
  27. # passing in the current view context.
  28. #
  29. # Otherwise, a partial is rendered using the second parameter as the locals hash.
  30. 9 def render(options = {}, locals = {}, &block)
  31. case options
  32. when Hash
  33. in_rendering_context(options) do |renderer|
  34. if block_given?
  35. view_renderer.render_partial(self, options.merge(partial: options[:layout]), &block)
  36. else
  37. view_renderer.render(self, options)
  38. end
  39. end
  40. else
  41. if options.respond_to?(:render_in)
  42. options.render_in(self, &block)
  43. else
  44. view_renderer.render_partial(self, partial: options, locals: locals, &block)
  45. end
  46. end
  47. end
  48. # Overwrites _layout_for in the context object so it supports the case a block is
  49. # passed to a partial. Returns the contents that are yielded to a layout, given a
  50. # name or a block.
  51. #
  52. # You can think of a layout as a method that is called with a block. If the user calls
  53. # <tt>yield :some_name</tt>, the block, by default, returns <tt>content_for(:some_name)</tt>.
  54. # If the user calls simply +yield+, the default block returns <tt>content_for(:layout)</tt>.
  55. #
  56. # The user can override this default by passing a block to the layout:
  57. #
  58. # # The template
  59. # <%= render layout: "my_layout" do %>
  60. # Content
  61. # <% end %>
  62. #
  63. # # The layout
  64. # <html>
  65. # <%= yield %>
  66. # </html>
  67. #
  68. # In this case, instead of the default block, which would return <tt>content_for(:layout)</tt>,
  69. # this method returns the block that was passed in to <tt>render :layout</tt>, and the response
  70. # would be
  71. #
  72. # <html>
  73. # Content
  74. # </html>
  75. #
  76. # Finally, the block can take block arguments, which can be passed in by +yield+:
  77. #
  78. # # The template
  79. # <%= render layout: "my_layout" do |customer| %>
  80. # Hello <%= customer.name %>
  81. # <% end %>
  82. #
  83. # # The layout
  84. # <html>
  85. # <%= yield Struct.new(:name).new("David") %>
  86. # </html>
  87. #
  88. # In this case, the layout would receive the block passed into <tt>render :layout</tt>,
  89. # and the struct specified would be passed into the block as an argument. The result
  90. # would be
  91. #
  92. # <html>
  93. # Hello David
  94. # </html>
  95. #
  96. 9 def _layout_for(*args, &block)
  97. name = args.first
  98. if block && !name.is_a?(Symbol)
  99. capture(*args, &block)
  100. else
  101. super
  102. end
  103. end
  104. end
  105. end
  106. end

lib/action_view/helpers/sanitize_helper.rb

62.96% lines covered

27 relevant lines. 17 lines covered and 10 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 require "rails-html-sanitizer"
  3. 9 module ActionView
  4. # = Action View Sanitize Helpers
  5. 9 module Helpers #:nodoc:
  6. # The SanitizeHelper module provides a set of methods for scrubbing text of undesired HTML elements.
  7. # These helper methods extend Action View making them callable within your template files.
  8. 9 module SanitizeHelper
  9. 9 extend ActiveSupport::Concern
  10. # Sanitizes HTML input, stripping all but known-safe tags and attributes.
  11. #
  12. # It also strips href/src attributes with unsafe protocols like
  13. # <tt>javascript:</tt>, while also protecting against attempts to use Unicode,
  14. # ASCII, and hex character references to work around these protocol filters.
  15. # All special characters will be escaped.
  16. #
  17. # The default sanitizer is Rails::Html::SafeListSanitizer. See {Rails HTML
  18. # Sanitizers}[https://github.com/rails/rails-html-sanitizer] for more information.
  19. #
  20. # Custom sanitization rules can also be provided.
  21. #
  22. # Please note that sanitizing user-provided text does not guarantee that the
  23. # resulting markup is valid or even well-formed.
  24. #
  25. # ==== Options
  26. #
  27. # * <tt>:tags</tt> - An array of allowed tags.
  28. # * <tt>:attributes</tt> - An array of allowed attributes.
  29. # * <tt>:scrubber</tt> - A {Rails::Html scrubber}[https://github.com/rails/rails-html-sanitizer]
  30. # or {Loofah::Scrubber}[https://github.com/flavorjones/loofah] object that
  31. # defines custom sanitization rules. A custom scrubber takes precedence over
  32. # custom tags and attributes.
  33. #
  34. # ==== Examples
  35. #
  36. # Normal use:
  37. #
  38. # <%= sanitize @comment.body %>
  39. #
  40. # Providing custom lists of permitted tags and attributes:
  41. #
  42. # <%= sanitize @comment.body, tags: %w(strong em a), attributes: %w(href) %>
  43. #
  44. # Providing a custom Rails::Html scrubber:
  45. #
  46. # class CommentScrubber < Rails::Html::PermitScrubber
  47. # def initialize
  48. # super
  49. # self.tags = %w( form script comment blockquote )
  50. # self.attributes = %w( style )
  51. # end
  52. #
  53. # def skip_node?(node)
  54. # node.text?
  55. # end
  56. # end
  57. #
  58. # <%= sanitize @comment.body, scrubber: CommentScrubber.new %>
  59. #
  60. # See {Rails HTML Sanitizer}[https://github.com/rails/rails-html-sanitizer] for
  61. # documentation about Rails::Html scrubbers.
  62. #
  63. # Providing a custom Loofah::Scrubber:
  64. #
  65. # scrubber = Loofah::Scrubber.new do |node|
  66. # node.remove if node.name == 'script'
  67. # end
  68. #
  69. # <%= sanitize @comment.body, scrubber: scrubber %>
  70. #
  71. # See {Loofah's documentation}[https://github.com/flavorjones/loofah] for more
  72. # information about defining custom Loofah::Scrubber objects.
  73. #
  74. # To set the default allowed tags or attributes across your application:
  75. #
  76. # # In config/application.rb
  77. # config.action_view.sanitized_allowed_tags = ['strong', 'em', 'a']
  78. # config.action_view.sanitized_allowed_attributes = ['href', 'title']
  79. 9 def sanitize(html, options = {})
  80. self.class.safe_list_sanitizer.sanitize(html, options)&.html_safe
  81. end
  82. # Sanitizes a block of CSS code. Used by +sanitize+ when it comes across a style attribute.
  83. 9 def sanitize_css(style)
  84. self.class.safe_list_sanitizer.sanitize_css(style)
  85. end
  86. # Strips all HTML tags from +html+, including comments and special characters.
  87. #
  88. # strip_tags("Strip <i>these</i> tags!")
  89. # # => Strip these tags!
  90. #
  91. # strip_tags("<b>Bold</b> no more! <a href='more.html'>See more here</a>...")
  92. # # => Bold no more! See more here...
  93. #
  94. # strip_tags("<div id='top-bar'>Welcome to my website!</div>")
  95. # # => Welcome to my website!
  96. #
  97. # strip_tags("> A quote from Smith & Wesson")
  98. # # => &gt; A quote from Smith &amp; Wesson
  99. 9 def strip_tags(html)
  100. self.class.full_sanitizer.sanitize(html)
  101. end
  102. # Strips all link tags from +html+ leaving just the link text.
  103. #
  104. # strip_links('<a href="http://www.rubyonrails.org">Ruby on Rails</a>')
  105. # # => Ruby on Rails
  106. #
  107. # strip_links('Please e-mail me at <a href="mailto:me@email.com">me@email.com</a>.')
  108. # # => Please e-mail me at me@email.com.
  109. #
  110. # strip_links('Blog: <a href="http://www.myblog.com/" class="nav" target=\"_blank\">Visit</a>.')
  111. # # => Blog: Visit.
  112. #
  113. # strip_links('<<a href="https://example.org">malformed & link</a>')
  114. # # => &lt;malformed &amp; link
  115. 9 def strip_links(html)
  116. self.class.link_sanitizer.sanitize(html)
  117. end
  118. 9 module ClassMethods #:nodoc:
  119. 9 attr_writer :full_sanitizer, :link_sanitizer, :safe_list_sanitizer
  120. 9 def sanitizer_vendor
  121. Rails::Html::Sanitizer
  122. end
  123. 9 def sanitized_allowed_tags
  124. safe_list_sanitizer.allowed_tags
  125. end
  126. 9 def sanitized_allowed_attributes
  127. safe_list_sanitizer.allowed_attributes
  128. end
  129. # Gets the Rails::Html::FullSanitizer instance used by +strip_tags+. Replace with
  130. # any object that responds to +sanitize+.
  131. #
  132. # class Application < Rails::Application
  133. # config.action_view.full_sanitizer = MySpecialSanitizer.new
  134. # end
  135. 9 def full_sanitizer
  136. @full_sanitizer ||= sanitizer_vendor.full_sanitizer.new
  137. end
  138. # Gets the Rails::Html::LinkSanitizer instance used by +strip_links+.
  139. # Replace with any object that responds to +sanitize+.
  140. #
  141. # class Application < Rails::Application
  142. # config.action_view.link_sanitizer = MySpecialSanitizer.new
  143. # end
  144. 9 def link_sanitizer
  145. @link_sanitizer ||= sanitizer_vendor.link_sanitizer.new
  146. end
  147. # Gets the Rails::Html::SafeListSanitizer instance used by sanitize and +sanitize_css+.
  148. # Replace with any object that responds to +sanitize+.
  149. #
  150. # class Application < Rails::Application
  151. # config.action_view.safe_list_sanitizer = MySpecialSanitizer.new
  152. # end
  153. 9 def safe_list_sanitizer
  154. @safe_list_sanitizer ||= sanitizer_vendor.safe_list_sanitizer.new
  155. end
  156. end
  157. end
  158. end
  159. end

lib/action_view/helpers/tag_helper.rb

41.58% lines covered

101 relevant lines. 42 lines covered and 59 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 require "active_support/core_ext/string/output_safety"
  3. 9 require "set"
  4. 9 module ActionView
  5. # = Action View Tag Helpers
  6. 9 module Helpers #:nodoc:
  7. # Provides methods to generate HTML tags programmatically both as a modern
  8. # HTML5 compliant builder style and legacy XHTML compliant tags.
  9. 9 module TagHelper
  10. 9 extend ActiveSupport::Concern
  11. 9 include CaptureHelper
  12. 9 include OutputSafetyHelper
  13. 9 BOOLEAN_ATTRIBUTES = %w(allowfullscreen allowpaymentrequest async autofocus
  14. autoplay checked compact controls declare default
  15. defaultchecked defaultmuted defaultselected defer
  16. disabled enabled formnovalidate hidden indeterminate
  17. inert ismap itemscope loop multiple muted nohref
  18. nomodule noresize noshade novalidate nowrap open
  19. pauseonexit playsinline readonly required reversed
  20. scoped seamless selected sortable truespeed
  21. typemustmatch visible).to_set
  22. 9 BOOLEAN_ATTRIBUTES.merge(BOOLEAN_ATTRIBUTES.map(&:to_sym))
  23. 9 BOOLEAN_ATTRIBUTES.freeze
  24. 9 TAG_PREFIXES = ["aria", "data", :aria, :data].to_set.freeze
  25. 9 TAG_TYPES = {}
  26. 9 TAG_TYPES.merge! BOOLEAN_ATTRIBUTES.index_with(:boolean)
  27. 9 TAG_TYPES.merge! TAG_PREFIXES.index_with(:prefix)
  28. 9 TAG_TYPES.freeze
  29. 9 PRE_CONTENT_STRINGS = Hash.new { "" }
  30. 9 PRE_CONTENT_STRINGS[:textarea] = "\n"
  31. 9 PRE_CONTENT_STRINGS["textarea"] = "\n"
  32. 9 class TagBuilder #:nodoc:
  33. 9 include CaptureHelper
  34. 9 include OutputSafetyHelper
  35. 9 VOID_ELEMENTS = %i(area base br col embed hr img input keygen link meta param source track wbr).to_set
  36. 9 def initialize(view_context)
  37. @view_context = view_context
  38. end
  39. 9 def tag_string(name, content = nil, escape_attributes: true, **options, &block)
  40. content = @view_context.capture(self, &block) if block_given?
  41. if VOID_ELEMENTS.include?(name) && content.nil?
  42. "<#{name.to_s.dasherize}#{tag_options(options, escape_attributes)}>".html_safe
  43. else
  44. content_tag_string(name.to_s.dasherize, content || "", options, escape_attributes)
  45. end
  46. end
  47. 9 def content_tag_string(name, content, options, escape = true)
  48. tag_options = tag_options(options, escape) if options
  49. content = ERB::Util.unwrapped_html_escape(content) if escape
  50. "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name]}#{content}</#{name}>".html_safe
  51. end
  52. 9 def tag_options(options, escape = true)
  53. return if options.blank?
  54. output = +""
  55. sep = " "
  56. options.each_pair do |key, value|
  57. type = TAG_TYPES[key]
  58. if type == :prefix && value.is_a?(Hash)
  59. value.each_pair do |k, v|
  60. next if v.nil?
  61. output << sep
  62. output << prefix_tag_option(key, k, v, escape)
  63. end
  64. elsif type == :boolean
  65. if value
  66. output << sep
  67. output << boolean_tag_option(key)
  68. end
  69. elsif !value.nil?
  70. output << sep
  71. output << tag_option(key, value, escape)
  72. end
  73. end
  74. output unless output.empty?
  75. end
  76. 9 def boolean_tag_option(key)
  77. %(#{key}="#{key}")
  78. end
  79. 9 def tag_option(key, value, escape)
  80. case value
  81. when Array, Hash
  82. value = TagHelper.build_tag_values(value) if key.to_s == "class"
  83. value = escape ? safe_join(value, " ") : value.join(" ")
  84. else
  85. value = escape ? ERB::Util.unwrapped_html_escape(value) : value.to_s
  86. end
  87. value = value.gsub('"', "&quot;") if value.include?('"')
  88. %(#{key}="#{value}")
  89. end
  90. 9 private
  91. 9 def prefix_tag_option(prefix, key, value, escape)
  92. key = "#{prefix}-#{key.to_s.dasherize}"
  93. unless value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(BigDecimal)
  94. value = value.to_json
  95. end
  96. tag_option(key, value, escape)
  97. end
  98. 9 def respond_to_missing?(*args)
  99. true
  100. end
  101. 9 def method_missing(called, *args, **options, &block)
  102. tag_string(called, *args, **options, &block)
  103. end
  104. end
  105. # Returns an HTML tag.
  106. #
  107. # === Building HTML tags
  108. #
  109. # Builds HTML5 compliant tags with a tag proxy. Every tag can be built with:
  110. #
  111. # tag.<tag name>(optional content, options)
  112. #
  113. # where tag name can be e.g. br, div, section, article, or any tag really.
  114. #
  115. # ==== Passing content
  116. #
  117. # Tags can pass content to embed within it:
  118. #
  119. # tag.h1 'All titles fit to print' # => <h1>All titles fit to print</h1>
  120. #
  121. # tag.div tag.p('Hello world!') # => <div><p>Hello world!</p></div>
  122. #
  123. # Content can also be captured with a block, which is useful in templates:
  124. #
  125. # <%= tag.p do %>
  126. # The next great American novel starts here.
  127. # <% end %>
  128. # # => <p>The next great American novel starts here.</p>
  129. #
  130. # ==== Options
  131. #
  132. # Use symbol keyed options to add attributes to the generated tag.
  133. #
  134. # tag.section class: %w( kitties puppies )
  135. # # => <section class="kitties puppies"></section>
  136. #
  137. # tag.section id: dom_id(@post)
  138. # # => <section id="<generated dom id>"></section>
  139. #
  140. # Pass +true+ for any attributes that can render with no values, like +disabled+ and +readonly+.
  141. #
  142. # tag.input type: 'text', disabled: true
  143. # # => <input type="text" disabled="disabled">
  144. #
  145. # HTML5 <tt>data-*</tt> attributes can be set with a single +data+ key
  146. # pointing to a hash of sub-attributes.
  147. #
  148. # To play nicely with JavaScript conventions, sub-attributes are dasherized.
  149. #
  150. # tag.article data: { user_id: 123 }
  151. # # => <article data-user-id="123"></article>
  152. #
  153. # Thus <tt>data-user-id</tt> can be accessed as <tt>dataset.userId</tt>.
  154. #
  155. # Data attribute values are encoded to JSON, with the exception of strings, symbols and
  156. # BigDecimals.
  157. # This may come in handy when using jQuery's HTML5-aware <tt>.data()</tt>
  158. # from 1.4.3.
  159. #
  160. # tag.div data: { city_state: %w( Chicago IL ) }
  161. # # => <div data-city-state="[&quot;Chicago&quot;,&quot;IL&quot;]"></div>
  162. #
  163. # The generated attributes are escaped by default. This can be disabled using
  164. # +escape_attributes+.
  165. #
  166. # tag.img src: 'open & shut.png'
  167. # # => <img src="open &amp; shut.png">
  168. #
  169. # tag.img src: 'open & shut.png', escape_attributes: false
  170. # # => <img src="open & shut.png">
  171. #
  172. # The tag builder respects
  173. # {HTML5 void elements}[https://www.w3.org/TR/html5/syntax.html#void-elements]
  174. # if no content is passed, and omits closing tags for those elements.
  175. #
  176. # # A standard element:
  177. # tag.div # => <div></div>
  178. #
  179. # # A void element:
  180. # tag.br # => <br>
  181. #
  182. # === Legacy syntax
  183. #
  184. # The following format is for legacy syntax support. It will be deprecated in future versions of Rails.
  185. #
  186. # tag(name, options = nil, open = false, escape = true)
  187. #
  188. # It returns an empty HTML tag of type +name+ which by default is XHTML
  189. # compliant. Set +open+ to true to create an open tag compatible
  190. # with HTML 4.0 and below. Add HTML attributes by passing an attributes
  191. # hash to +options+. Set +escape+ to false to disable attribute value
  192. # escaping.
  193. #
  194. # ==== Options
  195. #
  196. # You can use symbols or strings for the attribute names.
  197. #
  198. # Use +true+ with boolean attributes that can render with no value, like
  199. # +disabled+ and +readonly+.
  200. #
  201. # HTML5 <tt>data-*</tt> attributes can be set with a single +data+ key
  202. # pointing to a hash of sub-attributes.
  203. #
  204. # ==== Examples
  205. #
  206. # tag("br")
  207. # # => <br />
  208. #
  209. # tag("br", nil, true)
  210. # # => <br>
  211. #
  212. # tag("input", type: 'text', disabled: true)
  213. # # => <input type="text" disabled="disabled" />
  214. #
  215. # tag("input", type: 'text', class: ["strong", "highlight"])
  216. # # => <input class="strong highlight" type="text" />
  217. #
  218. # tag("img", src: "open & shut.png")
  219. # # => <img src="open &amp; shut.png" />
  220. #
  221. # tag("img", { src: "open &amp; shut.png" }, false, false)
  222. # # => <img src="open &amp; shut.png" />
  223. #
  224. # tag("div", data: { name: 'Stephen', city_state: %w(Chicago IL) })
  225. # # => <div data-name="Stephen" data-city-state="[&quot;Chicago&quot;,&quot;IL&quot;]" />
  226. #
  227. # tag("div", class: { highlight: current_user.admin? })
  228. # # => <div class="highlight" />
  229. 9 def tag(name = nil, options = nil, open = false, escape = true)
  230. if name.nil?
  231. tag_builder
  232. else
  233. "<#{name}#{tag_builder.tag_options(options, escape) if options}#{open ? ">" : " />"}".html_safe
  234. end
  235. end
  236. # Returns an HTML block tag of type +name+ surrounding the +content+. Add
  237. # HTML attributes by passing an attributes hash to +options+.
  238. # Instead of passing the content as an argument, you can also use a block
  239. # in which case, you pass your +options+ as the second parameter.
  240. # Set escape to false to disable attribute value escaping.
  241. # Note: this is legacy syntax, see +tag+ method description for details.
  242. #
  243. # ==== Options
  244. # The +options+ hash can be used with attributes with no value like (<tt>disabled</tt> and
  245. # <tt>readonly</tt>), which you can give a value of true in the +options+ hash. You can use
  246. # symbols or strings for the attribute names.
  247. #
  248. # ==== Examples
  249. # content_tag(:p, "Hello world!")
  250. # # => <p>Hello world!</p>
  251. # content_tag(:div, content_tag(:p, "Hello world!"), class: "strong")
  252. # # => <div class="strong"><p>Hello world!</p></div>
  253. # content_tag(:div, "Hello world!", class: ["strong", "highlight"])
  254. # # => <div class="strong highlight">Hello world!</div>
  255. # content_tag(:div, "Hello world!", class: ["strong", { highlight: current_user.admin? }])
  256. # # => <div class="strong highlight">Hello world!</div>
  257. # content_tag("select", options, multiple: true)
  258. # # => <select multiple="multiple">...options...</select>
  259. #
  260. # <%= content_tag :div, class: "strong" do -%>
  261. # Hello world!
  262. # <% end -%>
  263. # # => <div class="strong">Hello world!</div>
  264. 9 def content_tag(name, content_or_options_with_block = nil, options = nil, escape = true, &block)
  265. if block_given?
  266. options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash)
  267. tag_builder.content_tag_string(name, capture(&block), options, escape)
  268. else
  269. tag_builder.content_tag_string(name, content_or_options_with_block, options, escape)
  270. end
  271. end
  272. # Returns a string of class names built from +args+.
  273. #
  274. # ==== Examples
  275. # class_names("foo", "bar")
  276. # # => "foo bar"
  277. # class_names({ foo: true, bar: false })
  278. # # => "foo"
  279. # class_names(nil, false, 123, "", "foo", { bar: true })
  280. # # => "123 foo bar"
  281. 9 def class_names(*args)
  282. safe_join(build_tag_values(*args), " ")
  283. end
  284. # Returns a CDATA section with the given +content+. CDATA sections
  285. # are used to escape blocks of text containing characters which would
  286. # otherwise be recognized as markup. CDATA sections begin with the string
  287. # <tt><![CDATA[</tt> and end with (and may not contain) the string <tt>]]></tt>.
  288. #
  289. # cdata_section("<hello world>")
  290. # # => <![CDATA[<hello world>]]>
  291. #
  292. # cdata_section(File.read("hello_world.txt"))
  293. # # => <![CDATA[<hello from a text file]]>
  294. #
  295. # cdata_section("hello]]>world")
  296. # # => <![CDATA[hello]]]]><![CDATA[>world]]>
  297. 9 def cdata_section(content)
  298. splitted = content.to_s.gsub(/\]\]\>/, "]]]]><![CDATA[>")
  299. "<![CDATA[#{splitted}]]>".html_safe
  300. end
  301. # Returns an escaped version of +html+ without affecting existing escaped entities.
  302. #
  303. # escape_once("1 < 2 &amp; 3")
  304. # # => "1 &lt; 2 &amp; 3"
  305. #
  306. # escape_once("&lt;&lt; Accept & Checkout")
  307. # # => "&lt;&lt; Accept &amp; Checkout"
  308. 9 def escape_once(html)
  309. ERB::Util.html_escape_once(html)
  310. end
  311. 9 private
  312. 9 def build_tag_values(*args)
  313. tag_values = []
  314. args.each do |tag_value|
  315. case tag_value
  316. when Hash
  317. tag_value.each do |key, val|
  318. tag_values << key.to_s if val && key.present?
  319. end
  320. when Array
  321. tag_values.concat build_tag_values(*tag_value)
  322. else
  323. tag_values << tag_value.to_s if tag_value.present?
  324. end
  325. end
  326. tag_values
  327. end
  328. 9 module_function :build_tag_values
  329. 9 def tag_builder
  330. @tag_builder ||= TagBuilder.new(self)
  331. end
  332. end
  333. end
  334. end

lib/action_view/helpers/tags.rb

100.0% lines covered

37 relevant lines. 37 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 3 module ActionView
  3. 3 module Helpers #:nodoc:
  4. 3 module Tags #:nodoc:
  5. 3 extend ActiveSupport::Autoload
  6. 3 eager_autoload do
  7. 3 autoload :Base
  8. 3 autoload :Translator
  9. 3 autoload :CheckBox
  10. 3 autoload :CollectionCheckBoxes
  11. 3 autoload :CollectionRadioButtons
  12. 3 autoload :CollectionSelect
  13. 3 autoload :ColorField
  14. 3 autoload :DateField
  15. 3 autoload :DateSelect
  16. 3 autoload :DatetimeField
  17. 3 autoload :DatetimeLocalField
  18. 3 autoload :DatetimeSelect
  19. 3 autoload :EmailField
  20. 3 autoload :FileField
  21. 3 autoload :GroupedCollectionSelect
  22. 3 autoload :HiddenField
  23. 3 autoload :Label
  24. 3 autoload :MonthField
  25. 3 autoload :NumberField
  26. 3 autoload :PasswordField
  27. 3 autoload :RadioButton
  28. 3 autoload :RangeField
  29. 3 autoload :SearchField
  30. 3 autoload :Select
  31. 3 autoload :TelField
  32. 3 autoload :TextArea
  33. 3 autoload :TextField
  34. 3 autoload :TimeField
  35. 3 autoload :TimeSelect
  36. 3 autoload :TimeZoneSelect
  37. 3 autoload :UrlField
  38. 3 autoload :WeekField
  39. end
  40. end
  41. end
  42. end

lib/action_view/helpers/tags/base.rb

25.0% lines covered

108 relevant lines. 27 lines covered and 81 lines missed.
    
  1. # frozen_string_literal: true
  2. 3 module ActionView
  3. 3 module Helpers
  4. 3 module Tags # :nodoc:
  5. 3 class Base # :nodoc:
  6. 3 include Helpers::ActiveModelInstanceTag, Helpers::TagHelper, Helpers::FormTagHelper
  7. 3 include FormOptionsHelper
  8. 3 attr_reader :object
  9. 3 def initialize(object_name, method_name, template_object, options = {})
  10. @object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup
  11. @template_object = template_object
  12. @object_name.sub!(/\[\]$/, "") || @object_name.sub!(/\[\]\]$/, "]")
  13. @object = retrieve_object(options.delete(:object))
  14. @skip_default_ids = options.delete(:skip_default_ids)
  15. @allow_method_names_outside_object = options.delete(:allow_method_names_outside_object)
  16. @options = options
  17. if Regexp.last_match
  18. @generate_indexed_names = true
  19. @auto_index = retrieve_autoindex(Regexp.last_match.pre_match)
  20. else
  21. @generate_indexed_names = false
  22. @auto_index = nil
  23. end
  24. end
  25. # This is what child classes implement.
  26. 3 def render
  27. raise NotImplementedError, "Subclasses must implement a render method"
  28. end
  29. 3 private
  30. 3 def value
  31. if @allow_method_names_outside_object
  32. object.public_send @method_name if object && object.respond_to?(@method_name)
  33. else
  34. object.public_send @method_name if object
  35. end
  36. end
  37. 3 def value_before_type_cast
  38. unless object.nil?
  39. method_before_type_cast = @method_name + "_before_type_cast"
  40. if value_came_from_user? && object.respond_to?(method_before_type_cast)
  41. object.public_send(method_before_type_cast)
  42. else
  43. value
  44. end
  45. end
  46. end
  47. 3 def value_came_from_user?
  48. method_name = "#{@method_name}_came_from_user?"
  49. !object.respond_to?(method_name) || object.public_send(method_name)
  50. end
  51. 3 def retrieve_object(object)
  52. if object
  53. object
  54. elsif @template_object.instance_variable_defined?("@#{@object_name}")
  55. @template_object.instance_variable_get("@#{@object_name}")
  56. end
  57. rescue NameError
  58. # As @object_name may contain the nested syntax (item[subobject]) we need to fallback to nil.
  59. nil
  60. end
  61. 3 def retrieve_autoindex(pre_match)
  62. object = self.object || @template_object.instance_variable_get("@#{pre_match}")
  63. if object && object.respond_to?(:to_param)
  64. object.to_param
  65. else
  66. raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
  67. end
  68. end
  69. 3 def add_default_name_and_id_for_value(tag_value, options)
  70. if tag_value.nil?
  71. add_default_name_and_id(options)
  72. else
  73. specified_id = options["id"]
  74. add_default_name_and_id(options)
  75. if specified_id.blank? && options["id"].present?
  76. options["id"] += "_#{sanitized_value(tag_value)}"
  77. end
  78. end
  79. end
  80. 3 def add_default_name_and_id(options)
  81. index = name_and_id_index(options)
  82. options["name"] = options.fetch("name") { tag_name(options["multiple"], index) }
  83. if generate_ids?
  84. options["id"] = options.fetch("id") { tag_id(index) }
  85. if namespace = options.delete("namespace")
  86. options["id"] = options["id"] ? "#{namespace}_#{options['id']}" : namespace
  87. end
  88. end
  89. end
  90. 3 def tag_name(multiple = false, index = nil)
  91. # a little duplication to construct fewer strings
  92. case
  93. when @object_name.empty?
  94. "#{sanitized_method_name}#{multiple ? "[]" : ""}"
  95. when index
  96. "#{@object_name}[#{index}][#{sanitized_method_name}]#{multiple ? "[]" : ""}"
  97. else
  98. "#{@object_name}[#{sanitized_method_name}]#{multiple ? "[]" : ""}"
  99. end
  100. end
  101. 3 def tag_id(index = nil)
  102. # a little duplication to construct fewer strings
  103. case
  104. when @object_name.empty?
  105. sanitized_method_name.dup
  106. when index
  107. "#{sanitized_object_name}_#{index}_#{sanitized_method_name}"
  108. else
  109. "#{sanitized_object_name}_#{sanitized_method_name}"
  110. end
  111. end
  112. 3 def sanitized_object_name
  113. @sanitized_object_name ||= @object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").delete_suffix("_")
  114. end
  115. 3 def sanitized_method_name
  116. @sanitized_method_name ||= @method_name.delete_suffix("?")
  117. end
  118. 3 def sanitized_value(value)
  119. value.to_s.gsub(/[\s\.]/, "_").gsub(/[^-[[:word:]]]/, "").downcase
  120. end
  121. 3 def select_content_tag(option_tags, options, html_options)
  122. html_options = html_options.stringify_keys
  123. add_default_name_and_id(html_options)
  124. if placeholder_required?(html_options)
  125. raise ArgumentError, "include_blank cannot be false for a required field." if options[:include_blank] == false
  126. options[:include_blank] ||= true unless options[:prompt]
  127. end
  128. value = options.fetch(:selected) { value() }
  129. select = content_tag("select", add_options(option_tags, options, value), html_options)
  130. if html_options["multiple"] && options.fetch(:include_hidden, true)
  131. tag("input", disabled: html_options["disabled"], name: html_options["name"], type: "hidden", value: "") + select
  132. else
  133. select
  134. end
  135. end
  136. 3 def placeholder_required?(html_options)
  137. # See https://html.spec.whatwg.org/multipage/forms.html#attr-select-required
  138. html_options["required"] && !html_options["multiple"] && html_options.fetch("size", 1).to_i == 1
  139. end
  140. 3 def add_options(option_tags, options, value = nil)
  141. if options[:include_blank]
  142. content = (options[:include_blank] if options[:include_blank].is_a?(String))
  143. label = (" " unless content)
  144. option_tags = tag_builder.content_tag_string("option", content, value: "", label: label) + "\n" + option_tags
  145. end
  146. if value.blank? && options[:prompt]
  147. tag_options = { value: "" }.tap do |prompt_opts|
  148. prompt_opts[:disabled] = true if options[:disabled] == ""
  149. prompt_opts[:selected] = true if options[:selected] == ""
  150. end
  151. option_tags = tag_builder.content_tag_string("option", prompt_text(options[:prompt]), tag_options) + "\n" + option_tags
  152. end
  153. option_tags
  154. end
  155. 3 def name_and_id_index(options)
  156. if options.key?("index")
  157. options.delete("index") || ""
  158. elsif @generate_indexed_names
  159. @auto_index || ""
  160. end
  161. end
  162. 3 def generate_ids?
  163. !@skip_default_ids
  164. end
  165. end
  166. end
  167. end
  168. end

lib/action_view/helpers/tags/check_box.rb

0.0% lines covered

55 relevant lines. 0 lines covered and 55 lines missed.
    
  1. # frozen_string_literal: true
  2. require "action_view/helpers/tags/checkable"
  3. module ActionView
  4. module Helpers
  5. module Tags # :nodoc:
  6. class CheckBox < Base #:nodoc:
  7. include Checkable
  8. def initialize(object_name, method_name, template_object, checked_value, unchecked_value, options)
  9. @checked_value = checked_value
  10. @unchecked_value = unchecked_value
  11. super(object_name, method_name, template_object, options)
  12. end
  13. def render
  14. options = @options.stringify_keys
  15. options["type"] = "checkbox"
  16. options["value"] = @checked_value
  17. options["checked"] = "checked" if input_checked?(options)
  18. if options["multiple"]
  19. add_default_name_and_id_for_value(@checked_value, options)
  20. options.delete("multiple")
  21. else
  22. add_default_name_and_id(options)
  23. end
  24. include_hidden = options.delete("include_hidden") { true }
  25. checkbox = tag("input", options)
  26. if include_hidden
  27. hidden = hidden_field_for_checkbox(options)
  28. hidden + checkbox
  29. else
  30. checkbox
  31. end
  32. end
  33. private
  34. def checked?(value)
  35. case value
  36. when TrueClass, FalseClass
  37. value == !!@checked_value
  38. when NilClass
  39. false
  40. when String
  41. value == @checked_value
  42. else
  43. if value.respond_to?(:include?)
  44. value.include?(@checked_value)
  45. else
  46. value.to_i == @checked_value.to_i
  47. end
  48. end
  49. end
  50. def hidden_field_for_checkbox(options)
  51. @unchecked_value ? tag("input", options.slice("name", "disabled", "form").merge!("type" => "hidden", "value" => @unchecked_value)) : "".html_safe
  52. end
  53. end
  54. end
  55. end
  56. end

lib/action_view/helpers/tags/checkable.rb

0.0% lines covered

16 relevant lines. 0 lines covered and 16 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView
  3. module Helpers
  4. module Tags # :nodoc:
  5. module Checkable # :nodoc:
  6. def input_checked?(options)
  7. if options.has_key?("checked")
  8. checked = options.delete "checked"
  9. checked == true || checked == "checked"
  10. else
  11. checked?(value)
  12. end
  13. end
  14. end
  15. end
  16. end
  17. end

lib/action_view/helpers/tags/collection_check_boxes.rb

0.0% lines covered

28 relevant lines. 0 lines covered and 28 lines missed.
    
  1. # frozen_string_literal: true
  2. require "action_view/helpers/tags/collection_helpers"
  3. module ActionView
  4. module Helpers
  5. module Tags # :nodoc:
  6. class CollectionCheckBoxes < Base # :nodoc:
  7. include CollectionHelpers
  8. class CheckBoxBuilder < Builder # :nodoc:
  9. def check_box(extra_html_options = {})
  10. html_options = extra_html_options.merge(@input_html_options)
  11. html_options[:multiple] = true
  12. html_options[:skip_default_ids] = false
  13. @template_object.check_box(@object_name, @method_name, html_options, @value, nil)
  14. end
  15. end
  16. def render(&block)
  17. render_collection_for(CheckBoxBuilder, &block)
  18. end
  19. private
  20. def render_component(builder)
  21. builder.check_box + builder.label
  22. end
  23. def hidden_field_name
  24. "#{super}[]"
  25. end
  26. end
  27. end
  28. end
  29. end

lib/action_view/helpers/tags/collection_helpers.rb

0.0% lines covered

93 relevant lines. 0 lines covered and 93 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView
  3. module Helpers
  4. module Tags # :nodoc:
  5. module CollectionHelpers # :nodoc:
  6. class Builder # :nodoc:
  7. attr_reader :object, :text, :value
  8. def initialize(template_object, object_name, method_name, object,
  9. sanitized_attribute_name, text, value, input_html_options)
  10. @template_object = template_object
  11. @object_name = object_name
  12. @method_name = method_name
  13. @object = object
  14. @sanitized_attribute_name = sanitized_attribute_name
  15. @text = text
  16. @value = value
  17. @input_html_options = input_html_options
  18. end
  19. def label(label_html_options = {}, &block)
  20. html_options = @input_html_options.slice(:index, :namespace).merge(label_html_options)
  21. html_options[:for] ||= @input_html_options[:id] if @input_html_options[:id]
  22. @template_object.label(@object_name, @sanitized_attribute_name, @text, html_options, &block)
  23. end
  24. end
  25. def initialize(object_name, method_name, template_object, collection, value_method, text_method, options, html_options)
  26. @collection = collection
  27. @value_method = value_method
  28. @text_method = text_method
  29. @html_options = html_options
  30. super(object_name, method_name, template_object, options)
  31. end
  32. private
  33. def instantiate_builder(builder_class, item, value, text, html_options)
  34. builder_class.new(@template_object, @object_name, @method_name, item,
  35. sanitize_attribute_name(value), text, value, html_options)
  36. end
  37. # Generate default options for collection helpers, such as :checked and
  38. # :disabled.
  39. def default_html_options_for_collection(item, value)
  40. html_options = @html_options.dup
  41. [:checked, :selected, :disabled, :readonly].each do |option|
  42. current_value = @options[option]
  43. next if current_value.nil?
  44. accept = if current_value.respond_to?(:call)
  45. current_value.call(item)
  46. else
  47. Array(current_value).map(&:to_s).include?(value.to_s)
  48. end
  49. if accept
  50. html_options[option] = true
  51. elsif option == :checked
  52. html_options[option] = false
  53. end
  54. end
  55. html_options[:object] = @object
  56. html_options
  57. end
  58. def sanitize_attribute_name(value)
  59. "#{sanitized_method_name}_#{sanitized_value(value)}"
  60. end
  61. def render_collection
  62. @collection.map do |item|
  63. value = value_for_collection(item, @value_method)
  64. text = value_for_collection(item, @text_method)
  65. default_html_options = default_html_options_for_collection(item, value)
  66. additional_html_options = option_html_attributes(item)
  67. yield item, value, text, default_html_options.merge(additional_html_options)
  68. end.join.html_safe
  69. end
  70. def render_collection_for(builder_class, &block)
  71. options = @options.stringify_keys
  72. rendered_collection = render_collection do |item, value, text, default_html_options|
  73. builder = instantiate_builder(builder_class, item, value, text, default_html_options)
  74. if block_given?
  75. @template_object.capture(builder, &block)
  76. else
  77. render_component(builder)
  78. end
  79. end
  80. # Prepend a hidden field to make sure something will be sent back to the
  81. # server if all radio buttons are unchecked.
  82. if options.fetch("include_hidden", true)
  83. hidden_field + rendered_collection
  84. else
  85. rendered_collection
  86. end
  87. end
  88. def hidden_field
  89. hidden_name = @html_options[:name] || hidden_field_name
  90. @template_object.hidden_field_tag(hidden_name, "", id: nil)
  91. end
  92. def hidden_field_name
  93. "#{tag_name(false, @options[:index])}"
  94. end
  95. end
  96. end
  97. end
  98. end

lib/action_view/helpers/tags/collection_radio_buttons.rb

0.0% lines covered

24 relevant lines. 0 lines covered and 24 lines missed.
    
  1. # frozen_string_literal: true
  2. require "action_view/helpers/tags/collection_helpers"
  3. module ActionView
  4. module Helpers
  5. module Tags # :nodoc:
  6. class CollectionRadioButtons < Base # :nodoc:
  7. include CollectionHelpers
  8. class RadioButtonBuilder < Builder # :nodoc:
  9. def radio_button(extra_html_options = {})
  10. html_options = extra_html_options.merge(@input_html_options)
  11. html_options[:skip_default_ids] = false
  12. @template_object.radio_button(@object_name, @method_name, @value, html_options)
  13. end
  14. end
  15. def render(&block)
  16. render_collection_for(RadioButtonBuilder, &block)
  17. end
  18. private
  19. def render_component(builder)
  20. builder.radio_button + builder.label
  21. end
  22. end
  23. end
  24. end
  25. end

lib/action_view/helpers/tags/collection_select.rb

0.0% lines covered

25 relevant lines. 0 lines covered and 25 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView
  3. module Helpers
  4. module Tags # :nodoc:
  5. class CollectionSelect < Base #:nodoc:
  6. def initialize(object_name, method_name, template_object, collection, value_method, text_method, options, html_options)
  7. @collection = collection
  8. @value_method = value_method
  9. @text_method = text_method
  10. @html_options = html_options
  11. super(object_name, method_name, template_object, options)
  12. end
  13. def render
  14. option_tags_options = {
  15. selected: @options.fetch(:selected) { value },
  16. disabled: @options[:disabled]
  17. }
  18. select_content_tag(
  19. options_from_collection_for_select(@collection, @value_method, @text_method, option_tags_options),
  20. @options, @html_options
  21. )
  22. end
  23. end
  24. end
  25. end
  26. end

lib/action_view/helpers/tags/color_field.rb

0.0% lines covered

23 relevant lines. 0 lines covered and 23 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView
  3. module Helpers
  4. module Tags # :nodoc:
  5. class ColorField < TextField # :nodoc:
  6. def render
  7. options = @options.stringify_keys
  8. options["value"] ||= validate_color_string(value)
  9. @options = options
  10. super
  11. end
  12. private
  13. def validate_color_string(string)
  14. regex = /#[0-9a-fA-F]{6}/
  15. if regex.match?(string)
  16. string.downcase
  17. else
  18. "#000000"
  19. end
  20. end
  21. end
  22. end
  23. end
  24. end

lib/action_view/helpers/tags/date_field.rb

0.0% lines covered

12 relevant lines. 0 lines covered and 12 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView
  3. module Helpers
  4. module Tags # :nodoc:
  5. class DateField < DatetimeField # :nodoc:
  6. private
  7. def format_date(value)
  8. value&.strftime("%Y-%m-%d")
  9. end
  10. end
  11. end
  12. end
  13. end

lib/action_view/helpers/tags/date_select.rb

0.0% lines covered

56 relevant lines. 0 lines covered and 56 lines missed.
    
  1. # frozen_string_literal: true
  2. require "active_support/core_ext/time/calculations"
  3. module ActionView
  4. module Helpers
  5. module Tags # :nodoc:
  6. class DateSelect < Base # :nodoc:
  7. def initialize(object_name, method_name, template_object, options, html_options)
  8. @html_options = html_options
  9. super(object_name, method_name, template_object, options)
  10. end
  11. def render
  12. error_wrapping(datetime_selector(@options, @html_options).send("select_#{select_type}").html_safe)
  13. end
  14. class << self
  15. def select_type
  16. @select_type ||= name.split("::").last.sub("Select", "").downcase
  17. end
  18. end
  19. private
  20. def select_type
  21. self.class.select_type
  22. end
  23. def datetime_selector(options, html_options)
  24. datetime = options.fetch(:selected) { value || default_datetime(options) }
  25. @auto_index ||= nil
  26. options = options.dup
  27. options[:field_name] = @method_name
  28. options[:include_position] = true
  29. options[:prefix] ||= @object_name
  30. options[:index] = @auto_index if @auto_index && !options.has_key?(:index)
  31. DateTimeSelector.new(datetime, options, html_options)
  32. end
  33. def default_datetime(options)
  34. return if options[:include_blank] || options[:prompt]
  35. case options[:default]
  36. when nil
  37. Time.current
  38. when Date, Time
  39. options[:default]
  40. else
  41. default = options[:default].dup
  42. # Rename :minute and :second to :min and :sec
  43. default[:min] ||= default[:minute]
  44. default[:sec] ||= default[:second]
  45. time = Time.current
  46. [:year, :month, :day, :hour, :min, :sec].each do |key|
  47. default[key] ||= time.send(key)
  48. end
  49. Time.utc(
  50. default[:year], default[:month], default[:day],
  51. default[:hour], default[:min], default[:sec]
  52. )
  53. end
  54. end
  55. end
  56. end
  57. end
  58. end

lib/action_view/helpers/tags/datetime_field.rb

0.0% lines covered

27 relevant lines. 0 lines covered and 27 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView
  3. module Helpers
  4. module Tags # :nodoc:
  5. class DatetimeField < TextField # :nodoc:
  6. def render
  7. options = @options.stringify_keys
  8. options["value"] ||= format_date(value)
  9. options["min"] = format_date(datetime_value(options["min"]))
  10. options["max"] = format_date(datetime_value(options["max"]))
  11. @options = options
  12. super
  13. end
  14. private
  15. def format_date(value)
  16. raise NotImplementedError
  17. end
  18. def datetime_value(value)
  19. if value.is_a? String
  20. DateTime.parse(value) rescue nil
  21. else
  22. value
  23. end
  24. end
  25. end
  26. end
  27. end
  28. end

lib/action_view/helpers/tags/datetime_local_field.rb

0.0% lines covered

17 relevant lines. 0 lines covered and 17 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView
  3. module Helpers
  4. module Tags # :nodoc:
  5. class DatetimeLocalField < DatetimeField # :nodoc:
  6. class << self
  7. def field_type
  8. @field_type ||= "datetime-local"
  9. end
  10. end
  11. private
  12. def format_date(value)
  13. value&.strftime("%Y-%m-%dT%T")
  14. end
  15. end
  16. end
  17. end
  18. end

lib/action_view/helpers/tags/datetime_select.rb

0.0% lines covered

8 relevant lines. 0 lines covered and 8 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView
  3. module Helpers
  4. module Tags # :nodoc:
  5. class DatetimeSelect < DateSelect # :nodoc:
  6. end
  7. end
  8. end
  9. end

lib/action_view/helpers/tags/email_field.rb

0.0% lines covered

8 relevant lines. 0 lines covered and 8 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView
  3. module Helpers
  4. module Tags # :nodoc:
  5. class EmailField < TextField # :nodoc:
  6. end
  7. end
  8. end
  9. end

lib/action_view/helpers/tags/file_field.rb

0.0% lines covered

8 relevant lines. 0 lines covered and 8 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView
  3. module Helpers
  4. module Tags # :nodoc:
  5. class FileField < TextField # :nodoc:
  6. end
  7. end
  8. end
  9. end

lib/action_view/helpers/tags/grouped_collection_select.rb

0.0% lines covered

26 relevant lines. 0 lines covered and 26 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView
  3. module Helpers
  4. module Tags # :nodoc:
  5. class GroupedCollectionSelect < Base # :nodoc:
  6. def initialize(object_name, method_name, template_object, collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options)
  7. @collection = collection
  8. @group_method = group_method
  9. @group_label_method = group_label_method
  10. @option_key_method = option_key_method
  11. @option_value_method = option_value_method
  12. @html_options = html_options
  13. super(object_name, method_name, template_object, options)
  14. end
  15. def render
  16. option_tags_options = {
  17. selected: @options.fetch(:selected) { value },
  18. disabled: @options[:disabled]
  19. }
  20. select_content_tag(
  21. option_groups_from_collection_for_select(@collection, @group_method, @group_label_method, @option_key_method, @option_value_method, option_tags_options), @options, @html_options
  22. )
  23. end
  24. end
  25. end
  26. end
  27. end

lib/action_view/helpers/tags/hidden_field.rb

0.0% lines covered

8 relevant lines. 0 lines covered and 8 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView
  3. module Helpers
  4. module Tags # :nodoc:
  5. class HiddenField < TextField # :nodoc:
  6. end
  7. end
  8. end
  9. end

lib/action_view/helpers/tags/label.rb

0.0% lines covered

64 relevant lines. 0 lines covered and 64 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView
  3. module Helpers
  4. module Tags # :nodoc:
  5. class Label < Base # :nodoc:
  6. class LabelBuilder # :nodoc:
  7. attr_reader :object
  8. def initialize(template_object, object_name, method_name, object, tag_value)
  9. @template_object = template_object
  10. @object_name = object_name
  11. @method_name = method_name
  12. @object = object
  13. @tag_value = tag_value
  14. end
  15. def translation
  16. method_and_value = @tag_value.present? ? "#{@method_name}.#{@tag_value}" : @method_name
  17. content ||= Translator
  18. .new(object, @object_name, method_and_value, scope: "helpers.label")
  19. .translate
  20. content ||= @method_name.humanize
  21. content
  22. end
  23. end
  24. def initialize(object_name, method_name, template_object, content_or_options = nil, options = nil)
  25. options ||= {}
  26. content_is_options = content_or_options.is_a?(Hash)
  27. if content_is_options
  28. options.merge! content_or_options
  29. @content = nil
  30. else
  31. @content = content_or_options
  32. end
  33. super(object_name, method_name, template_object, options)
  34. end
  35. def render(&block)
  36. options = @options.stringify_keys
  37. tag_value = options.delete("value")
  38. name_and_id = options.dup
  39. if name_and_id["for"]
  40. name_and_id["id"] = name_and_id["for"]
  41. else
  42. name_and_id.delete("id")
  43. end
  44. add_default_name_and_id_for_value(tag_value, name_and_id)
  45. options.delete("index")
  46. options.delete("namespace")
  47. options["for"] = name_and_id["id"] unless options.key?("for")
  48. builder = LabelBuilder.new(@template_object, @object_name, @method_name, @object, tag_value)
  49. content = if block_given?
  50. @template_object.capture(builder, &block)
  51. elsif @content.present?
  52. @content.to_s
  53. else
  54. render_component(builder)
  55. end
  56. label_tag(name_and_id["id"], content, options)
  57. end
  58. private
  59. def render_component(builder)
  60. builder.translation
  61. end
  62. end
  63. end
  64. end
  65. end

lib/action_view/helpers/tags/month_field.rb

0.0% lines covered

12 relevant lines. 0 lines covered and 12 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView
  3. module Helpers
  4. module Tags # :nodoc:
  5. class MonthField < DatetimeField # :nodoc:
  6. private
  7. def format_date(value)
  8. value&.strftime("%Y-%m")
  9. end
  10. end
  11. end
  12. end
  13. end

lib/action_view/helpers/tags/number_field.rb

0.0% lines covered

16 relevant lines. 0 lines covered and 16 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView
  3. module Helpers
  4. module Tags # :nodoc:
  5. class NumberField < TextField # :nodoc:
  6. def render
  7. options = @options.stringify_keys
  8. if range = options.delete("in") || options.delete("within")
  9. options.update("min" => range.min, "max" => range.max)
  10. end
  11. @options = options
  12. super
  13. end
  14. end
  15. end
  16. end
  17. end

lib/action_view/helpers/tags/password_field.rb

0.0% lines covered

12 relevant lines. 0 lines covered and 12 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView
  3. module Helpers
  4. module Tags # :nodoc:
  5. class PasswordField < TextField # :nodoc:
  6. def render
  7. @options = { value: nil }.merge!(@options)
  8. super
  9. end
  10. end
  11. end
  12. end
  13. end

lib/action_view/helpers/tags/placeholderable.rb

0.0% lines covered

20 relevant lines. 0 lines covered and 20 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView
  3. module Helpers
  4. module Tags # :nodoc:
  5. module Placeholderable # :nodoc:
  6. def initialize(*)
  7. super
  8. if tag_value = @options[:placeholder]
  9. placeholder = tag_value if tag_value.is_a?(String)
  10. method_and_value = tag_value.is_a?(TrueClass) ? @method_name : "#{@method_name}.#{tag_value}"
  11. placeholder ||= Tags::Translator
  12. .new(object, @object_name, method_and_value, scope: "helpers.placeholder")
  13. .translate
  14. placeholder ||= @method_name.humanize
  15. @options[:placeholder] = placeholder
  16. end
  17. end
  18. end
  19. end
  20. end
  21. end

lib/action_view/helpers/tags/radio_button.rb

0.0% lines covered

26 relevant lines. 0 lines covered and 26 lines missed.
    
  1. # frozen_string_literal: true
  2. require "action_view/helpers/tags/checkable"
  3. module ActionView
  4. module Helpers
  5. module Tags # :nodoc:
  6. class RadioButton < Base # :nodoc:
  7. include Checkable
  8. def initialize(object_name, method_name, template_object, tag_value, options)
  9. @tag_value = tag_value
  10. super(object_name, method_name, template_object, options)
  11. end
  12. def render
  13. options = @options.stringify_keys
  14. options["type"] = "radio"
  15. options["value"] = @tag_value
  16. options["checked"] = "checked" if input_checked?(options)
  17. add_default_name_and_id_for_value(@tag_value, options)
  18. tag("input", options)
  19. end
  20. private
  21. def checked?(value)
  22. value.to_s == @tag_value.to_s
  23. end
  24. end
  25. end
  26. end
  27. end

lib/action_view/helpers/tags/range_field.rb

0.0% lines covered

8 relevant lines. 0 lines covered and 8 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView
  3. module Helpers
  4. module Tags # :nodoc:
  5. class RangeField < NumberField # :nodoc:
  6. end
  7. end
  8. end
  9. end

lib/action_view/helpers/tags/search_field.rb

0.0% lines covered

22 relevant lines. 0 lines covered and 22 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView
  3. module Helpers
  4. module Tags # :nodoc:
  5. class SearchField < TextField # :nodoc:
  6. def render
  7. options = @options.stringify_keys
  8. if options["autosave"]
  9. if options["autosave"] == true
  10. options["autosave"] = request.host.split(".").reverse.join(".")
  11. end
  12. options["results"] ||= 10
  13. end
  14. if options["onsearch"]
  15. options["incremental"] = true unless options.has_key?("incremental")
  16. end
  17. @options = options
  18. super
  19. end
  20. end
  21. end
  22. end
  23. end

lib/action_view/helpers/tags/select.rb

0.0% lines covered

30 relevant lines. 0 lines covered and 30 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView
  3. module Helpers
  4. module Tags # :nodoc:
  5. class Select < Base # :nodoc:
  6. def initialize(object_name, method_name, template_object, choices, options, html_options)
  7. @choices = block_given? ? template_object.capture { yield || "" } : choices
  8. @choices = @choices.to_a if @choices.is_a?(Range)
  9. @html_options = html_options
  10. super(object_name, method_name, template_object, options)
  11. end
  12. def render
  13. option_tags_options = {
  14. selected: @options.fetch(:selected) { value.to_s },
  15. disabled: @options[:disabled]
  16. }
  17. option_tags = if grouped_choices?
  18. grouped_options_for_select(@choices, option_tags_options)
  19. else
  20. options_for_select(@choices, option_tags_options)
  21. end
  22. select_content_tag(option_tags, @options, @html_options)
  23. end
  24. private
  25. # Grouped choices look like this:
  26. #
  27. # [nil, []]
  28. # { nil => [] }
  29. def grouped_choices?
  30. !@choices.blank? && @choices.first.respond_to?(:last) && Array === @choices.first.last
  31. end
  32. end
  33. end
  34. end
  35. end

lib/action_view/helpers/tags/tel_field.rb

0.0% lines covered

8 relevant lines. 0 lines covered and 8 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView
  3. module Helpers
  4. module Tags # :nodoc:
  5. class TelField < TextField # :nodoc:
  6. end
  7. end
  8. end
  9. end

lib/action_view/helpers/tags/text_area.rb

0.0% lines covered

18 relevant lines. 0 lines covered and 18 lines missed.
    
  1. # frozen_string_literal: true
  2. require "action_view/helpers/tags/placeholderable"
  3. module ActionView
  4. module Helpers
  5. module Tags # :nodoc:
  6. class TextArea < Base # :nodoc:
  7. include Placeholderable
  8. def render
  9. options = @options.stringify_keys
  10. add_default_name_and_id(options)
  11. if size = options.delete("size")
  12. options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split)
  13. end
  14. content_tag("textarea", options.delete("value") { value_before_type_cast }, options)
  15. end
  16. end
  17. end
  18. end
  19. end

lib/action_view/helpers/tags/text_field.rb

0.0% lines covered

27 relevant lines. 0 lines covered and 27 lines missed.
    
  1. # frozen_string_literal: true
  2. require "action_view/helpers/tags/placeholderable"
  3. module ActionView
  4. module Helpers
  5. module Tags # :nodoc:
  6. class TextField < Base # :nodoc:
  7. include Placeholderable
  8. def render
  9. options = @options.stringify_keys
  10. options["size"] = options["maxlength"] unless options.key?("size")
  11. options["type"] ||= field_type
  12. options["value"] = options.fetch("value") { value_before_type_cast } unless field_type == "file"
  13. add_default_name_and_id(options)
  14. tag("input", options)
  15. end
  16. class << self
  17. def field_type
  18. @field_type ||= name.split("::").last.sub("Field", "").downcase
  19. end
  20. end
  21. private
  22. def field_type
  23. self.class.field_type
  24. end
  25. end
  26. end
  27. end
  28. end

lib/action_view/helpers/tags/time_field.rb

0.0% lines covered

12 relevant lines. 0 lines covered and 12 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView
  3. module Helpers
  4. module Tags # :nodoc:
  5. class TimeField < DatetimeField # :nodoc:
  6. private
  7. def format_date(value)
  8. value&.strftime("%T.%L")
  9. end
  10. end
  11. end
  12. end
  13. end

lib/action_view/helpers/tags/time_select.rb

0.0% lines covered

8 relevant lines. 0 lines covered and 8 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView
  3. module Helpers
  4. module Tags # :nodoc:
  5. class TimeSelect < DateSelect # :nodoc:
  6. end
  7. end
  8. end
  9. end

lib/action_view/helpers/tags/time_zone_select.rb

0.0% lines covered

18 relevant lines. 0 lines covered and 18 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView
  3. module Helpers
  4. module Tags # :nodoc:
  5. class TimeZoneSelect < Base # :nodoc:
  6. def initialize(object_name, method_name, template_object, priority_zones, options, html_options)
  7. @priority_zones = priority_zones
  8. @html_options = html_options
  9. super(object_name, method_name, template_object, options)
  10. end
  11. def render
  12. select_content_tag(
  13. time_zone_options_for_select(value || @options[:default], @priority_zones, @options[:model] || ActiveSupport::TimeZone), @options, @html_options
  14. )
  15. end
  16. end
  17. end
  18. end
  19. end

lib/action_view/helpers/tags/translator.rb

0.0% lines covered

33 relevant lines. 0 lines covered and 33 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView
  3. module Helpers
  4. module Tags # :nodoc:
  5. class Translator # :nodoc:
  6. def initialize(object, object_name, method_and_value, scope:)
  7. @object_name = object_name.gsub(/\[(.*)_attributes\]\[\d+\]/, '.\1')
  8. @method_and_value = method_and_value
  9. @scope = scope
  10. @model = object.respond_to?(:to_model) ? object.to_model : nil
  11. end
  12. def translate
  13. translated_attribute = I18n.t("#{object_name}.#{method_and_value}", default: i18n_default, scope: scope).presence
  14. translated_attribute || human_attribute_name
  15. end
  16. private
  17. attr_reader :object_name, :method_and_value, :scope, :model
  18. def i18n_default
  19. if model
  20. key = model.model_name.i18n_key
  21. ["#{key}.#{method_and_value}".to_sym, ""]
  22. else
  23. ""
  24. end
  25. end
  26. def human_attribute_name
  27. if model && model.class.respond_to?(:human_attribute_name)
  28. model.class.human_attribute_name(method_and_value)
  29. end
  30. end
  31. end
  32. end
  33. end
  34. end

lib/action_view/helpers/tags/url_field.rb

0.0% lines covered

8 relevant lines. 0 lines covered and 8 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView
  3. module Helpers
  4. module Tags # :nodoc:
  5. class UrlField < TextField # :nodoc:
  6. end
  7. end
  8. end
  9. end

lib/action_view/helpers/tags/week_field.rb

0.0% lines covered

12 relevant lines. 0 lines covered and 12 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView
  3. module Helpers
  4. module Tags # :nodoc:
  5. class WeekField < DatetimeField # :nodoc:
  6. private
  7. def format_date(value)
  8. value&.strftime("%Y-W%V")
  9. end
  10. end
  11. end
  12. end
  13. end

lib/action_view/helpers/text_helper.rb

28.69% lines covered

122 relevant lines. 35 lines covered and 87 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 require "active_support/core_ext/string/filters"
  3. 9 require "active_support/core_ext/array/extract_options"
  4. 9 module ActionView
  5. # = Action View Text Helpers
  6. 9 module Helpers #:nodoc:
  7. # The TextHelper module provides a set of methods for filtering, formatting
  8. # and transforming strings, which can reduce the amount of inline Ruby code in
  9. # your views. These helper methods extend Action View making them callable
  10. # within your template files.
  11. #
  12. # ==== Sanitization
  13. #
  14. # Most text helpers that generate HTML output sanitize the given input by default,
  15. # but do not escape it. This means HTML tags will appear in the page but all malicious
  16. # code will be removed. Let's look at some examples using the +simple_format+ method:
  17. #
  18. # simple_format('<a href="http://example.com/">Example</a>')
  19. # # => "<p><a href=\"http://example.com/\">Example</a></p>"
  20. #
  21. # simple_format('<a href="javascript:alert(\'no!\')">Example</a>')
  22. # # => "<p><a>Example</a></p>"
  23. #
  24. # If you want to escape all content, you should invoke the +h+ method before
  25. # calling the text helper.
  26. #
  27. # simple_format h('<a href="http://example.com/">Example</a>')
  28. # # => "<p>&lt;a href=\"http://example.com/\"&gt;Example&lt;/a&gt;</p>"
  29. 9 module TextHelper
  30. 9 extend ActiveSupport::Concern
  31. 9 include SanitizeHelper
  32. 9 include TagHelper
  33. 9 include OutputSafetyHelper
  34. # The preferred method of outputting text in your views is to use the
  35. # <%= "text" %> eRuby syntax. The regular _puts_ and _print_ methods
  36. # do not operate as expected in an eRuby code block. If you absolutely must
  37. # output text within a non-output code block (i.e., <% %>), you can use the concat method.
  38. #
  39. # <%
  40. # concat "hello"
  41. # # is the equivalent of <%= "hello" %>
  42. #
  43. # if logged_in
  44. # concat "Logged in!"
  45. # else
  46. # concat link_to('login', action: :login)
  47. # end
  48. # # will either display "Logged in!" or a login link
  49. # %>
  50. 9 def concat(string)
  51. output_buffer << string
  52. end
  53. 9 def safe_concat(string)
  54. output_buffer.respond_to?(:safe_concat) ? output_buffer.safe_concat(string) : concat(string)
  55. end
  56. # Truncates a given +text+ after a given <tt>:length</tt> if +text+ is longer than <tt>:length</tt>
  57. # (defaults to 30). The last characters will be replaced with the <tt>:omission</tt> (defaults to "...")
  58. # for a total length not exceeding <tt>:length</tt>.
  59. #
  60. # Pass a <tt>:separator</tt> to truncate +text+ at a natural break.
  61. #
  62. # Pass a block if you want to show extra content when the text is truncated.
  63. #
  64. # The result is marked as HTML-safe, but it is escaped by default, unless <tt>:escape</tt> is
  65. # +false+. Care should be taken if +text+ contains HTML tags or entities, because truncation
  66. # may produce invalid HTML (such as unbalanced or incomplete tags).
  67. #
  68. # truncate("Once upon a time in a world far far away")
  69. # # => "Once upon a time in a world..."
  70. #
  71. # truncate("Once upon a time in a world far far away", length: 17)
  72. # # => "Once upon a ti..."
  73. #
  74. # truncate("Once upon a time in a world far far away", length: 17, separator: ' ')
  75. # # => "Once upon a..."
  76. #
  77. # truncate("And they found that many people were sleeping better.", length: 25, omission: '... (continued)')
  78. # # => "And they f... (continued)"
  79. #
  80. # truncate("<p>Once upon a time in a world far far away</p>")
  81. # # => "&lt;p&gt;Once upon a time in a wo..."
  82. #
  83. # truncate("<p>Once upon a time in a world far far away</p>", escape: false)
  84. # # => "<p>Once upon a time in a wo..."
  85. #
  86. # truncate("Once upon a time in a world far far away") { link_to "Continue", "#" }
  87. # # => "Once upon a time in a wo...<a href="#">Continue</a>"
  88. 9 def truncate(text, options = {}, &block)
  89. if text
  90. length = options.fetch(:length, 30)
  91. content = text.truncate(length, options)
  92. content = options[:escape] == false ? content.html_safe : ERB::Util.html_escape(content)
  93. content << capture(&block) if block_given? && text.length > length
  94. content
  95. end
  96. end
  97. # Highlights one or more +phrases+ everywhere in +text+ by inserting it into
  98. # a <tt>:highlighter</tt> string. The highlighter can be specialized by passing <tt>:highlighter</tt>
  99. # as a single-quoted string with <tt>\1</tt> where the phrase is to be inserted (defaults to
  100. # '<mark>\1</mark>') or passing a block that receives each matched term. By default +text+
  101. # is sanitized to prevent possible XSS attacks. If the input is trustworthy, passing false
  102. # for <tt>:sanitize</tt> will turn sanitizing off.
  103. #
  104. # highlight('You searched for: rails', 'rails')
  105. # # => You searched for: <mark>rails</mark>
  106. #
  107. # highlight('You searched for: rails', /for|rails/)
  108. # # => You searched <mark>for</mark>: <mark>rails</mark>
  109. #
  110. # highlight('You searched for: ruby, rails, dhh', 'actionpack')
  111. # # => You searched for: ruby, rails, dhh
  112. #
  113. # highlight('You searched for: rails', ['for', 'rails'], highlighter: '<em>\1</em>')
  114. # # => You searched <em>for</em>: <em>rails</em>
  115. #
  116. # highlight('You searched for: rails', 'rails', highlighter: '<a href="search?q=\1">\1</a>')
  117. # # => You searched for: <a href="search?q=rails">rails</a>
  118. #
  119. # highlight('You searched for: rails', 'rails') { |match| link_to(search_path(q: match, match)) }
  120. # # => You searched for: <a href="search?q=rails">rails</a>
  121. #
  122. # highlight('<a href="javascript:alert(\'no!\')">ruby</a> on rails', 'rails', sanitize: false)
  123. # # => <a href="javascript:alert('no!')">ruby</a> on <mark>rails</mark>
  124. 9 def highlight(text, phrases, options = {})
  125. text = sanitize(text) if options.fetch(:sanitize, true)
  126. if text.blank? || phrases.blank?
  127. text || ""
  128. else
  129. match = Array(phrases).map do |p|
  130. Regexp === p ? p.to_s : Regexp.escape(p)
  131. end.join("|")
  132. if block_given?
  133. text.gsub(/(#{match})(?![^<]*?>)/i) { |found| yield found }
  134. else
  135. highlighter = options.fetch(:highlighter, '<mark>\1</mark>')
  136. text.gsub(/(#{match})(?![^<]*?>)/i, highlighter)
  137. end
  138. end.html_safe
  139. end
  140. # Extracts an excerpt from +text+ that matches the first instance of +phrase+.
  141. # The <tt>:radius</tt> option expands the excerpt on each side of the first occurrence of +phrase+ by the number of characters
  142. # defined in <tt>:radius</tt> (which defaults to 100). If the excerpt radius overflows the beginning or end of the +text+,
  143. # then the <tt>:omission</tt> option (which defaults to "...") will be prepended/appended accordingly. Use the
  144. # <tt>:separator</tt> option to choose the delimitation. The resulting string will be stripped in any case. If the +phrase+
  145. # isn't found, +nil+ is returned.
  146. #
  147. # excerpt('This is an example', 'an', radius: 5)
  148. # # => ...s is an exam...
  149. #
  150. # excerpt('This is an example', 'is', radius: 5)
  151. # # => This is a...
  152. #
  153. # excerpt('This is an example', 'is')
  154. # # => This is an example
  155. #
  156. # excerpt('This next thing is an example', 'ex', radius: 2)
  157. # # => ...next...
  158. #
  159. # excerpt('This is also an example', 'an', radius: 8, omission: '<chop> ')
  160. # # => <chop> is also an example
  161. #
  162. # excerpt('This is a very beautiful morning', 'very', separator: ' ', radius: 1)
  163. # # => ...a very beautiful...
  164. 9 def excerpt(text, phrase, options = {})
  165. return unless text && phrase
  166. separator = options.fetch(:separator, nil) || ""
  167. case phrase
  168. when Regexp
  169. regex = phrase
  170. else
  171. regex = /#{Regexp.escape(phrase)}/i
  172. end
  173. return unless matches = text.match(regex)
  174. phrase = matches[0]
  175. unless separator.empty?
  176. text.split(separator).each do |value|
  177. if value.match?(regex)
  178. phrase = value
  179. break
  180. end
  181. end
  182. end
  183. first_part, second_part = text.split(phrase, 2)
  184. prefix, first_part = cut_excerpt_part(:first, first_part, separator, options)
  185. postfix, second_part = cut_excerpt_part(:second, second_part, separator, options)
  186. affix = [first_part, separator, phrase, separator, second_part].join.strip
  187. [prefix, affix, postfix].join
  188. end
  189. # Attempts to pluralize the +singular+ word unless +count+ is 1. If
  190. # +plural+ is supplied, it will use that when count is > 1, otherwise
  191. # it will use the Inflector to determine the plural form for the given locale,
  192. # which defaults to I18n.locale
  193. #
  194. # The word will be pluralized using rules defined for the locale
  195. # (you must define your own inflection rules for languages other than English).
  196. # See ActiveSupport::Inflector.pluralize
  197. #
  198. # pluralize(1, 'person')
  199. # # => 1 person
  200. #
  201. # pluralize(2, 'person')
  202. # # => 2 people
  203. #
  204. # pluralize(3, 'person', plural: 'users')
  205. # # => 3 users
  206. #
  207. # pluralize(0, 'person')
  208. # # => 0 people
  209. #
  210. # pluralize(2, 'Person', locale: :de)
  211. # # => 2 Personen
  212. 9 def pluralize(count, singular, plural_arg = nil, plural: plural_arg, locale: I18n.locale)
  213. word = if count == 1 || count.to_s.match?(/^1(\.0+)?$/)
  214. singular
  215. else
  216. plural || singular.pluralize(locale)
  217. end
  218. "#{count || 0} #{word}"
  219. end
  220. # Wraps the +text+ into lines no longer than +line_width+ width. This method
  221. # breaks on the first whitespace character that does not exceed +line_width+
  222. # (which is 80 by default).
  223. #
  224. # word_wrap('Once upon a time')
  225. # # => Once upon a time
  226. #
  227. # word_wrap('Once upon a time, in a kingdom called Far Far Away, a king fell ill, and finding a successor to the throne turned out to be more trouble than anyone could have imagined...')
  228. # # => Once upon a time, in a kingdom called Far Far Away, a king fell ill, and finding\na successor to the throne turned out to be more trouble than anyone could have\nimagined...
  229. #
  230. # word_wrap('Once upon a time', line_width: 8)
  231. # # => Once\nupon a\ntime
  232. #
  233. # word_wrap('Once upon a time', line_width: 1)
  234. # # => Once\nupon\na\ntime
  235. #
  236. # You can also specify a custom +break_sequence+ ("\n" by default)
  237. #
  238. # word_wrap('Once upon a time', line_width: 1, break_sequence: "\r\n")
  239. # # => Once\r\nupon\r\na\r\ntime
  240. 9 def word_wrap(text, line_width: 80, break_sequence: "\n")
  241. text.split("\n").collect! do |line|
  242. line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1#{break_sequence}").rstrip : line
  243. end * break_sequence
  244. end
  245. # Returns +text+ transformed into HTML using simple formatting rules.
  246. # Two or more consecutive newlines(<tt>\n\n</tt> or <tt>\r\n\r\n</tt>) are
  247. # considered a paragraph and wrapped in <tt><p></tt> tags. One newline
  248. # (<tt>\n</tt> or <tt>\r\n</tt>) is considered a linebreak and a
  249. # <tt><br /></tt> tag is appended. This method does not remove the
  250. # newlines from the +text+.
  251. #
  252. # You can pass any HTML attributes into <tt>html_options</tt>. These
  253. # will be added to all created paragraphs.
  254. #
  255. # ==== Options
  256. # * <tt>:sanitize</tt> - If +false+, does not sanitize +text+.
  257. # * <tt>:wrapper_tag</tt> - String representing the wrapper tag, defaults to <tt>"p"</tt>
  258. #
  259. # ==== Examples
  260. # my_text = "Here is some basic text...\n...with a line break."
  261. #
  262. # simple_format(my_text)
  263. # # => "<p>Here is some basic text...\n<br />...with a line break.</p>"
  264. #
  265. # simple_format(my_text, {}, wrapper_tag: "div")
  266. # # => "<div>Here is some basic text...\n<br />...with a line break.</div>"
  267. #
  268. # more_text = "We want to put a paragraph...\n\n...right there."
  269. #
  270. # simple_format(more_text)
  271. # # => "<p>We want to put a paragraph...</p>\n\n<p>...right there.</p>"
  272. #
  273. # simple_format("Look ma! A class!", class: 'description')
  274. # # => "<p class='description'>Look ma! A class!</p>"
  275. #
  276. # simple_format("<blink>Unblinkable.</blink>")
  277. # # => "<p>Unblinkable.</p>"
  278. #
  279. # simple_format("<blink>Blinkable!</blink> It's true.", {}, sanitize: false)
  280. # # => "<p><blink>Blinkable!</blink> It's true.</p>"
  281. 9 def simple_format(text, html_options = {}, options = {})
  282. wrapper_tag = options.fetch(:wrapper_tag, :p)
  283. text = sanitize(text) if options.fetch(:sanitize, true)
  284. paragraphs = split_paragraphs(text)
  285. if paragraphs.empty?
  286. content_tag(wrapper_tag, nil, html_options)
  287. else
  288. paragraphs.map! { |paragraph|
  289. content_tag(wrapper_tag, raw(paragraph), html_options)
  290. }.join("\n\n").html_safe
  291. end
  292. end
  293. # Creates a Cycle object whose _to_s_ method cycles through elements of an
  294. # array every time it is called. This can be used for example, to alternate
  295. # classes for table rows. You can use named cycles to allow nesting in loops.
  296. # Passing a Hash as the last parameter with a <tt>:name</tt> key will create a
  297. # named cycle. The default name for a cycle without a +:name+ key is
  298. # <tt>"default"</tt>. You can manually reset a cycle by calling reset_cycle
  299. # and passing the name of the cycle. The current cycle string can be obtained
  300. # anytime using the current_cycle method.
  301. #
  302. # # Alternate CSS classes for even and odd numbers...
  303. # @items = [1,2,3,4]
  304. # <table>
  305. # <% @items.each do |item| %>
  306. # <tr class="<%= cycle("odd", "even") -%>">
  307. # <td><%= item %></td>
  308. # </tr>
  309. # <% end %>
  310. # </table>
  311. #
  312. #
  313. # # Cycle CSS classes for rows, and text colors for values within each row
  314. # @items = x = [{first: 'Robert', middle: 'Daniel', last: 'James'},
  315. # {first: 'Emily', middle: 'Shannon', maiden: 'Pike', last: 'Hicks'},
  316. # {first: 'June', middle: 'Dae', last: 'Jones'}]
  317. # <% @items.each do |item| %>
  318. # <tr class="<%= cycle("odd", "even", name: "row_class") -%>">
  319. # <td>
  320. # <% item.values.each do |value| %>
  321. # <%# Create a named cycle "colors" %>
  322. # <span style="color:<%= cycle("red", "green", "blue", name: "colors") -%>">
  323. # <%= value %>
  324. # </span>
  325. # <% end %>
  326. # <% reset_cycle("colors") %>
  327. # </td>
  328. # </tr>
  329. # <% end %>
  330. 9 def cycle(first_value, *values)
  331. options = values.extract_options!
  332. name = options.fetch(:name, "default")
  333. values.unshift(*first_value)
  334. cycle = get_cycle(name)
  335. unless cycle && cycle.values == values
  336. cycle = set_cycle(name, Cycle.new(*values))
  337. end
  338. cycle.to_s
  339. end
  340. # Returns the current cycle string after a cycle has been started. Useful
  341. # for complex table highlighting or any other design need which requires
  342. # the current cycle string in more than one place.
  343. #
  344. # # Alternate background colors
  345. # @items = [1,2,3,4]
  346. # <% @items.each do |item| %>
  347. # <div style="background-color:<%= cycle("red","white","blue") %>">
  348. # <span style="background-color:<%= current_cycle %>"><%= item %></span>
  349. # </div>
  350. # <% end %>
  351. 9 def current_cycle(name = "default")
  352. cycle = get_cycle(name)
  353. cycle.current_value if cycle
  354. end
  355. # Resets a cycle so that it starts from the first element the next time
  356. # it is called. Pass in +name+ to reset a named cycle.
  357. #
  358. # # Alternate CSS classes for even and odd numbers...
  359. # @items = [[1,2,3,4], [5,6,3], [3,4,5,6,7,4]]
  360. # <table>
  361. # <% @items.each do |item| %>
  362. # <tr class="<%= cycle("even", "odd") -%>">
  363. # <% item.each do |value| %>
  364. # <span style="color:<%= cycle("#333", "#666", "#999", name: "colors") -%>">
  365. # <%= value %>
  366. # </span>
  367. # <% end %>
  368. #
  369. # <% reset_cycle("colors") %>
  370. # </tr>
  371. # <% end %>
  372. # </table>
  373. 9 def reset_cycle(name = "default")
  374. cycle = get_cycle(name)
  375. cycle.reset if cycle
  376. end
  377. 9 class Cycle #:nodoc:
  378. 9 attr_reader :values
  379. 9 def initialize(first_value, *values)
  380. @values = values.unshift(first_value)
  381. reset
  382. end
  383. 9 def reset
  384. @index = 0
  385. end
  386. 9 def current_value
  387. @values[previous_index].to_s
  388. end
  389. 9 def to_s
  390. value = @values[@index].to_s
  391. @index = next_index
  392. value
  393. end
  394. 9 private
  395. 9 def next_index
  396. step_index(1)
  397. end
  398. 9 def previous_index
  399. step_index(-1)
  400. end
  401. 9 def step_index(n)
  402. (@index + n) % @values.size
  403. end
  404. end
  405. 9 private
  406. # The cycle helpers need to store the cycles in a place that is
  407. # guaranteed to be reset every time a page is rendered, so it
  408. # uses an instance variable of ActionView::Base.
  409. 9 def get_cycle(name)
  410. @_cycles = Hash.new unless defined?(@_cycles)
  411. @_cycles[name]
  412. end
  413. 9 def set_cycle(name, cycle_object)
  414. @_cycles = Hash.new unless defined?(@_cycles)
  415. @_cycles[name] = cycle_object
  416. end
  417. 9 def split_paragraphs(text)
  418. return [] if text.blank?
  419. text.to_str.gsub(/\r\n?/, "\n").split(/\n\n+/).map! do |t|
  420. t.gsub!(/([^\n]\n)(?=[^\n])/, '\1<br />') || t
  421. end
  422. end
  423. 9 def cut_excerpt_part(part_position, part, separator, options)
  424. return "", "" unless part
  425. radius = options.fetch(:radius, 100)
  426. omission = options.fetch(:omission, "...")
  427. part = part.split(separator)
  428. part.delete("")
  429. affix = part.size > radius ? omission : ""
  430. part = if part_position == :first
  431. drop_index = [part.length - radius, 0].max
  432. part.drop(drop_index)
  433. else
  434. part.first(radius)
  435. end
  436. return affix, part.join(separator)
  437. end
  438. end
  439. end
  440. end

lib/action_view/helpers/translation_helper.rb

30.91% lines covered

55 relevant lines. 17 lines covered and 38 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 require "action_view/helpers/tag_helper"
  3. 9 require "active_support/core_ext/string/access"
  4. 9 require "i18n/exceptions"
  5. 9 module ActionView
  6. # = Action View Translation Helpers
  7. 9 module Helpers #:nodoc:
  8. 9 module TranslationHelper
  9. 9 extend ActiveSupport::Concern
  10. 9 include TagHelper
  11. 9 included do
  12. 12 mattr_accessor :debug_missing_translation, default: true
  13. end
  14. # Delegates to <tt>I18n#translate</tt> but also performs three additional
  15. # functions.
  16. #
  17. # First, it will ensure that any thrown +MissingTranslation+ messages will
  18. # be rendered as inline spans that:
  19. #
  20. # * Have a <tt>translation-missing</tt> class applied
  21. # * Contain the missing key as the value of the +title+ attribute
  22. # * Have a titleized version of the last key segment as text
  23. #
  24. # For example, the value returned for the missing translation key
  25. # <tt>"blog.post.title"</tt> will be:
  26. #
  27. # <span
  28. # class="translation_missing"
  29. # title="translation missing: en.blog.post.title">Title</span>
  30. #
  31. # This allows for views to display rather reasonable strings while still
  32. # giving developers a way to find missing translations.
  33. #
  34. # If you would prefer missing translations to raise an error, you can
  35. # opt out of span-wrapping behavior globally by setting
  36. # <tt>ActionView::Base.raise_on_missing_translations = true</tt> or
  37. # individually by passing <tt>raise: true</tt> as an option to
  38. # <tt>translate</tt>.
  39. #
  40. # Second, if the key starts with a period <tt>translate</tt> will scope
  41. # the key by the current partial. Calling <tt>translate(".foo")</tt> from
  42. # the <tt>people/index.html.erb</tt> template is equivalent to calling
  43. # <tt>translate("people.index.foo")</tt>. This makes it less
  44. # repetitive to translate many keys within the same partial and provides
  45. # a convention to scope keys consistently.
  46. #
  47. # Third, the translation will be marked as <tt>html_safe</tt> if the key
  48. # has the suffix "_html" or the last element of the key is "html". Calling
  49. # <tt>translate("footer_html")</tt> or <tt>translate("footer.html")</tt>
  50. # will return an HTML safe string that won't be escaped by other HTML
  51. # helper methods. This naming convention helps to identify translations
  52. # that include HTML tags so that you know what kind of output to expect
  53. # when you call translate in a template and translators know which keys
  54. # they can provide HTML values for.
  55. 9 def translate(key, **options)
  56. unless options[:default].nil?
  57. remaining_defaults = Array.wrap(options.delete(:default)).compact
  58. options[:default] = remaining_defaults unless remaining_defaults.first.kind_of?(Symbol)
  59. end
  60. # If the user has explicitly decided to NOT raise errors, pass that option to I18n.
  61. # Otherwise, tell I18n to raise an exception, which we rescue further in this method.
  62. # Note: `raise_error` refers to us re-raising the error in this method. I18n is forced to raise by default.
  63. if options[:raise] == false
  64. raise_error = false
  65. i18n_raise = false
  66. else
  67. raise_error = options[:raise] || ActionView::Base.raise_on_missing_translations
  68. i18n_raise = true
  69. end
  70. if html_safe_translation_key?(key)
  71. html_safe_options = options.dup
  72. options.except(*I18n::RESERVED_KEYS).each do |name, value|
  73. unless name == :count && value.is_a?(Numeric)
  74. html_safe_options[name] = ERB::Util.html_escape(value.to_s)
  75. end
  76. end
  77. translation = I18n.translate(scope_key_by_partial(key), **html_safe_options.merge(raise: i18n_raise))
  78. if translation.respond_to?(:map)
  79. translation.map { |element| element.respond_to?(:html_safe) ? element.html_safe : element }
  80. else
  81. translation.respond_to?(:html_safe) ? translation.html_safe : translation
  82. end
  83. else
  84. I18n.translate(scope_key_by_partial(key), **options.merge(raise: i18n_raise))
  85. end
  86. rescue I18n::MissingTranslationData => e
  87. if remaining_defaults.present?
  88. translate remaining_defaults.shift, **options.merge(default: remaining_defaults)
  89. else
  90. raise e if raise_error
  91. keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope])
  92. title = +"translation missing: #{keys.join('.')}"
  93. interpolations = options.except(:default, :scope)
  94. if interpolations.any?
  95. title << ", " << interpolations.map { |k, v| "#{k}: #{ERB::Util.html_escape(v)}" }.join(", ")
  96. end
  97. return title unless ActionView::Base.debug_missing_translation
  98. content_tag("span", keys.last.to_s.titleize, class: "translation_missing", title: title)
  99. end
  100. end
  101. 9 alias :t :translate
  102. # Delegates to <tt>I18n.localize</tt> with no additional functionality.
  103. #
  104. # See https://www.rubydoc.info/github/svenfuchs/i18n/master/I18n/Backend/Base:localize
  105. # for more information.
  106. 9 def localize(object, **options)
  107. I18n.localize(object, **options)
  108. end
  109. 9 alias :l :localize
  110. 9 private
  111. 9 def scope_key_by_partial(key)
  112. stringified_key = key.to_s
  113. if stringified_key.start_with?(".")
  114. if @current_template&.virtual_path
  115. @_scope_key_by_partial_cache ||= {}
  116. @_scope_key_by_partial_cache[@current_template.virtual_path] ||= @current_template.virtual_path.gsub(%r{/_?}, ".")
  117. "#{@_scope_key_by_partial_cache[@current_template.virtual_path]}#{stringified_key}"
  118. else
  119. raise "Cannot use t(#{key.inspect}) shortcut because path is not available"
  120. end
  121. else
  122. key
  123. end
  124. end
  125. 9 def html_safe_translation_key?(key)
  126. /(?:_|\b)html\z/.match?(key.to_s)
  127. end
  128. end
  129. end
  130. end

lib/action_view/helpers/url_helper.rb

22.5% lines covered

160 relevant lines. 36 lines covered and 124 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 require "action_view/helpers/javascript_helper"
  3. 9 require "active_support/core_ext/array/access"
  4. 9 require "active_support/core_ext/hash/keys"
  5. 9 require "active_support/core_ext/string/output_safety"
  6. 9 module ActionView
  7. # = Action View URL Helpers
  8. 9 module Helpers #:nodoc:
  9. # Provides a set of methods for making links and getting URLs that
  10. # depend on the routing subsystem (see ActionDispatch::Routing).
  11. # This allows you to use the same format for links in views
  12. # and controllers.
  13. 9 module UrlHelper
  14. # This helper may be included in any class that includes the
  15. # URL helpers of a routes (routes.url_helpers). Some methods
  16. # provided here will only work in the context of a request
  17. # (link_to_unless_current, for instance), which must be provided
  18. # as a method called #request on the context.
  19. 9 BUTTON_TAG_METHOD_VERBS = %w{patch put delete}
  20. 9 extend ActiveSupport::Concern
  21. 9 include TagHelper
  22. 9 module ClassMethods
  23. 9 def _url_for_modules
  24. 3 ActionView::RoutingUrlFor
  25. end
  26. end
  27. # Basic implementation of url_for to allow use helpers without routes existence
  28. 9 def url_for(options = nil) # :nodoc:
  29. case options
  30. when String
  31. options
  32. when :back
  33. _back_url
  34. else
  35. raise ArgumentError, "arguments passed to url_for can't be handled. Please require " \
  36. "routes or provide your own implementation"
  37. end
  38. end
  39. 9 def _back_url # :nodoc:
  40. _filtered_referrer || "javascript:history.back()"
  41. end
  42. 9 private :_back_url
  43. 9 def _filtered_referrer # :nodoc:
  44. if controller.respond_to?(:request)
  45. referrer = controller.request.env["HTTP_REFERER"]
  46. if referrer && URI(referrer).scheme != "javascript"
  47. referrer
  48. end
  49. end
  50. rescue URI::InvalidURIError
  51. end
  52. 9 private :_filtered_referrer
  53. # Creates an anchor element of the given +name+ using a URL created by the set of +options+.
  54. # See the valid options in the documentation for +url_for+. It's also possible to
  55. # pass a \String instead of an options hash, which generates an anchor element that uses the
  56. # value of the \String as the href for the link. Using a <tt>:back</tt> \Symbol instead
  57. # of an options hash will generate a link to the referrer (a JavaScript back link
  58. # will be used in place of a referrer if none exists). If +nil+ is passed as the name
  59. # the value of the link itself will become the name.
  60. #
  61. # ==== Signatures
  62. #
  63. # link_to(body, url, html_options = {})
  64. # # url is a String; you can use URL helpers like
  65. # # posts_path
  66. #
  67. # link_to(body, url_options = {}, html_options = {})
  68. # # url_options, except :method, is passed to url_for
  69. #
  70. # link_to(options = {}, html_options = {}) do
  71. # # name
  72. # end
  73. #
  74. # link_to(url, html_options = {}) do
  75. # # name
  76. # end
  77. #
  78. # ==== Options
  79. # * <tt>:data</tt> - This option can be used to add custom data attributes.
  80. # * <tt>method: symbol of HTTP verb</tt> - This modifier will dynamically
  81. # create an HTML form and immediately submit the form for processing using
  82. # the HTTP verb specified. Useful for having links perform a POST operation
  83. # in dangerous actions like deleting a record (which search bots can follow
  84. # while spidering your site). Supported verbs are <tt>:post</tt>, <tt>:delete</tt>, <tt>:patch</tt>, and <tt>:put</tt>.
  85. # Note that if the user has JavaScript disabled, the request will fall back
  86. # to using GET. If <tt>href: '#'</tt> is used and the user has JavaScript
  87. # disabled clicking the link will have no effect. If you are relying on the
  88. # POST behavior, you should check for it in your controller's action by using
  89. # the request object's methods for <tt>post?</tt>, <tt>delete?</tt>, <tt>patch?</tt>, or <tt>put?</tt>.
  90. # * <tt>remote: true</tt> - This will allow the unobtrusive JavaScript
  91. # driver to make an Ajax request to the URL in question instead of following
  92. # the link. The drivers each provide mechanisms for listening for the
  93. # completion of the Ajax request and performing JavaScript operations once
  94. # they're complete
  95. #
  96. # ==== Data attributes
  97. #
  98. # * <tt>confirm: 'question?'</tt> - This will allow the unobtrusive JavaScript
  99. # driver to prompt with the question specified (in this case, the
  100. # resulting text would be <tt>question?</tt>. If the user accepts, the
  101. # link is processed normally, otherwise no action is taken.
  102. # * <tt>:disable_with</tt> - Value of this parameter will be used as the
  103. # name for a disabled version of the link. This feature is provided by
  104. # the unobtrusive JavaScript driver.
  105. #
  106. # ==== Examples
  107. # Because it relies on +url_for+, +link_to+ supports both older-style controller/action/id arguments
  108. # and newer RESTful routes. Current Rails style favors RESTful routes whenever possible, so base
  109. # your application on resources and use
  110. #
  111. # link_to "Profile", profile_path(@profile)
  112. # # => <a href="/profiles/1">Profile</a>
  113. #
  114. # or the even pithier
  115. #
  116. # link_to "Profile", @profile
  117. # # => <a href="/profiles/1">Profile</a>
  118. #
  119. # in place of the older more verbose, non-resource-oriented
  120. #
  121. # link_to "Profile", controller: "profiles", action: "show", id: @profile
  122. # # => <a href="/profiles/show/1">Profile</a>
  123. #
  124. # Similarly,
  125. #
  126. # link_to "Profiles", profiles_path
  127. # # => <a href="/profiles">Profiles</a>
  128. #
  129. # is better than
  130. #
  131. # link_to "Profiles", controller: "profiles"
  132. # # => <a href="/profiles">Profiles</a>
  133. #
  134. # When name is +nil+ the href is presented instead
  135. #
  136. # link_to nil, "http://example.com"
  137. # # => <a href="http://www.example.com">http://www.example.com</a>
  138. #
  139. # You can use a block as well if your link target is hard to fit into the name parameter. ERB example:
  140. #
  141. # <%= link_to(@profile) do %>
  142. # <strong><%= @profile.name %></strong> -- <span>Check it out!</span>
  143. # <% end %>
  144. # # => <a href="/profiles/1">
  145. # <strong>David</strong> -- <span>Check it out!</span>
  146. # </a>
  147. #
  148. # Classes and ids for CSS are easy to produce:
  149. #
  150. # link_to "Articles", articles_path, id: "news", class: "article"
  151. # # => <a href="/articles" class="article" id="news">Articles</a>
  152. #
  153. # Be careful when using the older argument style, as an extra literal hash is needed:
  154. #
  155. # link_to "Articles", { controller: "articles" }, id: "news", class: "article"
  156. # # => <a href="/articles" class="article" id="news">Articles</a>
  157. #
  158. # Leaving the hash off gives the wrong link:
  159. #
  160. # link_to "WRONG!", controller: "articles", id: "news", class: "article"
  161. # # => <a href="/articles/index/news?class=article">WRONG!</a>
  162. #
  163. # +link_to+ can also produce links with anchors or query strings:
  164. #
  165. # link_to "Comment wall", profile_path(@profile, anchor: "wall")
  166. # # => <a href="/profiles/1#wall">Comment wall</a>
  167. #
  168. # link_to "Ruby on Rails search", controller: "searches", query: "ruby on rails"
  169. # # => <a href="/searches?query=ruby+on+rails">Ruby on Rails search</a>
  170. #
  171. # link_to "Nonsense search", searches_path(foo: "bar", baz: "quux")
  172. # # => <a href="/searches?foo=bar&amp;baz=quux">Nonsense search</a>
  173. #
  174. # The only option specific to +link_to+ (<tt>:method</tt>) is used as follows:
  175. #
  176. # link_to("Destroy", "http://www.example.com", method: :delete)
  177. # # => <a href='http://www.example.com' rel="nofollow" data-method="delete">Destroy</a>
  178. #
  179. # You can also use custom data attributes using the <tt>:data</tt> option:
  180. #
  181. # link_to "Visit Other Site", "http://www.rubyonrails.org/", data: { confirm: "Are you sure?" }
  182. # # => <a href="http://www.rubyonrails.org/" data-confirm="Are you sure?">Visit Other Site</a>
  183. #
  184. # Also you can set any link attributes such as <tt>target</tt>, <tt>rel</tt>, <tt>type</tt>:
  185. #
  186. # link_to "External link", "http://www.rubyonrails.org/", target: "_blank", rel: "nofollow"
  187. # # => <a href="http://www.rubyonrails.org/" target="_blank" rel="nofollow">External link</a>
  188. 9 def link_to(name = nil, options = nil, html_options = nil, &block)
  189. html_options, options, name = options, name, block if block_given?
  190. options ||= {}
  191. html_options = convert_options_to_data_attributes(options, html_options)
  192. url = url_for(options)
  193. html_options["href"] ||= url
  194. content_tag("a", name || url, html_options, &block)
  195. end
  196. # Generates a form containing a single button that submits to the URL created
  197. # by the set of +options+. This is the safest method to ensure links that
  198. # cause changes to your data are not triggered by search bots or accelerators.
  199. # If the HTML button does not work with your layout, you can also consider
  200. # using the +link_to+ method with the <tt>:method</tt> modifier as described in
  201. # the +link_to+ documentation.
  202. #
  203. # By default, the generated form element has a class name of <tt>button_to</tt>
  204. # to allow styling of the form itself and its children. This can be changed
  205. # using the <tt>:form_class</tt> modifier within +html_options+. You can control
  206. # the form submission and input element behavior using +html_options+.
  207. # This method accepts the <tt>:method</tt> modifier described in the +link_to+ documentation.
  208. # If no <tt>:method</tt> modifier is given, it will default to performing a POST operation.
  209. # You can also disable the button by passing <tt>disabled: true</tt> in +html_options+.
  210. # If you are using RESTful routes, you can pass the <tt>:method</tt>
  211. # to change the HTTP verb used to submit the form.
  212. #
  213. # ==== Options
  214. # The +options+ hash accepts the same options as +url_for+.
  215. #
  216. # There are a few special +html_options+:
  217. # * <tt>:method</tt> - \Symbol of HTTP verb. Supported verbs are <tt>:post</tt>, <tt>:get</tt>,
  218. # <tt>:delete</tt>, <tt>:patch</tt>, and <tt>:put</tt>. By default it will be <tt>:post</tt>.
  219. # * <tt>:disabled</tt> - If set to true, it will generate a disabled button.
  220. # * <tt>:data</tt> - This option can be used to add custom data attributes.
  221. # * <tt>:remote</tt> - If set to true, will allow the Unobtrusive JavaScript drivers to control the
  222. # submit behavior. By default this behavior is an ajax submit.
  223. # * <tt>:form</tt> - This hash will be form attributes
  224. # * <tt>:form_class</tt> - This controls the class of the form within which the submit button will
  225. # be placed
  226. # * <tt>:params</tt> - \Hash of parameters to be rendered as hidden fields within the form.
  227. #
  228. # ==== Data attributes
  229. #
  230. # * <tt>:confirm</tt> - This will use the unobtrusive JavaScript driver to
  231. # prompt with the question specified. If the user accepts, the link is
  232. # processed normally, otherwise no action is taken.
  233. # * <tt>:disable_with</tt> - Value of this parameter will be
  234. # used as the value for a disabled version of the submit
  235. # button when the form is submitted. This feature is provided
  236. # by the unobtrusive JavaScript driver.
  237. #
  238. # ==== Examples
  239. # <%= button_to "New", action: "new" %>
  240. # # => "<form method="post" action="/controller/new" class="button_to">
  241. # # <input value="New" type="submit" />
  242. # # </form>"
  243. #
  244. # <%= button_to "New", new_article_path %>
  245. # # => "<form method="post" action="/articles/new" class="button_to">
  246. # # <input value="New" type="submit" />
  247. # # </form>"
  248. #
  249. # <%= button_to [:make_happy, @user] do %>
  250. # Make happy <strong><%= @user.name %></strong>
  251. # <% end %>
  252. # # => "<form method="post" action="/users/1/make_happy" class="button_to">
  253. # # <button type="submit">
  254. # # Make happy <strong><%= @user.name %></strong>
  255. # # </button>
  256. # # </form>"
  257. #
  258. # <%= button_to "New", { action: "new" }, form_class: "new-thing" %>
  259. # # => "<form method="post" action="/controller/new" class="new-thing">
  260. # # <input value="New" type="submit" />
  261. # # </form>"
  262. #
  263. #
  264. # <%= button_to "Create", { action: "create" }, remote: true, form: { "data-type" => "json" } %>
  265. # # => "<form method="post" action="/images/create" class="button_to" data-remote="true" data-type="json">
  266. # # <input value="Create" type="submit" />
  267. # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/>
  268. # # </form>"
  269. #
  270. #
  271. # <%= button_to "Delete Image", { action: "delete", id: @image.id },
  272. # method: :delete, data: { confirm: "Are you sure?" } %>
  273. # # => "<form method="post" action="/images/delete/1" class="button_to">
  274. # # <input type="hidden" name="_method" value="delete" />
  275. # # <input data-confirm='Are you sure?' value="Delete Image" type="submit" />
  276. # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/>
  277. # # </form>"
  278. #
  279. #
  280. # <%= button_to('Destroy', 'http://www.example.com',
  281. # method: :delete, remote: true, data: { confirm: 'Are you sure?', disable_with: 'loading...' }) %>
  282. # # => "<form class='button_to' method='post' action='http://www.example.com' data-remote='true'>
  283. # # <input name='_method' value='delete' type='hidden' />
  284. # # <input value='Destroy' type='submit' data-disable-with='loading...' data-confirm='Are you sure?' />
  285. # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/>
  286. # # </form>"
  287. # #
  288. 9 def button_to(name = nil, options = nil, html_options = nil, &block)
  289. html_options, options = options, name if block_given?
  290. options ||= {}
  291. html_options ||= {}
  292. html_options = html_options.stringify_keys
  293. url = options.is_a?(String) ? options : url_for(options)
  294. remote = html_options.delete("remote")
  295. params = html_options.delete("params")
  296. method = html_options.delete("method").to_s
  297. method_tag = BUTTON_TAG_METHOD_VERBS.include?(method) ? method_tag(method) : "".html_safe
  298. form_method = method == "get" ? "get" : "post"
  299. form_options = html_options.delete("form") || {}
  300. form_options[:class] ||= html_options.delete("form_class") || "button_to"
  301. form_options[:method] = form_method
  302. form_options[:action] = url
  303. form_options[:'data-remote'] = true if remote
  304. request_token_tag = if form_method == "post"
  305. request_method = method.empty? ? "post" : method
  306. token_tag(nil, form_options: { action: url, method: request_method })
  307. else
  308. ""
  309. end
  310. html_options = convert_options_to_data_attributes(options, html_options)
  311. html_options["type"] = "submit"
  312. button = if block_given?
  313. content_tag("button", html_options, &block)
  314. else
  315. html_options["value"] = name || url
  316. tag("input", html_options)
  317. end
  318. inner_tags = method_tag.safe_concat(button).safe_concat(request_token_tag)
  319. if params
  320. to_form_params(params).each do |param|
  321. inner_tags.safe_concat tag(:input, type: "hidden", name: param[:name], value: param[:value])
  322. end
  323. end
  324. content_tag("form", inner_tags, form_options)
  325. end
  326. # Creates a link tag of the given +name+ using a URL created by the set of
  327. # +options+ unless the current request URI is the same as the links, in
  328. # which case only the name is returned (or the given block is yielded, if
  329. # one exists). You can give +link_to_unless_current+ a block which will
  330. # specialize the default behavior (e.g., show a "Start Here" link rather
  331. # than the link's text).
  332. #
  333. # ==== Examples
  334. # Let's say you have a navigation menu...
  335. #
  336. # <ul id="navbar">
  337. # <li><%= link_to_unless_current("Home", { action: "index" }) %></li>
  338. # <li><%= link_to_unless_current("About Us", { action: "about" }) %></li>
  339. # </ul>
  340. #
  341. # If in the "about" action, it will render...
  342. #
  343. # <ul id="navbar">
  344. # <li><a href="/controller/index">Home</a></li>
  345. # <li>About Us</li>
  346. # </ul>
  347. #
  348. # ...but if in the "index" action, it will render:
  349. #
  350. # <ul id="navbar">
  351. # <li>Home</li>
  352. # <li><a href="/controller/about">About Us</a></li>
  353. # </ul>
  354. #
  355. # The implicit block given to +link_to_unless_current+ is evaluated if the current
  356. # action is the action given. So, if we had a comments page and wanted to render a
  357. # "Go Back" link instead of a link to the comments page, we could do something like this...
  358. #
  359. # <%=
  360. # link_to_unless_current("Comment", { controller: "comments", action: "new" }) do
  361. # link_to("Go back", { controller: "posts", action: "index" })
  362. # end
  363. # %>
  364. 9 def link_to_unless_current(name, options = {}, html_options = {}, &block)
  365. link_to_unless current_page?(options), name, options, html_options, &block
  366. end
  367. # Creates a link tag of the given +name+ using a URL created by the set of
  368. # +options+ unless +condition+ is true, in which case only the name is
  369. # returned. To specialize the default behavior (i.e., show a login link rather
  370. # than just the plaintext link text), you can pass a block that
  371. # accepts the name or the full argument list for +link_to_unless+.
  372. #
  373. # ==== Examples
  374. # <%= link_to_unless(@current_user.nil?, "Reply", { action: "reply" }) %>
  375. # # If the user is logged in...
  376. # # => <a href="/controller/reply/">Reply</a>
  377. #
  378. # <%=
  379. # link_to_unless(@current_user.nil?, "Reply", { action: "reply" }) do |name|
  380. # link_to(name, { controller: "accounts", action: "signup" })
  381. # end
  382. # %>
  383. # # If the user is logged in...
  384. # # => <a href="/controller/reply/">Reply</a>
  385. # # If not...
  386. # # => <a href="/accounts/signup">Reply</a>
  387. 9 def link_to_unless(condition, name, options = {}, html_options = {}, &block)
  388. link_to_if !condition, name, options, html_options, &block
  389. end
  390. # Creates a link tag of the given +name+ using a URL created by the set of
  391. # +options+ if +condition+ is true, otherwise only the name is
  392. # returned. To specialize the default behavior, you can pass a block that
  393. # accepts the name or the full argument list for +link_to_if+.
  394. #
  395. # ==== Examples
  396. # <%= link_to_if(@current_user.nil?, "Login", { controller: "sessions", action: "new" }) %>
  397. # # If the user isn't logged in...
  398. # # => <a href="/sessions/new/">Login</a>
  399. #
  400. # <%=
  401. # link_to_if(@current_user.nil?, "Login", { controller: "sessions", action: "new" }) do
  402. # link_to(@current_user.login, { controller: "accounts", action: "show", id: @current_user })
  403. # end
  404. # %>
  405. # # If the user isn't logged in...
  406. # # => <a href="/sessions/new/">Login</a>
  407. # # If they are logged in...
  408. # # => <a href="/accounts/show/3">my_username</a>
  409. 9 def link_to_if(condition, name, options = {}, html_options = {}, &block)
  410. if condition
  411. link_to(name, options, html_options)
  412. else
  413. if block_given?
  414. block.arity <= 1 ? capture(name, &block) : capture(name, options, html_options, &block)
  415. else
  416. ERB::Util.html_escape(name)
  417. end
  418. end
  419. end
  420. # Creates a mailto link tag to the specified +email_address+, which is
  421. # also used as the name of the link unless +name+ is specified. Additional
  422. # HTML attributes for the link can be passed in +html_options+.
  423. #
  424. # +mail_to+ has several methods for customizing the email itself by
  425. # passing special keys to +html_options+.
  426. #
  427. # ==== Options
  428. # * <tt>:subject</tt> - Preset the subject line of the email.
  429. # * <tt>:body</tt> - Preset the body of the email.
  430. # * <tt>:cc</tt> - Carbon Copy additional recipients on the email.
  431. # * <tt>:bcc</tt> - Blind Carbon Copy additional recipients on the email.
  432. # * <tt>:reply_to</tt> - Preset the Reply-To field of the email.
  433. #
  434. # ==== Obfuscation
  435. # Prior to Rails 4.0, +mail_to+ provided options for encoding the address
  436. # in order to hinder email harvesters. To take advantage of these options,
  437. # install the +actionview-encoded_mail_to+ gem.
  438. #
  439. # ==== Examples
  440. # mail_to "me@domain.com"
  441. # # => <a href="mailto:me@domain.com">me@domain.com</a>
  442. #
  443. # mail_to "me@domain.com", "My email"
  444. # # => <a href="mailto:me@domain.com">My email</a>
  445. #
  446. # mail_to "me@domain.com", "My email", cc: "ccaddress@domain.com",
  447. # subject: "This is an example email"
  448. # # => <a href="mailto:me@domain.com?cc=ccaddress@domain.com&subject=This%20is%20an%20example%20email">My email</a>
  449. #
  450. # You can use a block as well if your link target is hard to fit into the name parameter. ERB example:
  451. #
  452. # <%= mail_to "me@domain.com" do %>
  453. # <strong>Email me:</strong> <span>me@domain.com</span>
  454. # <% end %>
  455. # # => <a href="mailto:me@domain.com">
  456. # <strong>Email me:</strong> <span>me@domain.com</span>
  457. # </a>
  458. 9 def mail_to(email_address, name = nil, html_options = {}, &block)
  459. html_options, name = name, nil if block_given?
  460. html_options = (html_options || {}).stringify_keys
  461. extras = %w{ cc bcc body subject reply_to }.map! { |item|
  462. option = html_options.delete(item).presence || next
  463. "#{item.dasherize}=#{ERB::Util.url_encode(option)}"
  464. }.compact
  465. extras = extras.empty? ? "" : "?" + extras.join("&")
  466. encoded_email_address = ERB::Util.url_encode(email_address).gsub("%40", "@")
  467. html_options["href"] = "mailto:#{encoded_email_address}#{extras}"
  468. content_tag("a", name || email_address, html_options, &block)
  469. end
  470. # True if the current request URI was generated by the given +options+.
  471. #
  472. # ==== Examples
  473. # Let's say we're in the <tt>http://www.example.com/shop/checkout?order=desc&page=1</tt> action.
  474. #
  475. # current_page?(action: 'process')
  476. # # => false
  477. #
  478. # current_page?(action: 'checkout')
  479. # # => true
  480. #
  481. # current_page?(controller: 'library', action: 'checkout')
  482. # # => false
  483. #
  484. # current_page?(controller: 'shop', action: 'checkout')
  485. # # => true
  486. #
  487. # current_page?(controller: 'shop', action: 'checkout', order: 'asc')
  488. # # => false
  489. #
  490. # current_page?(controller: 'shop', action: 'checkout', order: 'desc', page: '1')
  491. # # => true
  492. #
  493. # current_page?(controller: 'shop', action: 'checkout', order: 'desc', page: '2')
  494. # # => false
  495. #
  496. # current_page?('http://www.example.com/shop/checkout')
  497. # # => true
  498. #
  499. # current_page?('http://www.example.com/shop/checkout', check_parameters: true)
  500. # # => false
  501. #
  502. # current_page?('/shop/checkout')
  503. # # => true
  504. #
  505. # current_page?('http://www.example.com/shop/checkout?order=desc&page=1')
  506. # # => true
  507. #
  508. # Let's say we're in the <tt>http://www.example.com/products</tt> action with method POST in case of invalid product.
  509. #
  510. # current_page?(controller: 'product', action: 'index')
  511. # # => false
  512. #
  513. # We can also pass in the symbol arguments instead of strings.
  514. #
  515. 9 def current_page?(options, check_parameters: false)
  516. unless request
  517. raise "You cannot use helpers that need to determine the current " \
  518. "page unless your view context provides a Request object " \
  519. "in a #request method"
  520. end
  521. return false unless request.get? || request.head?
  522. check_parameters ||= options.is_a?(Hash) && options.delete(:check_parameters)
  523. url_string = URI::DEFAULT_PARSER.unescape(url_for(options)).force_encoding(Encoding::BINARY)
  524. # We ignore any extra parameters in the request_uri if the
  525. # submitted URL doesn't have any either. This lets the function
  526. # work with things like ?order=asc
  527. # the behaviour can be disabled with check_parameters: true
  528. request_uri = url_string.index("?") || check_parameters ? request.fullpath : request.path
  529. request_uri = URI::DEFAULT_PARSER.unescape(request_uri).force_encoding(Encoding::BINARY)
  530. if url_string.start_with?("/") && url_string != "/"
  531. url_string.chomp!("/")
  532. request_uri.chomp!("/")
  533. end
  534. if %r{^\w+://}.match?(url_string)
  535. url_string == "#{request.protocol}#{request.host_with_port}#{request_uri}"
  536. else
  537. url_string == request_uri
  538. end
  539. end
  540. # Creates an SMS anchor link tag to the specified +phone_number+, which is
  541. # also used as the name of the link unless +name+ is specified. Additional
  542. # HTML attributes for the link can be passed in +html_options+.
  543. #
  544. # When clicked, an SMS message is prepopulated with the passed phone number
  545. # and optional +body+ value.
  546. #
  547. # +sms_to+ has a +body+ option for customizing the SMS message itself by
  548. # passing special keys to +html_options+.
  549. #
  550. # ==== Options
  551. # * <tt>:body</tt> - Preset the body of the message.
  552. #
  553. # ==== Examples
  554. # sms_to "5155555785"
  555. # # => <a href="sms:5155555785;">5155555785</a>
  556. #
  557. # sms_to "5155555785", "Text me"
  558. # # => <a href="sms:5155555785;">Text me</a>
  559. #
  560. # sms_to "5155555785", "Text me",
  561. # body: "Hello Jim I have a question about your product."
  562. # # => <a href="sms:5155555785;?body=Hello%20Jim%20I%20have%20a%20question%20about%20your%20product">Text me</a>
  563. #
  564. # You can use a block as well if your link target is hard to fit into the name parameter. \ERB example:
  565. #
  566. # <%= sms_to "5155555785" do %>
  567. # <strong>Text me:</strong>
  568. # <% end %>
  569. # # => <a href="sms:5155555785;">
  570. # <strong>Text me:</strong>
  571. # </a>
  572. 9 def sms_to(phone_number, name = nil, html_options = {}, &block)
  573. html_options, name = name, nil if block_given?
  574. html_options = (html_options || {}).stringify_keys
  575. extras = %w{ body }.map! { |item|
  576. option = html_options.delete(item).presence || next
  577. "#{item.dasherize}=#{ERB::Util.url_encode(option)}"
  578. }.compact
  579. extras = extras.empty? ? "" : "?&" + extras.join("&")
  580. encoded_phone_number = ERB::Util.url_encode(phone_number)
  581. html_options["href"] = "sms:#{encoded_phone_number};#{extras}"
  582. content_tag("a", name || phone_number, html_options, &block)
  583. end
  584. # Creates a TEL anchor link tag to the specified +phone_number+, which is
  585. # also used as the name of the link unless +name+ is specified. Additional
  586. # HTML attributes for the link can be passed in +html_options+.
  587. #
  588. # When clicked, the default app to make calls is opened, and it
  589. # is prepopulated with the passed phone number and optional
  590. # +country_code+ value.
  591. #
  592. # +phone_to+ has an optional +country_code+ option which automatically adds the country
  593. # code as well as the + sign in the phone numer that gets prepopulated,
  594. # for example if +country_code: "01"+ +\+01+ will be prepended to the
  595. # phone numer, by passing special keys to +html_options+.
  596. #
  597. # ==== Options
  598. # * <tt>:country_code</tt> - Prepends the country code to the number
  599. #
  600. # ==== Examples
  601. # phone_to "1234567890"
  602. # # => <a href="tel:1234567890">1234567890</a>
  603. #
  604. # phone_to "1234567890", "Phone me"
  605. # # => <a href="tel:134567890">Phone me</a>
  606. #
  607. # phone_to "1234567890", "Phone me", country_code: "01"
  608. # # => <a href="tel:+015155555785">Phone me</a>
  609. #
  610. # You can use a block as well if your link target is hard to fit into the name parameter. \ERB example:
  611. #
  612. # <%= phone_to "1234567890" do %>
  613. # <strong>Phone me:</strong>
  614. # <% end %>
  615. # # => <a href="tel:1234567890">
  616. # <strong>Phone me:</strong>
  617. # </a>
  618. 9 def phone_to(phone_number, name = nil, html_options = {}, &block)
  619. html_options, name = name, nil if block_given?
  620. html_options = (html_options || {}).stringify_keys
  621. country_code = html_options.delete("country_code").presence
  622. country_code = country_code.nil? ? "" : "+#{ERB::Util.url_encode(country_code)}"
  623. encoded_phone_number = ERB::Util.url_encode(phone_number)
  624. html_options["href"] = "tel:#{country_code}#{encoded_phone_number}"
  625. content_tag("a", name || phone_number, html_options, &block)
  626. end
  627. 9 private
  628. 9 def convert_options_to_data_attributes(options, html_options)
  629. if html_options
  630. html_options = html_options.stringify_keys
  631. html_options["data-remote"] = "true" if link_to_remote_options?(options) || link_to_remote_options?(html_options)
  632. method = html_options.delete("method")
  633. add_method_to_attributes!(html_options, method) if method
  634. html_options
  635. else
  636. link_to_remote_options?(options) ? { "data-remote" => "true" } : {}
  637. end
  638. end
  639. 9 def link_to_remote_options?(options)
  640. if options.is_a?(Hash)
  641. options.delete("remote") || options.delete(:remote)
  642. end
  643. end
  644. 9 def add_method_to_attributes!(html_options, method)
  645. if method_not_get_method?(method) && !html_options["rel"]&.match?(/nofollow/)
  646. if html_options["rel"].blank?
  647. html_options["rel"] = "nofollow"
  648. else
  649. html_options["rel"] = "#{html_options["rel"]} nofollow"
  650. end
  651. end
  652. html_options["data-method"] = method
  653. end
  654. 9 STRINGIFIED_COMMON_METHODS = {
  655. get: "get",
  656. delete: "delete",
  657. patch: "patch",
  658. post: "post",
  659. put: "put",
  660. }.freeze
  661. 9 def method_not_get_method?(method)
  662. return false unless method
  663. (STRINGIFIED_COMMON_METHODS[method] || method.to_s.downcase) != "get"
  664. end
  665. 9 def token_tag(token = nil, form_options: {})
  666. if token != false && defined?(protect_against_forgery?) && protect_against_forgery?
  667. token ||= form_authenticity_token(form_options: form_options)
  668. tag(:input, type: "hidden", name: request_forgery_protection_token.to_s, value: token)
  669. else
  670. ""
  671. end
  672. end
  673. 9 def method_tag(method)
  674. tag("input", type: "hidden", name: "_method", value: method.to_s)
  675. end
  676. # Returns an array of hashes each containing :name and :value keys
  677. # suitable for use as the names and values of form input fields:
  678. #
  679. # to_form_params(name: 'David', nationality: 'Danish')
  680. # # => [{name: 'name', value: 'David'}, {name: 'nationality', value: 'Danish'}]
  681. #
  682. # to_form_params(country: { name: 'Denmark' })
  683. # # => [{name: 'country[name]', value: 'Denmark'}]
  684. #
  685. # to_form_params(countries: ['Denmark', 'Sweden']})
  686. # # => [{name: 'countries[]', value: 'Denmark'}, {name: 'countries[]', value: 'Sweden'}]
  687. #
  688. # An optional namespace can be passed to enclose key names:
  689. #
  690. # to_form_params({ name: 'Denmark' }, 'country')
  691. # # => [{name: 'country[name]', value: 'Denmark'}]
  692. 9 def to_form_params(attribute, namespace = nil)
  693. attribute = if attribute.respond_to?(:permitted?)
  694. attribute.to_h
  695. else
  696. attribute
  697. end
  698. params = []
  699. case attribute
  700. when Hash
  701. attribute.each do |key, value|
  702. prefix = namespace ? "#{namespace}[#{key}]" : key
  703. params.push(*to_form_params(value, prefix))
  704. end
  705. when Array
  706. array_prefix = "#{namespace}[]"
  707. attribute.each do |value|
  708. params.push(*to_form_params(value, array_prefix))
  709. end
  710. else
  711. params << { name: namespace.to_s, value: attribute.to_param }
  712. end
  713. params.sort_by { |pair| pair[:name] }
  714. end
  715. end
  716. end
  717. end

lib/action_view/layouts.rb

63.1% lines covered

84 relevant lines. 53 lines covered and 31 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 require "action_view/rendering"
  3. 9 require "active_support/core_ext/module/redefine_method"
  4. 9 module ActionView
  5. # Layouts reverse the common pattern of including shared headers and footers in many templates to isolate changes in
  6. # repeated setups. The inclusion pattern has pages that look like this:
  7. #
  8. # <%= render "shared/header" %>
  9. # Hello World
  10. # <%= render "shared/footer" %>
  11. #
  12. # This approach is a decent way of keeping common structures isolated from the changing content, but it's verbose
  13. # and if you ever want to change the structure of these two includes, you'll have to change all the templates.
  14. #
  15. # With layouts, you can flip it around and have the common structure know where to insert changing content. This means
  16. # that the header and footer are only mentioned in one place, like this:
  17. #
  18. # // The header part of this layout
  19. # <%= yield %>
  20. # // The footer part of this layout
  21. #
  22. # And then you have content pages that look like this:
  23. #
  24. # hello world
  25. #
  26. # At rendering time, the content page is computed and then inserted in the layout, like this:
  27. #
  28. # // The header part of this layout
  29. # hello world
  30. # // The footer part of this layout
  31. #
  32. # == Accessing shared variables
  33. #
  34. # Layouts have access to variables specified in the content pages and vice versa. This allows you to have layouts with
  35. # references that won't materialize before rendering time:
  36. #
  37. # <h1><%= @page_title %></h1>
  38. # <%= yield %>
  39. #
  40. # ...and content pages that fulfill these references _at_ rendering time:
  41. #
  42. # <% @page_title = "Welcome" %>
  43. # Off-world colonies offers you a chance to start a new life
  44. #
  45. # The result after rendering is:
  46. #
  47. # <h1>Welcome</h1>
  48. # Off-world colonies offers you a chance to start a new life
  49. #
  50. # == Layout assignment
  51. #
  52. # You can either specify a layout declaratively (using the #layout class method) or give
  53. # it the same name as your controller, and place it in <tt>app/views/layouts</tt>.
  54. # If a subclass does not have a layout specified, it inherits its layout using normal Ruby inheritance.
  55. #
  56. # For instance, if you have PostsController and a template named <tt>app/views/layouts/posts.html.erb</tt>,
  57. # that template will be used for all actions in PostsController and controllers inheriting
  58. # from PostsController.
  59. #
  60. # If you use a module, for instance Weblog::PostsController, you will need a template named
  61. # <tt>app/views/layouts/weblog/posts.html.erb</tt>.
  62. #
  63. # Since all your controllers inherit from ApplicationController, they will use
  64. # <tt>app/views/layouts/application.html.erb</tt> if no other layout is specified
  65. # or provided.
  66. #
  67. # == Inheritance Examples
  68. #
  69. # class BankController < ActionController::Base
  70. # # bank.html.erb exists
  71. #
  72. # class ExchangeController < BankController
  73. # # exchange.html.erb exists
  74. #
  75. # class CurrencyController < BankController
  76. #
  77. # class InformationController < BankController
  78. # layout "information"
  79. #
  80. # class TellerController < InformationController
  81. # # teller.html.erb exists
  82. #
  83. # class EmployeeController < InformationController
  84. # # employee.html.erb exists
  85. # layout nil
  86. #
  87. # class VaultController < BankController
  88. # layout :access_level_layout
  89. #
  90. # class TillController < BankController
  91. # layout false
  92. #
  93. # In these examples, we have three implicit lookup scenarios:
  94. # * The +BankController+ uses the "bank" layout.
  95. # * The +ExchangeController+ uses the "exchange" layout.
  96. # * The +CurrencyController+ inherits the layout from BankController.
  97. #
  98. # However, when a layout is explicitly set, the explicitly set layout wins:
  99. # * The +InformationController+ uses the "information" layout, explicitly set.
  100. # * The +TellerController+ also uses the "information" layout, because the parent explicitly set it.
  101. # * The +EmployeeController+ uses the "employee" layout, because it set the layout to +nil+, resetting the parent configuration.
  102. # * The +VaultController+ chooses a layout dynamically by calling the <tt>access_level_layout</tt> method.
  103. # * The +TillController+ does not use a layout at all.
  104. #
  105. # == Types of layouts
  106. #
  107. # Layouts are basically just regular templates, but the name of this template needs not be specified statically. Sometimes
  108. # you want to alternate layouts depending on runtime information, such as whether someone is logged in or not. This can
  109. # be done either by specifying a method reference as a symbol or using an inline method (as a proc).
  110. #
  111. # The method reference is the preferred approach to variable layouts and is used like this:
  112. #
  113. # class WeblogController < ActionController::Base
  114. # layout :writers_and_readers
  115. #
  116. # def index
  117. # # fetching posts
  118. # end
  119. #
  120. # private
  121. # def writers_and_readers
  122. # logged_in? ? "writer_layout" : "reader_layout"
  123. # end
  124. # end
  125. #
  126. # Now when a new request for the index action is processed, the layout will vary depending on whether the person accessing
  127. # is logged in or not.
  128. #
  129. # If you want to use an inline method, such as a proc, do something like this:
  130. #
  131. # class WeblogController < ActionController::Base
  132. # layout proc { |controller| controller.logged_in? ? "writer_layout" : "reader_layout" }
  133. # end
  134. #
  135. # If an argument isn't given to the proc, it's evaluated in the context of
  136. # the current controller anyway.
  137. #
  138. # class WeblogController < ActionController::Base
  139. # layout proc { logged_in? ? "writer_layout" : "reader_layout" }
  140. # end
  141. #
  142. # Of course, the most common way of specifying a layout is still just as a plain template name:
  143. #
  144. # class WeblogController < ActionController::Base
  145. # layout "weblog_standard"
  146. # end
  147. #
  148. # The template will be looked always in <tt>app/views/layouts/</tt> folder. But you can point
  149. # <tt>layouts</tt> folder direct also. <tt>layout "layouts/demo"</tt> is the same as <tt>layout "demo"</tt>.
  150. #
  151. # Setting the layout to +nil+ forces it to be looked up in the filesystem and fallbacks to the parent behavior if none exists.
  152. # Setting it to +nil+ is useful to re-enable template lookup overriding a previous configuration set in the parent:
  153. #
  154. # class ApplicationController < ActionController::Base
  155. # layout "application"
  156. # end
  157. #
  158. # class PostsController < ApplicationController
  159. # # Will use "application" layout
  160. # end
  161. #
  162. # class CommentsController < ApplicationController
  163. # # Will search for "comments" layout and fallback "application" layout
  164. # layout nil
  165. # end
  166. #
  167. # == Conditional layouts
  168. #
  169. # If you have a layout that by default is applied to all the actions of a controller, you still have the option of rendering
  170. # a given action or set of actions without a layout, or restricting a layout to only a single action or a set of actions. The
  171. # <tt>:only</tt> and <tt>:except</tt> options can be passed to the layout call. For example:
  172. #
  173. # class WeblogController < ActionController::Base
  174. # layout "weblog_standard", except: :rss
  175. #
  176. # # ...
  177. #
  178. # end
  179. #
  180. # This will assign "weblog_standard" as the WeblogController's layout for all actions except for the +rss+ action, which will
  181. # be rendered directly, without wrapping a layout around the rendered view.
  182. #
  183. # Both the <tt>:only</tt> and <tt>:except</tt> condition can accept an arbitrary number of method references, so
  184. # #<tt>except: [ :rss, :text_only ]</tt> is valid, as is <tt>except: :rss</tt>.
  185. #
  186. # == Using a different layout in the action render call
  187. #
  188. # If most of your actions use the same layout, it makes perfect sense to define a controller-wide layout as described above.
  189. # Sometimes you'll have exceptions where one action wants to use a different layout than the rest of the controller.
  190. # You can do this by passing a <tt>:layout</tt> option to the <tt>render</tt> call. For example:
  191. #
  192. # class WeblogController < ActionController::Base
  193. # layout "weblog_standard"
  194. #
  195. # def help
  196. # render action: "help", layout: "help"
  197. # end
  198. # end
  199. #
  200. # This will override the controller-wide "weblog_standard" layout, and will render the help action with the "help" layout instead.
  201. 9 module Layouts
  202. 9 extend ActiveSupport::Concern
  203. 9 include ActionView::Rendering
  204. 9 included do
  205. 15 class_attribute :_layout, instance_accessor: false
  206. 15 class_attribute :_layout_conditions, instance_accessor: false, default: {}
  207. 15 _write_layout_method
  208. end
  209. 9 delegate :_layout_conditions, to: :class
  210. 9 module ClassMethods
  211. 9 def inherited(klass) # :nodoc:
  212. 213 super
  213. 213 klass._write_layout_method
  214. end
  215. # This module is mixed in if layout conditions are provided. This means
  216. # that if no layout conditions are used, this method is not used
  217. 9 module LayoutConditions # :nodoc:
  218. 9 private
  219. # Determines whether the current action has a layout definition by
  220. # checking the action name against the :only and :except conditions
  221. # set by the <tt>layout</tt> method.
  222. #
  223. # ==== Returns
  224. # * <tt>Boolean</tt> - True if the action has a layout definition, false otherwise.
  225. 9 def _conditional_layout?
  226. return unless super
  227. conditions = _layout_conditions
  228. if only = conditions[:only]
  229. only.include?(action_name)
  230. elsif except = conditions[:except]
  231. !except.include?(action_name)
  232. else
  233. true
  234. end
  235. end
  236. end
  237. # Specify the layout to use for this class.
  238. #
  239. # If the specified layout is a:
  240. # String:: the String is the template name
  241. # Symbol:: call the method specified by the symbol
  242. # Proc:: call the passed Proc
  243. # false:: There is no layout
  244. # true:: raise an ArgumentError
  245. # nil:: Force default layout behavior with inheritance
  246. #
  247. # Return value of +Proc+ and +Symbol+ arguments should be +String+, +false+, +true+ or +nil+
  248. # with the same meaning as described above.
  249. # ==== Parameters
  250. # * <tt>layout</tt> - The layout to use.
  251. #
  252. # ==== Options (conditions)
  253. # * :only - A list of actions to apply this layout to.
  254. # * :except - Apply this layout to all actions but this one.
  255. 9 def layout(layout, conditions = {})
  256. 108 include LayoutConditions unless conditions.empty?
  257. 135 conditions.each { |k, v| conditions[k] = Array(v).map(&:to_s) }
  258. 108 self._layout_conditions = conditions
  259. 108 self._layout = layout
  260. 108 _write_layout_method
  261. end
  262. # Creates a _layout method to be called by _default_layout .
  263. #
  264. # If a layout is not explicitly mentioned then look for a layout with the controller's name.
  265. # if nothing is found then try same procedure to find super class's layout.
  266. 9 def _write_layout_method # :nodoc:
  267. 336 silence_redefinition_of_method(:_layout)
  268. 336 prefixes = /\blayouts/.match?(_implied_layout_name) ? [] : ["layouts"]
  269. 336 default_behavior = "lookup_context.find_all('#{_implied_layout_name}', #{prefixes.inspect}, false, [], { formats: formats }).first || super"
  270. 336 name_clause = if name
  271. 336 default_behavior
  272. else
  273. <<-RUBY
  274. super
  275. RUBY
  276. end
  277. 336 layout_definition = \
  278. case _layout
  279. when String
  280. 87 _layout.inspect
  281. when Symbol
  282. 18 <<-RUBY
  283. #{_layout}.tap do |layout|
  284. return #{default_behavior} if layout.nil?
  285. unless layout.is_a?(String) || !layout
  286. raise ArgumentError, "Your layout method :#{_layout} returned \#{layout}. It " \
  287. "should have returned a String, false, or nil"
  288. end
  289. end
  290. RUBY
  291. when Proc
  292. 18 define_method :_layout_from_proc, &_layout
  293. 18 private :_layout_from_proc
  294. 18 <<-RUBY
  295. result = _layout_from_proc(#{_layout.arity == 0 ? '' : 'self'})
  296. return #{default_behavior} if result.nil?
  297. result
  298. RUBY
  299. when false
  300. 3 nil
  301. when true
  302. raise ArgumentError, "Layouts must be specified as a String, Symbol, Proc, false, or nil"
  303. when nil
  304. 210 name_clause
  305. end
  306. 336 class_eval <<-RUBY, __FILE__, __LINE__ + 1
  307. # frozen_string_literal: true
  308. def _layout(lookup_context, formats)
  309. if _conditional_layout?
  310. #{layout_definition}
  311. else
  312. #{name_clause}
  313. end
  314. end
  315. private :_layout
  316. RUBY
  317. end
  318. 9 private
  319. # If no layout is supplied, look for a template named the return
  320. # value of this method.
  321. #
  322. # ==== Returns
  323. # * <tt>String</tt> - A template name
  324. 9 def _implied_layout_name
  325. 510 controller_path
  326. end
  327. end
  328. 9 def _normalize_options(options) # :nodoc:
  329. super
  330. if _include_layout?(options)
  331. layout = options.delete(:layout) { :default }
  332. options[:layout] = _layout_for_option(layout)
  333. end
  334. end
  335. 9 attr_internal_writer :action_has_layout
  336. 9 def initialize(*) # :nodoc:
  337. @_action_has_layout = true
  338. super
  339. end
  340. # Controls whether an action should be rendered using a layout.
  341. # If you want to disable any <tt>layout</tt> settings for the
  342. # current action so that it is rendered without a layout then
  343. # either override this method in your controller to return false
  344. # for that action or set the <tt>action_has_layout</tt> attribute
  345. # to false before rendering.
  346. 9 def action_has_layout?
  347. @_action_has_layout
  348. end
  349. 9 private
  350. 9 def _conditional_layout?
  351. true
  352. end
  353. # This will be overwritten by _write_layout_method
  354. 9 def _layout(*); end
  355. # Determine the layout for a given name, taking into account the name type.
  356. #
  357. # ==== Parameters
  358. # * <tt>name</tt> - The name of the template
  359. 9 def _layout_for_option(name)
  360. case name
  361. when String then _normalize_layout(name)
  362. when Proc then name
  363. when true then Proc.new { |lookup_context, formats| _default_layout(lookup_context, formats, true) }
  364. when :default then Proc.new { |lookup_context, formats| _default_layout(lookup_context, formats, false) }
  365. when false, nil then nil
  366. else
  367. raise ArgumentError,
  368. "String, Proc, :default, true, or false, expected for `layout'; you passed #{name.inspect}"
  369. end
  370. end
  371. 9 def _normalize_layout(value)
  372. value.is_a?(String) && !value.match?(/\blayouts/) ? "layouts/#{value}" : value
  373. end
  374. # Returns the default layout for this controller.
  375. # Optionally raises an exception if the layout could not be found.
  376. #
  377. # ==== Parameters
  378. # * <tt>formats</tt> - The formats accepted to this layout
  379. # * <tt>require_layout</tt> - If set to +true+ and layout is not found,
  380. # an +ArgumentError+ exception is raised (defaults to +false+)
  381. #
  382. # ==== Returns
  383. # * <tt>template</tt> - The template object for the default layout (or +nil+)
  384. 9 def _default_layout(lookup_context, formats, require_layout = false)
  385. begin
  386. value = _layout(lookup_context, formats) if action_has_layout?
  387. rescue NameError => e
  388. raise e, "Could not render layout: #{e.message}"
  389. end
  390. if require_layout && action_has_layout? && !value
  391. raise ArgumentError,
  392. "There was no default layout for #{self.class} in #{view_paths.inspect}"
  393. end
  394. _normalize_layout(value)
  395. end
  396. 9 def _include_layout?(options)
  397. (options.keys & [:body, :plain, :html, :inline, :partial]).empty? || options.key?(:layout)
  398. end
  399. end
  400. end

lib/action_view/log_subscriber.rb

33.33% lines covered

63 relevant lines. 21 lines covered and 42 lines missed.
    
  1. # frozen_string_literal: true
  2. 3 require "active_support/log_subscriber"
  3. 3 module ActionView
  4. # = Action View Log Subscriber
  5. #
  6. # Provides functionality so that Rails can output logs from Action View.
  7. 3 class LogSubscriber < ActiveSupport::LogSubscriber
  8. 3 VIEWS_PATTERN = /^app\/views\//
  9. 3 def initialize
  10. 3 @root = nil
  11. 3 super
  12. end
  13. 3 def render_template(event)
  14. info do
  15. message = +" Rendered #{from_rails_root(event.payload[:identifier])}"
  16. message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout]
  17. message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
  18. end
  19. end
  20. 3 def render_partial(event)
  21. debug do
  22. message = +" Rendered #{from_rails_root(event.payload[:identifier])}"
  23. message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout]
  24. message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
  25. message << " #{cache_message(event.payload)}" unless event.payload[:cache_hit].nil?
  26. message
  27. end
  28. end
  29. 3 def render_layout(event)
  30. info do
  31. message = +" Rendered layout #{from_rails_root(event.payload[:identifier])}"
  32. message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
  33. end
  34. end
  35. 3 def render_collection(event)
  36. identifier = event.payload[:identifier] || "templates"
  37. debug do
  38. message = +" Rendered collection of #{from_rails_root(identifier)}"
  39. message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout]
  40. message << " #{render_count(event.payload)} (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
  41. message
  42. end
  43. end
  44. 3 def start(name, id, payload)
  45. log_rendering_start(payload, name)
  46. super
  47. end
  48. 3 def logger
  49. ActionView::Base.logger
  50. end
  51. 3 private
  52. 3 EMPTY = ""
  53. 3 def from_rails_root(string) # :doc:
  54. string = string.sub(rails_root, EMPTY)
  55. string.sub!(VIEWS_PATTERN, EMPTY)
  56. string
  57. end
  58. 3 def rails_root # :doc:
  59. @root ||= "#{Rails.root}/"
  60. end
  61. 3 def render_count(payload) # :doc:
  62. if payload[:cache_hits]
  63. "[#{payload[:cache_hits]} / #{payload[:count]} cache hits]"
  64. else
  65. "[#{payload[:count]} times]"
  66. end
  67. end
  68. 3 def cache_message(payload) # :doc:
  69. case payload[:cache_hit]
  70. when :hit
  71. "[cache hit]"
  72. when :miss
  73. "[cache miss]"
  74. end
  75. end
  76. 3 def log_rendering_start(payload, name)
  77. debug do
  78. qualifier =
  79. if name == "render_template.action_view"
  80. ""
  81. elsif name == "render_layout.action_view"
  82. "layout "
  83. end
  84. return unless qualifier
  85. message = +" Rendering #{qualifier}#{from_rails_root(payload[:identifier])}"
  86. message << " within #{from_rails_root(payload[:layout])}" if payload[:layout]
  87. message
  88. end
  89. end
  90. end
  91. end
  92. 3 ActionView::LogSubscriber.attach_to :action_view

lib/action_view/lookup_context.rb

43.98% lines covered

166 relevant lines. 73 lines covered and 93 lines missed.
    
  1. # frozen_string_literal: true
  2. 3 require "concurrent/map"
  3. 3 require "active_support/core_ext/module/attribute_accessors"
  4. 3 require "action_view/template/resolver"
  5. 3 module ActionView
  6. # = Action View Lookup Context
  7. #
  8. # <tt>LookupContext</tt> is the object responsible for holding all information
  9. # required for looking up templates, i.e. view paths and details.
  10. # <tt>LookupContext</tt> is also responsible for generating a key, given to
  11. # view paths, used in the resolver cache lookup. Since this key is generated
  12. # only once during the request, it speeds up all cache accesses.
  13. 3 class LookupContext #:nodoc:
  14. 3 attr_accessor :prefixes, :rendered_format
  15. 3 deprecate :rendered_format
  16. 3 deprecate :rendered_format=
  17. 3 mattr_accessor :fallbacks, default: FallbackFileSystemResolver.instances
  18. 3 mattr_accessor :registered_details, default: []
  19. 3 def self.register_detail(name, &block)
  20. 12 registered_details << name
  21. 12 Accessors::DEFAULT_PROCS[name] = block
  22. 12 Accessors.define_method(:"default_#{name}", &block)
  23. 12 Accessors.module_eval <<-METHOD, __FILE__, __LINE__ + 1
  24. def #{name}
  25. @details[:#{name}] || []
  26. end
  27. def #{name}=(value)
  28. value = value.present? ? Array(value) : default_#{name}
  29. _set_detail(:#{name}, value) if value != @details[:#{name}]
  30. end
  31. METHOD
  32. end
  33. # Holds accessors for the registered details.
  34. 3 module Accessors #:nodoc:
  35. 3 DEFAULT_PROCS = {}
  36. end
  37. 3 register_detail(:locale) do
  38. locales = [I18n.locale]
  39. locales.concat(I18n.fallbacks[I18n.locale]) if I18n.respond_to? :fallbacks
  40. locales << I18n.default_locale
  41. locales.uniq!
  42. locales
  43. end
  44. 3 register_detail(:formats) { ActionView::Base.default_formats || [:html, :text, :js, :css, :xml, :json] }
  45. 3 register_detail(:variants) { [] }
  46. 3 register_detail(:handlers) { Template::Handlers.extensions }
  47. 3 class DetailsKey #:nodoc:
  48. 3 alias :eql? :equal?
  49. 3 @details_keys = Concurrent::Map.new
  50. 3 @digest_cache = Concurrent::Map.new
  51. 3 @view_context_mutex = Mutex.new
  52. 3 def self.digest_cache(details)
  53. @digest_cache[details_cache_key(details)] ||= Concurrent::Map.new
  54. end
  55. 3 def self.details_cache_key(details)
  56. if details[:formats]
  57. details = details.dup
  58. details[:formats] &= Template::Types.symbols
  59. end
  60. @details_keys[details] ||= Object.new
  61. end
  62. 3 def self.clear
  63. 3 ActionView::ViewPaths.all_view_paths.each do |path_set|
  64. 3 path_set.each(&:clear_cache)
  65. end
  66. 3 ActionView::LookupContext.fallbacks.each(&:clear_cache)
  67. 3 @view_context_class = nil
  68. 3 @details_keys.clear
  69. 3 @digest_cache.clear
  70. end
  71. 3 def self.digest_caches
  72. @digest_cache.values
  73. end
  74. 3 def self.view_context_class(klass)
  75. @view_context_mutex.synchronize do
  76. @view_context_class ||= klass.with_empty_template_cache
  77. end
  78. end
  79. end
  80. # Add caching behavior on top of Details.
  81. 3 module DetailsCache
  82. 3 attr_accessor :cache
  83. # Calculate the details key. Remove the handlers from calculation to improve performance
  84. # since the user cannot modify it explicitly.
  85. 3 def details_key #:nodoc:
  86. @details_key ||= DetailsKey.details_cache_key(@details) if @cache
  87. end
  88. # Temporary skip passing the details_key forward.
  89. 3 def disable_cache
  90. old_value, @cache = @cache, false
  91. yield
  92. ensure
  93. @cache = old_value
  94. end
  95. 3 private
  96. 3 def _set_detail(key, value) # :doc:
  97. @details = @details.dup if @digest_cache || @details_key
  98. @digest_cache = nil
  99. @details_key = nil
  100. @details[key] = value
  101. end
  102. end
  103. # Helpers related to template lookup using the lookup context information.
  104. 3 module ViewPaths
  105. 3 attr_reader :view_paths, :html_fallback_for_js
  106. 3 def find(name, prefixes = [], partial = false, keys = [], options = {})
  107. @view_paths.find(*args_for_lookup(name, prefixes, partial, keys, options))
  108. end
  109. 3 alias :find_template :find
  110. 3 alias :find_file :find
  111. 3 deprecate :find_file
  112. 3 def find_all(name, prefixes = [], partial = false, keys = [], options = {})
  113. @view_paths.find_all(*args_for_lookup(name, prefixes, partial, keys, options))
  114. end
  115. 3 def exists?(name, prefixes = [], partial = false, keys = [], **options)
  116. @view_paths.exists?(*args_for_lookup(name, prefixes, partial, keys, options))
  117. end
  118. 3 alias :template_exists? :exists?
  119. 3 def any?(name, prefixes = [], partial = false)
  120. @view_paths.exists?(*args_for_any(name, prefixes, partial))
  121. end
  122. 3 alias :any_templates? :any?
  123. # Adds fallbacks to the view paths. Useful in cases when you are rendering
  124. # a :file.
  125. 3 def with_fallbacks
  126. view_paths = build_view_paths((@view_paths.paths + self.class.fallbacks).uniq)
  127. if block_given?
  128. ActiveSupport::Deprecation.warn <<~eowarn.squish
  129. Calling `with_fallbacks` with a block is deprecated. Call methods on
  130. the lookup context returned by `with_fallbacks` instead.
  131. eowarn
  132. begin
  133. _view_paths = @view_paths
  134. @view_paths = view_paths
  135. yield
  136. ensure
  137. @view_paths = _view_paths
  138. end
  139. else
  140. ActionView::LookupContext.new(view_paths, @details, @prefixes)
  141. end
  142. end
  143. 3 private
  144. # Whenever setting view paths, makes a copy so that we can manipulate them in
  145. # instance objects as we wish.
  146. 3 def build_view_paths(paths)
  147. ActionView::PathSet.new(Array(paths))
  148. end
  149. 3 def args_for_lookup(name, prefixes, partial, keys, details_options)
  150. name, prefixes = normalize_name(name, prefixes)
  151. details, details_key = detail_args_for(details_options)
  152. [name, prefixes, partial || false, details, details_key, keys]
  153. end
  154. # Compute details hash and key according to user options (e.g. passed from #render).
  155. 3 def detail_args_for(options) # :doc:
  156. return @details, details_key if options.empty? # most common path.
  157. user_details = @details.merge(options)
  158. if @cache
  159. details_key = DetailsKey.details_cache_key(user_details)
  160. else
  161. details_key = nil
  162. end
  163. [user_details, details_key]
  164. end
  165. 3 def args_for_any(name, prefixes, partial)
  166. name, prefixes = normalize_name(name, prefixes)
  167. details, details_key = detail_args_for_any
  168. [name, prefixes, partial || false, details, details_key]
  169. end
  170. 3 def detail_args_for_any
  171. @detail_args_for_any ||= begin
  172. details = {}
  173. registered_details.each do |k|
  174. if k == :variants
  175. details[k] = :any
  176. else
  177. details[k] = Accessors::DEFAULT_PROCS[k].call
  178. end
  179. end
  180. if @cache
  181. [details, DetailsKey.details_cache_key(details)]
  182. else
  183. [details, nil]
  184. end
  185. end
  186. end
  187. # Support legacy foo.erb names even though we now ignore .erb
  188. # as well as incorrectly putting part of the path in the template
  189. # name instead of the prefix.
  190. 3 def normalize_name(name, prefixes)
  191. prefixes = prefixes.presence
  192. parts = name.to_s.split("/")
  193. parts.shift if parts.first.empty?
  194. name = parts.pop
  195. return name, prefixes || [""] if parts.empty?
  196. parts = parts.join("/")
  197. prefixes = prefixes ? prefixes.map { |p| "#{p}/#{parts}" } : [parts]
  198. return name, prefixes
  199. end
  200. end
  201. 3 include Accessors
  202. 3 include DetailsCache
  203. 3 include ViewPaths
  204. 3 def initialize(view_paths, details = {}, prefixes = [])
  205. @details_key = nil
  206. @digest_cache = nil
  207. @cache = true
  208. @prefixes = prefixes
  209. @details = initialize_details({}, details)
  210. @view_paths = build_view_paths(view_paths)
  211. end
  212. 3 def digest_cache
  213. @digest_cache ||= DetailsKey.digest_cache(@details)
  214. end
  215. 3 def with_prepended_formats(formats)
  216. details = @details.dup
  217. details[:formats] = formats
  218. self.class.new(@view_paths, details, @prefixes)
  219. end
  220. 3 def initialize_details(target, details)
  221. registered_details.each do |k|
  222. target[k] = details[k] || Accessors::DEFAULT_PROCS[k].call
  223. end
  224. target
  225. end
  226. 3 private :initialize_details
  227. # Override formats= to expand ["*/*"] values and automatically
  228. # add :html as fallback to :js.
  229. 3 def formats=(values)
  230. if values
  231. values = values.dup
  232. values.concat(default_formats) if values.delete "*/*"
  233. values.uniq!
  234. invalid_values = (values - Template::Types.symbols)
  235. unless invalid_values.empty?
  236. raise ArgumentError, "Invalid formats: #{invalid_values.map(&:inspect).join(", ")}"
  237. end
  238. if values == [:js]
  239. values << :html
  240. @html_fallback_for_js = true
  241. end
  242. end
  243. super(values)
  244. end
  245. # Override locale to return a symbol instead of array.
  246. 3 def locale
  247. @details[:locale].first
  248. end
  249. # Overload locale= to also set the I18n.locale. If the current I18n.config object responds
  250. # to original_config, it means that it has a copy of the original I18n configuration and it's
  251. # acting as proxy, which we need to skip.
  252. 3 def locale=(value)
  253. if value
  254. config = I18n.config.respond_to?(:original_config) ? I18n.config.original_config : I18n.config
  255. config.locale = value
  256. end
  257. super(default_locale)
  258. end
  259. end
  260. end

lib/action_view/model_naming.rb

66.67% lines covered

6 relevant lines. 4 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 module ActionView
  3. 9 module ModelNaming #:nodoc:
  4. # Converts the given object to an ActiveModel compliant one.
  5. 9 def convert_to_model(object)
  6. object.respond_to?(:to_model) ? object.to_model : object
  7. end
  8. 9 def model_name_from_record_or_class(record_or_class)
  9. convert_to_model(record_or_class).model_name
  10. end
  11. end
  12. end

lib/action_view/path_set.rb

68.18% lines covered

44 relevant lines. 30 lines covered and 14 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 module ActionView #:nodoc:
  3. # = Action View PathSet
  4. #
  5. # This class is used to store and access paths in Action View. A number of
  6. # operations are defined so that you can search among the paths in this
  7. # set and also perform operations on other +PathSet+ objects.
  8. #
  9. # A +LookupContext+ will use a +PathSet+ to store the paths in its context.
  10. 9 class PathSet #:nodoc:
  11. 9 include Enumerable
  12. 9 attr_reader :paths
  13. 9 delegate :[], :include?, :pop, :size, :each, to: :paths
  14. 9 def initialize(paths = [])
  15. 60 @paths = typecast paths
  16. end
  17. 9 def initialize_copy(other)
  18. 3 @paths = other.paths.dup
  19. 3 self
  20. end
  21. 9 def to_ary
  22. 6 paths.dup
  23. end
  24. 9 def compact
  25. PathSet.new paths.compact
  26. end
  27. 9 def +(array)
  28. 6 PathSet.new(paths + array)
  29. end
  30. 9 %w(<< concat push insert unshift).each do |method|
  31. 45 class_eval <<-METHOD, __FILE__, __LINE__ + 1
  32. def #{method}(*args)
  33. paths.#{method}(*typecast(args))
  34. end
  35. METHOD
  36. end
  37. 9 def find(*args)
  38. find_all(*args).first || raise(MissingTemplate.new(self, *args))
  39. end
  40. 9 alias :find_file :find
  41. 9 deprecate :find_file
  42. 9 def find_all(path, prefixes = [], *args)
  43. _find_all path, prefixes, args
  44. end
  45. 9 def exists?(path, prefixes, *args)
  46. find_all(path, prefixes, *args).any?
  47. end
  48. 9 def find_all_with_query(query) # :nodoc:
  49. paths.each do |resolver|
  50. templates = resolver.find_all_with_query(query)
  51. return templates unless templates.empty?
  52. end
  53. []
  54. end
  55. 9 private
  56. 9 def _find_all(path, prefixes, args)
  57. prefixes = [prefixes] if String === prefixes
  58. prefixes.each do |prefix|
  59. paths.each do |resolver|
  60. templates = resolver.find_all(path, prefix, *args)
  61. return templates unless templates.empty?
  62. end
  63. end
  64. []
  65. end
  66. 9 def typecast(paths)
  67. 60 paths.map do |path|
  68. 33 case path
  69. when Pathname, String
  70. 21 OptimizedFileSystemResolver.new path.to_s
  71. else
  72. 12 path
  73. end
  74. end
  75. end
  76. end
  77. end

lib/action_view/railtie.rb

0.0% lines covered

87 relevant lines. 0 lines covered and 87 lines missed.
    
  1. # frozen_string_literal: true
  2. require "action_view"
  3. require "rails"
  4. module ActionView
  5. # = Action View Railtie
  6. class Railtie < Rails::Engine # :nodoc:
  7. NULL_OPTION = Object.new
  8. config.action_view = ActiveSupport::OrderedOptions.new
  9. config.action_view.embed_authenticity_token_in_remote_forms = nil
  10. config.action_view.debug_missing_translation = true
  11. config.action_view.default_enforce_utf8 = nil
  12. config.action_view.finalize_compiled_template_methods = NULL_OPTION
  13. config.eager_load_namespaces << ActionView
  14. config.after_initialize do |app|
  15. ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms =
  16. app.config.action_view.delete(:embed_authenticity_token_in_remote_forms)
  17. end
  18. config.after_initialize do |app|
  19. form_with_generates_remote_forms = app.config.action_view.delete(:form_with_generates_remote_forms)
  20. ActionView::Helpers::FormHelper.form_with_generates_remote_forms = form_with_generates_remote_forms
  21. end
  22. config.after_initialize do |app|
  23. form_with_generates_ids = app.config.action_view.delete(:form_with_generates_ids)
  24. unless form_with_generates_ids.nil?
  25. ActionView::Helpers::FormHelper.form_with_generates_ids = form_with_generates_ids
  26. end
  27. end
  28. config.after_initialize do |app|
  29. default_enforce_utf8 = app.config.action_view.delete(:default_enforce_utf8)
  30. unless default_enforce_utf8.nil?
  31. ActionView::Helpers::FormTagHelper.default_enforce_utf8 = default_enforce_utf8
  32. end
  33. end
  34. config.after_initialize do |app|
  35. ActiveSupport.on_load(:action_view) do
  36. app.config.action_view.each do |k, v|
  37. if k == :raise_on_missing_translations
  38. ActiveSupport::Deprecation.warn \
  39. "action_view.raise_on_missing_translations is deprecated and will be removed in Rails 6.2. " \
  40. "Set i18n.raise_on_missing_translations instead. " \
  41. "Note that this new setting also affects how missing translations are handled in controllers."
  42. end
  43. send "#{k}=", v
  44. end
  45. end
  46. end
  47. initializer "action_view.finalize_compiled_template_methods" do |app|
  48. ActiveSupport.on_load(:action_view) do
  49. option = app.config.action_view.delete(:finalize_compiled_template_methods)
  50. if option != NULL_OPTION
  51. ActiveSupport::Deprecation.warn "action_view.finalize_compiled_template_methods is deprecated and has no effect"
  52. end
  53. end
  54. end
  55. initializer "action_view.logger" do
  56. ActiveSupport.on_load(:action_view) { self.logger ||= Rails.logger }
  57. end
  58. initializer "action_view.caching" do |app|
  59. ActiveSupport.on_load(:action_view) do
  60. if app.config.action_view.cache_template_loading.nil?
  61. ActionView::Resolver.caching = app.config.cache_classes
  62. end
  63. end
  64. end
  65. initializer "action_view.setup_action_pack" do |app|
  66. ActiveSupport.on_load(:action_controller) do
  67. ActionView::RoutingUrlFor.include(ActionDispatch::Routing::UrlFor)
  68. end
  69. end
  70. initializer "action_view.collection_caching", after: "action_controller.set_configs" do |app|
  71. PartialRenderer.collection_cache = app.config.action_controller.cache_store
  72. end
  73. config.after_initialize do |app|
  74. enable_caching = if app.config.action_view.cache_template_loading.nil?
  75. app.config.cache_classes
  76. else
  77. app.config.action_view.cache_template_loading
  78. end
  79. unless enable_caching
  80. app.executor.to_run ActionView::CacheExpiry::Executor.new(watcher: app.config.file_watcher)
  81. end
  82. end
  83. rake_tasks do |app|
  84. unless app.config.api_only
  85. load "action_view/tasks/cache_digests.rake"
  86. end
  87. end
  88. end
  89. end

lib/action_view/record_identifier.rb

65.0% lines covered

20 relevant lines. 13 lines covered and 7 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 require "active_support/core_ext/module"
  3. 9 require "action_view/model_naming"
  4. 9 module ActionView
  5. # RecordIdentifier encapsulates methods used by various ActionView helpers
  6. # to associate records with DOM elements.
  7. #
  8. # Consider for example the following code that form of post:
  9. #
  10. # <%= form_for(post) do |f| %>
  11. # <%= f.text_field :body %>
  12. # <% end %>
  13. #
  14. # When +post+ is a new, unsaved ActiveRecord::Base instance, the resulting HTML
  15. # is:
  16. #
  17. # <form class="new_post" id="new_post" action="/posts" accept-charset="UTF-8" method="post">
  18. # <input type="text" name="post[body]" id="post_body" />
  19. # </form>
  20. #
  21. # When +post+ is a persisted ActiveRecord::Base instance, the resulting HTML
  22. # is:
  23. #
  24. # <form class="edit_post" id="edit_post_42" action="/posts/42" accept-charset="UTF-8" method="post">
  25. # <input type="text" value="What a wonderful world!" name="post[body]" id="post_body" />
  26. # </form>
  27. #
  28. # In both cases, the +id+ and +class+ of the wrapping DOM element are
  29. # automatically generated, following naming conventions encapsulated by the
  30. # RecordIdentifier methods #dom_id and #dom_class:
  31. #
  32. # dom_id(Post.new) # => "new_post"
  33. # dom_class(Post.new) # => "post"
  34. # dom_id(Post.find 42) # => "post_42"
  35. # dom_class(Post.find 42) # => "post"
  36. #
  37. # Note that these methods do not strictly require +Post+ to be a subclass of
  38. # ActiveRecord::Base.
  39. # Any +Post+ class will work as long as its instances respond to +to_key+
  40. # and +model_name+, given that +model_name+ responds to +param_key+.
  41. # For instance:
  42. #
  43. # class Post
  44. # attr_accessor :to_key
  45. #
  46. # def model_name
  47. # OpenStruct.new param_key: 'post'
  48. # end
  49. #
  50. # def self.find(id)
  51. # new.tap { |post| post.to_key = [id] }
  52. # end
  53. # end
  54. 9 module RecordIdentifier
  55. 9 extend self
  56. 9 extend ModelNaming
  57. 9 include ModelNaming
  58. 9 JOIN = "_"
  59. 9 NEW = "new"
  60. # The DOM class convention is to use the singular form of an object or class.
  61. #
  62. # dom_class(post) # => "post"
  63. # dom_class(Person) # => "person"
  64. #
  65. # If you need to address multiple instances of the same class in the same view, you can prefix the dom_class:
  66. #
  67. # dom_class(post, :edit) # => "edit_post"
  68. # dom_class(Person, :edit) # => "edit_person"
  69. 9 def dom_class(record_or_class, prefix = nil)
  70. singular = model_name_from_record_or_class(record_or_class).param_key
  71. prefix ? "#{prefix}#{JOIN}#{singular}" : singular
  72. end
  73. # The DOM id convention is to use the singular form of an object or class with the id following an underscore.
  74. # If no id is found, prefix with "new_" instead.
  75. #
  76. # dom_id(Post.find(45)) # => "post_45"
  77. # dom_id(Post.new) # => "new_post"
  78. #
  79. # If you need to address multiple instances of the same class in the same view, you can prefix the dom_id:
  80. #
  81. # dom_id(Post.find(45), :edit) # => "edit_post_45"
  82. # dom_id(Post.new, :custom) # => "custom_post"
  83. 9 def dom_id(record, prefix = nil)
  84. if record_id = record_key_for_dom_id(record)
  85. "#{dom_class(record, prefix)}#{JOIN}#{record_id}"
  86. else
  87. dom_class(record, prefix || NEW)
  88. end
  89. end
  90. 9 private
  91. # Returns a string representation of the key attribute(s) that is suitable for use in an HTML DOM id.
  92. # This can be overwritten to customize the default generated string representation if desired.
  93. # If you need to read back a key from a dom_id in order to query for the underlying database record,
  94. # you should write a helper like 'person_record_from_dom_id' that will extract the key either based
  95. # on the default implementation (which just joins all key attributes with '_') or on your own
  96. # overwritten version of the method. By default, this implementation passes the key string through a
  97. # method that replaces all characters that are invalid inside DOM ids, with valid ones. You need to
  98. # make sure yourself that your dom ids are valid, in case you overwrite this method.
  99. 9 def record_key_for_dom_id(record) # :doc:
  100. key = convert_to_model(record).to_key
  101. key ? key.join(JOIN) : key
  102. end
  103. end
  104. end

lib/action_view/renderer/abstract_renderer.rb

43.68% lines covered

87 relevant lines. 38 lines covered and 49 lines missed.
    
  1. # frozen_string_literal: true
  2. 3 require "concurrent/map"
  3. 3 module ActionView
  4. # This class defines the interface for a renderer. Each class that
  5. # subclasses +AbstractRenderer+ is used by the base +Renderer+ class to
  6. # render a specific type of object.
  7. #
  8. # The base +Renderer+ class uses its +render+ method to delegate to the
  9. # renderers. These currently consist of
  10. #
  11. # PartialRenderer - Used for rendering partials
  12. # TemplateRenderer - Used for rendering other types of templates
  13. # StreamingTemplateRenderer - Used for streaming
  14. #
  15. # Whenever the +render+ method is called on the base +Renderer+ class, a new
  16. # renderer object of the correct type is created, and the +render+ method on
  17. # that new object is called in turn. This abstracts the set up and rendering
  18. # into a separate classes for partials and templates.
  19. 3 class AbstractRenderer #:nodoc:
  20. 3 delegate :template_exists?, :any_templates?, :formats, to: :@lookup_context
  21. 3 def initialize(lookup_context)
  22. @lookup_context = lookup_context
  23. end
  24. 3 def render
  25. raise NotImplementedError
  26. end
  27. 3 module ObjectRendering # :nodoc:
  28. 3 PREFIXED_PARTIAL_NAMES = Concurrent::Map.new do |h, k|
  29. h[k] = Concurrent::Map.new
  30. end
  31. 3 def initialize(lookup_context, options)
  32. super
  33. @context_prefix = lookup_context.prefixes.first
  34. end
  35. 3 private
  36. 3 def local_variable(path)
  37. if as = @options[:as]
  38. raise_invalid_option_as(as) unless /\A[a-z_]\w*\z/.match?(as.to_s)
  39. as.to_sym
  40. else
  41. begin
  42. base = path.end_with?("/") ? "" : File.basename(path)
  43. raise_invalid_identifier(path) unless base =~ /\A_?(.*?)(?:\.\w+)*\z/
  44. $1.to_sym
  45. end
  46. end
  47. end
  48. 3 IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " \
  49. "make sure your partial name starts with underscore."
  50. 3 OPTION_AS_ERROR_MESSAGE = "The value (%s) of the option `as` is not a valid Ruby identifier; " \
  51. "make sure it starts with lowercase letter, " \
  52. "and is followed by any combination of letters, numbers and underscores."
  53. 3 def raise_invalid_identifier(path)
  54. raise ArgumentError, IDENTIFIER_ERROR_MESSAGE % path
  55. end
  56. 3 def raise_invalid_option_as(as)
  57. raise ArgumentError, OPTION_AS_ERROR_MESSAGE % as
  58. end
  59. # Obtains the path to where the object's partial is located. If the object
  60. # responds to +to_partial_path+, then +to_partial_path+ will be called and
  61. # will provide the path. If the object does not respond to +to_partial_path+,
  62. # then an +ArgumentError+ is raised.
  63. #
  64. # If +prefix_partial_path_with_controller_namespace+ is true, then this
  65. # method will prefix the partial paths with a namespace.
  66. 3 def partial_path(object, view)
  67. object = object.to_model if object.respond_to?(:to_model)
  68. path = if object.respond_to?(:to_partial_path)
  69. object.to_partial_path
  70. else
  71. raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.")
  72. end
  73. if view.prefix_partial_path_with_controller_namespace
  74. PREFIXED_PARTIAL_NAMES[@context_prefix][path] ||= merge_prefix_into_object_path(@context_prefix, path.dup)
  75. else
  76. path
  77. end
  78. end
  79. 3 def merge_prefix_into_object_path(prefix, object_path)
  80. if prefix.include?(?/) && object_path.include?(?/)
  81. prefixes = []
  82. prefix_array = File.dirname(prefix).split("/")
  83. object_path_array = object_path.split("/")[0..-3] # skip model dir & partial
  84. prefix_array.each_with_index do |dir, index|
  85. break if dir == object_path_array[index]
  86. prefixes << dir
  87. end
  88. (prefixes << object_path).join("/")
  89. else
  90. object_path
  91. end
  92. end
  93. end
  94. 3 class RenderedCollection # :nodoc:
  95. 3 def self.empty(format)
  96. EmptyCollection.new format
  97. end
  98. 3 attr_reader :rendered_templates
  99. 3 def initialize(rendered_templates, spacer)
  100. @rendered_templates = rendered_templates
  101. @spacer = spacer
  102. end
  103. 3 def body
  104. @rendered_templates.map(&:body).join(@spacer.body).html_safe
  105. end
  106. 3 def format
  107. rendered_templates.first.format
  108. end
  109. 3 class EmptyCollection
  110. 3 attr_reader :format
  111. 3 def initialize(format)
  112. @format = format
  113. end
  114. 3 def body; nil; end
  115. end
  116. end
  117. 3 class RenderedTemplate # :nodoc:
  118. 3 attr_reader :body, :template
  119. 3 def initialize(body, template)
  120. @body = body
  121. @template = template
  122. end
  123. 3 def format
  124. template.format
  125. end
  126. 3 EMPTY_SPACER = Struct.new(:body).new
  127. end
  128. 3 private
  129. 3 NO_DETAILS = {}.freeze
  130. 3 def extract_details(options) # :doc:
  131. details = nil
  132. @lookup_context.registered_details.each do |key|
  133. value = options[key]
  134. if value
  135. (details ||= {})[key] = Array(value)
  136. end
  137. end
  138. details || NO_DETAILS
  139. end
  140. 3 def prepend_formats(formats) # :doc:
  141. formats = Array(formats)
  142. return if formats.empty? || @lookup_context.html_fallback_for_js
  143. @lookup_context.formats = formats | @lookup_context.formats
  144. end
  145. 3 def build_rendered_template(content, template)
  146. RenderedTemplate.new content, template
  147. end
  148. 3 def build_rendered_collection(templates, spacer)
  149. RenderedCollection.new templates, spacer
  150. end
  151. end
  152. end

lib/action_view/renderer/collection_renderer.rb

32.35% lines covered

102 relevant lines. 33 lines covered and 69 lines missed.
    
  1. # frozen_string_literal: true
  2. 3 require "action_view/renderer/partial_renderer"
  3. 3 module ActionView
  4. 3 class PartialIteration
  5. # The number of iterations that will be done by the partial.
  6. 3 attr_reader :size
  7. # The current iteration of the partial.
  8. 3 attr_reader :index
  9. 3 def initialize(size)
  10. @size = size
  11. @index = 0
  12. end
  13. # Check if this is the first iteration of the partial.
  14. 3 def first?
  15. index == 0
  16. end
  17. # Check if this is the last iteration of the partial.
  18. 3 def last?
  19. index == size - 1
  20. end
  21. 3 def iterate! # :nodoc:
  22. @index += 1
  23. end
  24. end
  25. 3 class CollectionRenderer < PartialRenderer # :nodoc:
  26. 3 include ObjectRendering
  27. 3 class CollectionIterator # :nodoc:
  28. 3 include Enumerable
  29. 3 def initialize(collection)
  30. @collection = collection
  31. end
  32. 3 def each(&blk)
  33. @collection.each(&blk)
  34. end
  35. 3 def size
  36. @collection.size
  37. end
  38. end
  39. 3 class SameCollectionIterator < CollectionIterator # :nodoc:
  40. 3 def initialize(collection, path, variables)
  41. super(collection)
  42. @path = path
  43. @variables = variables
  44. end
  45. 3 def from_collection(collection)
  46. self.class.new(collection, @path, @variables)
  47. end
  48. 3 def each_with_info
  49. return enum_for(:each_with_info) unless block_given?
  50. variables = [@path] + @variables
  51. @collection.each { |o| yield(o, variables) }
  52. end
  53. end
  54. 3 class PreloadCollectionIterator < SameCollectionIterator # :nodoc:
  55. 3 def initialize(collection, path, variables, relation)
  56. super(collection, path, variables)
  57. relation.skip_preloading! unless relation.loaded?
  58. @relation = relation
  59. end
  60. 3 def from_collection(collection)
  61. self.class.new(collection, @path, @variables, @relation)
  62. end
  63. 3 def each_with_info
  64. return super unless block_given?
  65. @relation.preload_associations(@collection)
  66. super
  67. end
  68. end
  69. 3 class MixedCollectionIterator < CollectionIterator # :nodoc:
  70. 3 def initialize(collection, paths)
  71. super(collection)
  72. @paths = paths
  73. end
  74. 3 def each_with_info
  75. return enum_for(:each_with_info) unless block_given?
  76. @collection.each_with_index { |o, i| yield(o, @paths[i]) }
  77. end
  78. end
  79. 3 def render_collection_with_partial(collection, partial, context, block)
  80. iter_vars = retrieve_variable(partial)
  81. collection = if collection.respond_to?(:preload_associations)
  82. PreloadCollectionIterator.new(collection, partial, iter_vars, collection)
  83. else
  84. SameCollectionIterator.new(collection, partial, iter_vars)
  85. end
  86. template = find_template(partial, @locals.keys + iter_vars)
  87. layout = if !block && (layout = @options[:layout])
  88. find_template(layout.to_s, @locals.keys + iter_vars)
  89. end
  90. render_collection(collection, context, partial, template, layout, block)
  91. end
  92. 3 def render_collection_derive_partial(collection, context, block)
  93. paths = collection.map { |o| partial_path(o, context) }
  94. if paths.uniq.length == 1
  95. # Homogeneous
  96. render_collection_with_partial(collection, paths.first, context, block)
  97. else
  98. if @options[:cached]
  99. raise NotImplementedError, "render caching requires a template. Please specify a partial when rendering"
  100. end
  101. paths.map! { |path| retrieve_variable(path).unshift(path) }
  102. collection = MixedCollectionIterator.new(collection, paths)
  103. render_collection(collection, context, nil, nil, nil, block)
  104. end
  105. end
  106. 3 private
  107. 3 def retrieve_variable(path)
  108. variable = local_variable(path)
  109. [variable, :"#{variable}_counter", :"#{variable}_iteration"]
  110. end
  111. 3 def render_collection(collection, view, path, template, layout, block)
  112. identifier = (template && template.identifier) || path
  113. ActiveSupport::Notifications.instrument(
  114. "render_collection.action_view",
  115. identifier: identifier,
  116. layout: layout && layout.virtual_path,
  117. count: collection.size
  118. ) do |payload|
  119. spacer = if @options.key?(:spacer_template)
  120. spacer_template = find_template(@options[:spacer_template], @locals.keys)
  121. build_rendered_template(spacer_template.render(view, @locals), spacer_template)
  122. else
  123. RenderedTemplate::EMPTY_SPACER
  124. end
  125. collection_body = if template
  126. cache_collection_render(payload, view, template, collection) do |filtered_collection|
  127. collection_with_template(view, template, layout, filtered_collection)
  128. end
  129. else
  130. collection_with_template(view, nil, layout, collection)
  131. end
  132. return RenderedCollection.empty(@lookup_context.formats.first) if collection_body.empty?
  133. build_rendered_collection(collection_body, spacer)
  134. end
  135. end
  136. 3 def collection_with_template(view, template, layout, collection)
  137. locals = @locals
  138. cache = {}
  139. partial_iteration = PartialIteration.new(collection.size)
  140. collection.each_with_info.map do |object, (path, as, counter, iteration)|
  141. index = partial_iteration.index
  142. locals[as] = object
  143. locals[counter] = index
  144. locals[iteration] = partial_iteration
  145. _template = (cache[path] ||= (template || find_template(path, @locals.keys + [as, counter, iteration])))
  146. content = _template.render(view, locals)
  147. content = layout.render(view, locals) { content } if layout
  148. partial_iteration.iterate!
  149. build_rendered_template(content, _template)
  150. end
  151. end
  152. end
  153. end

lib/action_view/renderer/object_renderer.rb

0.0% lines covered

27 relevant lines. 0 lines covered and 27 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView
  3. class ObjectRenderer < PartialRenderer # :nodoc:
  4. include ObjectRendering
  5. def initialize(lookup_context, options)
  6. super
  7. @object = nil
  8. @local_name = nil
  9. end
  10. def render_object_with_partial(object, partial, context, block)
  11. @object = object
  12. @local_name = local_variable(partial)
  13. render(partial, context, block)
  14. end
  15. def render_object_derive_partial(object, context, block)
  16. path = partial_path(object, context)
  17. render_object_with_partial(object, path, context, block)
  18. end
  19. private
  20. def template_keys(path)
  21. super + [@local_name]
  22. end
  23. def render_partial_template(view, locals, template, layout, block)
  24. locals[@local_name || template.variable] = @object
  25. super(view, locals, template, layout, block)
  26. end
  27. end
  28. end

lib/action_view/renderer/partial_renderer.rb

37.04% lines covered

27 relevant lines. 10 lines covered and 17 lines missed.
    
  1. # frozen_string_literal: true
  2. 3 require "action_view/renderer/partial_renderer/collection_caching"
  3. 3 module ActionView
  4. # = Action View Partials
  5. #
  6. # There's also a convenience method for rendering sub templates within the current controller that depends on a
  7. # single object (we call this kind of sub templates for partials). It relies on the fact that partials should
  8. # follow the naming convention of being prefixed with an underscore -- as to separate them from regular
  9. # templates that could be rendered on their own.
  10. #
  11. # In a template for Advertiser#account:
  12. #
  13. # <%= render partial: "account" %>
  14. #
  15. # This would render "advertiser/_account.html.erb".
  16. #
  17. # In another template for Advertiser#buy, we could have:
  18. #
  19. # <%= render partial: "account", locals: { account: @buyer } %>
  20. #
  21. # <% @advertisements.each do |ad| %>
  22. # <%= render partial: "ad", locals: { ad: ad } %>
  23. # <% end %>
  24. #
  25. # This would first render <tt>advertiser/_account.html.erb</tt> with <tt>@buyer</tt> passed in as the local variable +account+, then
  26. # render <tt>advertiser/_ad.html.erb</tt> and pass the local variable +ad+ to the template for display.
  27. #
  28. # == The :as and :object options
  29. #
  30. # By default ActionView::PartialRenderer doesn't have any local variables.
  31. # The <tt>:object</tt> option can be used to pass an object to the partial. For instance:
  32. #
  33. # <%= render partial: "account", object: @buyer %>
  34. #
  35. # would provide the <tt>@buyer</tt> object to the partial, available under the local variable +account+ and is
  36. # equivalent to:
  37. #
  38. # <%= render partial: "account", locals: { account: @buyer } %>
  39. #
  40. # With the <tt>:as</tt> option we can specify a different name for said local variable. For example, if we
  41. # wanted it to be +user+ instead of +account+ we'd do:
  42. #
  43. # <%= render partial: "account", object: @buyer, as: 'user' %>
  44. #
  45. # This is equivalent to
  46. #
  47. # <%= render partial: "account", locals: { user: @buyer } %>
  48. #
  49. # == \Rendering a collection of partials
  50. #
  51. # The example of partial use describes a familiar pattern where a template needs to iterate over an array and
  52. # render a sub template for each of the elements. This pattern has been implemented as a single method that
  53. # accepts an array and renders a partial by the same name as the elements contained within. So the three-lined
  54. # example in "Using partials" can be rewritten with a single line:
  55. #
  56. # <%= render partial: "ad", collection: @advertisements %>
  57. #
  58. # This will render <tt>advertiser/_ad.html.erb</tt> and pass the local variable +ad+ to the template for display. An
  59. # iteration object will automatically be made available to the template with a name of the form
  60. # +partial_name_iteration+. The iteration object has knowledge about which index the current object has in
  61. # the collection and the total size of the collection. The iteration object also has two convenience methods,
  62. # +first?+ and +last?+. In the case of the example above, the template would be fed +ad_iteration+.
  63. # For backwards compatibility the +partial_name_counter+ is still present and is mapped to the iteration's
  64. # +index+ method.
  65. #
  66. # The <tt>:as</tt> option may be used when rendering partials.
  67. #
  68. # You can specify a partial to be rendered between elements via the <tt>:spacer_template</tt> option.
  69. # The following example will render <tt>advertiser/_ad_divider.html.erb</tt> between each ad partial:
  70. #
  71. # <%= render partial: "ad", collection: @advertisements, spacer_template: "ad_divider" %>
  72. #
  73. # If the given <tt>:collection</tt> is +nil+ or empty, <tt>render</tt> will return +nil+. This will allow you
  74. # to specify a text which will be displayed instead by using this form:
  75. #
  76. # <%= render(partial: "ad", collection: @advertisements) || "There's no ad to be displayed" %>
  77. #
  78. # == \Rendering shared partials
  79. #
  80. # Two controllers can share a set of partials and render them like this:
  81. #
  82. # <%= render partial: "advertisement/ad", locals: { ad: @advertisement } %>
  83. #
  84. # This will render the partial <tt>advertisement/_ad.html.erb</tt> regardless of which controller this is being called from.
  85. #
  86. # == \Rendering objects that respond to +to_partial_path+
  87. #
  88. # Instead of explicitly naming the location of a partial, you can also let PartialRenderer do the work
  89. # and pick the proper path by checking +to_partial_path+ method.
  90. #
  91. # # @account.to_partial_path returns 'accounts/account', so it can be used to replace:
  92. # # <%= render partial: "accounts/account", locals: { account: @account} %>
  93. # <%= render partial: @account %>
  94. #
  95. # # @posts is an array of Post instances, so every post record returns 'posts/post' on +to_partial_path+,
  96. # # that's why we can replace:
  97. # # <%= render partial: "posts/post", collection: @posts %>
  98. # <%= render partial: @posts %>
  99. #
  100. # == \Rendering the default case
  101. #
  102. # If you're not going to be using any of the options like collections or layouts, you can also use the short-hand
  103. # defaults of render to render partials. Examples:
  104. #
  105. # # Instead of <%= render partial: "account" %>
  106. # <%= render "account" %>
  107. #
  108. # # Instead of <%= render partial: "account", locals: { account: @buyer } %>
  109. # <%= render "account", account: @buyer %>
  110. #
  111. # # @account.to_partial_path returns 'accounts/account', so it can be used to replace:
  112. # # <%= render partial: "accounts/account", locals: { account: @account} %>
  113. # <%= render @account %>
  114. #
  115. # # @posts is an array of Post instances, so every post record returns 'posts/post' on +to_partial_path+,
  116. # # that's why we can replace:
  117. # # <%= render partial: "posts/post", collection: @posts %>
  118. # <%= render @posts %>
  119. #
  120. # == \Rendering partials with layouts
  121. #
  122. # Partials can have their own layouts applied to them. These layouts are different than the ones that are
  123. # specified globally for the entire action, but they work in a similar fashion. Imagine a list with two types
  124. # of users:
  125. #
  126. # <%# app/views/users/index.html.erb %>
  127. # Here's the administrator:
  128. # <%= render partial: "user", layout: "administrator", locals: { user: administrator } %>
  129. #
  130. # Here's the editor:
  131. # <%= render partial: "user", layout: "editor", locals: { user: editor } %>
  132. #
  133. # <%# app/views/users/_user.html.erb %>
  134. # Name: <%= user.name %>
  135. #
  136. # <%# app/views/users/_administrator.html.erb %>
  137. # <div id="administrator">
  138. # Budget: $<%= user.budget %>
  139. # <%= yield %>
  140. # </div>
  141. #
  142. # <%# app/views/users/_editor.html.erb %>
  143. # <div id="editor">
  144. # Deadline: <%= user.deadline %>
  145. # <%= yield %>
  146. # </div>
  147. #
  148. # ...this will return:
  149. #
  150. # Here's the administrator:
  151. # <div id="administrator">
  152. # Budget: $<%= user.budget %>
  153. # Name: <%= user.name %>
  154. # </div>
  155. #
  156. # Here's the editor:
  157. # <div id="editor">
  158. # Deadline: <%= user.deadline %>
  159. # Name: <%= user.name %>
  160. # </div>
  161. #
  162. # If a collection is given, the layout will be rendered once for each item in
  163. # the collection. For example, these two snippets have the same output:
  164. #
  165. # <%# app/views/users/_user.html.erb %>
  166. # Name: <%= user.name %>
  167. #
  168. # <%# app/views/users/index.html.erb %>
  169. # <%# This does not use layouts %>
  170. # <ul>
  171. # <% users.each do |user| -%>
  172. # <li>
  173. # <%= render partial: "user", locals: { user: user } %>
  174. # </li>
  175. # <% end -%>
  176. # </ul>
  177. #
  178. # <%# app/views/users/_li_layout.html.erb %>
  179. # <li>
  180. # <%= yield %>
  181. # </li>
  182. #
  183. # <%# app/views/users/index.html.erb %>
  184. # <ul>
  185. # <%= render partial: "user", layout: "li_layout", collection: users %>
  186. # </ul>
  187. #
  188. # Given two users whose names are Alice and Bob, these snippets return:
  189. #
  190. # <ul>
  191. # <li>
  192. # Name: Alice
  193. # </li>
  194. # <li>
  195. # Name: Bob
  196. # </li>
  197. # </ul>
  198. #
  199. # The current object being rendered, as well as the object_counter, will be
  200. # available as local variables inside the layout template under the same names
  201. # as available in the partial.
  202. #
  203. # You can also apply a layout to a block within any template:
  204. #
  205. # <%# app/views/users/_chief.html.erb %>
  206. # <%= render(layout: "administrator", locals: { user: chief }) do %>
  207. # Title: <%= chief.title %>
  208. # <% end %>
  209. #
  210. # ...this will return:
  211. #
  212. # <div id="administrator">
  213. # Budget: $<%= user.budget %>
  214. # Title: <%= chief.name %>
  215. # </div>
  216. #
  217. # As you can see, the <tt>:locals</tt> hash is shared between both the partial and its layout.
  218. #
  219. # If you pass arguments to "yield" then this will be passed to the block. One way to use this is to pass
  220. # an array to layout and treat it as an enumerable.
  221. #
  222. # <%# app/views/users/_user.html.erb %>
  223. # <div class="user">
  224. # Budget: $<%= user.budget %>
  225. # <%= yield user %>
  226. # </div>
  227. #
  228. # <%# app/views/users/index.html.erb %>
  229. # <%= render layout: @users do |user| %>
  230. # Title: <%= user.title %>
  231. # <% end %>
  232. #
  233. # This will render the layout for each user and yield to the block, passing the user, each time.
  234. #
  235. # You can also yield multiple times in one layout and use block arguments to differentiate the sections.
  236. #
  237. # <%# app/views/users/_user.html.erb %>
  238. # <div class="user">
  239. # <%= yield user, :header %>
  240. # Budget: $<%= user.budget %>
  241. # <%= yield user, :footer %>
  242. # </div>
  243. #
  244. # <%# app/views/users/index.html.erb %>
  245. # <%= render layout: @users do |user, section| %>
  246. # <%- case section when :header -%>
  247. # Title: <%= user.title %>
  248. # <%- when :footer -%>
  249. # Deadline: <%= user.deadline %>
  250. # <%- end -%>
  251. # <% end %>
  252. 3 class PartialRenderer < AbstractRenderer
  253. 3 include CollectionCaching
  254. 3 def initialize(lookup_context, options)
  255. super(lookup_context)
  256. @options = options
  257. @locals = @options[:locals] || {}
  258. @details = extract_details(@options)
  259. end
  260. 3 def render(partial, context, block)
  261. template = find_template(partial, template_keys(partial))
  262. if !block && (layout = @options[:layout])
  263. layout = find_template(layout.to_s, template_keys(partial))
  264. end
  265. render_partial_template(context, @locals, template, layout, block)
  266. end
  267. 3 private
  268. 3 def template_keys(_)
  269. @locals.keys
  270. end
  271. 3 def render_partial_template(view, locals, template, layout, block)
  272. ActiveSupport::Notifications.instrument(
  273. "render_partial.action_view",
  274. identifier: template.identifier,
  275. layout: layout && layout.virtual_path
  276. ) do |payload|
  277. content = template.render(view, locals, add_to_stack: !block) do |*name|
  278. view._layout_for(*name, &block)
  279. end
  280. content = layout.render(view, locals) { content } if layout
  281. payload[:cache_hit] = view.view_renderer.cache_hits[template.virtual_path]
  282. build_rendered_template(content, template)
  283. end
  284. end
  285. 3 def find_template(path, locals)
  286. prefixes = path.include?(?/) ? [] : @lookup_context.prefixes
  287. @lookup_context.find_template(path, prefixes, true, locals, @details)
  288. end
  289. end
  290. end

lib/action_view/renderer/partial_renderer/collection_caching.rb

32.5% lines covered

40 relevant lines. 13 lines covered and 27 lines missed.
    
  1. # frozen_string_literal: true
  2. 3 require "active_support/core_ext/enumerable"
  3. 3 module ActionView
  4. 3 module CollectionCaching # :nodoc:
  5. 3 extend ActiveSupport::Concern
  6. 3 included do
  7. # Fallback cache store if Action View is used without Rails.
  8. # Otherwise overridden in Railtie to use Rails.cache.
  9. 3 mattr_accessor :collection_cache, default: ActiveSupport::Cache::MemoryStore.new
  10. end
  11. 3 private
  12. 3 def will_cache?(options, view)
  13. options[:cached] && view.controller.respond_to?(:perform_caching) && view.controller.perform_caching
  14. end
  15. 3 def cache_collection_render(instrumentation_payload, view, template, collection)
  16. return yield(collection) unless will_cache?(@options, view)
  17. collection_iterator = collection
  18. # Result is a hash with the key represents the
  19. # key used for cache lookup and the value is the item
  20. # on which the partial is being rendered
  21. keyed_collection, ordered_keys = collection_by_cache_keys(view, template, collection)
  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)
  27. instrumentation_payload[:cache_hits] = cached_partials.size
  28. # Extract the items for the keys that are not found
  29. collection = keyed_collection.reject { |key, _| cached_partials.key?(key) }.values
  30. rendered_partials = collection.empty? ? [] : yield(collection_iterator.from_collection(collection))
  31. index = 0
  32. keyed_partials = fetch_or_cache_partial(cached_partials, template, order_by: keyed_collection.each_key) do
  33. # This block is called once
  34. # for every cache miss while preserving order.
  35. rendered_partials[index].tap { index += 1 }
  36. end
  37. ordered_keys.map do |key|
  38. keyed_partials[key]
  39. end
  40. end
  41. 3 def callable_cache_key?
  42. @options[:cached].respond_to?(:call)
  43. end
  44. 3 def collection_by_cache_keys(view, template, collection)
  45. seed = callable_cache_key? ? @options[:cached] : ->(i) { i }
  46. digest_path = view.digest_path_from_template(template)
  47. collection.each_with_object([{}, []]) do |item, (hash, ordered_keys)|
  48. key = expanded_cache_key(seed.call(item), view, template, digest_path)
  49. ordered_keys << key
  50. hash[key] = item
  51. end
  52. end
  53. 3 def expanded_cache_key(key, view, template, digest_path)
  54. key = view.combined_fragment_cache_key(view.cache_fragment_name(key, digest_path: digest_path))
  55. key.frozen? ? key.dup : key # #read_multi & #write may require mutability, Dalli 2.6.0.
  56. end
  57. # `order_by` is an enumerable object containing keys of the cache,
  58. # all keys are passed in whether found already or not.
  59. #
  60. # `cached_partials` is a hash. If the value exists
  61. # it represents the rendered partial from the cache
  62. # otherwise `Hash#fetch` will take the value of its block.
  63. #
  64. # This method expects a block that will return the rendered
  65. # partial. An example is to render all results
  66. # for each element that was not found in the cache and store it as an array.
  67. # Order it so that the first empty cache element in `cached_partials`
  68. # corresponds to the first element in `rendered_partials`.
  69. #
  70. # If the partial is not already cached it will also be
  71. # written back to the underlying cache store.
  72. 3 def fetch_or_cache_partial(cached_partials, template, order_by:)
  73. order_by.index_with do |cache_key|
  74. if content = cached_partials[cache_key]
  75. build_rendered_template(content, template)
  76. else
  77. yield.tap do |rendered_partial|
  78. collection_cache.write(cache_key, rendered_partial.body)
  79. end
  80. end
  81. end
  82. end
  83. end
  84. end

lib/action_view/renderer/renderer.rb

0.0% lines covered

77 relevant lines. 0 lines covered and 77 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView
  3. # This is the main entry point for rendering. It basically delegates
  4. # to other objects like TemplateRenderer and PartialRenderer which
  5. # actually renders the template.
  6. #
  7. # The Renderer will parse the options from the +render+ or +render_body+
  8. # method and render a partial or a template based on the options. The
  9. # +TemplateRenderer+ and +PartialRenderer+ objects are wrappers which do all
  10. # the setup and logic necessary to render a view and a new object is created
  11. # each time +render+ is called.
  12. class Renderer
  13. attr_accessor :lookup_context
  14. def initialize(lookup_context)
  15. @lookup_context = lookup_context
  16. end
  17. # Main render entry point shared by Action View and Action Controller.
  18. def render(context, options)
  19. render_to_object(context, options).body
  20. end
  21. def render_to_object(context, options) # :nodoc:
  22. if options.key?(:partial)
  23. render_partial_to_object(context, options)
  24. elsif options.key?(:object)
  25. object = options[:object]
  26. AbstractRenderer::RenderedTemplate.new(object.render_in(context), object)
  27. else
  28. render_template_to_object(context, options)
  29. end
  30. end
  31. # Render but returns a valid Rack body. If fibers are defined, we return
  32. # a streaming body that renders the template piece by piece.
  33. #
  34. # Note that partials are not supported to be rendered with streaming,
  35. # so in such cases, we just wrap them in an array.
  36. def render_body(context, options)
  37. if options.key?(:partial)
  38. [render_partial(context, options)]
  39. else
  40. StreamingTemplateRenderer.new(@lookup_context).render(context, options)
  41. end
  42. end
  43. # Direct access to template rendering.
  44. def render_template(context, options) #:nodoc:
  45. render_template_to_object(context, options).body
  46. end
  47. # Direct access to partial rendering.
  48. def render_partial(context, options, &block) #:nodoc:
  49. render_partial_to_object(context, options, &block).body
  50. end
  51. def cache_hits # :nodoc:
  52. @cache_hits ||= {}
  53. end
  54. def render_template_to_object(context, options) #:nodoc:
  55. TemplateRenderer.new(@lookup_context).render(context, options)
  56. end
  57. def render_partial_to_object(context, options, &block) #:nodoc:
  58. partial = options[:partial]
  59. if String === partial
  60. collection = collection_from_options(options)
  61. if collection
  62. # Collection + Partial
  63. renderer = CollectionRenderer.new(@lookup_context, options)
  64. renderer.render_collection_with_partial(collection, partial, context, block)
  65. else
  66. if options.key?(:object)
  67. # Object + Partial
  68. renderer = ObjectRenderer.new(@lookup_context, options)
  69. renderer.render_object_with_partial(options[:object], partial, context, block)
  70. else
  71. # Partial
  72. renderer = PartialRenderer.new(@lookup_context, options)
  73. renderer.render(partial, context, block)
  74. end
  75. end
  76. else
  77. collection = collection_from_object(partial) || collection_from_options(options)
  78. if collection
  79. # Collection + Derived Partial
  80. renderer = CollectionRenderer.new(@lookup_context, options)
  81. renderer.render_collection_derive_partial(collection, context, block)
  82. else
  83. # Object + Derived Partial
  84. renderer = ObjectRenderer.new(@lookup_context, options)
  85. renderer.render_object_derive_partial(partial, context, block)
  86. end
  87. end
  88. end
  89. private
  90. def collection_from_options(options)
  91. if options.key?(:collection)
  92. collection = options[:collection]
  93. collection || []
  94. end
  95. end
  96. def collection_from_object(object)
  97. object if object.respond_to?(:to_ary)
  98. end
  99. end
  100. end

lib/action_view/renderer/streaming_template_renderer.rb

0.0% lines covered

63 relevant lines. 0 lines covered and 63 lines missed.
    
  1. # frozen_string_literal: true
  2. require "fiber"
  3. module ActionView
  4. # == TODO
  5. #
  6. # * Support streaming from child templates, partials and so on.
  7. # * Rack::Cache needs to support streaming bodies
  8. class StreamingTemplateRenderer < TemplateRenderer #:nodoc:
  9. # A valid Rack::Body (i.e. it responds to each).
  10. # It is initialized with a block that, when called, starts
  11. # rendering the template.
  12. class Body #:nodoc:
  13. def initialize(&start)
  14. @start = start
  15. end
  16. def each(&block)
  17. begin
  18. @start.call(block)
  19. rescue Exception => exception
  20. log_error(exception)
  21. block.call ActionView::Base.streaming_completion_on_exception
  22. end
  23. self
  24. end
  25. private
  26. # This is the same logging logic as in ShowExceptions middleware.
  27. def log_error(exception)
  28. logger = ActionView::Base.logger
  29. return unless logger
  30. message = +"\n#{exception.class} (#{exception.message}):\n"
  31. message << exception.annotated_source_code.to_s if exception.respond_to?(:annotated_source_code)
  32. message << " " << exception.backtrace.join("\n ")
  33. logger.fatal("#{message}\n\n")
  34. end
  35. end
  36. # For streaming, instead of rendering a given a template, we return a Body
  37. # object that responds to each. This object is initialized with a block
  38. # that knows how to render the template.
  39. def render_template(view, template, layout_name = nil, locals = {}) #:nodoc:
  40. return [super.body] unless layout_name && template.supports_streaming?
  41. locals ||= {}
  42. layout = layout_name && find_layout(layout_name, locals.keys, [formats.first])
  43. Body.new do |buffer|
  44. delayed_render(buffer, template, layout, view, locals)
  45. end
  46. end
  47. private
  48. def delayed_render(buffer, template, layout, view, locals)
  49. # Wrap the given buffer in the StreamingBuffer and pass it to the
  50. # underlying template handler. Now, every time something is concatenated
  51. # to the buffer, it is not appended to an array, but streamed straight
  52. # to the client.
  53. output = ActionView::StreamingBuffer.new(buffer)
  54. yielder = lambda { |*name| view._layout_for(*name) }
  55. ActiveSupport::Notifications.instrument(
  56. "render_template.action_view",
  57. identifier: template.identifier,
  58. layout: layout && layout.virtual_path
  59. ) do
  60. outer_config = I18n.config
  61. fiber = Fiber.new do
  62. I18n.config = outer_config
  63. if layout
  64. layout.render(view, locals, output, &yielder)
  65. else
  66. # If you don't have a layout, just render the thing
  67. # and concatenate the final result. This is the same
  68. # as a layout with just <%= yield %>
  69. output.safe_concat view._layout_for
  70. end
  71. end
  72. # Set the view flow to support streaming. It will be aware
  73. # when to stop rendering the layout because it needs to search
  74. # something in the template and vice-versa.
  75. view.view_flow = StreamingFlow.new(view, fiber)
  76. # Yo! Start the fiber!
  77. fiber.resume
  78. # If the fiber is still alive, it means we need something
  79. # from the template, so start rendering it. If not, it means
  80. # the layout exited without requiring anything from the template.
  81. if fiber.alive?
  82. content = template.render(view, locals, &yielder)
  83. # Once rendering the template is done, sets its content in the :layout key.
  84. view.view_flow.set(:layout, content)
  85. # In case the layout continues yielding, we need to resume
  86. # the fiber until all yields are handled.
  87. fiber.resume while fiber.alive?
  88. end
  89. end
  90. end
  91. end
  92. end

lib/action_view/renderer/template_renderer.rb

0.0% lines covered

92 relevant lines. 0 lines covered and 92 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView
  3. class TemplateRenderer < AbstractRenderer #:nodoc:
  4. def render(context, options)
  5. @details = extract_details(options)
  6. template = determine_template(options)
  7. prepend_formats(template.format)
  8. render_template(context, template, options[:layout], options[:locals] || {})
  9. end
  10. private
  11. # Determine the template to be rendered using the given options.
  12. def determine_template(options)
  13. keys = options.has_key?(:locals) ? options[:locals].keys : []
  14. if options.key?(:body)
  15. Template::Text.new(options[:body])
  16. elsif options.key?(:plain)
  17. Template::Text.new(options[:plain])
  18. elsif options.key?(:html)
  19. Template::HTML.new(options[:html], formats.first)
  20. elsif options.key?(:file)
  21. if File.exist?(options[:file])
  22. Template::RawFile.new(options[:file])
  23. else
  24. ActiveSupport::Deprecation.warn "render file: should be given the absolute path to a file"
  25. @lookup_context.with_fallbacks.find_template(options[:file], nil, false, keys, @details)
  26. end
  27. elsif options.key?(:inline)
  28. handler = Template.handler_for_extension(options[:type] || "erb")
  29. format = if handler.respond_to?(:default_format)
  30. handler.default_format
  31. else
  32. @lookup_context.formats.first
  33. end
  34. Template::Inline.new(options[:inline], "inline template", handler, locals: keys, format: format)
  35. elsif options.key?(:template)
  36. if options[:template].respond_to?(:render)
  37. options[:template]
  38. else
  39. @lookup_context.find_template(options[:template], options[:prefixes], false, keys, @details)
  40. end
  41. else
  42. raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :plain, :html or :body option."
  43. end
  44. end
  45. # Renders the given template. A string representing the layout can be
  46. # supplied as well.
  47. def render_template(view, template, layout_name, locals)
  48. render_with_layout(view, template, layout_name, locals) do |layout|
  49. ActiveSupport::Notifications.instrument(
  50. "render_template.action_view",
  51. identifier: template.identifier,
  52. layout: layout && layout.virtual_path
  53. ) do
  54. template.render(view, locals) { |*name| view._layout_for(*name) }
  55. end
  56. end
  57. end
  58. def render_with_layout(view, template, path, locals)
  59. layout = path && find_layout(path, locals.keys, [formats.first])
  60. body = if layout
  61. ActiveSupport::Notifications.instrument("render_layout.action_view", identifier: layout.identifier) do
  62. view.view_flow.set(:layout, yield(layout))
  63. layout.render(view, locals) { |*name| view._layout_for(*name) }
  64. end
  65. else
  66. yield
  67. end
  68. build_rendered_template(body, template)
  69. end
  70. # This is the method which actually finds the layout using details in the lookup
  71. # context object. If no layout is found, it checks if at least a layout with
  72. # the given name exists across all details before raising the error.
  73. def find_layout(layout, keys, formats)
  74. resolve_layout(layout, keys, formats)
  75. end
  76. def resolve_layout(layout, keys, formats)
  77. details = @details.dup
  78. details[:formats] = formats
  79. case layout
  80. when String
  81. begin
  82. if layout.start_with?("/")
  83. ActiveSupport::Deprecation.warn "Rendering layouts from an absolute path is deprecated."
  84. @lookup_context.with_fallbacks.find_template(layout, nil, false, [], details)
  85. else
  86. @lookup_context.find_template(layout, nil, false, [], details)
  87. end
  88. rescue ActionView::MissingTemplate
  89. all_details = @details.merge(formats: @lookup_context.default_formats)
  90. raise unless template_exists?(layout, nil, false, [], **all_details)
  91. end
  92. when Proc
  93. resolve_layout(layout.call(@lookup_context, formats), keys, formats)
  94. else
  95. layout
  96. end
  97. end
  98. end
  99. end

lib/action_view/rendering.rb

32.53% lines covered

83 relevant lines. 27 lines covered and 56 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 require "action_view/view_paths"
  3. 9 module ActionView
  4. # This is a class to fix I18n global state. Whenever you provide I18n.locale during a request,
  5. # it will trigger the lookup_context and consequently expire the cache.
  6. 9 class I18nProxy < ::I18n::Config #:nodoc:
  7. 9 attr_reader :original_config, :lookup_context
  8. 9 def initialize(original_config, lookup_context)
  9. original_config = original_config.original_config if original_config.respond_to?(:original_config)
  10. @original_config, @lookup_context = original_config, lookup_context
  11. end
  12. 9 def locale
  13. @original_config.locale
  14. end
  15. 9 def locale=(value)
  16. @lookup_context.locale = value
  17. end
  18. end
  19. 9 module Rendering
  20. 9 extend ActiveSupport::Concern
  21. 9 include ActionView::ViewPaths
  22. 9 attr_reader :rendered_format
  23. 9 def initialize
  24. @rendered_format = nil
  25. super
  26. end
  27. # Overwrite process to set up I18n proxy.
  28. 9 def process(*) #:nodoc:
  29. old_config, I18n.config = I18n.config, I18nProxy.new(I18n.config, lookup_context)
  30. super
  31. ensure
  32. I18n.config = old_config
  33. end
  34. 9 module ClassMethods
  35. 9 def _routes
  36. end
  37. 9 def _helpers
  38. end
  39. 9 def build_view_context_class(klass, supports_path, routes, helpers)
  40. Class.new(klass) do
  41. if routes
  42. include routes.url_helpers(supports_path)
  43. include routes.mounted_helpers
  44. end
  45. if helpers
  46. include helpers
  47. end
  48. end
  49. end
  50. 9 def view_context_class
  51. klass = ActionView::LookupContext::DetailsKey.view_context_class(ActionView::Base)
  52. @view_context_class ||= build_view_context_class(klass, supports_path?, _routes, _helpers)
  53. if klass.changed?(@view_context_class)
  54. @view_context_class = build_view_context_class(klass, supports_path?, _routes, _helpers)
  55. end
  56. @view_context_class
  57. end
  58. end
  59. 9 def view_context_class
  60. self.class.view_context_class
  61. end
  62. # An instance of a view class. The default view class is ActionView::Base.
  63. #
  64. # The view class must have the following methods:
  65. #
  66. # * <tt>View.new(lookup_context, assigns, controller)</tt> — Create a new
  67. # ActionView instance for a controller and we can also pass the arguments.
  68. #
  69. # * <tt>View#render(option)</tt> — Returns String with the rendered template.
  70. #
  71. # Override this method in a module to change the default behavior.
  72. 9 def view_context
  73. view_context_class.new(lookup_context, view_assigns, self)
  74. end
  75. # Returns an object that is able to render templates.
  76. 9 def view_renderer # :nodoc:
  77. # Lifespan: Per controller
  78. @_view_renderer ||= ActionView::Renderer.new(lookup_context)
  79. end
  80. 9 def render_to_body(options = {})
  81. _process_options(options)
  82. _render_template(options)
  83. end
  84. 9 private
  85. # Find and render a template based on the options given.
  86. 9 def _render_template(options)
  87. variant = options.delete(:variant)
  88. assigns = options.delete(:assigns)
  89. context = view_context
  90. context.assign assigns if assigns
  91. lookup_context.variants = variant if variant
  92. rendered_template = context.in_rendering_context(options) do |renderer|
  93. renderer.render_to_object(context, options)
  94. end
  95. rendered_format = rendered_template.format || lookup_context.formats.first
  96. @rendered_format = Template::Types[rendered_format]
  97. rendered_template.body
  98. end
  99. # Assign the rendered format to look up context.
  100. 9 def _process_format(format)
  101. super
  102. lookup_context.formats = [format.to_sym] if format.to_sym
  103. end
  104. # Normalize args by converting render "foo" to render :action => "foo" and
  105. # render "foo/bar" to render :template => "foo/bar".
  106. 9 def _normalize_args(action = nil, options = {})
  107. options = super(action, options)
  108. case action
  109. when NilClass
  110. when Hash
  111. options = action
  112. when String, Symbol
  113. action = action.to_s
  114. key = action.include?(?/) ? :template : :action
  115. options[key] = action
  116. else
  117. if action.respond_to?(:permitted?) && action.permitted?
  118. options = action
  119. elsif action.respond_to?(:render_in)
  120. options[:object] = action
  121. else
  122. options[:partial] = action
  123. end
  124. end
  125. options
  126. end
  127. # Normalize options.
  128. 9 def _normalize_options(options)
  129. options = super(options)
  130. if options[:partial] == true
  131. options[:partial] = action_name
  132. end
  133. if (options.keys & [:partial, :file, :template]).empty?
  134. options[:prefixes] ||= _prefixes
  135. end
  136. options[:template] ||= (options[:action] || action_name).to_s
  137. options
  138. end
  139. end
  140. end

lib/action_view/routing_url_for.rb

25.64% lines covered

39 relevant lines. 10 lines covered and 29 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 require "action_dispatch/routing/polymorphic_routes"
  3. 9 module ActionView
  4. 9 module RoutingUrlFor
  5. # Returns the URL for the set of +options+ provided. This takes the
  6. # same options as +url_for+ in Action Controller (see the
  7. # documentation for <tt>ActionController::Base#url_for</tt>). Note that by default
  8. # <tt>:only_path</tt> is <tt>true</tt> so you'll get the relative "/controller/action"
  9. # instead of the fully qualified URL like "http://example.com/controller/action".
  10. #
  11. # ==== Options
  12. # * <tt>:anchor</tt> - Specifies the anchor name to be appended to the path.
  13. # * <tt>:only_path</tt> - If true, returns the relative URL (omitting the protocol, host name, and port) (<tt>true</tt> by default unless <tt>:host</tt> is specified).
  14. # * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2005/". Note that this
  15. # is currently not recommended since it breaks caching.
  16. # * <tt>:host</tt> - Overrides the default (current) host if provided.
  17. # * <tt>:protocol</tt> - Overrides the default (current) protocol if provided.
  18. # * <tt>:user</tt> - Inline HTTP authentication (only plucked out if <tt>:password</tt> is also present).
  19. # * <tt>:password</tt> - Inline HTTP authentication (only plucked out if <tt>:user</tt> is also present).
  20. #
  21. # ==== Relying on named routes
  22. #
  23. # Passing a record (like an Active Record) instead of a hash as the options parameter will
  24. # trigger the named route for that record. The lookup will happen on the name of the class. So passing a
  25. # Workshop object will attempt to use the +workshop_path+ route. If you have a nested route, such as
  26. # +admin_workshop_path+ you'll have to call that explicitly (it's impossible for +url_for+ to guess that route).
  27. #
  28. # ==== Implicit Controller Namespacing
  29. #
  30. # Controllers passed in using the +:controller+ option will retain their namespace unless it is an absolute one.
  31. #
  32. # ==== Examples
  33. # <%= url_for(action: 'index') %>
  34. # # => /blogs/
  35. #
  36. # <%= url_for(action: 'find', controller: 'books') %>
  37. # # => /books/find
  38. #
  39. # <%= url_for(action: 'login', controller: 'members', only_path: false, protocol: 'https') %>
  40. # # => https://www.example.com/members/login/
  41. #
  42. # <%= url_for(action: 'play', anchor: 'player') %>
  43. # # => /messages/play/#player
  44. #
  45. # <%= url_for(action: 'jump', anchor: 'tax&ship') %>
  46. # # => /testing/jump/#tax&ship
  47. #
  48. # <%= url_for(Workshop.new) %>
  49. # # relies on Workshop answering a persisted? call (and in this case returning false)
  50. # # => /workshops
  51. #
  52. # <%= url_for(@workshop) %>
  53. # # calls @workshop.to_param which by default returns the id
  54. # # => /workshops/5
  55. #
  56. # # to_param can be re-defined in a model to provide different URL names:
  57. # # => /workshops/1-workshop-name
  58. #
  59. # <%= url_for("http://www.example.com") %>
  60. # # => http://www.example.com
  61. #
  62. # <%= url_for(:back) %>
  63. # # if request.env["HTTP_REFERER"] is set to "http://www.example.com"
  64. # # => http://www.example.com
  65. #
  66. # <%= url_for(:back) %>
  67. # # if request.env["HTTP_REFERER"] is not set or is blank
  68. # # => javascript:history.back()
  69. #
  70. # <%= url_for(action: 'index', controller: 'users') %>
  71. # # Assuming an "admin" namespace
  72. # # => /admin/users
  73. #
  74. # <%= url_for(action: 'index', controller: '/users') %>
  75. # # Specify absolute path with beginning slash
  76. # # => /users
  77. 9 def url_for(options = nil)
  78. case options
  79. when String
  80. options
  81. when nil
  82. super(only_path: _generate_paths_by_default)
  83. when Hash
  84. options = options.symbolize_keys
  85. ensure_only_path_option(options)
  86. super(options)
  87. when ActionController::Parameters
  88. ensure_only_path_option(options)
  89. super(options)
  90. when :back
  91. _back_url
  92. when Array
  93. components = options.dup
  94. options = components.extract_options!
  95. ensure_only_path_option(options)
  96. if options[:only_path]
  97. polymorphic_path(components, options)
  98. else
  99. polymorphic_url(components, options)
  100. end
  101. else
  102. method = _generate_paths_by_default ? :path : :url
  103. builder = ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.send(method)
  104. case options
  105. when Symbol
  106. builder.handle_string_call(self, options)
  107. when Class
  108. builder.handle_class_call(self, options)
  109. else
  110. builder.handle_model_call(self, options)
  111. end
  112. end
  113. end
  114. 9 def url_options #:nodoc:
  115. return super unless controller.respond_to?(:url_options)
  116. controller.url_options
  117. end
  118. 9 private
  119. 9 def _routes_context
  120. controller
  121. end
  122. 9 def optimize_routes_generation?
  123. controller.respond_to?(:optimize_routes_generation?, true) ?
  124. controller.optimize_routes_generation? : super
  125. end
  126. 9 def _generate_paths_by_default
  127. true
  128. end
  129. 9 def ensure_only_path_option(options)
  130. unless options.key?(:only_path)
  131. options[:only_path] = _generate_paths_by_default unless options[:host]
  132. end
  133. end
  134. end
  135. end

lib/action_view/template.rb

38.35% lines covered

133 relevant lines. 51 lines covered and 82 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 require "thread"
  3. 9 require "delegate"
  4. 9 module ActionView
  5. # = Action View Template
  6. 9 class Template
  7. 9 extend ActiveSupport::Autoload
  8. 9 def self.finalize_compiled_template_methods
  9. ActiveSupport::Deprecation.warn "ActionView::Template.finalize_compiled_template_methods is deprecated and has no effect"
  10. end
  11. 9 def self.finalize_compiled_template_methods=(_)
  12. ActiveSupport::Deprecation.warn "ActionView::Template.finalize_compiled_template_methods= is deprecated and has no effect"
  13. end
  14. # === Encodings in ActionView::Template
  15. #
  16. # ActionView::Template is one of a few sources of potential
  17. # encoding issues in Rails. This is because the source for
  18. # templates are usually read from disk, and Ruby (like most
  19. # encoding-aware programming languages) assumes that the
  20. # String retrieved through File IO is encoded in the
  21. # <tt>default_external</tt> encoding. In Rails, the default
  22. # <tt>default_external</tt> encoding is UTF-8.
  23. #
  24. # As a result, if a user saves their template as ISO-8859-1
  25. # (for instance, using a non-Unicode-aware text editor),
  26. # and uses characters outside of the ASCII range, their
  27. # users will see diamonds with question marks in them in
  28. # the browser.
  29. #
  30. # For the rest of this documentation, when we say "UTF-8",
  31. # we mean "UTF-8 or whatever the default_internal encoding
  32. # is set to". By default, it will be UTF-8.
  33. #
  34. # To mitigate this problem, we use a few strategies:
  35. # 1. If the source is not valid UTF-8, we raise an exception
  36. # when the template is compiled to alert the user
  37. # to the problem.
  38. # 2. The user can specify the encoding using Ruby-style
  39. # encoding comments in any template engine. If such
  40. # a comment is supplied, Rails will apply that encoding
  41. # to the resulting compiled source returned by the
  42. # template handler.
  43. # 3. In all cases, we transcode the resulting String to
  44. # the UTF-8.
  45. #
  46. # This means that other parts of Rails can always assume
  47. # that templates are encoded in UTF-8, even if the original
  48. # source of the template was not UTF-8.
  49. #
  50. # From a user's perspective, the easiest thing to do is
  51. # to save your templates as UTF-8. If you do this, you
  52. # do not need to do anything else for things to "just work".
  53. #
  54. # === Instructions for template handlers
  55. #
  56. # The easiest thing for you to do is to simply ignore
  57. # encodings. Rails will hand you the template source
  58. # as the default_internal (generally UTF-8), raising
  59. # an exception for the user before sending the template
  60. # to you if it could not determine the original encoding.
  61. #
  62. # For the greatest simplicity, you can support only
  63. # UTF-8 as the <tt>default_internal</tt>. This means
  64. # that from the perspective of your handler, the
  65. # entire pipeline is just UTF-8.
  66. #
  67. # === Advanced: Handlers with alternate metadata sources
  68. #
  69. # If you want to provide an alternate mechanism for
  70. # specifying encodings (like ERB does via <%# encoding: ... %>),
  71. # you may indicate that you will handle encodings yourself
  72. # by implementing <tt>handles_encoding?</tt> on your handler.
  73. #
  74. # If you do, Rails will not try to encode the String
  75. # into the default_internal, passing you the unaltered
  76. # bytes tagged with the assumed encoding (from
  77. # default_external).
  78. #
  79. # In this case, make sure you return a String from
  80. # your handler encoded in the default_internal. Since
  81. # you are handling out-of-band metadata, you are
  82. # also responsible for alerting the user to any
  83. # problems with converting the user's data to
  84. # the <tt>default_internal</tt>.
  85. #
  86. # To do so, simply raise +WrongEncodingError+ as follows:
  87. #
  88. # raise WrongEncodingError.new(
  89. # problematic_string,
  90. # expected_encoding
  91. # )
  92. ##
  93. # :method: local_assigns
  94. #
  95. # Returns a hash with the defined local variables.
  96. #
  97. # Given this sub template rendering:
  98. #
  99. # <%= render "shared/header", { headline: "Welcome", person: person } %>
  100. #
  101. # You can use +local_assigns+ in the sub templates to access the local variables:
  102. #
  103. # local_assigns[:headline] # => "Welcome"
  104. 9 eager_autoload do
  105. 9 autoload :Error
  106. 9 autoload :RawFile
  107. 9 autoload :Handlers
  108. 9 autoload :HTML
  109. 9 autoload :Inline
  110. 9 autoload :Sources
  111. 9 autoload :Text
  112. 9 autoload :Types
  113. end
  114. 9 extend Template::Handlers
  115. 9 attr_reader :identifier, :handler, :original_encoding, :updated_at
  116. 9 attr_reader :variable, :format, :variant, :locals, :virtual_path
  117. 9 def initialize(source, identifier, handler, format: nil, variant: nil, locals: nil, virtual_path: nil, updated_at: nil)
  118. unless locals
  119. ActiveSupport::Deprecation.warn "ActionView::Template#initialize requires a locals parameter"
  120. locals = []
  121. end
  122. @source = source
  123. @identifier = identifier
  124. @handler = handler
  125. @compiled = false
  126. @locals = locals
  127. @virtual_path = virtual_path
  128. @variable = if @virtual_path
  129. base = @virtual_path.end_with?("/") ? "" : ::File.basename(@virtual_path)
  130. base =~ /\A_?(.*?)(?:\.\w+)*\z/
  131. $1.to_sym
  132. end
  133. if updated_at
  134. ActiveSupport::Deprecation.warn "ActionView::Template#updated_at is deprecated"
  135. @updated_at = updated_at
  136. else
  137. @updated_at = Time.now
  138. end
  139. @format = format
  140. @variant = variant
  141. @compile_mutex = Mutex.new
  142. end
  143. 9 deprecate :original_encoding
  144. 9 deprecate :updated_at
  145. 9 deprecate def virtual_path=(_); end
  146. 9 deprecate def locals=(_); end
  147. 9 deprecate def formats=(_); end
  148. 9 deprecate def formats; Array(format); end
  149. 9 deprecate def variants=(_); end
  150. 9 deprecate def variants; [variant]; end
  151. 9 deprecate def refresh(_); self; end
  152. # Returns whether the underlying handler supports streaming. If so,
  153. # a streaming buffer *may* be passed when it starts rendering.
  154. 9 def supports_streaming?
  155. handler.respond_to?(:supports_streaming?) && handler.supports_streaming?
  156. end
  157. # Render a template. If the template was not compiled yet, it is done
  158. # exactly before rendering.
  159. #
  160. # This method is instrumented as "!render_template.action_view". Notice that
  161. # we use a bang in this instrumentation because you don't want to
  162. # consume this in production. This is only slow if it's being listened to.
  163. 9 def render(view, locals, buffer = ActionView::OutputBuffer.new, add_to_stack: true, &block)
  164. instrument_render_template do
  165. compile!(view)
  166. view._run(method_name, self, locals, buffer, add_to_stack: add_to_stack, &block)
  167. end
  168. rescue => e
  169. handle_render_error(view, e)
  170. end
  171. 9 def type
  172. @type ||= Types[format]
  173. end
  174. 9 def short_identifier
  175. @short_identifier ||= defined?(Rails.root) ? identifier.delete_prefix("#{Rails.root}/") : identifier
  176. end
  177. 9 def inspect
  178. "#<#{self.class.name} #{short_identifier} locals=#{@locals.inspect}>"
  179. end
  180. 9 def source
  181. @source.to_s
  182. end
  183. # This method is responsible for properly setting the encoding of the
  184. # source. Until this point, we assume that the source is BINARY data.
  185. # If no additional information is supplied, we assume the encoding is
  186. # the same as <tt>Encoding.default_external</tt>.
  187. #
  188. # The user can also specify the encoding via a comment on the first
  189. # line of the template (# encoding: NAME-OF-ENCODING). This will work
  190. # with any template engine, as we process out the encoding comment
  191. # before passing the source on to the template engine, leaving a
  192. # blank line in its stead.
  193. 9 def encode!
  194. source = self.source
  195. return source unless source.encoding == Encoding::BINARY
  196. # Look for # encoding: *. If we find one, we'll encode the
  197. # String in that encoding, otherwise, we'll use the
  198. # default external encoding.
  199. if source.sub!(/\A#{ENCODING_FLAG}/, "")
  200. encoding = magic_encoding = $1
  201. else
  202. encoding = Encoding.default_external
  203. end
  204. # Tag the source with the default external encoding
  205. # or the encoding specified in the file
  206. source.force_encoding(encoding)
  207. # If the user didn't specify an encoding, and the handler
  208. # handles encodings, we simply pass the String as is to
  209. # the handler (with the default_external tag)
  210. if !magic_encoding && @handler.respond_to?(:handles_encoding?) && @handler.handles_encoding?
  211. source
  212. # Otherwise, if the String is valid in the encoding,
  213. # encode immediately to default_internal. This means
  214. # that if a handler doesn't handle encodings, it will
  215. # always get Strings in the default_internal
  216. elsif source.valid_encoding?
  217. source.encode!
  218. # Otherwise, since the String is invalid in the encoding
  219. # specified, raise an exception
  220. else
  221. raise WrongEncodingError.new(source, encoding)
  222. end
  223. end
  224. # Exceptions are marshalled when using the parallel test runner with DRb, so we need
  225. # to ensure that references to the template object can be marshalled as well. This means forgoing
  226. # the marshalling of the compiler mutex and instantiating that again on unmarshalling.
  227. 9 def marshal_dump # :nodoc:
  228. [ @source, @identifier, @handler, @compiled, @locals, @virtual_path, @updated_at, @format, @variant ]
  229. end
  230. 9 def marshal_load(array) # :nodoc:
  231. @source, @identifier, @handler, @compiled, @locals, @virtual_path, @updated_at, @format, @variant = *array
  232. @compile_mutex = Mutex.new
  233. end
  234. 9 private
  235. # Compile a template. This method ensures a template is compiled
  236. # just once and removes the source after it is compiled.
  237. 9 def compile!(view)
  238. return if @compiled
  239. # Templates can be used concurrently in threaded environments
  240. # so compilation and any instance variable modification must
  241. # be synchronized
  242. @compile_mutex.synchronize do
  243. # Any thread holding this lock will be compiling the template needed
  244. # by the threads waiting. So re-check the @compiled flag to avoid
  245. # re-compilation
  246. return if @compiled
  247. mod = view.compiled_method_container
  248. instrument("!compile_template") do
  249. compile(mod)
  250. end
  251. @compiled = true
  252. end
  253. end
  254. 9 class LegacyTemplate < DelegateClass(Template) # :nodoc:
  255. 9 attr_reader :source
  256. 9 def initialize(template, source)
  257. super(template)
  258. @source = source
  259. end
  260. end
  261. # Among other things, this method is responsible for properly setting
  262. # the encoding of the compiled template.
  263. #
  264. # If the template engine handles encodings, we send the encoded
  265. # String to the engine without further processing. This allows
  266. # the template engine to support additional mechanisms for
  267. # specifying the encoding. For instance, ERB supports <%# encoding: %>
  268. #
  269. # Otherwise, after we figure out the correct encoding, we then
  270. # encode the source into <tt>Encoding.default_internal</tt>.
  271. # In general, this means that templates will be UTF-8 inside of Rails,
  272. # regardless of the original source encoding.
  273. 9 def compile(mod)
  274. source = encode!
  275. code = @handler.call(self, source)
  276. # Make sure that the resulting String to be eval'd is in the
  277. # encoding of the code
  278. original_source = source
  279. source = +<<-end_src
  280. def #{method_name}(local_assigns, output_buffer)
  281. @virtual_path = #{@virtual_path.inspect};#{locals_code};#{code}
  282. end
  283. end_src
  284. # Make sure the source is in the encoding of the returned code
  285. source.force_encoding(code.encoding)
  286. # In case we get back a String from a handler that is not in
  287. # BINARY or the default_internal, encode it to the default_internal
  288. source.encode!
  289. # Now, validate that the source we got back from the template
  290. # handler is valid in the default_internal. This is for handlers
  291. # that handle encoding but screw up
  292. unless source.valid_encoding?
  293. raise WrongEncodingError.new(source, Encoding.default_internal)
  294. end
  295. start_line = @handler.respond_to?(:start_line) ? @handler.start_line(self) : 0
  296. begin
  297. mod.module_eval(source, identifier, start_line)
  298. rescue SyntaxError
  299. # Account for when code in the template is not syntactically valid; e.g. if we're using
  300. # ERB and the user writes <%= foo( %>, attempting to call a helper `foo` and interpolate
  301. # the result into the template, but missing an end parenthesis.
  302. raise SyntaxErrorInTemplate.new(self, original_source)
  303. end
  304. end
  305. 9 def handle_render_error(view, e)
  306. if e.is_a?(Template::Error)
  307. e.sub_template_of(self)
  308. raise e
  309. else
  310. raise Template::Error.new(self)
  311. end
  312. end
  313. 9 def locals_code
  314. # Only locals with valid variable names get set directly. Others will
  315. # still be available in local_assigns.
  316. locals = @locals - Module::RUBY_RESERVED_KEYWORDS
  317. locals = locals.grep(/\A@?(?![A-Z0-9])(?:[[:alnum:]_]|[^\0-\177])+\z/)
  318. # Assign for the same variable is to suppress unused variable warning
  319. locals.each_with_object(+"") { |key, code| code << "#{key} = local_assigns[:#{key}]; #{key} = #{key};" }
  320. end
  321. 9 def method_name
  322. @method_name ||= begin
  323. m = +"_#{identifier_method_name}__#{@identifier.hash}_#{__id__}"
  324. m.tr!("-", "_")
  325. m
  326. end
  327. end
  328. 9 def identifier_method_name
  329. short_identifier.tr("^a-z_", "_")
  330. end
  331. 9 def instrument(action, &block) # :doc:
  332. ActiveSupport::Notifications.instrument("#{action}.action_view", instrument_payload, &block)
  333. end
  334. 9 def instrument_render_template(&block)
  335. ActiveSupport::Notifications.instrument("!render_template.action_view", instrument_payload, &block)
  336. end
  337. 9 def instrument_payload
  338. { virtual_path: @virtual_path, identifier: @identifier }
  339. end
  340. end
  341. end

lib/action_view/template/error.rb

0.0% lines covered

122 relevant lines. 0 lines covered and 122 lines missed.
    
  1. # frozen_string_literal: true
  2. require "active_support/core_ext/enumerable"
  3. module ActionView
  4. # = Action View Errors
  5. class ActionViewError < StandardError #:nodoc:
  6. end
  7. class EncodingError < StandardError #:nodoc:
  8. end
  9. class WrongEncodingError < EncodingError #:nodoc:
  10. def initialize(string, encoding)
  11. @string, @encoding = string, encoding
  12. end
  13. def message
  14. @string.force_encoding(Encoding::ASCII_8BIT)
  15. "Your template was not saved as valid #{@encoding}. Please " \
  16. "either specify #{@encoding} as the encoding for your template " \
  17. "in your text editor, or mark the template with its " \
  18. "encoding by inserting the following as the first line " \
  19. "of the template:\n\n# encoding: <name of correct encoding>.\n\n" \
  20. "The source of your template was:\n\n#{@string}"
  21. end
  22. end
  23. class MissingTemplate < ActionViewError #:nodoc:
  24. attr_reader :path
  25. def initialize(paths, path, prefixes, partial, details, *)
  26. @path = path
  27. prefixes = Array(prefixes)
  28. template_type = if partial
  29. "partial"
  30. elsif /layouts/i.match?(path)
  31. "layout"
  32. else
  33. "template"
  34. end
  35. if partial && path.present?
  36. path = path.sub(%r{([^/]+)$}, "_\\1")
  37. end
  38. searched_paths = prefixes.map { |prefix| [prefix, path].join("/") }
  39. out = "Missing #{template_type} #{searched_paths.join(", ")} with #{details.inspect}. Searched in:\n"
  40. out += paths.compact.map { |p| " * #{p.to_s.inspect}\n" }.join
  41. super out
  42. end
  43. end
  44. class Template
  45. # The Template::Error exception is raised when the compilation or rendering of the template
  46. # fails. This exception then gathers a bunch of intimate details and uses it to report a
  47. # precise exception message.
  48. class Error < ActionViewError #:nodoc:
  49. SOURCE_CODE_RADIUS = 3
  50. # Override to prevent #cause resetting during re-raise.
  51. attr_reader :cause
  52. def initialize(template)
  53. super($!.message)
  54. set_backtrace($!.backtrace)
  55. @cause = $!
  56. @template, @sub_templates = template, nil
  57. end
  58. def file_name
  59. @template.identifier
  60. end
  61. def sub_template_message
  62. if @sub_templates
  63. "Trace of template inclusion: " +
  64. @sub_templates.collect(&:inspect).join(", ")
  65. else
  66. ""
  67. end
  68. end
  69. def source_extract(indentation = 0)
  70. return [] unless num = line_number
  71. num = num.to_i
  72. source_code = @template.encode!.split("\n")
  73. start_on_line = [ num - SOURCE_CODE_RADIUS - 1, 0 ].max
  74. end_on_line = [ num + SOURCE_CODE_RADIUS - 1, source_code.length].min
  75. indent = end_on_line.to_s.size + indentation
  76. return [] unless source_code = source_code[start_on_line..end_on_line]
  77. formatted_code_for(source_code, start_on_line, indent)
  78. end
  79. def sub_template_of(template_path)
  80. @sub_templates ||= []
  81. @sub_templates << template_path
  82. end
  83. def line_number
  84. @line_number ||=
  85. if file_name
  86. regexp = /#{Regexp.escape File.basename(file_name)}:(\d+)/
  87. $1 if message =~ regexp || backtrace.find { |line| line =~ regexp }
  88. end
  89. end
  90. def annotated_source_code
  91. source_extract(4)
  92. end
  93. private
  94. def source_location
  95. if line_number
  96. "on line ##{line_number} of "
  97. else
  98. "in "
  99. end + file_name
  100. end
  101. def formatted_code_for(source_code, line_counter, indent)
  102. indent_template = "%#{indent}s: %s"
  103. source_code.map do |line|
  104. line_counter += 1
  105. indent_template % [line_counter, line]
  106. end
  107. end
  108. end
  109. end
  110. TemplateError = Template::Error
  111. class SyntaxErrorInTemplate < TemplateError #:nodoc
  112. def initialize(template, offending_code_string)
  113. @offending_code_string = offending_code_string
  114. super(template)
  115. end
  116. def message
  117. <<~MESSAGE
  118. Encountered a syntax error while rendering template: check #{@offending_code_string}
  119. MESSAGE
  120. end
  121. def annotated_source_code
  122. @offending_code_string.split("\n").map.with_index(1) { |line, index|
  123. indentation = " " * 4
  124. "#{index}:#{indentation}#{line}"
  125. }
  126. end
  127. end
  128. end

lib/action_view/template/handlers.rb

80.0% lines covered

45 relevant lines. 36 lines covered and 9 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 module ActionView #:nodoc:
  3. # = Action View Template Handlers
  4. 9 class Template #:nodoc:
  5. 9 module Handlers #:nodoc:
  6. 9 autoload :Raw, "action_view/template/handlers/raw"
  7. 9 autoload :ERB, "action_view/template/handlers/erb"
  8. 9 autoload :Html, "action_view/template/handlers/html"
  9. 9 autoload :Builder, "action_view/template/handlers/builder"
  10. 9 def self.extended(base)
  11. 9 base.register_default_template_handler :raw, Raw.new
  12. 9 base.register_template_handler :erb, ERB.new
  13. 9 base.register_template_handler :html, Html.new
  14. 9 base.register_template_handler :builder, Builder.new
  15. 9 base.register_template_handler :ruby, lambda { |_, source| source }
  16. end
  17. 9 @@template_handlers = {}
  18. 9 @@default_template_handlers = nil
  19. 9 def self.extensions
  20. @@template_extensions ||= @@template_handlers.keys
  21. end
  22. 9 class LegacyHandlerWrapper < SimpleDelegator # :nodoc:
  23. 9 def call(view, source)
  24. __getobj__.call(ActionView::Template::LegacyTemplate.new(view, source))
  25. end
  26. end
  27. # Register an object that knows how to handle template files with the given
  28. # extensions. This can be used to implement new template types.
  29. # The handler must respond to +:call+, which will be passed the template
  30. # and should return the rendered template as a String.
  31. 9 def register_template_handler(*extensions, handler)
  32. 45 params = if handler.is_a?(Proc)
  33. 9 handler.parameters
  34. else
  35. 36 handler.method(:call).parameters
  36. end
  37. 135 unless params.find_all { |type, _| type == :req || type == :opt }.length >= 2
  38. ActiveSupport::Deprecation.warn <<~eowarn
  39. Single arity template handlers are deprecated. Template handlers must
  40. now accept two parameters, the view object and the source for the view object.
  41. Change:
  42. >> #{handler}.call(#{params.map(&:last).join(", ")})
  43. To:
  44. >> #{handler}.call(#{params.map(&:last).join(", ")}, source)
  45. eowarn
  46. handler = LegacyHandlerWrapper.new(handler)
  47. end
  48. 45 raise(ArgumentError, "Extension is required") if extensions.empty?
  49. 45 extensions.each do |extension|
  50. 45 @@template_handlers[extension.to_sym] = handler
  51. end
  52. 45 @@template_extensions = nil
  53. end
  54. # Opposite to register_template_handler.
  55. 9 def unregister_template_handler(*extensions)
  56. extensions.each do |extension|
  57. handler = @@template_handlers.delete extension.to_sym
  58. @@default_template_handlers = nil if @@default_template_handlers == handler
  59. end
  60. @@template_extensions = nil
  61. end
  62. 9 def template_handler_extensions
  63. @@template_handlers.keys.map(&:to_s).sort
  64. end
  65. 9 def registered_template_handler(extension)
  66. 3 extension && @@template_handlers[extension.to_sym]
  67. end
  68. 9 def register_default_template_handler(extension, klass)
  69. 9 register_template_handler(extension, klass)
  70. 9 @@default_template_handlers = klass
  71. end
  72. 9 def handler_for_extension(extension)
  73. 3 registered_template_handler(extension) || @@default_template_handlers
  74. end
  75. end
  76. end
  77. end

lib/action_view/template/handlers/builder.rb

58.33% lines covered

12 relevant lines. 7 lines covered and 5 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 module ActionView
  3. 9 module Template::Handlers
  4. 9 class Builder
  5. 9 class_attribute :default_format, default: :xml
  6. 9 def call(template, source)
  7. require_engine
  8. "xml = ::Builder::XmlMarkup.new(:indent => 2);" \
  9. "self.output_buffer = xml.target!;" +
  10. source +
  11. ";xml.target!;"
  12. end
  13. 9 private
  14. 9 def require_engine # :doc:
  15. @required ||= begin
  16. require "builder"
  17. true
  18. end
  19. end
  20. end
  21. end
  22. end

lib/action_view/template/handlers/erb.rb

51.22% lines covered

41 relevant lines. 21 lines covered and 20 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 module ActionView
  3. 9 class Template
  4. 9 module Handlers
  5. 9 class ERB
  6. 9 autoload :Erubi, "action_view/template/handlers/erb/erubi"
  7. # Specify trim mode for the ERB compiler. Defaults to '-'.
  8. # See ERB documentation for suitable values.
  9. 9 class_attribute :erb_trim_mode, default: "-"
  10. # Default implementation used.
  11. 9 class_attribute :erb_implementation, default: Erubi
  12. # Do not escape templates of these mime types.
  13. 9 class_attribute :escape_ignore_list, default: ["text/plain"]
  14. 9 [self, singleton_class].each do |base|
  15. 18 base.alias_method :escape_whitelist, :escape_ignore_list
  16. 18 base.alias_method :escape_whitelist=, :escape_ignore_list=
  17. 18 base.deprecate(
  18. escape_whitelist: "use #escape_ignore_list instead",
  19. :escape_whitelist= => "use #escape_ignore_list= instead"
  20. )
  21. end
  22. 9 ENCODING_TAG = Regexp.new("\\A(<%#{ENCODING_FLAG}-?%>)[ \\t]*")
  23. 9 def self.call(template, source)
  24. new.call(template, source)
  25. end
  26. 9 def supports_streaming?
  27. true
  28. end
  29. 9 def handles_encoding?
  30. true
  31. end
  32. # Line number to pass to #module_eval
  33. #
  34. # If we're annotating the template, we need to offset the starting
  35. # line number passed to #module_eval so that errors in the template
  36. # will be raised on the correct line.
  37. 9 def start_line(template)
  38. annotate?(template) ? -1 : 0
  39. end
  40. 9 def call(template, source)
  41. # First, convert to BINARY, so in case the encoding is
  42. # wrong, we can still find an encoding tag
  43. # (<%# encoding %>) inside the String using a regular
  44. # expression
  45. template_source = source.b
  46. erb = template_source.gsub(ENCODING_TAG, "")
  47. encoding = $2
  48. erb.force_encoding valid_encoding(source.dup, encoding)
  49. # Always make sure we return a String in the default_internal
  50. erb.encode!
  51. options = {
  52. escape: (self.class.escape_ignore_list.include? template.type),
  53. trim: (self.class.erb_trim_mode == "-")
  54. }
  55. if annotate?(template)
  56. options[:preamble] = "@output_buffer.safe_append='<!-- BEGIN #{template.short_identifier} -->\n';"
  57. options[:postamble] = "@output_buffer.safe_append='<!-- END #{template.short_identifier} -->\n';@output_buffer.to_s"
  58. end
  59. self.class.erb_implementation.new(erb, options).src
  60. end
  61. 9 private
  62. 9 def annotate?(template)
  63. ActionView::Base.annotate_rendered_view_with_filenames && template.format == :html
  64. end
  65. 9 def valid_encoding(string, encoding)
  66. # If a magic encoding comment was found, tag the
  67. # String with this encoding. This is for a case
  68. # where the original String was assumed to be,
  69. # for instance, UTF-8, but a magic comment
  70. # proved otherwise
  71. string.force_encoding(encoding) if encoding
  72. # If the String is valid, return the encoding we found
  73. return string.encoding if string.valid_encoding?
  74. # Otherwise, raise an exception
  75. raise WrongEncodingError.new(string, string.encoding)
  76. end
  77. end
  78. end
  79. end
  80. end

lib/action_view/template/handlers/erb/erubi.rb

30.61% lines covered

49 relevant lines. 15 lines covered and 34 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 require "erubi"
  3. 9 module ActionView
  4. 9 class Template
  5. 9 module Handlers
  6. 9 class ERB
  7. 9 class Erubi < ::Erubi::Engine
  8. # :nodoc: all
  9. 9 def initialize(input, properties = {})
  10. @newline_pending = 0
  11. # Dup properties so that we don't modify argument
  12. properties = Hash[properties]
  13. properties[:bufvar] ||= "@output_buffer"
  14. properties[:preamble] ||= ""
  15. properties[:postamble] ||= "#{properties[:bufvar]}.to_s"
  16. properties[:escapefunc] = ""
  17. super
  18. end
  19. 9 def evaluate(action_view_erb_handler_context)
  20. src = @src
  21. view = Class.new(ActionView::Base) {
  22. include action_view_erb_handler_context._routes.url_helpers
  23. class_eval("define_method(:_template) { |local_assigns, output_buffer| #{src} }", defined?(@filename) ? @filename : "(erubi)", 0)
  24. }.empty
  25. view._run(:_template, nil, {}, ActionView::OutputBuffer.new)
  26. end
  27. 9 private
  28. 9 def add_text(text)
  29. return if text.empty?
  30. if text == "\n"
  31. @newline_pending += 1
  32. else
  33. src << bufvar << ".safe_append='"
  34. src << "\n" * @newline_pending if @newline_pending > 0
  35. src << text.gsub(/['\\]/, '\\\\\&')
  36. src << "'.freeze;"
  37. @newline_pending = 0
  38. end
  39. end
  40. 9 BLOCK_EXPR = /\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/
  41. 9 def add_expression(indicator, code)
  42. flush_newline_if_pending(src)
  43. if (indicator == "==") || @escape
  44. src << bufvar << ".safe_expr_append="
  45. else
  46. src << bufvar << ".append="
  47. end
  48. if BLOCK_EXPR.match?(code)
  49. src << " " << code
  50. else
  51. src << "(" << code << ");"
  52. end
  53. end
  54. 9 def add_code(code)
  55. flush_newline_if_pending(src)
  56. super
  57. end
  58. 9 def add_postamble(_)
  59. flush_newline_if_pending(src)
  60. super
  61. end
  62. 9 def flush_newline_if_pending(src)
  63. if @newline_pending > 0
  64. src << bufvar << ".safe_append='#{"\n" * @newline_pending}'.freeze;"
  65. @newline_pending = 0
  66. end
  67. end
  68. end
  69. end
  70. end
  71. end
  72. end

lib/action_view/template/handlers/html.rb

80.0% lines covered

5 relevant lines. 4 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 module ActionView
  3. 9 module Template::Handlers
  4. 9 class Html < Raw
  5. 9 def call(template, source)
  6. "ActionView::OutputBuffer.new #{super}"
  7. end
  8. end
  9. end
  10. end

lib/action_view/template/handlers/raw.rb

80.0% lines covered

5 relevant lines. 4 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 module ActionView
  3. 9 module Template::Handlers
  4. 9 class Raw
  5. 9 def call(template, source)
  6. "#{source.inspect}.html_safe;"
  7. end
  8. end
  9. end
  10. end

lib/action_view/template/html.rb

0.0% lines covered

30 relevant lines. 0 lines covered and 30 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView #:nodoc:
  3. # = Action View HTML Template
  4. class Template #:nodoc:
  5. class HTML #:nodoc:
  6. attr_reader :type
  7. def initialize(string, type = nil)
  8. unless type
  9. ActiveSupport::Deprecation.warn "ActionView::Template::HTML#initialize requires a type parameter"
  10. type = :html
  11. end
  12. @string = string.to_s
  13. @type = type
  14. end
  15. def identifier
  16. "html template"
  17. end
  18. alias_method :inspect, :identifier
  19. def to_str
  20. ERB::Util.h(@string)
  21. end
  22. def render(*args)
  23. to_str
  24. end
  25. def format
  26. @type
  27. end
  28. def formats; Array(format); end
  29. deprecate :formats
  30. end
  31. end
  32. end

lib/action_view/template/inline.rb

0.0% lines covered

17 relevant lines. 0 lines covered and 17 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView #:nodoc:
  3. class Template #:nodoc:
  4. class Inline < Template #:nodoc:
  5. # This finalizer is needed (and exactly with a proc inside another proc)
  6. # otherwise templates leak in development.
  7. Finalizer = proc do |method_name, mod| # :nodoc:
  8. proc do
  9. mod.module_eval do
  10. remove_possible_method method_name
  11. end
  12. end
  13. end
  14. def compile(mod)
  15. super
  16. ObjectSpace.define_finalizer(self, Finalizer[method_name, mod])
  17. end
  18. end
  19. end
  20. end

lib/action_view/template/raw_file.rb

0.0% lines covered

21 relevant lines. 0 lines covered and 21 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView #:nodoc:
  3. # = Action View RawFile Template
  4. class Template #:nodoc:
  5. class RawFile #:nodoc:
  6. attr_accessor :type, :format
  7. def initialize(filename)
  8. @filename = filename.to_s
  9. extname = ::File.extname(filename).delete(".")
  10. @type = Template::Types[extname] || Template::Types[:text]
  11. @format = @type.symbol
  12. end
  13. def identifier
  14. @filename
  15. end
  16. def render(*args)
  17. ::File.read(@filename)
  18. end
  19. def formats; Array(format); end
  20. deprecate :formats
  21. end
  22. end
  23. end

lib/action_view/template/resolver.rb

47.25% lines covered

218 relevant lines. 103 lines covered and 115 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 require "pathname"
  3. 9 require "active_support/core_ext/class"
  4. 9 require "active_support/core_ext/module/attribute_accessors"
  5. 9 require "action_view/template"
  6. 9 require "thread"
  7. 9 require "concurrent/map"
  8. 9 module ActionView
  9. # = Action View Resolver
  10. 9 class Resolver
  11. # Keeps all information about view path and builds virtual path.
  12. 9 class Path
  13. 9 attr_reader :name, :prefix, :partial, :virtual
  14. 9 alias_method :partial?, :partial
  15. 9 def self.build(name, prefix, partial)
  16. virtual = +""
  17. virtual << "#{prefix}/" unless prefix.empty?
  18. virtual << (partial ? "_#{name}" : name)
  19. new name, prefix, partial, virtual
  20. end
  21. 9 def initialize(name, prefix, partial, virtual)
  22. @name = name
  23. @prefix = prefix
  24. @partial = partial
  25. @virtual = virtual
  26. end
  27. 9 def to_str
  28. @virtual
  29. end
  30. 9 alias :to_s :to_str
  31. end
  32. 9 class PathParser # :nodoc:
  33. 9 def build_path_regex
  34. handlers = Template::Handlers.extensions.map { |x| Regexp.escape(x) }.join("|")
  35. formats = Template::Types.symbols.map { |x| Regexp.escape(x) }.join("|")
  36. locales = "[a-z]{2}(?:-[A-Z]{2})?"
  37. variants = "[^.]*"
  38. %r{
  39. \A
  40. (?:(?<prefix>.*)/)?
  41. (?<partial>_)?
  42. (?<action>.*?)
  43. (?:\.(?<locale>#{locales}))??
  44. (?:\.(?<format>#{formats}))??
  45. (?:\+(?<variant>#{variants}))??
  46. (?:\.(?<handler>#{handlers}))?
  47. \z
  48. }x
  49. end
  50. 9 def parse(path)
  51. @regex ||= build_path_regex
  52. match = @regex.match(path)
  53. {
  54. prefix: match[:prefix] || "",
  55. action: match[:action],
  56. partial: !!match[:partial],
  57. locale: match[:locale]&.to_sym,
  58. handler: match[:handler]&.to_sym,
  59. format: match[:format]&.to_sym,
  60. variant: match[:variant]
  61. }
  62. end
  63. end
  64. # Threadsafe template cache
  65. 9 class Cache #:nodoc:
  66. 9 class SmallCache < Concurrent::Map
  67. 9 def initialize(options = {})
  68. 66 super(options.merge(initial_capacity: 2))
  69. end
  70. end
  71. # Preallocate all the default blocks for performance/memory consumption reasons
  72. 9 PARTIAL_BLOCK = lambda { |cache, partial| cache[partial] = SmallCache.new }
  73. 9 PREFIX_BLOCK = lambda { |cache, prefix| cache[prefix] = SmallCache.new(&PARTIAL_BLOCK) }
  74. 9 NAME_BLOCK = lambda { |cache, name| cache[name] = SmallCache.new(&PREFIX_BLOCK) }
  75. 9 KEY_BLOCK = lambda { |cache, key| cache[key] = SmallCache.new(&NAME_BLOCK) }
  76. # Usually a majority of template look ups return nothing, use this canonical preallocated array to save memory
  77. 9 NO_TEMPLATES = [].freeze
  78. 9 def initialize
  79. 33 @data = SmallCache.new(&KEY_BLOCK)
  80. 33 @query_cache = SmallCache.new
  81. end
  82. 9 def inspect
  83. "#{to_s[0..-2]} keys=#{@data.size} queries=#{@query_cache.size}>"
  84. end
  85. # Cache the templates returned by the block
  86. 9 def cache(key, name, prefix, partial, locals)
  87. @data[key][name][prefix][partial][locals] ||= canonical_no_templates(yield)
  88. end
  89. 9 def cache_query(query) # :nodoc:
  90. @query_cache[query] ||= canonical_no_templates(yield)
  91. end
  92. 9 def clear
  93. 9 @data.clear
  94. 9 @query_cache.clear
  95. end
  96. # Get the cache size. Do not call this
  97. # method. This method is not guaranteed to be here ever.
  98. 9 def size # :nodoc:
  99. size = 0
  100. @data.each_value do |v1|
  101. v1.each_value do |v2|
  102. v2.each_value do |v3|
  103. v3.each_value do |v4|
  104. size += v4.size
  105. end
  106. end
  107. end
  108. end
  109. size + @query_cache.size
  110. end
  111. 9 private
  112. 9 def canonical_no_templates(templates)
  113. templates.empty? ? NO_TEMPLATES : templates
  114. end
  115. end
  116. 9 cattr_accessor :caching, default: true
  117. 9 class << self
  118. 9 alias :caching? :caching
  119. end
  120. 9 def initialize
  121. 33 @cache = Cache.new
  122. end
  123. 9 def clear_cache
  124. 9 @cache.clear
  125. end
  126. # Normalizes the arguments and passes it on to find_templates.
  127. 9 def find_all(name, prefix = nil, partial = false, details = {}, key = nil, locals = [])
  128. locals = locals.map(&:to_s).sort!.freeze
  129. cached(key, [name, prefix, partial], details, locals) do
  130. _find_all(name, prefix, partial, details, key, locals)
  131. end
  132. end
  133. 9 alias :find_all_anywhere :find_all
  134. 9 deprecate :find_all_anywhere
  135. 9 def find_all_with_query(query) # :nodoc:
  136. @cache.cache_query(query) { find_template_paths(File.join(@path, query)) }
  137. end
  138. 9 private
  139. 9 def _find_all(name, prefix, partial, details, key, locals)
  140. find_templates(name, prefix, partial, details, locals)
  141. end
  142. 9 delegate :caching?, to: :class
  143. # This is what child classes implement. No defaults are needed
  144. # because Resolver guarantees that the arguments are present and
  145. # normalized.
  146. 9 def find_templates(name, prefix, partial, details, locals = [])
  147. raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details, locals = []) method"
  148. end
  149. # Handles templates caching. If a key is given and caching is on
  150. # always check the cache before hitting the resolver. Otherwise,
  151. # it always hits the resolver but if the key is present, check if the
  152. # resolver is fresher before returning it.
  153. 9 def cached(key, path_info, details, locals)
  154. name, prefix, partial = path_info
  155. if key
  156. @cache.cache(key, name, prefix, partial, locals) do
  157. yield
  158. end
  159. else
  160. yield
  161. end
  162. end
  163. end
  164. # An abstract class that implements a Resolver with path semantics.
  165. 9 class PathResolver < Resolver #:nodoc:
  166. 9 EXTENSIONS = { locale: ".", formats: ".", variants: "+", handlers: "." }
  167. 9 DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}"
  168. 9 def initialize(pattern = nil)
  169. 33 if pattern
  170. ActiveSupport::Deprecation.warn "Specifying a custom path for #{self.class} is deprecated. Implement a custom Resolver subclass instead."
  171. @pattern = pattern
  172. else
  173. 33 @pattern = DEFAULT_PATTERN
  174. end
  175. 33 @unbound_templates = Concurrent::Map.new
  176. 33 @path_parser = PathParser.new
  177. 33 super()
  178. end
  179. 9 def clear_cache
  180. 9 @unbound_templates.clear
  181. 9 @path_parser = PathParser.new
  182. 9 super()
  183. end
  184. 9 private
  185. 9 def _find_all(name, prefix, partial, details, key, locals)
  186. path = Path.build(name, prefix, partial)
  187. query(path, details, details[:formats], locals, cache: !!key)
  188. end
  189. 9 def query(path, details, formats, locals, cache:)
  190. template_paths = find_template_paths_from_details(path, details)
  191. template_paths = reject_files_external_to_app(template_paths)
  192. template_paths.map do |template|
  193. unbound_template =
  194. if cache
  195. @unbound_templates.compute_if_absent([template, path.virtual]) do
  196. build_unbound_template(template, path.virtual)
  197. end
  198. else
  199. build_unbound_template(template, path.virtual)
  200. end
  201. unbound_template.bind_locals(locals)
  202. end
  203. end
  204. 9 def source_for_template(template)
  205. Template::Sources::File.new(template)
  206. end
  207. 9 def build_unbound_template(template, virtual_path)
  208. handler, format, variant = extract_handler_and_format_and_variant(template)
  209. source = source_for_template(template)
  210. UnboundTemplate.new(
  211. source,
  212. template,
  213. handler,
  214. virtual_path: virtual_path,
  215. format: format,
  216. variant: variant,
  217. )
  218. end
  219. 9 def reject_files_external_to_app(files)
  220. files.reject { |filename| !inside_path?(@path, filename) }
  221. end
  222. 9 def find_template_paths_from_details(path, details)
  223. if path.name.include?(".")
  224. ActiveSupport::Deprecation.warn("Rendering actions with '.' in the name is deprecated: #{path}")
  225. end
  226. query = build_query(path, details)
  227. find_template_paths(query)
  228. end
  229. 9 def find_template_paths(query)
  230. Dir[query].uniq.reject do |filename|
  231. File.directory?(filename) ||
  232. # deals with case-insensitive file systems.
  233. !File.fnmatch(query, filename, File::FNM_EXTGLOB)
  234. end
  235. end
  236. 9 def inside_path?(path, filename)
  237. filename = File.expand_path(filename)
  238. path = File.join(path, "")
  239. filename.start_with?(path)
  240. end
  241. # Helper for building query glob string based on resolver's pattern.
  242. 9 def build_query(path, details)
  243. query = @pattern.dup
  244. prefix = path.prefix.empty? ? "" : "#{escape_entry(path.prefix)}\\1"
  245. query.gsub!(/:prefix(\/)?/, prefix)
  246. partial = escape_entry(path.partial? ? "_#{path.name}" : path.name)
  247. query.gsub!(":action", partial)
  248. details.each do |ext, candidates|
  249. if ext == :variants && candidates == :any
  250. query.gsub!(/:#{ext}/, "*")
  251. else
  252. query.gsub!(/:#{ext}/, "{#{candidates.compact.uniq.join(',')}}")
  253. end
  254. end
  255. File.expand_path(query, @path)
  256. end
  257. 9 def escape_entry(entry)
  258. entry.gsub(/[*?{}\[\]]/, '\\\\\\&')
  259. end
  260. # Extract handler, formats and variant from path. If a format cannot be found neither
  261. # from the path, or the handler, we should return the array of formats given
  262. # to the resolver.
  263. 9 def extract_handler_and_format_and_variant(path)
  264. details = @path_parser.parse(path)
  265. handler = Template.handler_for_extension(details[:handler])
  266. format = details[:format] || handler.try(:default_format)
  267. variant = details[:variant]
  268. # Template::Types[format] and handler.default_format can return nil
  269. [handler, format, variant]
  270. end
  271. end
  272. # A resolver that loads files from the filesystem.
  273. 9 class FileSystemResolver < PathResolver
  274. 9 attr_reader :path
  275. 9 def initialize(path, pattern = nil)
  276. 33 raise ArgumentError, "path already is a Resolver class" if path.is_a?(Resolver)
  277. 33 super(pattern)
  278. 33 @path = File.expand_path(path)
  279. end
  280. 9 def to_s
  281. @path.to_s
  282. end
  283. 9 alias :to_path :to_s
  284. 9 def eql?(resolver)
  285. self.class.equal?(resolver.class) && to_path == resolver.to_path
  286. end
  287. 9 alias :== :eql?
  288. end
  289. # An Optimized resolver for Rails' most common case.
  290. 9 class OptimizedFileSystemResolver < FileSystemResolver #:nodoc:
  291. 9 def initialize(path)
  292. 27 super(path)
  293. end
  294. 9 private
  295. 9 def find_candidate_template_paths(path)
  296. # Instead of checking for every possible path, as our other globs would
  297. # do, scan the directory for files with the right prefix.
  298. query = "#{escape_entry(File.join(@path, path))}*"
  299. Dir[query].reject do |filename|
  300. File.directory?(filename)
  301. end
  302. end
  303. 9 def find_template_paths_from_details(path, details)
  304. if path.name.include?(".")
  305. # Fall back to the unoptimized resolver, which will warn
  306. return super
  307. end
  308. candidates = find_candidate_template_paths(path)
  309. regex = build_regex(path, details)
  310. candidates.uniq.reject do |filename|
  311. # This regex match does double duty of finding only files which match
  312. # details (instead of just matching the prefix) and also filtering for
  313. # case-insensitive file systems.
  314. !regex.match?(filename) ||
  315. File.directory?(filename)
  316. end.sort_by do |filename|
  317. # Because we scanned the directory, instead of checking for files
  318. # one-by-one, they will be returned in an arbitrary order.
  319. # We can use the matches found by the regex and sort by their index in
  320. # details.
  321. match = filename.match(regex)
  322. EXTENSIONS.keys.map do |ext|
  323. if ext == :variants && details[ext] == :any
  324. match[ext].nil? ? 0 : 1
  325. elsif match[ext].nil?
  326. # No match should be last
  327. details[ext].length
  328. else
  329. found = match[ext].to_sym
  330. details[ext].index(found)
  331. end
  332. end
  333. end
  334. end
  335. 9 def build_regex(path, details)
  336. query = Regexp.escape(File.join(@path, path))
  337. exts = EXTENSIONS.map do |ext, prefix|
  338. match =
  339. if ext == :variants && details[ext] == :any
  340. ".*?"
  341. else
  342. arr = details[ext].compact
  343. arr.uniq!
  344. arr.map! { |e| Regexp.escape(e) }
  345. arr.join("|")
  346. end
  347. prefix = Regexp.escape(prefix)
  348. "(#{prefix}(?<#{ext}>#{match}))?"
  349. end.join
  350. %r{\A#{query}#{exts}\z}
  351. end
  352. end
  353. # The same as FileSystemResolver but does not allow templates to store
  354. # a virtual path since it is invalid for such resolvers.
  355. 9 class FallbackFileSystemResolver < FileSystemResolver #:nodoc:
  356. 9 private_class_method :new
  357. 9 def self.instances
  358. 3 [new(""), new("/")]
  359. end
  360. 9 def build_unbound_template(template, _)
  361. super(template, nil)
  362. end
  363. 9 def reject_files_external_to_app(files)
  364. files
  365. end
  366. end
  367. end

lib/action_view/template/sources.rb

0.0% lines covered

10 relevant lines. 0 lines covered and 10 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView
  3. class Template
  4. module Sources
  5. extend ActiveSupport::Autoload
  6. eager_autoload do
  7. autoload :File
  8. end
  9. end
  10. end
  11. end

lib/action_view/template/sources/file.rb

0.0% lines covered

14 relevant lines. 0 lines covered and 14 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView
  3. class Template
  4. module Sources
  5. class File
  6. def initialize(filename)
  7. @filename = filename
  8. end
  9. def to_s
  10. ::File.binread @filename
  11. end
  12. end
  13. end
  14. end
  15. end

lib/action_view/template/text.rb

0.0% lines covered

25 relevant lines. 0 lines covered and 25 lines missed.
    
  1. # frozen_string_literal: true
  2. module ActionView #:nodoc:
  3. # = Action View Text Template
  4. class Template #:nodoc:
  5. class Text #:nodoc:
  6. attr_accessor :type
  7. def initialize(string)
  8. @string = string.to_s
  9. end
  10. def identifier
  11. "text template"
  12. end
  13. alias_method :inspect, :identifier
  14. def to_str
  15. @string
  16. end
  17. def render(*args)
  18. to_str
  19. end
  20. def format
  21. :text
  22. end
  23. def formats; Array(format); end
  24. deprecate :formats
  25. end
  26. end
  27. end

lib/action_view/template/types.rb

68.97% lines covered

29 relevant lines. 20 lines covered and 9 lines missed.
    
  1. # frozen_string_literal: true
  2. 3 require "active_support/core_ext/module/attribute_accessors"
  3. 3 module ActionView
  4. 3 class Template #:nodoc:
  5. 3 class Types
  6. 3 class Type
  7. 3 SET = Struct.new(:symbols).new([ :html, :text, :js, :css, :xml, :json ])
  8. 3 def self.[](type)
  9. if type.is_a?(self)
  10. type
  11. else
  12. new(type)
  13. end
  14. end
  15. 3 attr_reader :symbol
  16. 3 def initialize(symbol)
  17. @symbol = symbol.to_sym
  18. end
  19. 3 def to_s
  20. @symbol.to_s
  21. end
  22. 3 alias to_str to_s
  23. 3 def ref
  24. @symbol
  25. end
  26. 3 alias to_sym ref
  27. 3 def ==(type)
  28. @symbol == type.to_sym unless type.blank?
  29. end
  30. end
  31. 3 cattr_accessor :type_klass
  32. 3 def self.delegate_to(klass)
  33. 9 self.type_klass = klass
  34. end
  35. 3 delegate_to Type
  36. 3 def self.[](type)
  37. type_klass[type]
  38. end
  39. 3 def self.symbols
  40. type_klass::SET.symbols
  41. end
  42. end
  43. end
  44. end

lib/action_view/test_case.rb

50.35% lines covered

141 relevant lines. 71 lines covered and 70 lines missed.
    
  1. # frozen_string_literal: true
  2. 6 require "active_support/core_ext/module/redefine_method"
  3. 6 require "action_controller"
  4. 6 require "action_controller/test_case"
  5. 6 require "action_view"
  6. 6 require "rails-dom-testing"
  7. 6 module ActionView
  8. # = Action View Test Case
  9. 6 class TestCase < ActiveSupport::TestCase
  10. 6 class TestController < ActionController::Base
  11. 6 include ActionDispatch::TestProcess
  12. 6 attr_accessor :request, :response, :params
  13. 6 class << self
  14. 6 attr_writer :controller_path
  15. end
  16. 6 def controller_path=(path)
  17. self.class.controller_path = (path)
  18. end
  19. 6 def initialize
  20. super
  21. self.class.controller_path = ""
  22. @request = ActionController::TestRequest.create(self.class)
  23. @response = ActionDispatch::TestResponse.new
  24. @request.env.delete("PATH_INFO")
  25. @params = ActionController::Parameters.new
  26. end
  27. end
  28. 6 module Behavior
  29. 6 extend ActiveSupport::Concern
  30. 6 include ActionDispatch::Assertions, ActionDispatch::TestProcess
  31. 6 include Rails::Dom::Testing::Assertions
  32. 6 include ActionController::TemplateAssertions
  33. 6 include ActionView::Context
  34. 6 include ActionDispatch::Routing::PolymorphicRoutes
  35. 6 include AbstractController::Helpers
  36. 6 include ActionView::Helpers
  37. 6 include ActionView::RecordIdentifier
  38. 6 include ActionView::RoutingUrlFor
  39. 6 include ActiveSupport::Testing::ConstantLookup
  40. 6 delegate :lookup_context, to: :controller
  41. 6 attr_accessor :controller, :output_buffer, :rendered
  42. 6 module ClassMethods
  43. 6 def tests(helper_class)
  44. 81 case helper_class
  45. when String, Symbol
  46. 6 self.helper_class = "#{helper_class.to_s.underscore}_helper".camelize.safe_constantize
  47. when Module
  48. 75 self.helper_class = helper_class
  49. end
  50. end
  51. 6 def determine_default_helper_class(name)
  52. determine_constant_from_test_name(name) do |constant|
  53. Module === constant && !(Class === constant)
  54. end
  55. end
  56. 6 def helper_method(*methods)
  57. # Almost a duplicate from ActionController::Helpers
  58. 6 methods.flatten.each do |method|
  59. 6 _helpers.module_eval <<-end_eval, __FILE__, __LINE__ + 1
  60. def #{method}(*args, &block) # def current_user(*args, &block)
  61. _test_case.send(:'#{method}', *args, &block) # _test_case.send(:'current_user', *args, &block)
  62. end # end
  63. ruby2_keywords(:'#{method}') if respond_to?(:ruby2_keywords, true)
  64. end_eval
  65. end
  66. end
  67. 6 attr_writer :helper_class
  68. 6 def helper_class
  69. @helper_class ||= determine_default_helper_class(name)
  70. end
  71. 6 def new(*)
  72. include_helper_modules!
  73. super
  74. end
  75. 6 private
  76. 6 def include_helper_modules!
  77. helper(helper_class) if helper_class
  78. include _helpers
  79. end
  80. end
  81. 6 def setup_with_controller
  82. @controller = ActionView::TestCase::TestController.new
  83. @request = @controller.request
  84. @view_flow = ActionView::OutputFlow.new
  85. # empty string ensures buffer has UTF-8 encoding as
  86. # new without arguments returns ASCII-8BIT encoded buffer like String#new
  87. @output_buffer = ActiveSupport::SafeBuffer.new ""
  88. @rendered = +""
  89. make_test_case_available_to_view!
  90. say_no_to_protect_against_forgery!
  91. end
  92. 6 def config
  93. @controller.config if @controller.respond_to?(:config)
  94. end
  95. 6 def render(options = {}, local_assigns = {}, &block)
  96. view.assign(view_assigns)
  97. @rendered << output = view.render(options, local_assigns, &block)
  98. output
  99. end
  100. 6 def rendered_views
  101. @_rendered_views ||= RenderedViewsCollection.new
  102. end
  103. 6 def _routes
  104. @controller._routes if @controller.respond_to?(:_routes)
  105. end
  106. # Need to experiment if this priority is the best one: rendered => output_buffer
  107. 6 class RenderedViewsCollection
  108. 6 def initialize
  109. @rendered_views ||= Hash.new { |hash, key| hash[key] = [] }
  110. end
  111. 6 def add(view, locals)
  112. @rendered_views[view] ||= []
  113. @rendered_views[view] << locals
  114. end
  115. 6 def locals_for(view)
  116. @rendered_views[view]
  117. end
  118. 6 def rendered_views
  119. @rendered_views.keys
  120. end
  121. 6 def view_rendered?(view, expected_locals)
  122. locals_for(view).any? do |actual_locals|
  123. expected_locals.all? { |key, value| value == actual_locals[key] }
  124. end
  125. end
  126. end
  127. 6 included do
  128. 6 setup :setup_with_controller
  129. 6 ActiveSupport.run_load_hooks(:action_view_test_case, self)
  130. end
  131. 6 private
  132. # Need to experiment if this priority is the best one: rendered => output_buffer
  133. 6 def document_root_element
  134. Nokogiri::HTML::Document.parse(@rendered.blank? ? @output_buffer : @rendered).root
  135. end
  136. 6 def say_no_to_protect_against_forgery!
  137. _helpers.module_eval do
  138. silence_redefinition_of_method :protect_against_forgery?
  139. def protect_against_forgery?
  140. false
  141. end
  142. end
  143. end
  144. 6 def make_test_case_available_to_view!
  145. test_case_instance = self
  146. _helpers.module_eval do
  147. unless private_method_defined?(:_test_case)
  148. define_method(:_test_case) { test_case_instance }
  149. private :_test_case
  150. end
  151. end
  152. end
  153. 6 module Locals
  154. 6 attr_accessor :rendered_views
  155. 6 def render(options = {}, local_assigns = {})
  156. case options
  157. when Hash
  158. if block_given?
  159. rendered_views.add options[:layout], options[:locals]
  160. elsif options.key?(:partial)
  161. rendered_views.add options[:partial], options[:locals]
  162. end
  163. else
  164. rendered_views.add options, local_assigns
  165. end
  166. super
  167. end
  168. end
  169. # The instance of ActionView::Base that is used by +render+.
  170. 6 def view
  171. @view ||= begin
  172. view = @controller.view_context
  173. view.singleton_class.include(_helpers)
  174. view.extend(Locals)
  175. view.rendered_views = rendered_views
  176. view.output_buffer = output_buffer
  177. view
  178. end
  179. end
  180. 6 alias_method :_view, :view
  181. 6 INTERNAL_IVARS = [
  182. :@NAME,
  183. :@failures,
  184. :@assertions,
  185. :@__io__,
  186. :@_assertion_wrapped,
  187. :@_assertions,
  188. :@_result,
  189. :@_routes,
  190. :@controller,
  191. :@_layouts,
  192. :@_files,
  193. :@_rendered_views,
  194. :@method_name,
  195. :@output_buffer,
  196. :@_partials,
  197. :@passed,
  198. :@rendered,
  199. :@request,
  200. :@routes,
  201. :@tagged_logger,
  202. :@_templates,
  203. :@options,
  204. :@test_passed,
  205. :@view,
  206. :@view_context_class,
  207. :@view_flow,
  208. :@_subscribers,
  209. :@html_document
  210. ]
  211. 6 def _user_defined_ivars
  212. instance_variables - INTERNAL_IVARS
  213. end
  214. # Returns a Hash of instance variables and their values, as defined by
  215. # the user in the test case, which are then assigned to the view being
  216. # rendered. This is generally intended for internal use and extension
  217. # frameworks.
  218. 6 def view_assigns
  219. Hash[_user_defined_ivars.map do |ivar|
  220. [ivar[1..-1].to_sym, instance_variable_get(ivar)]
  221. end]
  222. end
  223. 6 def method_missing(selector, *args)
  224. begin
  225. routes = @controller.respond_to?(:_routes) && @controller._routes
  226. rescue
  227. # Don't call routes, if there is an error on _routes call
  228. end
  229. if routes &&
  230. (routes.named_routes.route_defined?(selector) ||
  231. routes.mounted_helpers.method_defined?(selector))
  232. @controller.__send__(selector, *args)
  233. else
  234. super
  235. end
  236. end
  237. 6 def respond_to_missing?(name, include_private = false)
  238. begin
  239. routes = defined?(@controller) && @controller.respond_to?(:_routes) && @controller._routes
  240. rescue
  241. # Don't call routes, if there is an error on _routes call
  242. end
  243. routes &&
  244. (routes.named_routes.route_defined?(name) ||
  245. routes.mounted_helpers.method_defined?(name))
  246. end
  247. end
  248. 6 include Behavior
  249. end
  250. end

lib/action_view/testing/resolvers.rb

60.0% lines covered

25 relevant lines. 15 lines covered and 10 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 require "action_view/template/resolver"
  3. 9 module ActionView #:nodoc:
  4. # Use FixtureResolver in your tests to simulate the presence of files on the
  5. # file system. This is used internally by Rails' own test suite, and is
  6. # useful for testing extensions that have no way of knowing what the file
  7. # system will look like at runtime.
  8. 9 class FixtureResolver < OptimizedFileSystemResolver
  9. 9 def initialize(hash = {}, pattern = nil)
  10. 6 super("")
  11. 6 if pattern
  12. ActiveSupport::Deprecation.warn "Specifying a custom path for #{self.class} is deprecated. Implement a custom Resolver subclass instead."
  13. @pattern = pattern
  14. end
  15. 6 @hash = hash
  16. 6 @path = ""
  17. end
  18. 9 def data
  19. @hash
  20. end
  21. 9 def to_s
  22. @hash.keys.join(", ")
  23. end
  24. 9 private
  25. 9 def find_candidate_template_paths(path)
  26. @hash.keys.select do |fixture|
  27. fixture.start_with?(path.virtual)
  28. end.map do |fixture|
  29. "/#{fixture}"
  30. end
  31. end
  32. 9 def source_for_template(template)
  33. @hash[template[1..template.size]]
  34. end
  35. end
  36. 9 class NullResolver < PathResolver
  37. 9 def query(path, exts, _, locals, cache:)
  38. handler, format, variant = extract_handler_and_format_and_variant(path)
  39. [ActionView::Template.new("Template generated by Null Resolver", path.virtual, handler, virtual_path: path.virtual, format: format, variant: variant, locals: locals)]
  40. end
  41. end
  42. end

lib/action_view/unbound_template.rb

0.0% lines covered

25 relevant lines. 0 lines covered and 25 lines missed.
    
  1. # frozen_string_literal: true
  2. require "concurrent/map"
  3. module ActionView
  4. class UnboundTemplate
  5. def initialize(source, identifier, handler, options)
  6. @source = source
  7. @identifier = identifier
  8. @handler = handler
  9. @options = options
  10. @templates = Concurrent::Map.new(initial_capacity: 2)
  11. end
  12. def bind_locals(locals)
  13. @templates[locals] ||= build_template(locals)
  14. end
  15. private
  16. def build_template(locals)
  17. options = @options.merge(locals: locals)
  18. Template.new(
  19. @source,
  20. @identifier,
  21. @handler,
  22. **options
  23. )
  24. end
  25. end
  26. end

lib/action_view/version.rb

75.0% lines covered

4 relevant lines. 3 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 require_relative "gem_version"
  3. 9 module ActionView
  4. # Returns the version of the currently loaded ActionView as a <tt>Gem::Version</tt>
  5. 9 def self.version
  6. gem_version
  7. end
  8. end

lib/action_view/view_paths.rb

76.74% lines covered

43 relevant lines. 33 lines covered and 10 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 module ActionView
  3. 9 module ViewPaths
  4. 9 extend ActiveSupport::Concern
  5. 9 included do
  6. 24 ViewPaths.set_view_paths(self, ActionView::PathSet.new.freeze)
  7. end
  8. 9 delegate :template_exists?, :any_templates?, :view_paths, :formats, :formats=,
  9. :locale, :locale=, to: :lookup_context
  10. 9 module ClassMethods
  11. 9 def _view_paths
  12. 12 ViewPaths.get_view_paths(self)
  13. end
  14. 9 def _view_paths=(paths)
  15. 36 ViewPaths.set_view_paths(self, paths)
  16. end
  17. 9 def _prefixes # :nodoc:
  18. @_prefixes ||= begin
  19. return local_prefixes if superclass.abstract?
  20. local_prefixes + superclass._prefixes
  21. end
  22. end
  23. # Append a path to the list of view paths for this controller.
  24. #
  25. # ==== Parameters
  26. # * <tt>path</tt> - If a String is provided, it gets converted into
  27. # the default view path. You may also provide a custom view path
  28. # (see ActionView::PathSet for more information)
  29. 9 def append_view_path(path)
  30. 6 self._view_paths = view_paths + Array(path)
  31. end
  32. # Prepend a path to the list of view paths for this controller.
  33. #
  34. # ==== Parameters
  35. # * <tt>path</tt> - If a String is provided, it gets converted into
  36. # the default view path. You may also provide a custom view path
  37. # (see ActionView::PathSet for more information)
  38. 9 def prepend_view_path(path)
  39. self._view_paths = ActionView::PathSet.new(Array(path) + view_paths)
  40. end
  41. # A list of all of the default view paths for this controller.
  42. 9 def view_paths
  43. 12 _view_paths
  44. end
  45. # Set the view paths.
  46. #
  47. # ==== Parameters
  48. # * <tt>paths</tt> - If a PathSet is provided, use that;
  49. # otherwise, process the parameter into a PathSet.
  50. 9 def view_paths=(paths)
  51. 30 self._view_paths = ActionView::PathSet.new(Array(paths))
  52. end
  53. 9 private
  54. # Override this method in your controller if you want to change paths prefixes for finding views.
  55. # Prefixes defined here will still be added to parents' <tt>._prefixes</tt>.
  56. 9 def local_prefixes
  57. [controller_path]
  58. end
  59. end
  60. # :stopdoc:
  61. 9 @all_view_paths = {}
  62. 9 def self.get_view_paths(klass)
  63. 12 @all_view_paths[klass] || get_view_paths(klass.superclass)
  64. end
  65. 9 def self.set_view_paths(klass, paths)
  66. 60 @all_view_paths[klass] = paths
  67. end
  68. 9 def self.all_view_paths
  69. 3 @all_view_paths.values.uniq
  70. end
  71. # :startdoc:
  72. # The prefixes used in render "foo" shortcuts.
  73. 9 def _prefixes # :nodoc:
  74. self.class._prefixes
  75. end
  76. # <tt>LookupContext</tt> is the object responsible for holding all
  77. # information required for looking up templates, i.e. view paths and
  78. # details. Check <tt>ActionView::LookupContext</tt> for more information.
  79. 9 def lookup_context
  80. @_lookup_context ||=
  81. ActionView::LookupContext.new(self.class._view_paths, details_for_lookup, _prefixes)
  82. end
  83. 9 def details_for_lookup
  84. {}
  85. end
  86. # Append a path to the list of view paths for the current <tt>LookupContext</tt>.
  87. #
  88. # ==== Parameters
  89. # * <tt>path</tt> - If a String is provided, it gets converted into
  90. # the default view path. You may also provide a custom view path
  91. # (see ActionView::PathSet for more information)
  92. 9 def append_view_path(path)
  93. lookup_context.view_paths.push(*path)
  94. end
  95. # Prepend a path to the list of view paths for the current <tt>LookupContext</tt>.
  96. #
  97. # ==== Parameters
  98. # * <tt>path</tt> - If a String is provided, it gets converted into
  99. # the default view path. You may also provide a custom view path
  100. # (see ActionView::PathSet for more information)
  101. 9 def prepend_view_path(path)
  102. lookup_context.view_paths.unshift(*path)
  103. end
  104. end
  105. end