aboutsummaryrefslogtreecommitdiffstats
path: root/actionview
diff options
context:
space:
mode:
Diffstat (limited to 'actionview')
-rw-r--r--actionview/CHANGELOG.md238
-rw-r--r--actionview/MIT-LICENSE2
-rw-r--r--actionview/README.rdoc2
-rw-r--r--actionview/Rakefile43
-rw-r--r--actionview/actionview.gemspec4
-rwxr-xr-xactionview/bin/test4
-rw-r--r--actionview/lib/action_view.rb3
-rw-r--r--actionview/lib/action_view/base.rb29
-rw-r--r--actionview/lib/action_view/buffers.rb7
-rw-r--r--actionview/lib/action_view/dependency_tracker.rb61
-rw-r--r--actionview/lib/action_view/digestor.rb25
-rw-r--r--actionview/lib/action_view/gem_version.rb6
-rw-r--r--actionview/lib/action_view/helpers/asset_tag_helper.rb20
-rw-r--r--actionview/lib/action_view/helpers/asset_url_helper.rb65
-rw-r--r--actionview/lib/action_view/helpers/atom_feed_helper.rb9
-rw-r--r--actionview/lib/action_view/helpers/cache_helper.rb109
-rw-r--r--actionview/lib/action_view/helpers/capture_helper.rb9
-rw-r--r--actionview/lib/action_view/helpers/controller_helper.rb1
-rw-r--r--actionview/lib/action_view/helpers/date_helper.rb58
-rw-r--r--actionview/lib/action_view/helpers/debug_helper.rb2
-rw-r--r--actionview/lib/action_view/helpers/form_helper.rb173
-rw-r--r--actionview/lib/action_view/helpers/form_options_helper.rb117
-rw-r--r--actionview/lib/action_view/helpers/form_tag_helper.rb112
-rw-r--r--actionview/lib/action_view/helpers/javascript_helper.rb8
-rw-r--r--actionview/lib/action_view/helpers/number_helper.rb32
-rw-r--r--actionview/lib/action_view/helpers/output_safety_helper.rb6
-rw-r--r--actionview/lib/action_view/helpers/record_tag_helper.rb111
-rw-r--r--actionview/lib/action_view/helpers/rendering_helper.rb8
-rw-r--r--actionview/lib/action_view/helpers/sanitize_helper.rb225
-rw-r--r--actionview/lib/action_view/helpers/tag_helper.rb39
-rw-r--r--actionview/lib/action_view/helpers/tags.rb1
-rw-r--r--actionview/lib/action_view/helpers/tags/base.rb74
-rw-r--r--actionview/lib/action_view/helpers/tags/collection_check_boxes.rb32
-rw-r--r--actionview/lib/action_view/helpers/tags/collection_helpers.rb28
-rw-r--r--actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb10
-rw-r--r--actionview/lib/action_view/helpers/tags/file_field.rb15
-rw-r--r--actionview/lib/action_view/helpers/tags/label.rb58
-rw-r--r--actionview/lib/action_view/helpers/tags/placeholderable.rb22
-rw-r--r--actionview/lib/action_view/helpers/tags/search_field.rb1
-rw-r--r--actionview/lib/action_view/helpers/tags/select.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/text_area.rb4
-rw-r--r--actionview/lib/action_view/helpers/tags/text_field.rb4
-rw-r--r--actionview/lib/action_view/helpers/tags/translator.rb40
-rw-r--r--actionview/lib/action_view/helpers/text_helper.rb36
-rw-r--r--actionview/lib/action_view/helpers/translation_helper.rb118
-rw-r--r--actionview/lib/action_view/helpers/url_helper.rb128
-rw-r--r--actionview/lib/action_view/layouts.rb21
-rw-r--r--actionview/lib/action_view/lookup_context.rb50
-rw-r--r--actionview/lib/action_view/model_naming.rb4
-rw-r--r--actionview/lib/action_view/path_set.rb9
-rw-r--r--actionview/lib/action_view/railtie.rb22
-rw-r--r--actionview/lib/action_view/record_identifier.rb63
-rw-r--r--actionview/lib/action_view/renderer/partial_renderer.rb65
-rw-r--r--actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb70
-rw-r--r--actionview/lib/action_view/renderer/renderer.rb2
-rw-r--r--actionview/lib/action_view/renderer/streaming_template_renderer.rb2
-rw-r--r--actionview/lib/action_view/renderer/template_renderer.rb23
-rw-r--r--actionview/lib/action_view/rendering.rb21
-rw-r--r--actionview/lib/action_view/routing_url_for.rb50
-rw-r--r--actionview/lib/action_view/tasks/dependencies.rake16
-rw-r--r--actionview/lib/action_view/template.rb79
-rw-r--r--actionview/lib/action_view/template/error.rb2
-rw-r--r--actionview/lib/action_view/template/handlers.rb8
-rw-r--r--actionview/lib/action_view/template/handlers/erb.rb31
-rw-r--r--actionview/lib/action_view/template/handlers/raw.rb2
-rw-r--r--actionview/lib/action_view/template/resolver.rb72
-rw-r--r--actionview/lib/action_view/template/types.rb2
-rw-r--r--actionview/lib/action_view/test_case.rb35
-rw-r--r--actionview/lib/action_view/vendor/html-scanner.rb20
-rw-r--r--actionview/lib/action_view/vendor/html-scanner/html/document.rb68
-rw-r--r--actionview/lib/action_view/vendor/html-scanner/html/node.rb532
-rw-r--r--actionview/lib/action_view/vendor/html-scanner/html/sanitizer.rb188
-rw-r--r--actionview/lib/action_view/vendor/html-scanner/html/selector.rb830
-rw-r--r--actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb107
-rw-r--r--actionview/lib/action_view/vendor/html-scanner/html/version.rb11
-rw-r--r--actionview/lib/action_view/view_paths.rb24
-rw-r--r--actionview/test/abstract_unit.rb80
-rw-r--r--actionview/test/actionpack/abstract/abstract_controller_test.rb18
-rw-r--r--actionview/test/actionpack/abstract/layouts_test.rb4
-rw-r--r--actionview/test/actionpack/abstract/render_test.rb2
-rw-r--r--actionview/test/actionpack/abstract/views/abstract_controller/testing/me5/index.erb1
-rw-r--r--actionview/test/actionpack/controller/capture_test.rb2
-rw-r--r--actionview/test/actionpack/controller/layout_test.rb27
-rw-r--r--actionview/test/actionpack/controller/render_test.rb162
-rw-r--r--actionview/test/actionpack/controller/view_paths_test.rb6
-rw-r--r--actionview/test/active_record_unit.rb2
-rw-r--r--actionview/test/activerecord/controller_runtime_test.rb8
-rw-r--r--actionview/test/activerecord/debug_helper_test.rb (renamed from actionview/test/template/debug_helper_test.rb)6
-rw-r--r--actionview/test/activerecord/form_helper_activerecord_test.rb4
-rw-r--r--actionview/test/activerecord/polymorphic_routes_test.rb71
-rw-r--r--actionview/test/activerecord/relation_cache_test.rb18
-rw-r--r--actionview/test/activerecord/render_partial_with_record_identification_test.rb18
-rw-r--r--actionview/test/fixtures/actionpack/layout_tests/alt/layouts/alt.erb1
-rw-r--r--actionview/test/fixtures/actionpack/layouts/standard.text.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/hyphen-ated.erb2
-rw-r--r--actionview/test/fixtures/blog_public/.gitignore1
-rw-r--r--actionview/test/fixtures/blog_public/blog.html1
-rw-r--r--actionview/test/fixtures/blog_public/index.html1
-rw-r--r--actionview/test/fixtures/blog_public/subdir/index.html1
-rw-r--r--actionview/test/fixtures/digestor/comments/_comment.html.erb2
-rw-r--r--actionview/test/fixtures/digestor/events/_completed.html.erb0
-rw-r--r--actionview/test/fixtures/digestor/events/index.html.erb1
-rw-r--r--actionview/test/fixtures/functional_caching/fragment_cached_without_digest.html.erb3
-rw-r--r--actionview/test/fixtures/happy_path/render_action/hello_world.erb1
-rw-r--r--actionview/test/fixtures/layouts/streaming_with_capture.erb6
-rw-r--r--actionview/test/fixtures/multipart/bracketed_utf8_param5
-rw-r--r--actionview/test/fixtures/multipart/single_utf8_param5
-rw-r--r--actionview/test/fixtures/project.rb4
-rw-r--r--actionview/test/fixtures/scope/test/modgreet.erb1
-rw-r--r--actionview/test/fixtures/test/_FooBar.html.erb1
-rw-r--r--actionview/test/fixtures/test/_a-in.html.erb0
-rw-r--r--actionview/test/fixtures/test/_cached_customer.erb3
-rw-r--r--actionview/test/fixtures/test/_cached_customer_as.erb3
-rw-r--r--actionview/test/fixtures/test/_label_with_block.erb2
-rw-r--r--actionview/test/fixtures/test/_partial_name_in_local_assigns.erb1
-rw-r--r--actionview/test/fixtures/test/_partial_shortcut_with_block_content.html.erb3
-rw-r--r--actionview/test/fixtures/test/nil_return.erb1
-rw-r--r--actionview/test/lib/controller/fake_models.rb41
-rw-r--r--actionview/test/template/asset_tag_helper_test.rb39
-rw-r--r--actionview/test/template/atom_feed_helper_test.rb86
-rw-r--r--actionview/test/template/capture_helper_test.rb6
-rw-r--r--actionview/test/template/compiled_templates_test.rb4
-rw-r--r--actionview/test/template/controller_helper_test.rb21
-rw-r--r--actionview/test/template/date_helper_test.rb43
-rw-r--r--actionview/test/template/dependency_tracker_test.rb3
-rw-r--r--actionview/test/template/digestor_test.rb73
-rw-r--r--actionview/test/template/erb_util_test.rb2
-rw-r--r--actionview/test/template/form_collections_helper_test.rb141
-rw-r--r--actionview/test/template/form_helper_test.rb520
-rw-r--r--actionview/test/template/form_options_helper_i18n_test.rb5
-rw-r--r--actionview/test/template/form_options_helper_test.rb20
-rw-r--r--actionview/test/template/form_tag_helper_test.rb104
-rw-r--r--actionview/test/template/html-scanner/cdata_node_test.rb15
-rw-r--r--actionview/test/template/html-scanner/document_test.rb148
-rw-r--r--actionview/test/template/html-scanner/node_test.rb89
-rw-r--r--actionview/test/template/html-scanner/sanitizer_test.rb330
-rw-r--r--actionview/test/template/html-scanner/tag_node_test.rb243
-rw-r--r--actionview/test/template/html-scanner/text_node_test.rb50
-rw-r--r--actionview/test/template/html-scanner/tokenizer_test.rb131
-rw-r--r--actionview/test/template/javascript_helper_test.rb9
-rw-r--r--actionview/test/template/lookup_context_test.rb70
-rw-r--r--actionview/test/template/number_helper_test.rb5
-rw-r--r--actionview/test/template/record_identifier_test.rb44
-rw-r--r--actionview/test/template/record_tag_helper_test.rb93
-rw-r--r--actionview/test/template/render_test.rb111
-rw-r--r--actionview/test/template/sanitize_helper_test.rb26
-rw-r--r--actionview/test/template/streaming_render_test.rb5
-rw-r--r--actionview/test/template/tag_helper_test.rb17
-rw-r--r--actionview/test/template/template_test.rb45
-rw-r--r--actionview/test/template/test_case_test.rb85
-rw-r--r--actionview/test/template/text_helper_test.rb23
-rw-r--r--actionview/test/template/translation_helper_test.rb92
-rw-r--r--actionview/test/template/url_helper_test.rb62
153 files changed, 3166 insertions, 4644 deletions
diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md
index 3fc2ab178c..c010b7ce91 100644
--- a/actionview/CHANGELOG.md
+++ b/actionview/CHANGELOG.md
@@ -1,159 +1,211 @@
-* Fix that render layout: 'messages/layout' should also be added to the dependency tracker tree.
+* Collection input propagates input's `id` to the label's `for` attribute when
+ using html options as the last element of collection.
- *DHH*
+ *Vasiliy Ermolovich*
-* Add `PartialIteration` object used when rendering collections.
+* Add a `hidden_field` on the `collection_radio_buttons` to avoid raising a error
+ when the only input on the form is the `collection_radio_buttons`.
- The iteration object is available as the local variable
- `#{template_name}_iteration` when rendering partials with collections.
+ *Mauro George*
- It gives access to the `size` of the collection being iterated over,
- the current `index` and two convenience methods `first?` and `last?`.
+* `url_for` does not modify its arguments when generating polymorphic URLs.
- *Joel Junström*, *Lucas Uyezu*
+ *Bernerd Schaefer*
-* Return an absolute instead of relative path from an asset url in the case
- of the `asset_host` proc returning nil
+* `number_to_currency` and `number_with_delimiter` now accept custom `delimiter_pattern` option
+ to handle placement of delimiter, to support currency formats like INR
+
+ Example:
+
+ number_to_currency(1230000, delimiter_pattern: /(\d+?)(?=(\d\d)+(\d)(?!\d))/, unit: '₹', format: "%u %n")
+ # => '₹ 12,30,000.00'
+
+ *Vipul A M*
+
+* Make `disable_with` the default behavior for submit tags. Disables the
+ button on submit to prevent double submits.
- *Jolyon Pawlyn*
+ *Justin Schiff*
-* Fix `html_escape_once` to properly handle hex escape sequences (e.g. ᨫ)
+* Add a break_sequence option to word_wrap so you can specify a custom break.
- *John F. Douthat*
+ * Mauricio Gomez *
-* Added String support for min and max properties for date field helpers.
+* Add wildcard matching to explicit dependencies.
- *Todd Bealmear*
+ Turns:
-* The `highlight` helper now accepts a block to be used instead of the `highlighter`
- option.
+ ```erb
+ <% # Template Dependency: recordings/threads/events/subscribers_changed %>
+ <% # Template Dependency: recordings/threads/events/completed %>
+ <% # Template Dependency: recordings/threads/events/uncompleted %>
+ ```
- *Lucas Mazza*
+ Into:
-* The `except` and `highlight` helpers now accept regular expressions.
+ ```erb
+ <% # Template Dependency: recordings/threads/events/* %>
+ ```
- *Jan Szumiec*
+ *Kasper Timm Hansen*
-* Flatten the array parameter in `safe_join`, so it behaves consistently with
- `Array#join`.
+* Allow defining explicit collection caching using a `# Template Collection: ...`
+ directive inside templates.
- *Paul Grayson*
+ *Dov Murik*
-* Honor `html_safe` on array elements in tag values, as we do for plain string
- values.
+* Asset helpers raise `ArgumentError` when `nil` is passed as a source.
- *Paul Grayson*
+ *Anton Kolomiychuk*
-* Add `ActionView::Template::Handler.unregister_template_handler`.
+* Always attach the template digest to the cache key for collection caching
+ even when `virtual_path` is not available from the view context.
+ Which could happen if the rendering was done directly in the controller
+ and not in a template.
- It performs the opposite of `ActionView::Template::Handler.register_template_handler`.
+ Fixes #20535
- *Zuhao Wan*
+ *Roque Pinel*
-* Bring `cache_digest` rake tasks up-to-date with the latest API changes
+* Improve detection of partial templates eligible for collection caching,
+ now allowing multi-line comments at the beginning of the template file.
- *Jiri Pospisil*
+ *Dov Murik*
-* Allow custom `:host` option to be passed to `asset_url` helper that
- overwrites `config.action_controller.asset_host` for particular asset.
+* Raise an ArgumentError when a false value for `include_blank` is passed to a
+ required select field (to comply with the HTML5 spec).
- *Hubert Łępicki*
+ *Grey Baker*
-* Deprecate `AbstractController::Base.parent_prefixes`.
- Override `AbstractController::Base.local_prefixes` when you want to change
- where to find views.
+* Do not put partial name to `local_assigns` when rendering without
+ an object or a collection.
- *Nick Sutterer*
+ *Henrik Nygren*
-* Take label values into account when doing I18n lookups for model attributes.
+* Remove `:rescue_format` option for `translate` helper since it's no longer
+ supported by I18n.
- The following:
+ *Bernard Potocki*
- # form.html.erb
- <%= form_for @post do |f| %>
- <%= f.label :type, value: "long" %>
- <% end %>
+* `translate` should handle `raise` flag correctly in case of both main and default
+ translation is missing.
- # en.yml
- en:
- activerecord:
- attributes:
- post/long: "Long-form Post"
+ Fixes #19967
- Used to simply return "long", but now it will return "Long-form
- Post".
+ *Bernard Potocki*
- *Joshua Cody*
+* Load the `default_form_builder` from the controller on initialization, which overrides
+ the global config if it is present.
-* Change `asset_path` to use File.join to create proper paths:
+ *Kevin McPhillips*
- Before:
+* Accept lambda as `child_index` option in `fields_for` method.
- https://some.host.com//assets/some.js
+ *Karol Galanciak*
- After:
+* `translate` allows `default: [[]]` again for a default value of `[]`.
- https://some.host.com/assets/some.js
+ Fixes #19640.
- *Peter Schröder*
+ *Adam Prescott*
-* Change `favicon_link_tag` default mimetype from `image/vnd.microsoft.icon` to
- `image/x-icon`.
+* `translate` should accept nils as members of the `:default`
+ parameter without raising a translation missing error.
- Before:
+ Fixes #19419
- #=> favicon_link_tag 'myicon.ico'
- <link href="/assets/myicon.ico" rel="shortcut icon" type="image/vnd.microsoft.icon" />
+ *Justin Coyne*
- After:
+* `number_to_percentage` does not crash with `Float::NAN` or `Float::INFINITY`
+ as input when `precision: 0` is used.
- #=> favicon_link_tag 'myicon.ico'
- <link href="/assets/myicon.ico" rel="shortcut icon" type="image/x-icon" />
+ Fixes #19227.
- *Geoffroy Lorieux*
+ *Yves Senn*
-* Remove wrapping div with inline styles for hidden form fields.
+* Fixed the translation helper method to accept different default values types
+ besides String.
- We are dropping HTML 4.01 and XHTML strict compliance since input tags directly
- inside a form are valid HTML5, and the absence of inline styles help in validating
- for Content Security Policy.
+ *Ulisses Almeida*
- *Joost Baaij*
+* Collection rendering automatically caches and fetches multiple partials.
-* `collection_check_boxes` respects `:index` option for the hidden filed name.
+ Collections rendered as:
- Fixes #14147.
+ ```ruby
+ <%= render @notifications %>
+ <%= render partial: 'notifications/notification', collection: @notifications, as: :notification %>
+ ```
- *Vasiliy Ermolovich*
+ will now read several partials from cache at once, if the template starts with a cache call:
-* `date_select` helper with option `with_css_classes: true` does not overwrite other classes.
+ ```ruby
+ # notifications/_notification.html.erb
+ <% cache notification do %>
+ <%# ... %>
+ <% end %>
+ ```
- *Izumi Wong-Horiuchi*
+ *Kasper Timm Hansen*
-* `number_to_percentage` does not crash with `Float::NAN` or `Float::INFINITY`
- as input.
+* Fixed a dependency tracker bug that caused template dependencies not
+ count layouts as dependencies for partials.
- Fixes #14405.
+ *Juho Leinonen*
- *Yves Senn*
+* Extracted `ActionView::Helpers::RecordTagHelper` to external gem
+ (`record_tag_helper`) and added removal notices.
-* Add `include_hidden` option to `collection_check_boxes` helper.
+ *Todd Bealmear*
- *Vasiliy Ermolovich*
+* Allow to pass a string value to `size` option in `image_tag` and `video_tag`.
+
+ This makes the behavior more consistent with `width` or `height` options.
+
+ *Mehdi Lahmam*
+
+* Partial template name does no more have to be a valid Ruby identifier.
+
+ There used to be a naming rule that the partial name should start with
+ underscore, and should be followed by any combination of letters, numbers
+ and underscores.
+ But now we can give our partials any name starting with underscore, such as
+ _🍔.html.erb.
+
+ *Akira Matsuda*
+
+* Change the default template handler from `ERB` to `Raw`.
+
+ Files without a template handler in their extension will be rendered using the raw
+ handler instead of ERB.
+
+ *Rafael Mendonça França*
+
+* Remove deprecated `AbstractController::Base::parent_prefixes`.
+
+ *Rafael Mendonça França*
+
+* Default translations that have a lower precedence than a html safe default,
+ but are not themselves safe, should not be marked as html_safe.
+
+ *Justin Coyne*
+
+* Make possible to use blocks with short version of `render "partial"` helper.
+
+ *Nikolay Shebanov*
-* Fixed a problem where the default options for the `button_tag` helper is not
- applied correctly.
+* Add a `hidden_field` on the `file_field` to avoid raising an error when the only
+ input on the form is the `file_field`.
- Fixes #14254.
+ *Mauro George*
- *Sergey Prikhodko*
+* Add an explicit error message, in `ActionView::PartialRenderer` for partial
+ `rendering`, when the value of option `as` has invalid characters.
-* Take variants into account when calculating template digests in ActionView::Digestor.
+ *Angelo Capilleri*
- The arguments to ActionView::Digestor#digest are now being passed as a hash
- to support variants and allow more flexibility in the future. The support for
- regular (required) arguments is deprecated and will be removed in Rails 5.0 or later.
+* Allow entries without a link tag in `AtomFeedHelper`.
- *Piotr Chmolowski, Łukasz Strzałkowski*
+ *Daniel Gomez de Souza*
-Please check [4-1-stable](https://github.com/rails/rails/blob/4-1-stable/actionview/CHANGELOG.md) for previous changes.
+Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/actionview/CHANGELOG.md) for previous changes.
diff --git a/actionview/MIT-LICENSE b/actionview/MIT-LICENSE
index d58dd9ed9b..3ec7a617cf 100644
--- a/actionview/MIT-LICENSE
+++ b/actionview/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2014 David Heinemeier Hansson
+Copyright (c) 2004-2015 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/actionview/README.rdoc b/actionview/README.rdoc
index 5bb62c7562..8b1f85f748 100644
--- a/actionview/README.rdoc
+++ b/actionview/README.rdoc
@@ -9,7 +9,7 @@ used to inline short Ruby snippets inside HTML), and XML Builder.
The latest version of Action View can be installed with RubyGems:
- % [sudo] gem install actionview
+ % gem install actionview
Source code can be downloaded as part of the Rails project on GitHub
diff --git a/actionview/Rakefile b/actionview/Rakefile
index d56fe9ea76..93be50721d 100644
--- a/actionview/Rakefile
+++ b/actionview/Rakefile
@@ -1,5 +1,4 @@
require 'rake/testtask'
-require 'rubygems/package_task'
desc "Default Task"
task :default => :test
@@ -18,9 +17,10 @@ namespace :test do
Rake::TestTask.new(:template) do |t|
t.libs << 'test'
- t.test_files = Dir.glob('test/template/**/*_test.rb').sort
+ t.test_files = Dir.glob('test/template/**/*_test.rb')
t.warning = true
t.verbose = true
+ t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
end
namespace :integration do
@@ -30,6 +30,7 @@ namespace :test do
t.test_files = Dir.glob("test/activerecord/*_test.rb")
t.warning = true
t.verbose = true
+ t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
end
desc 'ActionPack Integration Tests'
@@ -38,43 +39,13 @@ namespace :test do
t.test_files = Dir.glob("test/actionpack/**/*_test.rb")
t.warning = true
t.verbose = true
+ t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
end
end
end
-spec = eval(File.read('actionview.gemspec'))
-
-Gem::PackageTask.new(spec) do |p|
- p.gem_spec = spec
-end
-
-desc "Release to rubygems"
-task :release => :package do
- require 'rake/gemcutter'
- Rake::Gemcutter::Tasks.new(spec).define
- Rake::Task['gem:push'].invoke
-end
-
task :lines do
- lines, codelines, total_lines, total_codelines = 0, 0, 0, 0
-
- FileList["lib/**/*.rb"].each do |file_name|
- next if file_name =~ /vendor/
- File.open(file_name, 'r') do |f|
- while line = f.gets
- lines += 1
- next if line =~ /^\s*$/
- next if line =~ /^\s*#/
- codelines += 1
- end
- end
- puts "L: #{sprintf("%4d", lines)}, LOC #{sprintf("%4d", codelines)} | #{file_name}"
-
- total_lines += lines
- total_codelines += codelines
-
- lines, codelines = 0, 0
- end
-
- puts "Total: Lines #{total_lines}, LOC #{total_codelines}"
+ load File.expand_path('..', File.dirname(__FILE__)) + '/tools/line_statistics'
+ files = FileList["lib/**/*.rb"]
+ CodeTools::LineStatistics.new(files).print_loc
end
diff --git a/actionview/actionview.gemspec b/actionview/actionview.gemspec
index e45dd04225..612e94021d 100644
--- a/actionview/actionview.gemspec
+++ b/actionview/actionview.gemspec
@@ -7,7 +7,7 @@ Gem::Specification.new do |s|
s.summary = 'Rendering framework putting the V in MVC (part of Rails).'
s.description = 'Simple, battle-tested conventions and helpers for building web pages.'
- s.required_ruby_version = '>= 1.9.3'
+ s.required_ruby_version = '>= 2.2.2'
s.license = 'MIT'
@@ -23,6 +23,8 @@ Gem::Specification.new do |s|
s.add_dependency 'builder', '~> 3.1'
s.add_dependency 'erubis', '~> 2.7.0'
+ s.add_dependency 'rails-html-sanitizer', '~> 1.0', '>= 1.0.2'
+ s.add_dependency 'rails-dom-testing', '~> 1.0', '>= 1.0.5'
s.add_development_dependency 'actionpack', version
s.add_development_dependency 'activemodel', version
diff --git a/actionview/bin/test b/actionview/bin/test
new file mode 100755
index 0000000000..404cabba51
--- /dev/null
+++ b/actionview/bin/test
@@ -0,0 +1,4 @@
+#!/usr/bin/env ruby
+COMPONENT_ROOT = File.expand_path("../../", __FILE__)
+require File.expand_path("../tools/test", COMPONENT_ROOT)
+exit Minitest.run(ARGV)
diff --git a/actionview/lib/action_view.rb b/actionview/lib/action_view.rb
index 50712e0830..c3bbac27fd 100644
--- a/actionview/lib/action_view.rb
+++ b/actionview/lib/action_view.rb
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2004-2014 David Heinemeier Hansson
+# Copyright (c) 2004-2015 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -86,7 +86,6 @@ module ActionView
super
ActionView::Helpers.eager_load!
ActionView::Template.eager_load!
- HTML.eager_load!
end
end
diff --git a/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb
index 900f96255e..ad1cb1a4be 100644
--- a/actionview/lib/action_view/base.rb
+++ b/actionview/lib/action_view/base.rb
@@ -10,8 +10,10 @@ require 'action_view/lookup_context'
module ActionView #:nodoc:
# = Action View Base
#
- # Action View templates can be written in several ways. If the template file has a <tt>.erb</tt> extension then it uses a mixture of ERB
- # (included in Ruby) and HTML. If the template file has a <tt>.builder</tt> extension then Jim Weirich's Builder::XmlMarkup library is used.
+ # Action View templates can be written in several ways.
+ # If the template file has a <tt>.erb</tt> extension, then it uses the erubis[https://rubygems.org/gems/erubis]
+ # template system which can embed Ruby into an HTML document.
+ # If the template file has a <tt>.builder</tt> extension, then Jim Weirich's Builder::XmlMarkup library is used.
#
# == ERB
#
@@ -31,7 +33,9 @@ module ActionView #:nodoc:
#
# If you absolutely must write from within a function use +concat+.
#
- # <%- and -%> suppress leading and trailing whitespace, including the trailing newline, and can be used interchangeably with <% and %>.
+ # When on a line that only contains whitespaces except for the tag, <% %> suppress leading and trailing whitespace,
+ # including the trailing newline. <% %> and <%- -%> are the same.
+ # Note however that <%= %> and <%= -%> are different: only the latter removes trailing whitespaces.
#
# === Using sub templates
#
@@ -66,14 +70,13 @@ module ActionView #:nodoc:
# Headline: <%= headline %>
# First name: <%= person.first_name %>
#
- # If you need to find out whether a certain local variable has been assigned a value in a particular render call,
- # you need to use the following pattern:
+ # 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
+ # variables as:
#
- # <% if local_assigns.has_key? :headline %>
- # Headline: <%= headline %>
- # <% end %>
+ # Headline: <%= local_assigns[:headline] %>
#
- # Testing using <tt>defined? headline</tt> will not work. This is an implementation restriction.
+ # This is useful in cases where you aren't sure if the local variable has been assigned. Alternatively, you could also use
+ # <tt>defined? headline</tt> to first check if the variable has been assigned before using it.
#
# === Template caching
#
@@ -131,8 +134,8 @@ module ActionView #:nodoc:
# end
# end
#
- # For more information on Builder please consult the [source
- # code](https://github.com/jimweirich/builder).
+ # For more information on Builder please consult the {source
+ # code}[https://github.com/jimweirich/builder].
class Base
include Helpers, ::ERB::Util, Context
@@ -158,6 +161,10 @@ module ActionView #:nodoc:
cattr_accessor :raise_on_missing_translations
@@raise_on_missing_translations = false
+ # Specify whether submit_tag should automatically disable on click
+ cattr_accessor :automatically_disable_submit_tag
+ @@automatically_disable_submit_tag = true
+
class_attribute :_routes
class_attribute :logger
diff --git a/actionview/lib/action_view/buffers.rb b/actionview/lib/action_view/buffers.rb
index 361a0dccbe..be5d86b1dc 100644
--- a/actionview/lib/action_view/buffers.rb
+++ b/actionview/lib/action_view/buffers.rb
@@ -13,10 +13,11 @@ module ActionView
end
alias :append= :<<
- def safe_concat(value)
- return self if value.nil?
- super(value.to_s)
+ def safe_expr_append=(val)
+ return self if val.nil?
+ safe_concat val.to_s
end
+
alias :safe_append= :safe_concat
end
diff --git a/actionview/lib/action_view/dependency_tracker.rb b/actionview/lib/action_view/dependency_tracker.rb
index e34bdd4a46..7716955fd9 100644
--- a/actionview/lib/action_view/dependency_tracker.rb
+++ b/actionview/lib/action_view/dependency_tracker.rb
@@ -1,16 +1,18 @@
-require 'thread_safe'
+require 'concurrent'
+require 'action_view/path_set'
module ActionView
class DependencyTracker # :nodoc:
- @trackers = ThreadSafe::Cache.new
+ @trackers = Concurrent::Map.new
- def self.find_dependencies(name, template)
+ def self.find_dependencies(name, template, view_paths = nil)
tracker = @trackers[template.handler]
+ return [] unless tracker.present?
- if tracker.present?
- tracker.call(name, template)
+ if tracker.respond_to?(:supports_view_paths?) && tracker.supports_view_paths?
+ tracker.call(name, template, view_paths)
else
- []
+ tracker.call(name, template)
end
end
@@ -76,12 +78,22 @@ module ActionView
(?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest
/xm
- def self.call(name, template)
- new(name, template).dependencies
+ LAYOUT_DEPENDENCY = /\A
+ (?:\s*\(?\s*) # optional opening paren surrounded by spaces
+ (?:.*?#{LAYOUT_HASH_KEY}) # check if the line has layout key declaration
+ (?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest
+ /xm
+
+ def self.supports_view_paths? # :nodoc:
+ true
+ end
+
+ def self.call(name, template, view_paths = nil)
+ new(name, template, view_paths).dependencies
end
- def initialize(name, template)
- @name, @template = name, template
+ def initialize(name, template, view_paths = nil)
+ @name, @template, @view_paths = name, template, view_paths
end
def dependencies
@@ -106,15 +118,20 @@ module ActionView
render_calls = source.split(/\brender\b/).drop(1)
render_calls.each do |arguments|
- arguments.scan(RENDER_ARGUMENTS) do
- add_dynamic_dependency(render_dependencies, Regexp.last_match[:dynamic])
- add_static_dependency(render_dependencies, Regexp.last_match[:static])
- end
+ add_dependencies(render_dependencies, arguments, LAYOUT_DEPENDENCY)
+ add_dependencies(render_dependencies, arguments, RENDER_ARGUMENTS)
end
render_dependencies.uniq
end
+ def add_dependencies(render_dependencies, arguments, pattern)
+ arguments.scan(pattern) do
+ add_dynamic_dependency(render_dependencies, Regexp.last_match[:dynamic])
+ add_static_dependency(render_dependencies, Regexp.last_match[:static])
+ end
+ end
+
def add_dynamic_dependency(dependencies, dependency)
if dependency
dependencies << "#{dependency.pluralize}/#{dependency.singularize}"
@@ -131,8 +148,22 @@ module ActionView
end
end
+ def resolve_directories(wildcard_dependencies)
+ return [] unless @view_paths
+
+ wildcard_dependencies.each_with_object([]) do |query, templates|
+ @view_paths.find_all_with_query(query).each do |template|
+ templates << "#{File.dirname(query)}/#{File.basename(template).split('.').first}"
+ end
+ end
+ end
+
def explicit_dependencies
- source.scan(EXPLICIT_DEPENDENCY).flatten.uniq
+ dependencies = source.scan(EXPLICIT_DEPENDENCY).flatten.uniq
+
+ wildcards, explicits = dependencies.partition { |dependency| dependency[-1] == '*' }
+
+ (explicits + resolve_directories(wildcards)).uniq
end
end
diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb
index 1f103786cb..12e9723a02 100644
--- a/actionview/lib/action_view/digestor.rb
+++ b/actionview/lib/action_view/digestor.rb
@@ -1,18 +1,25 @@
-require 'thread_safe'
+require 'concurrent'
require 'action_view/dependency_tracker'
require 'monitor'
module ActionView
class Digestor
cattr_reader(:cache)
- @@cache = ThreadSafe::Cache.new
+ @@cache = Concurrent::Map.new
@@digest_monitor = Monitor.new
+ class PerRequestDigestCacheExpiry < Struct.new(:app) # :nodoc:
+ def call(env)
+ ActionView::Digestor.cache.clear
+ app.call(env)
+ end
+ end
+
class << self
# Supported options:
#
# * <tt>name</tt> - Template name
- # * <tt>finder</tt> - An instance of ActionView::LookupContext
+ # * <tt>finder</tt> - An instance of <tt>ActionView::LookupContext</tt>
# * <tt>dependencies</tt> - An array of dependent views
# * <tt>partial</tt> - Specifies whether the template is a partial
def digest(options)
@@ -21,7 +28,7 @@ module ActionView
cache_key = ([ options[:name], options[:finder].details_key.hash ].compact + Array.wrap(options[:dependencies])).join('.')
# this is a correctly done double-checked locking idiom
- # (ThreadSafe::Cache's lookups have volatile semantics)
+ # (Concurrent::Map's lookups have volatile semantics)
@@cache[cache_key] || @@digest_monitor.synchronize do
@@cache.fetch(cache_key) do # re-check under lock
compute_and_store_digest(cache_key, options)
@@ -41,10 +48,7 @@ module ActionView
Digestor
end
- digest = klass.new(options).digest
- # Store the actual digest if config.cache_template_loading is true
- @@cache[cache_key] = stored_digest = digest if ActionView::Resolver.caching?
- digest
+ @@cache[cache_key] = stored_digest = klass.new(options).digest
ensure
# something went wrong or ActionView::Resolver.caching? is false, make sure not to corrupt the @@cache
@@cache.delete_pair(cache_key, false) if pre_stored && !stored_digest
@@ -68,9 +72,10 @@ module ActionView
end
def dependencies
- DependencyTracker.find_dependencies(name, template)
+ DependencyTracker.find_dependencies(name, template, finder.view_paths)
rescue ActionView::MissingTemplate
- [] # File doesn't exist, so no dependencies
+ logger.try :error, " '#{name}' file doesn't exist, so no dependencies"
+ []
end
def nested_dependencies
diff --git a/actionview/lib/action_view/gem_version.rb b/actionview/lib/action_view/gem_version.rb
index 9266e55c47..4f45f5b8c8 100644
--- a/actionview/lib/action_view/gem_version.rb
+++ b/actionview/lib/action_view/gem_version.rb
@@ -1,12 +1,12 @@
module ActionView
- # Returns the version of the currently loaded ActionView as a <tt>Gem::Version</tt>
+ # Returns the version of the currently loaded Action View as a <tt>Gem::Version</tt>
def self.gem_version
Gem::Version.new VERSION::STRING
end
module VERSION
- MAJOR = 4
- MINOR = 2
+ MAJOR = 5
+ MINOR = 0
TINY = 0
PRE = "alpha"
diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb
index 669050e7a7..fa46a22500 100644
--- a/actionview/lib/action_view/helpers/asset_tag_helper.rb
+++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb
@@ -60,7 +60,7 @@ module ActionView
tag_options = {
"src" => path_to_javascript(source, path_options)
}.merge!(options)
- content_tag(:script, "", tag_options)
+ content_tag("script".freeze, "", tag_options)
}.join("\n").html_safe
end
@@ -127,7 +127,7 @@ module ActionView
# auto_discovery_link_tag(:rss, {controller: "news", action: "feed"})
# # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/news/feed" />
# auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", {title: "Example RSS"})
- # # => <link rel="alternate" type="application/rss+xml" title="Example RSS" href="http://www.example.com/feed" />
+ # # => <link rel="alternate" type="application/rss+xml" title="Example RSS" href="http://www.example.com/feed.rss" />
def auto_discovery_link_tag(type = :rss, url_options = {}, tag_options = {})
if !(type == :rss || type == :atom) && tag_options[:type].blank?
raise ArgumentError.new("You should pass :type tag_option key explicitly, because you have passed #{type} type other than :rss or :atom.")
@@ -136,7 +136,7 @@ module ActionView
tag(
"link",
"rel" => tag_options[:rel] || "alternate",
- "type" => tag_options[:type] || Mime::Type.lookup_by_extension(type.to_s).to_s,
+ "type" => tag_options[:type] || Mime[type].to_s,
"title" => tag_options[:title] || type.to_s.upcase,
"href" => url_options.is_a?(Hash) ? url_for(url_options.merge(:only_path => false)) : url_options
)
@@ -207,6 +207,7 @@ module ActionView
# # => <img alt="Icon" class="menu_icon" src="/icons/icon.gif" />
def image_tag(source, options={})
options = options.symbolize_keys
+ check_for_image_tag_errors(options)
src = options[:src] = path_to_image(source)
@@ -218,7 +219,7 @@ module ActionView
tag("img", options)
end
- # Returns a string suitable for an html image tag alt attribute.
+ # Returns a string suitable for an HTML image tag alt attribute.
# The +src+ argument is meant to be an image file path.
# The method removes the basename of the file path and the digest,
# if any. It also removes hyphens and underscores from file names and
@@ -236,10 +237,10 @@ module ActionView
# image_alt('underscored_file_name.png')
# # => Underscored file name
def image_alt(src)
- File.basename(src, '.*').sub(/-[[:xdigit:]]{32}\z/, '').tr('-_', ' ').capitalize
+ File.basename(src, '.*'.freeze).sub(/-[[:xdigit:]]{32}\z/, ''.freeze).tr('-_'.freeze, ' '.freeze).capitalize
end
- # Returns an html video tag for the +sources+. If +sources+ is a string,
+ # Returns an HTML video tag for the +sources+. If +sources+ is a string,
# a single video tag will be returned. If +sources+ is an array, a video
# tag with nested source tags for each source will be returned. The
# +sources+ can be full paths or files that exists in your public videos
@@ -318,12 +319,19 @@ module ActionView
end
def extract_dimensions(size)
+ size = size.to_s
if size =~ %r{\A\d+x\d+\z}
size.split('x')
elsif size =~ %r{\A\d+\z}
[size, size]
end
end
+
+ def check_for_image_tag_errors(options)
+ if options[:size] && (options[:height] || options[:width])
+ raise ArgumentError, "Cannot pass a :size option with a :height or :width option"
+ end
+ end
end
end
end
diff --git a/actionview/lib/action_view/helpers/asset_url_helper.rb b/actionview/lib/action_view/helpers/asset_url_helper.rb
index 9e8d005ec7..717b326740 100644
--- a/actionview/lib/action_view/helpers/asset_url_helper.rb
+++ b/actionview/lib/action_view/helpers/asset_url_helper.rb
@@ -31,26 +31,33 @@ module ActionView
# stylesheet_link_tag("application")
# # => <link href="http://assets.example.com/assets/application.css" media="screen" rel="stylesheet" />
#
- # Browsers typically open at most two simultaneous connections to a single
- # host, which means your assets often have to wait for other assets to finish
- # downloading. You can alleviate this by using a <tt>%d</tt> wildcard in the
- # +asset_host+. For example, "assets%d.example.com". If that wildcard is
- # present Rails distributes asset requests among the corresponding four hosts
- # "assets0.example.com", ..., "assets3.example.com". With this trick browsers
- # will open eight simultaneous connections rather than two.
+ # Browsers open a limited number of simultaneous connections to a single
+ # host. The exact number varies by browser and version. This limit may cause
+ # some asset downloads to wait for previous assets to finish before they can
+ # begin. You can use the <tt>%d</tt> wildcard in the +asset_host+ to
+ # distribute the requests over four hosts. For example,
+ # <tt>assets%d.example.com<tt> will spread the asset requests over
+ # "assets0.example.com", ..., "assets3.example.com".
#
# image_tag("rails.png")
# # => <img alt="Rails" src="http://assets0.example.com/assets/rails.png" />
# stylesheet_link_tag("application")
# # => <link href="http://assets2.example.com/assets/application.css" media="screen" rel="stylesheet" />
#
- # To do this, you can either setup four actual hosts, or you can use wildcard
- # DNS to CNAME the wildcard to a single asset host. You can read more about
- # setting up your DNS CNAME records from your ISP.
+ # This may improve the asset loading performance of your application.
+ # It is also possible the combination of additional connection overhead
+ # (DNS, SSL) and the overall browser connection limits may result in this
+ # solution being slower. You should be sure to measure your actual
+ # performance across targeted browsers both before and after this change.
+ #
+ # To implement the corresponding hosts you can either setup four actual
+ # hosts or use wildcard DNS to CNAME the wildcard to a single asset host.
+ # You can read more about setting up your DNS CNAME records from your ISP.
#
# Note: This is purely a browser performance optimization and is not meant
# for server load balancing. See http://www.die.net/musings/page_load_time/
- # for background.
+ # for background and http://www.browserscope.org/?category=network for
+ # connection limit data.
#
# Alternatively, you can exert more control over the asset host by setting
# +asset_host+ to a proc like this:
@@ -121,11 +128,13 @@ module ActionView
# asset_path "application", type: :stylesheet # => /assets/application.css
# asset_path "http://www.example.com/js/xmlhr.js" # => http://www.example.com/js/xmlhr.js
def asset_path(source, options = {})
- return "" unless source.present?
+ raise ArgumentError, "nil is not a valid asset source" if source.nil?
+
source = source.to_s
+ return "" unless source.present?
return source if source =~ URI_REGEXP
- tail, source = source[/([\?#].+)$/], source.sub(/([\?#].+)$/, '')
+ tail, source = source[/([\?#].+)$/], source.sub(/([\?#].+)$/, ''.freeze)
if extname = compute_asset_extname(source, options)
source = "#{source}#{extname}"
@@ -248,6 +257,11 @@ module ActionView
# Computes the full URL to a JavaScript asset in the public javascripts directory.
# This will use +javascript_path+ internally, so most of their behaviors will be the same.
+ # Since +javascript_url+ is based on +asset_url+ method you can set :host options. If :host
+ # options is set, it overwrites global +config.action_controller.asset_host+ setting.
+ #
+ # javascript_url "js/xmlhr.js", host: "http://stage.example.com" # => http://stage.example.com/assets/dir/xmlhr.js
+ #
def javascript_url(source, options = {})
url_to_asset(source, {type: :javascript}.merge!(options))
end
@@ -270,6 +284,11 @@ module ActionView
# Computes the full URL to a stylesheet asset in the public stylesheets directory.
# This will use +stylesheet_path+ internally, so most of their behaviors will be the same.
+ # Since +stylesheet_url+ is based on +asset_url+ method you can set :host options. If :host
+ # options is set, it overwrites global +config.action_controller.asset_host+ setting.
+ #
+ # stylesheet_url "css/style.css", host: "http://stage.example.com" # => http://stage.example.com/css/style.css
+ #
def stylesheet_url(source, options = {})
url_to_asset(source, {type: :stylesheet}.merge!(options))
end
@@ -295,6 +314,11 @@ module ActionView
# Computes the full URL to an image asset.
# This will use +image_path+ internally, so most of their behaviors will be the same.
+ # Since +image_url+ is based on +asset_url+ method you can set :host options. If :host
+ # options is set, it overwrites global +config.action_controller.asset_host+ setting.
+ #
+ # image_url "edit.png", host: "http://stage.example.com" # => http://stage.example.com/edit.png
+ #
def image_url(source, options = {})
url_to_asset(source, {type: :image}.merge!(options))
end
@@ -316,6 +340,11 @@ module ActionView
# Computes the full URL to a video asset in the public videos directory.
# This will use +video_path+ internally, so most of their behaviors will be the same.
+ # Since +video_url+ is based on +asset_url+ method you can set :host options. If :host
+ # options is set, it overwrites global +config.action_controller.asset_host+ setting.
+ #
+ # video_url "hd.avi", host: "http://stage.example.com" # => http://stage.example.com/hd.avi
+ #
def video_url(source, options = {})
url_to_asset(source, {type: :video}.merge!(options))
end
@@ -337,6 +366,11 @@ module ActionView
# Computes the full URL to an audio asset in the public audios directory.
# This will use +audio_path+ internally, so most of their behaviors will be the same.
+ # Since +audio_url+ is based on +asset_url+ method you can set :host options. If :host
+ # options is set, it overwrites global +config.action_controller.asset_host+ setting.
+ #
+ # audio_url "horse.wav", host: "http://stage.example.com" # => http://stage.example.com/horse.wav
+ #
def audio_url(source, options = {})
url_to_asset(source, {type: :audio}.merge!(options))
end
@@ -357,6 +391,11 @@ module ActionView
# Computes the full URL to a font asset.
# This will use +font_path+ internally, so most of their behaviors will be the same.
+ # Since +font_url+ is based on +asset_url+ method you can set :host options. If :host
+ # options is set, it overwrites global +config.action_controller.asset_host+ setting.
+ #
+ # font_url "font.ttf", host: "http://stage.example.com" # => http://stage.example.com/font.ttf
+ #
def font_url(source, options = {})
url_to_asset(source, {type: :font}.merge!(options))
end
diff --git a/actionview/lib/action_view/helpers/atom_feed_helper.rb b/actionview/lib/action_view/helpers/atom_feed_helper.rb
index 227ad4cdfa..bb1cdd0f8d 100644
--- a/actionview/lib/action_view/helpers/atom_feed_helper.rb
+++ b/actionview/lib/action_view/helpers/atom_feed_helper.rb
@@ -16,7 +16,7 @@ module ActionView
# end
#
# app/controllers/posts_controller.rb:
- # class PostsController < ApplicationController::Base
+ # class PostsController < ApplicationController
# # GET /posts.html
# # GET /posts.atom
# def index
@@ -51,7 +51,7 @@ module ActionView
# * <tt>:language</tt>: Defaults to "en-US".
# * <tt>:root_url</tt>: The HTML alternative that this feed is doubling for. Defaults to / on the current host.
# * <tt>:url</tt>: The URL for this feed. Defaults to the current URL.
- # * <tt>:id</tt>: The id for this feed. Defaults to "tag:#{request.host},#{options[:schema_date]}:#{request.fullpath.split(".")[0]}"
+ # * <tt>:id</tt>: The id for this feed. Defaults to "tag:localhost,2005:/posts", in this case.
# * <tt>:schema_date</tt>: The date at which the tag scheme for the feed was first used. A good default is the year you
# created the feed. See http://feedvalidator.org/docs/error/InvalidTAG.html for more information. If not specified,
# 2005 is used (as an "I don't care" value).
@@ -174,7 +174,7 @@ module ActionView
#
# * <tt>:published</tt>: Time first published. Defaults to the created_at attribute on the record if one such exists.
# * <tt>:updated</tt>: Time of update. Defaults to the updated_at attribute on the record if one such exists.
- # * <tt>:url</tt>: The URL for this entry. Defaults to the polymorphic_url for the record.
+ # * <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.
# * <tt>:id</tt>: The ID for this entry. Defaults to "tag:#{@view.request.host},#{@feed_options[:schema_date]}:#{record.class}/#{record.id}"
# * <tt>:type</tt>: The TYPE for this entry. Defaults to "text/html".
def entry(record, options = {})
@@ -191,7 +191,8 @@ module ActionView
type = options.fetch(:type, 'text/html')
- @xml.link(:rel => 'alternate', :type => type, :href => options[:url] || @view.polymorphic_url(record))
+ url = options.fetch(:url) { @view.polymorphic_url(record) }
+ @xml.link(:rel => 'alternate', :type => type, :href => url) if url
yield AtomBuilder.new(@xml)
end
diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb
index 4db8930a26..e473aeaea9 100644
--- a/actionview/lib/action_view/helpers/cache_helper.rb
+++ b/actionview/lib/action_view/helpers/cache_helper.rb
@@ -39,7 +39,7 @@ module ActionView
# This will include both records as part of the cache key and updating either of them will
# expire the cache.
#
- # ==== Template digest
+ # ==== \Template digest
#
# The template digest that's added to the cache key is computed by taking an md5 of the
# contents of the entire template file. This ensures that your caches will automatically
@@ -75,7 +75,8 @@ module ActionView
# render(topics) => render("topics/topic")
# render(message.topics) => render("topics/topic")
#
- # It's not possible to derive all render calls like that, though. Here are a few examples of things that can't be derived:
+ # It's not possible to derive all render calls like that, though.
+ # Here are a few examples of things that can't be derived:
#
# render group_of_attachments
# render @project.documents.where(published: true).order('created_at')
@@ -97,21 +98,74 @@ module ActionView
# <%# Template Dependency: todolists/todolist %>
# <%= render_sortable_todolists @project.todolists %>
#
- # The pattern used to match these is /# Template Dependency: ([^ ]+)/, so it's important that you type it out just so.
+ # In some cases, like a single table inheritance setup, you might have
+ # a bunch of explicit dependencies. Instead of writing every template out,
+ # you can use a wildcard to match any template in a directory:
+ #
+ # <%# Template Dependency: events/* %>
+ # <%= render_categorizable_events @person.events %>
+ #
+ # This marks every template in the directory as a dependency. To find those
+ # templates, the wildcard path must be absolutely defined from app/views or paths
+ # otherwise added with +prepend_view_path+ or +append_view_path+.
+ # This way the wildcard for `app/views/recordings/events` would be `recordings/events/*` etc.
+ #
+ # The pattern used to match explicit dependencies is <tt>/# Template Dependency: (\S+)/</tt>,
+ # so it's important that you type it out just so.
# You can only declare one template dependency per line.
#
# === External dependencies
#
- # If you use a helper method, for example, inside of a cached block and you then update that helper,
- # you'll have to bump the cache as well. It doesn't really matter how you do it, but the md5 of the template file
+ # If you use a helper method, for example, inside a cached block and
+ # you then update that helper, you'll have to bump the cache as well.
+ # It doesn't really matter how you do it, but the md5 of the template file
# must change. One recommendation is to simply be explicit in a comment, like:
#
# <%# Helper Dependency Updated: May 6, 2012 at 6pm %>
# <%= some_helper_method(person) %>
#
- # Now all you'll have to do is change that timestamp when the helper method changes.
- def cache(name = {}, options = nil, &block)
- if controller.perform_caching
+ # Now all you have to do is change that timestamp when the helper method changes.
+ #
+ # === Automatic Collection Caching
+ #
+ # When rendering collections such as:
+ #
+ # <%= render @notifications %>
+ # <%= render partial: 'notifications/notification', collection: @notifications %>
+ #
+ # If the notifications/_notification partial starts with a cache call as:
+ #
+ # <% cache notification do %>
+ # <%= notification.name %>
+ # <% end %>
+ #
+ # The collection can then automatically use any cached renders for that
+ # template by reading them at once instead of one by one.
+ #
+ # See ActionView::Template::Handlers::ERB.resource_cache_call_pattern for
+ # more information on what cache calls make a template eligible for this
+ # collection caching.
+ #
+ # The automatic cache multi read can be turned off like so:
+ #
+ # <%= render @notifications, cache: false %>
+ #
+ # === Explicit Collection Caching
+ #
+ # If the partial template doesn't start with a clean cache call as
+ # mentioned above, you can still benefit from collection caching by
+ # adding a special comment format anywhere in the template, like:
+ #
+ # <%# Template Collection: notification %>
+ # <% my_helper_that_calls_cache(some_arg, notification) do %>
+ # <%= notification.name %>
+ # <% end %>
+ #
+ # The pattern used to match these is <tt>/# Template Collection: (\S+)/</tt>,
+ # so it's important that you type it out just so.
+ # You can only declare one collection in a partial template file.
+ def cache(name = {}, options = {}, &block)
+ if controller.respond_to?(:perform_caching) && controller.perform_caching
safe_concat(fragment_for(cache_fragment_name(name, options), options, &block))
else
yield
@@ -122,11 +176,11 @@ module ActionView
# Cache fragments of a view if +condition+ is true
#
- # <%= cache_if admin?, project do %>
+ # <% cache_if admin?, project do %>
# <b>All the topics on this project</b>
# <%= render project.topics %>
# <% end %>
- def cache_if(condition, name = {}, options = nil, &block)
+ def cache_if(condition, name = {}, options = {}, &block)
if condition
cache(name, options, &block)
else
@@ -138,37 +192,46 @@ module ActionView
# Cache fragments of a view unless +condition+ is true
#
- # <%= cache_unless admin?, project do %>
+ # <% cache_unless admin?, project do %>
# <b>All the topics on this project</b>
# <%= render project.topics %>
# <% end %>
- def cache_unless(condition, name = {}, options = nil, &block)
+ def cache_unless(condition, name = {}, options = {}, &block)
cache_if !condition, name, options, &block
end
# This helper returns the name of a cache key for a given fragment cache
- # call. By supplying skip_digest: true to cache, the digestion of cache
+ # call. By supplying +skip_digest:+ true to cache, the digestion of cache
# fragments can be manually bypassed. This is useful when cache fragments
# cannot be manually expired unless you know the exact key which is the
# case when using memcached.
- def cache_fragment_name(name = {}, options = nil)
- skip_digest = options && options[:skip_digest]
-
+ #
+ # The digest will be generated using +virtual_path:+ if it is provided.
+ #
+ def cache_fragment_name(name = {}, skip_digest: nil, virtual_path: nil)
if skip_digest
name
else
- fragment_name_with_digest(name)
+ fragment_name_with_digest(name, virtual_path)
end
end
- private
+ # Given a key (as described in ActionController::Caching::Fragments.expire_fragment),
+ # returns a key suitable for use in reading, writing, or expiring a
+ # cached fragment. All keys are prefixed with <tt>views/</tt> and uses
+ # ActiveSupport::Cache.expand_cache_key for the expansion.
+ def fragment_cache_key(key)
+ ActiveSupport::Cache.expand_cache_key(key.is_a?(Hash) ? url_for(key).split("://").last : key, :views)
+ end
- def fragment_name_with_digest(name) #:nodoc:
- if @virtual_path
- names = Array(name.is_a?(Hash) ? controller.url_for(name).split("://").last : name)
- digest = Digestor.digest name: @virtual_path, finder: lookup_context, dependencies: view_cache_dependencies
+ private
- [ *names, digest ]
+ def fragment_name_with_digest(name, virtual_path) #:nodoc:
+ virtual_path ||= @virtual_path
+ if virtual_path
+ name = controller.url_for(name).split("://").last if name.is_a?(Hash)
+ digest = Digestor.digest name: virtual_path, finder: lookup_context, dependencies: view_cache_dependencies
+ [ name, digest ]
else
name
end
diff --git a/actionview/lib/action_view/helpers/capture_helper.rb b/actionview/lib/action_view/helpers/capture_helper.rb
index 75d1634b2e..93c7cba395 100644
--- a/actionview/lib/action_view/helpers/capture_helper.rb
+++ b/actionview/lib/action_view/helpers/capture_helper.rb
@@ -31,7 +31,8 @@ module ActionView
# <head><title><%= @greeting %></title></head>
# <body>
# <b><%= @greeting %></b>
- # </body></html>
+ # </body>
+ # </html>
#
def capture(*args)
value = nil
@@ -114,7 +115,7 @@ module ActionView
# <li><%= link_to 'Home', action: 'index' %></li>
# <% end %>
#
- # And in other place:
+ # And in another place:
#
# <% content_for :navigation do %>
# <li><%= link_to 'Login', action: 'login' %></li>
@@ -194,7 +195,9 @@ module ActionView
def with_output_buffer(buf = nil) #:nodoc:
unless buf
buf = ActionView::OutputBuffer.new
- buf.force_encoding(output_buffer.encoding) if output_buffer
+ if output_buffer && output_buffer.respond_to?(:encoding)
+ buf.force_encoding(output_buffer.encoding)
+ end
end
self.output_buffer, old_buffer = buf, output_buffer
yield
diff --git a/actionview/lib/action_view/helpers/controller_helper.rb b/actionview/lib/action_view/helpers/controller_helper.rb
index 74ef25f7c1..3569fba8c6 100644
--- a/actionview/lib/action_view/helpers/controller_helper.rb
+++ b/actionview/lib/action_view/helpers/controller_helper.rb
@@ -14,6 +14,7 @@ module ActionView
if @_controller = controller
@_request = controller.request if controller.respond_to?(:request)
@_config = controller.config.inheritable_copy if controller.respond_to?(:config)
+ @_default_form_builder = controller.default_form_builder if controller.respond_to?(:default_form_builder)
end
end
diff --git a/actionview/lib/action_view/helpers/date_helper.rb b/actionview/lib/action_view/helpers/date_helper.rb
index 27c7a26098..312e41ee48 100644
--- a/actionview/lib/action_view/helpers/date_helper.rb
+++ b/actionview/lib/action_view/helpers/date_helper.rb
@@ -68,6 +68,27 @@ module ActionView
# distance_of_time_in_words(from_time, to_time, include_seconds: true) # => about 6 years
# distance_of_time_in_words(to_time, from_time, include_seconds: true) # => about 6 years
# distance_of_time_in_words(Time.now, Time.now) # => less than a minute
+ #
+ # With the <tt>scope</tt> option, you can define a custom scope for Rails
+ # to look up the translation.
+ #
+ # For example you can define the following in your locale (e.g. en.yml).
+ #
+ # datetime:
+ # distance_in_words:
+ # short:
+ # about_x_hours:
+ # one: 'an hour'
+ # other: '%{count} hours'
+ #
+ # See https://github.com/svenfuchs/rails-i18n/blob/master/rails/locale/en.yml
+ # for more examples.
+ #
+ # Which will then result in the following:
+ #
+ # from_time = Time.now
+ # distance_of_time_in_words(from_time, from_time + 50.minutes, scope: 'datetime.distance_in_words.short') # => "an hour"
+ # distance_of_time_in_words(from_time, from_time + 3.hours, scope: 'datetime.distance_in_words.short') # => "3 hours"
def distance_of_time_in_words(from_time, to_time = 0, options = {})
options = {
scope: :'datetime.distance_in_words'
@@ -177,7 +198,9 @@ module ActionView
# and +:name+ (string). A format string would be something like "%{name} (%<number>02d)" for example.
# See <tt>Kernel.sprintf</tt> for documentation on format sequences.
# * <tt>:date_separator</tt> - Specifies a string to separate the date fields. Default is "" (i.e. nothing).
- # * <tt>:start_year</tt> - Set the start year for the year select. Default is <tt>Date.today.year - 5</tt>if
+ # * <tt>:time_separator</tt> - Specifies a string to separate the time fields. Default is "" (i.e. nothing).
+ # * <tt>:datetime_separator</tt>- Specifies a string to separate the date and time fields. Default is "" (i.e. nothing).
+ # * <tt>:start_year</tt> - Set the start year for the year select. Default is <tt>Date.today.year - 5</tt> if
# you are creating new record. While editing existing record, <tt>:start_year</tt> defaults to
# the current selected year minus 5.
# * <tt>:end_year</tt> - Set the end year for the year select. Default is <tt>Date.today.year + 5</tt> if
@@ -205,6 +228,7 @@ module ActionView
# or the given prompt string.
# * <tt>:with_css_classes</tt> - Set to true if you want assign different styles for 'select' tags. This option
# automatically set classes 'year', 'month', 'day', 'hour', 'minute' and 'second' for your 'select' tags.
+ # * <tt>:use_hidden</tt> - Set to true if you only want to generate hidden input tags.
#
# If anything is passed in the +html_options+ hash it will be applied to every select tag in the set.
#
@@ -330,7 +354,7 @@ module ActionView
Tags::DatetimeSelect.new(object_name, method, self, options, html_options).render
end
- # Returns a set of html select-tags (one for year, month, day, hour, minute, and second) pre-selected with the
+ # Returns a set of HTML select-tags (one for year, month, day, hour, minute, and second) pre-selected with the
# +datetime+. It's also possible to explicitly set the order of the tags using the <tt>:order</tt> option with
# an array of symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. If you do not
# supply a Symbol, it will be appended onto the <tt>:order</tt> passed in. You can also add
@@ -379,7 +403,7 @@ module ActionView
DateTimeSelector.new(datetime, options, html_options).select_datetime
end
- # Returns a set of html select-tags (one for year, month, and day) pre-selected with the +date+.
+ # Returns a set of HTML select-tags (one for year, month, and day) pre-selected with the +date+.
# It's possible to explicitly set the order of the tags using the <tt>:order</tt> option with an array of
# symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order.
# If the array passed to the <tt>:order</tt> option does not contain all the three symbols, all tags will be hidden.
@@ -418,7 +442,7 @@ module ActionView
DateTimeSelector.new(date, options, html_options).select_date
end
- # Returns a set of html select-tags (one for hour and minute).
+ # Returns a set of HTML select-tags (one for hour and minute).
# You can set <tt>:time_separator</tt> key to format the output, and
# the <tt>:include_seconds</tt> option to include an input for seconds.
#
@@ -462,7 +486,7 @@ module ActionView
# The <tt>datetime</tt> can be either a +Time+ or +DateTime+ object or an integer.
# Override the field name using the <tt>:field_name</tt> option, 'second' by default.
#
- # my_time = Time.now + 16.minutes
+ # my_time = Time.now + 16.seconds
#
# # Generates a select field for seconds that defaults to the seconds for the time in my_time.
# select_second(my_time)
@@ -486,7 +510,7 @@ module ActionView
# selected. The <tt>datetime</tt> can be either a +Time+ or +DateTime+ object or an integer.
# Override the field name using the <tt>:field_name</tt> option, 'minute' by default.
#
- # my_time = Time.now + 6.hours
+ # my_time = Time.now + 10.minutes
#
# # Generates a select field for minutes that defaults to the minutes for the time in my_time.
# select_minute(my_time)
@@ -635,7 +659,7 @@ module ActionView
DateTimeSelector.new(date, options, html_options).select_year
end
- # Returns an html time tag for the given date or time.
+ # Returns an HTML time tag for the given date or time.
#
# time_tag Date.today # =>
# <time datetime="2010-11-04">November 04, 2010</time>
@@ -658,7 +682,7 @@ module ActionView
content = args.first || I18n.l(date_or_time, :format => format)
datetime = date_or_time.acts_like?(:time) ? date_or_time.xmlschema : date_or_time.iso8601
- content_tag(:time, content, options.reverse_merge(:datetime => datetime), &block)
+ content_tag("time".freeze, content, options.reverse_merge(:datetime => datetime), &block)
end
end
@@ -786,7 +810,7 @@ module ActionView
1.upto(12) do |month_number|
options = { :value => month_number }
options[:selected] = "selected" if month == month_number
- month_options << content_tag(:option, month_name(month_number), options) + "\n"
+ month_options << content_tag("option".freeze, month_name(month_number), options) + "\n"
end
build_select(:month, month_options.join)
end
@@ -898,7 +922,7 @@ module ActionView
def translated_date_order
date_order = I18n.translate(:'date.order', :locale => @options[:locale], :default => [])
- date_order = date_order.map { |element| element.to_sym }
+ date_order = date_order.map(&:to_sym)
forbidden_elements = date_order - [:year, :month, :day]
if forbidden_elements.any?
@@ -914,7 +938,7 @@ module ActionView
build_select(type, build_options(selected, options))
end
- # Build select option html from date value and options.
+ # Build select option HTML from date value and options.
# build_options(15, start: 1, end: 31)
# => "<option value="1">1</option>
# <option value="2">2</option>
@@ -948,13 +972,13 @@ module ActionView
tag_options[:selected] = "selected" if selected == i
text = options[:use_two_digit_numbers] ? sprintf("%02d", i) : value
text = options[:ampm] ? AMPM_TRANSLATION[i] : text
- select_options << content_tag(:option, text, tag_options)
+ select_options << content_tag("option".freeze, text, tag_options)
end
(select_options.join("\n") + "\n").html_safe
end
- # Builds select tag from date type and html select options.
+ # Builds select tag from date type and HTML select options.
# build_select(:month, "<option value="1">January</option>...")
# => "<select id="post_written_on_2i" name="post[written_on(2i)]">
# <option value="1">January</option>...
@@ -968,11 +992,11 @@ module ActionView
select_options[:class] = [select_options[:class], type].compact.join(' ') if @options[:with_css_classes]
select_html = "\n"
- select_html << content_tag(:option, '', :value => '') + "\n" if @options[:include_blank]
+ select_html << content_tag("option".freeze, '', :value => '') + "\n" if @options[:include_blank]
select_html << prompt_option_tag(type, @options[:prompt]) + "\n" if @options[:prompt]
select_html << select_options_as_html
- (content_tag(:select, select_html.html_safe, select_options) + "\n").html_safe
+ (content_tag("select".freeze, select_html.html_safe, select_options) + "\n").html_safe
end
# Builds a prompt option tag with supplied options or from default options.
@@ -989,7 +1013,7 @@ module ActionView
I18n.translate(:"datetime.prompts.#{type}", :locale => @options[:locale])
end
- prompt ? content_tag(:option, prompt, :value => '') : ''
+ prompt ? content_tag("option".freeze, prompt, :value => '') : ''
end
# Builds hidden input tag for date part and value.
@@ -1035,7 +1059,7 @@ module ActionView
def build_selects_from_types(order)
select = ''
first_visible = order.find { |type| !@options[:"discard_#{type}"] }
- order.reverse.each do |type|
+ order.reverse_each do |type|
separator = separator(type) unless type == first_visible # don't add before first visible field
select.insert(0, separator.to_s + send("select_#{type}").to_s)
end
diff --git a/actionview/lib/action_view/helpers/debug_helper.rb b/actionview/lib/action_view/helpers/debug_helper.rb
index ba47eee9ba..e9dccbad1c 100644
--- a/actionview/lib/action_view/helpers/debug_helper.rb
+++ b/actionview/lib/action_view/helpers/debug_helper.rb
@@ -26,7 +26,7 @@ module ActionView
Marshal::dump(object)
object = ERB::Util.html_escape(object.to_yaml)
content_tag(:pre, object, :class => "debug_dump")
- rescue Exception # errors from Marshal or YAML
+ rescue # errors from Marshal or YAML
# Object couldn't be dumped, perhaps because of singleton methods -- this is the fallback
content_tag(:code, object.inspect, :class => "debug_dump")
end
diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb
index c6bc0c9e38..2a367b85af 100644
--- a/actionview/lib/action_view/helpers/form_helper.rb
+++ b/actionview/lib/action_view/helpers/form_helper.rb
@@ -4,6 +4,7 @@ require 'action_view/helpers/tag_helper'
require 'action_view/helpers/form_tag_helper'
require 'action_view/helpers/active_model_helper'
require 'action_view/model_naming'
+require 'action_view/record_identifier'
require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/hash/slice'
require 'active_support/core_ext/string/output_safety'
@@ -51,9 +52,7 @@ module ActionView
# The HTML generated for this would be (modulus formatting):
#
# <form action="/people" class="new_person" id="new_person" method="post">
- # <div style="display:none">
- # <input name="authenticity_token" type="hidden" value="NrOp5bsjoLRuK8IW5+dQEYjKGUJDe7TQoZVvq95Wteg=" />
- # </div>
+ # <input name="authenticity_token" type="hidden" value="NrOp5bsjoLRuK8IW5+dQEYjKGUJDe7TQoZVvq95Wteg=" />
# <label for="person_first_name">First name</label>:
# <input id="person_first_name" name="person[first_name]" type="text" /><br />
#
@@ -68,9 +67,10 @@ module ActionView
#
# In particular, thanks to the conventions followed in the generated field names, the
# controller gets a nested hash <tt>params[:person]</tt> with the person attributes
- # set in the form. That hash is ready to be passed to <tt>Person.create</tt>:
+ # set in the form. That hash is ready to be passed to <tt>Person.new</tt>:
#
- # if @person = Person.create(params[:person])
+ # @person = Person.new(params[:person])
+ # if @person.save
# # success
# else
# # error handling
@@ -81,10 +81,8 @@ module ActionView
# the code above as is would yield instead:
#
# <form action="/people/256" class="edit_person" id="edit_person_256" method="post">
- # <div style="display:none">
- # <input name="_method" type="hidden" value="patch" />
- # <input name="authenticity_token" type="hidden" value="NrOp5bsjoLRuK8IW5+dQEYjKGUJDe7TQoZVvq95Wteg=" />
- # </div>
+ # <input name="_method" type="hidden" value="patch" />
+ # <input name="authenticity_token" type="hidden" value="NrOp5bsjoLRuK8IW5+dQEYjKGUJDe7TQoZVvq95Wteg=" />
# <label for="person_first_name">First name</label>:
# <input id="person_first_name" name="person[first_name]" type="text" value="John" /><br />
#
@@ -114,6 +112,9 @@ module ActionView
include FormTagHelper
include UrlHelper
include ModelNaming
+ include RecordIdentifier
+
+ attr_internal :default_form_builder
# Creates a form that allows the user to create or update the attributes
# of a specific model object.
@@ -142,7 +143,8 @@ module ActionView
# will get expanded to
#
# <%= text_field :person, :first_name %>
- # which results in an html <tt><input></tt> tag whose +name+ attribute is
+ #
+ # which results in an HTML <tt><input></tt> tag whose +name+ attribute is
# <tt>person[first_name]</tt>. This means that when the form is submitted,
# the value entered by the user will be available in the controller as
# <tt>params[:person][:first_name]</tt>.
@@ -168,6 +170,23 @@ module ActionView
# * <tt>:namespace</tt> - A namespace for your form to ensure uniqueness of
# id attributes on form elements. The namespace attribute will be prefixed
# with underscore on the generated HTML id.
+ # * <tt>:method</tt> - The method to use when submitting the form, usually
+ # either "get" or "post". If "patch", "put", "delete", or another verb
+ # is used, a hidden input with name <tt>_method</tt> is added to
+ # simulate the verb over post.
+ # * <tt>:authenticity_token</tt> - Authenticity token to use in the form.
+ # Use only if you need to pass custom authenticity token string, or to
+ # not add authenticity_token field at all (by passing <tt>false</tt>).
+ # Remote forms may omit the embedded authenticity token by setting
+ # <tt>config.action_view.embed_authenticity_token_in_remote_forms = false</tt>.
+ # This is helpful when you're fragment-caching the form. Remote forms
+ # get the authenticity token from the <tt>meta</tt> tag, so embedding is
+ # unnecessary unless you support browsers without JavaScript.
+ # * <tt>:remote</tt> - If set to true, will allow the Unobtrusive
+ # JavaScript drivers to control the submit behavior. By default this
+ # behavior is an ajax submit.
+ # * <tt>:enforce_utf8</tt> - If set to false, a hidden input with name
+ # utf8 is not output.
# * <tt>:html</tt> - Optional HTML attributes for the form tag.
#
# Also note that +form_for+ doesn't create an exclusive scope. It's still
@@ -315,9 +334,7 @@ module ActionView
# The HTML generated for this would be:
#
# <form action='http://www.example.com' method='post' data-remote='true'>
- # <div style='display:none'>
- # <input name='_method' type='hidden' value='patch' />
- # </div>
+ # <input name='_method' type='hidden' value='patch' />
# ...
# </form>
#
@@ -333,9 +350,7 @@ module ActionView
# The HTML generated for this would be:
#
# <form action='http://www.example.com' method='post' data-behavior='autosave' name='go'>
- # <div style='display:none'>
- # <input name='_method' type='hidden' value='patch' />
- # </div>
+ # <input name='_method' type='hidden' value='patch' />
# ...
# </form>
#
@@ -428,6 +443,7 @@ module ActionView
html_options[:data] = options.delete(:data) if options.has_key?(:data)
html_options[:remote] = options.delete(:remote) if options.has_key?(:remote)
html_options[:method] = options.delete(:method) if options.has_key?(:method)
+ html_options[:enforce_utf8] = options.delete(:enforce_utf8) if options.has_key?(:enforce_utf8)
html_options[:authenticity_token] = options.delete(:authenticity_token)
builder = instantiate_builder(object_name, object, options)
@@ -833,8 +849,8 @@ module ActionView
# file_field(:user, :avatar)
# # => <input type="file" id="user_avatar" name="user[avatar]" />
#
- # file_field(:post, :image, :multiple => true)
- # # => <input type="file" id="post_image" name="post[image]" multiple="true" />
+ # file_field(:post, :image, multiple: true)
+ # # => <input type="file" id="post_image" name="post[image][]" multiple="multiple" />
#
# file_field(:post, :attached, accept: 'text/html')
# # => <input accept="text/html" type="file" id="post_attached" name="post[attached]" />
@@ -844,6 +860,24 @@ module ActionView
#
# file_field(:attachment, :file, class: 'file_input')
# # => <input type="file" id="attachment_file" name="attachment[file]" class="file_input" />
+ #
+ # ==== Gotcha
+ #
+ # The HTML specification says that when a file field is empty, web browsers
+ # do not send any value to the server. Unfortunately this introduces a
+ # gotcha: if a +User+ model has an +avatar+ field, and no file is selected,
+ # then the +avatar+ parameter is empty. Thus, any mass-assignment idiom like
+ #
+ # @user.update(params[:user])
+ #
+ # wouldn't update the +avatar+ field.
+ #
+ # To prevent this, the helper generates an auxiliary hidden field before
+ # every file field. The hidden field has the same name as the file one and
+ # a blank value.
+ #
+ # In case you don't want the helper to generate this hidden field you can
+ # specify the <tt>include_hidden: false</tt> option.
def file_field(object_name, method, options = {})
Tags::FileField.new(object_name, method, self, options).render
end
@@ -1004,7 +1038,7 @@ module ActionView
# date_field("user", "born_on")
# # => <input id="user_born_on" name="user[born_on]" type="date" />
#
- # The default value is generated by trying to call "to_date"
+ # The default value is generated by trying to call +strftime+ with "%Y-%m-%d"
# on the object's value, which makes it behave as expected for instances
# of DateTime and ActiveSupport::TimeWithZone. You can still override that
# by passing the "value" option explicitly, e.g.
@@ -1196,12 +1230,12 @@ module ActionView
object_name = model_name_from_record_or_class(object).param_key
end
- builder = options[:builder] || default_form_builder
+ builder = options[:builder] || default_form_builder_class
builder.new(object_name, object, self, options)
end
- def default_form_builder
- builder = ActionView::Base.default_form_builder
+ def default_form_builder_class
+ builder = default_form_builder || ActionView::Base.default_form_builder
builder.respond_to?(:constantize) ? builder.constantize : builder
end
end
@@ -1216,7 +1250,7 @@ module ActionView
# Admin: <%= person_form.check_box :admin %>
# <% end %>
#
- # In the above block, the a +FormBuilder+ object is yielded as the
+ # In the above block, a +FormBuilder+ object is yielded as the
# +person_form+ variable. This allows you to generate the +text_field+
# and +check_box+ fields by specifying their eponymous methods, which
# modify the underlying template and associates the +@person+ model object
@@ -1237,10 +1271,11 @@ module ActionView
# )
# )
# end
+ # end
#
# The above code creates a new method +div_radio_button+ which wraps a div
- # around the a new radio button. Note that when options are passed in, you
- # must called +objectify_options+ in order for the model object to get
+ # around the new radio button. Note that when options are passed in, you
+ # must call +objectify_options+ in order for the model object to get
# correctly passed to the method. If +objectify_options+ is not called,
# then the newly created helper will not be linked back to the model.
#
@@ -1582,7 +1617,14 @@ module ActionView
@auto_index
end
- record_name = index ? "#{object_name}[#{index}][#{record_name}]" : "#{object_name}[#{record_name}]"
+ record_name = if index
+ "#{object_name}[#{index}][#{record_name}]"
+ elsif record_name.to_s.end_with?('[]')
+ record_name = record_name.to_s.sub(/(.*)\[\]$/, "[\\1][#{record_object.id}]")
+ "#{object_name}#{record_name}"
+ else
+ "#{object_name}[#{record_name}]"
+ end
fields_options[:child_index] = index
@template.fields_for(record_name, record_object, fields_options, &block)
@@ -1596,7 +1638,7 @@ module ActionView
# target labels for radio_button tags (where the value is used in the ID of the input tag).
#
# ==== Examples
- # label(:post, :title)
+ # label(:title)
# # => <label for="post_title">Title</label>
#
# You can localize your labels based on model and attribute names.
@@ -1609,7 +1651,7 @@ module ActionView
#
# Which then will result in
#
- # label(:post, :body)
+ # label(:body)
# # => <label for="post_body">Write your entire text here</label>
#
# Localization can also be based purely on the translation of the attribute-name
@@ -1620,21 +1662,22 @@ module ActionView
# post:
# cost: "Total cost"
#
- # label(:post, :cost)
+ # label(:cost)
# # => <label for="post_cost">Total cost</label>
#
- # label(:post, :title, "A short title")
+ # label(:title, "A short title")
# # => <label for="post_title">A short title</label>
#
- # label(:post, :title, "A short title", class: "title_label")
+ # label(:title, "A short title", class: "title_label")
# # => <label for="post_title" class="title_label">A short title</label>
#
- # label(:post, :privacy, "Public Post", value: "public")
+ # label(:privacy, "Public Post", value: "public")
# # => <label for="post_privacy_public">Public Post</label>
#
- # label(:post, :terms) do
+ # label(:terms) do
# 'Accept <a href="/terms">Terms</a>.'.html_safe
# end
+ # # => <label for="post_terms">Accept <a href="/terms">Terms</a>.</label>
def label(method, text = nil, options = {}, &block)
@template.label(@object_name, method, text, objectify_options(options), &block)
end
@@ -1683,16 +1726,17 @@ module ActionView
# hashes instead of arrays.
#
# # Let's say that @post.validated? is 1:
- # check_box("post", "validated")
+ # check_box("validated")
# # => <input name="post[validated]" type="hidden" value="0" />
# # <input checked="checked" type="checkbox" id="post_validated" name="post[validated]" value="1" />
#
# # Let's say that @puppy.gooddog is "no":
- # check_box("puppy", "gooddog", {}, "yes", "no")
+ # check_box("gooddog", {}, "yes", "no")
# # => <input name="puppy[gooddog]" type="hidden" value="no" />
# # <input type="checkbox" id="puppy_gooddog" name="puppy[gooddog]" value="yes" />
#
- # check_box("eula", "accepted", { class: 'eula_check' }, "yes", "no")
+ # # Let's say that @eula.accepted is "no":
+ # check_box("accepted", { class: 'eula_check' }, "yes", "no")
# # => <input name="eula[accepted]" type="hidden" value="no" />
# # <input type="checkbox" class="eula_check" id="eula_accepted" name="eula[accepted]" value="yes" />
def check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
@@ -1707,13 +1751,14 @@ module ActionView
# +options+ hash. You may pass HTML options there as well.
#
# # Let's say that @post.category returns "rails":
- # radio_button("post", "category", "rails")
- # radio_button("post", "category", "java")
+ # radio_button("category", "rails")
+ # radio_button("category", "java")
# # => <input type="radio" id="post_category_rails" name="post[category]" value="rails" checked="checked" />
# # <input type="radio" id="post_category_java" name="post[category]" value="java" />
#
- # radio_button("user", "receive_newsletter", "yes")
- # radio_button("user", "receive_newsletter", "no")
+ # # Let's say that @user.category returns "no":
+ # radio_button("receive_newsletter", "yes")
+ # radio_button("receive_newsletter", "no")
# # => <input type="radio" id="user_receive_newsletter_yes" name="user[receive_newsletter]" value="yes" />
# # <input type="radio" id="user_receive_newsletter_no" name="user[receive_newsletter]" value="no" checked="checked" />
def radio_button(method, tag_value, options = {})
@@ -1726,14 +1771,17 @@ module ActionView
# shown.
#
# ==== Examples
- # hidden_field(:signup, :pass_confirm)
- # # => <input type="hidden" id="signup_pass_confirm" name="signup[pass_confirm]" value="#{@signup.pass_confirm}" />
+ # # Let's say that @signup.pass_confirm returns true:
+ # hidden_field(:pass_confirm)
+ # # => <input type="hidden" id="signup_pass_confirm" name="signup[pass_confirm]" value="true" />
#
- # hidden_field(:post, :tag_list)
- # # => <input type="hidden" id="post_tag_list" name="post[tag_list]" value="#{@post.tag_list}" />
+ # # Let's say that @post.tag_list returns "blog, ruby":
+ # hidden_field(:tag_list)
+ # # => <input type="hidden" id="post_tag_list" name="post[tag_list]" value="blog, ruby" />
#
- # hidden_field(:user, :token)
- # # => <input type="hidden" id="user_token" name="user[token]" value="#{@user.token}" />
+ # # Let's say that @user.token returns "abcde":
+ # hidden_field(:token)
+ # # => <input type="hidden" id="user_token" name="user[token]" value="abcde" />
#
def hidden_field(method, options = {})
@emitted_hidden_id = true if method == :id
@@ -1754,19 +1802,24 @@ module ActionView
# * <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.
#
# ==== Examples
- # file_field(:user, :avatar)
+ # # Let's say that @user has avatar:
+ # file_field(:avatar)
# # => <input type="file" id="user_avatar" name="user[avatar]" />
#
- # file_field(:post, :image, :multiple => true)
- # # => <input type="file" id="post_image" name="post[image]" multiple="true" />
+ # # Let's say that @post has image:
+ # file_field(:image, :multiple => true)
+ # # => <input type="file" id="post_image" name="post[image][]" multiple="multiple" />
#
- # file_field(:post, :attached, accept: 'text/html')
+ # # Let's say that @post has attached:
+ # file_field(:attached, accept: 'text/html')
# # => <input accept="text/html" type="file" id="post_attached" name="post[attached]" />
#
- # file_field(:post, :image, accept: 'image/png,image/gif,image/jpeg')
+ # # Let's say that @post has image:
+ # file_field(:image, accept: 'image/png,image/gif,image/jpeg')
# # => <input type="file" id="post_image" name="post[image]" accept="image/png,image/gif,image/jpeg" />
#
- # file_field(:attachment, :file, class: 'file_input')
+ # # Let's say that @attachment has file:
+ # file_field(:file, class: 'file_input')
# # => <input type="file" id="attachment_file" name="attachment[file]" class="file_input" />
def file_field(method, options = {})
self.multipart = true
@@ -1834,7 +1887,7 @@ module ActionView
# create: "Add %{model}"
#
# ==== Examples
- # button("Create a post")
+ # button("Create post")
# # => <button name='button' type='submit'>Create post</button>
#
# button do
@@ -1863,8 +1916,8 @@ module ActionView
object = convert_to_model(@object)
key = object ? (object.persisted? ? :update : :create) : :submit
- model = if object.class.respond_to?(:model_name)
- object.class.model_name.human
+ model = if object.respond_to?(:model_name)
+ object.model_name.human
else
@object_name.to_s.humanize
end
@@ -1895,7 +1948,11 @@ module ActionView
explicit_child_index = options[:child_index]
output = ActiveSupport::SafeBuffer.new
association.each do |child|
- options[:child_index] = nested_child_index(name) unless explicit_child_index
+ if explicit_child_index
+ options[:child_index] = explicit_child_index.call if explicit_child_index.respond_to?(:call)
+ else
+ options[:child_index] = nested_child_index(name)
+ end
output << fields_for_nested_model("#{name}[#{options[:child_index]}]", child, options, block)
end
output
@@ -1925,6 +1982,8 @@ module ActionView
end
ActiveSupport.on_load(:action_view) do
- cattr_accessor(:default_form_builder) { ::ActionView::Helpers::FormBuilder }
+ cattr_accessor(:default_form_builder, instance_writer: false, instance_reader: false) do
+ ::ActionView::Helpers::FormBuilder
+ end
end
end
diff --git a/actionview/lib/action_view/helpers/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb
index 8ade7c6a74..430051379d 100644
--- a/actionview/lib/action_view/helpers/form_options_helper.rb
+++ b/actionview/lib/action_view/helpers/form_options_helper.rb
@@ -18,10 +18,10 @@ module ActionView
#
# could become:
#
- # <select name="post[category]">
- # <option></option>
- # <option>joke</option>
- # <option>poem</option>
+ # <select name="post[category]" id="post_category">
+ # <option value=""></option>
+ # <option value="joke">joke</option>
+ # <option value="poem">poem</option>
# </select>
#
# Another common case is a select tag for a <tt>belongs_to</tt>-associated object.
@@ -32,11 +32,11 @@ module ActionView
#
# could become:
#
- # <select name="post[person_id]">
+ # <select name="post[person_id]" id="post_person_id">
# <option value="">None</option>
# <option value="1">David</option>
- # <option value="2" selected="selected">Sam</option>
- # <option value="3">Tobias</option>
+ # <option value="2" selected="selected">Eileen</option>
+ # <option value="3">Rafael</option>
# </select>
#
# * <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.
@@ -45,11 +45,11 @@ module ActionView
#
# could become:
#
- # <select name="post[person_id]">
+ # <select name="post[person_id]" id="post_person_id">
# <option value="">Select Person</option>
# <option value="1">David</option>
- # <option value="2">Sam</option>
- # <option value="3">Tobias</option>
+ # <option value="2">Eileen</option>
+ # <option value="3">Rafael</option>
# </select>
#
# * <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
@@ -71,19 +71,19 @@ module ActionView
#
# could become:
#
- # <select name="post[category]">
- # <option></option>
- # <option>joke</option>
- # <option>poem</option>
- # <option disabled="disabled">restricted</option>
+ # <select name="post[category]" id="post_category">
+ # <option value=""></option>
+ # <option value="joke">joke</option>
+ # <option value="poem">poem</option>
+ # <option disabled="disabled" value="restricted">restricted</option>
# </select>
#
# 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.
#
- # collection_select(:post, :category_id, Category.all, :id, :name, {disabled: lambda{|category| category.archived? }})
+ # collection_select(:post, :category_id, Category.all, :id, :name, {disabled: -> (category) { category.archived? }})
#
# If the categories "2008 stuff" and "Christmas" return true when the method <tt>archived?</tt> is called, this would return:
- # <select name="post[category_id]">
+ # <select name="post[category_id]" id="post_category_id">
# <option value="1" disabled="disabled">2008 stuff</option>
# <option value="2" disabled="disabled">Christmas</option>
# <option value="3">Jokes</option>
@@ -109,11 +109,11 @@ module ActionView
#
# would become:
#
- # <select name="post[person_id]">
+ # <select name="post[person_id]" id="post_person_id">
# <option value=""></option>
# <option value="1" selected="selected">David</option>
- # <option value="2">Sam</option>
- # <option value="3">Tobias</option>
+ # <option value="2">Eileen</option>
+ # <option value="3">Rafael</option>
# </select>
#
# assuming the associated person has ID 1.
@@ -192,7 +192,7 @@ module ActionView
# collection_select(:post, :author_id, Author.all, :id, :name_with_initial, prompt: true)
#
# If <tt>@post.author_id</tt> is already <tt>1</tt>, this would return:
- # <select name="post[author_id]">
+ # <select name="post[author_id]" id="post_author_id">
# <option value="">Please select</option>
# <option value="1" selected="selected">D. Heinemeier Hansson</option>
# <option value="2">D. Thomas</option>
@@ -243,7 +243,7 @@ module ActionView
#
# Possible output:
#
- # <select name="city[country_id]">
+ # <select name="city[country_id]" id="city_country_id">
# <optgroup label="Africa">
# <option value="1">South Africa</option>
# <option value="3">Somalia</option>
@@ -302,19 +302,19 @@ module ActionView
# # => <option value="DKK">Kroner</option>
#
# options_for_select([ "VISA", "MasterCard" ], "MasterCard")
- # # => <option>VISA</option>
- # # => <option selected="selected">MasterCard</option>
+ # # => <option value="VISA">VISA</option>
+ # # => <option selected="selected" value="MasterCard">MasterCard</option>
#
# options_for_select({ "Basic" => "$20", "Plus" => "$40" }, "$40")
# # => <option value="$20">Basic</option>
# # => <option value="$40" selected="selected">Plus</option>
#
# options_for_select([ "VISA", "MasterCard", "Discover" ], ["VISA", "Discover"])
- # # => <option selected="selected">VISA</option>
- # # => <option>MasterCard</option>
- # # => <option selected="selected">Discover</option>
+ # # => <option selected="selected" value="VISA">VISA</option>
+ # # => <option value="MasterCard">MasterCard</option>
+ # # => <option selected="selected" value="Discover">Discover</option>
#
- # You can optionally provide html attributes as the last element of the array.
+ # You can optionally provide HTML attributes as the last element of the array.
#
# options_for_select([ "Denmark", ["USA", {class: 'bold'}], "Sweden" ], ["USA", "Sweden"])
# # => <option value="Denmark">Denmark</option>
@@ -351,12 +351,12 @@ module ActionView
return container if String === container
selected, disabled = extract_selected_and_disabled(selected).map do |r|
- Array(r).map { |item| item.to_s }
+ Array(r).map(&:to_s)
end
container.map do |element|
html_attributes = option_html_attributes(element)
- text, value = option_text_and_value(element).map { |item| item.to_s }
+ text, value = option_text_and_value(element).map(&:to_s)
html_attributes[:selected] ||= option_value_selected?(value, selected)
html_attributes[:disabled] ||= disabled && option_value_selected?(value, disabled)
@@ -410,7 +410,7 @@ module ActionView
# * +collection+ - An array of objects representing the <tt><optgroup></tt> tags.
# * +group_method+ - The name of a method which, when called on a member of +collection+, returns an
# array of child objects representing the <tt><option></tt> tags.
- # * group_label_method+ - The name of a method which, when called on a member of +collection+, returns a
+ # * +group_label_method+ - The name of a method which, when called on a member of +collection+, returns a
# string to be used as the +label+ attribute for its <tt><optgroup></tt> tag.
# * +option_key_method+ - The name of a method which, when called on a child object of a member of
# +collection+, returns a value to be used as the +value+ attribute for its <tt><option></tt> tag.
@@ -456,7 +456,7 @@ module ActionView
option_tags = options_from_collection_for_select(
group.send(group_method), option_key_method, option_value_method, selected_key)
- content_tag(:optgroup, option_tags, label: group.send(group_label_method))
+ content_tag("optgroup".freeze, option_tags, label: group.send(group_label_method))
end.join.html_safe
end
@@ -528,7 +528,7 @@ module ActionView
body = "".html_safe
if prompt
- body.safe_concat content_tag(:option, prompt_text(prompt), value: "")
+ body.safe_concat content_tag("option".freeze, prompt_text(prompt), value: "")
end
grouped_options.each do |container|
@@ -541,14 +541,14 @@ module ActionView
end
html_attributes = { label: label }.merge!(html_attributes)
- body.safe_concat content_tag(:optgroup, options_for_select(container, selected_key), html_attributes)
+ body.safe_concat content_tag("optgroup".freeze, options_for_select(container, selected_key), html_attributes)
end
body
end
# Returns a string of option tags for pretty much any time zone in the
- # world. Supply a ActiveSupport::TimeZone name as +selected+ to have it
+ # world. Supply an ActiveSupport::TimeZone name as +selected+ to have it
# marked as the selected option tag. You can also supply an array of
# ActiveSupport::TimeZone objects as +priority_zones+, so that they will
# be listed above the rest of the (long) list. (You can use
@@ -556,7 +556,7 @@ module ActionView
# of the US time zones, or a Regexp to select the zones of your choice)
#
# The +selected+ parameter must be either +nil+, or a string that names
- # a ActiveSupport::TimeZone.
+ # an ActiveSupport::TimeZone.
#
# By default, +model+ is the ActiveSupport::TimeZone constant (which can
# be obtained in Active Record as a value object). The only requirement
@@ -577,7 +577,7 @@ module ActionView
end
zone_options.safe_concat options_for_select(convert_zones[priority_zones], selected)
- zone_options.safe_concat content_tag(:option, '-------------', value: '', disabled: true)
+ zone_options.safe_concat content_tag("option".freeze, '-------------', value: '', disabled: true)
zone_options.safe_concat "\n"
zones = zones - priority_zones
@@ -633,7 +633,7 @@ module ActionView
# even use the label as wrapper, as in the example above.
#
# The builder methods <tt>label</tt> and <tt>radio_button</tt> also accept
- # extra html options:
+ # extra HTML options:
# collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) do |b|
# b.label(class: "radio_button") { b.radio_button(class: "radio_button") }
# end
@@ -644,6 +644,24 @@ module ActionView
# collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) do |b|
# b.label(:"data-value" => b.value) { b.radio_button + b.text }
# end
+ #
+ # ==== Gotcha
+ #
+ # The HTML specification says when nothing is select on a collection of radio buttons
+ # web browsers do not send any value to server.
+ # Unfortunately this introduces a gotcha:
+ # if a +User+ model has a +category_id+ field, and in the form none category is selected no +category_id+ parameter is sent. So,
+ # any strong parameters idiom like
+ #
+ # params.require(:user).permit(...)
+ #
+ # will raise an error since no +{user: ...}+ will be present.
+ #
+ # To prevent this the helper generates an auxiliary hidden field before
+ # every collection of radio buttons. The hidden field has the same name as collection radio button and blank value.
+ #
+ # In case if you don't want the helper to generate this hidden field you can specify
+ # <tt>include_hidden: false</tt> option.
def collection_radio_buttons(object, method, collection, value_method, text_method, options = {}, html_options = {}, &block)
Tags::CollectionRadioButtons.new(object, method, self, collection, value_method, text_method, options, html_options).render(&block)
end
@@ -696,7 +714,7 @@ module ActionView
# use the label as wrapper, as in the example above.
#
# The builder methods <tt>label</tt> and <tt>check_box</tt> also accept
- # extra html options:
+ # extra HTML options:
# collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) do |b|
# b.label(class: "check_box") { b.check_box(class: "check_box") }
# end
@@ -707,6 +725,27 @@ module ActionView
# collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) do |b|
# b.label(:"data-value" => b.value) { b.check_box + b.text }
# end
+ #
+ # ==== Gotcha
+ #
+ # When no selection is made for a collection of checkboxes most
+ # web browsers will not send any value.
+ #
+ # For example, if we have a +User+ model with +category_ids+ field and we
+ # have the following code in our update action:
+ #
+ # @user.update(params[:user])
+ #
+ # If no +category_ids+ are selected then we can safely assume this field
+ # will not be updated.
+ #
+ # This is possible thanks to a hidden field generated by the helper method
+ # for every collection of checkboxes.
+ # This hidden field is given the same field name as the checkboxes with a
+ # blank value.
+ #
+ # In the rare case you don't want this hidden field, you can pass the
+ # <tt>include_hidden: false</tt> option to the helper method.
def collection_check_boxes(object, method, collection, value_method, text_method, options = {}, html_options = {}, &block)
Tags::CollectionCheckBoxes.new(object, method, self, collection, value_method, text_method, options, html_options).render(&block)
end
diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb
index b18f578183..0191064326 100644
--- a/actionview/lib/action_view/helpers/form_tag_helper.rb
+++ b/actionview/lib/action_view/helpers/form_tag_helper.rb
@@ -20,7 +20,7 @@ module ActionView
mattr_accessor :embed_authenticity_token_in_remote_forms
self.embed_authenticity_token_in_remote_forms = false
- # Starts a form tag that points the action to an url configured with <tt>url_for_options</tt> just like
+ # Starts a form tag that points the action to a url configured with <tt>url_for_options</tt> just like
# ActionController::Base#url_for. The method for the form defaults to POST.
#
# ==== Options
@@ -35,10 +35,10 @@ module ActionView
# This is helpful when you're fragment-caching the form. Remote forms get the
# authenticity token from the <tt>meta</tt> tag, so embedding is unnecessary unless you
# support browsers without JavaScript.
- # * A list of parameters to feed to the URL the form will be posted to.
# * <tt>:remote</tt> - If set to true, will allow the Unobtrusive JavaScript drivers to control the
# submit behavior. By default this behavior is an ajax submit.
# * <tt>:enforce_utf8</tt> - If set to false, a hidden input with name utf8 is not output.
+ # * Any other key creates standard HTML attributes for the tag.
#
# ==== Examples
# form_tag('/posts')
@@ -80,18 +80,17 @@ module ActionView
# associated records. <tt>option_tags</tt> is a string containing the option tags for the select box.
#
# ==== Options
- # * <tt>:multiple</tt> - If set to true the selection will allow multiple choices.
+ # * <tt>:multiple</tt> - If set to true, the selection will allow multiple choices.
# * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
# * <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.
# * <tt>:prompt</tt> - Create a prompt option with blank value and the text asking user to select something.
- # * <tt>:selected</tt> - Provide a default selected value. It should be of the exact type as the provided options.
# * Any other key creates standard HTML attributes for the tag.
#
# ==== Examples
# select_tag "people", options_from_collection_for_select(@people, "id", "name")
# # <select id="people" name="people"><option value="1">David</option></select>
#
- # select_tag "people", options_from_collection_for_select(@people, "id", "name"), selected: ["1", "David"]
+ # select_tag "people", options_from_collection_for_select(@people, "id", "name", "1")
# # <select id="people" name="people"><option value="1" selected="selected">David</option></select>
#
# select_tag "people", "<option>David</option>".html_safe
@@ -133,15 +132,23 @@ module ActionView
option_tags ||= ""
html_name = (options[:multiple] == true && !name.to_s.ends_with?("[]")) ? "#{name}[]" : name
- if options.delete(:include_blank)
- option_tags = content_tag(:option, '', :value => '').safe_concat(option_tags)
+ if options.include?(:include_blank)
+ include_blank = options.delete(:include_blank)
+
+ if include_blank == true
+ include_blank = ''
+ end
+
+ if include_blank
+ option_tags = content_tag("option".freeze, include_blank, value: '').safe_concat(option_tags)
+ end
end
if prompt = options.delete(:prompt)
- option_tags = content_tag(:option, prompt, :value => '').safe_concat(option_tags)
+ option_tags = content_tag("option".freeze, prompt, value: '').safe_concat(option_tags)
end
- content_tag :select, option_tags, { "name" => html_name, "id" => sanitize_to_id(name) }.update(options.stringify_keys)
+ content_tag "select".freeze, option_tags, { "name" => html_name, "id" => sanitize_to_id(name) }.update(options.stringify_keys)
end
# Creates a standard text field; use these text fields to input smaller chunks of text like a username
@@ -224,7 +231,7 @@ module ActionView
# # => <input id="collected_input" name="collected_input" onchange="alert('Input collected!')"
# # type="hidden" value="" />
def hidden_field_tag(name, value = nil, options = {})
- text_field_tag(name, value, options.stringify_keys.update("type" => "hidden"))
+ text_field_tag(name, value, options.merge(type: :hidden))
end
# Creates a file upload field. If you are using file uploads then you will also need
@@ -263,7 +270,7 @@ module ActionView
# file_field_tag 'file', accept: 'text/html', class: 'upload', value: 'index.html'
# # => <input accept="text/html" class="upload" id="file" name="file" type="file" value="index.html" />
def file_field_tag(name, options = {})
- text_field_tag(name, nil, options.update("type" => "file"))
+ text_field_tag(name, nil, options.merge(type: :file))
end
# Creates a password field, a masked text field that will hide the users input behind a mask character.
@@ -296,7 +303,7 @@ module ActionView
# password_field_tag 'pin', '1234', maxlength: 4, size: 6, class: "pin_input"
# # => <input class="pin_input" id="pin" maxlength="4" name="pin" size="6" type="password" value="1234" />
def password_field_tag(name = "password", value = nil, options = {})
- text_field_tag(name, value, options.update("type" => "password"))
+ text_field_tag(name, value, options.merge(type: :password))
end
# Creates a text input area; use a textarea for longer text inputs such as blog posts or descriptions.
@@ -407,42 +414,57 @@ module ActionView
# the form is processed normally, otherwise no action is taken.
# * <tt>:disable_with</tt> - Value of this parameter will be used as the value for a
# disabled version of the submit button when the form is submitted. This feature is
- # provided by the unobtrusive JavaScript driver.
+ # provided by the unobtrusive JavaScript driver. To disable this feature for a single submit tag
+ # pass <tt>:data => { disable_with: false }</tt> Defaults to value attribute.
#
# ==== Examples
# submit_tag
- # # => <input name="commit" type="submit" value="Save changes" />
+ # # => <input name="commit" data-disable-with="Save changes" type="submit" value="Save changes" />
#
# submit_tag "Edit this article"
- # # => <input name="commit" type="submit" value="Edit this article" />
+ # # => <input name="commit" data-disable-with="Edit this article" type="submit" value="Edit this article" />
#
# submit_tag "Save edits", disabled: true
- # # => <input disabled="disabled" name="commit" type="submit" value="Save edits" />
+ # # => <input disabled="disabled" name="commit" data-disable-with="Save edits" type="submit" value="Save edits" />
#
- # submit_tag "Complete sale", data: { disable_with: "Please wait..." }
- # # => <input name="commit" data-disable-with="Please wait..." type="submit" value="Complete sale" />
+ # submit_tag "Complete sale", data: { disable_with: "Submitting..." }
+ # # => <input name="commit" data-disable-with="Submitting..." type="submit" value="Complete sale" />
#
# submit_tag nil, class: "form_submit"
# # => <input class="form_submit" name="commit" type="submit" />
#
# submit_tag "Edit", class: "edit_button"
- # # => <input class="edit_button" name="commit" type="submit" value="Edit" />
+ # # => <input class="edit_button" data-disable-with="Edit" name="commit" type="submit" value="Edit" />
#
# submit_tag "Save", data: { confirm: "Are you sure?" }
- # # => <input name='commit' type='submit' value='Save' data-confirm="Are you sure?" />
+ # # => <input name='commit' type='submit' value='Save' data-disable-with="Save" data-confirm="Are you sure?" />
#
def submit_tag(value = "Save changes", options = {})
options = options.stringify_keys
+ tag_options = { "type" => "submit", "name" => "commit", "value" => value }.update(options)
+
+ if ActionView::Base.automatically_disable_submit_tag
+ unless tag_options["data-disable-with"] == false || (tag_options["data"] && tag_options["data"][:disable_with] == false)
+ disable_with_text = tag_options["data-disable-with"]
+ disable_with_text ||= tag_options["data"][:disable_with] if tag_options["data"]
+ disable_with_text ||= value.clone
+ tag_options.deep_merge!("data" => { "disable_with" => disable_with_text })
+ else
+ tag_options["data"].delete(:disable_with) if tag_options["data"]
+ end
+ tag_options.delete("data-disable-with")
+ end
- tag :input, { "type" => "submit", "name" => "commit", "value" => value }.update(options)
+ tag :input, tag_options
end
# Creates a button element that defines a <tt>submit</tt> button,
# <tt>reset</tt>button or a generic button which can be used in
# JavaScript, for example. You can use the button tag as a regular
# submit tag but it isn't supported in legacy browsers. However,
- # the button tag allows richer labels such as images and emphasis,
- # so this helper will also accept a block.
+ # the button tag does allow for richer labels such as images and emphasis,
+ # so this helper will also accept a block. By default, it will create
+ # a button tag with type `submit`, if type is not given.
#
# ==== Options
# * <tt>:data</tt> - This option can be used to add custom data attributes.
@@ -465,6 +487,15 @@ module ActionView
# button_tag
# # => <button name="button" type="submit">Button</button>
#
+ # button_tag 'Reset', type: 'reset'
+ # # => <button name="button" type="reset">Reset</button>
+ #
+ # button_tag 'Button', type: 'button'
+ # # => <button name="button" type="button">Button</button>
+ #
+ # button_tag 'Reset', type: 'reset', disabled: true
+ # # => <button name="button" type="reset" disabled="disabled">Reset</button>
+ #
# button_tag(type: 'button') do
# content_tag(:strong, 'Ask me!')
# end
@@ -472,6 +503,9 @@ module ActionView
# # <strong>Ask me!</strong>
# # </button>
#
+ # button_tag "Save", data: { confirm: "Are you sure?" }
+ # # => <button name="button" type="submit" data-confirm="Are you sure?">Save</button>
+ #
# button_tag "Checkout", data: { disable_with: "Please wait..." }
# # => <button data-disable-with="Please wait..." name="button" type="submit">Checkout</button>
#
@@ -548,7 +582,7 @@ module ActionView
# # => <fieldset class="format"><p><input id="name" name="name" type="text" /></p></fieldset>
def field_set_tag(legend = nil, options = nil, &block)
output = tag(:fieldset, options, true)
- output.safe_concat(content_tag(:legend, legend)) unless legend.blank?
+ output.safe_concat(content_tag("legend".freeze, legend)) unless legend.blank?
output.concat(capture(&block)) if block_given?
output.safe_concat("</fieldset>")
end
@@ -571,7 +605,7 @@ module ActionView
# color_field_tag 'color', '#DEF726', class: 'special_input', disabled: true
# # => <input disabled="disabled" class="special_input" id="color" name="color" type="color" value="#DEF726" />
def color_field_tag(name, value = nil, options = {})
- text_field_tag(name, value, options.stringify_keys.update("type" => "color"))
+ text_field_tag(name, value, options.merge(type: :color))
end
# Creates a text field of type "search".
@@ -592,7 +626,7 @@ module ActionView
# search_field_tag 'search', 'Enter your search query here', class: 'special_input', disabled: true
# # => <input disabled="disabled" class="special_input" id="search" name="search" type="search" value="Enter your search query here" />
def search_field_tag(name, value = nil, options = {})
- text_field_tag(name, value, options.stringify_keys.update("type" => "search"))
+ text_field_tag(name, value, options.merge(type: :search))
end
# Creates a text field of type "tel".
@@ -613,7 +647,7 @@ module ActionView
# telephone_field_tag 'tel', '0123456789', class: 'special_input', disabled: true
# # => <input disabled="disabled" class="special_input" id="tel" name="tel" type="tel" value="0123456789" />
def telephone_field_tag(name, value = nil, options = {})
- text_field_tag(name, value, options.stringify_keys.update("type" => "tel"))
+ text_field_tag(name, value, options.merge(type: :tel))
end
alias phone_field_tag telephone_field_tag
@@ -635,7 +669,7 @@ module ActionView
# date_field_tag 'date', '01/01/2014', class: 'special_input', disabled: true
# # => <input disabled="disabled" class="special_input" id="date" name="date" type="date" value="01/01/2014" />
def date_field_tag(name, value = nil, options = {})
- text_field_tag(name, value, options.stringify_keys.update("type" => "date"))
+ text_field_tag(name, value, options.merge(type: :date))
end
# Creates a text field of type "time".
@@ -646,7 +680,7 @@ module ActionView
# * <tt>:step</tt> - The acceptable value granularity.
# * Otherwise accepts the same options as text_field_tag.
def time_field_tag(name, value = nil, options = {})
- text_field_tag(name, value, options.stringify_keys.update("type" => "time"))
+ text_field_tag(name, value, options.merge(type: :time))
end
# Creates a text field of type "datetime".
@@ -657,7 +691,7 @@ module ActionView
# * <tt>:step</tt> - The acceptable value granularity.
# * Otherwise accepts the same options as text_field_tag.
def datetime_field_tag(name, value = nil, options = {})
- text_field_tag(name, value, options.stringify_keys.update("type" => "datetime"))
+ text_field_tag(name, value, options.merge(type: :datetime))
end
# Creates a text field of type "datetime-local".
@@ -668,7 +702,7 @@ module ActionView
# * <tt>:step</tt> - The acceptable value granularity.
# * Otherwise accepts the same options as text_field_tag.
def datetime_local_field_tag(name, value = nil, options = {})
- text_field_tag(name, value, options.stringify_keys.update("type" => "datetime-local"))
+ text_field_tag(name, value, options.merge(type: 'datetime-local'))
end
# Creates a text field of type "month".
@@ -679,7 +713,7 @@ module ActionView
# * <tt>:step</tt> - The acceptable value granularity.
# * Otherwise accepts the same options as text_field_tag.
def month_field_tag(name, value = nil, options = {})
- text_field_tag(name, value, options.stringify_keys.update("type" => "month"))
+ text_field_tag(name, value, options.merge(type: :month))
end
# Creates a text field of type "week".
@@ -690,7 +724,7 @@ module ActionView
# * <tt>:step</tt> - The acceptable value granularity.
# * Otherwise accepts the same options as text_field_tag.
def week_field_tag(name, value = nil, options = {})
- text_field_tag(name, value, options.stringify_keys.update("type" => "week"))
+ text_field_tag(name, value, options.merge(type: :week))
end
# Creates a text field of type "url".
@@ -711,7 +745,7 @@ module ActionView
# url_field_tag 'url', 'http://rubyonrails.org', class: 'special_input', disabled: true
# # => <input disabled="disabled" class="special_input" id="url" name="url" type="url" value="http://rubyonrails.org" />
def url_field_tag(name, value = nil, options = {})
- text_field_tag(name, value, options.stringify_keys.update("type" => "url"))
+ text_field_tag(name, value, options.merge(type: :url))
end
# Creates a text field of type "email".
@@ -732,7 +766,7 @@ module ActionView
# email_field_tag 'email', 'email@example.com', class: 'special_input', disabled: true
# # => <input disabled="disabled" class="special_input" id="email" name="email" type="email" value="email@example.com" />
def email_field_tag(name, value = nil, options = {})
- text_field_tag(name, value, options.stringify_keys.update("type" => "email"))
+ text_field_tag(name, value, options.merge(type: :email))
end
# Creates a number field.
@@ -769,10 +803,10 @@ module ActionView
# # => <input id="quantity" name="quantity" min="1" max="9" type="number" />
#
# number_field_tag 'quantity', nil, min: 1, max: 10
- # # => <input id="quantity" name="quantity" min="1" max="9" type="number" />
+ # # => <input id="quantity" name="quantity" min="1" max="10" type="number" />
#
# number_field_tag 'quantity', nil, min: 1, max: 10, step: 2
- # # => <input id="quantity" name="quantity" min="1" max="9" step="2" type="number" />
+ # # => <input id="quantity" name="quantity" min="1" max="10" step="2" type="number" />
#
# number_field_tag 'quantity', '1', class: 'special_input', disabled: true
# # => <input disabled="disabled" class="special_input" id="quantity" name="quantity" type="number" value="1" />
@@ -790,7 +824,7 @@ module ActionView
# ==== Options
# * Accepts the same options as number_field_tag.
def range_field_tag(name, value = nil, options = {})
- number_field_tag(name, value, options.stringify_keys.update("type" => "range"))
+ number_field_tag(name, value, options.merge(type: :range))
end
# Creates the hidden UTF8 enforcer tag. Override this method in a helper
@@ -862,7 +896,7 @@ module ActionView
# see http://www.w3.org/TR/html4/types.html#type-name
def sanitize_to_id(name)
- name.to_s.delete(']').gsub(/[^-a-zA-Z0-9:.]/, "_")
+ name.to_s.delete(']').tr('^-a-zA-Z0-9:.', "_")
end
end
end
diff --git a/actionview/lib/action_view/helpers/javascript_helper.rb b/actionview/lib/action_view/helpers/javascript_helper.rb
index 629c447f3f..ed7e882c94 100644
--- a/actionview/lib/action_view/helpers/javascript_helper.rb
+++ b/actionview/lib/action_view/helpers/javascript_helper.rb
@@ -21,7 +21,7 @@ module ActionView
# Also available through the alias j(). This is particularly helpful in JavaScript
# responses, like:
#
- # $('some_element').replaceWith('<%=j render 'some/element_template' %>');
+ # $('some_element').replaceWith('<%= j render 'some/element_template' %>');
def escape_javascript(javascript)
if javascript
result = javascript.gsub(/(\\|<\/|\r\n|\342\200\250|\342\200\251|[\n\r"'])/u) {|match| JS_ESCAPE_MAP[match] }
@@ -47,8 +47,8 @@ module ActionView
# tag.
#
# javascript_tag "alert('All is good')", defer: 'defer'
- #
- # Returns:
+ #
+ # Returns:
# <script defer="defer">
# //<![CDATA[
# alert('All is good')
@@ -70,7 +70,7 @@ module ActionView
content_or_options_with_block
end
- content_tag(:script, javascript_cdata_section(content), html_options)
+ content_tag("script".freeze, javascript_cdata_section(content), html_options)
end
def javascript_cdata_section(content) #:nodoc:
diff --git a/actionview/lib/action_view/helpers/number_helper.rb b/actionview/lib/action_view/helpers/number_helper.rb
index 7220bded3c..d7182d1fac 100644
--- a/actionview/lib/action_view/helpers/number_helper.rb
+++ b/actionview/lib/action_view/helpers/number_helper.rb
@@ -1,5 +1,3 @@
-# encoding: utf-8
-
require 'active_support/core_ext/hash/keys'
require 'active_support/core_ext/string/output_safety'
require 'active_support/number_helper'
@@ -117,8 +115,8 @@ module ActionView
# (defaults to current locale).
# * <tt>:precision</tt> - Sets the precision of the number
# (defaults to 3).
- # * <tt>:significant</tt> - If +true+, precision will be the #
- # of significant_digits. If +false+, the # of fractional
+ # * <tt>:significant</tt> - If +true+, precision will be the number
+ # of significant_digits. If +false+, the number of fractional
# digits (defaults to +false+).
# * <tt>:separator</tt> - Sets the separator between the
# fractional and integer digits (defaults to ".").
@@ -141,7 +139,7 @@ module ActionView
# number_to_percentage(302.24398923423, precision: 5) # => 302.24399%
# number_to_percentage(1000, locale: :fr) # => 1 000,000%
# number_to_percentage("98a") # => 98a%
- # number_to_percentage(100, format: "%n %") # => 100 %
+ # number_to_percentage(100, format: "%n %") # => 100.000 %
#
# number_to_percentage("98a", raise: true) # => InvalidNumberError
def number_to_percentage(number, options = {})
@@ -192,8 +190,8 @@ module ActionView
# (defaults to current locale).
# * <tt>:precision</tt> - Sets the precision of the number
# (defaults to 3).
- # * <tt>:significant</tt> - If +true+, precision will be the #
- # of significant_digits. If +false+, the # of fractional
+ # * <tt>:significant</tt> - If +true+, precision will be the number
+ # of significant_digits. If +false+, the number of fractional
# digits (defaults to +false+).
# * <tt>:separator</tt> - Sets the separator between the
# fractional and integer digits (defaults to ".").
@@ -240,8 +238,8 @@ module ActionView
# (defaults to current locale).
# * <tt>:precision</tt> - Sets the precision of the number
# (defaults to 3).
- # * <tt>:significant</tt> - If +true+, precision will be the #
- # of significant_digits. If +false+, the # of fractional
+ # * <tt>:significant</tt> - If +true+, precision will be the number
+ # of significant_digits. If +false+, the number of fractional
# digits (defaults to +true+)
# * <tt>:separator</tt> - Sets the separator between the
# fractional and integer digits (defaults to ".").
@@ -280,7 +278,7 @@ module ActionView
# See <tt>number_to_human_size</tt> if you want to print a file
# size.
#
- # You can also define you own unit-quantifier names if you want
+ # You can also define your own unit-quantifier names if you want
# to use other decimal units (eg.: 1500 becomes "1.5
# kilometers", 0.150 becomes "150 milliliters", etc). You may
# define a wide range of unit quantifiers, even fractional ones
@@ -292,8 +290,8 @@ module ActionView
# (defaults to current locale).
# * <tt>:precision</tt> - Sets the precision of the number
# (defaults to 3).
- # * <tt>:significant</tt> - If +true+, precision will be the #
- # of significant_digits. If +false+, the # of fractional
+ # * <tt>:significant</tt> - If +true+, precision will be the number
+ # of significant_digits. If +false+, the number of fractional
# digits (defaults to +true+)
# * <tt>:separator</tt> - Sets the separator between the
# fractional and integer digits (defaults to ".").
@@ -306,12 +304,12 @@ module ActionView
# string containing an i18n scope where to find this hash. It
# might have the following keys:
# * *integers*: <tt>:unit</tt>, <tt>:ten</tt>,
- # *<tt>:hundred</tt>, <tt>:thousand</tt>, <tt>:million</tt>,
- # *<tt>:billion</tt>, <tt>:trillion</tt>,
- # *<tt>:quadrillion</tt>
+ # <tt>:hundred</tt>, <tt>:thousand</tt>, <tt>:million</tt>,
+ # <tt>:billion</tt>, <tt>:trillion</tt>,
+ # <tt>:quadrillion</tt>
# * *fractionals*: <tt>:deci</tt>, <tt>:centi</tt>,
- # *<tt>:mili</tt>, <tt>:micro</tt>, <tt>:nano</tt>,
- # *<tt>:pico</tt>, <tt>:femto</tt>
+ # <tt>:mili</tt>, <tt>:micro</tt>, <tt>:nano</tt>,
+ # <tt>:pico</tt>, <tt>:femto</tt>
# * <tt>:format</tt> - Sets the format of the output string
# (defaults to "%n %u"). The field types are:
# * %u - The quantifier (ex.: 'thousand')
diff --git a/actionview/lib/action_view/helpers/output_safety_helper.rb b/actionview/lib/action_view/helpers/output_safety_helper.rb
index f03362d0f5..1c2a400245 100644
--- a/actionview/lib/action_view/helpers/output_safety_helper.rb
+++ b/actionview/lib/action_view/helpers/output_safety_helper.rb
@@ -17,10 +17,10 @@ module ActionView #:nodoc:
stringish.to_s.html_safe
end
- # This method returns an html safe string similar to what <tt>Array#join</tt>
+ # This method returns an HTML safe string similar to what <tt>Array#join</tt>
# would return. The array is flattened, and all items, including
- # the supplied separator, are html escaped unless they are html
- # safe, and the returned string is marked as html safe.
+ # the supplied separator, are HTML escaped unless they are HTML
+ # safe, and the returned string is marked as HTML safe.
#
# safe_join(["<p>foo</p>".html_safe, "<p>bar</p>"], "<br />")
# # => "<p>foo</p>&lt;br /&gt;&lt;p&gt;bar&lt;/p&gt;"
diff --git a/actionview/lib/action_view/helpers/record_tag_helper.rb b/actionview/lib/action_view/helpers/record_tag_helper.rb
index 77c3e6d394..f7ee573035 100644
--- a/actionview/lib/action_view/helpers/record_tag_helper.rb
+++ b/actionview/lib/action_view/helpers/record_tag_helper.rb
@@ -1,108 +1,21 @@
-require 'action_view/record_identifier'
-
module ActionView
- # = Action View Record Tag Helpers
module Helpers
module RecordTagHelper
- include ActionView::RecordIdentifier
-
- # Produces a wrapper DIV element with id and class parameters that
- # relate to the specified Active Record object. Usage example:
- #
- # <%= div_for(@person, class: "foo") do %>
- # <%= @person.name %>
- # <% end %>
- #
- # produces:
- #
- # <div id="person_123" class="person foo"> Joe Bloggs </div>
- #
- # You can also pass an array of Active Record objects, which will then
- # get iterated over and yield each record as an argument for the block.
- # For example:
- #
- # <%= div_for(@people, class: "foo") do |person| %>
- # <%= person.name %>
- # <% end %>
- #
- # produces:
- #
- # <div id="person_123" class="person foo"> Joe Bloggs </div>
- # <div id="person_124" class="person foo"> Jane Bloggs </div>
- #
- def div_for(record, *args, &block)
- content_tag_for(:div, record, *args, &block)
+ def div_for(*)
+ raise NoMethodError, "The `div_for` method has been removed from " \
+ "Rails. To continue using it, add the `record_tag_helper` gem to " \
+ "your Gemfile:\n" \
+ " gem 'record_tag_helper', '~> 1.0'\n" \
+ "Consult the Rails upgrade guide for details."
end
- # content_tag_for creates an HTML element with id and class parameters
- # that relate to the specified Active Record object. For example:
- #
- # <%= content_tag_for(:tr, @person) do %>
- # <td><%= @person.first_name %></td>
- # <td><%= @person.last_name %></td>
- # <% end %>
- #
- # would produce the following HTML (assuming @person is an instance of
- # a Person object, with an id value of 123):
- #
- # <tr id="person_123" class="person">....</tr>
- #
- # If you require the HTML id attribute to have a prefix, you can specify it:
- #
- # <%= content_tag_for(:tr, @person, :foo) do %> ...
- #
- # produces:
- #
- # <tr id="foo_person_123" class="person">...
- #
- # You can also pass an array of objects which this method will loop through
- # and yield the current object to the supplied block, reducing the need for
- # having to iterate through the object (using <tt>each</tt>) beforehand.
- # For example (assuming @people is an array of Person objects):
- #
- # <%= content_tag_for(:tr, @people) do |person| %>
- # <td><%= person.first_name %></td>
- # <td><%= person.last_name %></td>
- # <% end %>
- #
- # produces:
- #
- # <tr id="person_123" class="person">...</tr>
- # <tr id="person_124" class="person">...</tr>
- #
- # content_tag_for also accepts a hash of options, which will be converted to
- # additional HTML attributes. If you specify a <tt>:class</tt> value, it will be combined
- # with the default class name for your object. For example:
- #
- # <%= content_tag_for(:li, @person, class: "bar") %>...
- #
- # produces:
- #
- # <li id="person_123" class="person bar">...
- #
- def content_tag_for(tag_name, single_or_multiple_records, prefix = nil, options = nil, &block)
- options, prefix = prefix, nil if prefix.is_a?(Hash)
-
- Array(single_or_multiple_records).map do |single_record|
- content_tag_for_single_record(tag_name, single_record, prefix, options, &block)
- end.join("\n").html_safe
+ def content_tag_for(*)
+ raise NoMethodError, "The `content_tag_for` method has been removed from " \
+ "Rails. To continue using it, add the `record_tag_helper` gem to " \
+ "your Gemfile:\n" \
+ " gem 'record_tag_helper', '~> 1.0'\n" \
+ "Consult the Rails upgrade guide for details."
end
-
- private
-
- # Called by <tt>content_tag_for</tt> internally to render a content tag
- # for each record.
- def content_tag_for_single_record(tag_name, record, prefix, options, &block)
- options = options ? options.dup : {}
- options[:class] = [ dom_class(record, prefix), options[:class] ].compact
- options[:id] = dom_id(record, prefix)
-
- if block_given?
- content_tag(tag_name, capture(record, &block), options)
- else
- content_tag(tag_name, "", options)
- end
- end
end
end
end
diff --git a/actionview/lib/action_view/helpers/rendering_helper.rb b/actionview/lib/action_view/helpers/rendering_helper.rb
index 6cd6e858dd..c98f2d74a8 100644
--- a/actionview/lib/action_view/helpers/rendering_helper.rb
+++ b/actionview/lib/action_view/helpers/rendering_helper.rb
@@ -14,11 +14,11 @@ module ActionView
# * <tt>:text</tt> - Renders the text passed in out.
# * <tt>:plain</tt> - Renders the text passed in out. Setting the content
# type as <tt>text/plain</tt>.
- # * <tt>:html</tt> - Renders the html safe string passed in out, otherwise
- # performs html escape on the string first. Setting the content type as
+ # * <tt>:html</tt> - Renders the HTML safe string passed in out, otherwise
+ # performs HTML escape on the string first. Setting the content type as
# <tt>text/html</tt>.
# * <tt>:body</tt> - Renders the text passed in, and inherits the content
- # type of <tt>text/html</tt> from <tt>ActionDispatch::Response</tt>
+ # type of <tt>text/plain</tt> from <tt>ActionDispatch::Response</tt>
# object.
#
# If no options hash is passed or :update specified, the default is to render a partial and use the second parameter
@@ -32,7 +32,7 @@ module ActionView
view_renderer.render(self, options)
end
else
- view_renderer.render_partial(self, :partial => options, :locals => locals)
+ view_renderer.render_partial(self, :partial => options, :locals => locals, &block)
end
end
diff --git a/actionview/lib/action_view/helpers/sanitize_helper.rb b/actionview/lib/action_view/helpers/sanitize_helper.rb
index 049af275b6..191a881de0 100644
--- a/actionview/lib/action_view/helpers/sanitize_helper.rb
+++ b/actionview/lib/action_view/helpers/sanitize_helper.rb
@@ -1,5 +1,5 @@
require 'active_support/core_ext/object/try'
-require 'action_view/vendor/html-scanner'
+require 'rails-html-sanitizer'
module ActionView
# = Action View Sanitize Helpers
@@ -8,54 +8,77 @@ module ActionView
# These helper methods extend Action View making them callable within your template files.
module SanitizeHelper
extend ActiveSupport::Concern
- # This +sanitize+ helper will html encode all tags and strip all attributes that
- # aren't specifically allowed.
+ # Sanitizes HTML input, stripping all tags and attributes that aren't whitelisted.
#
- # It also strips href/src tags with invalid protocols, like javascript: especially.
- # It does its best to counter any tricks that hackers may use, like throwing in
- # unicode/ascii/hex values to get past the javascript: filters. Check out
- # the extensive test suite.
+ # It also strips href/src attributes with unsafe protocols like
+ # <tt>javascript:</tt>, while also protecting against attempts to use Unicode,
+ # ASCII, and hex character references to work around these protocol filters.
#
- # <%= sanitize @article.body %>
+ # The default sanitizer is Rails::Html::WhiteListSanitizer. See {Rails HTML
+ # Sanitizers}[https://github.com/rails/rails-html-sanitizer] for more information.
#
- # You can add or remove tags/attributes if you want to customize it a bit.
- # See ActionView::Base for full docs on the available options. You can add
- # tags/attributes for single uses of +sanitize+ by passing either the
- # <tt>:attributes</tt> or <tt>:tags</tt> options:
+ # Custom sanitization rules can also be provided.
#
- # Normal Use
+ # Please note that sanitizing user-provided text does not guarantee that the
+ # resulting markup is valid or even well-formed. For example, the output may still
+ # contain unescaped characters like <tt><</tt>, <tt>></tt>, or <tt>&</tt>.
#
- # <%= sanitize @article.body %>
+ # ==== Options
#
- # Custom Use (only the mentioned tags and attributes are allowed, nothing else)
+ # * <tt>:tags</tt> - An array of allowed tags.
+ # * <tt>:attributes</tt> - An array of allowed attributes.
+ # * <tt>:scrubber</tt> - A {Rails::Html scrubber}[https://github.com/rails/rails-html-sanitizer]
+ # or {Loofah::Scrubber}[https://github.com/flavorjones/loofah] object that
+ # defines custom sanitization rules. A custom scrubber takes precedence over
+ # custom tags and attributes.
#
- # <%= sanitize @article.body, tags: %w(table tr td), attributes: %w(id class style) %>
+ # ==== Examples
#
- # Add table tags to the default allowed tags
+ # Normal use:
#
- # class Application < Rails::Application
- # config.action_view.sanitized_allowed_tags = 'table', 'tr', 'td'
- # end
+ # <%= sanitize @comment.body %>
+ #
+ # Providing custom whitelisted tags and attributes:
+ #
+ # <%= sanitize @comment.body, tags: %w(strong em a), attributes: %w(href) %>
+ #
+ # Providing a custom Rails::Html scrubber:
#
- # Remove tags to the default allowed tags
+ # class CommentScrubber < Rails::Html::PermitScrubber
+ # def allowed_node?(node)
+ # !%w(form script comment blockquote).include?(node.name)
+ # end
+ #
+ # def skip_node?(node)
+ # node.text?
+ # end
#
- # class Application < Rails::Application
- # config.after_initialize do
- # ActionView::Base.sanitized_allowed_tags.delete 'div'
+ # def scrub_attribute?(name)
+ # name == 'style'
# end
# end
#
- # Change allowed default attributes
+ # <%= sanitize @comment.body, scrubber: CommentScrubber.new %>
+ #
+ # See {Rails HTML Sanitizer}[https://github.com/rails/rails-html-sanitizer] for
+ # documentation about Rails::Html scrubbers.
+ #
+ # Providing a custom Loofah::Scrubber:
#
- # class Application < Rails::Application
- # config.action_view.sanitized_allowed_attributes = ['id', 'class', 'style']
+ # scrubber = Loofah::Scrubber.new do |node|
+ # node.remove if node.name == 'script'
# end
#
- # Please note that sanitizing user-provided text does not guarantee that the
- # resulting markup is valid (conforming to a document type) or even well-formed.
- # The output may still contain e.g. unescaped '<', '>', '&' characters and
- # confuse browsers.
+ # <%= sanitize @comment.body, scrubber: scrubber %>
+ #
+ # See {Loofah's documentation}[https://github.com/flavorjones/loofah] for more
+ # information about defining custom Loofah::Scrubber objects.
+ #
+ # To set the default allowed tags or attributes across your application:
#
+ # # In config/application.rb
+ # config.action_view.sanitized_allowed_tags = ['strong', 'em', 'a']
+ # config.action_view.sanitized_allowed_attributes = ['href', 'title']
def sanitize(html, options = {})
self.class.white_list_sanitizer.sanitize(html, options).try(:html_safe)
end
@@ -65,9 +88,7 @@ module ActionView
self.class.white_list_sanitizer.sanitize_css(style)
end
- # Strips all HTML tags from the +html+, including comments. This uses the
- # html-scanner tokenizer and so its HTML parsing ability is limited by
- # that of html-scanner.
+ # Strips all HTML tags from +html+, including comments.
#
# strip_tags("Strip <i>these</i> tags!")
# # => Strip these tags!
@@ -78,10 +99,10 @@ module ActionView
# strip_tags("<div id='top-bar'>Welcome to my website!</div>")
# # => Welcome to my website!
def strip_tags(html)
- self.class.full_sanitizer.sanitize(html)
+ self.class.full_sanitizer.sanitize(html, encode_special_chars: false)
end
- # Strips all link tags from +text+ leaving just the link text.
+ # Strips all link tags from +html+ leaving just the link text.
#
# strip_links('<a href="http://www.rubyonrails.org">Ruby on Rails</a>')
# # => Ruby on Rails
@@ -98,47 +119,21 @@ module ActionView
module ClassMethods #:nodoc:
attr_writer :full_sanitizer, :link_sanitizer, :white_list_sanitizer
- def sanitized_protocol_separator
- white_list_sanitizer.protocol_separator
- end
-
- def sanitized_uri_attributes
- white_list_sanitizer.uri_attributes
- end
-
- def sanitized_bad_tags
- white_list_sanitizer.bad_tags
+ # Vendors the full, link and white list sanitizers.
+ # Provided strictly for compatibility and can be removed in Rails 5.
+ def sanitizer_vendor
+ Rails::Html::Sanitizer
end
def sanitized_allowed_tags
- white_list_sanitizer.allowed_tags
+ sanitizer_vendor.white_list_sanitizer.allowed_tags
end
def sanitized_allowed_attributes
- white_list_sanitizer.allowed_attributes
+ sanitizer_vendor.white_list_sanitizer.allowed_attributes
end
- def sanitized_allowed_css_properties
- white_list_sanitizer.allowed_css_properties
- end
-
- def sanitized_allowed_css_keywords
- white_list_sanitizer.allowed_css_keywords
- end
-
- def sanitized_shorthand_css_properties
- white_list_sanitizer.shorthand_css_properties
- end
-
- def sanitized_allowed_protocols
- white_list_sanitizer.allowed_protocols
- end
-
- def sanitized_protocol_separator=(value)
- white_list_sanitizer.protocol_separator = value
- end
-
- # Gets the HTML::FullSanitizer instance used by +strip_tags+. Replace with
+ # Gets the Rails::Html::FullSanitizer instance used by +strip_tags+. Replace with
# any object that responds to +sanitize+.
#
# class Application < Rails::Application
@@ -146,21 +141,21 @@ module ActionView
# end
#
def full_sanitizer
- @full_sanitizer ||= HTML::FullSanitizer.new
+ @full_sanitizer ||= sanitizer_vendor.full_sanitizer.new
end
- # Gets the HTML::LinkSanitizer instance used by +strip_links+. Replace with
- # any object that responds to +sanitize+.
+ # Gets the Rails::Html::LinkSanitizer instance used by +strip_links+.
+ # Replace with any object that responds to +sanitize+.
#
# class Application < Rails::Application
# config.action_view.link_sanitizer = MySpecialSanitizer.new
# end
#
def link_sanitizer
- @link_sanitizer ||= HTML::LinkSanitizer.new
+ @link_sanitizer ||= sanitizer_vendor.link_sanitizer.new
end
- # Gets the HTML::WhiteListSanitizer instance used by sanitize and +sanitize_css+.
+ # Gets the Rails::Html::WhiteListSanitizer instance used by sanitize and +sanitize_css+.
# Replace with any object that responds to +sanitize+.
#
# class Application < Rails::Application
@@ -168,87 +163,7 @@ module ActionView
# end
#
def white_list_sanitizer
- @white_list_sanitizer ||= HTML::WhiteListSanitizer.new
- end
-
- # Adds valid HTML attributes that the +sanitize+ helper checks for URIs.
- #
- # class Application < Rails::Application
- # config.action_view.sanitized_uri_attributes = 'lowsrc', 'target'
- # end
- #
- def sanitized_uri_attributes=(attributes)
- HTML::WhiteListSanitizer.uri_attributes.merge(attributes)
- end
-
- # Adds to the Set of 'bad' tags for the +sanitize+ helper.
- #
- # class Application < Rails::Application
- # config.action_view.sanitized_bad_tags = 'embed', 'object'
- # end
- #
- def sanitized_bad_tags=(attributes)
- HTML::WhiteListSanitizer.bad_tags.merge(attributes)
- end
-
- # Adds to the Set of allowed tags for the +sanitize+ helper.
- #
- # class Application < Rails::Application
- # config.action_view.sanitized_allowed_tags = 'table', 'tr', 'td'
- # end
- #
- def sanitized_allowed_tags=(attributes)
- HTML::WhiteListSanitizer.allowed_tags.merge(attributes)
- end
-
- # Adds to the Set of allowed HTML attributes for the +sanitize+ helper.
- #
- # class Application < Rails::Application
- # config.action_view.sanitized_allowed_attributes = ['onclick', 'longdesc']
- # end
- #
- def sanitized_allowed_attributes=(attributes)
- HTML::WhiteListSanitizer.allowed_attributes.merge(attributes)
- end
-
- # Adds to the Set of allowed CSS properties for the #sanitize and +sanitize_css+ helpers.
- #
- # class Application < Rails::Application
- # config.action_view.sanitized_allowed_css_properties = 'expression'
- # end
- #
- def sanitized_allowed_css_properties=(attributes)
- HTML::WhiteListSanitizer.allowed_css_properties.merge(attributes)
- end
-
- # Adds to the Set of allowed CSS keywords for the +sanitize+ and +sanitize_css+ helpers.
- #
- # class Application < Rails::Application
- # config.action_view.sanitized_allowed_css_keywords = 'expression'
- # end
- #
- def sanitized_allowed_css_keywords=(attributes)
- HTML::WhiteListSanitizer.allowed_css_keywords.merge(attributes)
- end
-
- # Adds to the Set of allowed shorthand CSS properties for the +sanitize+ and +sanitize_css+ helpers.
- #
- # class Application < Rails::Application
- # config.action_view.sanitized_shorthand_css_properties = 'expression'
- # end
- #
- def sanitized_shorthand_css_properties=(attributes)
- HTML::WhiteListSanitizer.shorthand_css_properties.merge(attributes)
- end
-
- # Adds to the Set of allowed protocols for the +sanitize+ helper.
- #
- # class Application < Rails::Application
- # config.action_view.sanitized_allowed_protocols = 'ssh', 'feed'
- # end
- #
- def sanitized_allowed_protocols=(attributes)
- HTML::WhiteListSanitizer.allowed_protocols.merge(attributes)
+ @white_list_sanitizer ||= sanitizer_vendor.white_list_sanitizer.new
end
end
end
diff --git a/actionview/lib/action_view/helpers/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb
index 268558669e..2562504896 100644
--- a/actionview/lib/action_view/helpers/tag_helper.rb
+++ b/actionview/lib/action_view/helpers/tag_helper.rb
@@ -18,11 +18,14 @@ module ActionView
itemscope allowfullscreen default inert sortable
truespeed typemustmatch).to_set
- BOOLEAN_ATTRIBUTES.merge(BOOLEAN_ATTRIBUTES.map {|attribute| attribute.to_sym })
+ BOOLEAN_ATTRIBUTES.merge(BOOLEAN_ATTRIBUTES.map(&:to_sym))
+
+ TAG_PREFIXES = ['aria', 'data', :aria, :data].to_set
+
+ PRE_CONTENT_STRINGS = Hash.new { "".freeze }
+ PRE_CONTENT_STRINGS[:textarea] = "\n"
+ PRE_CONTENT_STRINGS["textarea"] = "\n"
- PRE_CONTENT_STRINGS = {
- :textarea => "\n"
- }
# Returns an empty HTML tag of type +name+ which by default is XHTML
# compliant. Set +open+ to true to create an open tag compatible
@@ -121,7 +124,7 @@ module ActionView
# cdata_section("hello]]>world")
# # => <![CDATA[hello]]]]><![CDATA[>world]]>
def cdata_section(content)
- splitted = content.to_s.gsub(']]>', ']]]]><![CDATA[>')
+ splitted = content.to_s.gsub(/\]\]\>/, ']]]]><![CDATA[>')
"<![CDATA[#{splitted}]]>".html_safe
end
@@ -141,28 +144,34 @@ module ActionView
def content_tag_string(name, content, options, escape = true)
tag_options = tag_options(options, escape) if options
content = ERB::Util.unwrapped_html_escape(content) if escape
- "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name.to_sym]}#{content}</#{name}>".html_safe
+ "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name]}#{content}</#{name}>".html_safe
end
def tag_options(options, escape = true)
return if options.blank?
- attrs = []
+ output = ""
+ sep = " ".freeze
options.each_pair do |key, value|
- if key.to_s == 'data' && value.is_a?(Hash)
+ if TAG_PREFIXES.include?(key) && value.is_a?(Hash)
value.each_pair do |k, v|
- attrs << data_tag_option(k, v, escape)
+ output << sep
+ output << prefix_tag_option(key, k, v, escape)
end
elsif BOOLEAN_ATTRIBUTES.include?(key)
- attrs << boolean_tag_option(key) if value
+ if value
+ output << sep
+ output << boolean_tag_option(key)
+ end
elsif !value.nil?
- attrs << tag_option(key, value, escape)
+ output << sep
+ output << tag_option(key, value, escape)
end
end
- " #{attrs.sort! * ' '}" unless attrs.empty?
+ output unless output.empty?
end
- def data_tag_option(key, value, escape)
- key = "data-#{key.to_s.dasherize}"
+ def prefix_tag_option(prefix, key, value, escape)
+ key = "#{prefix}-#{key.to_s.dasherize}"
unless value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(BigDecimal)
value = value.to_json
end
@@ -175,7 +184,7 @@ module ActionView
def tag_option(key, value, escape)
if value.is_a?(Array)
- value = escape ? safe_join(value, " ") : value.join(" ")
+ value = escape ? safe_join(value, " ".freeze) : value.join(" ".freeze)
else
value = escape ? ERB::Util.unwrapped_html_escape(value) : value
end
diff --git a/actionview/lib/action_view/helpers/tags.rb b/actionview/lib/action_view/helpers/tags.rb
index 45c75d10c0..a4f6eb0150 100644
--- a/actionview/lib/action_view/helpers/tags.rb
+++ b/actionview/lib/action_view/helpers/tags.rb
@@ -5,6 +5,7 @@ module ActionView
eager_autoload do
autoload :Base
+ autoload :Translator
autoload :CheckBox
autoload :CollectionCheckBoxes
autoload :CollectionRadioButtons
diff --git a/actionview/lib/action_view/helpers/tags/base.rb b/actionview/lib/action_view/helpers/tags/base.rb
index 8607da301c..d57f26ba4f 100644
--- a/actionview/lib/action_view/helpers/tags/base.rb
+++ b/actionview/lib/action_view/helpers/tags/base.rb
@@ -14,7 +14,7 @@ module ActionView
@object_name.sub!(/\[\]$/,"") || @object_name.sub!(/\[\]\]$/,"]")
@object = retrieve_object(options.delete(:object))
@options = options
- @auto_index = retrieve_autoindex(Regexp.last_match.pre_match) if Regexp.last_match
+ @auto_index = Regexp.last_match ? retrieve_autoindex(Regexp.last_match.pre_match) : nil
end
# This is what child classes implement.
@@ -25,19 +25,26 @@ module ActionView
private
def value(object)
- object.send @method_name if object
+ object.public_send @method_name if object
end
def value_before_type_cast(object)
unless object.nil?
method_before_type_cast = @method_name + "_before_type_cast"
- object.respond_to?(method_before_type_cast) ?
- object.send(method_before_type_cast) :
+ if value_came_from_user?(object) && object.respond_to?(method_before_type_cast)
+ object.public_send(method_before_type_cast)
+ else
value(object)
+ end
end
end
+ def value_came_from_user?(object)
+ method_name = "#{@method_name}_came_from_user?"
+ !object.respond_to?(method_name) || object.public_send(method_name)
+ end
+
def retrieve_object(object)
if object
object
@@ -72,35 +79,30 @@ module ActionView
end
def add_default_name_and_id(options)
- if options.has_key?("index")
- options["name"] ||= options.fetch("name"){ tag_name_with_index(options["index"], options["multiple"]) }
- options["id"] = options.fetch("id"){ tag_id_with_index(options["index"]) }
- options.delete("index")
- elsif defined?(@auto_index)
- options["name"] ||= options.fetch("name"){ tag_name_with_index(@auto_index, options["multiple"]) }
- options["id"] = options.fetch("id"){ tag_id_with_index(@auto_index) }
- else
- options["name"] ||= options.fetch("name"){ tag_name(options["multiple"]) }
- options["id"] = options.fetch("id"){ tag_id }
+ index = name_and_id_index(options)
+ options["name"] = options.fetch("name"){ tag_name(options["multiple"], index) }
+ options["id"] = options.fetch("id"){ tag_id(index) }
+ if namespace = options.delete("namespace")
+ options['id'] = options['id'] ? "#{namespace}_#{options['id']}" : namespace
end
-
- options["id"] = [options.delete('namespace'), options["id"]].compact.join("_").presence
- end
-
- def tag_name(multiple = false)
- "#{@object_name}[#{sanitized_method_name}]#{"[]" if multiple}"
- end
-
- def tag_name_with_index(index, multiple = false)
- "#{@object_name}[#{index}][#{sanitized_method_name}]#{"[]" if multiple}"
end
- def tag_id
- "#{sanitized_object_name}_#{sanitized_method_name}"
+ def tag_name(multiple = false, index = nil)
+ # a little duplication to construct less strings
+ if index
+ "#{@object_name}[#{index}][#{sanitized_method_name}]#{"[]" if multiple}"
+ else
+ "#{@object_name}[#{sanitized_method_name}]#{"[]" if multiple}"
+ end
end
- def tag_id_with_index(index)
- "#{sanitized_object_name}_#{index}_#{sanitized_method_name}"
+ def tag_id(index = nil)
+ # a little duplication to construct less strings
+ if index
+ "#{sanitized_object_name}_#{index}_#{sanitized_method_name}"
+ else
+ "#{sanitized_object_name}_#{sanitized_method_name}"
+ end
end
def sanitized_object_name
@@ -118,7 +120,12 @@ module ActionView
def select_content_tag(option_tags, options, html_options)
html_options = html_options.stringify_keys
add_default_name_and_id(html_options)
- options[:include_blank] ||= true unless options[:prompt] || select_not_required?(html_options)
+
+ if placeholder_required?(html_options)
+ raise ArgumentError, "include_blank cannot be false for a required field." if options[:include_blank] == false
+ options[:include_blank] ||= true unless options[:prompt]
+ end
+
value = options.fetch(:selected) { value(object) }
select = content_tag("select", add_options(option_tags, options, value), html_options)
@@ -129,8 +136,9 @@ module ActionView
end
end
- def select_not_required?(html_options)
- !html_options["required"] || html_options["multiple"] || html_options["size"].to_i > 1
+ def placeholder_required?(html_options)
+ # See https://html.spec.whatwg.org/multipage/forms.html#attr-select-required
+ html_options["required"] && !html_options["multiple"] && html_options.fetch("size", 1).to_i == 1
end
def add_options(option_tags, options, value = nil)
@@ -142,6 +150,10 @@ module ActionView
end
option_tags
end
+
+ def name_and_id_index(options)
+ options.key?("index") ? options.delete("index") || "" : @auto_index
+ end
end
end
end
diff --git a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb
index 6242a2a085..3256d44e18 100644
--- a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb
+++ b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb
@@ -9,29 +9,13 @@ module ActionView
class CheckBoxBuilder < Builder # :nodoc:
def check_box(extra_html_options={})
html_options = extra_html_options.merge(@input_html_options)
+ html_options[:multiple] = true
@template_object.check_box(@object_name, @method_name, html_options, @value, nil)
end
end
def render(&block)
- rendered_collection = render_collection do |item, value, text, default_html_options|
- default_html_options[:multiple] = true
- builder = instantiate_builder(CheckBoxBuilder, item, value, text, default_html_options)
-
- if block_given?
- @template_object.capture(builder, &block)
- else
- render_component(builder)
- end
- end
-
- # Append a hidden field to make sure something will be sent back to the
- # server if all check boxes are unchecked.
- if @options.fetch(:include_hidden, true)
- rendered_collection + hidden_field
- else
- rendered_collection
- end
+ render_collection_for(CheckBoxBuilder, &block)
end
private
@@ -39,18 +23,6 @@ module ActionView
def render_component(builder)
builder.check_box + builder.label
end
-
- def hidden_field
- hidden_name = @html_options[:name]
-
- hidden_name ||= if @options.has_key?(:index)
- "#{tag_name_with_index(@options[:index])}[]"
- else
- "#{tag_name}[]"
- end
-
- @template_object.hidden_field_tag(hidden_name, "", id: nil)
- end
end
end
end
diff --git a/actionview/lib/action_view/helpers/tags/collection_helpers.rb b/actionview/lib/action_view/helpers/tags/collection_helpers.rb
index 8050638363..b87b4281d6 100644
--- a/actionview/lib/action_view/helpers/tags/collection_helpers.rb
+++ b/actionview/lib/action_view/helpers/tags/collection_helpers.rb
@@ -19,6 +19,8 @@ module ActionView
def label(label_html_options={}, &block)
html_options = @input_html_options.slice(:index, :namespace).merge(label_html_options)
+ html_options[:for] ||= @input_html_options[:id] if @input_html_options[:id]
+
@template_object.label(@object_name, @sanitized_attribute_name, @text, html_options, &block)
end
end
@@ -79,6 +81,32 @@ module ActionView
yield item, value, text, default_html_options.merge(additional_html_options)
end.join.html_safe
end
+
+ def render_collection_for(builder_class, &block) #:nodoc:
+ options = @options.stringify_keys
+ rendered_collection = render_collection do |item, value, text, default_html_options|
+ builder = instantiate_builder(builder_class, item, value, text, default_html_options)
+
+ if block_given?
+ @template_object.capture(builder, &block)
+ else
+ render_component(builder)
+ end
+ end
+
+ # Append a hidden field to make sure something will be sent back to the
+ # server if all radio buttons are unchecked.
+ if options.fetch('include_hidden', true)
+ rendered_collection + hidden_field
+ else
+ rendered_collection
+ end
+ end
+
+ def hidden_field #:nodoc:
+ hidden_name = @html_options[:name] || "#{tag_name(false, @options[:index])}[]"
+ @template_object.hidden_field_tag(hidden_name, "", id: nil)
+ end
end
end
end
diff --git a/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb b/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb
index 20be34c1f2..21aaf122f8 100644
--- a/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb
+++ b/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb
@@ -14,15 +14,7 @@ module ActionView
end
def render(&block)
- render_collection do |item, value, text, default_html_options|
- builder = instantiate_builder(RadioButtonBuilder, item, value, text, default_html_options)
-
- if block_given?
- @template_object.capture(builder, &block)
- else
- render_component(builder)
- end
- end
+ render_collection_for(RadioButtonBuilder, &block)
end
private
diff --git a/actionview/lib/action_view/helpers/tags/file_field.rb b/actionview/lib/action_view/helpers/tags/file_field.rb
index 476b820d84..e6a1d9c62d 100644
--- a/actionview/lib/action_view/helpers/tags/file_field.rb
+++ b/actionview/lib/action_view/helpers/tags/file_field.rb
@@ -2,6 +2,21 @@ module ActionView
module Helpers
module Tags # :nodoc:
class FileField < TextField # :nodoc:
+
+ def render
+ options = @options.stringify_keys
+
+ if options.fetch("include_hidden", true)
+ add_default_name_and_id(options)
+ options[:type] = "file"
+ tag("input", name: options["name"], type: "hidden", value: "") + tag("input", options)
+ else
+ options.delete("include_hidden")
+ @options = options
+
+ super
+ end
+ end
end
end
end
diff --git a/actionview/lib/action_view/helpers/tags/label.rb b/actionview/lib/action_view/helpers/tags/label.rb
index a5bcaf8153..b31d5fda66 100644
--- a/actionview/lib/action_view/helpers/tags/label.rb
+++ b/actionview/lib/action_view/helpers/tags/label.rb
@@ -2,6 +2,29 @@ module ActionView
module Helpers
module Tags # :nodoc:
class Label < Base # :nodoc:
+ class LabelBuilder # :nodoc:
+ attr_reader :object
+
+ def initialize(template_object, object_name, method_name, object, tag_value)
+ @template_object = template_object
+ @object_name = object_name
+ @method_name = method_name
+ @object = object
+ @tag_value = tag_value
+ end
+
+ def translation
+ method_and_value = @tag_value.present? ? "#{@method_name}.#{@tag_value}" : @method_name
+
+ content ||= Translator
+ .new(object, @object_name, method_and_value, scope: "helpers.label")
+ .translate
+ content ||= @method_name.humanize
+
+ content
+ end
+ end
+
def initialize(object_name, method_name, template_object, content_or_options = nil, options = nil)
options ||= {}
@@ -32,33 +55,24 @@ module ActionView
options.delete("namespace")
options["for"] = name_and_id["id"] unless options.key?("for")
- if block_given?
- content = @template_object.capture(&block)
- else
- method_and_value = tag_value.present? ? "#{@method_name}.#{tag_value}" : @method_name
- content = if @content.blank?
- @object_name.gsub!(/\[(.*)_attributes\]\[\d+\]/, '.\1')
-
- if object.respond_to?(:to_model)
- key = object.class.model_name.i18n_key
- i18n_default = ["#{key}.#{method_and_value}".to_sym, ""]
- end
-
- i18n_default ||= ""
- I18n.t("#{@object_name}.#{method_and_value}", :default => i18n_default, :scope => "helpers.label").presence
- else
- @content.to_s
- end
+ builder = LabelBuilder.new(@template_object, @object_name, @method_name, @object, tag_value)
- content ||= if object && object.class.respond_to?(:human_attribute_name)
- object.class.human_attribute_name(method_and_value)
- end
-
- content ||= @method_name.humanize
+ content = if block_given?
+ @template_object.capture(builder, &block)
+ elsif @content.present?
+ @content.to_s
+ else
+ render_component(builder)
end
label_tag(name_and_id["id"], content, options)
end
+
+ private
+
+ def render_component(builder)
+ builder.translation
+ end
end
end
end
diff --git a/actionview/lib/action_view/helpers/tags/placeholderable.rb b/actionview/lib/action_view/helpers/tags/placeholderable.rb
new file mode 100644
index 0000000000..cf7b117614
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/placeholderable.rb
@@ -0,0 +1,22 @@
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ module Placeholderable # :nodoc:
+ def initialize(*)
+ super
+
+ if tag_value = @options[:placeholder]
+ placeholder = tag_value if tag_value.is_a?(String)
+ method_and_value = tag_value.is_a?(TrueClass) ? @method_name : "#{@method_name}.#{tag_value}"
+
+ placeholder ||= Tags::Translator
+ .new(object, @object_name, method_and_value, scope: "helpers.placeholder")
+ .translate
+ placeholder ||= @method_name.humanize
+ @options[:placeholder] = placeholder
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tags/search_field.rb b/actionview/lib/action_view/helpers/tags/search_field.rb
index c09e2f1be7..a848aeabfa 100644
--- a/actionview/lib/action_view/helpers/tags/search_field.rb
+++ b/actionview/lib/action_view/helpers/tags/search_field.rb
@@ -16,6 +16,7 @@ module ActionView
options["incremental"] = true unless options.has_key?("incremental")
end
+ @options = options
super
end
end
diff --git a/actionview/lib/action_view/helpers/tags/select.rb b/actionview/lib/action_view/helpers/tags/select.rb
index 00881d9978..180900cc8d 100644
--- a/actionview/lib/action_view/helpers/tags/select.rb
+++ b/actionview/lib/action_view/helpers/tags/select.rb
@@ -3,7 +3,7 @@ module ActionView
module Tags # :nodoc:
class Select < Base # :nodoc:
def initialize(object_name, method_name, template_object, choices, options, html_options)
- @choices = block_given? ? template_object.capture { yield } : choices
+ @choices = block_given? ? template_object.capture { yield || "" } : choices
@choices = @choices.to_a if @choices.is_a?(Range)
@html_options = html_options
diff --git a/actionview/lib/action_view/helpers/tags/text_area.rb b/actionview/lib/action_view/helpers/tags/text_area.rb
index 9ee83ee7c2..69038c1498 100644
--- a/actionview/lib/action_view/helpers/tags/text_area.rb
+++ b/actionview/lib/action_view/helpers/tags/text_area.rb
@@ -1,7 +1,11 @@
+require 'action_view/helpers/tags/placeholderable'
+
module ActionView
module Helpers
module Tags # :nodoc:
class TextArea < Base # :nodoc:
+ include Placeholderable
+
def render
options = @options.stringify_keys
add_default_name_and_id(options)
diff --git a/actionview/lib/action_view/helpers/tags/text_field.rb b/actionview/lib/action_view/helpers/tags/text_field.rb
index e0b80d81c2..5c576a20ca 100644
--- a/actionview/lib/action_view/helpers/tags/text_field.rb
+++ b/actionview/lib/action_view/helpers/tags/text_field.rb
@@ -1,7 +1,11 @@
+require 'action_view/helpers/tags/placeholderable'
+
module ActionView
module Helpers
module Tags # :nodoc:
class TextField < Base # :nodoc:
+ include Placeholderable
+
def render
options = @options.stringify_keys
options["size"] = options["maxlength"] unless options.key?("size")
diff --git a/actionview/lib/action_view/helpers/tags/translator.rb b/actionview/lib/action_view/helpers/tags/translator.rb
new file mode 100644
index 0000000000..8b6655481d
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/translator.rb
@@ -0,0 +1,40 @@
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class Translator # :nodoc:
+ def initialize(object, object_name, method_and_value, scope:)
+ @object_name = object_name.gsub(/\[(.*)_attributes\]\[\d+\]/, '.\1')
+ @method_and_value = method_and_value
+ @scope = scope
+ @model = object.respond_to?(:to_model) ? object.to_model : nil
+ end
+
+ def translate
+ translated_attribute = I18n.t("#{object_name}.#{method_and_value}", default: i18n_default, scope: scope).presence
+ translated_attribute || human_attribute_name
+ end
+
+ protected
+
+ attr_reader :object_name, :method_and_value, :scope, :model
+
+ private
+
+ def i18n_default
+ if model
+ key = model.model_name.i18n_key
+ ["#{key}.#{method_and_value}".to_sym, ""]
+ else
+ ""
+ end
+ end
+
+ def human_attribute_name
+ if model && model.class.respond_to?(:human_attribute_name)
+ model.class.human_attribute_name(method_and_value)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/text_helper.rb b/actionview/lib/action_view/helpers/text_helper.rb
index b859653bc9..432693bc23 100644
--- a/actionview/lib/action_view/helpers/text_helper.rb
+++ b/actionview/lib/action_view/helpers/text_helper.rb
@@ -103,7 +103,9 @@ module ActionView
# Highlights one or more +phrases+ everywhere in +text+ by inserting it into
# a <tt>:highlighter</tt> string. The highlighter can be specialized by passing <tt>:highlighter</tt>
# as a single-quoted string with <tt>\1</tt> where the phrase is to be inserted (defaults to
- # '<mark>\1</mark>') or passing a block that receives each matched term.
+ # '<mark>\1</mark>') or passing a block that receives each matched term. By default +text+
+ # is sanitized to prevent possible XSS attacks. If the input is trustworthy, passing false
+ # for <tt>:sanitize</tt> will turn sanitizing off.
#
# highlight('You searched for: rails', 'rails')
# # => You searched for: <mark>rails</mark>
@@ -122,11 +124,14 @@ module ActionView
#
# highlight('You searched for: rails', 'rails') { |match| link_to(search_path(q: match, match)) }
# # => You searched for: <a href="search?q=rails">rails</a>
+ #
+ # highlight('<a href="javascript:alert(\'no!\')">ruby</a> on rails', 'rails', sanitize: false)
+ # # => "<a>ruby</a> on <mark>rails</mark>"
def highlight(text, phrases, options = {})
text = sanitize(text) if options.fetch(:sanitize, true)
if text.blank? || phrases.blank?
- text
+ text || ""
else
match = Array(phrases).map do |p|
Regexp === p ? p.to_s : Regexp.escape(p)
@@ -201,6 +206,11 @@ module ActionView
# +plural+ is supplied, it will use that when count is > 1, otherwise
# it will use the Inflector to determine the plural form.
#
+ # If passed an optional +locale:+ parameter, the word will be pluralized
+ # using rules defined for that language (you must define your own
+ # inflection rules for languages other than English). See
+ # ActiveSupport::Inflector.pluralize
+ #
# pluralize(1, 'person')
# # => 1 person
#
@@ -212,11 +222,14 @@ module ActionView
#
# pluralize(0, 'person')
# # => 0 people
- def pluralize(count, singular, plural = nil)
+ #
+ # pluralize(2, 'Person', locale: :de)
+ # # => 2 Personen
+ def pluralize(count, singular, plural = nil, locale: nil)
word = if (count == 1 || count =~ /^1(\.0+)?$/)
singular
else
- plural || singular.pluralize
+ plural || singular.pluralize(locale)
end
"#{count || 0} #{word}"
@@ -237,12 +250,15 @@ module ActionView
#
# word_wrap('Once upon a time', line_width: 1)
# # => Once\nupon\na\ntime
- def word_wrap(text, options = {})
- line_width = options.fetch(:line_width, 80)
-
+ #
+ # You can also specify a custom +break_sequence+ ("\n" by default)
+ #
+ # word_wrap('Once upon a time', line_width: 1, break_sequence: "\r\n")
+ # # => Once\r\nupon\r\na\r\ntime
+ def word_wrap(text, line_width: 80, break_sequence: "\n")
text.split("\n").collect! do |line|
- line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1\n").strip : line
- end * "\n"
+ line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1#{break_sequence}").strip : line
+ end * break_sequence
end
# Returns +text+ transformed into HTML using simple formatting rules.
@@ -309,7 +325,7 @@ module ActionView
# <table>
# <% @items.each do |item| %>
# <tr class="<%= cycle("odd", "even") -%>">
- # <td>item</td>
+ # <td><%= item %></td>
# </tr>
# <% end %>
# </table>
diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb
index 17ec6a40bf..4c4d2c4457 100644
--- a/actionview/lib/action_view/helpers/translation_helper.rb
+++ b/actionview/lib/action_view/helpers/translation_helper.rb
@@ -1,49 +1,71 @@
require 'action_view/helpers/tag_helper'
+require 'active_support/core_ext/string/access'
require 'i18n/exceptions'
module ActionView
# = Action View Translation Helpers
module Helpers
module TranslationHelper
- # Delegates to <tt>I18n#translate</tt> but also performs three additional functions.
+ include TagHelper
+ # Delegates to <tt>I18n#translate</tt> but also performs three additional
+ # functions.
#
- # First, it will ensure that any thrown +MissingTranslation+ messages will be turned
- # into inline spans that:
+ # First, it will ensure that any thrown +MissingTranslation+ messages will
+ # be rendered as inline spans that:
#
- # * have a "translation-missing" class set,
- # * contain the missing key as a title attribute and
- # * a titleized version of the last key segment as a text.
+ # * Have a <tt>translation-missing</tt> class applied
+ # * Contain the missing key as the value of the +title+ attribute
+ # * Have a titleized version of the last key segment as text
#
- # E.g. the value returned for a missing translation key :"blog.post.title" will be
- # <span class="translation_missing" title="translation missing: en.blog.post.title">Title</span>.
- # This way your views will display rather reasonable strings but it will still
- # be easy to spot missing translations.
+ # For example, the value returned for the missing translation key
+ # <tt>"blog.post.title"</tt> will be:
#
- # Second, it'll scope the key by the current partial if the key starts
- # with a period. So if you call <tt>translate(".foo")</tt> from the
- # <tt>people/index.html.erb</tt> template, you'll actually be calling
- # <tt>I18n.translate("people.index.foo")</tt>. This makes it less repetitive
- # to translate many keys within the same partials and gives you a simple framework
- # for scoping them consistently. If you don't prepend the key with a period,
- # nothing is converted.
+ # <span
+ # class="translation_missing"
+ # title="translation missing: en.blog.post.title">Title</span>
#
- # Third, it'll mark the translation as safe HTML if the key has the suffix
- # "_html" or the last element of the key is the word "html". For example,
- # calling translate("footer_html") or translate("footer.html") will return
- # a safe HTML string that won't be escaped by other HTML helper methods. This
- # naming convention helps to identify translations that include HTML tags so that
- # you know what kind of output to expect when you call translate in a template.
+ # This allows for views to display rather reasonable strings while still
+ # giving developers a way to find missing translations.
+ #
+ # If you would prefer missing translations to raise an error, you can
+ # opt out of span-wrapping behavior globally by setting
+ # <tt>ActionView::Base.raise_on_missing_translations = true</tt> or
+ # individually by passing <tt>raise: true</tt> as an option to
+ # <tt>translate</tt>.
+ #
+ # Second, if the key starts with a period <tt>translate</tt> will scope
+ # the key by the current partial. Calling <tt>translate(".foo")</tt> from
+ # the <tt>people/index.html.erb</tt> template is equivalent to calling
+ # <tt>translate("people.index.foo")</tt>. This makes it less
+ # repetitive to translate many keys within the same partial and provides
+ # a convention to scope keys consistently.
+ #
+ # Third, the translation will be marked as <tt>html_safe</tt> if the key
+ # has the suffix "_html" or the last element of the key is "html". Calling
+ # <tt>translate("footer_html")</tt> or <tt>translate("footer.html")</tt>
+ # will return an HTML safe string that won't be escaped by other HTML
+ # helper methods. This naming convention helps to identify translations
+ # that include HTML tags so that you know what kind of output to expect
+ # when you call translate in a template and translators know which keys
+ # they can provide HTML values for.
def translate(key, options = {})
options = options.dup
- options[:default] = wrap_translate_defaults(options[:default]) if options[:default]
+ has_default = options.has_key?(:default)
+ remaining_defaults = Array(options.delete(:default)).compact
- # If the user has specified rescue_format then pass it all through, otherwise use
- # raise and do the work ourselves
- options[:raise] ||= ActionView::Base.raise_on_missing_translations
+ if has_default && !remaining_defaults.first.kind_of?(Symbol)
+ options[:default] = remaining_defaults
+ end
- raise_error = options[:raise] || options.key?(:rescue_format)
- unless raise_error
- options[:raise] = true
+ # If the user has explicitly decided to NOT raise errors, pass that option to I18n.
+ # Otherwise, tell I18n to raise an exception, which we rescue further in this method.
+ # Note: `raise_error` refers to us re-raising the error in this method. I18n is forced to raise by default.
+ if options[:raise] == false
+ raise_error = false
+ i18n_raise = false
+ else
+ raise_error = options[:raise] || ActionView::Base.raise_on_missing_translations
+ i18n_raise = true
end
if html_safe_translation_key?(key)
@@ -53,17 +75,28 @@ module ActionView
html_safe_options[name] = ERB::Util.html_escape(value.to_s)
end
end
- translation = I18n.translate(scope_key_by_partial(key), html_safe_options)
+ translation = I18n.translate(scope_key_by_partial(key), html_safe_options.merge(raise: i18n_raise))
translation.respond_to?(:html_safe) ? translation.html_safe : translation
else
- I18n.translate(scope_key_by_partial(key), options)
+ I18n.translate(scope_key_by_partial(key), options.merge(raise: i18n_raise))
end
rescue I18n::MissingTranslationData => e
- raise e if raise_error
+ if remaining_defaults.present?
+ translate remaining_defaults.shift, options.merge(default: remaining_defaults)
+ else
+ raise e if raise_error
+
+ keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope])
+ title = "translation missing: #{keys.join('.')}"
+
+ interpolations = options.except(:default, :scope)
+ if interpolations.any?
+ title << ", " << interpolations.map { |k, v| "#{k}: #{ERB::Util.html_escape(v)}" }.join(', ')
+ end
- keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope])
- content_tag('span', keys.last.to_s.titleize, :class => 'translation_missing', :title => "translation missing: #{keys.join('.')}")
+ content_tag('span', keys.last.to_s.titleize, class: 'translation_missing', title: title)
+ end
end
alias :t :translate
@@ -92,21 +125,6 @@ module ActionView
def html_safe_translation_key?(key)
key.to_s =~ /(\b|_|\.)html$/
end
-
- def wrap_translate_defaults(defaults)
- new_defaults = []
- defaults = Array(defaults)
- while key = defaults.shift
- if key.is_a?(Symbol)
- new_defaults << lambda { |_, options| translate key, options.merge(:default => defaults) }
- break
- else
- new_defaults << key
- end
- end
-
- new_defaults
- end
end
end
end
diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb
index c3be47133c..5684de35e8 100644
--- a/actionview/lib/action_view/helpers/url_helper.rb
+++ b/actionview/lib/action_view/helpers/url_helper.rb
@@ -46,9 +46,9 @@ module ActionView
end
protected :_back_url
- # Creates a link tag of the given +name+ using a URL created by the set of +options+.
+ # Creates an anchor element of the given +name+ using a URL created by the set of +options+.
# See the valid options in the documentation for +url_for+. It's also possible to
- # pass a String instead of an options hash, which generates a link tag that uses the
+ # pass a String instead of an options hash, which generates an anchor element that uses the
# value of the String as the href for the link. Using a <tt>:back</tt> Symbol instead
# of an options hash will generate a link to the referrer (a JavaScript back link
# will be used in place of a referrer if none exists). If +nil+ is passed as the name
@@ -172,6 +172,11 @@ module ActionView
#
# link_to "Visit Other Site", "http://www.rubyonrails.org/", data: { confirm: "Are you sure?" }
# # => <a href="http://www.rubyonrails.org/" data-confirm="Are you sure?">Visit Other Site</a>
+ #
+ # Also you can set any link attributes such as <tt>target</tt>, <tt>rel</tt>, <tt>type</tt>:
+ #
+ # link_to "External link", "http://www.rubyonrails.org/", target: "_blank", rel: "nofollow"
+ # # => <a href="http://www.rubyonrails.org/" target="_blank" rel="nofollow">External link</a>
def link_to(name = nil, options = nil, html_options = nil, &block)
html_options, options, name = options, name, block if block_given?
options ||= {}
@@ -179,9 +184,9 @@ module ActionView
html_options = convert_options_to_data_attributes(options, html_options)
url = url_for(options)
- html_options['href'] ||= url
+ html_options["href".freeze] ||= url
- content_tag(:a, name || url, html_options, &block)
+ content_tag("a".freeze, name || url, html_options, &block)
end
# Generates a form containing a single button that submits to the URL created
@@ -229,68 +234,58 @@ module ActionView
# ==== Examples
# <%= button_to "New", action: "new" %>
# # => "<form method="post" action="/controller/new" class="button_to">
- # # <div><input value="New" type="submit" /></div>
+ # # <input value="New" type="submit" />
# # </form>"
#
# <%= button_to "New", new_articles_path %>
# # => "<form method="post" action="/articles/new" class="button_to">
- # # <div><input value="New" type="submit" /></div>
+ # # <input value="New" type="submit" />
# # </form>"
#
# <%= button_to [:make_happy, @user] do %>
# Make happy <strong><%= @user.name %></strong>
# <% end %>
# # => "<form method="post" action="/users/1/make_happy" class="button_to">
- # # <div>
- # # <button type="submit">
- # # Make happy <strong><%= @user.name %></strong>
- # # </button>
- # # </div>
+ # # <button type="submit">
+ # # Make happy <strong><%= @user.name %></strong>
+ # # </button>
# # </form>"
#
# <%= button_to "New", { action: "new" }, form_class: "new-thing" %>
# # => "<form method="post" action="/controller/new" class="new-thing">
- # # <div><input value="New" type="submit" /></div>
+ # # <input value="New" type="submit" />
# # </form>"
#
#
# <%= button_to "Create", { action: "create" }, remote: true, form: { "data-type" => "json" } %>
# # => "<form method="post" action="/images/create" class="button_to" data-remote="true" data-type="json">
- # # <div>
- # # <input value="Create" type="submit" />
- # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/>
- # # </div>
+ # # <input value="Create" type="submit" />
+ # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/>
# # </form>"
#
#
# <%= button_to "Delete Image", { action: "delete", id: @image.id },
# method: :delete, data: { confirm: "Are you sure?" } %>
# # => "<form method="post" action="/images/delete/1" class="button_to">
- # # <div>
- # # <input type="hidden" name="_method" value="delete" />
- # # <input data-confirm='Are you sure?' value="Delete Image" type="submit" />
- # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/>
- # # </div>
+ # # <input type="hidden" name="_method" value="delete" />
+ # # <input data-confirm='Are you sure?' value="Delete Image" type="submit" />
+ # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/>
# # </form>"
#
#
# <%= button_to('Destroy', 'http://www.example.com',
# method: "delete", remote: true, data: { confirm: 'Are you sure?', disable_with: 'loading...' }) %>
# # => "<form class='button_to' method='post' action='http://www.example.com' data-remote='true'>
- # # <div>
- # # <input name='_method' value='delete' type='hidden' />
- # # <input value='Destroy' type='submit' data-disable-with='loading...' data-confirm='Are you sure?' />
- # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/>
- # # </div>
+ # # <input name='_method' value='delete' type='hidden' />
+ # # <input value='Destroy' type='submit' data-disable-with='loading...' data-confirm='Are you sure?' />
+ # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/>
# # </form>"
# #
def button_to(name = nil, options = nil, html_options = nil, &block)
html_options, options = options, name if block_given?
options ||= {}
html_options ||= {}
-
html_options = html_options.stringify_keys
- convert_boolean_attributes!(html_options, %w(disabled))
url = options.is_a?(String) ? options : url_for(options)
remote = html_options.delete('remote')
@@ -302,8 +297,9 @@ module ActionView
form_method = method == 'get' ? 'get' : 'post'
form_options = html_options.delete('form') || {}
form_options[:class] ||= html_options.delete('form_class') || 'button_to'
- form_options.merge!(method: form_method, action: url)
- form_options.merge!("data-remote" => "true") if remote
+ form_options[:method] = form_method
+ form_options[:action] = url
+ form_options[:'data-remote'] = true if remote
request_token_tag = form_method == 'post' ? token_tag : ''
@@ -436,6 +432,7 @@ module ActionView
# * <tt>:body</tt> - Preset the body of the email.
# * <tt>:cc</tt> - Carbon Copy additional recipients on the email.
# * <tt>:bcc</tt> - Blind Carbon Copy additional recipients on the email.
+ # * <tt>:reply_to</tt> - Preset the Reply-To field of the email.
#
# ==== Obfuscation
# Prior to Rails 4.0, +mail_to+ provided options for encoding the address
@@ -465,71 +462,60 @@ module ActionView
html_options, name = name, nil if block_given?
html_options = (html_options || {}).stringify_keys
- extras = %w{ cc bcc body subject }.map! { |item|
- option = html_options.delete(item) || next
- "#{item}=#{Rack::Utils.escape_path(option)}"
+ extras = %w{ cc bcc body subject reply_to }.map! { |item|
+ option = html_options.delete(item).presence || next
+ "#{item.dasherize}=#{ERB::Util.url_encode(option)}"
}.compact
extras = extras.empty? ? '' : '?' + extras.join('&')
- html_options["href"] = "mailto:#{email_address}#{extras}"
+ encoded_email_address = ERB::Util.url_encode(email_address).gsub("%40", "@")
+ html_options["href"] = "mailto:#{encoded_email_address}#{extras}"
- content_tag(:a, name || email_address, html_options, &block)
+ content_tag("a".freeze, name || email_address, html_options, &block)
end
# True if the current request URI was generated by the given +options+.
#
# ==== Examples
- # Let's say we're in the <tt>http://www.example.com/shop/checkout?order=desc</tt> action.
+ # Let's say we're in the <tt>http://www.example.com/shop/checkout?order=desc&page=1</tt> action.
#
# current_page?(action: 'process')
# # => false
#
- # current_page?(controller: 'shop', action: 'checkout')
- # # => true
- #
- # current_page?(controller: 'shop', action: 'checkout', order: 'asc')
- # # => false
- #
# current_page?(action: 'checkout')
# # => true
#
# current_page?(controller: 'library', action: 'checkout')
# # => false
#
- # current_page?('http://www.example.com/shop/checkout')
- # # => true
- #
- # current_page?('/shop/checkout')
+ # current_page?(controller: 'shop', action: 'checkout')
# # => true
#
- # Let's say we're in the <tt>http://www.example.com/shop/checkout?order=desc&page=1</tt> action.
- #
- # current_page?(action: 'process')
+ # current_page?(controller: 'shop', action: 'checkout', order: 'asc')
# # => false
#
- # current_page?(controller: 'shop', action: 'checkout')
- # # => true
- #
# current_page?(controller: 'shop', action: 'checkout', order: 'desc', page: '1')
# # => true
#
# current_page?(controller: 'shop', action: 'checkout', order: 'desc', page: '2')
# # => false
#
- # current_page?(controller: 'shop', action: 'checkout', order: 'desc')
- # # => false
+ # current_page?('http://www.example.com/shop/checkout')
+ # # => true
#
- # current_page?(action: 'checkout')
+ # current_page?('/shop/checkout')
# # => true
#
- # current_page?(controller: 'library', action: 'checkout')
- # # => false
+ # current_page?('http://www.example.com/shop/checkout?order=desc&page=1')
+ # # => true
#
# Let's say we're in the <tt>http://www.example.com/products</tt> action with method POST in case of invalid product.
#
# current_page?(controller: 'product', action: 'index')
# # => false
#
+ # We can also pass in the symbol arguments instead of strings.
+ #
def current_page?(options)
unless request
raise "You cannot use helpers that need to determine the current " \
@@ -583,34 +569,6 @@ module ActionView
html_options["data-method"] = method
end
- # Processes the +html_options+ hash, converting the boolean
- # attributes from true/false form into the form required by
- # HTML/XHTML. (An attribute is considered to be boolean if
- # its name is listed in the given +bool_attrs+ array.)
- #
- # More specifically, for each boolean attribute in +html_options+
- # given as:
- #
- # "attr" => bool_value
- #
- # if the associated +bool_value+ evaluates to true, it is
- # replaced with the attribute's name; otherwise the attribute is
- # removed from the +html_options+ hash. (See the XHTML 1.0 spec,
- # section 4.5 "Attribute Minimization" for more:
- # http://www.w3.org/TR/xhtml1/#h-4.5)
- #
- # Returns the updated +html_options+ hash, which is also modified
- # in place.
- #
- # Example:
- #
- # convert_boolean_attributes!( html_options,
- # %w( checked disabled readonly ) )
- def convert_boolean_attributes!(html_options, bool_attrs)
- bool_attrs.each { |x| html_options[x] = x if html_options.delete(x) }
- html_options
- end
-
def token_tag(token=nil)
if token != false && protect_against_forgery?
token ||= form_authenticity_token
diff --git a/actionview/lib/action_view/layouts.rb b/actionview/lib/action_view/layouts.rb
index 9ee05bd816..a74a5e05f3 100644
--- a/actionview/lib/action_view/layouts.rb
+++ b/actionview/lib/action_view/layouts.rb
@@ -228,7 +228,7 @@ module ActionView
# set by the <tt>layout</tt> method.
#
# ==== Returns
- # * <tt> Boolean</tt> - True if the action has a layout definition, false otherwise.
+ # * <tt>Boolean</tt> - True if the action has a layout definition, false otherwise.
def _conditional_layout?
return unless super
@@ -262,7 +262,7 @@ module ActionView
def layout(layout, conditions = {})
include LayoutConditions unless conditions.empty?
- conditions.each {|k, v| conditions[k] = Array(v).map {|a| a.to_s} }
+ conditions.each {|k, v| conditions[k] = Array(v).map(&:to_s) }
self._layout_conditions = conditions
self._layout = layout
@@ -277,7 +277,7 @@ module ActionView
remove_possible_method(:_layout)
prefixes = _implied_layout_name =~ /\blayouts/ ? [] : ["layouts"]
- default_behavior = "lookup_context.find_all('#{_implied_layout_name}', #{prefixes.inspect}).first || super"
+ default_behavior = "lookup_context.find_all('#{_implied_layout_name}', #{prefixes.inspect}, false, [], { formats: formats }).first || super"
name_clause = if name
default_behavior
else
@@ -316,7 +316,7 @@ module ActionView
end
self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
- def _layout
+ def _layout(formats)
if _conditional_layout?
#{layout_definition}
else
@@ -372,7 +372,7 @@ module ActionView
end
# This will be overwritten by _write_layout_method
- def _layout; end
+ def _layout(*); end
# Determine the layout for a given name, taking into account the name type.
#
@@ -382,8 +382,8 @@ module ActionView
case name
when String then _normalize_layout(name)
when Proc then name
- when true then Proc.new { _default_layout(true) }
- when :default then Proc.new { _default_layout(false) }
+ when true then Proc.new { |formats| _default_layout(formats, true) }
+ when :default then Proc.new { |formats| _default_layout(formats, false) }
when false, nil then nil
else
raise ArgumentError,
@@ -399,14 +399,15 @@ module ActionView
# Optionally raises an exception if the layout could not be found.
#
# ==== Parameters
+ # * <tt>formats</tt> - The formats accepted to this layout
# * <tt>require_layout</tt> - If set to true and layout is not found,
- # an ArgumentError exception is raised (defaults to false)
+ # an +ArgumentError+ exception is raised (defaults to false)
#
# ==== Returns
# * <tt>template</tt> - The template object for the default layout (or nil)
- def _default_layout(require_layout = false)
+ def _default_layout(formats, require_layout = false)
begin
- value = _layout if action_has_layout?
+ value = _layout(formats) if action_has_layout?
rescue NameError => e
raise e, "Could not render layout: #{e.message}"
end
diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb
index ea687d9cca..ec6edfaaa3 100644
--- a/actionview/lib/action_view/lookup_context.rb
+++ b/actionview/lib/action_view/lookup_context.rb
@@ -1,4 +1,4 @@
-require 'thread_safe'
+require 'concurrent'
require 'active_support/core_ext/module/remove_method'
require 'active_support/core_ext/module/attribute_accessors'
require 'action_view/template/resolver'
@@ -6,10 +6,11 @@ require 'action_view/template/resolver'
module ActionView
# = Action View Lookup Context
#
- # LookupContext is the object responsible to hold all information required to lookup
- # templates, i.e. view paths and details. The LookupContext is also responsible to
- # generate a key, given to view paths, used in the resolver cache lookup. Since
- # this key is generated just once during the request, it speeds up all cache accesses.
+ # <tt>LookupContext</tt> is the object responsible for holding all information
+ # required for looking up templates, i.e. view paths and details.
+ # <tt>LookupContext</tt> is also responsible for generating a key, given to
+ # view paths, used in the resolver cache lookup. Since this key is generated
+ # only once during the request, it speeds up all cache accesses.
class LookupContext #:nodoc:
attr_accessor :prefixes, :rendered_format
@@ -19,7 +20,7 @@ module ActionView
mattr_accessor :registered_details
self.registered_details = []
- def self.register_detail(name, options = {}, &block)
+ def self.register_detail(name, &block)
self.registered_details << name
initialize = registered_details.map { |n| "@details[:#{n}] = details[:#{n}] || default_#{n}" }
@@ -54,14 +55,14 @@ module ActionView
end
register_detail(:formats) { ActionView::Base.default_formats || [:html, :text, :js, :css, :xml, :json] }
register_detail(:variants) { [] }
- register_detail(:handlers){ Template::Handlers.extensions }
+ register_detail(:handlers) { Template::Handlers.extensions }
class DetailsKey #:nodoc:
alias :eql? :equal?
alias :object_hash :hash
attr_reader :hash
- @details_keys = ThreadSafe::Cache.new
+ @details_keys = Concurrent::Map.new
def self.get(details)
if details[:formats]
@@ -126,7 +127,7 @@ module ActionView
@view_paths.find_all(*args_for_lookup(name, prefixes, partial, keys, options))
end
- def exists?(name, prefixes = [], partial = false, keys = [], options = {})
+ def exists?(name, prefixes = [], partial = false, keys = [], **options)
@view_paths.exists?(*args_for_lookup(name, prefixes, partial, keys, options))
end
alias :template_exists? :exists?
@@ -172,13 +173,13 @@ module ActionView
# name instead of the prefix.
def normalize_name(name, prefixes) #:nodoc:
prefixes = prefixes.presence
- parts = name.to_s.split('/')
+ parts = name.to_s.split('/'.freeze)
parts.shift if parts.first.empty?
name = parts.pop
return name, prefixes || [""] if parts.empty?
- parts = parts.join('/')
+ parts = parts.join('/'.freeze)
prefixes = prefixes ? prefixes.map { |p| "#{p}/#{parts}" } : [parts]
return name, prefixes
@@ -191,7 +192,6 @@ module ActionView
def initialize(view_paths, details = {}, prefixes = [])
@details, @details_key = {}, nil
- @skip_default_locale = false
@cache = true
@prefixes = prefixes
@rendered_format = nil
@@ -204,7 +204,7 @@ module ActionView
# add :html as fallback to :js.
def formats=(values)
if values
- values.concat(default_formats) if values.delete "*/*"
+ values.concat(default_formats) if values.delete "*/*".freeze
if values == [:js]
values << :html
@html_fallback_for_js = true
@@ -213,12 +213,6 @@ module ActionView
super(values)
end
- # Do not use the default locale on template lookup.
- def skip_default_locale!
- @skip_default_locale = true
- self.locale = nil
- end
-
# Override locale to return a symbol instead of array.
def locale
@details[:locale].first
@@ -233,23 +227,7 @@ module ActionView
config.locale = value
end
- super(@skip_default_locale ? I18n.locale : default_locale)
- end
-
- # Uses the first format in the formats array for layout lookup.
- def with_layout_format
- if formats.size == 1
- yield
- else
- old_formats = formats
- _set_detail(:formats, formats[0,1])
-
- begin
- yield
- ensure
- _set_detail(:formats, old_formats)
- end
- end
+ super(default_locale)
end
end
end
diff --git a/actionview/lib/action_view/model_naming.rb b/actionview/lib/action_view/model_naming.rb
index e09ebd60df..b6ed13424e 100644
--- a/actionview/lib/action_view/model_naming.rb
+++ b/actionview/lib/action_view/model_naming.rb
@@ -1,12 +1,12 @@
module ActionView
- module ModelNaming
+ module ModelNaming #:nodoc:
# Converts the given object to an ActiveModel compliant one.
def convert_to_model(object)
object.respond_to?(:to_model) ? object.to_model : object
end
def model_name_from_record_or_class(record_or_class)
- (record_or_class.is_a?(Class) ? record_or_class : convert_to_model(record_or_class).class).model_name
+ convert_to_model(record_or_class).model_name
end
end
end
diff --git a/actionview/lib/action_view/path_set.rb b/actionview/lib/action_view/path_set.rb
index 91ee2ea8f5..7a88f6bc50 100644
--- a/actionview/lib/action_view/path_set.rb
+++ b/actionview/lib/action_view/path_set.rb
@@ -61,6 +61,15 @@ module ActionView #:nodoc:
find_all(path, prefixes, *args).any?
end
+ def find_all_with_query(query) # :nodoc:
+ paths.each do |resolver|
+ templates = resolver.find_all_with_query(query)
+ return templates unless templates.empty?
+ end
+
+ []
+ end
+
private
def typecast(paths)
diff --git a/actionview/lib/action_view/railtie.rb b/actionview/lib/action_view/railtie.rb
index 81f9c40b85..e829d86c99 100644
--- a/actionview/lib/action_view/railtie.rb
+++ b/actionview/lib/action_view/railtie.rb
@@ -36,14 +36,30 @@ module ActionView
end
end
+ initializer "action_view.collection_caching" do |app|
+ ActiveSupport.on_load(:action_controller) do
+ PartialRenderer.collection_cache = app.config.action_controller.cache_store
+ end
+ end
+
+ initializer "action_view.per_request_digest_cache" do |app|
+ ActiveSupport.on_load(:action_view) do
+ if app.config.consider_all_requests_local
+ app.middleware.use ActionView::Digestor::PerRequestDigestCacheExpiry
+ end
+ end
+ end
+
initializer "action_view.setup_action_pack" do |app|
ActiveSupport.on_load(:action_controller) do
- ActionView::RoutingUrlFor.send(:include, ActionDispatch::Routing::UrlFor)
+ ActionView::RoutingUrlFor.include(ActionDispatch::Routing::UrlFor)
end
end
- rake_tasks do
- load "action_view/tasks/dependencies.rake"
+ rake_tasks do |app|
+ unless app.config.api_only
+ load "action_view/tasks/dependencies.rake"
+ end
end
end
end
diff --git a/actionview/lib/action_view/record_identifier.rb b/actionview/lib/action_view/record_identifier.rb
index 63f645431a..4b44eb5520 100644
--- a/actionview/lib/action_view/record_identifier.rb
+++ b/actionview/lib/action_view/record_identifier.rb
@@ -2,29 +2,54 @@ require 'active_support/core_ext/module'
require 'action_view/model_naming'
module ActionView
- # The record identifier encapsulates a number of naming conventions for dealing with records, like Active Records or
- # pretty much any other model type that has an id. These patterns are then used to try elevate the view actions to
- # a higher logical level.
+ # RecordIdentifier encapsulates methods used by various ActionView helpers
+ # to associate records with DOM elements.
#
- # # routes
- # resources :posts
+ # Consider for example the following code that displays the body of a post:
#
- # # view
- # <%= div_for(post) do %> <div id="post_45" class="post">
- # <%= post.body %> What a wonderful world!
- # <% end %> </div>
+ # <%= div_for(post) do %>
+ # <%= post.body %>
+ # <% end %>
#
- # # controller
- # def update
- # post = Post.find(params[:id])
- # post.update(params[:post])
+ # When +post+ is a new, unsaved ActiveRecord::Base instance, the resulting HTML
+ # is:
#
- # redirect_to(post) # Calls polymorphic_url(post) which in turn calls post_url(post)
- # end
+ # <div id="new_post" class="post">
+ # </div>
+ #
+ # When +post+ is a persisted ActiveRecord::Base instance, the resulting HTML
+ # is:
+ #
+ # <div id="post_42" class="post">
+ # What a wonderful world!
+ # </div>
+ #
+ # In both cases, the +id+ and +class+ of the wrapping DOM element are
+ # automatically generated, following naming conventions encapsulated by the
+ # RecordIdentifier methods #dom_id and #dom_class:
+ #
+ # dom_id(Post.new) # => "new_post"
+ # dom_class(Post.new) # => "post"
+ # dom_id(Post.find 42) # => "post_42"
+ # dom_class(Post.find 42) # => "post"
#
- # As the example above shows, you can stop caring to a large extent what the actual id of the post is.
- # You just know that one is being assigned and that the subsequent calls in redirect_to expect that
- # same naming convention and allows you to write less code if you follow it.
+ # Note that these methods do not strictly require +Post+ to be a subclass of
+ # ActiveRecord::Base.
+ # Any +Post+ class will work as long as its instances respond to +to_key+
+ # and +model_name+, given that +model_name+ responds to +param_key+.
+ # For instance:
+ #
+ # class Post
+ # attr_accessor :to_key
+ #
+ # def model_name
+ # OpenStruct.new param_key: 'post'
+ # end
+ #
+ # def self.find(id)
+ # new.tap { |post| post.to_key = [id] }
+ # end
+ # end
module RecordIdentifier
extend self
extend ModelNaming
@@ -78,7 +103,7 @@ module ActionView
# make sure yourself that your dom ids are valid, in case you overwrite this method.
def record_key_for_dom_id(record)
key = convert_to_model(record).to_key
- key ? key.join('_') : key
+ key ? key.join(JOIN) : key
end
end
end
diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb
index 0407632435..39c8658ffe 100644
--- a/actionview/lib/action_view/renderer/partial_renderer.rb
+++ b/actionview/lib/action_view/renderer/partial_renderer.rb
@@ -1,4 +1,5 @@
-require 'thread_safe'
+require 'action_view/renderer/partial_renderer/collection_caching'
+require 'concurrent'
module ActionView
class PartialIteration
@@ -73,7 +74,7 @@ module ActionView
#
# <%= render partial: "account", locals: { user: @buyer } %>
#
- # == Rendering a collection of partials
+ # == \Rendering a collection of partials
#
# The example of partial use describes a familiar pattern where a template needs to iterate over an array and
# render a sub template for each of the elements. This pattern has been implemented as a single method that
@@ -105,7 +106,7 @@ module ActionView
# NOTE: Due to backwards compatibility concerns, the collection can't be one of hashes. Normally you'd also
# just keep domain objects, like Active Records, in there.
#
- # == Rendering shared partials
+ # == \Rendering shared partials
#
# Two controllers can share a set of partials and render them like this:
#
@@ -113,7 +114,7 @@ module ActionView
#
# This will render the partial "advertisement/_ad.html.erb" regardless of which controller this is being called from.
#
- # == Rendering objects that respond to `to_partial_path`
+ # == \Rendering objects that respond to `to_partial_path`
#
# Instead of explicitly naming the location of a partial, you can also let PartialRenderer do the work
# and pick the proper path by checking `to_partial_path` method.
@@ -127,7 +128,7 @@ module ActionView
# # <%= render partial: "posts/post", collection: @posts %>
# <%= render partial: @posts %>
#
- # == Rendering the default case
+ # == \Rendering the default case
#
# If you're not going to be using any of the options like collections or layouts, you can also use the short-hand
# defaults of render to render partials. Examples:
@@ -147,29 +148,29 @@ module ActionView
# # <%= render partial: "posts/post", collection: @posts %>
# <%= render @posts %>
#
- # == Rendering partials with layouts
+ # == \Rendering partials with layouts
#
# Partials can have their own layouts applied to them. These layouts are different than the ones that are
# specified globally for the entire action, but they work in a similar fashion. Imagine a list with two types
# of users:
#
- # <%# app/views/users/index.html.erb &>
+ # <%# app/views/users/index.html.erb %>
# Here's the administrator:
# <%= render partial: "user", layout: "administrator", locals: { user: administrator } %>
#
# Here's the editor:
# <%= render partial: "user", layout: "editor", locals: { user: editor } %>
#
- # <%# app/views/users/_user.html.erb &>
+ # <%# app/views/users/_user.html.erb %>
# Name: <%= user.name %>
#
- # <%# app/views/users/_administrator.html.erb &>
+ # <%# app/views/users/_administrator.html.erb %>
# <div id="administrator">
# Budget: $<%= user.budget %>
# <%= yield %>
# </div>
#
- # <%# app/views/users/_editor.html.erb &>
+ # <%# app/views/users/_editor.html.erb %>
# <div id="editor">
# Deadline: <%= user.deadline %>
# <%= yield %>
@@ -232,7 +233,7 @@ module ActionView
#
# You can also apply a layout to a block within any template:
#
- # <%# app/views/users/_chief.html.erb &>
+ # <%# app/views/users/_chief.html.erb %>
# <%= render(layout: "administrator", locals: { user: chief }) do %>
# Title: <%= chief.title %>
# <% end %>
@@ -249,13 +250,13 @@ module ActionView
# If you pass arguments to "yield" then this will be passed to the block. One way to use this is to pass
# an array to layout and treat it as an enumerable.
#
- # <%# app/views/users/_user.html.erb &>
+ # <%# app/views/users/_user.html.erb %>
# <div class="user">
# Budget: $<%= user.budget %>
# <%= yield user %>
# </div>
#
- # <%# app/views/users/index.html.erb &>
+ # <%# app/views/users/index.html.erb %>
# <%= render layout: @users do |user| %>
# Title: <%= user.title %>
# <% end %>
@@ -264,14 +265,14 @@ module ActionView
#
# You can also yield multiple times in one layout and use block arguments to differentiate the sections.
#
- # <%# app/views/users/_user.html.erb &>
+ # <%# app/views/users/_user.html.erb %>
# <div class="user">
# <%= yield user, :header %>
# Budget: $<%= user.budget %>
# <%= yield user, :footer %>
# </div>
#
- # <%# app/views/users/index.html.erb &>
+ # <%# app/views/users/index.html.erb %>
# <%= render layout: @users do |user, section| %>
# <%- case section when :header -%>
# Title: <%= user.title %>
@@ -280,8 +281,10 @@ module ActionView
# <%- end -%>
# <% end %>
class PartialRenderer < AbstractRenderer
- PREFIXED_PARTIAL_NAMES = ThreadSafe::Cache.new do |h, k|
- h[k] = ThreadSafe::Cache.new
+ include CollectionCaching
+
+ PREFIXED_PARTIAL_NAMES = Concurrent::Map.new do |h, k|
+ h[k] = Concurrent::Map.new
end
def initialize(*)
@@ -321,8 +324,9 @@ module ActionView
spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals)
end
- result = @template ? collection_with_template : collection_without_template
- result.join(spacer).html_safe
+ cache_collection_render do
+ @template ? collection_with_template : collection_without_template
+ end.join(spacer).html_safe
end
def render_partial
@@ -334,7 +338,7 @@ module ActionView
end
object ||= locals[as]
- locals[as] = object
+ locals[as] = object if @has_object
content = @template.render(view, locals) do |*name|
view._layout_for(*name, &block)
@@ -344,8 +348,6 @@ module ActionView
content
end
- private
-
# Sets up instance variables needed for rendering a partial. This method
# finds the options and details and extracts them. The method also contains
# logic that handles the type of object passed in as the partial.
@@ -366,10 +368,12 @@ module ActionView
partial = options[:partial]
if String === partial
+ @has_object = options.key?(:object)
@object = options[:object]
@collection = collection_from_options
@path = partial
else
+ @has_object = true
@object = partial
@collection = collection_from_object || collection_from_options
@@ -382,7 +386,7 @@ module ActionView
end
if as = options[:as]
- raise_invalid_identifier(as) unless as.to_s =~ /\A[a-z_]\w*\z/
+ raise_invalid_option_as(as) unless as.to_s =~ /\A[a-z_]\w*\z/
as = as.to_sym
end
@@ -506,7 +510,7 @@ module ActionView
def retrieve_template_keys
keys = @locals.keys
- keys << @variable if @object || @collection
+ keys << @variable if @has_object || @collection
if @collection
keys << @variable_counter
keys << @variable_iteration
@@ -516,8 +520,8 @@ module ActionView
def retrieve_variable(path, as)
variable = as || begin
- base = path[-1] == "/" ? "" : File.basename(path)
- raise_invalid_identifier(path) unless base =~ /\A_?([a-z]\w*)(\.\w+)*\z/
+ base = path[-1] == "/".freeze ? "".freeze : File.basename(path)
+ raise_invalid_identifier(path) unless base =~ /\A_?(.*)(?:\.\w+)*\z/
$1.to_sym
end
if @collection
@@ -528,11 +532,18 @@ module ActionView
end
IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " +
- "make sure your partial name starts with a lowercase letter or underscore, " +
+ "make sure your partial name starts with underscore."
+
+ OPTION_AS_ERROR_MESSAGE = "The value (%s) of the option `as` is not a valid Ruby identifier; " +
+ "make sure it starts with lowercase letter, " +
"and is followed by any combination of letters, numbers and underscores."
def raise_invalid_identifier(path)
raise ArgumentError.new(IDENTIFIER_ERROR_MESSAGE % (path))
end
+
+ def raise_invalid_option_as(as)
+ raise ArgumentError.new(OPTION_AS_ERROR_MESSAGE % (as))
+ end
end
end
diff --git a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb
new file mode 100644
index 0000000000..1147963882
--- /dev/null
+++ b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb
@@ -0,0 +1,70 @@
+require 'active_support/core_ext/object/try'
+
+module ActionView
+ module CollectionCaching # :nodoc:
+ extend ActiveSupport::Concern
+
+ included do
+ # Fallback cache store if Action View is used without Rails.
+ # Otherwise overridden in Railtie to use Rails.cache.
+ mattr_accessor(:collection_cache) { ActiveSupport::Cache::MemoryStore.new }
+ end
+
+ private
+ def cache_collection_render
+ return yield unless cache_collection?
+
+ keyed_collection = collection_by_cache_keys
+ partial_cache = collection_cache.read_multi(*keyed_collection.keys)
+
+ @collection = keyed_collection.reject { |key, _| partial_cache.key?(key) }.values
+ rendered_partials = @collection.any? ? yield.dup : []
+
+ fetch_or_cache_partial(partial_cache, order_by: keyed_collection.each_key) do
+ rendered_partials.shift
+ end
+ end
+
+ def cache_collection?
+ @options.fetch(:cache, automatic_cache_eligible?)
+ end
+
+ def automatic_cache_eligible?
+ single_template_render? && !callable_cache_key? &&
+ @template.eligible_for_collection_caching?(as: @options[:as])
+ end
+
+ def single_template_render?
+ @template # Template is only set when a collection renders one template.
+ end
+
+ def callable_cache_key?
+ @options[:cache].respond_to?(:call)
+ end
+
+ def collection_by_cache_keys
+ seed = callable_cache_key? ? @options[:cache] : ->(i) { i }
+
+ @collection.each_with_object({}) do |item, hash|
+ hash[expanded_cache_key(seed.call(item))] = item
+ end
+ end
+
+ def expanded_cache_key(key)
+ key = @view.fragment_cache_key(@view.cache_fragment_name(key, virtual_path: @template.virtual_path))
+ key.frozen? ? key.dup : key # #read_multi & #write may require mutability, Dalli 2.6.0.
+ end
+
+ def fetch_or_cache_partial(cached_partials, order_by:)
+ cache_options = @options[:cache_options] || @locals[:cache_options] || {}
+
+ order_by.map do |key|
+ cached_partials.fetch(key) do
+ yield.tap do |rendered_partial|
+ collection_cache.write(key, rendered_partial, cache_options)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/renderer/renderer.rb b/actionview/lib/action_view/renderer/renderer.rb
index 964b18337e..1bee35d80d 100644
--- a/actionview/lib/action_view/renderer/renderer.rb
+++ b/actionview/lib/action_view/renderer/renderer.rb
@@ -37,7 +37,7 @@ module ActionView
end
end
- # Direct accessor to template rendering.
+ # Direct access to template rendering.
def render_template(context, options) #:nodoc:
TemplateRenderer.new(@lookup_context).render(context, options)
end
diff --git a/actionview/lib/action_view/renderer/streaming_template_renderer.rb b/actionview/lib/action_view/renderer/streaming_template_renderer.rb
index 3ab2cd36fc..f38e2764d0 100644
--- a/actionview/lib/action_view/renderer/streaming_template_renderer.rb
+++ b/actionview/lib/action_view/renderer/streaming_template_renderer.rb
@@ -47,7 +47,7 @@ module ActionView
return [super] unless layout_name && template.supports_streaming?
locals ||= {}
- layout = layout_name && find_layout(layout_name, locals.keys)
+ layout = layout_name && find_layout(layout_name, locals.keys, [formats.first])
Body.new do |buffer|
delayed_render(buffer, template, layout, @view, locals)
diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb
index f3a48ecfa0..75217e1630 100644
--- a/actionview/lib/action_view/renderer/template_renderer.rb
+++ b/actionview/lib/action_view/renderer/template_renderer.rb
@@ -18,7 +18,7 @@ module ActionView
# Determine the template to be rendered using the given options.
def determine_template(options)
- keys = options.fetch(:locals, {}).keys
+ keys = options.has_key?(:locals) ? options[:locals].keys : []
if options.key?(:body)
Template::Text.new(options[:body])
@@ -40,7 +40,7 @@ module ActionView
find_template(options[:template], options[:prefixes], false, keys, @details)
end
else
- raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :plain, :text or :body option."
+ raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :plain, :html, :text or :body option."
end
end
@@ -57,7 +57,7 @@ module ActionView
end
def render_with_layout(path, locals) #:nodoc:
- layout = path && find_layout(path, locals.keys)
+ layout = path && find_layout(path, locals.keys, [formats.first])
content = yield(layout)
if layout
@@ -72,27 +72,28 @@ module ActionView
# This is the method which actually finds the layout using details in the lookup
# context object. If no layout is found, it checks if at least a layout with
# the given name exists across all details before raising the error.
- def find_layout(layout, keys)
- with_layout_format { resolve_layout(layout, keys) }
+ def find_layout(layout, keys, formats)
+ resolve_layout(layout, keys, formats)
end
- def resolve_layout(layout, keys)
+ def resolve_layout(layout, keys, formats)
+ details = @details.dup
+ details[:formats] = formats
+
case layout
when String
begin
if layout =~ /^\//
- with_fallbacks { find_template(layout, nil, false, keys, @details) }
+ with_fallbacks { find_template(layout, nil, false, keys, details) }
else
- find_template(layout, nil, false, keys, @details)
+ find_template(layout, nil, false, keys, details)
end
rescue ActionView::MissingTemplate
all_details = @details.merge(:formats => @lookup_context.default_formats)
raise unless template_exists?(layout, nil, false, keys, all_details)
end
when Proc
- resolve_layout(layout.call, keys)
- when FalseClass
- nil
+ resolve_layout(layout.call(formats), keys, formats)
else
layout
end
diff --git a/actionview/lib/action_view/rendering.rb b/actionview/lib/action_view/rendering.rb
index 81d5836a8c..8604637da2 100644
--- a/actionview/lib/action_view/rendering.rb
+++ b/actionview/lib/action_view/rendering.rb
@@ -35,13 +35,13 @@ module ActionView
module ClassMethods
def view_context_class
@view_context_class ||= begin
- include_path_helpers = supports_path?
+ supports_path = supports_path?
routes = respond_to?(:_routes) && _routes
helpers = respond_to?(:_helpers) && _helpers
Class.new(ActionView::Base) do
if routes
- include routes.url_helpers(include_path_helpers)
+ include routes.url_helpers(supports_path)
include routes.mounted_helpers
end
@@ -59,7 +59,7 @@ module ActionView
@_view_context_class ||= self.class.view_context_class
end
- # An instance of a view class. The default view class is ActionView::Base
+ # An instance of a view class. The default view class is ActionView::Base.
#
# The view class must have the following methods:
# View.new[lookup_context, assigns, controller]
@@ -92,23 +92,26 @@ module ActionView
# Find and render a template based on the options given.
# :api: private
def _render_template(options) #:nodoc:
- variant = options[:variant]
+ variant = options.delete(:variant)
+ assigns = options.delete(:assigns)
+ context = view_context
+ context.assign assigns if assigns
lookup_context.rendered_format = nil if options[:formats]
lookup_context.variants = variant if variant
- view_renderer.render(view_context, options)
+ view_renderer.render(context, options)
end
- # Assign the rendered format to lookup context.
- def _process_format(format, options = {}) #:nodoc:
+ # Assign the rendered format to look up context.
+ def _process_format(format) #:nodoc:
super
lookup_context.formats = [format.to_sym]
lookup_context.rendered_format = lookup_context.formats.first
end
# Normalize args by converting render "foo" to render :action => "foo" and
- # render "foo/bar" to render :file => "foo/bar".
+ # render "foo/bar" to render :template => "foo/bar".
# :api: private
def _normalize_args(action=nil, options={})
options = super(action, options)
@@ -118,7 +121,7 @@ module ActionView
options = action
when String, Symbol
action = action.to_s
- key = action.include?(?/) ? :file : :action
+ key = action.include?(?/) ? :template : :action
options[key] = action
else
options[:partial] = action
diff --git a/actionview/lib/action_view/routing_url_for.rb b/actionview/lib/action_view/routing_url_for.rb
index 881a123572..45e78d1ad9 100644
--- a/actionview/lib/action_view/routing_url_for.rb
+++ b/actionview/lib/action_view/routing_url_for.rb
@@ -32,7 +32,7 @@ module ActionView
#
# ==== Examples
# <%= url_for(action: 'index') %>
- # # => /blog/
+ # # => /blogs/
#
# <%= url_for(action: 'find', controller: 'books') %>
# # => /books/find
@@ -80,19 +80,41 @@ module ActionView
when String
options
when nil
- super({:only_path => true})
+ super(only_path: _generate_paths_by_default)
when Hash
- super({ :only_path => options[:host].nil? }.merge!(options.symbolize_keys))
+ options = options.symbolize_keys
+ unless options.key?(:only_path)
+ options[:only_path] = only_path?(options[:host])
+ end
+
+ super(options)
+ when ActionController::Parameters
+ unless options.key?(:only_path)
+ options[:only_path] = only_path?(options[:host])
+ end
+
+ super(options)
when :back
_back_url
- when Symbol
- ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_string_call self, options
when Array
- polymorphic_path(options, options.extract_options!)
- when Class
- ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_class_call self, options
+ components = options.dup
+ if _generate_paths_by_default
+ polymorphic_path(components, components.extract_options!)
+ else
+ polymorphic_url(components, components.extract_options!)
+ end
else
- ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_model_call self, options
+ method = _generate_paths_by_default ? :path : :url
+ builder = ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.send(method)
+
+ case options
+ when Symbol
+ builder.handle_string_call(self, options)
+ when Class
+ builder.handle_class_call(self, options)
+ else
+ builder.handle_model_call(self, options)
+ end
end
end
@@ -111,5 +133,15 @@ module ActionView
controller.optimize_routes_generation? : super
end
protected :optimize_routes_generation?
+
+ private
+
+ def _generate_paths_by_default
+ true
+ end
+
+ def only_path?(host)
+ _generate_paths_by_default unless host
+ end
end
end
diff --git a/actionview/lib/action_view/tasks/dependencies.rake b/actionview/lib/action_view/tasks/dependencies.rake
index b39f7d583b..f394c319c1 100644
--- a/actionview/lib/action_view/tasks/dependencies.rake
+++ b/actionview/lib/action_view/tasks/dependencies.rake
@@ -2,20 +2,22 @@ namespace :cache_digests do
desc 'Lookup nested dependencies for TEMPLATE (like messages/show or comments/_comment.html)'
task :nested_dependencies => :environment do
abort 'You must provide TEMPLATE for the task to run' unless ENV['TEMPLATE'].present?
- puts JSON.pretty_generate ActionView::Digestor.new(name: template_name, finder: finder).nested_dependencies
+ puts JSON.pretty_generate ActionView::Digestor.new(name: CacheDigests.template_name, finder: CacheDigests.finder).nested_dependencies
end
desc 'Lookup first-level dependencies for TEMPLATE (like messages/show or comments/_comment.html)'
task :dependencies => :environment do
abort 'You must provide TEMPLATE for the task to run' unless ENV['TEMPLATE'].present?
- puts JSON.pretty_generate ActionView::Digestor.new(name: template_name, finder: finder).dependencies
+ puts JSON.pretty_generate ActionView::Digestor.new(name: CacheDigests.template_name, finder: CacheDigests.finder).dependencies
end
- def template_name
- ENV['TEMPLATE'].split('.', 2).first
- end
+ class CacheDigests
+ def self.template_name
+ ENV['TEMPLATE'].split('.', 2).first
+ end
- def finder
- ApplicationController.new.lookup_context
+ def self.finder
+ ApplicationController.new.lookup_context
+ end
end
end
diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb
index 9d39d02a37..0ed208f27e 100644
--- a/actionview/lib/action_view/template.rb
+++ b/actionview/lib/action_view/template.rb
@@ -87,6 +87,19 @@ module ActionView
# expected_encoding
# )
+ ##
+ # :method: local_assigns
+ #
+ # Returns a hash with the defined local variables.
+ #
+ # Given this sub template rendering:
+ #
+ # <%= render "shared/header", { headline: "Welcome", person: person } %>
+ #
+ # You can use +local_assigns+ in the sub templates to access the local variables:
+ #
+ # local_assigns[:headline] # => "Welcome"
+
eager_autoload do
autoload :Error
autoload :Handlers
@@ -103,7 +116,7 @@ module ActionView
# This finalizer is needed (and exactly with a proc inside another proc)
# otherwise templates leak in development.
- Finalizer = proc do |method_name, mod|
+ Finalizer = proc do |method_name, mod| # :nodoc:
proc do
mod.module_eval do
remove_possible_method method_name
@@ -117,6 +130,7 @@ module ActionView
@source = source
@identifier = identifier
@handler = handler
+ @cache_name = extract_resource_cache_name
@compiled = false
@original_encoding = nil
@locals = details[:locals] || []
@@ -127,7 +141,7 @@ module ActionView
@compile_mutex = Mutex.new
end
- # Returns if the underlying handler supports streaming. If so,
+ # Returns whether the underlying handler supports streaming. If so,
# a streaming buffer *may* be passed when it start rendering.
def supports_streaming?
handler.respond_to?(:supports_streaming?) && handler.supports_streaming?
@@ -140,7 +154,7 @@ module ActionView
# we use a bang in this instrumentation because you don't want to
# consume this in production. This is only slow if it's being listened to.
def render(view, locals, buffer=nil, &block)
- instrument("!render_template") do
+ instrument("!render_template".freeze) do
compile!(view)
view.send(method_name, locals, buffer, &block)
end
@@ -152,6 +166,10 @@ module ActionView
@type ||= Types[@formats.first] if @formats.first
end
+ def eligible_for_collection_caching?(as: nil)
+ @cache_name == (as || inferred_cache_name).to_s
+ end
+
# Receives a view object and return a template similar to self by using @virtual_path.
#
# This method is useful if you have a template object but it does not contain its source
@@ -172,7 +190,7 @@ module ActionView
end
def inspect
- @inspect ||= defined?(Rails.root) ? identifier.sub("#{Rails.root}/", '') : identifier
+ @inspect ||= defined?(Rails.root) ? identifier.sub("#{Rails.root}/", ''.freeze) : identifier
end
# This method is responsible for properly setting the encoding of the
@@ -242,7 +260,7 @@ module ActionView
end
instrument("!compile_template") do
- compile(view, mod)
+ compile(mod)
end
# Just discard the source if we have a virtual path. This
@@ -264,7 +282,7 @@ module ActionView
# encode the source into <tt>Encoding.default_internal</tt>.
# In general, this means that templates will be UTF-8 inside of Rails,
# regardless of the original source encoding.
- def compile(view, mod) #:nodoc:
+ def compile(mod) #:nodoc:
encode!
method_name = self.method_name
code = @handler.call(self)
@@ -293,18 +311,8 @@ module ActionView
raise WrongEncodingError.new(@source, Encoding.default_internal)
end
- begin
- mod.module_eval(source, identifier, 0)
- ObjectSpace.define_finalizer(self, Finalizer[method_name, mod])
- rescue => e # errors from template code
- if logger = (view && view.logger)
- logger.debug "ERROR: compiling #{method_name} RAISED #{e}"
- logger.debug "Function body: #{source}"
- logger.debug "Backtrace: #{e.backtrace.join("\n")}"
- end
-
- raise ActionView::Template::Error.new(self, e)
- end
+ mod.module_eval(source, identifier, 0)
+ ObjectSpace.define_finalizer(self, Finalizer[method_name, mod])
end
def handle_render_error(view, e) #:nodoc:
@@ -323,20 +331,47 @@ module ActionView
def locals_code #:nodoc:
# Double assign to suppress the dreaded 'assigned but unused variable' warning
- @locals.map { |key| "#{key} = #{key} = local_assigns[:#{key}];" }.join
+ @locals.each_with_object('') { |key, code| code << "#{key} = #{key} = local_assigns[:#{key}];" }
end
def method_name #:nodoc:
- @method_name ||= "_#{identifier_method_name}__#{@identifier.hash}_#{__id__}".gsub('-', "_")
+ @method_name ||= begin
+ m = "_#{identifier_method_name}__#{@identifier.hash}_#{__id__}"
+ m.tr!('-'.freeze, '_'.freeze)
+ m
+ end
end
def identifier_method_name #:nodoc:
- inspect.gsub(/[^a-z_]/, '_')
+ inspect.tr('^a-z_'.freeze, '_'.freeze)
end
def instrument(action, &block)
payload = { virtual_path: @virtual_path, identifier: @identifier }
- ActiveSupport::Notifications.instrument("#{action}.action_view", payload, &block)
+ case action
+ when "!render_template".freeze
+ ActiveSupport::Notifications.instrument("!render_template.action_view".freeze, payload, &block)
+ else
+ ActiveSupport::Notifications.instrument("#{action}.action_view".freeze, payload, &block)
+ end
+ end
+
+ EXPLICIT_COLLECTION = /# Template Collection: (?<resource_name>\w+)/
+
+ def extract_resource_cache_name
+ if match = @source.match(EXPLICIT_COLLECTION) || resource_cache_call_match
+ match[:resource_name]
+ end
+ end
+
+ def resource_cache_call_match
+ if @handler.respond_to?(:resource_cache_call_pattern)
+ @source.match(@handler.resource_cache_call_pattern)
+ end
+ end
+
+ def inferred_cache_name
+ @inferred_cache_name ||= @virtual_path.split('/'.freeze).last.sub('_'.freeze, ''.freeze)
end
end
end
diff --git a/actionview/lib/action_view/template/error.rb b/actionview/lib/action_view/template/error.rb
index 743ef6de0a..390bce98a2 100644
--- a/actionview/lib/action_view/template/error.rb
+++ b/actionview/lib/action_view/template/error.rb
@@ -75,7 +75,7 @@ module ActionView
def sub_template_message
if @sub_templates
"Trace of template inclusion: " +
- @sub_templates.collect { |template| template.inspect }.join(", ")
+ @sub_templates.collect(&:inspect).join(", ")
else
""
end
diff --git a/actionview/lib/action_view/template/handlers.rb b/actionview/lib/action_view/template/handlers.rb
index 33bfcb458c..0105e88a49 100644
--- a/actionview/lib/action_view/template/handlers.rb
+++ b/actionview/lib/action_view/template/handlers.rb
@@ -7,9 +7,9 @@ module ActionView #:nodoc:
autoload :Raw, 'action_view/template/handlers/raw'
def self.extended(base)
- base.register_default_template_handler :erb, ERB.new
+ base.register_default_template_handler :raw, Raw.new
+ base.register_template_handler :erb, ERB.new
base.register_template_handler :builder, Builder.new
- base.register_template_handler :raw, Raw.new
base.register_template_handler :ruby, :source.to_proc
end
@@ -22,7 +22,7 @@ module ActionView #:nodoc:
# Register an object that knows how to handle template files with the given
# extensions. This can be used to implement new template types.
- # The handler must respond to `:call`, which will be passed the template
+ # The handler must respond to +:call+, which will be passed the template
# and should return the rendered template as a String.
def register_template_handler(*extensions, handler)
raise(ArgumentError, "Extension is required") if extensions.empty?
@@ -42,7 +42,7 @@ module ActionView #:nodoc:
end
def template_handler_extensions
- @@template_handlers.keys.map {|key| key.to_s }.sort
+ @@template_handlers.keys.map(&:to_s).sort
end
def registered_template_handler(extension)
diff --git a/actionview/lib/action_view/template/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb
index 4523060442..1f8459c24b 100644
--- a/actionview/lib/action_view/template/handlers/erb.rb
+++ b/actionview/lib/action_view/template/handlers/erb.rb
@@ -35,7 +35,7 @@ module ActionView
end
end
- BLOCK_EXPR = /\s+(do|\{)(\s*\|[^|]*\|)?\s*\Z/
+ BLOCK_EXPR = /\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/
def add_expr_literal(src, code)
flush_newline_if_pending(src)
@@ -49,9 +49,9 @@ module ActionView
def add_expr_escaped(src, code)
flush_newline_if_pending(src)
if code =~ BLOCK_EXPR
- src << "@output_buffer.safe_append= " << code
+ src << "@output_buffer.safe_expr_append= " << code
else
- src << "@output_buffer.safe_append=(" << code << ");"
+ src << "@output_buffer.safe_expr_append=(" << code << ");"
end
end
@@ -123,6 +123,31 @@ module ActionView
).src
end
+ # Returns Regexp to extract a cached resource's name from a cache call at the
+ # first line of a template.
+ # The extracted cache name is captured as :resource_name.
+ #
+ # <% cache notification do %> # => notification
+ #
+ # The pattern should support templates with a beginning comment:
+ #
+ # <%# Still extractable even though there's a comment %>
+ # <% cache notification do %> # => notification
+ #
+ # But fail to extract a name if a resource association is cached.
+ #
+ # <% cache notification.event do %> # => nil
+ def resource_cache_call_pattern
+ /\A
+ (?:<%\#.*%>)* # optional initial comment
+ \s* # followed by optional spaces or newlines
+ <%\s*cache[\(\s] # followed by an ERB call to cache
+ \s* # followed by optional spaces or newlines
+ (?<resource_name>\w+) # capture the cache call argument as :resource_name
+ [\s\)] # followed by a space or close paren
+ /xm
+ end
+
private
def valid_encoding(string, encoding)
diff --git a/actionview/lib/action_view/template/handlers/raw.rb b/actionview/lib/action_view/template/handlers/raw.rb
index 0c0d1fffcb..b08fb0870f 100644
--- a/actionview/lib/action_view/template/handlers/raw.rb
+++ b/actionview/lib/action_view/template/handlers/raw.rb
@@ -2,7 +2,7 @@ module ActionView
module Template::Handlers
class Raw
def call(template)
- escaped = template.source.gsub(':', '\:')
+ escaped = template.source.gsub(':'.freeze, '\:'.freeze)
'%q:' + escaped + ':;'
end
diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb
index d29d020c17..7859c58b43 100644
--- a/actionview/lib/action_view/template/resolver.rb
+++ b/actionview/lib/action_view/template/resolver.rb
@@ -3,7 +3,7 @@ require "active_support/core_ext/class"
require "active_support/core_ext/module/attribute_accessors"
require "action_view/template"
require "thread"
-require "thread_safe"
+require "concurrent"
module ActionView
# = Action View Resolver
@@ -35,7 +35,7 @@ module ActionView
# Threadsafe template cache
class Cache #:nodoc:
- class SmallCache < ThreadSafe::Cache
+ class SmallCache < Concurrent::Map
def initialize(options = {})
super(options.merge(:initial_capacity => 2))
end
@@ -52,6 +52,7 @@ module ActionView
def initialize
@data = SmallCache.new(&KEY_BLOCK)
+ @query_cache = SmallCache.new
end
# Cache the templates returned by the block
@@ -70,8 +71,17 @@ module ActionView
end
end
+ def cache_query(query) # :nodoc:
+ if Resolver.caching?
+ @query_cache[query] ||= canonical_no_templates(yield)
+ else
+ yield
+ end
+ end
+
def clear
@data.clear
+ @query_cache.clear
end
private
@@ -116,6 +126,10 @@ module ActionView
end
end
+ def find_all_with_query(query) # :nodoc:
+ @cache.cache_query(query) { find_template_paths(File.join(@path, query)) }
+ end
+
private
delegate :caching?, to: :class
@@ -138,7 +152,7 @@ module ActionView
# resolver is fresher before returning it.
def cached(key, path_info, details, locals) #:nodoc:
name, prefix, partial = path_info
- locals = locals.map { |x| x.to_s }.sort!
+ locals = locals.map(&:to_s).sort!
if key
@cache.cache(key, name, prefix, partial, locals) do
@@ -181,9 +195,9 @@ module ActionView
def query(path, details, formats)
query = build_query(path, details)
- template_paths = find_template_paths query
+ template_paths = find_template_paths(query)
- template_paths.map { |template|
+ template_paths.map do |template|
handler, format, variant = extract_handler_and_format_and_variant(template, formats)
contents = File.binread(template)
@@ -193,26 +207,14 @@ module ActionView
:variant => variant,
:updated_at => mtime(template)
)
- }
+ end
end
- if RUBY_VERSION >= '2.2.0'
- def find_template_paths(query)
- Dir[query].reject { |filename|
- File.directory?(filename) ||
- # deals with case-insensitive file systems.
- !File.fnmatch(query, filename, File::FNM_EXTGLOB)
- }
- end
- else
- def find_template_paths(query)
- # deals with case-insensitive file systems.
- sanitizer = Hash.new { |h,dir| h[dir] = Dir["#{dir}/*"] }
-
- Dir[query].reject { |filename|
- File.directory?(filename) ||
- !sanitizer[File.dirname(filename)].include?(filename)
- }
+ def find_template_paths(query)
+ Dir[query].reject do |filename|
+ File.directory?(filename) ||
+ # deals with case-insensitive file systems.
+ !File.fnmatch(query, filename, File::FNM_EXTGLOB)
end
end
@@ -220,21 +222,21 @@ module ActionView
def build_query(path, details)
query = @pattern.dup
- prefix = path.prefix.empty? ? "" : "#{escape_entry(path.prefix)}\\1"
- query.gsub!(/\:prefix(\/)?/, prefix)
+ prefix = path.prefix.empty? ? '' : "#{escape_entry(path.prefix)}\\1"
+ query.gsub!(/:prefix(\/)?/, prefix)
partial = escape_entry(path.partial? ? "_#{path.name}" : path.name)
- query.gsub!(/\:action/, partial)
+ query.gsub!(/:action/, partial)
details.each do |ext, variants|
- query.gsub!(/\:#{ext}/, "{#{variants.compact.uniq.join(',')}}")
+ query.gsub!(/:#{ext}/, "{#{variants.compact.uniq.join(',')}}")
end
File.expand_path(query, @path)
end
def escape_entry(entry)
- entry.gsub(/[*?{}\[\]]/, '\\\\\\&')
+ entry.gsub(/[*?{}\[\]]/, '\\\\\\&'.freeze)
end
# Returns the file mtime from the filesystem.
@@ -246,15 +248,10 @@ module ActionView
# from the path, or the handler, we should return the array of formats given
# to the resolver.
def extract_handler_and_format_and_variant(path, default_formats)
- pieces = File.basename(path).split(".")
+ pieces = File.basename(path).split('.'.freeze)
pieces.shift
extension = pieces.pop
- unless extension
- message = "The file #{path} did not specify a template handler. The default is currently ERB, " \
- "but will change to RAW in the future."
- ActiveSupport::Deprecation.warn message
- end
handler = Template.handler_for_extension(extension)
format, variant = pieces.last.split(EXTENSIONS[:variants], 2) if pieces.last
@@ -272,13 +269,13 @@ module ActionView
# Default pattern, loads views the same way as previous versions of rails, eg. when you're
# looking for `users/new` it will produce query glob: `users/new{.{en},}{.{html,js},}{.{erb,haml},}`
#
- # FileSystemResolver.new("/path/to/views", ":prefix/:action{.:locale,}{.:formats,}{.:handlers,}")
+ # FileSystemResolver.new("/path/to/views", ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}")
#
# This one allows you to keep files with different formats in separate subdirectories,
# eg. `users/new.html` will be loaded from `users/html/new.erb` or `users/new.html.erb`,
# `users/new.js` from `users/js/new.erb` or `users/new.js.erb`, etc.
#
- # FileSystemResolver.new("/path/to/views", ":prefix/{:formats/,}:action{.:locale,}{.:formats,}{.:handlers,}")
+ # FileSystemResolver.new("/path/to/views", ":prefix/{:formats/,}:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}")
#
# If you don't specify a pattern then the default will be used.
#
@@ -287,7 +284,7 @@ module ActionView
#
# ActionController::Base.view_paths = FileSystemResolver.new(
# Rails.root.join("app/views"),
- # ":prefix{/:locale}/:action{.:formats,}{.:handlers,}"
+ # ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}",
# )
#
# ==== Pattern format and variables
@@ -299,6 +296,7 @@ module ActionView
# * <tt>:action</tt> - name of the action
# * <tt>:locale</tt> - possible locale versions
# * <tt>:formats</tt> - possible request formats (for example html, json, xml...)
+ # * <tt>:variants</tt> - possible request variants (for example phone, tablet...)
# * <tt>:handlers</tt> - possible handlers (for example erb, haml, builder...)
#
class FileSystemResolver < PathResolver
diff --git a/actionview/lib/action_view/template/types.rb b/actionview/lib/action_view/template/types.rb
index b84e0281ae..be45fcf742 100644
--- a/actionview/lib/action_view/template/types.rb
+++ b/actionview/lib/action_view/template/types.rb
@@ -9,7 +9,7 @@ module ActionView
self.types = Set.new
def self.register(*t)
- types.merge(t.map { |type| type.to_s })
+ types.merge(t.map(&:to_s))
end
register :html, :text, :js, :css, :xml, :json
diff --git a/actionview/lib/action_view/test_case.rb b/actionview/lib/action_view/test_case.rb
index d0da415c5d..f6b5696a13 100644
--- a/actionview/lib/action_view/test_case.rb
+++ b/actionview/lib/action_view/test_case.rb
@@ -3,6 +3,8 @@ require 'action_controller'
require 'action_controller/test_case'
require 'action_view'
+require 'rails-dom-testing'
+
module ActionView
# = Action View Test Case
class TestCase < ActiveSupport::TestCase
@@ -22,8 +24,8 @@ module ActionView
def initialize
super
self.class.controller_path = ""
- @request = ActionController::TestRequest.new
- @response = ActionController::TestResponse.new
+ @request = ActionController::TestRequest.create
+ @response = ActionDispatch::TestResponse.new
@request.env.delete('PATH_INFO')
@params = {}
@@ -34,6 +36,7 @@ module ActionView
extend ActiveSupport::Concern
include ActionDispatch::Assertions, ActionDispatch::TestProcess
+ include Rails::Dom::Testing::Assertions
include ActionController::TemplateAssertions
include ActionView::Context
@@ -99,7 +102,9 @@ module ActionView
def setup_with_controller
@controller = ActionView::TestCase::TestController.new
@request = @controller.request
- @output_buffer = ActiveSupport::SafeBuffer.new
+ # empty string ensures buffer has UTF-8 encoding as
+ # new without arguments returns ASCII-8BIT encoded buffer like String#new
+ @output_buffer = ActiveSupport::SafeBuffer.new ''
@rendered = ''
make_test_case_available_to_view!
@@ -120,6 +125,7 @@ module ActionView
@_rendered_views ||= RenderedViewsCollection.new
end
+ # Need to experiment if this priority is the best one: rendered => output_buffer
class RenderedViewsCollection
def initialize
@rendered_views ||= Hash.new { |hash, key| hash[key] = [] }
@@ -151,11 +157,9 @@ module ActionView
private
- # Support the selector assertions
- #
# Need to experiment if this priority is the best one: rendered => output_buffer
- def response_from_page
- HTML::Document.new(@rendered.blank? ? @output_buffer : @rendered).root
+ def document_root_element
+ Nokogiri::HTML::Document.parse(@rendered.blank? ? @output_buffer : @rendered).root
end
def say_no_to_protect_against_forgery!
@@ -200,7 +204,7 @@ module ActionView
def view
@view ||= begin
view = @controller.view_context
- view.singleton_class.send :include, _helpers
+ view.singleton_class.include(_helpers)
view.extend(Locals)
view.rendered_views = self.rendered_views
view.output_buffer = self.output_buffer
@@ -236,7 +240,8 @@ module ActionView
:@test_passed,
:@view,
:@view_context_class,
- :@_subscribers
+ :@_subscribers,
+ :@html_document
]
def _user_defined_ivars
@@ -258,9 +263,15 @@ module ActionView
end
def method_missing(selector, *args)
- if @controller.respond_to?(:_routes) &&
- ( @controller._routes.named_routes.route_defined?(selector) ||
- @controller._routes.mounted_helpers.method_defined?(selector) )
+ begin
+ routes = @controller.respond_to?(:_routes) && @controller._routes
+ rescue
+ # Dont call routes, if there is an error on _routes call
+ end
+
+ if routes &&
+ ( routes.named_routes.route_defined?(selector) ||
+ routes.mounted_helpers.method_defined?(selector) )
@controller.__send__(selector, *args)
else
super
diff --git a/actionview/lib/action_view/vendor/html-scanner.rb b/actionview/lib/action_view/vendor/html-scanner.rb
deleted file mode 100644
index 775b827529..0000000000
--- a/actionview/lib/action_view/vendor/html-scanner.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/html-scanner"
-
-module HTML
- extend ActiveSupport::Autoload
-
- eager_autoload do
- autoload :CDATA, 'html/node'
- autoload :Document, 'html/document'
- autoload :FullSanitizer, 'html/sanitizer'
- autoload :LinkSanitizer, 'html/sanitizer'
- autoload :Node, 'html/node'
- autoload :Sanitizer, 'html/sanitizer'
- autoload :Selector, 'html/selector'
- autoload :Tag, 'html/node'
- autoload :Text, 'html/node'
- autoload :Tokenizer, 'html/tokenizer'
- autoload :Version, 'html/version'
- autoload :WhiteListSanitizer, 'html/sanitizer'
- end
-end
diff --git a/actionview/lib/action_view/vendor/html-scanner/html/document.rb b/actionview/lib/action_view/vendor/html-scanner/html/document.rb
deleted file mode 100644
index 386820300a..0000000000
--- a/actionview/lib/action_view/vendor/html-scanner/html/document.rb
+++ /dev/null
@@ -1,68 +0,0 @@
-require 'html/tokenizer'
-require 'html/node'
-require 'html/selector'
-require 'html/sanitizer'
-
-module HTML #:nodoc:
- # A top-level HTML document. You give it a body of text, and it will parse that
- # text into a tree of nodes.
- class Document #:nodoc:
-
- # The root of the parsed document.
- attr_reader :root
-
- # Create a new Document from the given text.
- def initialize(text, strict=false, xml=false)
- tokenizer = Tokenizer.new(text)
- @root = Node.new(nil)
- node_stack = [ @root ]
- while token = tokenizer.next
- node = Node.parse(node_stack.last, tokenizer.line, tokenizer.position, token, strict)
-
- node_stack.last.children << node unless node.tag? && node.closing == :close
- if node.tag?
- if node_stack.length > 1 && node.closing == :close
- if node_stack.last.name == node.name
- if node_stack.last.children.empty?
- node_stack.last.children << Text.new(node_stack.last, node.line, node.position, "")
- end
- node_stack.pop
- else
- open_start = node_stack.last.position - 20
- open_start = 0 if open_start < 0
- close_start = node.position - 20
- close_start = 0 if close_start < 0
- msg = <<EOF.strip
-ignoring attempt to close #{node_stack.last.name} with #{node.name}
- opened at byte #{node_stack.last.position}, line #{node_stack.last.line}
- closed at byte #{node.position}, line #{node.line}
- attributes at open: #{node_stack.last.attributes.inspect}
- text around open: #{text[open_start,40].inspect}
- text around close: #{text[close_start,40].inspect}
-EOF
- strict ? raise(msg) : warn(msg)
- end
- elsif !node.childless?(xml) && node.closing != :close
- node_stack.push node
- end
- end
- end
- end
-
- # Search the tree for (and return) the first node that matches the given
- # conditions. The conditions are interpreted differently for different node
- # types, see HTML::Text#find and HTML::Tag#find.
- def find(conditions)
- @root.find(conditions)
- end
-
- # Search the tree for (and return) all nodes that match the given
- # conditions. The conditions are interpreted differently for different node
- # types, see HTML::Text#find and HTML::Tag#find.
- def find_all(conditions)
- @root.find_all(conditions)
- end
-
- end
-
-end
diff --git a/actionview/lib/action_view/vendor/html-scanner/html/node.rb b/actionview/lib/action_view/vendor/html-scanner/html/node.rb
deleted file mode 100644
index 27f0f2f6f8..0000000000
--- a/actionview/lib/action_view/vendor/html-scanner/html/node.rb
+++ /dev/null
@@ -1,532 +0,0 @@
-require 'strscan'
-
-module HTML #:nodoc:
-
- class Conditions < Hash #:nodoc:
- def initialize(hash)
- super()
- hash = { :content => hash } unless Hash === hash
- hash = keys_to_symbols(hash)
- hash.each do |k,v|
- case k
- when :tag, :content then
- # keys are valid, and require no further processing
- when :attributes then
- hash[k] = keys_to_strings(v)
- when :parent, :child, :ancestor, :descendant, :sibling, :before,
- :after
- hash[k] = Conditions.new(v)
- when :children
- hash[k] = v = keys_to_symbols(v)
- v.each do |key,value|
- case key
- when :count, :greater_than, :less_than
- # keys are valid, and require no further processing
- when :only
- v[key] = Conditions.new(value)
- else
- raise "illegal key #{key.inspect} => #{value.inspect}"
- end
- end
- else
- raise "illegal key #{k.inspect} => #{v.inspect}"
- end
- end
- update hash
- end
-
- private
-
- def keys_to_strings(hash)
- Hash[hash.keys.map {|k| [k.to_s, hash[k]]}]
- end
-
- def keys_to_symbols(hash)
- Hash[hash.keys.map do |k|
- raise "illegal key #{k.inspect}" unless k.respond_to?(:to_sym)
- [k.to_sym, hash[k]]
- end]
- end
- end
-
- # The base class of all nodes, textual and otherwise, in an HTML document.
- class Node #:nodoc:
- # The array of children of this node. Not all nodes have children.
- attr_reader :children
-
- # The parent node of this node. All nodes have a parent, except for the
- # root node.
- attr_reader :parent
-
- # The line number of the input where this node was begun
- attr_reader :line
-
- # The byte position in the input where this node was begun
- attr_reader :position
-
- # Create a new node as a child of the given parent.
- def initialize(parent, line=0, pos=0)
- @parent = parent
- @children = []
- @line, @position = line, pos
- end
-
- # Returns a textual representation of the node.
- def to_s
- @children.join()
- end
-
- # Returns false (subclasses must override this to provide specific matching
- # behavior.) +conditions+ may be of any type.
- def match(conditions)
- false
- end
-
- # Search the children of this node for the first node for which #find
- # returns non +nil+. Returns the result of the #find call that succeeded.
- def find(conditions)
- conditions = validate_conditions(conditions)
- @children.each do |child|
- node = child.find(conditions)
- return node if node
- end
- nil
- end
-
- # Search for all nodes that match the given conditions, and return them
- # as an array.
- def find_all(conditions)
- conditions = validate_conditions(conditions)
-
- matches = []
- matches << self if match(conditions)
- @children.each do |child|
- matches.concat child.find_all(conditions)
- end
- matches
- end
-
- # Returns +false+. Subclasses may override this if they define a kind of
- # tag.
- def tag?
- false
- end
-
- def validate_conditions(conditions)
- Conditions === conditions ? conditions : Conditions.new(conditions)
- end
-
- def ==(node)
- return false unless self.class == node.class && children.size == node.children.size
-
- equivalent = true
-
- children.size.times do |i|
- equivalent &&= children[i] == node.children[i]
- end
-
- equivalent
- end
-
- class <<self
- def parse(parent, line, pos, content, strict=true)
- if content !~ /^<\S/
- Text.new(parent, line, pos, content)
- else
- scanner = StringScanner.new(content)
-
- unless scanner.skip(/</)
- if strict
- raise "expected <"
- else
- return Text.new(parent, line, pos, content)
- end
- end
-
- if scanner.skip(/!\[CDATA\[/)
- unless scanner.skip_until(/\]\]>/)
- if strict
- raise "expected ]]> (got #{scanner.rest.inspect} for #{content})"
- else
- scanner.skip_until(/\Z/)
- end
- end
-
- return CDATA.new(parent, line, pos, scanner.pre_match.gsub(/<!\[CDATA\[/, ''))
- end
-
- closing = ( scanner.scan(/\//) ? :close : nil )
- return Text.new(parent, line, pos, content) unless name = scanner.scan(/[^\s!>\/]+/)
- name.downcase!
-
- unless closing
- scanner.skip(/\s*/)
- attributes = {}
- while attr = scanner.scan(/[-\w:]+/)
- value = true
- if scanner.scan(/\s*=\s*/)
- if delim = scanner.scan(/['"]/)
- value = ""
- while text = scanner.scan(/[^#{delim}\\]+|./)
- case text
- when "\\" then
- value << text
- break if scanner.eos?
- value << scanner.getch
- when delim
- break
- else value << text
- end
- end
- else
- value = scanner.scan(/[^\s>\/]+/)
- end
- end
- attributes[attr.downcase] = value
- scanner.skip(/\s*/)
- end
-
- closing = ( scanner.scan(/\//) ? :self : nil )
- end
-
- unless scanner.scan(/\s*>/)
- if strict
- raise "expected > (got #{scanner.rest.inspect} for #{content}, #{attributes.inspect})"
- else
- # throw away all text until we find what we're looking for
- scanner.skip_until(/>/) or scanner.terminate
- end
- end
-
- Tag.new(parent, line, pos, name, attributes, closing)
- end
- end
- end
- end
-
- # A node that represents text, rather than markup.
- class Text < Node #:nodoc:
-
- attr_reader :content
-
- # Creates a new text node as a child of the given parent, with the given
- # content.
- def initialize(parent, line, pos, content)
- super(parent, line, pos)
- @content = content
- end
-
- # Returns the content of this node.
- def to_s
- @content
- end
-
- # Returns +self+ if this node meets the given conditions. Text nodes support
- # conditions of the following kinds:
- #
- # * if +conditions+ is a string, it must be a substring of the node's
- # content
- # * if +conditions+ is a regular expression, it must match the node's
- # content
- # * if +conditions+ is a hash, it must contain a <tt>:content</tt> key that
- # is either a string or a regexp, and which is interpreted as described
- # above.
- def find(conditions)
- match(conditions) && self
- end
-
- # Returns non-+nil+ if this node meets the given conditions, or +nil+
- # otherwise. See the discussion of #find for the valid conditions.
- def match(conditions)
- case conditions
- when String
- @content == conditions
- when Regexp
- @content =~ conditions
- when Hash
- conditions = validate_conditions(conditions)
-
- # Text nodes only have :content, :parent, :ancestor
- unless (conditions.keys - [:content, :parent, :ancestor]).empty?
- return false
- end
-
- match(conditions[:content])
- else
- nil
- end
- end
-
- def ==(node)
- return false unless super
- content == node.content
- end
- end
-
- # A CDATA node is simply a text node with a specialized way of displaying
- # itself.
- class CDATA < Text #:nodoc:
- def to_s
- "<![CDATA[#{super}]]>"
- end
- end
-
- # A Tag is any node that represents markup. It may be an opening tag, a
- # closing tag, or a self-closing tag. It has a name, and may have a hash of
- # attributes.
- class Tag < Node #:nodoc:
-
- # Either +nil+, <tt>:close</tt>, or <tt>:self</tt>
- attr_reader :closing
-
- # Either +nil+, or a hash of attributes for this node.
- attr_reader :attributes
-
- # The name of this tag.
- attr_reader :name
-
- # Create a new node as a child of the given parent, using the given content
- # to describe the node. It will be parsed and the node name, attributes and
- # closing status extracted.
- def initialize(parent, line, pos, name, attributes, closing)
- super(parent, line, pos)
- @name = name
- @attributes = attributes
- @closing = closing
- end
-
- # A convenience for obtaining an attribute of the node. Returns +nil+ if
- # the node has no attributes.
- def [](attr)
- @attributes ? @attributes[attr] : nil
- end
-
- # Returns non-+nil+ if this tag can contain child nodes.
- def childless?(xml = false)
- return false if xml && @closing.nil?
- !@closing.nil? ||
- @name =~ /^(img|br|hr|link|meta|area|base|basefont|
- col|frame|input|isindex|param)$/ox
- end
-
- # Returns a textual representation of the node
- def to_s
- if @closing == :close
- "</#{@name}>"
- else
- s = "<#{@name}"
- @attributes.each do |k,v|
- s << " #{k}"
- s << "=\"#{v}\"" if String === v
- end
- s << " /" if @closing == :self
- s << ">"
- @children.each { |child| s << child.to_s }
- s << "</#{@name}>" if @closing != :self && !@children.empty?
- s
- end
- end
-
- # If either the node or any of its children meet the given conditions, the
- # matching node is returned. Otherwise, +nil+ is returned. (See the
- # description of the valid conditions in the +match+ method.)
- def find(conditions)
- match(conditions) && self || super
- end
-
- # Returns +true+, indicating that this node represents an HTML tag.
- def tag?
- true
- end
-
- # Returns +true+ if the node meets any of the given conditions. The
- # +conditions+ parameter must be a hash of any of the following keys
- # (all are optional):
- #
- # * <tt>:tag</tt>: the node name must match the corresponding value
- # * <tt>:attributes</tt>: a hash. The node's values must match the
- # corresponding values in the hash.
- # * <tt>:parent</tt>: a hash. The node's parent must match the
- # corresponding hash.
- # * <tt>:child</tt>: a hash. At least one of the node's immediate children
- # must meet the criteria described by the hash.
- # * <tt>:ancestor</tt>: a hash. At least one of the node's ancestors must
- # meet the criteria described by the hash.
- # * <tt>:descendant</tt>: a hash. At least one of the node's descendants
- # must meet the criteria described by the hash.
- # * <tt>:sibling</tt>: a hash. At least one of the node's siblings must
- # meet the criteria described by the hash.
- # * <tt>:after</tt>: a hash. The node must be after any sibling meeting
- # the criteria described by the hash, and at least one sibling must match.
- # * <tt>:before</tt>: a hash. The node must be before any sibling meeting
- # the criteria described by the hash, and at least one sibling must match.
- # * <tt>:children</tt>: a hash, for counting children of a node. Accepts the
- # keys:
- # ** <tt>:count</tt>: either a number or a range which must equal (or
- # include) the number of children that match.
- # ** <tt>:less_than</tt>: the number of matching children must be less than
- # this number.
- # ** <tt>:greater_than</tt>: the number of matching children must be
- # greater than this number.
- # ** <tt>:only</tt>: another hash consisting of the keys to use
- # to match on the children, and only matching children will be
- # counted.
- #
- # Conditions are matched using the following algorithm:
- #
- # * if the condition is a string, it must be a substring of the value.
- # * if the condition is a regexp, it must match the value.
- # * if the condition is a number, the value must match number.to_s.
- # * if the condition is +true+, the value must not be +nil+.
- # * if the condition is +false+ or +nil+, the value must be +nil+.
- #
- # Usage:
- #
- # # test if the node is a "span" tag
- # node.match tag: "span"
- #
- # # test if the node's parent is a "div"
- # node.match parent: { tag: "div" }
- #
- # # test if any of the node's ancestors are "table" tags
- # node.match ancestor: { tag: "table" }
- #
- # # test if any of the node's immediate children are "em" tags
- # node.match child: { tag: "em" }
- #
- # # test if any of the node's descendants are "strong" tags
- # node.match descendant: { tag: "strong" }
- #
- # # test if the node has between 2 and 4 span tags as immediate children
- # node.match children: { count: 2..4, only: { tag: "span" } }
- #
- # # get funky: test to see if the node is a "div", has a "ul" ancestor
- # # and an "li" parent (with "class" = "enum"), and whether or not it has
- # # a "span" descendant that contains # text matching /hello world/:
- # node.match tag: "div",
- # ancestor: { tag: "ul" },
- # parent: { tag: "li",
- # attributes: { class: "enum" } },
- # descendant: { tag: "span",
- # child: /hello world/ }
- def match(conditions)
- conditions = validate_conditions(conditions)
- # check content of child nodes
- if conditions[:content]
- if children.empty?
- return false unless match_condition("", conditions[:content])
- else
- return false unless children.find { |child| child.match(conditions[:content]) }
- end
- end
-
- # test the name
- return false unless match_condition(@name, conditions[:tag]) if conditions[:tag]
-
- # test attributes
- (conditions[:attributes] || {}).each do |key, value|
- return false unless match_condition(self[key], value)
- end
-
- # test parent
- return false unless parent.match(conditions[:parent]) if conditions[:parent]
-
- # test children
- return false unless children.find { |child| child.match(conditions[:child]) } if conditions[:child]
-
- # test ancestors
- if conditions[:ancestor]
- return false unless catch :found do
- p = self
- throw :found, true if p.match(conditions[:ancestor]) while p = p.parent
- end
- end
-
- # test descendants
- if conditions[:descendant]
- return false unless children.find do |child|
- # test the child
- child.match(conditions[:descendant]) ||
- # test the child's descendants
- child.match(:descendant => conditions[:descendant])
- end
- end
-
- # count children
- if opts = conditions[:children]
- matches = children.select do |c|
- (c.kind_of?(HTML::Tag) and (c.closing == :self or ! c.childless?))
- end
-
- matches = matches.select { |c| c.match(opts[:only]) } if opts[:only]
- opts.each do |key, value|
- next if key == :only
- case key
- when :count
- if Integer === value
- return false if matches.length != value
- else
- return false unless value.include?(matches.length)
- end
- when :less_than
- return false unless matches.length < value
- when :greater_than
- return false unless matches.length > value
- else raise "unknown count condition #{key}"
- end
- end
- end
-
- # test siblings
- if conditions[:sibling] || conditions[:before] || conditions[:after]
- siblings = parent ? parent.children : []
- self_index = siblings.index(self)
-
- if conditions[:sibling]
- return false unless siblings.detect do |s|
- s != self && s.match(conditions[:sibling])
- end
- end
-
- if conditions[:before]
- return false unless siblings[self_index+1..-1].detect do |s|
- s != self && s.match(conditions[:before])
- end
- end
-
- if conditions[:after]
- return false unless siblings[0,self_index].detect do |s|
- s != self && s.match(conditions[:after])
- end
- end
- end
-
- true
- end
-
- def ==(node)
- return false unless super
- return false unless closing == node.closing && self.name == node.name
- attributes == node.attributes
- end
-
- private
- # Match the given value to the given condition.
- def match_condition(value, condition)
- case condition
- when String
- value && value == condition
- when Regexp
- value && value.match(condition)
- when Numeric
- value == condition.to_s
- when true
- !value.nil?
- when false, nil
- value.nil?
- else
- false
- end
- end
- end
-end
diff --git a/actionview/lib/action_view/vendor/html-scanner/html/sanitizer.rb b/actionview/lib/action_view/vendor/html-scanner/html/sanitizer.rb
deleted file mode 100644
index ed34eecf55..0000000000
--- a/actionview/lib/action_view/vendor/html-scanner/html/sanitizer.rb
+++ /dev/null
@@ -1,188 +0,0 @@
-require 'set'
-require 'cgi'
-require 'active_support/core_ext/module/attribute_accessors'
-
-module HTML
- class Sanitizer
- def sanitize(text, options = {})
- validate_options(options)
- return text unless sanitizeable?(text)
- tokenize(text, options).join
- end
-
- def sanitizeable?(text)
- !(text.nil? || text.empty? || !text.index("<"))
- end
-
- protected
- def tokenize(text, options)
- tokenizer = HTML::Tokenizer.new(text)
- result = []
- while token = tokenizer.next
- node = Node.parse(nil, 0, 0, token, false)
- process_node node, result, options
- end
- result
- end
-
- def process_node(node, result, options)
- result << node.to_s
- end
-
- def validate_options(options)
- if options[:tags] && !options[:tags].is_a?(Enumerable)
- raise ArgumentError, "You should pass :tags as an Enumerable"
- end
-
- if options[:attributes] && !options[:attributes].is_a?(Enumerable)
- raise ArgumentError, "You should pass :attributes as an Enumerable"
- end
- end
- end
-
- class FullSanitizer < Sanitizer
- def sanitize(text, options = {})
- result = super
- # strip any comments, and if they have a newline at the end (ie. line with
- # only a comment) strip that too
- result = result.gsub(/<!--(.*?)-->[\n]?/m, "") if (result && result =~ /<!--(.*?)-->[\n]?/m)
- # Recurse - handle all dirty nested tags
- result == text ? result : sanitize(result, options)
- end
-
- def process_node(node, result, options)
- result << node.to_s if node.class == HTML::Text
- end
- end
-
- class LinkSanitizer < FullSanitizer
- cattr_accessor :included_tags, :instance_writer => false
- self.included_tags = Set.new(%w(a href))
-
- def sanitizeable?(text)
- !(text.nil? || text.empty? || !((text.index("<a") || text.index("<href")) && text.index(">")))
- end
-
- protected
- def process_node(node, result, options)
- result << node.to_s unless node.is_a?(HTML::Tag) && included_tags.include?(node.name)
- end
- end
-
- class WhiteListSanitizer < Sanitizer
- [:protocol_separator, :uri_attributes, :allowed_attributes, :allowed_tags, :allowed_protocols, :bad_tags,
- :allowed_css_properties, :allowed_css_keywords, :shorthand_css_properties].each do |attr|
- class_attribute attr, :instance_writer => false
- end
-
- # A regular expression of the valid characters used to separate protocols like
- # the ':' in 'http://foo.com'
- self.protocol_separator = /:|(&#0*58)|(&#x70)|(&#x0*3a)|(%|&#37;)3A/i
-
- # Specifies a Set of HTML attributes that can have URIs.
- self.uri_attributes = Set.new(%w(href src cite action longdesc xlink:href lowsrc))
-
- # Specifies a Set of 'bad' tags that the #sanitize helper will remove completely, as opposed
- # to just escaping harmless tags like &lt;font&gt;
- self.bad_tags = Set.new(%w(script))
-
- # Specifies the default Set of tags that the #sanitize helper will allow unscathed.
- self.allowed_tags = Set.new(%w(strong em b i p code pre tt samp kbd var sub
- sup dfn cite big small address hr br div span h1 h2 h3 h4 h5 h6 ul ol li dl dt dd abbr
- acronym a img blockquote del ins))
-
- # Specifies the default Set of html attributes that the #sanitize helper will leave
- # in the allowed tag.
- self.allowed_attributes = Set.new(%w(href src width height alt cite datetime title class name xml:lang abbr))
-
- # Specifies the default Set of acceptable css properties that #sanitize and #sanitize_css will accept.
- self.allowed_protocols = Set.new(%w(ed2k ftp http https irc mailto news gopher nntp telnet webcal xmpp callto
- feed svn urn aim rsync tag ssh sftp rtsp afs))
-
- # Specifies the default Set of acceptable css properties that #sanitize and #sanitize_css will accept.
- self.allowed_css_properties = Set.new(%w(azimuth background-color border-bottom-color border-collapse
- border-color border-left-color border-right-color border-top-color clear color cursor direction display
- elevation float font font-family font-size font-style font-variant font-weight height letter-spacing line-height
- overflow pause pause-after pause-before pitch pitch-range richness speak speak-header speak-numeral speak-punctuation
- speech-rate stress text-align text-decoration text-indent unicode-bidi vertical-align voice-family volume white-space
- width))
-
- # Specifies the default Set of acceptable css keywords that #sanitize and #sanitize_css will accept.
- self.allowed_css_keywords = Set.new(%w(auto aqua black block blue bold both bottom brown center
- collapse dashed dotted fuchsia gray green !important italic left lime maroon medium none navy normal
- nowrap olive pointer purple red right solid silver teal top transparent underline white yellow))
-
- # Specifies the default Set of allowed shorthand css properties for the #sanitize and #sanitize_css helpers.
- self.shorthand_css_properties = Set.new(%w(background border margin padding))
-
- # Sanitizes a block of css code. Used by #sanitize when it comes across a style attribute
- def sanitize_css(style)
- # disallow urls
- style = style.to_s.gsub(/url\s*\(\s*[^\s)]+?\s*\)\s*/, ' ')
-
- # gauntlet
- if style !~ /\A([:,;#%.\sa-zA-Z0-9!]|\w-\w|\'[\s\w]+\'|\"[\s\w]+\"|\([\d,\s]+\))*\z/ ||
- style !~ /\A(\s*[-\w]+\s*:\s*[^:;]*(;|$)\s*)*\z/
- return ''
- end
-
- clean = []
- style.scan(/([-\w]+)\s*:\s*([^:;]*)/) do |prop,val|
- if allowed_css_properties.include?(prop.downcase)
- clean << prop + ': ' + val + ';'
- elsif shorthand_css_properties.include?(prop.split('-')[0].downcase)
- unless val.split().any? do |keyword|
- !allowed_css_keywords.include?(keyword) &&
- keyword !~ /\A(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)\z/
- end
- clean << prop + ': ' + val + ';'
- end
- end
- end
- clean.join(' ')
- end
-
- protected
- def tokenize(text, options)
- options[:parent] = []
- options[:attributes] ||= allowed_attributes
- options[:tags] ||= allowed_tags
- super
- end
-
- def process_node(node, result, options)
- result << case node
- when HTML::Tag
- if node.closing == :close
- options[:parent].shift
- else
- options[:parent].unshift node.name
- end
-
- process_attributes_for node, options
-
- options[:tags].include?(node.name) ? node : nil
- else
- bad_tags.include?(options[:parent].first) ? nil : node.to_s.gsub(/</, "&lt;")
- end
- end
-
- def process_attributes_for(node, options)
- return unless node.attributes
- node.attributes.keys.each do |attr_name|
- value = node.attributes[attr_name].to_s
-
- if !options[:attributes].include?(attr_name) || contains_bad_protocols?(attr_name, value)
- node.attributes.delete(attr_name)
- else
- node.attributes[attr_name] = attr_name == 'style' ? sanitize_css(value) : CGI::escapeHTML(CGI::unescapeHTML(value))
- end
- end
- end
-
- def contains_bad_protocols?(attr_name, value)
- uri_attributes.include?(attr_name) &&
- (value =~ /(^[^\/:]*):|(&#0*58)|(&#x70)|(&#x0*3a)|(%|&#37;)3A/i && !allowed_protocols.include?(value.split(protocol_separator).first.downcase.strip))
- end
- end
-end
diff --git a/actionview/lib/action_view/vendor/html-scanner/html/selector.rb b/actionview/lib/action_view/vendor/html-scanner/html/selector.rb
deleted file mode 100644
index dfdd724b9b..0000000000
--- a/actionview/lib/action_view/vendor/html-scanner/html/selector.rb
+++ /dev/null
@@ -1,830 +0,0 @@
-#--
-# Copyright (c) 2006 Assaf Arkin (http://labnotes.org)
-# Under MIT and/or CC By license.
-#++
-
-module HTML
-
- # Selects HTML elements using CSS 2 selectors.
- #
- # The +Selector+ class uses CSS selector expressions to match and select
- # HTML elements.
- #
- # For example:
- # selector = HTML::Selector.new "form.login[action=/login]"
- # creates a new selector that matches any +form+ element with the class
- # +login+ and an attribute +action+ with the value <tt>/login</tt>.
- #
- # === Matching Elements
- #
- # Use the #match method to determine if an element matches the selector.
- #
- # For simple selectors, the method returns an array with that element,
- # or +nil+ if the element does not match. For complex selectors (see below)
- # the method returns an array with all matched elements, of +nil+ if no
- # match found.
- #
- # For example:
- # if selector.match(element)
- # puts "Element is a login form"
- # end
- #
- # === Selecting Elements
- #
- # Use the #select method to select all matching elements starting with
- # one element and going through all children in depth-first order.
- #
- # This method returns an array of all matching elements, an empty array
- # if no match is found
- #
- # For example:
- # selector = HTML::Selector.new "input[type=text]"
- # matches = selector.select(element)
- # matches.each do |match|
- # puts "Found text field with name #{match.attributes['name']}"
- # end
- #
- # === Expressions
- #
- # Selectors can match elements using any of the following criteria:
- # * <tt>name</tt> -- Match an element based on its name (tag name).
- # For example, <tt>p</tt> to match a paragraph. You can use <tt>*</tt>
- # to match any element.
- # * <tt>#</tt><tt>id</tt> -- Match an element based on its identifier (the
- # <tt>id</tt> attribute). For example, <tt>#</tt><tt>page</tt>.
- # * <tt>.class</tt> -- Match an element based on its class name, all
- # class names if more than one specified.
- # * <tt>[attr]</tt> -- Match an element that has the specified attribute.
- # * <tt>[attr=value]</tt> -- Match an element that has the specified
- # attribute and value. (More operators are supported see below)
- # * <tt>:pseudo-class</tt> -- Match an element based on a pseudo class,
- # such as <tt>:nth-child</tt> and <tt>:empty</tt>.
- # * <tt>:not(expr)</tt> -- Match an element that does not match the
- # negation expression.
- #
- # When using a combination of the above, the element name comes first
- # followed by identifier, class names, attributes, pseudo classes and
- # negation in any order. Do not separate these parts with spaces!
- # Space separation is used for descendant selectors.
- #
- # For example:
- # selector = HTML::Selector.new "form.login[action=/login]"
- # The matched element must be of type +form+ and have the class +login+.
- # It may have other classes, but the class +login+ is required to match.
- # It must also have an attribute called +action+ with the value
- # <tt>/login</tt>.
- #
- # This selector will match the following element:
- # <form class="login form" method="post" action="/login">
- # but will not match the element:
- # <form method="post" action="/logout">
- #
- # === Attribute Values
- #
- # Several operators are supported for matching attributes:
- # * <tt>name</tt> -- The element must have an attribute with that name.
- # * <tt>name=value</tt> -- The element must have an attribute with that
- # name and value.
- # * <tt>name^=value</tt> -- The attribute value must start with the
- # specified value.
- # * <tt>name$=value</tt> -- The attribute value must end with the
- # specified value.
- # * <tt>name*=value</tt> -- The attribute value must contain the
- # specified value.
- # * <tt>name~=word</tt> -- The attribute value must contain the specified
- # word (space separated).
- # * <tt>name|=word</tt> -- The attribute value must start with specified
- # word.
- #
- # For example, the following two selectors match the same element:
- # #my_id
- # [id=my_id]
- # and so do the following two selectors:
- # .my_class
- # [class~=my_class]
- #
- # === Alternatives, siblings, children
- #
- # Complex selectors use a combination of expressions to match elements:
- # * <tt>expr1 expr2</tt> -- Match any element against the second expression
- # if it has some parent element that matches the first expression.
- # * <tt>expr1 > expr2</tt> -- Match any element against the second expression
- # if it is the child of an element that matches the first expression.
- # * <tt>expr1 + expr2</tt> -- Match any element against the second expression
- # if it immediately follows an element that matches the first expression.
- # * <tt>expr1 ~ expr2</tt> -- Match any element against the second expression
- # that comes after an element that matches the first expression.
- # * <tt>expr1, expr2</tt> -- Match any element against the first expression,
- # or against the second expression.
- #
- # Since children and sibling selectors may match more than one element given
- # the first element, the #match method may return more than one match.
- #
- # === Pseudo classes
- #
- # Pseudo classes were introduced in CSS 3. They are most often used to select
- # elements in a given position:
- # * <tt>:root</tt> -- Match the element only if it is the root element
- # (no parent element).
- # * <tt>:empty</tt> -- Match the element only if it has no child elements,
- # and no text content.
- # * <tt>:content(string)</tt> -- Match the element only if it has <tt>string</tt>
- # as its text content (ignoring leading and trailing whitespace).
- # * <tt>:only-child</tt> -- Match the element if it is the only child (element)
- # of its parent element.
- # * <tt>:only-of-type</tt> -- Match the element if it is the only child (element)
- # of its parent element and its type.
- # * <tt>:first-child</tt> -- Match the element if it is the first child (element)
- # of its parent element.
- # * <tt>:first-of-type</tt> -- Match the element if it is the first child (element)
- # of its parent element of its type.
- # * <tt>:last-child</tt> -- Match the element if it is the last child (element)
- # of its parent element.
- # * <tt>:last-of-type</tt> -- Match the element if it is the last child (element)
- # of its parent element of its type.
- # * <tt>:nth-child(b)</tt> -- Match the element if it is the b-th child (element)
- # of its parent element. The value <tt>b</tt> specifies its index, starting with 1.
- # * <tt>:nth-child(an+b)</tt> -- Match the element if it is the b-th child (element)
- # in each group of <tt>a</tt> child elements of its parent element.
- # * <tt>:nth-child(-an+b)</tt> -- Match the element if it is the first child (element)
- # in each group of <tt>a</tt> child elements, up to the first <tt>b</tt> child
- # elements of its parent element.
- # * <tt>:nth-child(odd)</tt> -- Match element in the odd position (i.e. first, third).
- # Same as <tt>:nth-child(2n+1)</tt>.
- # * <tt>:nth-child(even)</tt> -- Match element in the even position (i.e. second,
- # fourth). Same as <tt>:nth-child(2n+2)</tt>.
- # * <tt>:nth-of-type(..)</tt> -- As above, but only counts elements of its type.
- # * <tt>:nth-last-child(..)</tt> -- As above, but counts from the last child.
- # * <tt>:nth-last-of-type(..)</tt> -- As above, but counts from the last child and
- # only elements of its type.
- # * <tt>:not(selector)</tt> -- Match the element only if the element does not
- # match the simple selector.
- #
- # As you can see, <tt>:nth-child</tt> pseudo class and its variant can get quite
- # tricky and the CSS specification doesn't do a much better job explaining it.
- # But after reading the examples and trying a few combinations, it's easy to
- # figure out.
- #
- # For example:
- # table tr:nth-child(odd)
- # Selects every second row in the table starting with the first one.
- #
- # div p:nth-child(4)
- # Selects the fourth paragraph in the +div+, but not if the +div+ contains
- # other elements, since those are also counted.
- #
- # div p:nth-of-type(4)
- # Selects the fourth paragraph in the +div+, counting only paragraphs, and
- # ignoring all other elements.
- #
- # div p:nth-of-type(-n+4)
- # Selects the first four paragraphs, ignoring all others.
- #
- # And you can always select an element that matches one set of rules but
- # not another using <tt>:not</tt>. For example:
- # p:not(.post)
- # Matches all paragraphs that do not have the class <tt>.post</tt>.
- #
- # === Substitution Values
- #
- # You can use substitution with identifiers, class names and element values.
- # A substitution takes the form of a question mark (<tt>?</tt>) and uses the
- # next value in the argument list following the CSS expression.
- #
- # The substitution value may be a string or a regular expression. All other
- # values are converted to strings.
- #
- # For example:
- # selector = HTML::Selector.new "#?", /^\d+$/
- # matches any element whose identifier consists of one or more digits.
- #
- # See http://www.w3.org/TR/css3-selectors/
- class Selector
-
-
- # An invalid selector.
- class InvalidSelectorError < StandardError #:nodoc:
- end
-
-
- class << self
-
- # :call-seq:
- # Selector.for_class(cls) => selector
- #
- # Creates a new selector for the given class name.
- def for_class(cls)
- self.new([".?", cls])
- end
-
-
- # :call-seq:
- # Selector.for_id(id) => selector
- #
- # Creates a new selector for the given id.
- def for_id(id)
- self.new(["#?", id])
- end
-
- end
-
-
- # :call-seq:
- # Selector.new(string, [values ...]) => selector
- #
- # Creates a new selector from a CSS 2 selector expression.
- #
- # The first argument is the selector expression. All other arguments
- # are used for value substitution.
- #
- # Throws InvalidSelectorError is the selector expression is invalid.
- def initialize(selector, *values)
- raise ArgumentError, "CSS expression cannot be empty" if selector.empty?
- @source = ""
- values = values[0] if values.size == 1 && values[0].is_a?(Array)
-
- # We need a copy to determine if we failed to parse, and also
- # preserve the original pass by-ref statement.
- statement = selector.strip.dup
-
- # Create a simple selector, along with negation.
- simple_selector(statement, values).each { |name, value| instance_variable_set("@#{name}", value) }
-
- @alternates = []
- @depends = nil
-
- # Alternative selector.
- if statement.sub!(/^\s*,\s*/, "")
- second = Selector.new(statement, values)
- @alternates << second
- # If there are alternate selectors, we group them in the top selector.
- if alternates = second.instance_variable_get(:@alternates)
- second.instance_variable_set(:@alternates, [])
- @alternates.concat alternates
- end
- @source << " , " << second.to_s
- # Sibling selector: create a dependency into second selector that will
- # match element immediately following this one.
- elsif statement.sub!(/^\s*\+\s*/, "")
- second = next_selector(statement, values)
- @depends = lambda do |element, first|
- if element = next_element(element)
- second.match(element, first)
- end
- end
- @source << " + " << second.to_s
- # Adjacent selector: create a dependency into second selector that will
- # match all elements following this one.
- elsif statement.sub!(/^\s*~\s*/, "")
- second = next_selector(statement, values)
- @depends = lambda do |element, first|
- matches = []
- while element = next_element(element)
- if subset = second.match(element, first)
- if first && !subset.empty?
- matches << subset.first
- break
- else
- matches.concat subset
- end
- end
- end
- matches.empty? ? nil : matches
- end
- @source << " ~ " << second.to_s
- # Child selector: create a dependency into second selector that will
- # match a child element of this one.
- elsif statement.sub!(/^\s*>\s*/, "")
- second = next_selector(statement, values)
- @depends = lambda do |element, first|
- matches = []
- element.children.each do |child|
- if child.tag? && subset = second.match(child, first)
- if first && !subset.empty?
- matches << subset.first
- break
- else
- matches.concat subset
- end
- end
- end
- matches.empty? ? nil : matches
- end
- @source << " > " << second.to_s
- # Descendant selector: create a dependency into second selector that
- # will match all descendant elements of this one. Note,
- elsif statement =~ /^\s+\S+/ && statement != selector
- second = next_selector(statement, values)
- @depends = lambda do |element, first|
- matches = []
- stack = element.children.reverse
- while node = stack.pop
- next unless node.tag?
- if subset = second.match(node, first)
- if first && !subset.empty?
- matches << subset.first
- break
- else
- matches.concat subset
- end
- elsif children = node.children
- stack.concat children.reverse
- end
- end
- matches.empty? ? nil : matches
- end
- @source << " " << second.to_s
- else
- # The last selector is where we check that we parsed
- # all the parts.
- unless statement.empty? || statement.strip.empty?
- raise ArgumentError, "Invalid selector: #{statement}"
- end
- end
- end
-
-
- # :call-seq:
- # match(element, first?) => array or nil
- #
- # Matches an element against the selector.
- #
- # For a simple selector this method returns an array with the
- # element if the element matches, nil otherwise.
- #
- # For a complex selector (sibling and descendant) this method
- # returns an array with all matching elements, nil if no match is
- # found.
- #
- # Use +first_only=true+ if you are only interested in the first element.
- #
- # For example:
- # if selector.match(element)
- # puts "Element is a login form"
- # end
- def match(element, first_only = false)
- # Match element if no element name or element name same as element name
- if matched = (!@tag_name || @tag_name == element.name)
- # No match if one of the attribute matches failed
- for attr in @attributes
- if element.attributes[attr[0]] !~ attr[1]
- matched = false
- break
- end
- end
- end
-
- # Pseudo class matches (nth-child, empty, etc).
- if matched
- for pseudo in @pseudo
- unless pseudo.call(element)
- matched = false
- break
- end
- end
- end
-
- # Negation. Same rules as above, but we fail if a match is made.
- if matched && @negation
- for negation in @negation
- if negation[:tag_name] == element.name
- matched = false
- else
- for attr in negation[:attributes]
- if element.attributes[attr[0]] =~ attr[1]
- matched = false
- break
- end
- end
- end
- if matched
- for pseudo in negation[:pseudo]
- if pseudo.call(element)
- matched = false
- break
- end
- end
- end
- break unless matched
- end
- end
-
- # If element matched but depends on another element (child,
- # sibling, etc), apply the dependent matches instead.
- if matched && @depends
- matches = @depends.call(element, first_only)
- else
- matches = matched ? [element] : nil
- end
-
- # If this selector is part of the group, try all the alternative
- # selectors (unless first_only).
- if !first_only || !matches
- @alternates.each do |alternate|
- break if matches && first_only
- if subset = alternate.match(element, first_only)
- if matches
- matches.concat subset
- else
- matches = subset
- end
- end
- end
- end
-
- matches
- end
-
-
- # :call-seq:
- # select(root) => array
- #
- # Selects and returns an array with all matching elements, beginning
- # with one node and traversing through all children depth-first.
- # Returns an empty array if no match is found.
- #
- # The root node may be any element in the document, or the document
- # itself.
- #
- # For example:
- # selector = HTML::Selector.new "input[type=text]"
- # matches = selector.select(element)
- # matches.each do |match|
- # puts "Found text field with name #{match.attributes['name']}"
- # end
- def select(root)
- matches = []
- stack = [root]
- while node = stack.pop
- if node.tag? && subset = match(node, false)
- subset.each do |match|
- matches << match unless matches.any? { |item| item.equal?(match) }
- end
- elsif children = node.children
- stack.concat children.reverse
- end
- end
- matches
- end
-
-
- # Similar to #select but returns the first matching element. Returns +nil+
- # if no element matches the selector.
- def select_first(root)
- stack = [root]
- while node = stack.pop
- if node.tag? && subset = match(node, true)
- return subset.first if !subset.empty?
- elsif children = node.children
- stack.concat children.reverse
- end
- end
- nil
- end
-
-
- def to_s #:nodoc:
- @source
- end
-
-
- # Returns the next element after this one. Skips sibling text nodes.
- #
- # With the +name+ argument, returns the next element with that name,
- # skipping other sibling elements.
- def next_element(element, name = nil)
- if siblings = element.parent.children
- found = false
- siblings.each do |node|
- if node.equal?(element)
- found = true
- elsif found && node.tag?
- return node if (name.nil? || node.name == name)
- end
- end
- end
- nil
- end
-
-
- protected
-
-
- # Creates a simple selector given the statement and array of
- # substitution values.
- #
- # Returns a hash with the values +tag_name+, +attributes+,
- # +pseudo+ (classes) and +negation+.
- #
- # Called the first time with +can_negate+ true to allow
- # negation. Called a second time with false since negation
- # cannot be negated.
- def simple_selector(statement, values, can_negate = true)
- tag_name = nil
- attributes = []
- pseudo = []
- negation = []
-
- # Element name. (Note that in negation, this can come at
- # any order, but for simplicity we allow if only first).
- statement.sub!(/^(\*|[[:alpha:]][\w\-]*)/) do |match|
- match.strip!
- tag_name = match.downcase unless match == "*"
- @source << match
- "" # Remove
- end
-
- # Get identifier, class, attribute name, pseudo or negation.
- while true
- # Element identifier.
- next if statement.sub!(/^#(\?|[\w\-]+)/) do
- id = $1
- if id == "?"
- id = values.shift
- end
- @source << "##{id}"
- id = Regexp.new("^#{Regexp.escape(id.to_s)}$") unless id.is_a?(Regexp)
- attributes << ["id", id]
- "" # Remove
- end
-
- # Class name.
- next if statement.sub!(/^\.([\w\-]+)/) do
- class_name = $1
- @source << ".#{class_name}"
- class_name = Regexp.new("(^|\s)#{Regexp.escape(class_name)}($|\s)") unless class_name.is_a?(Regexp)
- attributes << ["class", class_name]
- "" # Remove
- end
-
- # Attribute value.
- next if statement.sub!(/^\[\s*([[:alpha:]][\w\-:]*)\s*((?:[~|^$*])?=)?\s*('[^']*'|"[^*]"|[^\]]*)\s*\]/) do
- name, equality, value = $1, $2, $3
- if value == "?"
- value = values.shift
- else
- # Handle single and double quotes.
- value.strip!
- if (value[0] == ?" || value[0] == ?') && value[0] == value[-1]
- value = value[1..-2]
- end
- end
- @source << "[#{name}#{equality}'#{value}']"
- attributes << [name.downcase.strip, attribute_match(equality, value)]
- "" # Remove
- end
-
- # Root element only.
- next if statement.sub!(/^:root/) do
- pseudo << lambda do |element|
- element.parent.nil? || !element.parent.tag?
- end
- @source << ":root"
- "" # Remove
- end
-
- # Nth-child including last and of-type.
- next if statement.sub!(/^:nth-(last-)?(child|of-type)\((odd|even|(\d+|\?)|(-?\d*|\?)?n([+\-]\d+|\?)?)\)/) do |match|
- reverse = $1 == "last-"
- of_type = $2 == "of-type"
- @source << ":nth-#{$1}#{$2}("
- case $3
- when "odd"
- pseudo << nth_child(2, 1, of_type, reverse)
- @source << "odd)"
- when "even"
- pseudo << nth_child(2, 2, of_type, reverse)
- @source << "even)"
- when /^(\d+|\?)$/ # b only
- b = ($1 == "?" ? values.shift : $1).to_i
- pseudo << nth_child(0, b, of_type, reverse)
- @source << "#{b})"
- when /^(-?\d*|\?)?n([+\-]\d+|\?)?$/
- a = ($1 == "?" ? values.shift :
- $1 == "" ? 1 : $1 == "-" ? -1 : $1).to_i
- b = ($2 == "?" ? values.shift : $2).to_i
- pseudo << nth_child(a, b, of_type, reverse)
- @source << (b >= 0 ? "#{a}n+#{b})" : "#{a}n#{b})")
- else
- raise ArgumentError, "Invalid nth-child #{match}"
- end
- "" # Remove
- end
- # First/last child (of type).
- next if statement.sub!(/^:(first|last)-(child|of-type)/) do
- reverse = $1 == "last"
- of_type = $2 == "of-type"
- pseudo << nth_child(0, 1, of_type, reverse)
- @source << ":#{$1}-#{$2}"
- "" # Remove
- end
- # Only child (of type).
- next if statement.sub!(/^:only-(child|of-type)/) do
- of_type = $1 == "of-type"
- pseudo << only_child(of_type)
- @source << ":only-#{$1}"
- "" # Remove
- end
-
- # Empty: no child elements or meaningful content (whitespaces
- # are ignored).
- next if statement.sub!(/^:empty/) do
- pseudo << lambda do |element|
- empty = true
- for child in element.children
- if child.tag? || !child.content.strip.empty?
- empty = false
- break
- end
- end
- empty
- end
- @source << ":empty"
- "" # Remove
- end
- # Content: match the text content of the element, stripping
- # leading and trailing spaces.
- next if statement.sub!(/^:content\(\s*(\?|'[^']*'|"[^"]*"|[^)]*)\s*\)/) do
- content = $1
- if content == "?"
- content = values.shift
- elsif (content[0] == ?" || content[0] == ?') && content[0] == content[-1]
- content = content[1..-2]
- end
- @source << ":content('#{content}')"
- content = Regexp.new("^#{Regexp.escape(content.to_s)}$") unless content.is_a?(Regexp)
- pseudo << lambda do |element|
- text = ""
- for child in element.children
- unless child.tag?
- text << child.content
- end
- end
- text.strip =~ content
- end
- "" # Remove
- end
-
- # Negation. Create another simple selector to handle it.
- if statement.sub!(/^:not\(\s*/, "")
- raise ArgumentError, "Double negatives are not missing feature" unless can_negate
- @source << ":not("
- negation << simple_selector(statement, values, false)
- raise ArgumentError, "Negation not closed" unless statement.sub!(/^\s*\)/, "")
- @source << ")"
- next
- end
-
- # No match: moving on.
- break
- end
-
- # Return hash. The keys are mapped to instance variables.
- {:tag_name=>tag_name, :attributes=>attributes, :pseudo=>pseudo, :negation=>negation}
- end
-
-
- # Create a regular expression to match an attribute value based
- # on the equality operator (=, ^=, |=, etc).
- def attribute_match(equality, value)
- regexp = value.is_a?(Regexp) ? value : Regexp.escape(value.to_s)
- case equality
- when "=" then
- # Match the attribute value in full
- Regexp.new("^#{regexp}$")
- when "~=" then
- # Match a space-separated word within the attribute value
- Regexp.new("(^|\s)#{regexp}($|\s)")
- when "^="
- # Match the beginning of the attribute value
- Regexp.new("^#{regexp}")
- when "$="
- # Match the end of the attribute value
- Regexp.new("#{regexp}$")
- when "*="
- # Match substring of the attribute value
- regexp.is_a?(Regexp) ? regexp : Regexp.new(regexp)
- when "|=" then
- # Match the first space-separated item of the attribute value
- Regexp.new("^#{regexp}($|\s)")
- else
- raise InvalidSelectorError, "Invalid operation/value" unless value.empty?
- # Match all attributes values (existence check)
- //
- end
- end
-
-
- # Returns a lambda that can match an element against the nth-child
- # pseudo class, given the following arguments:
- # * +a+ -- Value of a part.
- # * +b+ -- Value of b part.
- # * +of_type+ -- True to test only elements of this type (of-type).
- # * +reverse+ -- True to count in reverse order (last-).
- def nth_child(a, b, of_type, reverse)
- # a = 0 means select at index b, if b = 0 nothing selected
- return lambda { |element| false } if a == 0 && b == 0
- # a < 0 and b < 0 will never match against an index
- return lambda { |element| false } if a < 0 && b < 0
- b = a + b + 1 if b < 0 # b < 0 just picks last element from each group
- b -= 1 unless b == 0 # b == 0 is same as b == 1, otherwise zero based
- lambda do |element|
- # Element must be inside parent element.
- return false unless element.parent && element.parent.tag?
- index = 0
- # Get siblings, reverse if counting from last.
- siblings = element.parent.children
- siblings = siblings.reverse if reverse
- # Match element name if of-type, otherwise ignore name.
- name = of_type ? element.name : nil
- found = false
- for child in siblings
- # Skip text nodes/comments.
- if child.tag? && (name == nil || child.name == name)
- if a == 0
- # Shortcut when a == 0 no need to go past count
- if index == b
- found = child.equal?(element)
- break
- end
- elsif a < 0
- # Only look for first b elements
- break if index > b
- if child.equal?(element)
- found = (index % a) == 0
- break
- end
- else
- # Otherwise, break if child found and count == an+b
- if child.equal?(element)
- found = (index % a) == b
- break
- end
- end
- index += 1
- end
- end
- found
- end
- end
-
-
- # Creates a only child lambda. Pass +of-type+ to only look at
- # elements of its type.
- def only_child(of_type)
- lambda do |element|
- # Element must be inside parent element.
- return false unless element.parent && element.parent.tag?
- name = of_type ? element.name : nil
- other = false
- for child in element.parent.children
- # Skip text nodes/comments.
- if child.tag? && (name == nil || child.name == name)
- unless child.equal?(element)
- other = true
- break
- end
- end
- end
- !other
- end
- end
-
-
- # Called to create a dependent selector (sibling, descendant, etc).
- # Passes the remainder of the statement that will be reduced to zero
- # eventually, and array of substitution values.
- #
- # This method is called from four places, so it helps to put it here
- # for reuse. The only logic deals with the need to detect comma
- # separators (alternate) and apply them to the selector group of the
- # top selector.
- def next_selector(statement, values)
- second = Selector.new(statement, values)
- # If there are alternate selectors, we group them in the top selector.
- if alternates = second.instance_variable_get(:@alternates)
- second.instance_variable_set(:@alternates, [])
- @alternates.concat alternates
- end
- second
- end
-
- end
-
-
- # See HTML::Selector.new
- def self.selector(statement, *values)
- Selector.new(statement, *values)
- end
-
-
- class Tag
-
- def select(selector, *values)
- selector = HTML::Selector.new(selector, values)
- selector.select(self)
- end
-
- end
-
-end
diff --git a/actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb b/actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb
deleted file mode 100644
index adf4e45930..0000000000
--- a/actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb
+++ /dev/null
@@ -1,107 +0,0 @@
-require 'strscan'
-
-module HTML #:nodoc:
-
- # A simple HTML tokenizer. It simply breaks a stream of text into tokens, where each
- # token is a string. Each string represents either "text", or an HTML element.
- #
- # This currently assumes valid XHTML, which means no free < or > characters.
- #
- # Usage:
- #
- # tokenizer = HTML::Tokenizer.new(text)
- # while token = tokenizer.next
- # p token
- # end
- class Tokenizer #:nodoc:
-
- # The current (byte) position in the text
- attr_reader :position
-
- # The current line number
- attr_reader :line
-
- # Create a new Tokenizer for the given text.
- def initialize(text)
- text.encode!
- @scanner = StringScanner.new(text)
- @position = 0
- @line = 0
- @current_line = 1
- end
-
- # Returns the next token in the sequence, or +nil+ if there are no more tokens in
- # the stream.
- def next
- return nil if @scanner.eos?
- @position = @scanner.pos
- @line = @current_line
- if @scanner.check(/<\S/)
- update_current_line(scan_tag)
- else
- update_current_line(scan_text)
- end
- end
-
- private
-
- # Treat the text at the current position as a tag, and scan it. Supports
- # comments, doctype tags, and regular tags, and ignores less-than and
- # greater-than characters within quoted strings.
- def scan_tag
- tag = @scanner.getch
- if @scanner.scan(/!--/) # comment
- tag << @scanner.matched
- tag << (@scanner.scan_until(/--\s*>/) || @scanner.scan_until(/\Z/))
- elsif @scanner.scan(/!\[CDATA\[/)
- tag << @scanner.matched
- tag << (@scanner.scan_until(/\]\]>/) || @scanner.scan_until(/\Z/))
- elsif @scanner.scan(/!/) # doctype
- tag << @scanner.matched
- tag << consume_quoted_regions
- else
- tag << consume_quoted_regions
- end
- tag
- end
-
- # Scan all text up to the next < character and return it.
- def scan_text
- "#{@scanner.getch}#{@scanner.scan(/[^<]*/)}"
- end
-
- # Counts the number of newlines in the text and updates the current line
- # accordingly.
- def update_current_line(text)
- text.scan(/\r?\n/) { @current_line += 1 }
- end
-
- # Skips over quoted strings, so that less-than and greater-than characters
- # within the strings are ignored.
- def consume_quoted_regions
- text = ""
- loop do
- match = @scanner.scan_until(/['"<>]/) or break
-
- delim = @scanner.matched
- if delim == "<"
- match = match.chop
- @scanner.pos -= 1
- end
-
- text << match
- break if delim == "<" || delim == ">"
-
- # consume the quoted region
- while match = @scanner.scan_until(/[\\#{delim}]/)
- text << match
- break if @scanner.matched == delim
- break if @scanner.eos?
- text << @scanner.getch # skip the escaped character
- end
- end
- text
- end
- end
-
-end
diff --git a/actionview/lib/action_view/vendor/html-scanner/html/version.rb b/actionview/lib/action_view/vendor/html-scanner/html/version.rb
deleted file mode 100644
index 6d645c3e14..0000000000
--- a/actionview/lib/action_view/vendor/html-scanner/html/version.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-module HTML #:nodoc:
- module Version #:nodoc:
-
- MAJOR = 0
- MINOR = 5
- TINY = 3
-
- STRING = [ MAJOR, MINOR, TINY ].join(".")
-
- end
-end
diff --git a/actionview/lib/action_view/view_paths.rb b/actionview/lib/action_view/view_paths.rb
index 80a41f2418..37722013ce 100644
--- a/actionview/lib/action_view/view_paths.rb
+++ b/actionview/lib/action_view/view_paths.rb
@@ -16,14 +16,9 @@ module ActionView
module ClassMethods
def _prefixes # :nodoc:
@_prefixes ||= begin
- deprecated_prefixes = handle_deprecated_parent_prefixes
- if deprecated_prefixes
- deprecated_prefixes
- else
- return local_prefixes if superclass.abstract?
-
- local_prefixes + superclass._prefixes
- end
+ return local_prefixes if superclass.abstract?
+
+ local_prefixes + superclass._prefixes
end
end
@@ -34,13 +29,6 @@ module ActionView
def local_prefixes
[controller_path]
end
-
- def handle_deprecated_parent_prefixes # TODO: remove in 4.3/5.0.
- return unless respond_to?(:parent_prefixes)
-
- ActiveSupport::Deprecation.warn "Overriding ActionController::Base::parent_prefixes is deprecated, override .local_prefixes instead."
- local_prefixes + parent_prefixes
- end
end
# The prefixes used in render "foo" shortcuts.
@@ -48,9 +36,9 @@ module ActionView
self.class._prefixes
end
- # LookupContext is the object responsible to hold all information required to lookup
- # templates, i.e. view paths and details. Check ActionView::LookupContext for more
- # information.
+ # <tt>LookupContext</tt> is the object responsible for holding all
+ # information required for looking up templates, i.e. view paths and
+ # details. Check <tt>ActionView::LookupContext</tt> for more information.
def lookup_context
@_lookup_context ||=
ActionView::LookupContext.new(self.class._view_paths, details_for_lookup, _prefixes)
diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb
index d60712255b..2354e91822 100644
--- a/actionview/test/abstract_unit.rb
+++ b/actionview/test/abstract_unit.rb
@@ -16,11 +16,10 @@ silence_warnings do
end
require 'active_support/testing/autorun'
-require 'abstract_controller'
+require 'active_support/testing/method_call_assertions'
require 'action_controller'
require 'action_view'
require 'action_view/testing/resolvers'
-require 'action_dispatch'
require 'active_support/dependencies'
require 'active_model'
require 'active_record'
@@ -48,23 +47,9 @@ I18n.enforce_available_locales = false
# Register danish language for testing
I18n.backend.store_translations 'da', {}
I18n.backend.store_translations 'pt-BR', {}
-ORIGINAL_LOCALES = I18n.available_locales.map {|locale| locale.to_s }.sort
+ORIGINAL_LOCALES = I18n.available_locales.map(&:to_s).sort
FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), 'fixtures')
-FIXTURES = Pathname.new(FIXTURE_LOAD_PATH)
-
-module RackTestUtils
- def body_to_string(body)
- if body.respond_to?(:each)
- str = ""
- body.each {|s| str << s }
- str
- else
- body
- end
- end
- extend self
-end
module RenderERBUtils
def view
@@ -163,13 +148,12 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase
def self.build_app(routes = nil)
RoutedRackApp.new(routes || ActionDispatch::Routing::RouteSet.new) do |middleware|
- middleware.use "ActionDispatch::ShowExceptions", ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public")
- middleware.use "ActionDispatch::DebugExceptions"
- middleware.use "ActionDispatch::Callbacks"
- middleware.use "ActionDispatch::ParamsParser"
- middleware.use "ActionDispatch::Cookies"
- middleware.use "ActionDispatch::Flash"
- middleware.use "Rack::Head"
+ middleware.use ActionDispatch::ShowExceptions, ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public")
+ middleware.use ActionDispatch::DebugExceptions
+ middleware.use ActionDispatch::Callbacks
+ middleware.use ActionDispatch::Cookies
+ middleware.use ActionDispatch::Flash
+ middleware.use Rack::Head
yield(middleware) if block_given?
end
end
@@ -227,50 +211,7 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase
end
end
-# Temporary base class
-class Rack::TestCase < ActionDispatch::IntegrationTest
- def self.testing(klass = nil)
- if klass
- @testing = "/#{klass.name.underscore}".sub!(/_controller$/, '')
- else
- @testing
- end
- end
-
- def get(thing, *args)
- if thing.is_a?(Symbol)
- super("#{self.class.testing}/#{thing}", *args)
- else
- super
- end
- end
-
- def assert_body(body)
- assert_equal body, Array(response.body).join
- end
-
- def assert_status(code)
- assert_equal code, response.status
- end
-
- def assert_response(body, status = 200, headers = {})
- assert_body body
- assert_status status
- headers.each do |header, value|
- assert_header header, value
- end
- end
-
- def assert_content_type(type)
- assert_equal type, response.headers["Content-Type"]
- end
-
- def assert_header(name, value)
- assert_equal value, response.headers[name]
- end
-end
-
-ActionView::RoutingUrlFor.send(:include, ActionDispatch::Routing::UrlFor)
+ActionView::RoutingUrlFor.include(ActionDispatch::Routing::UrlFor)
module ActionController
class Base
@@ -340,3 +281,6 @@ def jruby_skip(message = '')
end
require 'mocha/setup' # FIXME: stop using mocha
+class ActiveSupport::TestCase
+ include ActiveSupport::Testing::MethodCallAssertions
+end
diff --git a/actionview/test/actionpack/abstract/abstract_controller_test.rb b/actionview/test/actionpack/abstract/abstract_controller_test.rb
index e653b12d32..490932fef0 100644
--- a/actionview/test/actionpack/abstract/abstract_controller_test.rb
+++ b/actionview/test/actionpack/abstract/abstract_controller_test.rb
@@ -168,7 +168,7 @@ module AbstractController
end
end
- class OverridingLocalPrefixesTest < ActiveSupport::TestCase # TODO: remove me in 5.0/4.3.
+ class OverridingLocalPrefixesTest < ActiveSupport::TestCase
test "overriding .local_prefixes adds prefix" do
@controller = OverridingLocalPrefixes.new
@controller.process(:index)
@@ -182,22 +182,6 @@ module AbstractController
end
end
- class DeprecatedParentPrefixes < OverridingLocalPrefixes
- def self.parent_prefixes
- ["abstract_controller/testing/me3"]
- end
- end
-
- class DeprecatedParentPrefixesTest < ActiveSupport::TestCase # TODO: remove me in 5.0/4.3.
- test "overriding .parent_prefixes is deprecated" do
- @controller = DeprecatedParentPrefixes.new
- assert_deprecated do
- @controller.process(:index)
- end
- assert_equal "Hello from me3/index.erb", @controller.response_body
- end
- end
-
# Test rendering with layouts
# ====
# self._layout is used when defined
diff --git a/actionview/test/actionpack/abstract/layouts_test.rb b/actionview/test/actionpack/abstract/layouts_test.rb
index a6786d9b6b..80bc665b0a 100644
--- a/actionview/test/actionpack/abstract/layouts_test.rb
+++ b/actionview/test/actionpack/abstract/layouts_test.rb
@@ -52,7 +52,7 @@ module AbstractControllerTests
end
def overwrite_skip
- render :text => "Hello text!"
+ render plain: "Hello text!"
end
end
@@ -371,7 +371,7 @@ module AbstractControllerTests
test "layout for anonymous controller" do
klass = Class.new(WithString) do
def index
- render :text => 'index', :layout => true
+ render plain: 'index', layout: true
end
end
diff --git a/actionview/test/actionpack/abstract/render_test.rb b/actionview/test/actionpack/abstract/render_test.rb
index d09f91c1e2..e185b76adb 100644
--- a/actionview/test/actionpack/abstract/render_test.rb
+++ b/actionview/test/actionpack/abstract/render_test.rb
@@ -33,7 +33,7 @@ module AbstractController
end
def text
- render :text => "With Text"
+ render plain: "With Text"
end
def default
diff --git a/actionview/test/actionpack/abstract/views/abstract_controller/testing/me5/index.erb b/actionview/test/actionpack/abstract/views/abstract_controller/testing/me5/index.erb
deleted file mode 100644
index 84d0b7417e..0000000000
--- a/actionview/test/actionpack/abstract/views/abstract_controller/testing/me5/index.erb
+++ /dev/null
@@ -1 +0,0 @@
-Hello from me5/index.erb \ No newline at end of file
diff --git a/actionview/test/actionpack/controller/capture_test.rb b/actionview/test/actionpack/controller/capture_test.rb
index f8387b27b0..933456ce9d 100644
--- a/actionview/test/actionpack/controller/capture_test.rb
+++ b/actionview/test/actionpack/controller/capture_test.rb
@@ -54,7 +54,7 @@ class CaptureTest < ActionController::TestCase
assert_equal expected_content_for_output, @response.body
end
- def test_should_concatentate_content_for
+ def test_should_concatenate_content_for
get :content_for_concatenated
assert_equal expected_content_for_output, @response.body
end
diff --git a/actionview/test/actionpack/controller/layout_test.rb b/actionview/test/actionpack/controller/layout_test.rb
index bd345fe873..64bc4c41d6 100644
--- a/actionview/test/actionpack/controller/layout_test.rb
+++ b/actionview/test/actionpack/controller/layout_test.rb
@@ -1,5 +1,4 @@
require 'abstract_unit'
-require 'rbconfig'
require 'active_support/core_ext/array/extract_options'
# The view_paths array must be set on Base and not LayoutTest so that LayoutTest's inherited
@@ -150,75 +149,75 @@ class LayoutSetInResponseTest < ActionController::TestCase
def test_layout_set_when_using_default_layout
@controller = DefaultLayoutController.new
get :hello
- assert_template :layout => "layouts/layout_test"
+ assert_includes @response.body, 'layout_test.erb'
end
def test_layout_set_when_using_streaming_layout
@controller = StreamingLayoutController.new
get :hello
- assert_template :hello
+ assert_includes @response.body, 'layout_test.erb'
end
def test_layout_set_when_set_in_controller
@controller = HasOwnLayoutController.new
get :hello
- assert_template :layout => "layouts/item"
+ assert_includes @response.body, 'item.erb'
end
def test_layout_symbol_set_in_controller_returning_nil_falls_back_to_default
@controller = HasNilLayoutSymbol.new
get :hello
- assert_template layout: "layouts/layout_test"
+ assert_includes @response.body, 'layout_test.erb'
end
def test_layout_proc_set_in_controller_returning_nil_falls_back_to_default
@controller = HasNilLayoutProc.new
get :hello
- assert_template layout: "layouts/layout_test"
+ assert_includes @response.body, 'layout_test.erb'
end
def test_layout_only_exception_when_included
@controller = OnlyLayoutController.new
get :hello
- assert_template :layout => "layouts/item"
+ assert_includes @response.body, 'item.erb'
end
def test_layout_only_exception_when_excepted
@controller = OnlyLayoutController.new
get :goodbye
- assert !@response.body.include?("item.erb"), "#{@response.body.inspect} included 'item.erb'"
+ assert_not_includes @response.body, 'item.erb'
end
def test_layout_except_exception_when_included
@controller = ExceptLayoutController.new
get :hello
- assert_template :layout => "layouts/item"
+ assert_includes @response.body, 'item.erb'
end
def test_layout_except_exception_when_excepted
@controller = ExceptLayoutController.new
get :goodbye
- assert !@response.body.include?("item.erb"), "#{@response.body.inspect} included 'item.erb'"
+ assert_not_includes @response.body, 'item.erb'
end
def test_layout_set_when_using_render
with_template_handler :mab, lambda { |template| template.source.inspect } do
@controller = SetsLayoutInRenderController.new
get :hello
- assert_template :layout => "layouts/third_party_template_library"
+ assert_includes @response.body, 'layouts/third_party_template_library.mab'
end
end
def test_layout_is_not_set_when_none_rendered
@controller = RendersNoLayoutController.new
get :hello
- assert_template :layout => nil
+ assert_equal 'hello.erb', @response.body
end
def test_layout_is_picked_from_the_controller_instances_view_path
@controller = PrependsViewPathController.new
get :hello
- assert_template :layout => /layouts\/alt/
+ assert_includes @response.body, 'alt.erb'
end
def test_absolute_pathed_layout
@@ -263,7 +262,7 @@ unless RbConfig::CONFIG['host_os'] =~ /mswin|mingw/
@controller = LayoutSymlinkedTest.new
get :hello
assert_response 200
- assert_template :layout => "layouts/symlinked/symlinked_layout"
+ assert_includes @response.body, 'This is my layout'
end
end
end
diff --git a/actionview/test/actionpack/controller/render_test.rb b/actionview/test/actionpack/controller/render_test.rb
index b3b51ae583..bdb9e0397b 100644
--- a/actionview/test/actionpack/controller/render_test.rb
+++ b/actionview/test/actionpack/controller/render_test.rb
@@ -1,5 +1,5 @@
require 'abstract_unit'
-require "active_model"
+require 'active_model'
class ApplicationController < ActionController::Base
self.view_paths = File.join(FIXTURE_LOAD_PATH, "actionpack")
@@ -31,6 +31,10 @@ class Customer < Struct.new(:name, :id)
def persisted?
id.present?
end
+
+ def cache_key
+ name.to_s
+ end
end
module Quiz
@@ -91,17 +95,17 @@ class TestController < ApplicationController
# :ported:
def render_hello_world
- render :template => "test/hello_world"
+ render "test/hello_world"
end
def render_hello_world_with_last_modified_set
response.last_modified = Date.new(2008, 10, 10).to_time
- render :template => "test/hello_world"
+ render "test/hello_world"
end
# :ported: compatibility
def render_hello_world_with_forward_slash
- render :template => "/test/hello_world"
+ render "/test/hello_world"
end
# :ported:
@@ -111,13 +115,13 @@ class TestController < ApplicationController
# :deprecated:
def render_template_in_top_directory_with_slash
- render :template => '/shared'
+ render '/shared'
end
# :ported:
def render_hello_world_from_variable
@person = "david"
- render :text => "hello #{@person}"
+ render plain: "hello #{@person}"
end
# :ported:
@@ -139,13 +143,13 @@ class TestController < ApplicationController
# :ported:
def render_text_hello_world
- render :text => "hello world"
+ render plain: "hello world"
end
# :ported:
def render_text_hello_world_with_layout
@variable_for_layout = ", I am here!"
- render :text => "hello world", :layout => true
+ render plain: "hello world", :layout => true
end
def hello_world_with_layout_false
@@ -160,13 +164,6 @@ class TestController < ApplicationController
end
# :ported:
- def render_file_as_string_with_instance_variables
- @secret = 'in the sauce'
- path = File.expand_path(File.join(File.dirname(__FILE__), '../../fixtures/test/render_file_with_ivar'))
- render path
- end
-
- # :ported:
def render_file_not_using_full_path
@secret = 'in the sauce'
render :file => 'test/render_file_with_ivar'
@@ -194,7 +191,7 @@ class TestController < ApplicationController
def render_file_as_string_with_locals
path = File.expand_path(File.join(File.dirname(__FILE__), '../../fixtures/test/render_file_with_locals'))
- render path, :locals => {:secret => 'in the sauce'}
+ render file: path, :locals => {:secret => 'in the sauce'}
end
def accessing_request_in_template
@@ -215,26 +212,26 @@ class TestController < ApplicationController
# :ported:
def render_custom_code
- render :text => "hello world", :status => 404
+ render plain: "hello world", :status => 404
end
# :ported:
def render_text_with_nil
- render :text => nil
+ render plain: nil
end
# :ported:
def render_text_with_false
- render :text => false
+ render plain: false
end
def render_text_with_resource
- render :text => Customer.new("David")
+ render plain: Customer.new("David")
end
# :ported:
def render_nothing_with_appendix
- render :text => "appended"
+ render plain: "appended"
end
# This test is testing 3 things:
@@ -265,7 +262,7 @@ class TestController < ApplicationController
# :ported:
def blank_response
- render :text => ' '
+ render plain: ' '
end
# :ported:
@@ -297,7 +294,7 @@ class TestController < ApplicationController
def hello_in_a_string
@customers = [ Customer.new("david"), Customer.new("mary") ]
- render :text => "How's there? " + render_to_string(:template => "test/list")
+ render plain: "How's there? " + render_to_string(:template => "test/list")
end
def accessing_params_in_template
@@ -360,12 +357,12 @@ class TestController < ApplicationController
end
def rendering_nothing_on_layout
- render :nothing => true
+ head :ok
end
def render_to_string_with_assigns
@before = "i'm before the render"
- render_to_string :text => "foo"
+ render_to_string plain: "foo"
@after = "i'm after the render"
render :template => "test/hello_world"
end
@@ -412,8 +409,8 @@ class TestController < ApplicationController
# :ported:
def double_render
- render :text => "hello"
- render :text => "world"
+ render plain: "hello"
+ render plain: "world"
end
def double_redirect
@@ -422,13 +419,13 @@ class TestController < ApplicationController
end
def render_and_redirect
- render :text => "hello"
+ render plain: "hello"
redirect_to :action => "double_render"
end
def render_to_string_and_render
- @stuff = render_to_string :text => "here is some cached stuff"
- render :text => "Hi web users! #{@stuff}"
+ @stuff = render_to_string plain: "here is some cached stuff"
+ render plain: "Hi web users! #{@stuff}"
end
def render_to_string_with_inline_and_render
@@ -457,7 +454,11 @@ class TestController < ApplicationController
# :addressed:
def render_text_with_assigns
@hello = "world"
- render :text => "foo"
+ render plain: "foo"
+ end
+
+ def render_with_assigns_option
+ render inline: '<%= @hello %>', assigns: { hello: "world" }
end
def yield_content_for
@@ -465,8 +466,8 @@ class TestController < ApplicationController
end
def render_content_type_from_body
- response.content_type = Mime::RSS
- render :text => "hello world!"
+ response.content_type = Mime[:rss]
+ render body: "hello world!"
end
def render_using_layout_around_block
@@ -682,20 +683,19 @@ class RenderTest < ActionController::TestCase
get :hello_world
assert_response 200
assert_response :success
- assert_template "test/hello_world"
assert_equal "<html>Hello world!</html>", @response.body
end
# :ported:
def test_renders_default_template_for_missing_action
get :'hyphen-ated'
- assert_template 'test/hyphen-ated'
+ assert_equal "hyphen-ated.erb", @response.body
end
# :ported:
def test_render
get :render_hello_world
- assert_template "test/hello_world"
+ assert_equal "Hello world!", @response.body
end
def test_line_offset
@@ -711,26 +711,24 @@ class RenderTest < ActionController::TestCase
# :ported: compatibility
def test_render_with_forward_slash
get :render_hello_world_with_forward_slash
- assert_template "test/hello_world"
+ assert_equal "Hello world!", @response.body
end
# :ported:
def test_render_in_top_directory
get :render_template_in_top_directory
- assert_template "shared"
assert_equal "Elastica", @response.body
end
# :ported:
def test_render_in_top_directory_with_slash
get :render_template_in_top_directory_with_slash
- assert_template "shared"
assert_equal "Elastica", @response.body
end
def test_render_process
get :render_action_hello_world_as_string
- assert_equal ["Hello world!"], @controller.process(:render_action_hello_world_as_string)
+ assert_equal "Hello world!", @controller.process(:render_action_hello_world_as_string)
end
# :ported:
@@ -742,7 +740,7 @@ class RenderTest < ActionController::TestCase
# :ported:
def test_render_action
get :render_action_hello_world
- assert_template "test/hello_world"
+ assert_equal "Hello world!", @response.body
end
def test_render_action_upcased
@@ -755,13 +753,12 @@ class RenderTest < ActionController::TestCase
def test_render_action_hello_world_as_string
get :render_action_hello_world_as_string
assert_equal "Hello world!", @response.body
- assert_template "test/hello_world"
end
# :ported:
def test_render_action_with_symbol
get :render_action_hello_world_with_symbol
- assert_template "test/hello_world"
+ assert_equal "Hello world!", @response.body
end
# :ported:
@@ -773,7 +770,7 @@ class RenderTest < ActionController::TestCase
# :ported:
def test_do_with_render_text_and_layout
get :render_text_hello_world_with_layout
- assert_equal "<html>hello world, I am here!</html>", @response.body
+ assert_equal "{{hello world, I am here!}}\n", @response.body
end
# :ported:
@@ -794,12 +791,6 @@ class RenderTest < ActionController::TestCase
end
# :ported:
- def test_render_file_as_string_with_instance_variables
- get :render_file_as_string_with_instance_variables
- assert_equal "The secret is in the sauce\n", @response.body
- end
-
- # :ported:
def test_render_file_not_using_full_path
get :render_file_not_using_full_path
assert_equal "The secret is in the sauce\n", @response.body
@@ -870,12 +861,12 @@ class RenderTest < ActionController::TestCase
# :ported:
def test_attempt_to_access_object_method
- assert_raise(AbstractController::ActionNotFound, "No action responded to [clone]") { get :clone }
+ assert_raise(AbstractController::ActionNotFound) { get :clone }
end
# :ported:
def test_private_methods
- assert_raise(AbstractController::ActionNotFound, "No action responded to [determine_layout]") { get :determine_layout }
+ assert_raise(AbstractController::ActionNotFound) { get :determine_layout }
end
# :ported:
@@ -955,7 +946,7 @@ class RenderTest < ActionController::TestCase
def test_render_to_string_inline
get :render_to_string_with_inline_and_render
- assert_template "test/hello_world"
+ assert_equal 'Hello world!', @response.body
end
# :ported:
@@ -966,23 +957,23 @@ class RenderTest < ActionController::TestCase
end
def test_accessing_params_in_template
- get :accessing_params_in_template, :name => "David"
+ get :accessing_params_in_template, params: { name: "David" }
assert_equal "Hello: David", @response.body
end
def test_accessing_local_assigns_in_inline_template
- get :accessing_local_assigns_in_inline_template, :local_name => "Local David"
+ get :accessing_local_assigns_in_inline_template, params: { local_name: "Local David" }
assert_equal "Goodbye, Local David", @response.body
assert_equal "text/html", @response.content_type
end
def test_should_implicitly_render_html_template_from_xhr_request
- xhr :get, :render_implicit_html_template_from_xhr_request
+ get :render_implicit_html_template_from_xhr_request, xhr: true
assert_equal "XHR!\nHello HTML!", @response.body
end
def test_should_implicitly_render_js_template_without_layout
- xhr :get, :render_implicit_js_template_without_layout, :format => :js
+ get :render_implicit_js_template_without_layout, format: :js, xhr: true
assert_no_match %r{<html>}, @response.body
end
@@ -1040,8 +1031,8 @@ class RenderTest < ActionController::TestCase
def test_render_to_string_doesnt_break_assigns
get :render_to_string_with_assigns
- assert_equal "i'm before the render", assigns(:before)
- assert_equal "i'm after the render", assigns(:after)
+ assert_equal "i'm before the render", @controller.instance_variable_get(:@before)
+ assert_equal "i'm after the render", @controller.instance_variable_get(:@after)
end
def test_bad_render_to_string_still_throws_exception
@@ -1050,12 +1041,12 @@ class RenderTest < ActionController::TestCase
def test_render_to_string_that_throws_caught_exception_doesnt_break_assigns
assert_nothing_raised { get :render_to_string_with_caught_exception }
- assert_equal "i'm before the render", assigns(:before)
- assert_equal "i'm after the render", assigns(:after)
+ assert_equal "i'm before the render", @controller.instance_variable_get(:@before)
+ assert_equal "i'm after the render", @controller.instance_variable_get(:@after)
end
def test_accessing_params_in_template_with_layout
- get :accessing_params_in_template_with_layout, :name => "David"
+ get :accessing_params_in_template_with_layout, params: { name: "David" }
assert_equal "<html>Hello: David</html>", @response.body
end
@@ -1112,7 +1103,12 @@ class RenderTest < ActionController::TestCase
# :addressed:
def test_render_text_with_assigns
get :render_text_with_assigns
- assert_equal "world", assigns["hello"]
+ assert_equal "world", @controller.instance_variable_get(:@hello)
+ end
+
+ def test_render_text_with_assigns_option
+ get :render_with_assigns_option
+ assert_equal 'world', response.body
end
# :ported:
@@ -1126,7 +1122,7 @@ class RenderTest < ActionController::TestCase
assert_equal "<title>Putting stuff in the title!</title>\nGreat stuff!\n", @response.body
end
- def test_overwritting_rendering_relative_file_with_extension
+ def test_overwriting_rendering_relative_file_with_extension
get :hello_world_from_rxml_using_template
assert_equal "<html>\n <p>Hello</p>\n</html>\n", @response.body
@@ -1173,22 +1169,22 @@ class RenderTest < ActionController::TestCase
def test_render_to_string_partial
get :render_to_string_with_partial
- assert_equal "only partial", assigns(:partial_only)
- assert_equal "Hello: david", assigns(:partial_with_locals)
+ assert_equal "only partial", @controller.instance_variable_get(:@partial_only)
+ assert_equal "Hello: david", @controller.instance_variable_get(:@partial_with_locals)
assert_equal "text/html", @response.content_type
end
def test_render_to_string_with_template_and_html_partial
get :render_to_string_with_template_and_html_partial
- assert_equal "**only partial**\n", assigns(:text)
- assert_equal "<strong>only partial</strong>\n", assigns(:html)
+ assert_equal "**only partial**\n", @controller.instance_variable_get(:@text)
+ assert_equal "<strong>only partial</strong>\n", @controller.instance_variable_get(:@html)
assert_equal "<strong>only html partial</strong>\n", @response.body
assert_equal "text/html", @response.content_type
end
def test_render_to_string_and_render_with_different_formats
get :render_to_string_and_render_with_different_formats
- assert_equal "<strong>only partial</strong>\n", assigns(:html)
+ assert_equal "<strong>only partial</strong>\n", @controller.instance_variable_get(:@html)
assert_equal "**only partial**\n", @response.body
assert_equal "text/plain", @response.content_type
end
@@ -1212,21 +1208,18 @@ class RenderTest < ActionController::TestCase
def test_partial_with_form_builder
get :partial_with_form_builder
- assert_match(/<label/, @response.body)
- assert_template('test/_form')
+ assert_equal "<label for=\"post_title\">Title</label>\n", @response.body
end
def test_partial_with_form_builder_subclass
get :partial_with_form_builder_subclass
- assert_match(/<label/, @response.body)
- assert_template('test/_labelling_form')
+ assert_equal "<label for=\"post_title\">Title</label>\n", @response.body
end
def test_nested_partial_with_form_builder
@controller = Fun::GamesController.new
get :nested_partial_with_form_builder
- assert_match(/<label/, @response.body)
- assert_template('fun/games/_form')
+ assert_equal "<label for=\"post_title\">Title</label>\n", @response.body
end
def test_namespaced_object_partial
@@ -1270,48 +1263,29 @@ class RenderTest < ActionController::TestCase
assert_equal "Bonjour: davidBonjour: mary", @response.body
end
- def test_locals_option_to_assert_template_is_not_supported
- get :partial_collection_with_locals
-
- warning_buffer = StringIO.new
- $stderr = warning_buffer
-
- assert_template partial: 'customer_greeting', locals: { greeting: 'Bonjour' }
- assert_equal "the :locals option to #assert_template is only supported in a ActionView::TestCase\n", warning_buffer.string
- ensure
- $stderr = STDERR
- end
-
def test_partial_collection_with_spacer
get :partial_collection_with_spacer
assert_equal "Hello: davidonly partialHello: mary", @response.body
- assert_template :partial => '_customer'
end
def test_partial_collection_with_spacer_which_uses_render
get :partial_collection_with_spacer_which_uses_render
assert_equal "Hello: davidpartial html\npartial with partial\nHello: mary", @response.body
- assert_template :partial => '_customer'
end
def test_partial_collection_shorthand_with_locals
get :partial_collection_shorthand_with_locals
assert_equal "Bonjour: davidBonjour: mary", @response.body
- assert_template :partial => 'customers/_customer', :count => 2
- assert_template :partial => '_completely_fake_and_made_up_template_that_cannot_possibly_be_rendered', :count => 0
end
def test_partial_collection_shorthand_with_different_types_of_records
get :partial_collection_shorthand_with_different_types_of_records
assert_equal "Bonjour bad customer: mark0Bonjour good customer: craig1Bonjour bad customer: john2Bonjour good customer: zach3Bonjour good customer: brandon4Bonjour bad customer: dan5", @response.body
- assert_template :partial => 'good_customers/_good_customer', :count => 3
- assert_template :partial => 'bad_customers/_bad_customer', :count => 3
end
def test_empty_partial_collection
get :empty_partial_collection
assert_equal " ", @response.body
- assert_template :partial => false
end
def test_partial_with_hash_object
diff --git a/actionview/test/actionpack/controller/view_paths_test.rb b/actionview/test/actionpack/controller/view_paths_test.rb
index c6e7a523b9..e99659c802 100644
--- a/actionview/test/actionpack/controller/view_paths_test.rb
+++ b/actionview/test/actionpack/controller/view_paths_test.rb
@@ -23,8 +23,8 @@ class ViewLoadPathsTest < ActionController::TestCase
end
def setup
- @request = ActionController::TestRequest.new
- @response = ActionController::TestResponse.new
+ @request = ActionController::TestRequest.create
+ @response = ActionDispatch::TestResponse.new
@controller = TestController.new
@paths = TestController.view_paths
end
@@ -39,7 +39,7 @@ class ViewLoadPathsTest < ActionController::TestCase
def assert_paths(*paths)
controller = paths.first.is_a?(Class) ? paths.shift : @controller
- assert_equal expand(paths), controller.view_paths.map { |p| p.to_s }
+ assert_equal expand(paths), controller.view_paths.map(&:to_s)
end
def test_template_load_path_was_set_correctly
diff --git a/actionview/test/active_record_unit.rb b/actionview/test/active_record_unit.rb
index cca55c9af4..f9e94413b5 100644
--- a/actionview/test/active_record_unit.rb
+++ b/actionview/test/active_record_unit.rb
@@ -76,7 +76,7 @@ class ActiveRecordTestCase < ActionController::TestCase
# Set our fixture path
if ActiveRecordTestConnector.able_to_connect
self.fixture_path = [FIXTURE_LOAD_PATH]
- self.use_transactional_fixtures = false
+ self.use_transactional_tests = false
end
def self.fixtures(*args)
diff --git a/actionview/test/activerecord/controller_runtime_test.rb b/actionview/test/activerecord/controller_runtime_test.rb
index 368bec1c70..af91348d76 100644
--- a/actionview/test/activerecord/controller_runtime_test.rb
+++ b/actionview/test/activerecord/controller_runtime_test.rb
@@ -4,12 +4,10 @@ require 'fixtures/project'
require 'active_support/log_subscriber/test_helper'
require 'action_controller/log_subscriber'
-ActionController::Base.send :include, ActiveRecord::Railties::ControllerRuntime
+ActionController::Base.include(ActiveRecord::Railties::ControllerRuntime)
class ControllerRuntimeLogSubscriberTest < ActionController::TestCase
class LogSubscriberController < ActionController::Base
- respond_to :html
-
def show
render :inline => "<%= Project.all %>"
end
@@ -20,8 +18,8 @@ class ControllerRuntimeLogSubscriberTest < ActionController::TestCase
def create
ActiveRecord::LogSubscriber.runtime += 100
- project = Project.last
- respond_with(project, location: url_for(action: :show))
+ Project.last
+ redirect_to "/"
end
def redirect
diff --git a/actionview/test/template/debug_helper_test.rb b/actionview/test/activerecord/debug_helper_test.rb
index 5609694cd5..03cb1d5a91 100644
--- a/actionview/test/template/debug_helper_test.rb
+++ b/actionview/test/activerecord/debug_helper_test.rb
@@ -1,8 +1,14 @@
require 'active_record_unit'
+require 'nokogiri'
class DebugHelperTest < ActionView::TestCase
def test_debug
company = Company.new(name: "firebase")
assert_match "name: firebase", debug(company)
end
+
+ def test_debug_with_marshal_error
+ obj = -> { }
+ assert_match obj.inspect, Nokogiri.XML(debug(obj)).content
+ end
end
diff --git a/actionview/test/activerecord/form_helper_activerecord_test.rb b/actionview/test/activerecord/form_helper_activerecord_test.rb
index 0a62f49f35..2769b97445 100644
--- a/actionview/test/activerecord/form_helper_activerecord_test.rb
+++ b/actionview/test/activerecord/form_helper_activerecord_test.rb
@@ -35,10 +35,6 @@ class FormHelperActiveRecordTest < ActionView::TestCase
end
end
- def _routes
- Routes
- end
-
include Routes.url_helpers
def test_nested_fields_for_with_child_index_option_override_on_a_nested_attributes_collection_association
diff --git a/actionview/test/activerecord/polymorphic_routes_test.rb b/actionview/test/activerecord/polymorphic_routes_test.rb
index e220dcb8cb..34b2698c7f 100644
--- a/actionview/test/activerecord/polymorphic_routes_test.rb
+++ b/actionview/test/activerecord/polymorphic_routes_test.rb
@@ -25,17 +25,19 @@ class Series < ActiveRecord::Base
self.table_name = 'projects'
end
-class ModelDelegator < ActiveRecord::Base
- self.table_name = 'projects'
-
+class ModelDelegator
def to_model
ModelDelegate.new
end
end
class ModelDelegate
- def self.model_name
- ActiveModel::Name.new(self)
+ def persisted?
+ true
+ end
+
+ def model_name
+ ActiveModel::Name.new(self.class)
end
def to_param
@@ -111,7 +113,7 @@ class PolymorphicRoutesTest < ActionController::TestCase
def test_passing_routes_proxy
with_namespaced_routes(:blog) do
- proxy = ActionDispatch::Routing::RoutesProxy.new(_routes, self)
+ proxy = ActionDispatch::Routing::RoutesProxy.new(_routes, self, _routes.url_helpers)
@blog_post.save
assert_url "http://example.com/posts/#{@blog_post.id}", [proxy, @blog_post]
end
@@ -183,16 +185,33 @@ class PolymorphicRoutesTest < ActionController::TestCase
end
end
- def test_with_nil_in_list
+ def test_with_entirely_nil_list
with_test_routes do
exception = assert_raise ArgumentError do
@series.save
- polymorphic_url([nil, @series])
+ polymorphic_url([nil, nil])
end
assert_equal "Nil location provided. Can't build URI.", exception.message
end
end
+ def test_with_nil_in_list_for_resource_that_could_be_top_level_or_nested
+ with_top_level_and_nested_routes do
+ @blog_post.save
+ assert_equal "http://example.com/posts/#{@blog_post.id}", polymorphic_url([nil, @blog_post])
+ end
+ end
+
+ def test_with_nil_in_list_does_not_generate_invalid_link
+ with_top_level_and_nested_routes do
+ exception = assert_raise NoMethodError do
+ @series.save
+ polymorphic_url([nil, @series])
+ end
+ assert_match(/undefined method `series_url'/, exception.message)
+ end
+ end
+
def test_with_record
with_test_routes do
@project.save
@@ -265,6 +284,15 @@ class PolymorphicRoutesTest < ActionController::TestCase
end
end
+ def test_regression_path_helper_prefixed_with_new_and_edit
+ with_test_routes do
+ assert_equal "/projects/new", new_polymorphic_path(@project)
+
+ @project.save
+ assert_equal "/projects/#{@project.id}/edit", edit_polymorphic_path(@project)
+ end
+ end
+
def test_url_helper_prefixed_with_edit
with_test_routes do
@project.save
@@ -579,13 +607,18 @@ class PolymorphicRoutesTest < ActionController::TestCase
end
end
- def test_routing_a_to_model_delegate
+ def test_routing_to_a_model_delegate
with_test_routes do
- @delegator.save
assert_url "http://example.com/model_delegates/overridden", @delegator
end
end
+ def test_nested_routing_to_a_model_delegate
+ with_test_routes do
+ assert_url "http://example.com/foo/model_delegates/overridden", [:foo, @delegator]
+ end
+ end
+
def with_namespaced_routes(name)
with_routing do |set|
set.draw do
@@ -619,6 +652,24 @@ class PolymorphicRoutesTest < ActionController::TestCase
end
resources :series
resources :model_delegates
+ namespace :foo do
+ resources :model_delegates
+ end
+ end
+
+ extend @routes.url_helpers
+ yield
+ end
+ end
+
+ def with_top_level_and_nested_routes(options = {})
+ with_routing do |set|
+ set.draw do
+ resources :blogs do
+ resources :posts
+ resources :series
+ end
+ resources :posts
end
extend @routes.url_helpers
diff --git a/actionview/test/activerecord/relation_cache_test.rb b/actionview/test/activerecord/relation_cache_test.rb
new file mode 100644
index 0000000000..8e97417b94
--- /dev/null
+++ b/actionview/test/activerecord/relation_cache_test.rb
@@ -0,0 +1,18 @@
+require 'active_record_unit'
+
+class RelationCacheTest < ActionView::TestCase
+ tests ActionView::Helpers::CacheHelper
+
+ def setup
+ @virtual_path = "path"
+ controller.cache_store = ActiveSupport::Cache::MemoryStore.new
+ end
+
+ def test_cache_relation_other
+ cache(Project.all){ concat("Hello World") }
+ assert_equal "Hello World", controller.cache_store.read("views/projects-#{Project.count}/")
+ end
+
+ def view_cache_dependencies; end
+
+end
diff --git a/actionview/test/activerecord/render_partial_with_record_identification_test.rb b/actionview/test/activerecord/render_partial_with_record_identification_test.rb
index 409370104d..9772ebb39e 100644
--- a/actionview/test/activerecord/render_partial_with_record_identification_test.rb
+++ b/actionview/test/activerecord/render_partial_with_record_identification_test.rb
@@ -52,43 +52,37 @@ class RenderPartialWithRecordIdentificationTest < ActiveRecordTestCase
def test_rendering_partial_with_has_many_and_belongs_to_association
get :render_with_has_many_and_belongs_to_association
- assert_template 'projects/_project'
- assert_equal assigns(:developer).projects.map(&:name).join, @response.body
+ assert_equal Developer.find(1).projects.map(&:name).join, @response.body
end
def test_rendering_partial_with_has_many_association
get :render_with_has_many_association
- assert_template 'replies/_reply'
assert_equal 'Birdman is better!', @response.body
end
def test_rendering_partial_with_scope
get :render_with_scope
- assert_template 'replies/_reply'
assert_equal 'Birdman is better!Nuh uh!', @response.body
end
def test_render_with_record
get :render_with_record
- assert_template 'developers/_developer'
assert_equal 'David', @response.body
end
def test_render_with_record_collection
get :render_with_record_collection
- assert_template 'developers/_developer'
assert_equal 'DavidJamisfixture_3fixture_4fixture_5fixture_6fixture_7fixture_8fixture_9fixture_10Jamis', @response.body
end
def test_render_with_record_collection_and_spacer_template
get :render_with_record_collection_and_spacer_template
- assert_equal assigns(:developer).projects.map(&:name).join('only partial'), @response.body
+ assert_equal Developer.find(1).projects.map(&:name).join('only partial'), @response.body
end
def test_rendering_partial_with_has_one_association
mascot = Company.find(1).mascot
get :render_with_has_one_association
- assert_template 'mascots/_mascot'
assert_equal mascot.name, @response.body
end
end
@@ -130,13 +124,11 @@ class RenderPartialWithRecordIdentificationAndNestedControllersTest < ActiveReco
def test_render_with_record_in_nested_controller
get :render_with_record_in_nested_controller
- assert_template %r{\Afun/games/_game\Z}
assert_equal "Fun Pong\n", @response.body
end
def test_render_with_record_collection_in_nested_controller
get :render_with_record_collection_in_nested_controller
- assert_template %r{\Afun/games/_game\Z}
assert_equal "Fun Pong\nFun Tank\n", @response.body
end
end
@@ -149,7 +141,6 @@ class RenderPartialWithRecordIdentificationAndNestedControllersWithoutPrefixTest
ActionView::Base.prefix_partial_path_with_controller_namespace = false
get :render_with_record_in_nested_controller
- assert_template %r{\Agames/_game\Z}
assert_equal "Just Pong\n", @response.body
ensure
ActionView::Base.prefix_partial_path_with_controller_namespace = old_config
@@ -160,7 +151,6 @@ class RenderPartialWithRecordIdentificationAndNestedControllersWithoutPrefixTest
ActionView::Base.prefix_partial_path_with_controller_namespace = false
get :render_with_record_collection_in_nested_controller
- assert_template %r{\Agames/_game\Z}
assert_equal "Just Pong\nJust Tank\n", @response.body
ensure
ActionView::Base.prefix_partial_path_with_controller_namespace = old_config
@@ -172,13 +162,11 @@ class RenderPartialWithRecordIdentificationAndNestedDeeperControllersTest < Acti
def test_render_with_record_in_deeper_nested_controller
get :render_with_record_in_deeper_nested_controller
- assert_template %r{\Afun/serious/games/_game\Z}
assert_equal "Serious Chess\n", @response.body
end
def test_render_with_record_collection_in_deeper_nested_controller
get :render_with_record_collection_in_deeper_nested_controller
- assert_template %r{\Afun/serious/games/_game\Z}
assert_equal "Serious Chess\nSerious Sudoku\nSerious Solitaire\n", @response.body
end
end
@@ -191,7 +179,6 @@ class RenderPartialWithRecordIdentificationAndNestedDeeperControllersWithoutPref
ActionView::Base.prefix_partial_path_with_controller_namespace = false
get :render_with_record_in_deeper_nested_controller
- assert_template %r{\Agames/_game\Z}
assert_equal "Just Chess\n", @response.body
ensure
ActionView::Base.prefix_partial_path_with_controller_namespace = old_config
@@ -202,7 +189,6 @@ class RenderPartialWithRecordIdentificationAndNestedDeeperControllersWithoutPref
ActionView::Base.prefix_partial_path_with_controller_namespace = false
get :render_with_record_collection_in_deeper_nested_controller
- assert_template %r{\Agames/_game\Z}
assert_equal "Just Chess\nJust Sudoku\nJust Solitaire\n", @response.body
ensure
ActionView::Base.prefix_partial_path_with_controller_namespace = old_config
diff --git a/actionview/test/fixtures/actionpack/layout_tests/alt/layouts/alt.erb b/actionview/test/fixtures/actionpack/layout_tests/alt/layouts/alt.erb
index e69de29bb2..60b81525b5 100644
--- a/actionview/test/fixtures/actionpack/layout_tests/alt/layouts/alt.erb
+++ b/actionview/test/fixtures/actionpack/layout_tests/alt/layouts/alt.erb
@@ -0,0 +1 @@
+alt.erb
diff --git a/actionview/test/fixtures/actionpack/layouts/standard.text.erb b/actionview/test/fixtures/actionpack/layouts/standard.text.erb
new file mode 100644
index 0000000000..a58afb1aa2
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/layouts/standard.text.erb
@@ -0,0 +1 @@
+{{<%= yield %><%= @variable_for_layout %>}}
diff --git a/actionview/test/fixtures/actionpack/test/hyphen-ated.erb b/actionview/test/fixtures/actionpack/test/hyphen-ated.erb
index cd0875583a..28dbe94ee1 100644
--- a/actionview/test/fixtures/actionpack/test/hyphen-ated.erb
+++ b/actionview/test/fixtures/actionpack/test/hyphen-ated.erb
@@ -1 +1 @@
-Hello world!
+hyphen-ated.erb \ No newline at end of file
diff --git a/actionview/test/fixtures/blog_public/.gitignore b/actionview/test/fixtures/blog_public/.gitignore
deleted file mode 100644
index 312e635ee6..0000000000
--- a/actionview/test/fixtures/blog_public/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-absolute/*
diff --git a/actionview/test/fixtures/blog_public/blog.html b/actionview/test/fixtures/blog_public/blog.html
deleted file mode 100644
index 79ad44c010..0000000000
--- a/actionview/test/fixtures/blog_public/blog.html
+++ /dev/null
@@ -1 +0,0 @@
-/blog/blog.html \ No newline at end of file
diff --git a/actionview/test/fixtures/blog_public/index.html b/actionview/test/fixtures/blog_public/index.html
deleted file mode 100644
index 2de3825481..0000000000
--- a/actionview/test/fixtures/blog_public/index.html
+++ /dev/null
@@ -1 +0,0 @@
-/blog/index.html \ No newline at end of file
diff --git a/actionview/test/fixtures/blog_public/subdir/index.html b/actionview/test/fixtures/blog_public/subdir/index.html
deleted file mode 100644
index 517bded335..0000000000
--- a/actionview/test/fixtures/blog_public/subdir/index.html
+++ /dev/null
@@ -1 +0,0 @@
-/blog/subdir/index.html \ No newline at end of file
diff --git a/actionview/test/fixtures/digestor/comments/_comment.html.erb b/actionview/test/fixtures/digestor/comments/_comment.html.erb
index f172e749da..a8fa21f644 100644
--- a/actionview/test/fixtures/digestor/comments/_comment.html.erb
+++ b/actionview/test/fixtures/digestor/comments/_comment.html.erb
@@ -1 +1 @@
-Great story, bro!
+Great story!
diff --git a/actionview/test/fixtures/digestor/events/_completed.html.erb b/actionview/test/fixtures/digestor/events/_completed.html.erb
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/actionview/test/fixtures/digestor/events/_completed.html.erb
diff --git a/actionview/test/fixtures/digestor/events/index.html.erb b/actionview/test/fixtures/digestor/events/index.html.erb
new file mode 100644
index 0000000000..bc45e41bcb
--- /dev/null
+++ b/actionview/test/fixtures/digestor/events/index.html.erb
@@ -0,0 +1 @@
+<% # Template Dependency: events/* %> \ No newline at end of file
diff --git a/actionview/test/fixtures/functional_caching/fragment_cached_without_digest.html.erb b/actionview/test/fixtures/functional_caching/fragment_cached_without_digest.html.erb
deleted file mode 100644
index 3125583a28..0000000000
--- a/actionview/test/fixtures/functional_caching/fragment_cached_without_digest.html.erb
+++ /dev/null
@@ -1,3 +0,0 @@
-<body>
-<%= cache 'nodigest', skip_digest: true do %><p>ERB</p><% end %>
-</body>
diff --git a/actionview/test/fixtures/happy_path/render_action/hello_world.erb b/actionview/test/fixtures/happy_path/render_action/hello_world.erb
deleted file mode 100644
index 6769dd60bd..0000000000
--- a/actionview/test/fixtures/happy_path/render_action/hello_world.erb
+++ /dev/null
@@ -1 +0,0 @@
-Hello world! \ No newline at end of file
diff --git a/actionview/test/fixtures/layouts/streaming_with_capture.erb b/actionview/test/fixtures/layouts/streaming_with_capture.erb
new file mode 100644
index 0000000000..538c19ce3a
--- /dev/null
+++ b/actionview/test/fixtures/layouts/streaming_with_capture.erb
@@ -0,0 +1,6 @@
+<%= yield :header -%>
+<%= capture do %>
+ this works
+<% end %>
+<%= yield :footer -%>
+<%= yield(:unknown).presence || "." -%>
diff --git a/actionview/test/fixtures/multipart/bracketed_utf8_param b/actionview/test/fixtures/multipart/bracketed_utf8_param
deleted file mode 100644
index df9cecea08..0000000000
--- a/actionview/test/fixtures/multipart/bracketed_utf8_param
+++ /dev/null
@@ -1,5 +0,0 @@
---AaB03x
-Content-Disposition: form-data; name="Iñtërnâtiônàlizætiøn_name[Iñtërnâtiônàlizætiøn_nested_name]"
-
-Iñtërnâtiônàlizætiøn_value
---AaB03x--
diff --git a/actionview/test/fixtures/multipart/single_utf8_param b/actionview/test/fixtures/multipart/single_utf8_param
deleted file mode 100644
index 1d9fae7b17..0000000000
--- a/actionview/test/fixtures/multipart/single_utf8_param
+++ /dev/null
@@ -1,5 +0,0 @@
---AaB03x
-Content-Disposition: form-data; name="Iñtërnâtiônàlizætiøn_name"
-
-Iñtërnâtiônàlizætiøn_value
---AaB03x--
diff --git a/actionview/test/fixtures/project.rb b/actionview/test/fixtures/project.rb
index c124a9e605..404b12cbab 100644
--- a/actionview/test/fixtures/project.rb
+++ b/actionview/test/fixtures/project.rb
@@ -1,3 +1,7 @@
class Project < ActiveRecord::Base
has_and_belongs_to_many :developers, -> { uniq }
+
+ def self.collection_cache_key(collection = all, timestamp_column = :updated_at)
+ "projects-#{collection.count}"
+ end
end
diff --git a/actionview/test/fixtures/scope/test/modgreet.erb b/actionview/test/fixtures/scope/test/modgreet.erb
deleted file mode 100644
index 8947726e89..0000000000
--- a/actionview/test/fixtures/scope/test/modgreet.erb
+++ /dev/null
@@ -1 +0,0 @@
-<p>Beautiful modules!</p> \ No newline at end of file
diff --git a/actionview/test/fixtures/test/_FooBar.html.erb b/actionview/test/fixtures/test/_FooBar.html.erb
new file mode 100644
index 0000000000..4bbe59410a
--- /dev/null
+++ b/actionview/test/fixtures/test/_FooBar.html.erb
@@ -0,0 +1 @@
+🍣
diff --git a/actionview/test/fixtures/test/_a-in.html.erb b/actionview/test/fixtures/test/_a-in.html.erb
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/actionview/test/fixtures/test/_a-in.html.erb
diff --git a/actionview/test/fixtures/test/_cached_customer.erb b/actionview/test/fixtures/test/_cached_customer.erb
new file mode 100644
index 0000000000..52f35a3497
--- /dev/null
+++ b/actionview/test/fixtures/test/_cached_customer.erb
@@ -0,0 +1,3 @@
+<% cache cached_customer do %>
+ Hello: <%= cached_customer.name %>
+<% end %> \ No newline at end of file
diff --git a/actionview/test/fixtures/test/_cached_customer_as.erb b/actionview/test/fixtures/test/_cached_customer_as.erb
new file mode 100644
index 0000000000..fca8d19e34
--- /dev/null
+++ b/actionview/test/fixtures/test/_cached_customer_as.erb
@@ -0,0 +1,3 @@
+<% cache buyer do %>
+ <%= greeting %>: <%= customer.name %>
+<% end %> \ No newline at end of file
diff --git a/actionview/test/fixtures/test/_label_with_block.erb b/actionview/test/fixtures/test/_label_with_block.erb
index 40117e594e..94089ea93d 100644
--- a/actionview/test/fixtures/test/_label_with_block.erb
+++ b/actionview/test/fixtures/test/_label_with_block.erb
@@ -1,4 +1,4 @@
-<%= label 'post', 'message' do %>
+<%= label('post', 'message')do %>
Message
<%= text_field 'post', 'message' %>
<% end %>
diff --git a/actionview/test/fixtures/test/_partial_name_in_local_assigns.erb b/actionview/test/fixtures/test/_partial_name_in_local_assigns.erb
new file mode 100644
index 0000000000..28ee9f41c5
--- /dev/null
+++ b/actionview/test/fixtures/test/_partial_name_in_local_assigns.erb
@@ -0,0 +1 @@
+<%= local_assigns.has_key?(:partial_name_in_local_assigns) %> \ No newline at end of file
diff --git a/actionview/test/fixtures/test/_partial_shortcut_with_block_content.html.erb b/actionview/test/fixtures/test/_partial_shortcut_with_block_content.html.erb
new file mode 100644
index 0000000000..352128f3ba
--- /dev/null
+++ b/actionview/test/fixtures/test/_partial_shortcut_with_block_content.html.erb
@@ -0,0 +1,3 @@
+<%= render "test/layout_for_block_with_args" do |arg_1, arg_2| %>
+ Yielded: <%= arg_1 %>/<%= arg_2 %>
+<% end %>
diff --git a/actionview/test/fixtures/test/nil_return.erb b/actionview/test/fixtures/test/nil_return.erb
new file mode 100644
index 0000000000..90ce3881f6
--- /dev/null
+++ b/actionview/test/fixtures/test/nil_return.erb
@@ -0,0 +1 @@
+This is nil: <%== nil %>
diff --git a/actionview/test/lib/controller/fake_models.rb b/actionview/test/lib/controller/fake_models.rb
index a463a08bb6..65c68fc34a 100644
--- a/actionview/test/lib/controller/fake_models.rb
+++ b/actionview/test/lib/controller/fake_models.rb
@@ -54,6 +54,22 @@ class Post < Struct.new(:title, :author_name, :body, :secret, :persisted, :writt
def tags_attributes=(attributes); end
end
+class PostDelegator < Post
+ def to_model
+ PostDelegate.new
+ end
+end
+
+class PostDelegate < Post
+ def self.human_attribute_name(attribute)
+ "Delegate #{super}"
+ end
+
+ def model_name
+ ActiveModel::Name.new(self.class)
+ end
+end
+
class Comment
extend ActiveModel::Naming
include ActiveModel::Conversion
@@ -111,19 +127,6 @@ class CommentRelevance
end
end
-class Sheep
- extend ActiveModel::Naming
- include ActiveModel::Conversion
-
- attr_reader :id
- def to_key; id ? [id] : nil end
- def save; @id = 1 end
- def new_record?; @id.nil? end
- def name
- @id.nil? ? 'new sheep' : "sheep ##{@id}"
- end
-end
-
class TagRelevance
extend ActiveModel::Naming
include ActiveModel::Conversion
@@ -183,3 +186,15 @@ end
class Car < Struct.new(:color)
end
+
+class Plane
+ attr_reader :to_key
+
+ def model_name
+ OpenStruct.new param_key: 'airplane'
+ end
+
+ def save
+ @to_key = [1]
+ end
+end
diff --git a/actionview/test/template/asset_tag_helper_test.rb b/actionview/test/template/asset_tag_helper_test.rb
index d789a5ca27..496b33b35e 100644
--- a/actionview/test/template/asset_tag_helper_test.rb
+++ b/actionview/test/template/asset_tag_helper_test.rb
@@ -1,4 +1,3 @@
-require 'zlib'
require 'abstract_unit'
require 'active_support/ordered_options'
@@ -180,6 +179,7 @@ class AssetTagHelperTest < ActionView::TestCase
%(image_tag("xml.png")) => %(<img alt="Xml" src="/images/xml.png" />),
%(image_tag("rss.gif", :alt => "rss syndication")) => %(<img alt="rss syndication" src="/images/rss.gif" />),
%(image_tag("gold.png", :size => "20")) => %(<img alt="Gold" height="20" src="/images/gold.png" width="20" />),
+ %(image_tag("gold.png", :size => 20)) => %(<img alt="Gold" height="20" src="/images/gold.png" width="20" />),
%(image_tag("gold.png", :size => "45x70")) => %(<img alt="Gold" height="70" src="/images/gold.png" width="45" />),
%(image_tag("gold.png", "size" => "45x70")) => %(<img alt="Gold" height="70" src="/images/gold.png" width="45" />),
%(image_tag("error.png", "size" => "45 x 70")) => %(<img alt="Error" src="/images/error.png" />),
@@ -238,6 +238,7 @@ class AssetTagHelperTest < ActionView::TestCase
%(video_tag("gold.m4v", "size" => "320x240")) => %(<video height="240" src="/videos/gold.m4v" width="320"></video>),
%(video_tag("trailer.ogg", :poster => "screenshot.png")) => %(<video poster="/images/screenshot.png" src="/videos/trailer.ogg"></video>),
%(video_tag("error.avi", "size" => "100")) => %(<video height="100" src="/videos/error.avi" width="100"></video>),
+ %(video_tag("error.avi", "size" => 100)) => %(<video height="100" src="/videos/error.avi" width="100"></video>),
%(video_tag("error.avi", "size" => "100 x 100")) => %(<video src="/videos/error.avi"></video>),
%(video_tag("error.avi", "size" => "x")) => %(<video src="/videos/error.avi"></video>),
%(video_tag("http://media.rubyonrails.org/video/rails_blog_2.mov")) => %(<video src="http://media.rubyonrails.org/video/rails_blog_2.mov"></video>),
@@ -302,13 +303,18 @@ class AssetTagHelperTest < ActionView::TestCase
def test_autodiscovery_link_tag_with_unknown_type
result = auto_discovery_link_tag(:xml, '/feed.xml', :type => 'application/xml')
expected = %(<link href="/feed.xml" rel="alternate" title="XML" type="application/xml" />)
- assert_equal expected, result
+ assert_dom_equal expected, result
end
def test_asset_path_tag
AssetPathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end
+ def test_asset_path_tag_raises_an_error_for_nil_source
+ e = assert_raise(ArgumentError) { asset_path(nil) }
+ assert_equal("nil is not a valid asset source", e.message)
+ end
+
def test_asset_path_tag_to_not_create_duplicate_slashes
@controller.config.asset_host = "host/"
assert_dom_equal('http://host/foo', asset_path("foo"))
@@ -463,6 +469,14 @@ class AssetTagHelperTest < ActionView::TestCase
assert_equal({:size => '16x10'}, options)
end
+ def test_image_tag_raises_an_error_for_competing_size_arguments
+ exception = assert_raise(ArgumentError) do
+ image_tag("gold.png", :height => "100", :width => "200", :size => "45x70")
+ end
+
+ assert_equal("Cannot pass a :size option with a :height or :width option", exception.message)
+ end
+
def test_favicon_link_tag
FaviconLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end
@@ -535,6 +549,17 @@ class AssetTagHelperTest < ActionView::TestCase
assert_equal copy, source
end
+ class PlaceholderImage
+ def blank?; true; end
+ def to_s; 'no-image-yet.png'; end
+ end
+ def test_image_tag_with_blank_placeholder
+ assert_equal '<img alt="" src="/images/no-image-yet.png" />', image_tag(PlaceholderImage.new, alt: "")
+ end
+ def test_image_path_with_blank_placeholder
+ assert_equal '/images/no-image-yet.png', image_path(PlaceholderImage.new)
+ end
+
def test_image_path_with_asset_host_proc_returning_nil
@controller.config.asset_host = Proc.new do |source|
unless source.end_with?("tiff")
@@ -563,11 +588,13 @@ class AssetTagHelperTest < ActionView::TestCase
end
end
- @controller.request.stubs(:ssl?).returns(false)
- assert_equal "http://assets15.example.com/images/xml.png", image_path("xml.png")
+ @controller.request.stub(:ssl?, false) do
+ assert_equal "http://assets15.example.com/images/xml.png", image_path("xml.png")
+ end
- @controller.request.stubs(:ssl?).returns(true)
- assert_equal "http://localhost/images/xml.png", image_path("xml.png")
+ @controller.request.stub(:ssl?, true) do
+ assert_equal "http://localhost/images/xml.png", image_path("xml.png")
+ end
end
end
diff --git a/actionview/test/template/atom_feed_helper_test.rb b/actionview/test/template/atom_feed_helper_test.rb
index 63b5ac0fab..591cd71404 100644
--- a/actionview/test/template/atom_feed_helper_test.rb
+++ b/actionview/test/template/atom_feed_helper_test.rb
@@ -14,7 +14,7 @@ class ScrollsController < ActionController::Base
FEEDS["defaults"] = <<-EOT
atom_feed(:schema_date => '2008') do |feed|
feed.title("My great blog!")
- feed.updated((@scrolls.first.created_at))
+ feed.updated(@scrolls.first.created_at)
@scrolls.each do |scroll|
feed.entry(scroll) do |entry|
@@ -31,7 +31,7 @@ class ScrollsController < ActionController::Base
FEEDS["entry_options"] = <<-EOT
atom_feed do |feed|
feed.title("My great blog!")
- feed.updated((@scrolls.first.created_at))
+ feed.updated(@scrolls.first.created_at)
@scrolls.each do |scroll|
feed.entry(scroll, :url => "/otherstuff/" + scroll.to_param.to_s, :updated => Time.utc(2007, 1, scroll.id)) do |entry|
@@ -48,7 +48,7 @@ class ScrollsController < ActionController::Base
FEEDS["entry_type_options"] = <<-EOT
atom_feed(:schema_date => '2008') do |feed|
feed.title("My great blog!")
- feed.updated((@scrolls.first.created_at))
+ feed.updated(@scrolls.first.created_at)
@scrolls.each do |scroll|
feed.entry(scroll, :type => 'text/xml') do |entry|
@@ -62,10 +62,27 @@ class ScrollsController < ActionController::Base
end
end
EOT
+ FEEDS["entry_url_false_option"] = <<-EOT
+ atom_feed do |feed|
+ feed.title("My great blog!")
+ feed.updated(@scrolls.first.created_at)
+
+ @scrolls.each do |scroll|
+ feed.entry(scroll, :url => false) do |entry|
+ entry.title(scroll.title)
+ entry.content(scroll.body, :type => 'html')
+
+ entry.author do |author|
+ author.name("DHH")
+ end
+ end
+ end
+ end
+ EOT
FEEDS["xml_block"] = <<-EOT
atom_feed do |feed|
feed.title("My great blog!")
- feed.updated((@scrolls.first.created_at))
+ feed.updated(@scrolls.first.created_at)
feed.author do |author|
author.name("DHH")
@@ -83,7 +100,7 @@ class ScrollsController < ActionController::Base
atom_feed({'xmlns:app' => 'http://www.w3.org/2007/app',
'xmlns:openSearch' => 'http://a9.com/-/spec/opensearch/1.1/'}) do |feed|
feed.title("My great blog!")
- feed.updated((@scrolls.first.created_at))
+ feed.updated(@scrolls.first.created_at)
@scrolls.each do |scroll|
feed.entry(scroll) do |entry|
@@ -101,7 +118,7 @@ class ScrollsController < ActionController::Base
FEEDS["feed_with_overridden_ids"] = <<-EOT
atom_feed({:id => 'tag:test.rubyonrails.org,2008:test/'}) do |feed|
feed.title("My great blog!")
- feed.updated((@scrolls.first.created_at))
+ feed.updated(@scrolls.first.created_at)
@scrolls.each do |scroll|
feed.entry(scroll, :id => "tag:test.rubyonrails.org,2008:"+scroll.id.to_s) do |entry|
@@ -120,7 +137,7 @@ class ScrollsController < ActionController::Base
atom_feed(:schema_date => '2008',
:instruct => {'xml-stylesheet' => { :href=> 't.css', :type => 'text/css' }}) do |feed|
feed.title("My great blog!")
- feed.updated((@scrolls.first.created_at))
+ feed.updated(@scrolls.first.created_at)
@scrolls.each do |scroll|
feed.entry(scroll) do |entry|
@@ -138,7 +155,7 @@ class ScrollsController < ActionController::Base
atom_feed(:schema_date => '2008',
:instruct => {'target1' => [{ :a => '1', :b => '2' }, { :c => '3', :d => '4' }]}) do |feed|
feed.title("My great blog!")
- feed.updated((@scrolls.first.created_at))
+ feed.updated(@scrolls.first.created_at)
@scrolls.each do |scroll|
feed.entry(scroll) do |entry|
@@ -155,7 +172,7 @@ class ScrollsController < ActionController::Base
FEEDS["feed_with_xhtml_content"] = <<-'EOT'
atom_feed do |feed|
feed.title("My great blog!")
- feed.updated((@scrolls.first.created_at))
+ feed.updated(@scrolls.first.created_at)
@scrolls.each do |scroll|
feed.entry(scroll) do |entry|
@@ -180,7 +197,7 @@ class ScrollsController < ActionController::Base
new_xml = Builder::XmlMarkup.new(:target=>'')
atom_feed(:xml => new_xml) do |feed|
feed.title("My great blog!")
- feed.updated((@scrolls.first.created_at))
+ feed.updated(@scrolls.first.created_at)
@scrolls.each do |scroll|
feed.entry(scroll) do |entry|
@@ -214,28 +231,28 @@ class AtomFeedTest < ActionController::TestCase
def test_feed_should_use_default_language_if_none_is_given
with_restful_routing(:scrolls) do
- get :index, :id => "defaults"
+ get :index, params: { id: "defaults" }
assert_match(%r{xml:lang="en-US"}, @response.body)
end
end
def test_feed_should_include_two_entries
with_restful_routing(:scrolls) do
- get :index, :id => "defaults"
+ get :index, params: { id: "defaults" }
assert_select "entry", 2
end
end
def test_entry_should_only_use_published_if_created_at_is_present
with_restful_routing(:scrolls) do
- get :index, :id => "defaults"
+ get :index, params: { id: "defaults" }
assert_select "published", 1
end
end
def test_providing_builder_to_atom_feed
with_restful_routing(:scrolls) do
- get :index, :id=>"provide_builder"
+ get :index, params: { id: "provide_builder" }
# because we pass in the non-default builder, the content generated by the
# helper should go 'nowhere'. Leaving the response body blank.
assert @response.body.blank?
@@ -244,7 +261,7 @@ class AtomFeedTest < ActionController::TestCase
def test_entry_with_prefilled_options_should_use_those_instead_of_querying_the_record
with_restful_routing(:scrolls) do
- get :index, :id => "entry_options"
+ get :index, params: { id: "entry_options" }
assert_select "updated", Time.utc(2007, 1, 1).xmlschema
assert_select "updated", Time.utc(2007, 1, 2).xmlschema
@@ -253,21 +270,21 @@ class AtomFeedTest < ActionController::TestCase
def test_self_url_should_default_to_current_request_url
with_restful_routing(:scrolls) do
- get :index, :id => "defaults"
- assert_select "link[rel=self][href=http://www.nextangle.com/scrolls?id=defaults]"
+ get :index, params: { id: "defaults" }
+ assert_select "link[rel=self][href=\"http://www.nextangle.com/scrolls?id=defaults\"]"
end
end
def test_feed_id_should_be_a_valid_tag
with_restful_routing(:scrolls) do
- get :index, :id => "defaults"
+ get :index, params: { id: "defaults" }
assert_select "id", :text => "tag:www.nextangle.com,2008:/scrolls?id=defaults"
end
end
def test_entry_id_should_be_a_valid_tag
with_restful_routing(:scrolls) do
- get :index, :id => "defaults"
+ get :index, params: { id: "defaults" }
assert_select "entry id", :text => "tag:www.nextangle.com,2008:Scroll/1"
assert_select "entry id", :text => "tag:www.nextangle.com,2008:Scroll/2"
end
@@ -275,14 +292,14 @@ class AtomFeedTest < ActionController::TestCase
def test_feed_should_allow_nested_xml_blocks
with_restful_routing(:scrolls) do
- get :index, :id => "xml_block"
+ get :index, params: { id: "xml_block" }
assert_select "author name", :text => "DHH"
end
end
def test_feed_should_include_atomPub_namespace
with_restful_routing(:scrolls) do
- get :index, :id => "feed_with_atomPub_namespace"
+ get :index, params: { id: "feed_with_atomPub_namespace" }
assert_match %r{xml:lang="en-US"}, @response.body
assert_match %r{xmlns="http://www.w3.org/2005/Atom"}, @response.body
assert_match %r{xmlns:app="http://www.w3.org/2007/app"}, @response.body
@@ -291,7 +308,7 @@ class AtomFeedTest < ActionController::TestCase
def test_feed_should_allow_overriding_ids
with_restful_routing(:scrolls) do
- get :index, :id => "feed_with_overridden_ids"
+ get :index, params: { id: "feed_with_overridden_ids" }
assert_select "id", :text => "tag:test.rubyonrails.org,2008:test/"
assert_select "entry id", :text => "tag:test.rubyonrails.org,2008:1"
assert_select "entry id", :text => "tag:test.rubyonrails.org,2008:2"
@@ -300,7 +317,7 @@ class AtomFeedTest < ActionController::TestCase
def test_feed_xml_processing_instructions
with_restful_routing(:scrolls) do
- get :index, :id => 'feed_with_xml_processing_instructions'
+ get :index, params: { id: 'feed_with_xml_processing_instructions' }
assert_match %r{<\?xml-stylesheet [^\?]*type="text/css"}, @response.body
assert_match %r{<\?xml-stylesheet [^\?]*href="t.css"}, @response.body
end
@@ -308,7 +325,7 @@ class AtomFeedTest < ActionController::TestCase
def test_feed_xml_processing_instructions_duplicate_targets
with_restful_routing(:scrolls) do
- get :index, :id => 'feed_with_xml_processing_instructions_duplicate_targets'
+ get :index, params: { id: 'feed_with_xml_processing_instructions_duplicate_targets' }
assert_match %r{<\?target1 (a="1" b="2"|b="2" a="1")\?>}, @response.body
assert_match %r{<\?target1 (c="3" d="4"|d="4" c="3")\?>}, @response.body
end
@@ -316,24 +333,31 @@ class AtomFeedTest < ActionController::TestCase
def test_feed_xhtml
with_restful_routing(:scrolls) do
- get :index, :id => "feed_with_xhtml_content"
+ get :index, params: { id: "feed_with_xhtml_content" }
assert_match %r{xmlns="http://www.w3.org/1999/xhtml"}, @response.body
- assert_select "summary div p", :text => "Something Boring"
- assert_select "summary div p", :text => "after 2"
+ assert_select "summary", :text => /Something Boring/
+ assert_select "summary", :text => /after 2/
end
end
def test_feed_entry_type_option_default_to_text_html
with_restful_routing(:scrolls) do
- get :index, :id => 'defaults'
- assert_select "entry link[rel=alternate][type=text/html]"
+ get :index, params: { id: 'defaults' }
+ assert_select "entry link[rel=alternate][type=\"text/html\"]"
end
end
def test_feed_entry_type_option_specified
with_restful_routing(:scrolls) do
- get :index, :id => 'entry_type_options'
- assert_select "entry link[rel=alternate][type=text/xml]"
+ get :index, params: { id: 'entry_type_options' }
+ assert_select "entry link[rel=alternate][type=\"text/xml\"]"
+ end
+ end
+
+ def test_feed_entry_url_false_option_adds_no_link
+ with_restful_routing(:scrolls) do
+ get :index, params: { id: 'entry_url_false_option' }
+ assert_select "entry link", false
end
end
diff --git a/actionview/test/template/capture_helper_test.rb b/actionview/test/template/capture_helper_test.rb
index f213da5934..1e099d482c 100644
--- a/actionview/test/template/capture_helper_test.rb
+++ b/actionview/test/template/capture_helper_test.rb
@@ -210,10 +210,4 @@ class CaptureHelperTest < ActionView::TestCase
def alt_encoding(output_buffer)
output_buffer.encoding == Encoding::US_ASCII ? Encoding::UTF_8 : Encoding::US_ASCII
end
-
- def view_with_controller
- TestController.new.view_context.tap do |view|
- view.output_buffer = ActionView::OutputBuffer.new
- end
- end
end
diff --git a/actionview/test/template/compiled_templates_test.rb b/actionview/test/template/compiled_templates_test.rb
index b84aca6746..f6c1283b92 100644
--- a/actionview/test/template/compiled_templates_test.rb
+++ b/actionview/test/template/compiled_templates_test.rb
@@ -5,6 +5,10 @@ class CompiledTemplatesTest < ActiveSupport::TestCase
ActionView::LookupContext::DetailsKey.clear
end
+ def test_template_with_nil_erb_return
+ assert_equal "This is nil: \n", render(:template => "test/nil_return")
+ end
+
def test_template_gets_recompiled_when_using_different_keys_in_local_assigns
assert_equal "one", render(:file => "test/render_file_with_locals_and_default")
assert_equal "two", render(:file => "test/render_file_with_locals_and_default", :locals => { :secret => "two" })
diff --git a/actionview/test/template/controller_helper_test.rb b/actionview/test/template/controller_helper_test.rb
new file mode 100644
index 0000000000..b5e94ea4f1
--- /dev/null
+++ b/actionview/test/template/controller_helper_test.rb
@@ -0,0 +1,21 @@
+require 'abstract_unit'
+
+class ControllerHelperTest < ActionView::TestCase
+ tests ActionView::Helpers::ControllerHelper
+
+ class SpecializedFormBuilder < ActionView::Helpers::FormBuilder ; end
+
+ def test_assign_controller_sets_default_form_builder
+ @controller = OpenStruct.new(default_form_builder: SpecializedFormBuilder)
+ assign_controller(@controller)
+
+ assert_equal SpecializedFormBuilder, self.default_form_builder
+ end
+
+ def test_assign_controller_skips_default_form_builder
+ @controller = OpenStruct.new
+ assign_controller(@controller)
+
+ assert_nil self.default_form_builder
+ end
+end
diff --git a/actionview/test/template/date_helper_test.rb b/actionview/test/template/date_helper_test.rb
index b86ae910c4..9212420ec9 100644
--- a/actionview/test/template/date_helper_test.rb
+++ b/actionview/test/template/date_helper_test.rb
@@ -130,7 +130,7 @@ class DateHelperTest < ActionView::TestCase
def test_distance_in_words_with_mathn_required
# test we avoid Integer#/ (redefined by mathn)
- require 'mathn'
+ silence_warnings { require "mathn" }
from = Time.utc(2004, 6, 6, 21, 45, 0)
assert_distance_of_time_in_words(from)
end
@@ -1504,7 +1504,7 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="">Choose seconds</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
expected << "</select>\n"
- assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => true, :include_seconds => true,
+ assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :include_seconds => true,
:prompt => {:hour => 'Choose hour', :minute => 'Choose minute', :second => 'Choose seconds'})
end
@@ -1652,9 +1652,9 @@ class DateHelperTest < ActionView::TestCase
concat f.date_select(:written_on)
end
- expected = "<select id='post_written_on_1i' name='post[written_on(1i)]'>\n<option value='1999'>1999</option>\n<option value='2000'>2000</option>\n<option value='2001'>2001</option>\n<option value='2002'>2002</option>\n<option value='2003'>2003</option>\n<option selected='selected' value='2004'>2004</option>\n<option value='2005'>2005</option>\n<option value='2006'>2006</option>\n<option value='2007'>2007</option>\n<option value='2008'>2008</option>\n<option value='2009'>2009</option>\n</select>\n"
- expected << "<select id='post_written_on_2i' name='post[written_on(2i)]'>\n<option value='1'>January</option>\n<option value='2'>February</option>\n<option value='3'>March</option>\n<option value='4'>April</option>\n<option value='5'>May</option>\n<option selected='selected' value='6'>June</option>\n<option value='7'>July</option>\n<option value='8'>August</option>\n<option value='9'>September</option>\n<option value='10'>October</option>\n<option value='11'>November</option>\n<option value='12'>December</option>\n</select>\n"
- expected << "<select id='post_written_on_3i' name='post[written_on(3i)]'>\n<option value='1'>1</option>\n<option value='2'>2</option>\n<option value='3'>3</option>\n<option value='4'>4</option>\n<option value='5'>5</option>\n<option value='6'>6</option>\n<option value='7'>7</option>\n<option value='8'>8</option>\n<option value='9'>9</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n<option value='13'>13</option>\n<option value='14'>14</option>\n<option selected='selected' value='15'>15</option>\n<option value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n<option value='24'>24</option>\n<option value='25'>25</option>\n<option value='26'>26</option>\n<option value='27'>27</option>\n<option value='28'>28</option>\n<option value='29'>29</option>\n<option value='30'>30</option>\n<option value='31'>31</option>\n</select>\n"
+ expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n}
+ expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option selected="selected" value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n</select>\n}
+ expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option selected="selected" value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n</select>\n}
assert_dom_equal(expected, output_buffer)
end
@@ -1668,9 +1668,9 @@ class DateHelperTest < ActionView::TestCase
concat f.date_select(:written_on)
end
- expected = "<select id='post_#{id}_written_on_1i' name='post[#{id}][written_on(1i)]'>\n<option value='1999'>1999</option>\n<option value='2000'>2000</option>\n<option value='2001'>2001</option>\n<option value='2002'>2002</option>\n<option value='2003'>2003</option>\n<option selected='selected' value='2004'>2004</option>\n<option value='2005'>2005</option>\n<option value='2006'>2006</option>\n<option value='2007'>2007</option>\n<option value='2008'>2008</option>\n<option value='2009'>2009</option>\n</select>\n"
- expected << "<select id='post_#{id}_written_on_2i' name='post[#{id}][written_on(2i)]'>\n<option value='1'>January</option>\n<option value='2'>February</option>\n<option value='3'>March</option>\n<option value='4'>April</option>\n<option value='5'>May</option>\n<option selected='selected' value='6'>June</option>\n<option value='7'>July</option>\n<option value='8'>August</option>\n<option value='9'>September</option>\n<option value='10'>October</option>\n<option value='11'>November</option>\n<option value='12'>December</option>\n</select>\n"
- expected << "<select id='post_#{id}_written_on_3i' name='post[#{id}][written_on(3i)]'>\n<option value='1'>1</option>\n<option value='2'>2</option>\n<option value='3'>3</option>\n<option value='4'>4</option>\n<option value='5'>5</option>\n<option value='6'>6</option>\n<option value='7'>7</option>\n<option value='8'>8</option>\n<option value='9'>9</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n<option value='13'>13</option>\n<option value='14'>14</option>\n<option selected='selected' value='15'>15</option>\n<option value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n<option value='24'>24</option>\n<option value='25'>25</option>\n<option value='26'>26</option>\n<option value='27'>27</option>\n<option value='28'>28</option>\n<option value='29'>29</option>\n<option value='30'>30</option>\n<option value='31'>31</option>\n</select>\n"
+ expected = %{<select id="post_#{id}_written_on_1i" name="post[#{id}][written_on(1i)]">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n}
+ expected << %{<select id="post_#{id}_written_on_2i" name="post[#{id}][written_on(2i)]">\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option selected="selected" value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n</select>\n}
+ expected << %{<select id="post_#{id}_written_on_3i" name="post[#{id}][written_on(3i)]">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option selected="selected" value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n</select>\n}
assert_dom_equal(expected, output_buffer)
end
@@ -1684,9 +1684,10 @@ class DateHelperTest < ActionView::TestCase
concat f.date_select(:written_on)
end
- expected = "<select id='post_#{id}_written_on_1i' name='post[#{id}][written_on(1i)]'>\n<option value='1999'>1999</option>\n<option value='2000'>2000</option>\n<option value='2001'>2001</option>\n<option value='2002'>2002</option>\n<option value='2003'>2003</option>\n<option selected='selected' value='2004'>2004</option>\n<option value='2005'>2005</option>\n<option value='2006'>2006</option>\n<option value='2007'>2007</option>\n<option value='2008'>2008</option>\n<option value='2009'>2009</option>\n</select>\n"
- expected << "<select id='post_#{id}_written_on_2i' name='post[#{id}][written_on(2i)]'>\n<option value='1'>January</option>\n<option value='2'>February</option>\n<option value='3'>March</option>\n<option value='4'>April</option>\n<option value='5'>May</option>\n<option selected='selected' value='6'>June</option>\n<option value='7'>July</option>\n<option value='8'>August</option>\n<option value='9'>September</option>\n<option value='10'>October</option>\n<option value='11'>November</option>\n<option value='12'>December</option>\n</select>\n"
- expected << "<select id='post_#{id}_written_on_3i' name='post[#{id}][written_on(3i)]'>\n<option value='1'>1</option>\n<option value='2'>2</option>\n<option value='3'>3</option>\n<option value='4'>4</option>\n<option value='5'>5</option>\n<option value='6'>6</option>\n<option value='7'>7</option>\n<option value='8'>8</option>\n<option value='9'>9</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n<option value='13'>13</option>\n<option value='14'>14</option>\n<option selected='selected' value='15'>15</option>\n<option value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n<option value='24'>24</option>\n<option value='25'>25</option>\n<option value='26'>26</option>\n<option value='27'>27</option>\n<option value='28'>28</option>\n<option value='29'>29</option>\n<option value='30'>30</option>\n<option value='31'>31</option>\n</select>\n"
+
+ expected = %{<select id="post_#{id}_written_on_1i" name="post[#{id}][written_on(1i)]">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n}
+ expected << %{<select id="post_#{id}_written_on_2i" name="post[#{id}][written_on(2i)]">\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option selected="selected" value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n</select>\n}
+ expected << %{<select id="post_#{id}_written_on_3i" name="post[#{id}][written_on(3i)]">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option selected="selected" value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n</select>\n}
assert_dom_equal(expected, output_buffer)
end
@@ -2329,7 +2330,7 @@ class DateHelperTest < ActionView::TestCase
# The love zone is UTC+0
mytz = Class.new(ActiveSupport::TimeZone) {
attr_accessor :now
- }.create('tenderlove', 0)
+ }.create('tenderlove', 0, ActiveSupport::TimeZone.find_tzinfo('UTC'))
now = Time.mktime(2004, 6, 15, 16, 35, 0)
mytz.now = now
@@ -2374,11 +2375,11 @@ class DateHelperTest < ActionView::TestCase
concat f.datetime_select(:updated_at, {}, :class => 'selector')
end
- expected = "<select id='post_updated_at_1i' name='post[updated_at(1i)]' class='selector'>\n<option value='1999'>1999</option>\n<option value='2000'>2000</option>\n<option value='2001'>2001</option>\n<option value='2002'>2002</option>\n<option value='2003'>2003</option>\n<option selected='selected' value='2004'>2004</option>\n<option value='2005'>2005</option>\n<option value='2006'>2006</option>\n<option value='2007'>2007</option>\n<option value='2008'>2008</option>\n<option value='2009'>2009</option>\n</select>\n"
- expected << "<select id='post_updated_at_2i' name='post[updated_at(2i)]' class='selector'>\n<option value='1'>January</option>\n<option value='2'>February</option>\n<option value='3'>March</option>\n<option value='4'>April</option>\n<option value='5'>May</option>\n<option selected='selected' value='6'>June</option>\n<option value='7'>July</option>\n<option value='8'>August</option>\n<option value='9'>September</option>\n<option value='10'>October</option>\n<option value='11'>November</option>\n<option value='12'>December</option>\n</select>\n"
- expected << "<select id='post_updated_at_3i' name='post[updated_at(3i)]' class='selector'>\n<option value='1'>1</option>\n<option value='2'>2</option>\n<option value='3'>3</option>\n<option value='4'>4</option>\n<option value='5'>5</option>\n<option value='6'>6</option>\n<option value='7'>7</option>\n<option value='8'>8</option>\n<option value='9'>9</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n<option value='13'>13</option>\n<option value='14'>14</option>\n<option selected='selected' value='15'>15</option>\n<option value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n<option value='24'>24</option>\n<option value='25'>25</option>\n<option value='26'>26</option>\n<option value='27'>27</option>\n<option value='28'>28</option>\n<option value='29'>29</option>\n<option value='30'>30</option>\n<option value='31'>31</option>\n</select>\n"
- expected << " &mdash; <select id='post_updated_at_4i' name='post[updated_at(4i)]' class='selector'>\n<option value='00'>00</option>\n<option value='01'>01</option>\n<option value='02'>02</option>\n<option value='03'>03</option>\n<option value='04'>04</option>\n<option value='05'>05</option>\n<option value='06'>06</option>\n<option value='07'>07</option>\n<option value='08'>08</option>\n<option value='09'>09</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n<option value='13'>13</option>\n<option value='14'>14</option>\n<option value='15'>15</option>\n<option selected='selected' value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n</select>\n"
- expected << " : <select id='post_updated_at_5i' name='post[updated_at(5i)]' class='selector'>\n<option value='00'>00</option>\n<option value='01'>01</option>\n<option value='02'>02</option>\n<option value='03'>03</option>\n<option value='04'>04</option>\n<option value='05'>05</option>\n<option value='06'>06</option>\n<option value='07'>07</option>\n<option value='08'>08</option>\n<option value='09'>09</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n<option value='13'>13</option>\n<option value='14'>14</option>\n<option value='15'>15</option>\n<option value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n<option value='24'>24</option>\n<option value='25'>25</option>\n<option value='26'>26</option>\n<option value='27'>27</option>\n<option value='28'>28</option>\n<option value='29'>29</option>\n<option value='30'>30</option>\n<option value='31'>31</option>\n<option value='32'>32</option>\n<option value='33'>33</option>\n<option value='34'>34</option>\n<option selected='selected' value='35'>35</option>\n<option value='36'>36</option>\n<option value='37'>37</option>\n<option value='38'>38</option>\n<option value='39'>39</option>\n<option value='40'>40</option>\n<option value='41'>41</option>\n<option value='42'>42</option>\n<option value='43'>43</option>\n<option value='44'>44</option>\n<option value='45'>45</option>\n<option value='46'>46</option>\n<option value='47'>47</option>\n<option value='48'>48</option>\n<option value='49'>49</option>\n<option value='50'>50</option>\n<option value='51'>51</option>\n<option value='52'>52</option>\n<option value='53'>53</option>\n<option value='54'>54</option>\n<option value='55'>55</option>\n<option value='56'>56</option>\n<option value='57'>57</option>\n<option value='58'>58</option>\n<option value='59'>59</option>\n</select>\n"
+ expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]" class="selector">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n}
+ expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]" class="selector">\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option selected="selected" value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n</select>\n}
+ expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]" class="selector">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option selected="selected" value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n</select>\n}
+ expected << %{ &mdash; <select id="post_updated_at_4i" name="post[updated_at(4i)]" class="selector">\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option selected="selected" value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n</select>\n}
+ expected << %{ : <select id="post_updated_at_5i" name="post[updated_at(5i)]" class="selector">\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option selected="selected" value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n</select>\n}
assert_dom_equal expected, output_buffer
end
@@ -3216,12 +3217,4 @@ class DateHelperTest < ActionView::TestCase
expected = '<time datetime="2013-02-20T00:00:00+00:00">20 Feb 00:00</time>'
assert_equal expected, time_tag(time, :format => :short)
end
-
- protected
- def with_env_tz(new_tz = 'US/Eastern')
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
- yield
- ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
- end
end
diff --git a/actionview/test/template/dependency_tracker_test.rb b/actionview/test/template/dependency_tracker_test.rb
index bb375076c6..3ece9e50cd 100644
--- a/actionview/test/template/dependency_tracker_test.rb
+++ b/actionview/test/template/dependency_tracker_test.rb
@@ -1,5 +1,3 @@
-# encoding: utf-8
-
require 'abstract_unit'
require 'action_view/dependency_tracker'
@@ -61,7 +59,6 @@ class ERBTrackerTest < Minitest::Test
end
def test_dependency_of_template_partial_with_layout
- skip # FIXME: Needs to be fixed properly, right now we can only match one dependency per line. Need multiple!
template = FakeTemplate.new("<%# render partial: 'messages/show', layout: 'messages/layout' %>", :erb)
tracker = make_tracker("multiple/_dependencies", template)
diff --git a/actionview/test/template/digestor_test.rb b/actionview/test/template/digestor_test.rb
index c2b8439df3..dde757b5a2 100644
--- a/actionview/test/template/digestor_test.rb
+++ b/actionview/test/template/digestor_test.rb
@@ -1,5 +1,6 @@
require 'abstract_unit'
require 'fileutils'
+require 'action_view/dependency_tracker'
class FixtureTemplate
attr_reader :source, :handler
@@ -15,12 +16,13 @@ end
class FixtureFinder
FIXTURES_DIR = "#{File.dirname(__FILE__)}/../fixtures/digestor"
- attr_reader :details
+ attr_reader :details, :view_paths
attr_accessor :formats
attr_accessor :variants
def initialize
@details = {}
+ @view_paths = ActionView::PathSet.new(['digestor'])
@formats = []
@variants = []
end
@@ -75,6 +77,34 @@ class TemplateDigestorTest < ActionView::TestCase
end
end
+ def test_explicit_dependency_wildcard
+ assert_digest_difference("events/index") do
+ change_template("events/_completed")
+ end
+ end
+
+ def test_explicit_dependency_wildcard_picks_up_added_file
+ old_caching, ActionView::Resolver.caching = ActionView::Resolver.caching, false
+
+ assert_digest_difference("events/index") do
+ add_template("events/_uncompleted")
+ end
+ ensure
+ remove_template("events/_uncompleted")
+ ActionView::Resolver.caching = old_caching
+ end
+
+ def test_explicit_dependency_wildcard_picks_up_removed_file
+ old_caching, ActionView::Resolver.caching = ActionView::Resolver.caching, false
+ add_template("events/_subscribers_changed")
+
+ assert_digest_difference("events/index") do
+ remove_template("events/_subscribers_changed")
+ end
+ ensure
+ ActionView::Resolver.caching = old_caching
+ end
+
def test_second_level_dependency
assert_digest_difference("messages/show") do
change_template("comments/_comments")
@@ -111,6 +141,18 @@ class TemplateDigestorTest < ActionView::TestCase
end
end
+ def test_logging_of_missing_template_for_dependencies
+ assert_logged "'messages/something_missing' file doesn't exist, so no dependencies" do
+ dependencies("messages/something_missing")
+ end
+ end
+
+ def test_logging_of_missing_template_for_nested_dependencies
+ assert_logged "'messages/something_missing' file doesn't exist, so no dependencies" do
+ nested_dependencies("messages/something_missing")
+ end
+ end
+
def test_nested_template_directory
assert_digest_difference("messages/show") do
change_template("messages/actions/_move")
@@ -207,7 +249,7 @@ class TemplateDigestorTest < ActionView::TestCase
end
def test_variants
- assert_digest_difference("messages/new", false, variants: [:iphone]) do
+ assert_digest_difference("messages/new", variants: [:iphone]) do
change_template("messages/new", :iphone)
change_template("messages/_header", :iphone)
end
@@ -227,16 +269,6 @@ class TemplateDigestorTest < ActionView::TestCase
assert_not_equal digest_phone, digest_fridge_phone
end
- def test_cache_template_loading
- resolver_before = ActionView::Resolver.caching
- ActionView::Resolver.caching = false
- assert_digest_difference("messages/edit", true) do
- change_template("comments/_comment")
- end
- ensure
- ActionView::Resolver.caching = resolver_before
- end
-
def test_digest_cache_cleanup_with_recursion
first_digest = digest("level/_recursion")
second_digest = digest("level/_recursion")
@@ -279,9 +311,9 @@ class TemplateDigestorTest < ActionView::TestCase
end
end
- def assert_digest_difference(template_name, persistent = false, options = {})
+ def assert_digest_difference(template_name, options = {})
previous_digest = digest(template_name, options)
- ActionView::Digestor.cache.clear unless persistent
+ ActionView::Digestor.cache.clear
yield
@@ -298,6 +330,14 @@ class TemplateDigestorTest < ActionView::TestCase
ActionView::Digestor.digest({ name: template_name, finder: finder }.merge(options))
end
+ def dependencies(template_name)
+ ActionView::Digestor.new({ name: template_name, finder: finder }).dependencies
+ end
+
+ def nested_dependencies(template_name)
+ ActionView::Digestor.new({ name: template_name, finder: finder }).nested_dependencies
+ end
+
def finder
@finder ||= FixtureFinder.new
end
@@ -309,4 +349,9 @@ class TemplateDigestorTest < ActionView::TestCase
f.write "\nTHIS WAS CHANGED!"
end
end
+ alias_method :add_template, :change_template
+
+ def remove_template(template_name)
+ File.delete("digestor/#{template_name}.html.erb")
+ end
end
diff --git a/actionview/test/template/erb_util_test.rb b/actionview/test/template/erb_util_test.rb
index 3bb84cbc50..3e72be31de 100644
--- a/actionview/test/template/erb_util_test.rb
+++ b/actionview/test/template/erb_util_test.rb
@@ -84,7 +84,7 @@ class ErbUtilTest < ActiveSupport::TestCase
end
def test_rest_in_ascii
- (0..127).to_a.map {|int| int.chr }.each do |chr|
+ (0..127).to_a.map(&:chr).each do |chr|
next if %('"&<>).include?(chr)
assert_equal chr, html_escape(chr)
end
diff --git a/actionview/test/template/form_collections_helper_test.rb b/actionview/test/template/form_collections_helper_test.rb
index 5e991d87ad..b59be8e36c 100644
--- a/actionview/test/template/form_collections_helper_test.rb
+++ b/actionview/test/template/form_collections_helper_test.rb
@@ -185,8 +185,8 @@ class FormCollectionsHelperTest < ActionView::TestCase
p.collection_radio_buttons :category_id, collection, :id, :name
end
- assert_select 'input#post_category_id_1[type=radio][value=1]'
- assert_select 'input#post_category_id_2[type=radio][value=2]'
+ assert_select 'input#post_category_id_1[type=radio][value="1"]'
+ assert_select 'input#post_category_id_2[type=radio][value="2"]'
assert_select 'label[for=post_category_id_1]', 'Category 1'
assert_select 'label[for=post_category_id_2]', 'Category 2'
@@ -198,41 +198,83 @@ class FormCollectionsHelperTest < ActionView::TestCase
assert_select 'input[type=radio][value=false][checked=checked]'
end
+ test 'collection radio buttons generates only one hidden field for the entire collection, to ensure something will be sent back to the server when posting an empty collection' do
+ collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ with_collection_radio_buttons :user, :category_ids, collection, :id, :name
+
+ assert_select "input[type=hidden][name='user[category_ids][]'][value='']", count: 1
+ end
+
+ test 'collection radio buttons generates a hidden field using the given :name in :html_options' do
+ collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ with_collection_radio_buttons :user, :category_ids, collection, :id, :name, {}, { name: "user[other_category_ids][]" }
+
+ assert_select "input[type=hidden][name='user[other_category_ids][]'][value='']", count: 1
+ end
+
+ test 'collection radio buttons generates a hidden field with index if it was provided' do
+ collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ with_collection_radio_buttons :user, :category_ids, collection, :id, :name, { index: 322 }
+
+ assert_select "input[type=hidden][name='user[322][category_ids][]'][value='']", count: 1
+ end
+
+ test 'collection radio buttons does not generate a hidden field if include_hidden option is false' do
+ collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ with_collection_radio_buttons :user, :category_ids, collection, :id, :name, include_hidden: false
+
+ assert_select "input[type=hidden][name='user[category_ids][]'][value='']", count: 0
+ end
+
+ test 'collection radio buttons does not generate a hidden field if include_hidden option is false with key as string' do
+ collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ with_collection_radio_buttons :user, :category_ids, collection, :id, :name, 'include_hidden' => false
+
+ assert_select "input[type=hidden][name='user[category_ids][]'][value='']", count: 0
+ end
+
# COLLECTION CHECK BOXES
test 'collection check boxes accepts a collection and generate a series of checkboxes for value method' do
collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
with_collection_check_boxes :user, :category_ids, collection, :id, :name
- assert_select 'input#user_category_ids_1[type=checkbox][value=1]'
- assert_select 'input#user_category_ids_2[type=checkbox][value=2]'
+ assert_select 'input#user_category_ids_1[type=checkbox][value="1"]'
+ assert_select 'input#user_category_ids_2[type=checkbox][value="2"]'
end
test 'collection check boxes generates only one hidden field for the entire collection, to ensure something will be sent back to the server when posting an empty collection' do
collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
with_collection_check_boxes :user, :category_ids, collection, :id, :name
- assert_select "input[type=hidden][name='user[category_ids][]'][value=]", :count => 1
+ assert_select "input[type=hidden][name='user[category_ids][]'][value='']", :count => 1
end
test 'collection check boxes generates a hidden field using the given :name in :html_options' do
collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
with_collection_check_boxes :user, :category_ids, collection, :id, :name, {}, {name: "user[other_category_ids][]"}
- assert_select "input[type=hidden][name='user[other_category_ids][]'][value=]", :count => 1
+ assert_select "input[type=hidden][name='user[other_category_ids][]'][value='']", :count => 1
end
test 'collection check boxes generates a hidden field with index if it was provided' do
collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
with_collection_check_boxes :user, :category_ids, collection, :id, :name, { index: 322 }
- assert_select "input[type=hidden][name='user[322][category_ids][]'][value=]", count: 1
+ assert_select "input[type=hidden][name='user[322][category_ids][]'][value='']", count: 1
end
test 'collection check boxes does not generate a hidden field if include_hidden option is false' do
collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
with_collection_check_boxes :user, :category_ids, collection, :id, :name, include_hidden: false
- assert_select "input[type=hidden][name='user[category_ids][]'][value=]", :count => 0
+ assert_select "input[type=hidden][name='user[category_ids][]'][value='']", :count => 0
+ end
+
+ test 'collection check boxes does not generate a hidden field if include_hidden option is false with key as string' do
+ collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ with_collection_check_boxes :user, :category_ids, collection, :id, :name, 'include_hidden' => false
+
+ assert_select "input[type=hidden][name='user[category_ids][]'][value='']", count: 0
end
test 'collection check boxes accepts a collection and generate a series of checkboxes with labels for label method' do
@@ -260,8 +302,19 @@ class FormCollectionsHelperTest < ActionView::TestCase
collection = [[1, 'Category 1', {class: 'foo'}], [2, 'Category 2', {class: 'bar'}]]
with_collection_check_boxes :user, :active, collection, :first, :second
- assert_select 'input[type=checkbox][value=1].foo'
- assert_select 'input[type=checkbox][value=2].bar'
+ assert_select 'input[type=checkbox][value="1"].foo'
+ assert_select 'input[type=checkbox][value="2"].bar'
+ end
+
+ test 'collection check boxes propagates input id to the label for attribute' do
+ collection = [[1, 'Category 1', {id: 'foo'}], [2, 'Category 2', {id: 'bar'}]]
+ with_collection_check_boxes :user, :active, collection, :first, :second
+
+ assert_select 'input[type=checkbox][value="1"]#foo'
+ assert_select 'input[type=checkbox][value="2"]#bar'
+
+ assert_select 'label[for=foo]'
+ assert_select 'label[for=bar]'
end
test 'collection check boxes sets the label class defined inside the block' do
@@ -286,27 +339,27 @@ class FormCollectionsHelperTest < ActionView::TestCase
collection = (1..3).map{|i| [i, "Category #{i}"] }
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :checked => [1, 3]
- assert_select 'input[type=checkbox][value=1][checked=checked]'
- assert_select 'input[type=checkbox][value=3][checked=checked]'
- assert_no_select 'input[type=checkbox][value=2][checked=checked]'
+ assert_select 'input[type=checkbox][value="1"][checked=checked]'
+ assert_select 'input[type=checkbox][value="3"][checked=checked]'
+ assert_no_select 'input[type=checkbox][value="2"][checked=checked]'
end
test 'collection check boxes accepts selected string values as :checked option' do
collection = (1..3).map{|i| [i, "Category #{i}"] }
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :checked => ['1', '3']
- assert_select 'input[type=checkbox][value=1][checked=checked]'
- assert_select 'input[type=checkbox][value=3][checked=checked]'
- assert_no_select 'input[type=checkbox][value=2][checked=checked]'
+ assert_select 'input[type=checkbox][value="1"][checked=checked]'
+ assert_select 'input[type=checkbox][value="3"][checked=checked]'
+ assert_no_select 'input[type=checkbox][value="2"][checked=checked]'
end
test 'collection check boxes accepts a single checked value' do
collection = (1..3).map{|i| [i, "Category #{i}"] }
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :checked => 3
- assert_select 'input[type=checkbox][value=3][checked=checked]'
- assert_no_select 'input[type=checkbox][value=1][checked=checked]'
- assert_no_select 'input[type=checkbox][value=2][checked=checked]'
+ assert_select 'input[type=checkbox][value="3"][checked=checked]'
+ assert_no_select 'input[type=checkbox][value="1"][checked=checked]'
+ assert_no_select 'input[type=checkbox][value="2"][checked=checked]'
end
test 'collection check boxes accepts selected values as :checked option and override the model values' do
@@ -317,71 +370,71 @@ class FormCollectionsHelperTest < ActionView::TestCase
p.collection_check_boxes :category_ids, collection, :first, :last, :checked => [1, 3]
end
- assert_select 'input[type=checkbox][value=1][checked=checked]'
- assert_select 'input[type=checkbox][value=3][checked=checked]'
- assert_no_select 'input[type=checkbox][value=2][checked=checked]'
+ assert_select 'input[type=checkbox][value="1"][checked=checked]'
+ assert_select 'input[type=checkbox][value="3"][checked=checked]'
+ assert_no_select 'input[type=checkbox][value="2"][checked=checked]'
end
test 'collection check boxes accepts multiple disabled items' do
collection = (1..3).map{|i| [i, "Category #{i}"] }
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :disabled => [1, 3]
- assert_select 'input[type=checkbox][value=1][disabled=disabled]'
- assert_select 'input[type=checkbox][value=3][disabled=disabled]'
- assert_no_select 'input[type=checkbox][value=2][disabled=disabled]'
+ assert_select 'input[type=checkbox][value="1"][disabled=disabled]'
+ assert_select 'input[type=checkbox][value="3"][disabled=disabled]'
+ assert_no_select 'input[type=checkbox][value="2"][disabled=disabled]'
end
test 'collection check boxes accepts single disabled item' do
collection = (1..3).map{|i| [i, "Category #{i}"] }
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :disabled => 1
- assert_select 'input[type=checkbox][value=1][disabled=disabled]'
- assert_no_select 'input[type=checkbox][value=3][disabled=disabled]'
- assert_no_select 'input[type=checkbox][value=2][disabled=disabled]'
+ assert_select 'input[type=checkbox][value="1"][disabled=disabled]'
+ assert_no_select 'input[type=checkbox][value="3"][disabled=disabled]'
+ assert_no_select 'input[type=checkbox][value="2"][disabled=disabled]'
end
test 'collection check boxes accepts a proc to disabled items' do
collection = (1..3).map{|i| [i, "Category #{i}"] }
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :disabled => proc { |i| i.first == 1 }
- assert_select 'input[type=checkbox][value=1][disabled=disabled]'
- assert_no_select 'input[type=checkbox][value=3][disabled=disabled]'
- assert_no_select 'input[type=checkbox][value=2][disabled=disabled]'
+ assert_select 'input[type=checkbox][value="1"][disabled=disabled]'
+ assert_no_select 'input[type=checkbox][value="3"][disabled=disabled]'
+ assert_no_select 'input[type=checkbox][value="2"][disabled=disabled]'
end
test 'collection check boxes accepts multiple readonly items' do
collection = (1..3).map{|i| [i, "Category #{i}"] }
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :readonly => [1, 3]
- assert_select 'input[type=checkbox][value=1][readonly=readonly]'
- assert_select 'input[type=checkbox][value=3][readonly=readonly]'
- assert_no_select 'input[type=checkbox][value=2][readonly=readonly]'
+ assert_select 'input[type=checkbox][value="1"][readonly=readonly]'
+ assert_select 'input[type=checkbox][value="3"][readonly=readonly]'
+ assert_no_select 'input[type=checkbox][value="2"][readonly=readonly]'
end
test 'collection check boxes accepts single readonly item' do
collection = (1..3).map{|i| [i, "Category #{i}"] }
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :readonly => 1
- assert_select 'input[type=checkbox][value=1][readonly=readonly]'
- assert_no_select 'input[type=checkbox][value=3][readonly=readonly]'
- assert_no_select 'input[type=checkbox][value=2][readonly=readonly]'
+ assert_select 'input[type=checkbox][value="1"][readonly=readonly]'
+ assert_no_select 'input[type=checkbox][value="3"][readonly=readonly]'
+ assert_no_select 'input[type=checkbox][value="2"][readonly=readonly]'
end
test 'collection check boxes accepts a proc to readonly items' do
collection = (1..3).map{|i| [i, "Category #{i}"] }
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :readonly => proc { |i| i.first == 1 }
- assert_select 'input[type=checkbox][value=1][readonly=readonly]'
- assert_no_select 'input[type=checkbox][value=3][readonly=readonly]'
- assert_no_select 'input[type=checkbox][value=2][readonly=readonly]'
+ assert_select 'input[type=checkbox][value="1"][readonly=readonly]'
+ assert_no_select 'input[type=checkbox][value="3"][readonly=readonly]'
+ assert_no_select 'input[type=checkbox][value="2"][readonly=readonly]'
end
test 'collection check boxes accepts html options' do
collection = [[1, 'Category 1'], [2, 'Category 2']]
with_collection_check_boxes :user, :category_ids, collection, :first, :last, {}, :class => 'check'
- assert_select 'input.check[type=checkbox][value=1]'
- assert_select 'input.check[type=checkbox][value=2]'
+ assert_select 'input.check[type=checkbox][value="1"]'
+ assert_select 'input.check[type=checkbox][value="2"]'
end
test 'collection check boxes with fields for' do
@@ -390,8 +443,8 @@ class FormCollectionsHelperTest < ActionView::TestCase
p.collection_check_boxes :category_ids, collection, :id, :name
end
- assert_select 'input#post_category_ids_1[type=checkbox][value=1]'
- assert_select 'input#post_category_ids_2[type=checkbox][value=2]'
+ assert_select 'input#post_category_ids_1[type=checkbox][value="1"]'
+ assert_select 'input#post_category_ids_2[type=checkbox][value="2"]'
assert_select 'label[for=post_category_ids_1]', 'Category 1'
assert_select 'label[for=post_category_ids_2]', 'Category 2'
diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb
index a9f137aec6..e540bf27d9 100644
--- a/actionview/test/template/form_helper_test.rb
+++ b/actionview/test/template/form_helper_test.rb
@@ -40,6 +40,9 @@ class FormHelperTest < ActionView::TestCase
},
tag: {
value: "Tag"
+ },
+ post_delegate: {
+ title: 'Delegate model_name title'
}
}
}
@@ -59,6 +62,38 @@ class FormHelperTest < ActionView::TestCase
}
}
+ I18n.backend.store_translations 'placeholder', {
+ activemodel: {
+ attributes: {
+ post: {
+ cost: "Total cost"
+ },
+ :"post/cost" => {
+ uk: "Pounds"
+ }
+ }
+ },
+ helpers: {
+ placeholder: {
+ post: {
+ title: "What is this about?",
+ written_on: {
+ spanish: "Escrito en"
+ },
+ comments: {
+ body: "Write body here"
+ }
+ },
+ post_delegate: {
+ title: 'Delegate model_name title'
+ },
+ tag: {
+ value: "Tag"
+ }
+ }
+ }
+ }
+
@post = Post.new
@comment = Comment.new
def @post.errors()
@@ -70,7 +105,9 @@ class FormHelperTest < ActionView::TestCase
}.new
end
def @post.to_key; [123]; end
- def @post.id_before_type_cast; 123; end
+ def @post.id; 0; end
+ def @post.id_before_type_cast; "omg"; end
+ def @post.id_came_from_user?; true; end
def @post.to_param; '123'; end
@post.persisted = true
@@ -86,6 +123,10 @@ class FormHelperTest < ActionView::TestCase
@post.tags = []
@post.tags << Tag.new
+ @post_delegator = PostDelegator.new
+
+ @post_delegator.title = 'Hello World'
+
@car = Car.new("#000FFF")
end
@@ -218,6 +259,18 @@ class FormHelperTest < ActionView::TestCase
end
end
+ def test_label_with_non_active_record_object
+ form_for(OpenStruct.new(name:'ok'), as: 'person', url: 'an_url', html: { id: 'create-person' }) do |f|
+ f.label(:name)
+ end
+
+ expected = whole_form("an_url", "create-person", "new_person", method: "post") do
+ '<label for="person_name">Name</label>'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
def test_label_with_for_attribute_as_symbol
assert_dom_equal('<label for="my_for">Title</label>', label(:post, :title, nil, for: "my_for"))
end
@@ -290,13 +343,122 @@ class FormHelperTest < ActionView::TestCase
)
end
+ def test_label_with_block_and_builder
+ with_locale :label do
+ assert_dom_equal(
+ '<label for="post_body"><b>Write entire text here</b></label>',
+ label(:post, :body) { |b| "<b>#{b.translation}</b>".html_safe }
+ )
+ end
+ end
+
def test_label_with_block_in_erb
- assert_equal(
+ assert_dom_equal(
%{<label for="post_message">\n Message\n <input id="post_message" name="post[message]" type="text" />\n</label>},
view.render("test/label_with_block")
)
end
+ def test_label_with_to_model
+ assert_dom_equal(
+ %{<label for="post_delegator_title">Delegate Title</label>},
+ label(:post_delegator, :title)
+ )
+ end
+
+ def test_label_with_to_model_and_overridden_model_name
+ with_locale :label do
+ assert_dom_equal(
+ %{<label for="post_delegator_title">Delegate model_name title</label>},
+ label(:post_delegator, :title)
+ )
+ end
+ end
+
+ def test_text_field_placeholder_without_locales
+ with_locale :placeholder do
+ assert_dom_equal('<input id="post_body" name="post[body]" placeholder="Body" type="text" value="Back to the hill and over it again!" />', text_field(:post, :body, placeholder: true))
+ end
+ end
+
+ def test_text_field_placeholder_with_locales
+ with_locale :placeholder do
+ assert_dom_equal('<input id="post_title" name="post[title]" placeholder="What is this about?" type="text" value="Hello World" />', text_field(:post, :title, placeholder: true))
+ end
+ end
+
+ def test_text_field_placeholder_with_locales_and_to_model
+ with_locale :placeholder do
+ assert_dom_equal(
+ '<input id="post_delegator_title" name="post_delegator[title]" placeholder="Delegate model_name title" type="text" value="Hello World" />',
+ text_field(:post_delegator, :title, placeholder: true)
+ )
+ end
+ end
+
+ def test_text_field_placeholder_with_human_attribute_name
+ with_locale :placeholder do
+ assert_dom_equal('<input id="post_cost" name="post[cost]" placeholder="Total cost" type="text" />', text_field(:post, :cost, placeholder: true))
+ end
+ end
+
+ def test_text_field_placeholder_with_human_attribute_name_and_to_model
+ assert_dom_equal(
+ '<input id="post_delegator_title" name="post_delegator[title]" placeholder="Delegate Title" type="text" value="Hello World" />',
+ text_field(:post_delegator, :title, placeholder: true)
+ )
+ end
+
+ def test_text_field_placeholder_with_string_value
+ with_locale :placeholder do
+ assert_dom_equal('<input id="post_cost" name="post[cost]" placeholder="HOW MUCH?" type="text" />', text_field(:post, :cost, placeholder: "HOW MUCH?"))
+ end
+ end
+
+ def test_text_field_placeholder_with_human_attribute_name_and_value
+ with_locale :placeholder do
+ assert_dom_equal('<input id="post_cost" name="post[cost]" placeholder="Pounds" type="text" />', text_field(:post, :cost, placeholder: :uk))
+ end
+ end
+
+ def test_text_field_placeholder_with_locales_and_value
+ with_locale :placeholder do
+ assert_dom_equal('<input id="post_written_on" name="post[written_on]" placeholder="Escrito en" type="text" value="2004-06-15" />', text_field(:post, :written_on, placeholder: :spanish))
+ end
+ end
+
+ def test_text_field_placeholder_with_locales_and_nested_attributes
+ with_locale :placeholder do
+ form_for(@post, html: { id: 'create-post' }) do |f|
+ f.fields_for(:comments) do |cf|
+ concat cf.text_field(:body, placeholder: true)
+ end
+ end
+
+ expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do
+ '<input id="post_comments_attributes_0_body" name="post[comments_attributes][0][body]" placeholder="Write body here" type="text" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+ end
+
+ def test_text_field_placeholder_with_locales_fallback_and_nested_attributes
+ with_locale :placeholder do
+ form_for(@post, html: { id: 'create-post' }) do |f|
+ f.fields_for(:tags) do |cf|
+ concat cf.text_field(:value, placeholder: true)
+ end
+ end
+
+ expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do
+ '<input id="post_tags_attributes_0_value" name="post[tags_attributes][0][value]" placeholder="Tag" type="text" value="new tag" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+ end
+
def test_text_field
assert_dom_equal(
'<input id="post_title" name="post[title]" type="text" value="Hello World" />',
@@ -363,22 +525,36 @@ class FormHelperTest < ActionView::TestCase
def test_text_field_doesnt_change_param_values
object_name = 'post[]'
expected = '<input id="post_123_title" name="post[123][title]" type="text" value="Hello World" />'
- assert_equal expected, text_field(object_name, "title")
- assert_equal object_name, "post[]"
+ assert_dom_equal expected, text_field(object_name, "title")
end
- def test_file_field_has_no_size
+ def test_file_field_does_generate_a_hidden_field
+ expected = '<input name="user[avatar]" type="hidden" value="" /><input id="user_avatar" name="user[avatar]" type="file" />'
+ assert_dom_equal expected, file_field("user", "avatar")
+ end
+
+ def test_file_field_does_not_generate_a_hidden_field_if_included_hidden_option_is_false
+ expected = '<input id="user_avatar" name="user[avatar]" type="file" />'
+ assert_dom_equal expected, file_field("user", "avatar", include_hidden: false)
+ end
+
+ def test_file_field_does_not_generate_a_hidden_field_if_included_hidden_option_is_false_with_key_as_string
expected = '<input id="user_avatar" name="user[avatar]" type="file" />'
+ assert_dom_equal expected, file_field("user", "avatar", "include_hidden" => false)
+ end
+
+ def test_file_field_has_no_size
+ expected = '<input name="user[avatar]" type="hidden" value="" /><input id="user_avatar" name="user[avatar]" type="file" />'
assert_dom_equal expected, file_field("user", "avatar")
end
def test_file_field_with_multiple_behavior
- expected = '<input id="import_file" multiple="multiple" name="import[file][]" type="file" />'
+ expected = '<input name="import[file][]" type="hidden" value="" /><input id="import_file" multiple="multiple" name="import[file][]" type="file" />'
assert_dom_equal expected, file_field("import", "file", :multiple => true)
end
def test_file_field_with_multiple_behavior_and_explicit_name
- expected = '<input id="import_file" multiple="multiple" name="custom" type="file" />'
+ expected = '<input name="custom" type="hidden" value="" /><input id="import_file" multiple="multiple" name="custom" type="file" />'
assert_dom_equal expected, file_field("import", "file", :multiple => true, :name => "custom")
end
@@ -665,6 +841,92 @@ class FormHelperTest < ActionView::TestCase
)
end
+ def test_text_area_placeholder_without_locales
+ with_locale :placeholder do
+ assert_dom_equal(
+ %{<textarea id="post_body" name="post[body]" placeholder="Body">\nBack to the hill and over it again!</textarea>},
+ text_area(:post, :body, placeholder: true)
+ )
+ end
+ end
+
+ def test_text_area_placeholder_with_locales
+ with_locale :placeholder do
+ assert_dom_equal(
+ %{<textarea id="post_title" name="post[title]" placeholder="What is this about?">\nHello World</textarea>},
+ text_area(:post, :title, placeholder: true)
+ )
+ end
+ end
+
+ def test_text_area_placeholder_with_human_attribute_name
+ with_locale :placeholder do
+ assert_dom_equal(
+ %{<textarea id="post_cost" name="post[cost]" placeholder="Total cost">\n</textarea>},
+ text_area(:post, :cost, placeholder: true)
+ )
+ end
+ end
+
+ def test_text_area_placeholder_with_string_value
+ with_locale :placeholder do
+ assert_dom_equal(
+ %{<textarea id="post_cost" name="post[cost]" placeholder="HOW MUCH?">\n</textarea>},
+ text_area(:post, :cost, placeholder: "HOW MUCH?")
+ )
+ end
+ end
+
+ def test_text_area_placeholder_with_human_attribute_name_and_value
+ with_locale :placeholder do
+ assert_dom_equal(
+ %{<textarea id="post_cost" name="post[cost]" placeholder="Pounds">\n</textarea>},
+ text_area(:post, :cost, placeholder: :uk)
+ )
+ end
+ end
+
+ def test_text_area_placeholder_with_locales_and_value
+ with_locale :placeholder do
+ assert_dom_equal(
+ %{<textarea id="post_written_on" name="post[written_on]" placeholder="Escrito en">\n2004-06-15</textarea>},
+ text_area(:post, :written_on, placeholder: :spanish)
+ )
+ end
+ end
+
+ def test_text_area_placeholder_with_locales_and_nested_attributes
+ with_locale :placeholder do
+ form_for(@post, html: { id: 'create-post' }) do |f|
+ f.fields_for(:comments) do |cf|
+ concat cf.text_area(:body, placeholder: true)
+ end
+ end
+
+ expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do
+ %{<textarea id="post_comments_attributes_0_body" name="post[comments_attributes][0][body]" placeholder="Write body here">\n</textarea>}
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+ end
+
+ def test_text_area_placeholder_with_locales_fallback_and_nested_attributes
+ with_locale :placeholder do
+ form_for(@post, html: { id: 'create-post' }) do |f|
+ f.fields_for(:tags) do |cf|
+ concat cf.text_area(:value, placeholder: true)
+ end
+ end
+
+ expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do
+ %{<textarea id="post_tags_attributes_0_value" name="post[tags_attributes][0][value]" placeholder="Tag">\nnew tag</textarea>}
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+ end
+
def test_text_area
assert_dom_equal(
%{<textarea id="post_body" name="post[body]">\nBack to the hill and over it again!</textarea>},
@@ -694,9 +956,29 @@ class FormHelperTest < ActionView::TestCase
)
end
- def test_text_area_with_value_before_type_cast
+ def test_inputs_use_before_type_cast_to_retain_information_from_validations_like_numericality
+ assert_dom_equal(
+ %{<textarea id="post_id" name="post[id]">\nomg</textarea>},
+ text_area("post", "id")
+ )
+ end
+
+ def test_inputs_dont_use_before_type_cast_when_value_did_not_come_from_user
+ class << @post
+ undef id_came_from_user?
+ def id_came_from_user?; false; end
+ end
+
+ assert_dom_equal(
+ %{<textarea id="post_id" name="post[id]">\n0</textarea>},
+ text_area("post", "id")
+ )
+ end
+
+ def test_inputs_use_before_typecast_when_object_doesnt_respond_to_came_from_user
+ class << @post; undef id_came_from_user?; end
assert_dom_equal(
- %{<textarea id="post_id" name="post[id]">\n123</textarea>},
+ %{<textarea id="post_id" name="post[id]">\nomg</textarea>},
text_area("post", "id")
)
end
@@ -737,6 +1019,11 @@ class FormHelperTest < ActionView::TestCase
assert_dom_equal(expected, search_field("contact", "notes_query"))
end
+ def test_search_field_with_onsearch_value
+ expected = %{<input onsearch="true" type="search" name="contact[notes_query]" id="contact_notes_query" incremental="true" />}
+ assert_dom_equal(expected, search_field("contact", "notes_query", onsearch: true))
+ end
+
def test_telephone_field
expected = %{<input id="user_cell" name="user[cell]" type="tel" />}
assert_dom_equal(expected, telephone_field("user", "cell"))
@@ -1252,9 +1539,10 @@ class FormHelperTest < ActionView::TestCase
end
def test_form_for_requires_block
- assert_raises(ArgumentError) do
- form_for(:post, @post, html: { id: 'create-post' })
+ error = assert_raises(ArgumentError) do
+ form_for(@post, html: { id: 'create-post' })
end
+ assert_equal "Missing block", error.message
end
def test_form_for_requires_arguments
@@ -1290,7 +1578,7 @@ class FormHelperTest < ActionView::TestCase
"<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" +
"<input name='post[secret]' type='hidden' value='0' />" +
"<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" +
- "<input name='commit' type='submit' value='Create post' />" +
+ "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />" +
"<button name='button' type='submit'>Create post</button>" +
"<button name='button' type='submit'><span>Create post</span></button>"
end
@@ -1309,7 +1597,8 @@ class FormHelperTest < ActionView::TestCase
"<input id='post_active_true' name='post[active]' type='radio' value='true' />" +
"<label for='post_active_true'>true</label>" +
"<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" +
- "<label for='post_active_false'>false</label>"
+ "<label for='post_active_false'>false</label>" +
+ "<input type='hidden' name='post[active][]' value='' />"
end
assert_dom_equal expected, output_buffer
@@ -1332,7 +1621,8 @@ class FormHelperTest < ActionView::TestCase
"true</label>" +
"<label for='post_active_false'>"+
"<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" +
- "false</label>"
+ "false</label>" +
+ "<input type='hidden' name='post[active][]' value='' />"
end
assert_dom_equal expected, output_buffer
@@ -1358,6 +1648,7 @@ class FormHelperTest < ActionView::TestCase
"<label for='post_active_false'>"+
"<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" +
"false</label>"+
+ "<input type='hidden' name='post[active][]' value='' />" +
"<input id='post_id' name='post[id]' type='hidden' value='1' />"
end
@@ -1376,7 +1667,8 @@ class FormHelperTest < ActionView::TestCase
"<input id='foo_post_active_true' name='post[active]' type='radio' value='true' />" +
"<label for='foo_post_active_true'>true</label>" +
"<input checked='checked' id='foo_post_active_false' name='post[active]' type='radio' value='false' />" +
- "<label for='foo_post_active_false'>false</label>"
+ "<label for='foo_post_active_false'>false</label>" +
+ "<input type='hidden' name='post[active][]' value='' />"
end
assert_dom_equal expected, output_buffer
@@ -1394,7 +1686,8 @@ class FormHelperTest < ActionView::TestCase
"<input id='post_1_active_true' name='post[1][active]' type='radio' value='true' />" +
"<label for='post_1_active_true'>true</label>" +
"<input checked='checked' id='post_1_active_false' name='post[1][active]' type='radio' value='false' />" +
- "<label for='post_1_active_false'>false</label>"
+ "<label for='post_1_active_false'>false</label>" +
+ "<input type='hidden' name='post[1][active][]' value='' />"
end
assert_dom_equal expected, output_buffer
@@ -1523,7 +1816,7 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch", multipart: true) do
- "<input name='post[file]' type='file' id='post_file' />"
+ "<input name='post[file]' type='hidden' value='' /><input name='post[file]' type='file' id='post_file' />"
end
assert_dom_equal expected, output_buffer
@@ -1539,7 +1832,7 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch", multipart: true) do
- "<input name='post[comment][file]' type='file' id='post_comment_file' />"
+ "<input name='post[comment][file]' type='hidden' value='' /><input name='post[comment][file]' type='file' id='post_comment_file' />"
end
assert_dom_equal expected, output_buffer
@@ -1567,7 +1860,7 @@ class FormHelperTest < ActionView::TestCase
expected = whole_form("/posts/44", "edit_post_44", "edit_post", method: "patch") do
"<input name='post[title]' type='text' id='post_title' value='And his name will be forty and four.' />" +
- "<input name='commit' type='submit' value='Edit post' />"
+ "<input name='commit' data-disable-with='Edit post' type='submit' value='Edit post' />"
end
assert_dom_equal expected, output_buffer
@@ -1588,12 +1881,26 @@ class FormHelperTest < ActionView::TestCase
"<textarea name='other_name[body]' id='other_name_body'>\nBack to the hill and over it again!</textarea>" +
"<input name='other_name[secret]' value='0' type='hidden' />" +
"<input name='other_name[secret]' checked='checked' id='other_name_secret' value='1' type='checkbox' />" +
- "<input name='commit' value='Create post' type='submit' />"
+ "<input name='commit' value='Create post' data-disable-with='Create post' type='submit' />"
end
assert_dom_equal expected, output_buffer
end
+ def test_form_tags_do_not_call_private_properties_on_form_object
+ obj = Class.new do
+ private
+
+ def private_property
+ raise "This method should not be called."
+ end
+ end.new
+
+ form_for(obj, as: "other_name", url: '/', html: { id: "edit-other-name" }) do |f|
+ assert_raise(NoMethodError) { f.hidden_field(:private_property) }
+ end
+ end
+
def test_form_for_with_method_as_part_of_html_options
form_for(@post, url: '/', html: { id: 'create-post', method: :delete }) do |f|
concat f.text_field(:title)
@@ -1659,6 +1966,30 @@ class FormHelperTest < ActionView::TestCase
assert_dom_equal expected, output_buffer
end
+ def test_form_for_enforce_utf8_true
+ form_for(:post, enforce_utf8: true) do |f|
+ concat f.text_field(:title)
+ end
+
+ expected = whole_form("/", nil, nil, enforce_utf8: true) do
+ "<input name='post[title]' type='text' id='post_title' value='Hello World' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_enforce_utf8_false
+ form_for(:post, enforce_utf8: false) do |f|
+ concat f.text_field(:title)
+ end
+
+ expected = whole_form("/", nil, nil, enforce_utf8: false) do
+ "<input name='post[title]' type='text' id='post_title' value='Hello World' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
def test_form_for_with_remote_in_html
form_for(@post, url: '/', html: { remote: true, id: 'create-post', method: :patch }) do |f|
concat f.text_field(:title)
@@ -1678,21 +2009,22 @@ class FormHelperTest < ActionView::TestCase
def test_form_for_with_remote_without_html
@post.persisted = false
- @post.stubs(:to_key).returns(nil)
- form_for(@post, remote: true) do |f|
- concat f.text_field(:title)
- concat f.text_area(:body)
- concat f.check_box(:secret)
- end
+ @post.stub(:to_key, nil) do
+ form_for(@post, remote: true) do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ end
- expected = whole_form("/posts", "new_post", "new_post", remote: true) do
- "<input name='post[title]' type='text' id='post_title' value='Hello World' />" +
- "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" +
- "<input name='post[secret]' type='hidden' value='0' />" +
- "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />"
- end
+ expected = whole_form("/posts", "new_post", "new_post", remote: true) do
+ "<input name='post[title]' type='text' id='post_title' value='Hello World' />" +
+ "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" +
+ "<input name='post[secret]' type='hidden' value='0' />" +
+ "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />"
+ end
- assert_dom_equal expected, output_buffer
+ assert_dom_equal expected, output_buffer
+ end
end
def test_form_for_without_object
@@ -1758,7 +2090,7 @@ class FormHelperTest < ActionView::TestCase
expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
"<div class='field_with_errors'><label for='post_author_name' class='label'>Author name</label></div>" +
"<div class='field_with_errors'><input name='post[author_name]' type='text' id='post_author_name' value='' /></div>" +
- "<input name='commit' type='submit' value='Create post' />"
+ "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />"
end
assert_dom_equal expected, output_buffer
@@ -1776,7 +2108,7 @@ class FormHelperTest < ActionView::TestCase
expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
"<div class='field_with_errors'><label for='post_author_name' class='label'>Author name</label></div>" +
"<div class='field_with_errors'><input name='post[author_name]' type='text' id='post_author_name' value='' /></div>" +
- "<input name='commit' type='submit' value='Create post' />"
+ "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />"
end
assert_dom_equal expected, output_buffer
@@ -1895,16 +2227,17 @@ class FormHelperTest < ActionView::TestCase
def test_submit_with_object_as_new_record_and_locale_strings
with_locale :submit do
@post.persisted = false
- @post.stubs(:to_key).returns(nil)
- form_for(@post) do |f|
- concat f.submit
- end
+ @post.stub(:to_key, nil) do
+ form_for(@post) do |f|
+ concat f.submit
+ end
- expected = whole_form('/posts', 'new_post', 'new_post') do
- "<input name='commit' type='submit' value='Create Post' />"
- end
+ expected = whole_form('/posts', 'new_post', 'new_post') do
+ "<input name='commit' data-disable-with='Create Post' type='submit' value='Create Post' />"
+ end
- assert_dom_equal expected, output_buffer
+ assert_dom_equal expected, output_buffer
+ end
end
end
@@ -1915,7 +2248,7 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
- "<input name='commit' type='submit' value='Confirm Post changes' />"
+ "<input name='commit' data-disable-with='Confirm Post changes' type='submit' value='Confirm Post changes' />"
end
assert_dom_equal expected, output_buffer
@@ -1929,7 +2262,7 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form do
- "<input name='commit' class='extra' type='submit' value='Save changes' />"
+ "<input name='commit' class='extra' data-disable-with='Save changes' type='submit' value='Save changes' />"
end
assert_dom_equal expected, output_buffer
@@ -1943,7 +2276,7 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form('/posts/123', 'edit_another_post', 'edit_another_post', method: 'patch') do
- "<input name='commit' type='submit' value='Update your Post' />"
+ "<input name='commit' data-disable-with='Update your Post' type='submit' value='Update your Post' />"
end
assert_dom_equal expected, output_buffer
@@ -1965,6 +2298,27 @@ class FormHelperTest < ActionView::TestCase
assert_dom_equal expected, output_buffer
end
+ def test_deep_nested_fields_for
+ @comment.save
+ form_for(:posts) do |f|
+ f.fields_for('post[]', @post) do |f2|
+ f2.text_field(:id)
+ @post.comments.each do |comment|
+ concat f2.fields_for('comment[]', comment) { |c|
+ concat c.text_field(:name)
+ }
+ end
+ end
+ end
+
+ expected = whole_form do
+ "<input name='posts[post][0][comment][1][name]' type='text' id='posts_post_0_comment_1_name' value='comment #1' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+
def test_nested_fields_for_with_nested_collections
form_for(@post, as: 'post[]') do |f|
concat f.text_field(:title)
@@ -2482,11 +2836,12 @@ class FormHelperTest < ActionView::TestCase
def test_nested_fields_label_translation_with_more_than_10_records
@post.comments = Array.new(11) { |id| Comment.new(id + 1) }
- I18n.expects(:t).with('post.comments.body', default: [:"comment.body", ''], scope: "helpers.label").times(11).returns "Write body here"
-
- form_for(@post) do |f|
- f.fields_for(:comments) do |cf|
- concat cf.label(:body)
+ params = 11.times.map { ['post.comments.body', default: [:"comment.body", ''], scope: "helpers.label"] }
+ assert_called_with(I18n, :t, params, returns: "Write body here") do
+ form_for(@post) do |f|
+ f.fields_for(:comments) do |cf|
+ concat cf.label(:body)
+ end
end
end
end
@@ -2553,6 +2908,23 @@ class FormHelperTest < ActionView::TestCase
assert_dom_equal expected, output_buffer
end
+ def test_nested_fields_for_with_child_index_as_lambda_option_override_on_a_nested_attributes_collection_association
+ @post.comments = []
+
+ form_for(@post) do |f|
+ concat f.fields_for(:comments, Comment.new(321), child_index: -> { 'abc' } ) { |cf|
+ concat cf.text_field(:name)
+ }
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ '<input id="post_comments_attributes_abc_name" name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' +
+ '<input id="post_comments_attributes_abc_id" name="post[comments_attributes][abc][id]" type="hidden" value="321" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
class FakeAssociationProxy
def to_ary
[1, 2, 3]
@@ -2927,6 +3299,30 @@ class FormHelperTest < ActionView::TestCase
ActionView::Base.default_form_builder = old_default_form_builder
end
+ def test_form_builder_override
+ self.default_form_builder = LabelledFormBuilder
+
+ output_buffer = fields_for(:post, @post) do |f|
+ concat f.text_field(:title)
+ end
+
+ expected = "<label for='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><br/>"
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_lazy_loading_form_builder_override
+ self.default_form_builder = "FormHelperTest::LabelledFormBuilder"
+
+ output_buffer = fields_for(:post, @post) do |f|
+ concat f.text_field(:title)
+ end
+
+ expected = "<label for='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><br/>"
+
+ assert_dom_equal expected, output_buffer
+ end
+
def test_fields_for_with_labelled_builder
output_buffer = fields_for(:post, @post, builder: LabelledFormBuilder) do |f|
concat f.text_field(:title)
@@ -3004,7 +3400,7 @@ class FormHelperTest < ActionView::TestCase
def test_form_for_with_string_url_option
form_for(@post, url: 'http://www.otherdomain.com') do |f| end
- assert_equal whole_form("http://www.otherdomain.com", "edit_post_123", "edit_post", method: "patch"), output_buffer
+ assert_dom_equal whole_form("http://www.otherdomain.com", "edit_post_123", "edit_post", method: "patch"), output_buffer
end
def test_form_for_with_hash_url_option
@@ -3018,14 +3414,14 @@ class FormHelperTest < ActionView::TestCase
form_for(@post, url: @post) do |f| end
expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch")
- assert_equal expected, output_buffer
+ assert_dom_equal expected, output_buffer
end
def test_form_for_with_existing_object
form_for(@post) do |f| end
expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch")
- assert_equal expected, output_buffer
+ assert_dom_equal expected, output_buffer
end
def test_form_for_with_new_object
@@ -3036,7 +3432,7 @@ class FormHelperTest < ActionView::TestCase
form_for(post) do |f| end
expected = whole_form("/posts", "new_post", "new_post")
- assert_equal expected, output_buffer
+ assert_dom_equal expected, output_buffer
end
def test_form_for_with_existing_object_in_list
@@ -3073,7 +3469,7 @@ class FormHelperTest < ActionView::TestCase
form_for(@post, url: "/super_posts") do |f| end
expected = whole_form("/super_posts", "edit_post_123", "edit_post", method: "patch")
- assert_equal expected, output_buffer
+ assert_dom_equal expected, output_buffer
end
def test_form_for_with_default_method_as_patch
@@ -3108,8 +3504,14 @@ class FormHelperTest < ActionView::TestCase
protected
- def hidden_fields(method = nil)
- txt = %{<input name="utf8" type="hidden" value="&#x2713;" />}
+ def hidden_fields(options = {})
+ method = options[:method]
+
+ if options.fetch(:enforce_utf8, true)
+ txt = %{<input name="utf8" type="hidden" value="&#x2713;" />}
+ else
+ txt = ''
+ end
if method && !%w(get post).include?(method.to_s)
txt << %{<input name="_method" type="hidden" value="#{method}" />}
@@ -3133,7 +3535,7 @@ class FormHelperTest < ActionView::TestCase
method, remote, multipart = options.values_at(:method, :remote, :multipart)
- form_text(action, id, html_class, remote, multipart, method) + hidden_fields(method) + contents + "</form>"
+ form_text(action, id, html_class, remote, multipart, method) + hidden_fields(options.slice :method, :enforce_utf8) + contents + "</form>"
end
def protect_against_forgery?
diff --git a/actionview/test/template/form_options_helper_i18n_test.rb b/actionview/test/template/form_options_helper_i18n_test.rb
index 4972ea6511..26ede09a5f 100644
--- a/actionview/test/template/form_options_helper_i18n_test.rb
+++ b/actionview/test/template/form_options_helper_i18n_test.rb
@@ -14,8 +14,9 @@ class FormOptionsHelperI18nTests < ActionView::TestCase
end
def test_select_with_prompt_true_translates_prompt_message
- I18n.expects(:translate).with('helpers.select.prompt', { :default => 'Please select' })
- select('post', 'category', [], :prompt => true)
+ assert_called_with(I18n, :translate, ['helpers.select.prompt', { :default => 'Please select' }]) do
+ select('post', 'category', [], :prompt => true)
+ end
end
def test_select_with_translated_prompt
diff --git a/actionview/test/template/form_options_helper_test.rb b/actionview/test/template/form_options_helper_test.rb
index fbafb7aa08..d7daba8bf3 100644
--- a/actionview/test/template/form_options_helper_test.rb
+++ b/actionview/test/template/form_options_helper_test.rb
@@ -591,6 +591,19 @@ class FormOptionsHelperTest < ActionView::TestCase
)
end
+ def test_select_under_fields_for_with_block_without_options
+ @post = Post.new
+
+ output_buffer = fields_for :post, @post do |f|
+ concat(f.select(:category) {})
+ end
+
+ assert_dom_equal(
+ "<select id=\"post_category\" name=\"post[category]\"></select>",
+ output_buffer
+ )
+ end
+
def test_select_with_multiple_to_add_hidden_input
output_buffer = select(:post, :category, "", {}, :multiple => true)
assert_dom_equal(
@@ -632,6 +645,13 @@ class FormOptionsHelperTest < ActionView::TestCase
)
end
+ def test_select_with_include_blank_false_and_required
+ @post = Post.new
+ @post.category = "<mus>"
+ e = assert_raises(ArgumentError) { select("post", "category", %w( abe <mus> hest), { include_blank: false }, required: 'required') }
+ assert_match(/include_blank cannot be false for a required field./, e.message)
+ end
+
def test_select_with_blank_as_string
@post = Post.new
@post.category = "<mus>"
diff --git a/actionview/test/template/form_tag_helper_test.rb b/actionview/test/template/form_tag_helper_test.rb
index 18c739674a..de1eb89dc5 100644
--- a/actionview/test/template/form_tag_helper_test.rb
+++ b/actionview/test/template/form_tag_helper_test.rb
@@ -64,6 +64,18 @@ class FormTagHelperTest < ActionView::TestCase
assert_dom_equal expected, actual
end
+ def test_check_box_tag_disabled
+ actual = check_box_tag "admin","1", false, disabled: true
+ expected = %(<input id="admin" disabled="disabled" name="admin" type="checkbox" value="1" />)
+ assert_dom_equal expected, actual
+ end
+
+ def test_check_box_tag_default_checked
+ actual = check_box_tag "admin","1", true
+ expected = %(<input id="admin" checked="checked" name="admin" type="checkbox" value="1" />)
+ assert_dom_equal expected, actual
+ end
+
def test_check_box_tag_id_sanitized
label_elem = root_elem(check_box_tag("project[2][admin]"))
assert_match VALID_HTML_ID, label_elem['id']
@@ -170,6 +182,13 @@ class FormTagHelperTest < ActionView::TestCase
assert_dom_equal expected, actual
end
+ def test_multiple_field_tags_with_same_options
+ options = {class: 'important'}
+ assert_dom_equal %(<input name="title" type="file" id="title" class="important"/>), file_field_tag("title", options)
+ assert_dom_equal %(<input type="password" name="title" id="title" value="Hello!" class="important" />), password_field_tag("title", "Hello!", options)
+ assert_dom_equal %(<input type="text" name="title" id="title" value="Hello!" class="important" />), text_field_tag("title", "Hello!", options)
+ end
+
def test_radio_button_tag
actual = radio_button_tag "people", "david"
expected = %(<input id="people_david" name="people" type="radio" value="david" />)
@@ -203,13 +222,13 @@ class FormTagHelperTest < ActionView::TestCase
end
def test_select_tag_with_multiple
- actual = select_tag "colors", "<option>Red</option><option>Blue</option><option>Green</option>".html_safe, :multiple => :true
- expected = %(<select id="colors" multiple="multiple" name="colors"><option>Red</option><option>Blue</option><option>Green</option></select>)
+ actual = select_tag "colors", "<option>Red</option><option>Blue</option><option>Green</option>".html_safe, multiple: true
+ expected = %(<select id="colors" multiple="multiple" name="colors[]"><option>Red</option><option>Blue</option><option>Green</option></select>)
assert_dom_equal expected, actual
end
def test_select_tag_disabled
- actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, :disabled => :true
+ actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, disabled: true
expected = %(<select id="places" disabled="disabled" name="places"><option>Home</option><option>Work</option><option>Pub</option></select>)
assert_dom_equal expected, actual
end
@@ -225,6 +244,18 @@ class FormTagHelperTest < ActionView::TestCase
assert_dom_equal expected, actual
end
+ def test_select_tag_with_include_blank_false
+ actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, include_blank: false
+ expected = %(<select id="places" name="places"><option>Home</option><option>Work</option><option>Pub</option></select>)
+ assert_dom_equal expected, actual
+ end
+
+ def test_select_tag_with_include_blank_string
+ actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, include_blank: 'Choose'
+ expected = %(<select id="places" name="places"><option value="">Choose</option><option>Home</option><option>Work</option><option>Pub</option></select>)
+ assert_dom_equal expected, actual
+ end
+
def test_select_tag_with_prompt
actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, :prompt => "string"
expected = %(<select id="places" name="places"><option value="">string</option><option>Home</option><option>Work</option><option>Pub</option></select>)
@@ -332,12 +363,18 @@ class FormTagHelperTest < ActionView::TestCase
assert_dom_equal expected, actual
end
- def test_text_field_disabled
- actual = text_field_tag "title", "Hello!", :disabled => :true
+ def test_text_field_tag_disabled
+ actual = text_field_tag "title", "Hello!", disabled: true
expected = %(<input id="title" name="title" disabled="disabled" type="text" value="Hello!" />)
assert_dom_equal expected, actual
end
+ def test_text_field_tag_with_placeholder_option
+ actual = text_field_tag "title", "Hello!", placeholder: 'Enter search term...'
+ expected = %(<input id="title" name="title" placeholder="Enter search term..." type="text" value="Hello!" />)
+ assert_dom_equal expected, actual
+ end
+
def test_text_field_tag_with_multiple_options
actual = text_field_tag "title", "Hello!", :size => 70, :maxlength => 80
expected = %(<input id="title" name="title" size="70" maxlength="80" type="text" value="Hello!" />)
@@ -414,6 +451,44 @@ class FormTagHelperTest < ActionView::TestCase
)
end
+ def test_empty_submit_tag
+ assert_dom_equal(
+ %(<input data-disable-with="Save" name='commit' type="submit" value="Save" />),
+ submit_tag("Save")
+ )
+ end
+
+ def test_empty_submit_tag_with_opt_out
+ ActionView::Base.automatically_disable_submit_tag = false
+ assert_dom_equal(
+ %(<input name='commit' type="submit" value="Save" />),
+ submit_tag("Save")
+ )
+ ensure
+ ActionView::Base.automatically_disable_submit_tag = true
+ end
+
+ def test_submit_tag_having_data_disable_with_string
+ assert_dom_equal(
+ %(<input data-disable-with="Processing..." data-confirm="Are you sure?" name='commit' type="submit" value="Save" />),
+ submit_tag("Save", { "data-disable-with" => "Processing...", "data-confirm" => "Are you sure?" })
+ )
+ end
+
+ def test_submit_tag_having_data_disable_with_boolean
+ assert_dom_equal(
+ %(<input data-confirm="Are you sure?" name='commit' type="submit" value="Save" />),
+ submit_tag("Save", { "data-disable-with" => false, "data-confirm" => "Are you sure?" })
+ )
+ end
+
+ def test_submit_tag_having_data_hash_disable_with_boolean
+ assert_dom_equal(
+ %(<input data-confirm="Are you sure?" name='commit' type="submit" value="Save" />),
+ submit_tag("Save", { :data => { :confirm => "Are you sure?", :disable_with => false } })
+ )
+ end
+
def test_submit_tag_with_no_onclick_options
assert_dom_equal(
%(<input name='commit' data-disable-with="Saving..." type="submit" value="Save" />),
@@ -423,11 +498,19 @@ class FormTagHelperTest < ActionView::TestCase
def test_submit_tag_with_confirmation
assert_dom_equal(
- %(<input name='commit' type='submit' value='Save' data-confirm="Are you sure?" />),
+ %(<input name='commit' type='submit' value='Save' data-confirm="Are you sure?" data-disable-with="Save" />),
submit_tag("Save", :data => { :confirm => "Are you sure?" })
)
end
+ def test_submit_tag_doesnt_have_data_disable_with_twice
+ assert_equal(
+ %(<input type="submit" name="commit" value="Save" data-confirm="Are you sure?" data-disable-with="Processing..." />),
+ submit_tag("Save", { "data-disable-with" => "Processing...", "data-confirm" => "Are you sure?" })
+ )
+ end
+
+
def test_button_tag
assert_dom_equal(
%(<button name="button" type="submit">Button</button>),
@@ -491,6 +574,13 @@ class FormTagHelperTest < ActionView::TestCase
)
end
+ def test_button_tag_with_data_disable_with_option
+ assert_dom_equal(
+ %(<button name="button" type="submit" data-disable-with="Please wait...">Checkout</button>),
+ button_tag("Checkout", data: { disable_with: "Please wait..." })
+ )
+ end
+
def test_image_submit_tag_with_confirmation
assert_dom_equal(
%(<input alt="Save" type="image" src="/images/save.gif" data-confirm="Are you sure?" />),
@@ -632,6 +722,6 @@ class FormTagHelperTest < ActionView::TestCase
private
def root_elem(rendered_content)
- HTML::Document.new(rendered_content).root.children[0]
+ Nokogiri::HTML::DocumentFragment.parse(rendered_content).children.first # extract from nodeset
end
end
diff --git a/actionview/test/template/html-scanner/cdata_node_test.rb b/actionview/test/template/html-scanner/cdata_node_test.rb
deleted file mode 100644
index 9b58174641..0000000000
--- a/actionview/test/template/html-scanner/cdata_node_test.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-require 'abstract_unit'
-
-class CDATANodeTest < ActiveSupport::TestCase
- def setup
- @node = HTML::CDATA.new(nil, 0, 0, "<p>howdy</p>")
- end
-
- def test_to_s
- assert_equal "<![CDATA[<p>howdy</p>]]>", @node.to_s
- end
-
- def test_content
- assert_equal "<p>howdy</p>", @node.content
- end
-end
diff --git a/actionview/test/template/html-scanner/document_test.rb b/actionview/test/template/html-scanner/document_test.rb
deleted file mode 100644
index 17f045d549..0000000000
--- a/actionview/test/template/html-scanner/document_test.rb
+++ /dev/null
@@ -1,148 +0,0 @@
-require 'abstract_unit'
-
-class DocumentTest < ActiveSupport::TestCase
- def test_handle_doctype
- doc = nil
- assert_nothing_raised do
- doc = HTML::Document.new <<-HTML.strip
- <!DOCTYPE "blah" "blah" "blah">
- <html>
- </html>
- HTML
- end
- assert_equal 3, doc.root.children.length
- assert_equal %{<!DOCTYPE "blah" "blah" "blah">}, doc.root.children[0].content
- assert_match %r{\s+}m, doc.root.children[1].content
- assert_equal "html", doc.root.children[2].name
- end
-
- def test_find_img
- doc = HTML::Document.new <<-HTML.strip
- <html>
- <body>
- <p><img src="hello.gif"></p>
- </body>
- </html>
- HTML
- assert doc.find(:tag=>"img", :attributes=>{"src"=>"hello.gif"})
- end
-
- def test_find_all
- doc = HTML::Document.new <<-HTML.strip
- <html>
- <body>
- <p class="test"><img src="hello.gif"></p>
- <div class="foo">
- <p class="test">something</p>
- <p>here is <em class="test">more</em></p>
- </div>
- </body>
- </html>
- HTML
- all = doc.find_all :attributes => { :class => "test" }
- assert_equal 3, all.length
- assert_equal [ "p", "p", "em" ], all.map { |n| n.name }
- end
-
- def test_find_with_text
- doc = HTML::Document.new <<-HTML.strip
- <html>
- <body>
- <p>Some text</p>
- </body>
- </html>
- HTML
- assert doc.find(:content => "Some text")
- assert doc.find(:tag => "p", :child => { :content => "Some text" })
- assert doc.find(:tag => "p", :child => "Some text")
- assert doc.find(:tag => "p", :content => "Some text")
- end
-
- def test_parse_xml
- assert_nothing_raised { HTML::Document.new("<tags><tag/></tags>", true, true) }
- assert_nothing_raised { HTML::Document.new("<outer><link>something</link></outer>", true, true) }
- end
-
- def test_parse_document
- doc = HTML::Document.new(<<-HTML)
- <div>
- <h2>blah</h2>
- <table>
- </table>
- </div>
- HTML
- assert_not_nil doc.find(:tag => "div", :children => { :count => 1, :only => { :tag => "table" } })
- end
-
- def test_tag_nesting_nothing_to_s
- doc = HTML::Document.new("<tag></tag>")
- assert_equal "<tag></tag>", doc.root.to_s
- end
-
- def test_tag_nesting_space_to_s
- doc = HTML::Document.new("<tag> </tag>")
- assert_equal "<tag> </tag>", doc.root.to_s
- end
-
- def test_tag_nesting_text_to_s
- doc = HTML::Document.new("<tag>text</tag>")
- assert_equal "<tag>text</tag>", doc.root.to_s
- end
-
- def test_tag_nesting_tag_to_s
- doc = HTML::Document.new("<tag><nested /></tag>")
- assert_equal "<tag><nested /></tag>", doc.root.to_s
- end
-
- def test_parse_cdata
- doc = HTML::Document.new(<<-HTML)
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
- <head>
- <title><![CDATA[<br>]]></title>
- </head>
- <body>
- <p>this document has &lt;br&gt; for a title</p>
- </body>
-</html>
-HTML
-
- assert_nil doc.find(:tag => "title", :descendant => { :tag => "br" })
- assert doc.find(:tag => "title", :child => "<br>")
- end
-
- def test_find_empty_tag
- doc = HTML::Document.new("<div id='map'></div>")
- assert_nil doc.find(:tag => "div", :attributes => { :id => "map" }, :content => /./)
- assert doc.find(:tag => "div", :attributes => { :id => "map" }, :content => /\A\Z/)
- assert doc.find(:tag => "div", :attributes => { :id => "map" }, :content => /^$/)
- assert doc.find(:tag => "div", :attributes => { :id => "map" }, :content => "")
- assert doc.find(:tag => "div", :attributes => { :id => "map" }, :content => nil)
- end
-
- def test_parse_invalid_document
- assert_nothing_raised do
- HTML::Document.new("<html>
- <table>
- <tr>
- <td style=\"color: #FFFFFF; height: 17px; onclick=\"window.location.href='http://www.rmeinc.com/about_rme.aspx'\" style=\"cursor:pointer; height: 17px;\"; nowrap onclick=\"window.location.href='http://www.rmeinc.com/about_rme.aspx'\" onmouseout=\"this.bgColor='#0066cc'; this.style.color='#FFFFFF'\" onmouseover=\"this.bgColor='#ffffff'; this.style.color='#0033cc'\">About Us</td>
- </tr>
- </table>
- </html>")
- end
- end
-
- def test_invalid_document_raises_exception_when_strict
- assert_raise RuntimeError do
- HTML::Document.new("<html>
- <table>
- <tr>
- <td style=\"color: #FFFFFF; height: 17px; onclick=\"window.location.href='http://www.rmeinc.com/about_rme.aspx'\" style=\"cursor:pointer; height: 17px;\"; nowrap onclick=\"window.location.href='http://www.rmeinc.com/about_rme.aspx'\" onmouseout=\"this.bgColor='#0066cc'; this.style.color='#FFFFFF'\" onmouseover=\"this.bgColor='#ffffff'; this.style.color='#0033cc'\">About Us</td>
- </tr>
- </table>
- </html>", true)
- end
- end
-
-end
diff --git a/actionview/test/template/html-scanner/node_test.rb b/actionview/test/template/html-scanner/node_test.rb
deleted file mode 100644
index 5b5d092036..0000000000
--- a/actionview/test/template/html-scanner/node_test.rb
+++ /dev/null
@@ -1,89 +0,0 @@
-require 'abstract_unit'
-
-class NodeTest < ActiveSupport::TestCase
-
- class MockNode
- def initialize(matched, value)
- @matched = matched
- @value = value
- end
-
- def find(conditions)
- @matched && self
- end
-
- def to_s
- @value.to_s
- end
- end
-
- def setup
- @node = HTML::Node.new("parent")
- @node.children.concat [MockNode.new(false,1), MockNode.new(true,"two"), MockNode.new(false,:three)]
- end
-
- def test_match
- assert !@node.match("foo")
- end
-
- def test_tag
- assert !@node.tag?
- end
-
- def test_to_s
- assert_equal "1twothree", @node.to_s
- end
-
- def test_find
- assert_equal "two", @node.find('blah').to_s
- end
-
- def test_parse_strict
- s = "<b foo='hello'' bar='baz'>"
- assert_raise(RuntimeError) { HTML::Node.parse(nil,0,0,s) }
- end
-
- def test_parse_relaxed
- s = "<b foo='hello'' bar='baz'>"
- node = nil
- assert_nothing_raised { node = HTML::Node.parse(nil,0,0,s,false) }
- assert node.attributes.has_key?("foo")
- assert !node.attributes.has_key?("bar")
- end
-
- def test_to_s_with_boolean_attrs
- s = "<b foo bar>"
- node = HTML::Node.parse(nil,0,0,s)
- assert node.attributes.has_key?("foo")
- assert node.attributes.has_key?("bar")
- assert "<b foo bar>", node.to_s
- end
-
- def test_parse_with_unclosed_tag
- s = "<span onmouseover='bang'"
- node = nil
- assert_nothing_raised { node = HTML::Node.parse(nil,0,0,s,false) }
- assert node.attributes.has_key?("onmouseover")
- end
-
- def test_parse_with_valid_cdata_section
- s = "<![CDATA[<span>contents</span>]]>"
- node = nil
- assert_nothing_raised { node = HTML::Node.parse(nil,0,0,s,false) }
- assert_kind_of HTML::CDATA, node
- assert_equal '<span>contents</span>', node.content
- end
-
- def test_parse_strict_with_unterminated_cdata_section
- s = "<![CDATA[neverending..."
- assert_raise(RuntimeError) { HTML::Node.parse(nil,0,0,s) }
- end
-
- def test_parse_relaxed_with_unterminated_cdata_section
- s = "<![CDATA[neverending..."
- node = nil
- assert_nothing_raised { node = HTML::Node.parse(nil,0,0,s,false) }
- assert_kind_of HTML::CDATA, node
- assert_equal 'neverending...', node.content
- end
-end
diff --git a/actionview/test/template/html-scanner/sanitizer_test.rb b/actionview/test/template/html-scanner/sanitizer_test.rb
deleted file mode 100644
index b1c1b83807..0000000000
--- a/actionview/test/template/html-scanner/sanitizer_test.rb
+++ /dev/null
@@ -1,330 +0,0 @@
-require 'abstract_unit'
-
-class SanitizerTest < ActionController::TestCase
- def setup
- @sanitizer = nil # used by assert_sanitizer
- end
-
- def test_strip_tags_with_quote
- sanitizer = HTML::FullSanitizer.new
- string = '<" <img src="trollface.gif" onload="alert(1)"> hi'
-
- assert_equal ' hi', sanitizer.sanitize(string)
- end
-
- def test_strip_tags
- sanitizer = HTML::FullSanitizer.new
- assert_equal("<<<bad html", sanitizer.sanitize("<<<bad html"))
- assert_equal("<<", sanitizer.sanitize("<<<bad html>"))
- assert_equal("Dont touch me", sanitizer.sanitize("Dont touch me"))
- assert_equal("This is a test.", sanitizer.sanitize("<p>This <u>is<u> a <a href='test.html'><strong>test</strong></a>.</p>"))
- assert_equal("Weirdos", sanitizer.sanitize("Wei<<a>a onclick='alert(document.cookie);'</a>/>rdos"))
- assert_equal("This is a test.", sanitizer.sanitize("This is a test."))
- assert_equal(
- %{This is a test.\n\n\nIt no longer contains any HTML.\n}, sanitizer.sanitize(
- %{<title>This is <b>a <a href="" target="_blank">test</a></b>.</title>\n\n<!-- it has a comment -->\n\n<p>It no <b>longer <strong>contains <em>any <strike>HTML</strike></em>.</strong></b></p>\n}))
- assert_equal "This has a here.", sanitizer.sanitize("This has a <!-- comment --> here.")
- assert_equal "This has a here.", sanitizer.sanitize("This has a <![CDATA[<section>]]> here.")
- assert_equal "This has an unclosed ", sanitizer.sanitize("This has an unclosed <![CDATA[<section>]] here...")
- [nil, '', ' '].each { |blank| assert_equal blank, sanitizer.sanitize(blank) }
- assert_nothing_raised { sanitizer.sanitize("This is a frozen string with no tags".freeze) }
- end
-
- def test_strip_links
- sanitizer = HTML::LinkSanitizer.new
- assert_equal "Dont touch me", sanitizer.sanitize("Dont touch me")
- assert_equal "on my mind\nall day long", sanitizer.sanitize("<a href='almost'>on my mind</a>\n<A href='almost'>all day long</A>")
- assert_equal "0wn3d", sanitizer.sanitize("<a href='http://www.rubyonrails.com/'><a href='http://www.rubyonrails.com/' onlclick='steal()'>0wn3d</a></a>")
- assert_equal "Magic", sanitizer.sanitize("<a href='http://www.rubyonrails.com/'>Mag<a href='http://www.ruby-lang.org/'>ic")
- assert_equal "FrrFox", sanitizer.sanitize("<href onlclick='steal()'>FrrFox</a></href>")
- assert_equal "My mind\nall <b>day</b> long", sanitizer.sanitize("<a href='almost'>My mind</a>\n<A href='almost'>all <b>day</b> long</A>")
- assert_equal "all <b>day</b> long", sanitizer.sanitize("<<a>a href='hello'>all <b>day</b> long<</A>/a>")
-
- assert_equal "<a<a", sanitizer.sanitize("<a<a")
- end
-
- def test_sanitize_form
- assert_sanitized "<form action=\"/foo/bar\" method=\"post\"><input></form>", ''
- end
-
- def test_sanitize_plaintext
- raw = "<plaintext><span>foo</span></plaintext>"
- assert_sanitized raw, "<span>foo</span>"
- end
-
- def test_sanitize_script
- assert_sanitized "a b c<script language=\"Javascript\">blah blah blah</script>d e f", "a b cd e f"
- end
-
- def test_sanitize_js_handlers
- raw = %{onthis="do that" <a href="#" onclick="hello" name="foo" onbogus="remove me">hello</a>}
- assert_sanitized raw, %{onthis="do that" <a name="foo" href="#">hello</a>}
- end
-
- def test_sanitize_javascript_href
- raw = %{href="javascript:bang" <a href="javascript:bang" name="hello">foo</a>, <span href="javascript:bang">bar</span>}
- assert_sanitized raw, %{href="javascript:bang" <a name="hello">foo</a>, <span>bar</span>}
- end
-
- def test_sanitize_image_src
- raw = %{src="javascript:bang" <img src="javascript:bang" width="5">foo</img>, <span src="javascript:bang">bar</span>}
- assert_sanitized raw, %{src="javascript:bang" <img width="5">foo</img>, <span>bar</span>}
- end
-
- HTML::WhiteListSanitizer.allowed_tags.each do |tag_name|
- define_method "test_should_allow_#{tag_name}_tag" do
- assert_sanitized "start <#{tag_name} title=\"1\" onclick=\"foo\">foo <bad>bar</bad> baz</#{tag_name}> end", %(start <#{tag_name} title="1">foo bar baz</#{tag_name}> end)
- end
- end
-
- def test_should_allow_anchors
- assert_sanitized %(<a href="foo" onclick="bar"><script>baz</script></a>), %(<a href="foo"></a>)
- end
-
- # RFC 3986, sec 4.2
- def test_allow_colons_in_path_component
- assert_sanitized("<a href=\"./this:that\">foo</a>")
- end
-
- %w(src width height alt).each do |img_attr|
- define_method "test_should_allow_image_#{img_attr}_attribute" do
- assert_sanitized %(<img #{img_attr}="foo" onclick="bar" />), %(<img #{img_attr}="foo" />)
- end
- end
-
- def test_should_handle_non_html
- assert_sanitized 'abc'
- end
-
- def test_should_handle_blank_text
- assert_sanitized nil
- assert_sanitized ''
- end
-
- def test_should_allow_custom_tags
- text = "<u>foo</u>"
- sanitizer = HTML::WhiteListSanitizer.new
- assert_equal(text, sanitizer.sanitize(text, :tags => %w(u)))
- end
-
- def test_should_allow_only_custom_tags
- text = "<u>foo</u> with <i>bar</i>"
- sanitizer = HTML::WhiteListSanitizer.new
- assert_equal("<u>foo</u> with bar", sanitizer.sanitize(text, :tags => %w(u)))
- end
-
- def test_should_allow_custom_tags_with_attributes
- text = %(<blockquote cite="http://example.com/">foo</blockquote>)
- sanitizer = HTML::WhiteListSanitizer.new
- assert_equal(text, sanitizer.sanitize(text))
- end
-
- def test_should_allow_custom_tags_with_custom_attributes
- text = %(<blockquote foo="bar">Lorem ipsum</blockquote>)
- sanitizer = HTML::WhiteListSanitizer.new
- assert_equal(text, sanitizer.sanitize(text, :attributes => ['foo']))
- end
-
- def test_should_raise_argument_error_if_tags_is_not_enumerable
- sanitizer = HTML::WhiteListSanitizer.new
- e = assert_raise(ArgumentError) do
- sanitizer.sanitize('', :tags => 'foo')
- end
-
- assert_equal "You should pass :tags as an Enumerable", e.message
- end
-
- def test_should_raise_argument_error_if_attributes_is_not_enumerable
- sanitizer = HTML::WhiteListSanitizer.new
- e = assert_raise(ArgumentError) do
- sanitizer.sanitize('', :attributes => 'foo')
- end
-
- assert_equal "You should pass :attributes as an Enumerable", e.message
- end
-
- [%w(img src), %w(a href)].each do |(tag, attr)|
- define_method "test_should_strip_#{attr}_attribute_in_#{tag}_with_bad_protocols" do
- assert_sanitized %(<#{tag} #{attr}="javascript:bang" title="1">boo</#{tag}>), %(<#{tag} title="1">boo</#{tag}>)
- end
- end
-
- def test_should_flag_bad_protocols
- sanitizer = HTML::WhiteListSanitizer.new
- %w(about chrome data disk hcp help javascript livescript lynxcgi lynxexec ms-help ms-its mhtml mocha opera res resource shell vbscript view-source vnd.ms.radio wysiwyg).each do |proto|
- assert sanitizer.send(:contains_bad_protocols?, 'src', "#{proto}://bad")
- end
- end
-
- def test_should_accept_good_protocols_ignoring_case
- sanitizer = HTML::WhiteListSanitizer.new
- HTML::WhiteListSanitizer.allowed_protocols.each do |proto|
- assert !sanitizer.send(:contains_bad_protocols?, 'src', "#{proto.capitalize}://good")
- end
- end
-
- def test_should_accept_good_protocols_ignoring_space
- sanitizer = HTML::WhiteListSanitizer.new
- HTML::WhiteListSanitizer.allowed_protocols.each do |proto|
- assert !sanitizer.send(:contains_bad_protocols?, 'src', " #{proto}://good")
- end
- end
-
- def test_should_accept_good_protocols
- sanitizer = HTML::WhiteListSanitizer.new
- HTML::WhiteListSanitizer.allowed_protocols.each do |proto|
- assert !sanitizer.send(:contains_bad_protocols?, 'src', "#{proto}://good")
- end
- end
-
- def test_should_reject_hex_codes_in_protocol
- assert_sanitized %(<a href="&#37;6A&#37;61&#37;76&#37;61&#37;73&#37;63&#37;72&#37;69&#37;70&#37;74&#37;3A&#37;61&#37;6C&#37;65&#37;72&#37;74&#37;28&#37;22&#37;58&#37;53&#37;53&#37;22&#37;29">1</a>), "<a>1</a>"
- assert @sanitizer.send(:contains_bad_protocols?, 'src', "%6A%61%76%61%73%63%72%69%70%74%3A%61%6C%65%72%74%28%22%58%53%53%22%29")
- end
-
- def test_should_block_script_tag
- assert_sanitized %(<SCRIPT\nSRC=http://ha.ckers.org/xss.js></SCRIPT>), ""
- end
-
- [%(<IMG SRC="javascript:alert('XSS');">),
- %(<IMG SRC=javascript:alert('XSS')>),
- %(<IMG SRC=JaVaScRiPt:alert('XSS')>),
- %(<IMG """><SCRIPT>alert("XSS")</SCRIPT>">),
- %(<IMG SRC=javascript:alert(&quot;XSS&quot;)>),
- %(<IMG SRC=javascript:alert(String.fromCharCode(88,83,83))>),
- %(<IMG SRC=&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;&#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;>),
- %(<IMG SRC=&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041>),
- %(<IMG SRC=&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29>),
- %(<IMG SRC="jav\tascript:alert('XSS');">),
- %(<IMG SRC="jav&#x09;ascript:alert('XSS');">),
- %(<IMG SRC="jav&#x0A;ascript:alert('XSS');">),
- %(<IMG SRC="jav&#x0D;ascript:alert('XSS');">),
- %(<IMG SRC=" &#14; javascript:alert('XSS');">),
- %(<IMG SRC="javascript&#x3a;alert('XSS');">),
- %(<IMG SRC=`javascript:alert("RSnake says, 'XSS'")`>)].each_with_index do |img_hack, i|
- define_method "test_should_not_fall_for_xss_image_hack_#{i+1}" do
- assert_sanitized img_hack, "<img>"
- end
- end
-
- def test_should_sanitize_tag_broken_up_by_null
- assert_sanitized %(<SCR\0IPT>alert(\"XSS\")</SCR\0IPT>), "alert(\"XSS\")"
- end
-
- def test_should_sanitize_invalid_script_tag
- assert_sanitized %(<SCRIPT/XSS SRC="http://ha.ckers.org/xss.js"></SCRIPT>), ""
- end
-
- def test_should_sanitize_script_tag_with_multiple_open_brackets
- assert_sanitized %(<<SCRIPT>alert("XSS");//<</SCRIPT>), "&lt;"
- assert_sanitized %(<iframe src=http://ha.ckers.org/scriptlet.html\n<a), %(&lt;a)
- end
-
- def test_should_sanitize_unclosed_script
- assert_sanitized %(<SCRIPT SRC=http://ha.ckers.org/xss.js?<B>), "<b>"
- end
-
- def test_should_sanitize_half_open_scripts
- assert_sanitized %(<IMG SRC="javascript:alert('XSS')"), "<img>"
- end
-
- def test_should_not_fall_for_ridiculous_hack
- img_hack = %(<IMG\nSRC\n=\n"\nj\na\nv\na\ns\nc\nr\ni\np\nt\n:\na\nl\ne\nr\nt\n(\n'\nX\nS\nS\n'\n)\n"\n>)
- assert_sanitized img_hack, "<img>"
- end
-
- def test_should_sanitize_attributes
- assert_sanitized %(<SPAN title="'><script>alert()</script>">blah</SPAN>), %(<span title="#{CGI.escapeHTML "'><script>alert()</script>"}">blah</span>)
- end
-
- def test_should_sanitize_illegal_style_properties
- raw = %(display:block; position:absolute; left:0; top:0; width:100%; height:100%; z-index:1; background-color:black; background-image:url(http://www.ragingplatypus.com/i/cam-full.jpg); background-x:center; background-y:center; background-repeat:repeat;)
- expected = %(display: block; width: 100%; height: 100%; background-color: black; background-image: ; background-x: center; background-y: center;)
- assert_equal expected, sanitize_css(raw)
- end
-
- def test_should_sanitize_with_trailing_space
- raw = "display:block; "
- expected = "display: block;"
- assert_equal expected, sanitize_css(raw)
- end
-
- def test_should_sanitize_xul_style_attributes
- raw = %(-moz-binding:url('http://ha.ckers.org/xssmoz.xml#xss'))
- assert_equal '', sanitize_css(raw)
- end
-
- def test_should_sanitize_invalid_tag_names
- assert_sanitized(%(a b c<script/XSS src="http://ha.ckers.org/xss.js"></script>d e f), "a b cd e f")
- end
-
- def test_should_sanitize_non_alpha_and_non_digit_characters_in_tags
- assert_sanitized('<a onclick!#$%&()*~+-_.,:;?@[/|\]^`=alert("XSS")>foo</a>', "<a>foo</a>")
- end
-
- def test_should_sanitize_invalid_tag_names_in_single_tags
- assert_sanitized('<img/src="http://ha.ckers.org/xss.js"/>', "<img />")
- end
-
- def test_should_sanitize_img_dynsrc_lowsrc
- assert_sanitized(%(<img lowsrc="javascript:alert('XSS')" />), "<img />")
- end
-
- def test_should_sanitize_div_background_image_unicode_encoded
- raw = %(background-image:\0075\0072\006C\0028'\006a\0061\0076\0061\0073\0063\0072\0069\0070\0074\003a\0061\006c\0065\0072\0074\0028.1027\0058.1053\0053\0027\0029'\0029)
- assert_equal '', sanitize_css(raw)
- end
-
- def test_should_sanitize_div_style_expression
- raw = %(width: expression(alert('XSS'));)
- assert_equal '', sanitize_css(raw)
- end
-
- def test_should_sanitize_across_newlines
- raw = %(\nwidth:\nexpression(alert('XSS'));\n)
- assert_equal '', sanitize_css(raw)
- end
-
- def test_should_sanitize_img_vbscript
- assert_sanitized %(<img src='vbscript:msgbox("XSS")' />), '<img />'
- end
-
- def test_should_sanitize_cdata_section
- assert_sanitized "<![CDATA[<span>section</span>]]>", "&lt;![CDATA[&lt;span>section&lt;/span>]]>"
- end
-
- def test_should_sanitize_unterminated_cdata_section
- assert_sanitized "<![CDATA[<span>neverending...", "&lt;![CDATA[&lt;span>neverending...]]>"
- end
-
- def test_should_not_mangle_urls_with_ampersand
- assert_sanitized %{<a href=\"http://www.domain.com?var1=1&amp;var2=2\">my link</a>}
- end
-
- def test_should_sanitize_neverending_attribute
- assert_sanitized "<span class=\"\\", "<span class=\"\\\">"
- end
-
- def test_x03a
- assert_sanitized %(<a href="javascript&#x3a;alert('XSS');">), "<a>"
- assert_sanitized %(<a href="javascript&#x003a;alert('XSS');">), "<a>"
- assert_sanitized %(<a href="http&#x3a;//legit">), %(<a href="http://legit">)
- assert_sanitized %(<a href="javascript&#x3A;alert('XSS');">), "<a>"
- assert_sanitized %(<a href="javascript&#x003A;alert('XSS');">), "<a>"
- assert_sanitized %(<a href="http&#x3A;//legit">), %(<a href="http://legit">)
- end
-
-protected
- def assert_sanitized(input, expected = nil)
- @sanitizer ||= HTML::WhiteListSanitizer.new
- if input
- assert_dom_equal expected || input, @sanitizer.sanitize(input)
- else
- assert_nil @sanitizer.sanitize(input)
- end
- end
-
- def sanitize_css(input)
- (@sanitizer ||= HTML::WhiteListSanitizer.new).sanitize_css(input)
- end
-end
diff --git a/actionview/test/template/html-scanner/tag_node_test.rb b/actionview/test/template/html-scanner/tag_node_test.rb
deleted file mode 100644
index a29d2d43d7..0000000000
--- a/actionview/test/template/html-scanner/tag_node_test.rb
+++ /dev/null
@@ -1,243 +0,0 @@
-require 'abstract_unit'
-
-class TagNodeTest < ActiveSupport::TestCase
- def test_open_without_attributes
- node = tag("<tag>")
- assert_equal "tag", node.name
- assert_equal Hash.new, node.attributes
- assert_nil node.closing
- end
-
- def test_open_with_attributes
- node = tag("<TAG1 foo=hey_ho x:bar=\"blah blah\" BAZ='blah blah blah' >")
- assert_equal "tag1", node.name
- assert_equal "hey_ho", node["foo"]
- assert_equal "blah blah", node["x:bar"]
- assert_equal "blah blah blah", node["baz"]
- end
-
- def test_self_closing_without_attributes
- node = tag("<tag/>")
- assert_equal "tag", node.name
- assert_equal Hash.new, node.attributes
- assert_equal :self, node.closing
- end
-
- def test_self_closing_with_attributes
- node = tag("<tag a=b/>")
- assert_equal "tag", node.name
- assert_equal( { "a" => "b" }, node.attributes )
- assert_equal :self, node.closing
- end
-
- def test_closing_without_attributes
- node = tag("</tag>")
- assert_equal "tag", node.name
- assert_nil node.attributes
- assert_equal :close, node.closing
- end
-
- def test_bracket_op_when_no_attributes
- node = tag("</tag>")
- assert_nil node["foo"]
- end
-
- def test_bracket_op_when_attributes
- node = tag("<tag a=b/>")
- assert_equal "b", node["a"]
- end
-
- def test_attributes_with_escaped_quotes
- node = tag("<tag a='b\\'c' b=\"bob \\\"float\\\"\">")
- assert_equal "b\\'c", node["a"]
- assert_equal "bob \\\"float\\\"", node["b"]
- end
-
- def test_to_s
- node = tag("<a b=c d='f' g=\"h 'i'\" />")
- node = node.to_s
- assert node.include?('a')
- assert node.include?('b="c"')
- assert node.include?('d="f"')
- assert node.include?('g="h')
- assert node.include?('i')
- end
-
- def test_tag
- assert tag("<tag>").tag?
- end
-
- def test_match_tag_as_string
- assert tag("<tag>").match(:tag => "tag")
- assert !tag("<tag>").match(:tag => "b")
- end
-
- def test_match_tag_as_regexp
- assert tag("<tag>").match(:tag => /t.g/)
- assert !tag("<tag>").match(:tag => /t[bqs]g/)
- end
-
- def test_match_attributes_as_string
- t = tag("<tag a=something b=else />")
- assert t.match(:attributes => {"a" => "something"})
- assert t.match(:attributes => {"b" => "else"})
- end
-
- def test_match_attributes_as_regexp
- t = tag("<tag a=something b=else />")
- assert t.match(:attributes => {"a" => /^something$/})
- assert t.match(:attributes => {"b" => /e.*e/})
- assert t.match(:attributes => {"a" => /me..i/, "b" => /.ls.$/})
- end
-
- def test_match_attributes_as_number
- t = tag("<tag a=15 b=3.1415 />")
- assert t.match(:attributes => {"a" => 15})
- assert t.match(:attributes => {"b" => 3.1415})
- assert t.match(:attributes => {"a" => 15, "b" => 3.1415})
- end
-
- def test_match_attributes_exist
- t = tag("<tag a=15 b=3.1415 />")
- assert t.match(:attributes => {"a" => true})
- assert t.match(:attributes => {"b" => true})
- assert t.match(:attributes => {"a" => true, "b" => true})
- end
-
- def test_match_attributes_not_exist
- t = tag("<tag a=15 b=3.1415 />")
- assert t.match(:attributes => {"c" => false})
- assert t.match(:attributes => {"c" => nil})
- assert t.match(:attributes => {"a" => true, "c" => false})
- end
-
- def test_match_parent_success
- t = tag("<tag a=15 b='hello'>", tag("<foo k='value'>"))
- assert t.match(:parent => {:tag => "foo", :attributes => {"k" => /v.l/, "j" => false}})
- end
-
- def test_match_parent_fail
- t = tag("<tag a=15 b='hello'>", tag("<foo k='value'>"))
- assert !t.match(:parent => {:tag => /kafka/})
- end
-
- def test_match_child_success
- t = tag("<tag x:k='something'>")
- tag("<child v=john a=kelly>", t)
- tag("<sib m=vaughn v=james>", t)
- assert t.match(:child => { :tag => "sib", :attributes => {"v" => /j/}})
- assert t.match(:child => { :attributes => {"a" => "kelly"}})
- end
-
- def test_match_child_fail
- t = tag("<tag x:k='something'>")
- tag("<child v=john a=kelly>", t)
- tag("<sib m=vaughn v=james>", t)
- assert !t.match(:child => { :tag => "sib", :attributes => {"v" => /r/}})
- assert !t.match(:child => { :attributes => {"v" => false}})
- end
-
- def test_match_ancestor_success
- t = tag("<tag x:k='something'>", tag("<parent v=john a=kelly>", tag("<grandparent m=vaughn v=james>")))
- assert t.match(:ancestor => {:tag => "parent", :attributes => {"a" => /ll/}})
- assert t.match(:ancestor => {:attributes => {"m" => "vaughn"}})
- end
-
- def test_match_ancestor_fail
- t = tag("<tag x:k='something'>", tag("<parent v=john a=kelly>", tag("<grandparent m=vaughn v=james>")))
- assert !t.match(:ancestor => {:tag => /^parent/, :attributes => {"v" => /m/}})
- assert !t.match(:ancestor => {:attributes => {"v" => false}})
- end
-
- def test_match_descendant_success
- tag("<grandchild m=vaughn v=james>", tag("<child v=john a=kelly>", t = tag("<tag x:k='something'>")))
- assert t.match(:descendant => {:tag => "child", :attributes => {"a" => /ll/}})
- assert t.match(:descendant => {:attributes => {"m" => "vaughn"}})
- end
-
- def test_match_descendant_fail
- tag("<grandchild m=vaughn v=james>", tag("<child v=john a=kelly>", t = tag("<tag x:k='something'>")))
- assert !t.match(:descendant => {:tag => /^child/, :attributes => {"v" => /m/}})
- assert !t.match(:descendant => {:attributes => {"v" => false}})
- end
-
- def test_match_child_count
- t = tag("<tag x:k='something'>")
- tag("hello", t)
- tag("<child v=john a=kelly>", t)
- tag("<sib m=vaughn v=james>", t)
- assert t.match(:children => { :count => 2 })
- assert t.match(:children => { :count => 2..4 })
- assert t.match(:children => { :less_than => 4 })
- assert t.match(:children => { :greater_than => 1 })
- assert !t.match(:children => { :count => 3 })
- end
-
- def test_conditions_as_strings
- t = tag("<tag x:k='something'>")
- assert t.match("tag" => "tag")
- assert t.match("attributes" => { "x:k" => "something" })
- assert !t.match("tag" => "gat")
- assert !t.match("attributes" => { "x:j" => "something" })
- end
-
- def test_attributes_as_symbols
- t = tag("<child v=john a=kelly>")
- assert t.match(:attributes => { :v => /oh/ })
- assert t.match(:attributes => { :a => /ll/ })
- end
-
- def test_match_sibling
- t = tag("<tag x:k='something'>")
- tag("hello", t)
- tag("<span a=b>", t)
- tag("world", t)
- m = tag("<span k=r>", t)
- tag("<span m=l>", t)
-
- assert m.match(:sibling => {:tag => "span", :attributes => {:a => true}})
- assert m.match(:sibling => {:tag => "span", :attributes => {:m => true}})
- assert !m.match(:sibling => {:tag => "span", :attributes => {:k => true}})
- end
-
- def test_match_sibling_before
- t = tag("<tag x:k='something'>")
- tag("hello", t)
- tag("<span a=b>", t)
- tag("world", t)
- m = tag("<span k=r>", t)
- tag("<span m=l>", t)
-
- assert m.match(:before => {:tag => "span", :attributes => {:m => true}})
- assert !m.match(:before => {:tag => "span", :attributes => {:a => true}})
- assert !m.match(:before => {:tag => "span", :attributes => {:k => true}})
- end
-
- def test_match_sibling_after
- t = tag("<tag x:k='something'>")
- tag("hello", t)
- tag("<span a=b>", t)
- tag("world", t)
- m = tag("<span k=r>", t)
- tag("<span m=l>", t)
-
- assert m.match(:after => {:tag => "span", :attributes => {:a => true}})
- assert !m.match(:after => {:tag => "span", :attributes => {:m => true}})
- assert !m.match(:after => {:tag => "span", :attributes => {:k => true}})
- end
-
- def test_tag_to_s
- t = tag("<b x='foo'>")
- tag("hello", t)
- tag("<hr />", t)
- assert_equal %(<b x="foo">hello<hr /></b>), t.to_s
- end
-
- private
-
- def tag(content, parent=nil)
- node = HTML::Node.parse(parent,0,0,content)
- parent.children << node if parent
- node
- end
-end
diff --git a/actionview/test/template/html-scanner/text_node_test.rb b/actionview/test/template/html-scanner/text_node_test.rb
deleted file mode 100644
index cbcb9e78f0..0000000000
--- a/actionview/test/template/html-scanner/text_node_test.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-require 'abstract_unit'
-
-class TextNodeTest < ActiveSupport::TestCase
- def setup
- @node = HTML::Text.new(nil, 0, 0, "hello, howdy, aloha, annyeong")
- end
-
- def test_to_s
- assert_equal "hello, howdy, aloha, annyeong", @node.to_s
- end
-
- def test_find_string
- assert_equal @node, @node.find("hello, howdy, aloha, annyeong")
- assert_equal false, @node.find("bogus")
- end
-
- def test_find_regexp
- assert_equal @node, @node.find(/an+y/)
- assert_nil @node.find(/b/)
- end
-
- def test_find_hash
- assert_equal @node, @node.find(:content => /howdy/)
- assert_nil @node.find(:content => /^howdy$/)
- assert_equal false, @node.find(:content => "howdy")
- end
-
- def test_find_other
- assert_nil @node.find(:hello)
- end
-
- def test_match_string
- assert @node.match("hello, howdy, aloha, annyeong")
- assert_equal false, @node.match("bogus")
- end
-
- def test_match_regexp
- assert_not_nil @node, @node.match(/an+y/)
- assert_nil @node.match(/b/)
- end
-
- def test_match_hash
- assert_not_nil @node, @node.match(:content => "howdy")
- assert_nil @node.match(:content => /^howdy$/)
- end
-
- def test_match_other
- assert_nil @node.match(:hello)
- end
-end
diff --git a/actionview/test/template/html-scanner/tokenizer_test.rb b/actionview/test/template/html-scanner/tokenizer_test.rb
deleted file mode 100644
index 1d59de23b6..0000000000
--- a/actionview/test/template/html-scanner/tokenizer_test.rb
+++ /dev/null
@@ -1,131 +0,0 @@
-require 'abstract_unit'
-
-class TokenizerTest < ActiveSupport::TestCase
-
- def test_blank
- tokenize ""
- assert_end
- end
-
- def test_space
- tokenize " "
- assert_next " "
- assert_end
- end
-
- def test_tag_simple_open
- tokenize "<tag>"
- assert_next "<tag>"
- assert_end
- end
-
- def test_tag_simple_self_closing
- tokenize "<tag />"
- assert_next "<tag />"
- assert_end
- end
-
- def test_tag_simple_closing
- tokenize "</tag>"
- assert_next "</tag>"
- end
-
- def test_tag_with_single_quoted_attribute
- tokenize %{<tag a='hello'>x}
- assert_next %{<tag a='hello'>}
- end
-
- def test_tag_with_single_quoted_attribute_with_escape
- tokenize %{<tag a='hello\\''>x}
- assert_next %{<tag a='hello\\''>}
- end
-
- def test_tag_with_double_quoted_attribute
- tokenize %{<tag a="hello">x}
- assert_next %{<tag a="hello">}
- end
-
- def test_tag_with_double_quoted_attribute_with_escape
- tokenize %{<tag a="hello\\"">x}
- assert_next %{<tag a="hello\\"">}
- end
-
- def test_tag_with_unquoted_attribute
- tokenize %{<tag a=hello>x}
- assert_next %{<tag a=hello>}
- end
-
- def test_tag_with_lt_char_in_attribute
- tokenize %{<tag a="x < y">x}
- assert_next %{<tag a="x < y">}
- end
-
- def test_tag_with_gt_char_in_attribute
- tokenize %{<tag a="x > y">x}
- assert_next %{<tag a="x > y">}
- end
-
- def test_doctype_tag
- tokenize %{<!DOCTYPE "blah" "blah" "blah">\n <html>}
- assert_next %{<!DOCTYPE "blah" "blah" "blah">}
- assert_next %{\n }
- assert_next %{<html>}
- end
-
- def test_cdata_tag
- tokenize %{<![CDATA[<br>]]>}
- assert_next %{<![CDATA[<br>]]>}
- assert_end
- end
-
- def test_unterminated_cdata_tag
- tokenize %{<content:encoded><![CDATA[ neverending...}
- assert_next %{<content:encoded>}
- assert_next %{<![CDATA[ neverending...}
- assert_end
- end
-
- def test_less_than_with_space
- tokenize %{original < hello > world}
- assert_next %{original }
- assert_next %{< hello > world}
- end
-
- def test_less_than_without_matching_greater_than
- tokenize %{hello <span onmouseover="gotcha"\n<b>foo</b>\nbar</span>}
- assert_next %{hello }
- assert_next %{<span onmouseover="gotcha"\n}
- assert_next %{<b>}
- assert_next %{foo}
- assert_next %{</b>}
- assert_next %{\nbar}
- assert_next %{</span>}
- assert_end
- end
-
- def test_unterminated_comment
- tokenize %{hello <!-- neverending...}
- assert_next %{hello }
- assert_next %{<!-- neverending...}
- assert_end
- end
-
- private
-
- def tokenize(text)
- @tokenizer = HTML::Tokenizer.new(text)
- end
-
- def assert_next(expected, message=nil)
- token = @tokenizer.next
- assert_equal expected, token, message
- end
-
- def assert_sequence(*expected)
- assert_next expected.shift until expected.empty?
- end
-
- def assert_end(message=nil)
- assert_nil @tokenizer.next, message
- end
-end
diff --git a/actionview/test/template/javascript_helper_test.rb b/actionview/test/template/javascript_helper_test.rb
index 9ba7f64ad1..9f1535ef53 100644
--- a/actionview/test/template/javascript_helper_test.rb
+++ b/actionview/test/template/javascript_helper_test.rb
@@ -3,14 +3,7 @@ require 'abstract_unit'
class JavaScriptHelperTest < ActionView::TestCase
tests ActionView::Helpers::JavaScriptHelper
- def _evaluate_assigns_and_ivars() end
-
- attr_accessor :formats, :output_buffer
-
- def update_details(details)
- @details = details
- yield if block_given?
- end
+ attr_accessor :output_buffer
setup do
@old_escape_html_entities_in_json = ActiveSupport.escape_html_entities_in_json
diff --git a/actionview/test/template/lookup_context_test.rb b/actionview/test/template/lookup_context_test.rb
index 4f7823045e..2e3a3f9bae 100644
--- a/actionview/test/template/lookup_context_test.rb
+++ b/actionview/test/template/lookup_context_test.rb
@@ -27,7 +27,7 @@ class LookupContextTest < ActiveSupport::TestCase
end
test "normalizes details on initialization" do
- assert_equal Mime::SET, @lookup_context.formats
+ assert_equal Mime::SET.to_a, @lookup_context.formats
assert_equal :en, @lookup_context.locale
end
@@ -48,7 +48,7 @@ class LookupContextTest < ActiveSupport::TestCase
test "handles */* formats" do
@lookup_context.formats = ["*/*"]
- assert_equal Mime::SET, @lookup_context.formats
+ assert_equal Mime::SET.to_a, @lookup_context.formats
end
test "handles explicitly defined */* formats fallback to :js" do
@@ -108,10 +108,11 @@ class LookupContextTest < ActiveSupport::TestCase
end
test "found templates respects given formats if one cannot be found from template or handler" do
- ActionView::Template::Handlers::Builder.expects(:default_format).returns(nil)
- @lookup_context.formats = [:text]
- template = @lookup_context.find("hello", %w(test))
- assert_equal [:text], template.formats
+ assert_called(ActionView::Template::Handlers::Builder, :default_format, returns: nil) do
+ @lookup_context.formats = [:text]
+ template = @lookup_context.find("hello", %w(test))
+ assert_equal [:text], template.formats
+ end
end
test "adds fallbacks to view paths when required" do
@@ -210,45 +211,50 @@ end
class LookupContextWithFalseCaching < ActiveSupport::TestCase
def setup
@resolver = ActionView::FixtureResolver.new("test/_foo.erb" => ["Foo", Time.utc(2000)])
- ActionView::Resolver.stubs(:caching?).returns(false)
@lookup_context = ActionView::LookupContext.new(@resolver, {})
end
test "templates are always found in the resolver but timestamp is checked before being compiled" do
- template = @lookup_context.find("foo", %w(test), true)
- assert_equal "Foo", template.source
-
- # Now we are going to change the template, but it won't change the returned template
- # since the timestamp is the same.
- @resolver.hash["test/_foo.erb"][0] = "Bar"
- template = @lookup_context.find("foo", %w(test), true)
- assert_equal "Foo", template.source
-
- # Now update the timestamp.
- @resolver.hash["test/_foo.erb"][1] = Time.now.utc
- template = @lookup_context.find("foo", %w(test), true)
- assert_equal "Bar", template.source
+ ActionView::Resolver.stub(:caching?, false) do
+ template = @lookup_context.find("foo", %w(test), true)
+ assert_equal "Foo", template.source
+
+ # Now we are going to change the template, but it won't change the returned template
+ # since the timestamp is the same.
+ @resolver.hash["test/_foo.erb"][0] = "Bar"
+ template = @lookup_context.find("foo", %w(test), true)
+ assert_equal "Foo", template.source
+
+ # Now update the timestamp.
+ @resolver.hash["test/_foo.erb"][1] = Time.now.utc
+ template = @lookup_context.find("foo", %w(test), true)
+ assert_equal "Bar", template.source
+ end
end
test "if no template was found in the second lookup, with no cache, raise error" do
- template = @lookup_context.find("foo", %w(test), true)
- assert_equal "Foo", template.source
+ ActionView::Resolver.stub(:caching?, false) do
+ template = @lookup_context.find("foo", %w(test), true)
+ assert_equal "Foo", template.source
- @resolver.hash.clear
- assert_raise ActionView::MissingTemplate do
- @lookup_context.find("foo", %w(test), true)
+ @resolver.hash.clear
+ assert_raise ActionView::MissingTemplate do
+ @lookup_context.find("foo", %w(test), true)
+ end
end
end
test "if no template was cached in the first lookup, retrieval should work in the second call" do
- @resolver.hash.clear
- assert_raise ActionView::MissingTemplate do
- @lookup_context.find("foo", %w(test), true)
- end
+ ActionView::Resolver.stub(:caching?, false) do
+ @resolver.hash.clear
+ assert_raise ActionView::MissingTemplate do
+ @lookup_context.find("foo", %w(test), true)
+ end
- @resolver.hash["test/_foo.erb"] = ["Foo", Time.utc(2000)]
- template = @lookup_context.find("foo", %w(test), true)
- assert_equal "Foo", template.source
+ @resolver.hash["test/_foo.erb"] = ["Foo", Time.utc(2000)]
+ template = @lookup_context.find("foo", %w(test), true)
+ assert_equal "Foo", template.source
+ end
end
end
diff --git a/actionview/test/template/number_helper_test.rb b/actionview/test/template/number_helper_test.rb
index b59883b760..ace3e950b8 100644
--- a/actionview/test/template/number_helper_test.rb
+++ b/actionview/test/template/number_helper_test.rb
@@ -21,6 +21,7 @@ class NumberHelperTest < ActionView::TestCase
assert_equal "&lt;b&gt;1,234,567,890.50&lt;/b&gt; $", number_to_currency("1234567890.50", format: "<b>%n</b> %u")
assert_equal "&lt;b&gt;1,234,567,890.50&lt;/b&gt; $", number_to_currency("-1234567890.50", negative_format: "<b>%n</b> %u")
assert_equal "&lt;b&gt;1,234,567,890.50&lt;/b&gt; $", number_to_currency("-1234567890.50", 'negative_format' => "<b>%n</b> %u")
+ assert_equal '₹ 12,30,000.00', number_to_currency(1230000, delimiter_pattern: /(\d+?)(?=(\d\d)+(\d)(?!\d))/, unit: '₹', format: "%u %n")
end
def test_number_to_percentage
@@ -35,6 +36,10 @@ class NumberHelperTest < ActionView::TestCase
assert_equal "98a%", number_to_percentage("98a")
assert_equal "NaN%", number_to_percentage(Float::NAN)
assert_equal "Inf%", number_to_percentage(Float::INFINITY)
+ assert_equal "NaN%", number_to_percentage(Float::NAN, precision: 0)
+ assert_equal "Inf%", number_to_percentage(Float::INFINITY, precision: 0)
+ assert_equal "NaN%", number_to_percentage(Float::NAN, precision: 1)
+ assert_equal "Inf%", number_to_percentage(Float::INFINITY, precision: 1)
end
def test_number_with_delimiter
diff --git a/actionview/test/template/record_identifier_test.rb b/actionview/test/template/record_identifier_test.rb
index 22038110a5..04898c0b0e 100644
--- a/actionview/test/template/record_identifier_test.rb
+++ b/actionview/test/template/record_identifier_test.rb
@@ -9,7 +9,6 @@ class RecordIdentifierTest < ActiveSupport::TestCase
@record = @klass.new
@singular = 'comment'
@plural = 'comments'
- @uncountable = Sheep
end
def test_dom_id_with_new_record
@@ -47,3 +46,46 @@ class RecordIdentifierTest < ActiveSupport::TestCase
assert_equal @singular, ActionView::RecordIdentifier.dom_class(@record)
end
end
+
+class RecordIdentifierWithoutActiveModelTest < ActiveSupport::TestCase
+ include ActionView::RecordIdentifier
+
+ def setup
+ @record = Plane.new
+ end
+
+ def test_dom_id_with_new_record
+ assert_equal "new_airplane", dom_id(@record)
+ end
+
+ def test_dom_id_with_new_record_and_prefix
+ assert_equal "custom_prefix_airplane", dom_id(@record, :custom_prefix)
+ end
+
+ def test_dom_id_with_saved_record
+ @record.save
+ assert_equal "airplane_1", dom_id(@record)
+ end
+
+ def test_dom_id_with_prefix
+ @record.save
+ assert_equal "edit_airplane_1", dom_id(@record, :edit)
+ end
+
+ def test_dom_class
+ assert_equal 'airplane', dom_class(@record)
+ end
+
+ def test_dom_class_with_prefix
+ assert_equal "custom_prefix_airplane", dom_class(@record, :custom_prefix)
+ end
+
+ def test_dom_id_as_singleton_method
+ @record.save
+ assert_equal "airplane_1", ActionView::RecordIdentifier.dom_id(@record)
+ end
+
+ def test_dom_class_as_singleton_method
+ assert_equal 'airplane', ActionView::RecordIdentifier.dom_class(@record)
+ end
+end
diff --git a/actionview/test/template/record_tag_helper_test.rb b/actionview/test/template/record_tag_helper_test.rb
index ab84bccb56..bfc5d04bed 100644
--- a/actionview/test/template/record_tag_helper_test.rb
+++ b/actionview/test/template/record_tag_helper_test.rb
@@ -2,7 +2,7 @@ require 'abstract_unit'
class RecordTagPost
extend ActiveModel::Naming
- include ActiveModel::Conversion
+
attr_accessor :id, :body
def initialize
@@ -14,7 +14,6 @@ class RecordTagPost
end
class RecordTagHelperTest < ActionView::TestCase
- include RenderERBUtils
tests ActionView::Helpers::RecordTagHelper
@@ -24,94 +23,10 @@ class RecordTagHelperTest < ActionView::TestCase
end
def test_content_tag_for
- expected = %(<li class="record_tag_post" id="record_tag_post_45"></li>)
- actual = content_tag_for(:li, @post)
- assert_dom_equal expected, actual
- end
-
- def test_content_tag_for_prefix
- expected = %(<ul class="archived_record_tag_post" id="archived_record_tag_post_45"></ul>)
- actual = content_tag_for(:ul, @post, :archived)
- assert_dom_equal expected, actual
- end
-
- def test_content_tag_for_with_extra_html_options
- expected = %(<tr class="record_tag_post special" id="record_tag_post_45" style='background-color: #f0f0f0'></tr>)
- actual = content_tag_for(:tr, @post, class: "special", style: "background-color: #f0f0f0")
- assert_dom_equal expected, actual
- end
-
- def test_content_tag_for_with_array_css_class
- expected = %(<tr class="record_tag_post special odd" id="record_tag_post_45"></tr>)
- actual = content_tag_for(:tr, @post, class: ["special", "odd"])
- assert_dom_equal expected, actual
- end
-
- def test_content_tag_for_with_prefix_and_extra_html_options
- expected = %(<tr class="archived_record_tag_post special" id="archived_record_tag_post_45" style='background-color: #f0f0f0'></tr>)
- actual = content_tag_for(:tr, @post, :archived, class: "special", style: "background-color: #f0f0f0")
- assert_dom_equal expected, actual
- end
-
- def test_block_not_in_erb_multiple_calls
- expected = %(<div class="record_tag_post special" id="record_tag_post_45">What a wonderful world!</div>)
- actual = div_for(@post, class: "special") { @post.body }
- assert_dom_equal expected, actual
- actual = div_for(@post, class: "special") { @post.body }
- assert_dom_equal expected, actual
- end
-
- def test_block_works_with_content_tag_for_in_erb
- expected = %(<tr class="record_tag_post" id="record_tag_post_45">What a wonderful world!</tr>)
- actual = render_erb("<%= content_tag_for(:tr, @post) do %><%= @post.body %><% end %>")
- assert_dom_equal expected, actual
- end
-
- def test_div_for_in_erb
- expected = %(<div class="record_tag_post special" id="record_tag_post_45">What a wonderful world!</div>)
- actual = render_erb("<%= div_for(@post, class: 'special') do %><%= @post.body %><% end %>")
- assert_dom_equal expected, actual
- end
-
- def test_content_tag_for_collection
- post_1 = RecordTagPost.new { |post| post.id = 101; post.body = "Hello!" }
- post_2 = RecordTagPost.new { |post| post.id = 102; post.body = "World!" }
- expected = %(<li class="record_tag_post" id="record_tag_post_101">Hello!</li>\n<li class="record_tag_post" id="record_tag_post_102">World!</li>)
- actual = content_tag_for(:li, [post_1, post_2]) { |post| post.body }
- assert_dom_equal expected, actual
- end
-
- def test_content_tag_for_collection_without_given_block
- post_1 = RecordTagPost.new.tap { |post| post.id = 101; post.body = "Hello!" }
- post_2 = RecordTagPost.new.tap { |post| post.id = 102; post.body = "World!" }
- expected = %(<li class="record_tag_post" id="record_tag_post_101"></li>\n<li class="record_tag_post" id="record_tag_post_102"></li>)
- actual = content_tag_for(:li, [post_1, post_2])
- assert_dom_equal expected, actual
- end
-
- def test_div_for_collection
- post_1 = RecordTagPost.new { |post| post.id = 101; post.body = "Hello!" }
- post_2 = RecordTagPost.new { |post| post.id = 102; post.body = "World!" }
- expected = %(<div class="record_tag_post" id="record_tag_post_101">Hello!</div>\n<div class="record_tag_post" id="record_tag_post_102">World!</div>)
- actual = div_for([post_1, post_2]) { |post| post.body }
- assert_dom_equal expected, actual
- end
-
- def test_content_tag_for_single_record_is_html_safe
- result = div_for(@post, class: "special") { @post.body }
- assert result.html_safe?
- end
-
- def test_content_tag_for_collection_is_html_safe
- post_1 = RecordTagPost.new { |post| post.id = 101; post.body = "Hello!" }
- post_2 = RecordTagPost.new { |post| post.id = 102; post.body = "World!" }
- result = content_tag_for(:li, [post_1, post_2]) { |post| post.body }
- assert result.html_safe?
+ assert_raises(NoMethodError) { content_tag_for(:li, @post) }
end
- def test_content_tag_for_does_not_change_options_hash
- options = { class: "important" }
- content_tag_for(:li, @post, options)
- assert_equal({ class: "important" }, options)
+ def test_div_for
+ assert_raises(NoMethodError) { div_for(@post, class: "special") }
end
end
diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb
index 85817119ba..00fc28a522 100644
--- a/actionview/test/template/render_test.rb
+++ b/actionview/test/template/render_test.rb
@@ -1,4 +1,3 @@
-# encoding: utf-8
require 'abstract_unit'
require 'controller/fake_models'
@@ -8,7 +7,10 @@ end
module RenderTestCases
def setup_view(paths)
@assigns = { :secret => 'in the sauce' }
- @view = ActionView::Base.new(paths, @assigns)
+ @view = Class.new(ActionView::Base) do
+ def view_cache_dependencies; end
+ end.new(paths, @assigns)
+
@controller_view = TestController.new.view_context
# Reload and register danish language for testing
@@ -16,7 +18,7 @@ module RenderTestCases
I18n.backend.store_translations 'pt-BR', {}
# Ensure original are still the same since we are reindexing view paths
- assert_equal ORIGINAL_LOCALES, I18n.available_locales.map {|l| l.to_s }.sort
+ assert_equal ORIGINAL_LOCALES, I18n.available_locales.map(&:to_s).sort
end
def test_render_without_options
@@ -62,9 +64,10 @@ module RenderTestCases
def test_render_template_with_a_missing_partial_of_another_format
@view.lookup_context.formats = [:html]
- assert_raise ActionView::Template::Error, "Missing partial /_missing with {:locale=>[:en], :formats=>[:json], :handlers=>[:erb, :builder]}" do
+ e = assert_raise ActionView::Template::Error do
@view.render(:template => "with_format", :formats => [:json])
end
+ assert_includes(e.message, "Missing partial /_missing with {:locale=>[:en], :formats=>[:json], :variants=>[], :handlers=>[:raw, :erb, :builder, :ruby]}.")
end
def test_render_file_with_locale
@@ -172,18 +175,12 @@ module RenderTestCases
assert_equal "only partial", @view.render("test/partial_only", :counter_counter => 5)
end
- def test_render_partial_with_invalid_name
- e = assert_raises(ArgumentError) { @view.render(:partial => "test/200") }
- assert_equal "The partial name (test/200) is not a valid Ruby identifier; " +
- "make sure your partial name starts with a lowercase letter or underscore, " +
- "and is followed by any combination of letters, numbers and underscores.", e.message
+ def test_render_partial_with_number
+ assert_nothing_raised { @view.render(:partial => "test/200") }
end
def test_render_partial_with_missing_filename
- e = assert_raises(ArgumentError) { @view.render(:partial => "test/") }
- assert_equal "The partial name (test/) is not a valid Ruby identifier; " +
- "make sure your partial name starts with a lowercase letter or underscore, " +
- "and is followed by any combination of letters, numbers and underscores.", e.message
+ assert_raises(ActionView::MissingTemplate) { @view.render(:partial => "test/") }
end
def test_render_partial_with_incompatible_object
@@ -191,10 +188,25 @@ module RenderTestCases
assert_equal "'#{nil.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.", e.message
end
+ def test_render_partial_starting_with_a_capital
+ assert_nothing_raised { @view.render(:partial => 'test/FooBar') }
+ end
+
def test_render_partial_with_hyphen
- e = assert_raises(ArgumentError) { @view.render(:partial => "test/a-in") }
- assert_equal "The partial name (test/a-in) is not a valid Ruby identifier; " +
- "make sure your partial name starts with a lowercase letter or underscore, " +
+ assert_nothing_raised { @view.render(:partial => "test/a-in") }
+ end
+
+ def test_render_partial_with_invalid_option_as
+ e = assert_raises(ArgumentError) { @view.render(:partial => "test/partial_only", :as => 'a-in') }
+ assert_equal "The value (a-in) of the option `as` is not a valid Ruby identifier; " +
+ "make sure it starts with lowercase letter, " +
+ "and is followed by any combination of letters, numbers and underscores.", e.message
+ end
+
+ def test_render_partial_with_hyphen_and_invalid_option_as
+ e = assert_raises(ArgumentError) { @view.render(:partial => "test/a-in", :as => 'a-in') }
+ assert_equal "The value (a-in) of the option `as` is not a valid Ruby identifier; " +
+ "make sure it starts with lowercase letter, " +
"and is followed by any combination of letters, numbers and underscores.", e.message
end
@@ -268,6 +280,14 @@ module RenderTestCases
assert_nil @view.render(:partial => "test/customer", :collection => nil)
end
+ def test_render_partial_without_object_does_not_put_partial_name_to_local_assigns
+ assert_equal 'false', @view.render(partial: 'test/partial_name_in_local_assigns')
+ end
+
+ def test_render_partial_with_nil_object_puts_partial_name_to_local_assigns
+ assert_equal 'true', @view.render(partial: 'test/partial_name_in_local_assigns', object: nil)
+ end
+
def test_render_partial_with_nil_values_in_collection
assert_equal "Hello: davidHello: Anonymous", @view.render(:partial => "test/customer", :collection => [ Customer.new("david"), nil ])
end
@@ -466,6 +486,11 @@ module RenderTestCases
@view.render(:partial => 'test/partial_with_partial', :layout => 'test/layout_for_partial', :locals => { :name => 'Foo!'})
end
+ def test_render_partial_shortcut_with_block_content
+ assert_equal %(Before (shortcut test)\nBefore\n\n Yielded: arg1/arg2\n\nAfter\nAfter),
+ @view.render(partial: "test/partial_shortcut_with_block_content", layout: "test/layout_for_partial", locals: { name: "shortcut test" })
+ end
+
def test_render_layout_with_a_nested_render_layout_call
assert_equal %(Before (Foo!)\nBefore (Bar!)\npartial html\nAfter\npartial with layout\n\nAfter),
@view.render(:partial => 'test/partial_with_layout', :layout => 'test/layout_for_partial', :locals => { :name => 'Foo!'})
@@ -584,3 +609,57 @@ class LazyViewRenderTest < ActiveSupport::TestCase
silence_warnings { Encoding.default_external = old }
end
end
+
+class CachedCollectionViewRenderTest < CachedViewRenderTest
+ class CachedCustomer < Customer; end
+
+ teardown do
+ ActionView::PartialRenderer.collection_cache.clear
+ end
+
+ test "with custom key" do
+ customer = Customer.new("david")
+ key = cache_key([customer, 'key'], "test/_customer")
+
+ ActionView::PartialRenderer.collection_cache.write(key, 'Hello')
+
+ assert_equal "Hello",
+ @view.render(partial: "test/customer", collection: [customer], cache: ->(item) { [item, 'key'] })
+ end
+
+ test "with caching with custom key and rendering with different key" do
+ customer = Customer.new("david")
+ key = cache_key([customer, 'key'], "test/_customer")
+
+ ActionView::PartialRenderer.collection_cache.write(key, 'Hello')
+
+ assert_equal "Hello: david",
+ @view.render(partial: "test/customer", collection: [customer], cache: ->(item) { [item, 'another_key'] })
+ end
+
+ test "automatic caching with inferred cache name" do
+ customer = CachedCustomer.new("david")
+ key = cache_key(customer, "test/_cached_customer")
+
+ ActionView::PartialRenderer.collection_cache.write(key, 'Cached')
+
+ assert_equal "Cached",
+ @view.render(partial: "test/cached_customer", collection: [customer])
+ end
+
+ test "automatic caching with as name" do
+ customer = CachedCustomer.new("david")
+ key = cache_key(customer, "test/_cached_customer_as")
+
+ ActionView::PartialRenderer.collection_cache.write(key, 'Cached')
+
+ assert_equal "Cached",
+ @view.render(partial: "test/cached_customer_as", collection: [customer], as: :buyer)
+ end
+
+ private
+ def cache_key(names, virtual_path)
+ digest = ActionView::Digestor.digest name: virtual_path, finder: @view.lookup_context, dependencies: []
+ @view.fragment_cache_key([ *Array(names), digest ])
+ end
+end
diff --git a/actionview/test/template/sanitize_helper_test.rb b/actionview/test/template/sanitize_helper_test.rb
index f7c8f36b78..efe846a7eb 100644
--- a/actionview/test/template/sanitize_helper_test.rb
+++ b/actionview/test/template/sanitize_helper_test.rb
@@ -1,19 +1,15 @@
require 'abstract_unit'
-# The exhaustive tests are in test/template/html-scanner/sanitizer_test.rb
-# This tests the that the helpers hook up correctly to the sanitizer classes.
+# The exhaustive tests are in test/controller/html/sanitizer_test.rb.
+# This tests that the helpers hook up correctly to the sanitizer classes.
class SanitizeHelperTest < ActionView::TestCase
tests ActionView::Helpers::SanitizeHelper
def test_strip_links
assert_equal "Dont touch me", strip_links("Dont touch me")
- assert_equal "<a<a", strip_links("<a<a")
assert_equal "on my mind\nall day long", strip_links("<a href='almost'>on my mind</a>\n<A href='almost'>all day long</A>")
- assert_equal "0wn3d", strip_links("<a href='http://www.rubyonrails.com/'><a href='http://www.rubyonrails.com/' onlclick='steal()'>0wn3d</a></a>")
assert_equal "Magic", strip_links("<a href='http://www.rubyonrails.com/'>Mag<a href='http://www.ruby-lang.org/'>ic")
- assert_equal "FrrFox", strip_links("<href onlclick='steal()'>FrrFox</a></href>")
assert_equal "My mind\nall <b>day</b> long", strip_links("<a href='almost'>My mind</a>\n<A href='almost'>all <b>day</b> long</A>")
- assert_equal "all <b>day</b> long", strip_links("<<a>a href='hello'>all <b>day</b> long<</A>/a>")
end
def test_sanitize_form
@@ -22,27 +18,19 @@ class SanitizeHelperTest < ActionView::TestCase
def test_should_sanitize_illegal_style_properties
raw = %(display:block; position:absolute; left:0; top:0; width:100%; height:100%; z-index:1; background-color:black; background-image:url(http://www.ragingplatypus.com/i/cam-full.jpg); background-x:center; background-y:center; background-repeat:repeat;)
- expected = %(display: block; width: 100%; height: 100%; background-color: black; background-image: ; background-x: center; background-y: center;)
+ expected = %(display: block; width: 100%; height: 100%; background-color: black; background-x: center; background-y: center;)
assert_equal expected, sanitize_css(raw)
end
def test_strip_tags
- assert_equal("<<<bad html", strip_tags("<<<bad html"))
- assert_equal("<<", strip_tags("<<<bad html>"))
assert_equal("Dont touch me", strip_tags("Dont touch me"))
assert_equal("This is a test.", strip_tags("<p>This <u>is<u> a <a href='test.html'><strong>test</strong></a>.</p>"))
- assert_equal("Weirdos", strip_tags("Wei<<a>a onclick='alert(document.cookie);'</a>/>rdos"))
- assert_equal("This is a test.", strip_tags("This is a test."))
- assert_equal(
- %{This is a test.\n\n\nIt no longer contains any HTML.\n}, strip_tags(
- %{<title>This is <b>a <a href="" target="_blank">test</a></b>.</title>\n\n<!-- it has a comment -->\n\n<p>It no <b>longer <strong>contains <em>any <strike>HTML</strike></em>.</strong></b></p>\n}))
assert_equal "This has a here.", strip_tags("This has a <!-- comment --> here.")
- [nil, '', ' '].each do |blank|
- stripped = strip_tags(blank)
- assert_equal blank, stripped
- end
assert_equal "", strip_tags("<script>")
- assert_equal "something &lt;img onerror=alert(1337)", ERB::Util.html_escape(strip_tags("something <img onerror=alert(1337)"))
+ end
+
+ def test_strip_tags_will_not_encode_special_characters
+ assert_equal "test\r\n\r\ntest", strip_tags("test\r\n\r\ntest")
end
def test_sanitize_is_marked_safe
diff --git a/actionview/test/template/streaming_render_test.rb b/actionview/test/template/streaming_render_test.rb
index 8a24d78e74..d06ba4ceb0 100644
--- a/actionview/test/template/streaming_render_test.rb
+++ b/actionview/test/template/streaming_render_test.rb
@@ -1,4 +1,3 @@
-# encoding: utf-8
require 'abstract_unit'
class TestController < ActionController::Base
@@ -105,4 +104,8 @@ class FiberedTest < ActiveSupport::TestCase
buffered_render(:template => "test/nested_streaming", :layout => "layouts/streaming")
end
+ def test_render_with_streaming_and_capture
+ assert_equal "Yes, \n this works\n like a charm.",
+ buffered_render(template: "test/streaming", layout: "layouts/streaming_with_capture")
+ end
end
diff --git a/actionview/test/template/tag_helper_test.rb b/actionview/test/template/tag_helper_test.rb
index 0ea669b3d0..d037447567 100644
--- a/actionview/test/template/tag_helper_test.rb
+++ b/actionview/test/template/tag_helper_test.rb
@@ -50,6 +50,11 @@ class TagHelperTest < ActionView::TestCase
assert_dom_equal "<div>Hello world!</div>", buffer
end
+ def test_content_tag_with_block_in_erb_containing_non_displayed_erb
+ buffer = render_erb("<%= content_tag(:p) do %><% 1 %><% end %>")
+ assert_dom_equal "<p></p>", buffer
+ end
+
def test_content_tag_with_block_and_options_in_erb
buffer = render_erb("<%= content_tag(:div, :class => 'green') do %>Hello world!<% end %>")
assert_dom_equal %(<div class="green">Hello world!</div>), buffer
@@ -64,6 +69,11 @@ class TagHelperTest < ActionView::TestCase
content_tag("a", "href" => "create") { "Create" }
end
+ def test_content_tag_with_block_and_non_string_outside_out_of_erb
+ assert_equal content_tag("p"),
+ content_tag("p") { 3.times { "do_something" } }
+ end
+
def test_content_tag_nested_in_content_tag_out_of_erb
assert_equal content_tag("p", content_tag("b", "Hello")),
content_tag("p") { content_tag("b", "Hello") },
@@ -156,4 +166,11 @@ class TagHelperTest < ActionView::TestCase
tag('a', { data => { a_float: 3.14, a_big_decimal: BigDecimal.new("-123.456"), a_number: 1, string: 'hello', symbol: :foo, array: [1, 2, 3], hash: { key: 'value'}, string_with_quotes: 'double"quote"party"' } })
}
end
+
+ def test_aria_attributes
+ ['aria', :aria].each { |aria|
+ assert_dom_equal '<a aria-a-float="3.14" aria-a-big-decimal="-123.456" aria-a-number="1" aria-array="[1,2,3]" aria-hash="{&quot;key&quot;:&quot;value&quot;}" aria-string-with-quotes="double&quot;quote&quot;party&quot;" aria-string="hello" aria-symbol="foo" />',
+ tag('a', { aria => { a_float: 3.14, a_big_decimal: BigDecimal.new("-123.456"), a_number: 1, string: 'hello', symbol: :foo, array: [1, 2, 3], hash: { key: 'value'}, string_with_quotes: 'double"quote"party"' } })
+ }
+ end
end
diff --git a/actionview/test/template/template_test.rb b/actionview/test/template/template_test.rb
index c94508d678..921011b073 100644
--- a/actionview/test/template/template_test.rb
+++ b/actionview/test/template/template_test.rb
@@ -118,15 +118,17 @@ class TestERBTemplate < ActiveSupport::TestCase
def test_refresh_with_templates
@template = new_template("Hello", :virtual_path => "test/foo/bar")
@template.locals = [:key]
- @context.lookup_context.expects(:find_template).with("bar", %w(test/foo), false, [:key]).returns("template")
- assert_equal "template", @template.refresh(@context)
+ assert_called_with(@context.lookup_context, :find_template,["bar", %w(test/foo), false, [:key]], returns: "template") do
+ assert_equal "template", @template.refresh(@context)
+ end
end
def test_refresh_with_partials
@template = new_template("Hello", :virtual_path => "test/_foo")
@template.locals = [:key]
- @context.lookup_context.expects(:find_template).with("foo", %w(test), true, [:key]).returns("partial")
- assert_equal "partial", @template.refresh(@context)
+ assert_called_with(@context.lookup_context, :find_template,[ "foo", %w(test), true, [:key]], returns: "partial") do
+ assert_equal "partial", @template.refresh(@context)
+ end
end
def test_refresh_raises_an_error_without_virtual_path
@@ -183,10 +185,43 @@ class TestERBTemplate < ActiveSupport::TestCase
end
def test_error_when_template_isnt_valid_utf8
- assert_raises(ActionView::Template::Error, /\xFC/) do
+ e = assert_raises ActionView::Template::Error do
@template = new_template("hello \xFCmlat", :virtual_path => nil)
render
end
+ assert_match(/\xFC/, e.message)
+ end
+
+ def test_not_eligible_for_collection_caching_without_cache_call
+ [
+ "<%= 'Hello' %>",
+ "<% cache_customer = 42 %>",
+ "<% cache customer.name do %><% end %>",
+ "<% my_cache customer do %><% end %>"
+ ].each do |body|
+ template = new_template(body, virtual_path: "test/foo/_customer")
+ assert_not template.eligible_for_collection_caching?, "Template #{body.inspect} should not be eligible for collection caching"
+ end
+ end
+
+ def test_eligible_for_collection_caching_with_cache_call_or_explicit
+ [
+ "<% cache customer do %><% end %>",
+ "<% cache(customer) do %><% end %>",
+ "<% cache( customer) do %><% end %>",
+ "<% cache( customer ) do %><% end %>",
+ "<%cache customer do %><% end %>",
+ "<% cache customer do %><% end %>",
+ " <% cache customer do %>\n<% end %>\n",
+ "<%# comment %><% cache customer do %><% end %>",
+ "<%# comment %>\n<% cache customer do %><% end %>",
+ "<%# comment\n line 2\n line 3 %>\n<% cache customer do %><% end %>",
+ "<%# comment 1 %>\n<%# comment 2 %>\n<% cache customer do %><% end %>",
+ "<%# comment 1 %>\n<%# Template Collection: customer %>\n<% my_cache customer do %><% end %>"
+ ].each do |body|
+ template = new_template(body, virtual_path: "test/foo/_customer")
+ assert template.eligible_for_collection_caching?, "Template #{body.inspect} should be eligible for collection caching"
+ end
end
def with_external_encoding(encoding)
diff --git a/actionview/test/template/test_case_test.rb b/actionview/test/template/test_case_test.rb
index 4582fa13ee..b057d43ee0 100644
--- a/actionview/test/template/test_case_test.rb
+++ b/actionview/test/template/test_case_test.rb
@@ -20,6 +20,7 @@ module ActionView
class TestCase
helper ASharedTestHelper
+ DeveloperStruct = Struct.new(:name)
module SharedTests
def self.included(test_case)
@@ -50,7 +51,7 @@ module ActionView
end
test "works without testing a helper module" do
- assert_equal 'Eloy', render('developers/developer', :developer => stub(:name => 'Eloy'))
+ assert_equal 'Eloy', render('developers/developer', :developer => DeveloperStruct.new('Eloy'))
end
test "can render a layout with block" do
@@ -69,13 +70,15 @@ module ActionView
end
test "delegates notice to request.flash[:notice]" do
- view.request.flash.expects(:[]).with(:notice)
- view.notice
+ assert_called_with(view.request.flash, :[], [:notice]) do
+ view.notice
+ end
end
test "delegates alert to request.flash[:alert]" do
- view.request.flash.expects(:[]).with(:alert)
- view.alert
+ assert_called_with(view.request.flash, :[], [:alert]) do
+ view.alert
+ end
end
test "uses controller lookup context" do
@@ -119,7 +122,7 @@ module ActionView
test "helper class that is being tested is always included in view instance" do
@controller.controller_path = 'test'
- @customers = [stub(:name => 'Eloy'), stub(:name => 'Manfred')]
+ @customers = [DeveloperStruct.new('Eloy'), DeveloperStruct.new('Manfred')]
assert_match(/Hello: EloyHello: Manfred/, render(:partial => 'test/from_helper'))
end
end
@@ -155,7 +158,7 @@ module ActionView
test "view_assigns excludes internal ivars" do
INTERNAL_IVARS.each do |ivar|
assert defined?(ivar), "expected #{ivar} to be defined"
- assert !view_assigns.keys.include?(ivar.to_s.sub('@', '').to_sym), "expected #{ivar} to be excluded from view_assigns"
+ assert !view_assigns.keys.include?(ivar.to_s.tr('@', '').to_sym), "expected #{ivar} to be excluded from view_assigns"
end
end
end
@@ -209,7 +212,7 @@ module ActionView
end
test "is able to use routes" do
- controller.request.assign_parameters(@routes, 'foo', 'index')
+ controller.request.assign_parameters(@routes, 'foo', 'index', {}, '/foo', [])
assert_equal '/foo', url_for
assert_equal '/bar', url_for(:controller => 'bar')
end
@@ -255,15 +258,15 @@ module ActionView
end
test "is able to render partials with local variables" do
- assert_equal 'Eloy', render('developers/developer', :developer => stub(:name => 'Eloy'))
+ assert_equal 'Eloy', render('developers/developer', :developer => DeveloperStruct.new('Eloy'))
assert_equal 'Eloy', render(:partial => 'developers/developer',
- :locals => { :developer => stub(:name => 'Eloy') })
+ :locals => { :developer => DeveloperStruct.new('Eloy') })
end
test "is able to render partials from templates and also use instance variables" do
@controller.controller_path = "test"
- @customers = [stub(:name => 'Eloy'), stub(:name => 'Manfred')]
+ @customers = [DeveloperStruct.new('Eloy'), DeveloperStruct.new('Manfred')]
assert_match(/Hello: EloyHello: Manfred/, render(:file => 'test/list'))
end
@@ -272,7 +275,7 @@ module ActionView
view
- @customers = [stub(:name => 'Eloy'), stub(:name => 'Manfred')]
+ @customers = [DeveloperStruct.new('Eloy'), DeveloperStruct.new('Manfred')]
assert_match(/Hello: EloyHello: Manfred/, render(:file => 'test/list'))
end
@@ -293,62 +296,16 @@ module ActionView
assert_select 'li', :text => 'foo'
end
end
- end
-
- class RenderTemplateTest < ActionView::TestCase
- test "supports specifying templates with a Regexp" do
- controller.controller_path = "fun"
- render(:template => "fun/games/hello_world")
- assert_template %r{\Afun/games/hello_world\Z}
- end
-
- test "supports specifying partials" do
- controller.controller_path = "test"
- render(:template => "test/calling_partial_with_layout")
- assert_template :partial => "_partial_for_use_in_layout"
- end
-
- test "supports specifying locals (passing)" do
- controller.controller_path = "test"
- render(:template => "test/calling_partial_with_layout")
- assert_template :partial => "_partial_for_use_in_layout", :locals => { :name => "David" }
- end
-
- test "supports specifying locals (failing)" do
- controller.controller_path = "test"
- render(:template => "test/calling_partial_with_layout")
- assert_raise ActiveSupport::TestCase::Assertion, /Somebody else.*David/m do
- assert_template :partial => "_partial_for_use_in_layout", :locals => { :name => "Somebody Else" }
- end
- end
- test 'supports different locals on the same partial' do
- controller.controller_path = "test"
- render(:template => "test/render_two_partials")
- assert_template partial: '_partial', locals: { 'first' => '1' }
- assert_template partial: '_partial', locals: { 'second' => '2' }
- end
+ test "do not memoize the document_root_element in view tests" do
+ concat form_tag('/foo')
- test 'raises descriptive error message when template was not rendered' do
- controller.controller_path = "test"
- render(template: "test/hello_world_with_partial")
- e = assert_raise ActiveSupport::TestCase::Assertion do
- assert_template partial: 'i_was_never_rendered', locals: { 'did_not' => 'happen' }
- end
- assert_match "i_was_never_rendered to be rendered but it was not.", e.message
- assert_match 'Expected ["/test/partial"] to include "i_was_never_rendered"', e.message
- end
+ assert_select 'form'
- test 'specifying locals works when the partial is inside a directory with underline prefix' do
- controller.controller_path = "test"
- render(template: 'test/render_partial_inside_directory')
- assert_template partial: 'test/_directory/_partial_with_locales', locals: { 'name' => 'Jane' }
- end
+ concat content_tag(:b, 'Strong', class: 'foo')
- test 'specifying locals works when the partial is inside a directory without underline prefix' do
- controller.controller_path = "test"
- render(template: 'test/render_partial_inside_directory')
- assert_template partial: 'test/_directory/partial_with_locales', locals: { 'name' => 'Jane' }
+ assert_select 'form'
+ assert_select 'b.foo'
end
end
diff --git a/actionview/test/template/text_helper_test.rb b/actionview/test/template/text_helper_test.rb
index db416a8de4..fae1965ffa 100644
--- a/actionview/test/template/text_helper_test.rb
+++ b/actionview/test/template/text_helper_test.rb
@@ -1,4 +1,3 @@
-# encoding: utf-8
require 'abstract_unit'
class TextHelperTest < ActionView::TestCase
@@ -187,10 +186,16 @@ class TextHelperTest < ActionView::TestCase
"This text is not changed because we supplied an empty phrase",
highlight("This text is not changed because we supplied an empty phrase", nil)
)
+ end
+ def test_highlight_pending
assert_equal ' ', highlight(' ', 'blank text is returned verbatim')
end
+ def test_highlight_should_return_blank_string_for_nil
+ assert_equal '', highlight(nil, 'blank string is returned for nil')
+ end
+
def test_highlight_should_sanitize_input
assert_equal(
"This is a <mark>beautiful</mark> morning",
@@ -361,6 +366,10 @@ class TextHelperTest < ActionView::TestCase
assert_equal options, passed_options
end
+ def test_word_wrap_with_custom_break_sequence
+ assert_equal("1234567890\r\n1234567890\r\n1234567890", word_wrap("1234567890 " * 3, line_width: 2, break_sequence: "\r\n"))
+ end
+
def test_pluralization
assert_equal("1 count", pluralize(1, "count"))
assert_equal("2 counts", pluralize(2, "count"))
@@ -378,6 +387,18 @@ class TextHelperTest < ActionView::TestCase
assert_equal("12 berries", pluralize(12, "berry"))
end
+ def test_pluralization_with_locale
+ ActiveSupport::Inflector.inflections(:de) do |inflect|
+ inflect.plural(/(person)$/i, '\1en')
+ inflect.singular(/(person)en$/i, '\1')
+ end
+
+ assert_equal("2 People", pluralize(2, "Person", locale: :en))
+ assert_equal("2 Personen", pluralize(2, "Person", locale: :de))
+
+ ActiveSupport::Inflector.inflections(:de).clear
+ end
+
def test_cycle_class
value = Cycle.new("one", 2, "3")
assert_equal("one", value.to_s)
diff --git a/actionview/test/template/translation_helper_test.rb b/actionview/test/template/translation_helper_test.rb
index 41f6770f23..631bceadd8 100644
--- a/actionview/test/template/translation_helper_test.rb
+++ b/actionview/test/template/translation_helper_test.rb
@@ -1,7 +1,14 @@
require 'abstract_unit'
+module I18n
+ class CustomExceptionHandler
+ def self.call(exception, locale, key, options)
+ 'from CustomExceptionHandler'
+ end
+ end
+end
+
class TranslationHelperTest < ActiveSupport::TestCase
- include ActionView::Helpers::TagHelper
include ActionView::Helpers::TranslationHelper
attr_reader :request, :view
@@ -34,15 +41,17 @@ class TranslationHelperTest < ActiveSupport::TestCase
I18n.backend.reload!
end
- def test_delegates_to_i18n_setting_the_rescue_format_option_to_html
- I18n.expects(:translate).with(:foo, :locale => 'en', :raise=>true).returns("")
- translate :foo, :locale => 'en'
+ def test_delegates_setting_to_i18n
+ assert_called_with(I18n, :translate, [:foo, :locale => 'en', :raise => true], returns: "") do
+ translate :foo, :locale => 'en'
+ end
end
def test_delegates_localize_to_i18n
@time = Time.utc(2008, 7, 8, 12, 18, 38)
- I18n.expects(:localize).with(@time)
- localize @time
+ assert_called_with(I18n, :localize, [@time]) do
+ localize @time
+ end
end
def test_returns_missing_translation_message_wrapped_into_span
@@ -51,10 +60,18 @@ class TranslationHelperTest < ActiveSupport::TestCase
assert_equal true, translate(:"translations.missing").html_safe?
end
- def test_returns_missing_translation_message_using_nil_as_rescue_format
- expected = 'translation missing: en.translations.missing'
- assert_equal expected, translate(:"translations.missing", :rescue_format => nil)
- assert_equal false, translate(:"translations.missing", :rescue_format => nil).html_safe?
+ def test_returns_missing_translation_message_with_unescaped_interpolation
+ expected = '<span class="translation_missing" title="translation missing: en.translations.missing, name: Kir, year: 2015, vulnerable: &amp;quot; onclick=&amp;quot;alert()&amp;quot;">Missing</span>'
+ assert_equal expected, translate(:"translations.missing", name: "Kir", year: "2015", vulnerable: %{" onclick="alert()"})
+ assert translate(:"translations.missing").html_safe?
+ end
+
+ def test_returns_missing_translation_message_does_filters_out_i18n_options
+ expected = '<span class="translation_missing" title="translation missing: en.translations.missing, year: 2015">Missing</span>'
+ assert_equal expected, translate(:"translations.missing", year: '2015', default: [])
+
+ expected = '<span class="translation_missing" title="translation missing: en.scoped.translations.missing, year: 2015">Missing</span>'
+ assert_equal expected, translate(:"translations.missing", year: '2015', scope: %i(scoped))
end
def test_raises_missing_translation_message_with_raise_config_option
@@ -73,10 +90,20 @@ class TranslationHelperTest < ActiveSupport::TestCase
end
end
- def test_i18n_translate_defaults_to_nil_rescue_format
- expected = 'translation missing: en.translations.missing'
- assert_equal expected, I18n.translate(:"translations.missing")
- assert_equal false, I18n.translate(:"translations.missing").html_safe?
+ def test_uses_custom_exception_handler_when_specified
+ old_exception_handler = I18n.exception_handler
+ I18n.exception_handler = I18n::CustomExceptionHandler
+ assert_equal 'from CustomExceptionHandler', translate(:"translations.missing", raise: false)
+ ensure
+ I18n.exception_handler = old_exception_handler
+ end
+
+ def test_uses_custom_exception_handler_when_specified_for_html
+ old_exception_handler = I18n.exception_handler
+ I18n.exception_handler = I18n::CustomExceptionHandler
+ assert_equal 'from CustomExceptionHandler', translate(:"translations.missing_html", raise: false)
+ ensure
+ I18n.exception_handler = old_exception_handler
end
def test_translation_returning_an_array
@@ -114,8 +141,9 @@ class TranslationHelperTest < ActiveSupport::TestCase
end
def test_translate_escapes_interpolations_in_translations_with_a_html_suffix
+ word_struct = Struct.new(:to_s)
assert_equal '<a>Hello &lt;World&gt;</a>', translate(:'translations.interpolated_html', :word => '<World>')
- assert_equal '<a>Hello &lt;World&gt;</a>', translate(:'translations.interpolated_html', :word => stub(:to_s => "<World>"))
+ assert_equal '<a>Hello &lt;World&gt;</a>', translate(:'translations.interpolated_html', :word => word_struct.new("<World>"))
end
def test_translate_with_html_count
@@ -134,6 +162,19 @@ class TranslationHelperTest < ActiveSupport::TestCase
assert_equal true, translation.html_safe?
end
+ def test_translate_with_missing_default
+ translation = translate(:'translations.missing', :default => :'translations.missing_html')
+ expected = '<span class="translation_missing" title="translation missing: en.translations.missing_html">Missing Html</span>'
+ assert_equal expected, translation
+ assert_equal true, translation.html_safe?
+ end
+
+ def test_translate_with_missing_default_and_raise_option
+ assert_raise(I18n::MissingTranslationData) do
+ translate(:'translations.missing', :default => :'translations.missing_html', :raise => true)
+ end
+ end
+
def test_translate_with_two_defaults_named_html
translation = translate(:'translations.missing', :default => [:'translations.missing_html', :'translations.hello_html'])
assert_equal '<a>Hello World</a>', translation
@@ -146,16 +187,37 @@ class TranslationHelperTest < ActiveSupport::TestCase
assert_equal true, translation.html_safe?
end
+ def test_translate_with_last_default_not_named_html
+ translation = translate(:'translations.missing', :default => [:'translations.missing_html', :'translations.foo'])
+ assert_equal 'Foo', translation
+ assert_equal false, translation.html_safe?
+ end
+
def test_translate_with_string_default
translation = translate(:'translations.missing', default: 'A Generic String')
assert_equal 'A Generic String', translation
end
+ def test_translate_with_object_default
+ translation = translate(:'translations.missing', default: 123)
+ assert_equal 123, translation
+ end
+
def test_translate_with_array_of_string_defaults
translation = translate(:'translations.missing', default: ['A Generic String', 'Second generic string'])
assert_equal 'A Generic String', translation
end
+ def test_translate_with_array_of_defaults_with_nil
+ translation = translate(:'translations.missing', default: [:'also_missing', nil, 'A Generic String'])
+ assert_equal 'A Generic String', translation
+ end
+
+ def test_translate_with_array_of_array_default
+ translation = translate(:'translations.missing', default: [[]])
+ assert_equal [], translation
+ end
+
def test_translate_does_not_change_options
options = {}
translate(:'translations.missing', options)
diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb
index 35279a4558..50b7865f88 100644
--- a/actionview/test/template/url_helper_test.rb
+++ b/actionview/test/template/url_helper_test.rb
@@ -1,6 +1,4 @@
-# encoding: utf-8
require 'abstract_unit'
-require 'minitest/mock'
class UrlHelperTest < ActiveSupport::TestCase
@@ -25,7 +23,7 @@ class UrlHelperTest < ActiveSupport::TestCase
include routes.url_helpers
include ActionView::Helpers::JavaScriptHelper
- include ActionDispatch::Assertions::DomAssertions
+ include Rails::Dom::Testing::Assertions::DomAssertions
include ActionView::Context
include RenderERBUtils
@@ -380,6 +378,11 @@ class UrlHelperTest < ActiveSupport::TestCase
assert_dom_equal %{<a href="/">Listing</a>}, link_to_if(true, "Listing", url_hash)
end
+ def test_link_to_if_with_block
+ assert_equal "Fallback", link_to_if(false, "Showing", url_hash) { "Fallback" }
+ assert_dom_equal %{<a href="/">Listing</a>}, link_to_if(true, "Listing", url_hash) { "Fallback" }
+ end
+
def request_for_url(url, opts = {})
env = Rack::MockRequest.env_for("http://www.example.com#{url}", opts)
ActionDispatch::Request.new(env)
@@ -480,6 +483,11 @@ class UrlHelperTest < ActiveSupport::TestCase
link_to_unless_current("Listing", "http://www.example.com/")
end
+ def test_link_to_unless_with_block
+ assert_dom_equal %{<a href="/">Showing</a>}, link_to_unless(false, "Showing", url_hash) { "Fallback" }
+ assert_equal "Fallback", link_to_unless(true, "Listing", url_hash) { "Fallback" }
+ end
+
def test_mail_to
assert_dom_equal %{<a href="mailto:david@loudthinking.com">david@loudthinking.com</a>}, mail_to("david@loudthinking.com")
assert_dom_equal %{<a href="mailto:david@loudthinking.com">David Heinemeier Hansson</a>}, mail_to("david@loudthinking.com", "David Heinemeier Hansson")
@@ -491,10 +499,22 @@ class UrlHelperTest < ActiveSupport::TestCase
mail_to("david@loudthinking.com", "David Heinemeier Hansson", class: "admin")
end
+ def test_mail_to_with_special_characters
+ assert_dom_equal(
+ %{<a href="mailto:%23%21%24%25%26%27%2A%2B-%2F%3D%3F%5E_%60%7B%7D%7C%7E@example.org">#!$%&amp;&#39;*+-/=?^_`{}|~@example.org</a>},
+ mail_to("#!$%&'*+-/=?^_`{}|~@example.org")
+ )
+ end
+
def test_mail_with_options
assert_dom_equal(
- %{<a href="mailto:me@example.com?cc=ccaddress%40example.com&amp;bcc=bccaddress%40example.com&amp;body=This%20is%20the%20body%20of%20the%20message.&amp;subject=This%20is%20an%20example%20email">My email</a>},
- mail_to("me@example.com", "My email", cc: "ccaddress@example.com", bcc: "bccaddress@example.com", subject: "This is an example email", body: "This is the body of the message.")
+ %{<a href="mailto:me@example.com?cc=ccaddress%40example.com&amp;bcc=bccaddress%40example.com&amp;body=This%20is%20the%20body%20of%20the%20message.&amp;subject=This%20is%20an%20example%20email&amp;reply-to=foo%40bar.com">My email</a>},
+ mail_to("me@example.com", "My email", cc: "ccaddress@example.com", bcc: "bccaddress@example.com", subject: "This is an example email", body: "This is the body of the message.", reply_to: "foo@bar.com")
+ )
+
+ assert_dom_equal(
+ %{<a href="mailto:me@example.com?body=This%20is%20the%20body%20of%20the%20message.&amp;subject=This%20is%20an%20example%20email">My email</a>},
+ mail_to("me@example.com", "My email", cc: '', bcc: '', subject: "This is an example email", body: "This is the body of the message.")
)
end
@@ -624,13 +644,13 @@ class UrlHelperControllerTest < ActionController::TestCase
end
def test_named_route_url_shows_host_and_path
- get :show_named_route, kind: 'url'
+ get :show_named_route, params: { kind: 'url' }
assert_equal 'http://test.host/url_helper_controller_test/url_helper/show_named_route',
@response.body
end
def test_named_route_path_shows_only_path
- get :show_named_route, kind: 'path'
+ get :show_named_route, params: { kind: 'path' }
assert_equal '/url_helper_controller_test/url_helper/show_named_route', @response.body
end
@@ -646,7 +666,7 @@ class UrlHelperControllerTest < ActionController::TestCase
end
end
- get :show_named_route, kind: 'url'
+ get :show_named_route, params: { kind: 'url' }
assert_equal 'http://testtwo.host/url_helper_controller_test/url_helper/show_named_route', @response.body
end
@@ -661,11 +681,11 @@ class UrlHelperControllerTest < ActionController::TestCase
end
def test_recall_params_should_normalize_id
- get :show, id: '123'
+ get :show, params: { id: '123' }
assert_equal 302, @response.status
assert_equal 'http://test.host/url_helper_controller_test/url_helper/profile/123', @response.location
- get :show, name: '123'
+ get :show, params: { name: '123' }
assert_equal 'ok', @response.body
end
@@ -704,7 +724,7 @@ class LinkToUnlessCurrentWithControllerTest < ActionController::TestCase
end
def test_link_to_unless_current_shows_link
- get :show, id: 1
+ get :show, params: { id: 1 }
assert_equal %{<a href="/tasks">tasks</a>\n} +
%{<a href="#{@request.protocol}#{@request.host_with_port}/tasks">tasks</a>},
@response.body
@@ -765,6 +785,13 @@ class SessionsController < ActionController::Base
@session = Session.new(params[:id])
render inline: "<%= url_for([@workshop, @session]) %>\n<%= link_to('Session', [@workshop, @session]) %>"
end
+
+ def edit
+ @workshop = Workshop.new(params[:workshop_id])
+ @session = Session.new(params[:id])
+ @url = [@workshop, @session, format: params[:format]]
+ render inline: "<%= url_for(@url) %>\n<%= link_to('Session', @url) %>"
+ end
end
class PolymorphicControllerTest < ActionController::TestCase
@@ -778,21 +805,28 @@ class PolymorphicControllerTest < ActionController::TestCase
def test_existing_resource
@controller = WorkshopsController.new
- get :show, id: 1
+ get :show, params: { id: 1 }
assert_equal %{/workshops/1\n<a href="/workshops/1">Workshop</a>}, @response.body
end
def test_new_nested_resource
@controller = SessionsController.new
- get :index, workshop_id: 1
+ get :index, params: { workshop_id: 1 }
assert_equal %{/workshops/1/sessions\n<a href="/workshops/1/sessions">Session</a>}, @response.body
end
def test_existing_nested_resource
@controller = SessionsController.new
- get :show, workshop_id: 1, id: 1
+ get :show, params: { workshop_id: 1, id: 1 }
assert_equal %{/workshops/1/sessions/1\n<a href="/workshops/1/sessions/1">Session</a>}, @response.body
end
+
+ def test_existing_nested_resource_with_params
+ @controller = SessionsController.new
+
+ get :edit, params: { workshop_id: 1, id: 1, format: "json" }
+ assert_equal %{/workshops/1/sessions/1.json\n<a href="/workshops/1/sessions/1.json">Session</a>}, @response.body
+ end
end