diff options
Diffstat (limited to 'actionview')
329 files changed, 32228 insertions, 0 deletions
diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md new file mode 100644 index 0000000000..135a96621d --- /dev/null +++ b/actionview/CHANGELOG.md @@ -0,0 +1,42 @@ +* Use a case insensitive URI Regexp for #asset_path. + + This fix a problem where the same asset path using different case are generating + different URIs. + + Before: + + image_tag("HTTP://google.com") + # => "<img alt=\"Google\" src=\"/assets/HTTP://google.com\" />" + image_tag("http://google.com") + # => "<img alt=\"Google\" src=\"http://google.com\" />" + + After: + + image_tag("HTTP://google.com") + # => "<img alt=\"Google\" src=\"HTTP://google.com\" />" + image_tag("http://google.com") + # => "<img alt=\"Google\" src=\"http://google.com\" />" + + *David Celis* + +* Element of the `collection_check_boxes` and `collection_radio_buttons` can + optionally contain html attributes as the last element of the array. + + *Vasiliy Ermolovich* + +* Update the HTML `BOOLEAN_ATTRIBUTES` in `ActionView::Helpers::TagHelper` + to conform to the latest HTML 5.1 spec. Add attributes `allowfullscreen`, + `default`, `inert`, `sortable`, `truespeed`, `typemustmatch`. Fix attribute + `seamless` (previously misspelled `seemless`). + + *Alex Peattie* + +* Fix an issue where partials with a number in the filename weren't being digested for cache dependencies. + + *Bryan Ricker* + +* First release, ActionView extracted from ActionPack + + *Piotr Sarnacki*, *Łukasz Strzałkowski* + +Please check [4-0-stable (ActionPack's CHANGELOG)](https://github.com/rails/rails/blob/4-0-stable/actionpack/CHANGELOG.md) for previous changes.
\ No newline at end of file diff --git a/actionview/MIT-LICENSE b/actionview/MIT-LICENSE new file mode 100644 index 0000000000..810daf856c --- /dev/null +++ b/actionview/MIT-LICENSE @@ -0,0 +1,21 @@ +Copyright (c) 2004-2012 David Heinemeier Hansson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/actionview/README.rdoc b/actionview/README.rdoc new file mode 100644 index 0000000000..09bbfdae0b --- /dev/null +++ b/actionview/README.rdoc @@ -0,0 +1,31 @@ += Action View + + + +== Download and installation + +The latest version of Action View can be installed with RubyGems: + + % [sudo] gem install actionview + +Source code can be downloaded as part of the Rails project on GitHub + +* https://github.com/rails/rails/tree/master/actionview + + +== License + +Action View is released under the MIT license: + +* http://www.opensource.org/licenses/MIT + + +== Support + +API documentation is at + +* http://api.rubyonrails.org + +Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here: + +* https://github.com/rails/rails/issues diff --git a/actionview/RUNNING_UNIT_TESTS b/actionview/RUNNING_UNIT_TESTS new file mode 100644 index 0000000000..a0c35e1810 --- /dev/null +++ b/actionview/RUNNING_UNIT_TESTS @@ -0,0 +1,27 @@ +== Running with Rake + +The easiest way to run the unit tests is through Rake. The default task runs +the entire test suite for all classes. For more information, checkout the +full array of rake tasks with "rake -T" + +Rake can be found at http://rake.rubyforge.org + +== Running by hand + +To run a single test suite + + rake test TEST=path/to/test.rb + +which can be further narrowed down to one test: + + rake test TEST=path/to/test.rb TESTOPTS="--name=test_something" + +== Dependency on Active Record and database setup + +Test cases in the test/active_record/ directory depend on having +activerecord and sqlite installed. If Active Record is not in +actionview/../activerecord directory, or the sqlite rubygem is not installed, +these tests are skipped. + +Other tests are runnable from a fresh copy of actionview without any configuration. + diff --git a/actionview/Rakefile b/actionview/Rakefile new file mode 100644 index 0000000000..087b249dfe --- /dev/null +++ b/actionview/Rakefile @@ -0,0 +1,79 @@ +require 'rake/testtask' +require 'rake/packagetask' +require 'rubygems/package_task' + +desc "Default Task" +task :default => :test + +# Run the unit tests + +desc "Run all unit tests" +task :test => [:test_action_view, :test_active_record_integration] + +Rake::TestTask.new(:test_action_view) do |t| + t.libs << 'test' + + # make sure we include the tests in alphabetical order as on some systems + # this will not happen automatically and the tests (as a whole) will error + t.test_files = Dir.glob('test/template/**/*_test.rb').sort + + t.warning = true + t.verbose = true +end + +namespace :test do + task :isolated do + ruby = File.join(*RbConfig::CONFIG.values_at('bindir', 'RUBY_INSTALL_NAME')) + Dir.glob("test/{active_record,template}/**/*_test.rb").all? do |file| + sh(ruby, '-Ilib:test', file) + end or raise "Failures" + end + + Rake::TestTask.new(:template) do |t| + t.libs << 'test' + t.pattern = 'test/template/**/*.rb' + end +end + +desc 'ActiveRecord Integration Tests' +Rake::TestTask.new(:test_active_record_integration) do |t| + t.libs << 'test' + t.test_files = Dir.glob("test/activerecord/*_test.rb") +end + +spec = eval(File.read('actionview.gemspec')) + +Gem::PackageTask.new(spec) do |p| + p.gem_spec = spec +end + +desc "Release to gemcutter" +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}" +end diff --git a/actionview/actionview.gemspec b/actionview/actionview.gemspec new file mode 100644 index 0000000000..87cb568300 --- /dev/null +++ b/actionview/actionview.gemspec @@ -0,0 +1,29 @@ +version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).strip + +Gem::Specification.new do |s| + s.platform = Gem::Platform::RUBY + s.name = 'actionview' + s.version = version + s.summary = 'Rendering framework putting the V in MVC (part of Rails).' + s.description = '' + + s.required_ruby_version = '>= 1.9.3' + + s.license = 'MIT' + + s.author = 'David Heinemeier Hansson' + s.email = 'david@loudthinking.com' + s.homepage = 'http://www.rubyonrails.org' + + s.files = Dir['CHANGELOG.md', 'README.rdoc', 'MIT-LICENSE', 'lib/**/*'] + s.require_path = 'lib' + s.requirements << 'none' + + s.add_dependency 'activesupport', version + s.add_dependency 'activemodel', version + + s.add_dependency 'builder', '~> 3.1.0' + s.add_dependency 'erubis', '~> 2.7.0' + + s.add_development_dependency 'actionpack', version +end diff --git a/actionview/lib/action_view.rb b/actionview/lib/action_view.rb new file mode 100644 index 0000000000..8def4ba7c5 --- /dev/null +++ b/actionview/lib/action_view.rb @@ -0,0 +1,92 @@ +#-- +# Copyright (c) 2004-2013 David Heinemeier Hansson +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#++ + +require 'active_support' +require 'active_support/rails' + +module ActionView + extend ActiveSupport::Autoload + + eager_autoload do + autoload :Base + autoload :Context + autoload :CompiledTemplates, "action_view/context" + autoload :Digestor + autoload :Helpers + autoload :LookupContext + autoload :PathSet + autoload :RecordIdentifier + autoload :RoutingUrlFor + autoload :Template + + autoload_under "renderer" do + autoload :Renderer + autoload :AbstractRenderer + autoload :PartialRenderer + autoload :TemplateRenderer + autoload :StreamingTemplateRenderer + end + + autoload_at "action_view/template/resolver" do + autoload :Resolver + autoload :PathResolver + autoload :FileSystemResolver + autoload :OptimizedFileSystemResolver + autoload :FallbackFileSystemResolver + end + + autoload_at "action_view/buffers" do + autoload :OutputBuffer + autoload :StreamingBuffer + end + + autoload_at "action_view/flows" do + autoload :OutputFlow + autoload :StreamingFlow + end + + autoload_at "action_view/template/error" do + autoload :MissingTemplate + autoload :ActionViewError + autoload :EncodingError + autoload :MissingRequestError + autoload :TemplateError + autoload :WrongEncodingError + end + end + + autoload :TestCase + + ENCODING_FLAG = '#.*coding[:=]\s*(\S+)[ \t]*' + + def self.eager_load! + super + ActionView::Template.eager_load! + end +end + +require 'active_support/core_ext/string/output_safety' + +ActiveSupport.on_load(:i18n) do + I18n.load_path << "#{File.dirname(__FILE__)}/action_view/locale/en.yml" +end diff --git a/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb new file mode 100644 index 0000000000..08253de3f4 --- /dev/null +++ b/actionview/lib/action_view/base.rb @@ -0,0 +1,201 @@ +require 'active_support/core_ext/module/attr_internal' +require 'active_support/core_ext/class/attribute_accessors' +require 'active_support/ordered_options' +require 'action_view/log_subscriber' + +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. + # + # == ERB + # + # You trigger ERB by using embeddings such as <% %>, <% -%>, and <%= %>. The <%= %> tag set is used when you want output. Consider the + # following loop for names: + # + # <b>Names of all the people</b> + # <% @people.each do |person| %> + # Name: <%= person.name %><br/> + # <% end %> + # + # The loop is setup in regular embedding tags <% %> and the name is written using the output embedding tag <%= %>. Note that this + # is not just a usage suggestion. Regular output functions like print or puts won't work with ERB templates. So this would be wrong: + # + # <%# WRONG %> + # Hi, Mr. <% puts "Frodo" %> + # + # 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 %>. + # + # === Using sub templates + # + # Using sub templates allows you to sidestep tedious replication and extract common display structures in shared templates. The + # classic example is the use of a header and footer (even though the Action Pack-way would be to use Layouts): + # + # <%= render "shared/header" %> + # Something really specific and terrific + # <%= render "shared/footer" %> + # + # As you see, we use the output embeddings for the render methods. The render call itself will just return a string holding the + # result of the rendering. The output embedding writes it to the current template. + # + # But you don't have to restrict yourself to static includes. Templates can share variables amongst themselves by using instance + # variables defined using the regular embedding tags. Like this: + # + # <% @page_title = "A Wonderful Hello" %> + # <%= render "shared/header" %> + # + # Now the header can pick up on the <tt>@page_title</tt> variable and use it for outputting a title tag: + # + # <title><%= @page_title %></title> + # + # === Passing local variables to sub templates + # + # You can pass local variables to sub templates by using a hash with the variable names as keys and the objects as values: + # + # <%= render "shared/header", { headline: "Welcome", person: person } %> + # + # These can now be accessed in <tt>shared/header</tt> with: + # + # 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: + # + # <% if local_assigns.has_key? :headline %> + # Headline: <%= headline %> + # <% end %> + # + # Testing using <tt>defined? headline</tt> will not work. This is an implementation restriction. + # + # === Template caching + # + # By default, Rails will compile each template to a method in order to render it. When you alter a template, + # Rails will check the file's modification time and recompile it in development mode. + # + # == Builder + # + # Builder templates are a more programmatic alternative to ERB. They are especially useful for generating XML content. An XmlMarkup object + # named +xml+ is automatically made available to templates with a <tt>.builder</tt> extension. + # + # Here are some basic examples: + # + # xml.em("emphasized") # => <em>emphasized</em> + # xml.em { xml.b("emph & bold") } # => <em><b>emph & bold</b></em> + # xml.a("A Link", "href" => "http://onestepback.org") # => <a href="http://onestepback.org">A Link</a> + # xml.target("name" => "compile", "option" => "fast") # => <target option="fast" name="compile"\> + # # NOTE: order of attributes is not specified. + # + # Any method with a block will be treated as an XML markup tag with nested markup in the block. For example, the following: + # + # xml.div do + # xml.h1(@person.name) + # xml.p(@person.bio) + # end + # + # would produce something like: + # + # <div> + # <h1>David Heinemeier Hansson</h1> + # <p>A product of Danish Design during the Winter of '79...</p> + # </div> + # + # A full-length RSS example actually used on Basecamp: + # + # xml.rss("version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/") do + # xml.channel do + # xml.title(@feed_title) + # xml.link(@url) + # xml.description "Basecamp: Recent items" + # xml.language "en-us" + # xml.ttl "40" + # + # @recent_items.each do |item| + # xml.item do + # xml.title(item_title(item)) + # xml.description(item_description(item)) if item_description(item) + # xml.pubDate(item_pubDate(item)) + # xml.guid(@person.firm.account.url + @recent_items.url(item)) + # xml.link(@person.firm.account.url + @recent_items.url(item)) + # + # xml.tag!("dc:creator", item.author_name) if item_has_creator?(item) + # end + # end + # end + # end + # + # More builder documentation can be found at http://builder.rubyforge.org. + class Base + include Helpers, ::ERB::Util, Context + + # Specify the proc used to decorate input tags that refer to attributes with errors. + cattr_accessor :field_error_proc + @@field_error_proc = Proc.new{ |html_tag, instance| "<div class=\"field_with_errors\">#{html_tag}</div>".html_safe } + + # How to complete the streaming when an exception occurs. + # This is our best guess: first try to close the attribute, then the tag. + cattr_accessor :streaming_completion_on_exception + @@streaming_completion_on_exception = %("><script>window.location = "/500.html"</script></html>) + + # Specify whether rendering within namespaced controllers should prefix + # the partial paths for ActiveModel objects with the namespace. + # (e.g., an Admin::PostsController would render @post using /admin/posts/_post.erb) + cattr_accessor :prefix_partial_path_with_controller_namespace + @@prefix_partial_path_with_controller_namespace = true + + # Specify default_formats that can be rendered. + cattr_accessor :default_formats + + class_attribute :_routes + class_attribute :logger + + class << self + delegate :erb_trim_mode=, :to => 'ActionView::Template::Handlers::ERB' + + def cache_template_loading + ActionView::Resolver.caching? + end + + def cache_template_loading=(value) + ActionView::Resolver.caching = value + end + + def xss_safe? #:nodoc: + true + end + end + + attr_accessor :view_renderer + attr_internal :config, :assigns + + delegate :lookup_context, :to => :view_renderer + delegate :formats, :formats=, :locale, :locale=, :view_paths, :view_paths=, :to => :lookup_context + + def assign(new_assigns) # :nodoc: + @_assigns = new_assigns.each { |key, value| instance_variable_set("@#{key}", value) } + end + + def initialize(context = nil, assigns = {}, controller = nil, formats = nil) #:nodoc: + @_config = ActiveSupport::InheritableOptions.new + + if context.is_a?(ActionView::Renderer) + @view_renderer = context + else + lookup_context = context.is_a?(ActionView::LookupContext) ? + context : ActionView::LookupContext.new(context) + lookup_context.formats = formats if formats + lookup_context.prefixes = controller._prefixes if controller + @view_renderer = ActionView::Renderer.new(lookup_context) + end + + assign(assigns) + assign_controller(controller) + _prepare_context + end + + ActiveSupport.run_load_hooks(:action_view, self) + end +end diff --git a/actionview/lib/action_view/buffers.rb b/actionview/lib/action_view/buffers.rb new file mode 100644 index 0000000000..361a0dccbe --- /dev/null +++ b/actionview/lib/action_view/buffers.rb @@ -0,0 +1,49 @@ +require 'active_support/core_ext/string/output_safety' + +module ActionView + class OutputBuffer < ActiveSupport::SafeBuffer #:nodoc: + def initialize(*) + super + encode! + end + + def <<(value) + return self if value.nil? + super(value.to_s) + end + alias :append= :<< + + def safe_concat(value) + return self if value.nil? + super(value.to_s) + end + alias :safe_append= :safe_concat + end + + class StreamingBuffer #:nodoc: + def initialize(block) + @block = block + end + + def <<(value) + value = value.to_s + value = ERB::Util.h(value) unless value.html_safe? + @block.call(value) + end + alias :concat :<< + alias :append= :<< + + def safe_concat(value) + @block.call(value.to_s) + end + alias :safe_append= :safe_concat + + def html_safe? + true + end + + def html_safe + self + end + end +end diff --git a/actionview/lib/action_view/context.rb b/actionview/lib/action_view/context.rb new file mode 100644 index 0000000000..ee263df484 --- /dev/null +++ b/actionview/lib/action_view/context.rb @@ -0,0 +1,36 @@ +module ActionView + module CompiledTemplates #:nodoc: + # holds compiled template code + end + + # = Action View Context + # + # Action View contexts are supplied to Action Controller to render a template. + # The default Action View context is ActionView::Base. + # + # In order to work with ActionController, a Context must just include this module. + # The initialization of the variables used by the context (@output_buffer, @view_flow, + # and @virtual_path) is responsibility of the object that includes this module + # (although you can call _prepare_context defined below). + module Context + include CompiledTemplates + attr_accessor :output_buffer, :view_flow + + # Prepares the context by setting the appropriate instance variables. + # :api: plugin + def _prepare_context + @view_flow = OutputFlow.new + @output_buffer = nil + @virtual_path = nil + end + + # Encapsulates the interaction with the view flow so it + # returns the correct buffer on +yield+. This is usually + # overwritten by helpers to add more behavior. + # :api: plugin + def _layout_for(name=nil) + name ||= :layout + view_flow.get(name).html_safe + end + end +end diff --git a/actionview/lib/action_view/dependency_tracker.rb b/actionview/lib/action_view/dependency_tracker.rb new file mode 100644 index 0000000000..b2e8334077 --- /dev/null +++ b/actionview/lib/action_view/dependency_tracker.rb @@ -0,0 +1,93 @@ +require 'thread_safe' + +module ActionView + class DependencyTracker + @trackers = ThreadSafe::Cache.new + + def self.find_dependencies(name, template) + tracker = @trackers[template.handler] + + if tracker.present? + tracker.call(name, template) + else + [] + end + end + + def self.register_tracker(extension, tracker) + handler = Template.handler_for_extension(extension) + @trackers[handler] = tracker + end + + def self.remove_tracker(handler) + @trackers.delete(handler) + end + + class ERBTracker + EXPLICIT_DEPENDENCY = /# Template Dependency: (\S+)/ + + # Matches: + # render partial: "comments/comment", collection: commentable.comments + # render "comments/comments" + # render 'comments/comments' + # render('comments/comments') + # + # render(@topic) => render("topics/topic") + # render(topics) => render("topics/topic") + # render(message.topics) => render("topics/topic") + RENDER_DEPENDENCY = / + render\s* # render, followed by optional whitespace + \(? # start an optional parenthesis for the render call + (partial:|:partial\s+=>)?\s* # naming the partial, used with collection -- 1st capture + ([@a-z"'][@\w\/\."']+) # the template name itself -- 2nd capture + /x + + def self.call(name, template) + new(name, template).dependencies + end + + def initialize(name, template) + @name, @template = name, template + end + + def dependencies + render_dependencies + explicit_dependencies + end + + attr_reader :name, :template + private :name, :template + + private + + def source + template.source + end + + def directory + name.split("/")[0..-2].join("/") + end + + def render_dependencies + source.scan(RENDER_DEPENDENCY). + collect(&:second).uniq. + + # render(@topic) => render("topics/topic") + # render(topics) => render("topics/topic") + # render(message.topics) => render("topics/topic") + collect { |name| name.sub(/\A@?([a-z_]+\.)*([a-z_]+)\z/) { "#{$2.pluralize}/#{$2.singularize}" } }. + + # render("headline") => render("message/headline") + collect { |name| name.include?("/") ? name : "#{directory}/#{name}" }. + + # replace quotes from string renders + collect { |name| name.gsub(/["']/, "") } + end + + def explicit_dependencies + source.scan(EXPLICIT_DEPENDENCY).flatten.uniq + end + end + + register_tracker :erb, ERBTracker + end +end diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb new file mode 100644 index 0000000000..9324a1ac50 --- /dev/null +++ b/actionview/lib/action_view/digestor.rb @@ -0,0 +1,85 @@ +require 'thread_safe' +require 'action_view/dependency_tracker' + +module ActionView + class Digestor + cattr_reader(:cache) + @@cache = ThreadSafe::Cache.new + + def self.digest(name, format, finder, options = {}) + cache_key = [name, format] + Array.wrap(options[:dependencies]) + @@cache[cache_key.join('.')] ||= begin + klass = options[:partial] || name.include?("/_") ? PartialDigestor : Digestor + klass.new(name, format, finder, options).digest + end + end + + attr_reader :name, :format, :finder, :options + + def initialize(name, format, finder, options={}) + @name, @format, @finder, @options = name, format, finder, options + end + + def digest + Digest::MD5.hexdigest("#{source}-#{dependency_digest}").tap do |digest| + logger.try :info, "Cache digest for #{name}.#{format}: #{digest}" + end + rescue ActionView::MissingTemplate + logger.try :error, "Couldn't find template for digesting: #{name}.#{format}" + '' + end + + def dependencies + DependencyTracker.find_dependencies(name, template) + rescue ActionView::MissingTemplate + [] # File doesn't exist, so no dependencies + end + + def nested_dependencies + dependencies.collect do |dependency| + dependencies = PartialDigestor.new(dependency, format, finder).nested_dependencies + dependencies.any? ? { dependency => dependencies } : dependency + end + end + + private + + def logger + ActionView::Base.logger + end + + def logical_name + name.gsub(%r|/_|, "/") + end + + def partial? + false + end + + def template + @template ||= finder.find(logical_name, [], partial?, formats: [ format ]) + end + + def source + template.source + end + + def dependency_digest + template_digests = dependencies.collect do |template_name| + Digestor.digest(template_name, format, finder, partial: true) + end + + (template_digests + injected_dependencies).join("-") + end + + def injected_dependencies + Array.wrap(options[:dependencies]) + end + end + + class PartialDigestor < Digestor # :nodoc: + def partial? + true + end + end +end diff --git a/actionview/lib/action_view/flows.rb b/actionview/lib/action_view/flows.rb new file mode 100644 index 0000000000..c0e458cd41 --- /dev/null +++ b/actionview/lib/action_view/flows.rb @@ -0,0 +1,76 @@ +require 'active_support/core_ext/string/output_safety' + +module ActionView + class OutputFlow #:nodoc: + attr_reader :content + + def initialize + @content = Hash.new { |h,k| h[k] = ActiveSupport::SafeBuffer.new } + end + + # Called by _layout_for to read stored values. + def get(key) + @content[key] + end + + # Called by each renderer object to set the layout contents. + def set(key, value) + @content[key] = value + end + + # Called by content_for + def append(key, value) + @content[key] << value + end + alias_method :append!, :append + + end + + class StreamingFlow < OutputFlow #:nodoc: + def initialize(view, fiber) + @view = view + @parent = nil + @child = view.output_buffer + @content = view.view_flow.content + @fiber = fiber + @root = Fiber.current.object_id + end + + # Try to get an stored content. If the content + # is not available and we are inside the layout + # fiber, we set that we are waiting for the given + # key and yield. + def get(key) + return super if @content.key?(key) + + if inside_fiber? + view = @view + + begin + @waiting_for = key + view.output_buffer, @parent = @child, view.output_buffer + Fiber.yield + ensure + @waiting_for = nil + view.output_buffer, @child = @parent, view.output_buffer + end + end + + super + end + + # Appends the contents for the given key. This is called + # by provides and resumes back to the fiber if it is + # the key it is waiting for. + def append!(key, value) + super + @fiber.resume if @waiting_for == key + end + + private + + def inside_fiber? + Fiber.current.object_id != @root + end + end +end
\ No newline at end of file diff --git a/actionview/lib/action_view/helpers.rb b/actionview/lib/action_view/helpers.rb new file mode 100644 index 0000000000..8a78685ae1 --- /dev/null +++ b/actionview/lib/action_view/helpers.rb @@ -0,0 +1,58 @@ +require 'active_support/benchmarkable' + +module ActionView #:nodoc: + module Helpers #:nodoc: + extend ActiveSupport::Autoload + + autoload :ActiveModelHelper + autoload :AssetTagHelper + autoload :AssetUrlHelper + autoload :AtomFeedHelper + autoload :CacheHelper + autoload :CaptureHelper + autoload :ControllerHelper + autoload :CsrfHelper + autoload :DateHelper + autoload :DebugHelper + autoload :FormHelper + autoload :FormOptionsHelper + autoload :FormTagHelper + autoload :JavaScriptHelper, "action_view/helpers/javascript_helper" + autoload :NumberHelper + autoload :OutputSafetyHelper + autoload :RecordTagHelper + autoload :RenderingHelper + autoload :SanitizeHelper + autoload :TagHelper + autoload :TextHelper + autoload :TranslationHelper + autoload :UrlHelper + + extend ActiveSupport::Concern + + include ActiveSupport::Benchmarkable + include ActiveModelHelper + include AssetTagHelper + include AssetUrlHelper + include AtomFeedHelper + include CacheHelper + include CaptureHelper + include ControllerHelper + include CsrfHelper + include DateHelper + include DebugHelper + include FormHelper + include FormOptionsHelper + include FormTagHelper + include JavaScriptHelper + include NumberHelper + include OutputSafetyHelper + include RecordTagHelper + include RenderingHelper + include SanitizeHelper + include TagHelper + include TextHelper + include TranslationHelper + include UrlHelper + end +end diff --git a/actionview/lib/action_view/helpers/active_model_helper.rb b/actionview/lib/action_view/helpers/active_model_helper.rb new file mode 100644 index 0000000000..901f433c70 --- /dev/null +++ b/actionview/lib/action_view/helpers/active_model_helper.rb @@ -0,0 +1,49 @@ +require 'active_support/core_ext/class/attribute_accessors' +require 'active_support/core_ext/enumerable' + +module ActionView + # = Active Model Helpers + module Helpers + module ActiveModelHelper + end + + module ActiveModelInstanceTag + def object + @active_model_object ||= begin + object = super + object.respond_to?(:to_model) ? object.to_model : object + end + end + + def content_tag(*) + error_wrapping(super) + end + + def tag(type, options, *) + tag_generate_errors?(options) ? error_wrapping(super) : super + end + + def error_wrapping(html_tag) + if object_has_errors? + Base.field_error_proc.call(html_tag, self) + else + html_tag + end + end + + def error_message + object.errors[@method_name] + end + + private + + def object_has_errors? + object.respond_to?(:errors) && object.errors.respond_to?(:[]) && error_message.present? + end + + def tag_generate_errors?(options) + options['type'] != 'hidden' + end + end + end +end diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb new file mode 100644 index 0000000000..2b3a3c6a29 --- /dev/null +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -0,0 +1,316 @@ +require 'active_support/core_ext/array/extract_options' +require 'active_support/core_ext/hash/keys' +require 'action_view/helpers/asset_url_helper' +require 'action_view/helpers/tag_helper' + +module ActionView + # = Action View Asset Tag Helpers + module Helpers #:nodoc: + # This module provides methods for generating HTML that links views to assets such + # as images, javascripts, stylesheets, and feeds. These methods do not verify + # the assets exist before linking to them: + # + # image_tag("rails.png") + # # => <img alt="Rails" src="/assets/rails.png" /> + # stylesheet_link_tag("application") + # # => <link href="/assets/application.css?body=1" media="screen" rel="stylesheet" /> + module AssetTagHelper + extend ActiveSupport::Concern + + include AssetUrlHelper + include TagHelper + + # Returns an HTML script tag for each of the +sources+ provided. + # + # Sources may be paths to JavaScript files. Relative paths are assumed to be relative + # to <tt>assets/javascripts</tt>, full paths are assumed to be relative to the document + # root. Relative paths are idiomatic, use absolute paths only when needed. + # + # When passing paths, the ".js" extension is optional. + # + # You can modify the HTML attributes of the script tag by passing a hash as the + # last argument. + # + # When the Asset Pipeline is enabled, you can pass the name of your manifest as + # source, and include other JavaScript or CoffeeScript files inside the manifest. + # + # javascript_include_tag "xmlhr" + # # => <script src="/assets/xmlhr.js?1284139606"></script> + # + # javascript_include_tag "xmlhr.js" + # # => <script src="/assets/xmlhr.js?1284139606"></script> + # + # javascript_include_tag "common.javascript", "/elsewhere/cools" + # # => <script src="/assets/common.javascript?1284139606"></script> + # # <script src="/elsewhere/cools.js?1423139606"></script> + # + # javascript_include_tag "http://www.example.com/xmlhr" + # # => <script src="http://www.example.com/xmlhr"></script> + # + # javascript_include_tag "http://www.example.com/xmlhr.js" + # # => <script src="http://www.example.com/xmlhr.js"></script> + def javascript_include_tag(*sources) + options = sources.extract_options!.stringify_keys + path_options = options.extract!('protocol').symbolize_keys + + sources.uniq.map { |source| + tag_options = { + "src" => path_to_javascript(source, path_options) + }.merge!(options) + content_tag(:script, "", tag_options) + }.join("\n").html_safe + end + + # Returns a stylesheet link tag for the sources specified as arguments. If + # you don't specify an extension, <tt>.css</tt> will be appended automatically. + # You can modify the link attributes by passing a hash as the last argument. + # For historical reasons, the 'media' attribute will always be present and defaults + # to "screen", so you must explicitly set it to "all" for the stylesheet(s) to + # apply to all media types. + # + # stylesheet_link_tag "style" + # # => <link href="/assets/style.css" media="screen" rel="stylesheet" /> + # + # stylesheet_link_tag "style.css" + # # => <link href="/assets/style.css" media="screen" rel="stylesheet" /> + # + # stylesheet_link_tag "http://www.example.com/style.css" + # # => <link href="http://www.example.com/style.css" media="screen" rel="stylesheet" /> + # + # stylesheet_link_tag "style", media: "all" + # # => <link href="/assets/style.css" media="all" rel="stylesheet" /> + # + # stylesheet_link_tag "style", media: "print" + # # => <link href="/assets/style.css" media="print" rel="stylesheet" /> + # + # stylesheet_link_tag "random.styles", "/css/stylish" + # # => <link href="/assets/random.styles" media="screen" rel="stylesheet" /> + # # <link href="/css/stylish.css" media="screen" rel="stylesheet" /> + def stylesheet_link_tag(*sources) + options = sources.extract_options!.stringify_keys + path_options = options.extract!('protocol').symbolize_keys + + sources.uniq.map { |source| + tag_options = { + "rel" => "stylesheet", + "media" => "screen", + "href" => path_to_stylesheet(source, path_options) + }.merge!(options) + tag(:link, tag_options) + }.join("\n").html_safe + end + + # Returns a link tag that browsers and news readers can use to auto-detect + # an RSS or Atom feed. The +type+ can either be <tt>:rss</tt> (default) or + # <tt>:atom</tt>. Control the link options in url_for format using the + # +url_options+. You can modify the LINK tag itself in +tag_options+. + # + # ==== Options + # + # * <tt>:rel</tt> - Specify the relation of this link, defaults to "alternate" + # * <tt>:type</tt> - Override the auto-generated mime type + # * <tt>:title</tt> - Specify the title of the link, defaults to the +type+ + # + # ==== Examples + # + # auto_discovery_link_tag + # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/action" /> + # auto_discovery_link_tag(:atom) + # # => <link rel="alternate" type="application/atom+xml" title="ATOM" href="http://www.currenthost.com/controller/action" /> + # auto_discovery_link_tag(:rss, {action: "feed"}) + # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/feed" /> + # auto_discovery_link_tag(:rss, {action: "feed"}, {title: "My RSS"}) + # # => <link rel="alternate" type="application/rss+xml" title="My RSS" href="http://www.currenthost.com/controller/feed" /> + # 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" /> + 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.") + end + + tag( + "link", + "rel" => tag_options[:rel] || "alternate", + "type" => tag_options[:type] || Mime::Type.lookup_by_extension(type.to_s).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 + ) + end + + # Returns a link loading a favicon file. You may specify a different file + # in the first argument. The helper accepts an additional options hash where + # you can override "rel" and "type". + # + # ==== Options + # + # * <tt>:rel</tt> - Specify the relation of this link, defaults to 'shortcut icon' + # * <tt>:type</tt> - Override the auto-generated mime type, defaults to 'image/vnd.microsoft.icon' + # + # ==== Examples + # + # favicon_link_tag '/myicon.ico' + # # => <link href="/assets/myicon.ico" rel="shortcut icon" type="image/vnd.microsoft.icon" /> + # + # Mobile Safari looks for a different <link> tag, pointing to an image that + # will be used if you add the page to the home screen of an iPod Touch, iPhone, or iPad. + # The following call would generate such a tag: + # + # favicon_link_tag '/mb-icon.png', rel: 'apple-touch-icon', type: 'image/png' + # # => <link href="/assets/mb-icon.png" rel="apple-touch-icon" type="image/png" /> + def favicon_link_tag(source='favicon.ico', options={}) + tag('link', { + :rel => 'shortcut icon', + :type => 'image/vnd.microsoft.icon', + :href => path_to_image(source) + }.merge!(options.symbolize_keys)) + end + + # Returns an HTML image tag for the +source+. The +source+ can be a full + # path or a file. + # + # ==== Options + # + # You can add HTML attributes using the +options+. The +options+ supports + # three additional keys for convenience and conformance: + # + # * <tt>:alt</tt> - If no alt text is given, the file name part of the + # +source+ is used (capitalized and without the extension) + # * <tt>:size</tt> - Supplied as "{Width}x{Height}" or "{Number}", so "30x45" becomes + # width="30" and height="45", and "50" becomes width="50" and height="50". + # <tt>:size</tt> will be ignored if the value is not in the correct format. + # + # ==== Examples + # + # image_tag("icon") + # # => <img alt="Icon" src="/assets/icon" /> + # image_tag("icon.png") + # # => <img alt="Icon" src="/assets/icon.png" /> + # image_tag("icon.png", size: "16x10", alt: "Edit Entry") + # # => <img src="/assets/icon.png" width="16" height="10" alt="Edit Entry" /> + # image_tag("/icons/icon.gif", size: "16") + # # => <img src="/icons/icon.gif" width="16" height="16" alt="Icon" /> + # image_tag("/icons/icon.gif", height: '32', width: '32') + # # => <img alt="Icon" height="32" src="/icons/icon.gif" width="32" /> + # image_tag("/icons/icon.gif", class: "menu_icon") + # # => <img alt="Icon" class="menu_icon" src="/icons/icon.gif" /> + def image_tag(source, options={}) + options = options.symbolize_keys + + src = options[:src] = path_to_image(source) + + unless src =~ /^(?:cid|data):/ || src.blank? + options[:alt] = options.fetch(:alt){ image_alt(src) } + end + + if size = options.delete(:size) + options[:width], options[:height] = size.split("x") if size =~ %r{\A\d+x\d+\z} + options[:width] = options[:height] = size if size =~ %r{\A\d+\z} + end + + tag("img", options) + end + + # 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 + # replaces them with spaces, returning a space-separated, titleized + # string. + # + # ==== Examples + # + # image_tag('rails.png') + # # => <img alt="Rails" src="/assets/rails.png" /> + # + # image_tag('hyphenated-file-name.png') + # # => <img alt="Hyphenated file name" src="/assets/hyphenated-file-name.png" /> + # + # image_tag('underscored_file_name.png') + # # => <img alt="Underscored file name" src="/assets/underscored_file_name.png" /> + def image_alt(src) + File.basename(src, '.*').sub(/-[[:xdigit:]]{32}\z/, '').tr('-_', ' ').capitalize + end + + # 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 + # directory. + # + # ==== Options + # You can add HTML attributes using the +options+. The +options+ supports + # two additional keys for convenience and conformance: + # + # * <tt>:poster</tt> - Set an image (like a screenshot) to be shown + # before the video loads. The path is calculated like the +src+ of +image_tag+. + # * <tt>:size</tt> - Supplied as "{Width}x{Height}", so "30x45" becomes + # width="30" and height="45". <tt>:size</tt> will be ignored if the + # value is not in the correct format. + # + # ==== Examples + # + # video_tag("trailer") + # # => <video src="/videos/trailer" /> + # video_tag("trailer.ogg") + # # => <video src="/videos/trailer.ogg" /> + # video_tag("trailer.ogg", controls: true, autobuffer: true) + # # => <video autobuffer="autobuffer" controls="controls" src="/videos/trailer.ogg" /> + # video_tag("trailer.m4v", size: "16x10", poster: "screenshot.png") + # # => <video src="/videos/trailer.m4v" width="16" height="10" poster="/assets/screenshot.png" /> + # video_tag("/trailers/hd.avi", size: "16x16") + # # => <video src="/trailers/hd.avi" width="16" height="16" /> + # video_tag("/trailers/hd.avi", height: '32', width: '32') + # # => <video height="32" src="/trailers/hd.avi" width="32" /> + # video_tag("trailer.ogg", "trailer.flv") + # # => <video><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video> + # video_tag(["trailer.ogg", "trailer.flv"]) + # # => <video><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video> + # video_tag(["trailer.ogg", "trailer.flv"], size: "160x120") + # # => <video height="120" width="160"><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video> + def video_tag(*sources) + multiple_sources_tag('video', sources) do |options| + options[:poster] = path_to_image(options[:poster]) if options[:poster] + + if size = options.delete(:size) + options[:width], options[:height] = size.split("x") if size =~ %r{^\d+x\d+$} + end + end + end + + # Returns an HTML audio tag for the +source+. + # The +source+ can be full path or file that exists in + # your public audios directory. + # + # audio_tag("sound") + # # => <audio src="/audios/sound" /> + # audio_tag("sound.wav") + # # => <audio src="/audios/sound.wav" /> + # audio_tag("sound.wav", autoplay: true, controls: true) + # # => <audio autoplay="autoplay" controls="controls" src="/audios/sound.wav" /> + # audio_tag("sound.wav", "sound.mid") + # # => <audio><source src="/audios/sound.wav" /><source src="/audios/sound.mid" /></audio> + def audio_tag(*sources) + multiple_sources_tag('audio', sources) + end + + private + def multiple_sources_tag(type, sources) + options = sources.extract_options!.symbolize_keys + sources.flatten! + + yield options if block_given? + + if sources.size > 1 + content_tag(type, options) do + safe_join sources.map { |source| tag("source", :src => send("path_to_#{type}", source)) } + end + else + options[:src] = send("path_to_#{type}", sources.first) + content_tag(type, nil, options) + 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 new file mode 100644 index 0000000000..0b957adb91 --- /dev/null +++ b/actionview/lib/action_view/helpers/asset_url_helper.rb @@ -0,0 +1,355 @@ +require 'zlib' + +module ActionView + # = Action View Asset URL Helpers + module Helpers + # This module provides methods for generating asset paths and + # urls. + # + # image_path("rails.png") + # # => "/assets/rails.png" + # + # image_url("rails.png") + # # => "http://www.example.com/assets/rails.png" + # + # === Using asset hosts + # + # By default, Rails links to these assets on the current host in the public + # folder, but you can direct Rails to link to assets from a dedicated asset + # server by setting <tt>ActionController::Base.asset_host</tt> in the application + # configuration, typically in <tt>config/environments/production.rb</tt>. + # For example, you'd define <tt>assets.example.com</tt> to be your asset + # host this way, inside the <tt>configure</tt> block of your environment-specific + # configuration files or <tt>config/application.rb</tt>: + # + # config.action_controller.asset_host = "assets.example.com" + # + # Helpers take that into account: + # + # image_tag("rails.png") + # # => <img alt="Rails" src="http://assets.example.com/assets/rails.png" /> + # 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. + # + # 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. + # + # 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. + # + # Alternatively, you can exert more control over the asset host by setting + # +asset_host+ to a proc like this: + # + # ActionController::Base.asset_host = Proc.new { |source| + # "http://assets#{Digest::MD5.hexdigest(source).to_i(16) % 2 + 1}.example.com" + # } + # image_tag("rails.png") + # # => <img alt="Rails" src="http://assets1.example.com/assets/rails.png" /> + # stylesheet_link_tag("application") + # # => <link href="http://assets2.example.com/assets/application.css" media="screen" rel="stylesheet" /> + # + # The example above generates "http://assets1.example.com" and + # "http://assets2.example.com". This option is useful for example if + # you need fewer/more than four hosts, custom host names, etc. + # + # As you see the proc takes a +source+ parameter. That's a string with the + # absolute path of the asset, for example "/assets/rails.png". + # + # ActionController::Base.asset_host = Proc.new { |source| + # if source.ends_with?('.css') + # "http://stylesheets.example.com" + # else + # "http://assets.example.com" + # end + # } + # image_tag("rails.png") + # # => <img alt="Rails" src="http://assets.example.com/assets/rails.png" /> + # stylesheet_link_tag("application") + # # => <link href="http://stylesheets.example.com/assets/application.css" media="screen" rel="stylesheet" /> + # + # Alternatively you may ask for a second parameter +request+. That one is + # particularly useful for serving assets from an SSL-protected page. The + # example proc below disables asset hosting for HTTPS connections, while + # still sending assets for plain HTTP requests from asset hosts. If you don't + # have SSL certificates for each of the asset hosts this technique allows you + # to avoid warnings in the client about mixed media. + # + # config.action_controller.asset_host = Proc.new { |source, request| + # if request.ssl? + # "#{request.protocol}#{request.host_with_port}" + # else + # "#{request.protocol}assets.example.com" + # end + # } + # + # You can also implement a custom asset host object that responds to +call+ + # and takes either one or two parameters just like the proc. + # + # config.action_controller.asset_host = AssetHostingWithMinimumSsl.new( + # "http://asset%d.example.com", "https://asset1.example.com" + # ) + # + module AssetUrlHelper + URI_REGEXP = %r{^[-a-z]+://|^(?:cid|data):|^//}i + + # Computes the path to asset in public directory. If :type + # options is set, a file extension will be appended and scoped + # to the corresponding public directory. + # + # All other asset *_path helpers delegate through this method. + # + # asset_path "application.js" # => /application.js + # asset_path "application", type: :javascript # => /javascripts/application.js + # asset_path "application", type: :stylesheet # => /stylesheets/application.css + # asset_path "http://www.example.com/js/xmlhr.js" # => http://www.example.com/js/xmlhr.js + def asset_path(source, options = {}) + source = source.to_s + return "" unless source.present? + return source if source =~ URI_REGEXP + + tail, source = source[/([\?#].+)$/], source.sub(/([\?#].+)$/, '') + + if extname = compute_asset_extname(source, options) + source = "#{source}#{extname}" + end + + if source[0] != ?/ + source = compute_asset_path(source, options) + end + + relative_url_root = defined?(config.relative_url_root) && config.relative_url_root + if relative_url_root + source = "#{relative_url_root}#{source}" unless source.starts_with?("#{relative_url_root}/") + end + + if host = compute_asset_host(source, options) + source = "#{host}#{source}" + end + + "#{source}#{tail}" + end + alias_method :path_to_asset, :asset_path # aliased to avoid conflicts with a asset_path named route + + # Computes the full URL to a asset in the public directory. This + # will use +asset_path+ internally, so most of their behaviors + # will be the same. + def asset_url(source, options = {}) + path_to_asset(source, options.merge(:protocol => :request)) + end + alias_method :url_to_asset, :asset_url # aliased to avoid conflicts with an asset_url named route + + ASSET_EXTENSIONS = { + javascript: '.js', + stylesheet: '.css' + } + + # Compute extname to append to asset path. Returns nil if + # nothing should be added. + def compute_asset_extname(source, options = {}) + return if options[:extname] == false + extname = options[:extname] || ASSET_EXTENSIONS[options[:type]] + extname if extname && File.extname(source) != extname + end + + # Maps asset types to public directory. + ASSET_PUBLIC_DIRECTORIES = { + audio: '/audios', + font: '/fonts', + image: '/images', + javascript: '/javascripts', + stylesheet: '/stylesheets', + video: '/videos' + } + + # Computes asset path to public directory. Plugins and + # extensions can override this method to point to custom assets + # or generate digested paths or query strings. + def compute_asset_path(source, options = {}) + dir = ASSET_PUBLIC_DIRECTORIES[options[:type]] || "" + File.join(dir, source) + end + + # Pick an asset host for this source. Returns +nil+ if no host is set, + # the host if no wildcard is set, the host interpolated with the + # numbers 0-3 if it contains <tt>%d</tt> (the number is the source hash mod 4), + # or the value returned from invoking call on an object responding to call + # (proc or otherwise). + def compute_asset_host(source = "", options = {}) + request = self.request if respond_to?(:request) + host = config.asset_host if defined? config.asset_host + host ||= request.base_url if request && options[:protocol] == :request + + if host.respond_to?(:call) + arity = host.respond_to?(:arity) ? host.arity : host.method(:call).arity + args = [source] + args << request if request && (arity > 1 || arity < 0) + host = host.call(*args) + elsif host =~ /%d/ + host = host % (Zlib.crc32(source) % 4) + end + + return unless host + + if host =~ URI_REGEXP + host + else + protocol = options[:protocol] || config.default_asset_host_protocol || (request ? :request : :relative) + case protocol + when :relative + "//#{host}" + when :request + "#{request.protocol}#{host}" + else + "#{protocol}://#{host}" + end + end + end + + # Computes the path to a javascript asset in the public javascripts directory. + # If the +source+ filename has no extension, .js will be appended (except for explicit URIs) + # Full paths from the document root will be passed through. + # Used internally by javascript_include_tag to build the script path. + # + # javascript_path "xmlhr" # => /javascripts/xmlhr.js + # javascript_path "dir/xmlhr.js" # => /javascripts/dir/xmlhr.js + # javascript_path "/dir/xmlhr" # => /dir/xmlhr.js + # javascript_path "http://www.example.com/js/xmlhr" # => http://www.example.com/js/xmlhr + # javascript_path "http://www.example.com/js/xmlhr.js" # => http://www.example.com/js/xmlhr.js + def javascript_path(source, options = {}) + path_to_asset(source, {type: :javascript}.merge!(options)) + end + alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with a javascript_path named route + + # 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. + def javascript_url(source, options = {}) + url_to_asset(source, {type: :javascript}.merge!(options)) + end + alias_method :url_to_javascript, :javascript_url # aliased to avoid conflicts with a javascript_url named route + + # Computes the path to a stylesheet asset in the public stylesheets directory. + # If the +source+ filename has no extension, <tt>.css</tt> will be appended (except for explicit URIs). + # Full paths from the document root will be passed through. + # Used internally by +stylesheet_link_tag+ to build the stylesheet path. + # + # stylesheet_path "style" # => /stylesheets/style.css + # stylesheet_path "dir/style.css" # => /stylesheets/dir/style.css + # stylesheet_path "/dir/style.css" # => /dir/style.css + # stylesheet_path "http://www.example.com/css/style" # => http://www.example.com/css/style + # stylesheet_path "http://www.example.com/css/style.css" # => http://www.example.com/css/style.css + def stylesheet_path(source, options = {}) + path_to_asset(source, {type: :stylesheet}.merge!(options)) + end + alias_method :path_to_stylesheet, :stylesheet_path # aliased to avoid conflicts with a stylesheet_path named route + + # 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. + def stylesheet_url(source, options = {}) + url_to_asset(source, {type: :stylesheet}.merge!(options)) + end + alias_method :url_to_stylesheet, :stylesheet_url # aliased to avoid conflicts with a stylesheet_url named route + + # Computes the path to an image asset. + # Full paths from the document root will be passed through. + # Used internally by +image_tag+ to build the image path: + # + # image_path("edit") # => "/assets/edit" + # image_path("edit.png") # => "/assets/edit.png" + # image_path("icons/edit.png") # => "/assets/icons/edit.png" + # image_path("/icons/edit.png") # => "/icons/edit.png" + # image_path("http://www.example.com/img/edit.png") # => "http://www.example.com/img/edit.png" + # + # If you have images as application resources this method may conflict with their named routes. + # The alias +path_to_image+ is provided to avoid that. Rails uses the alias internally, and + # plugin authors are encouraged to do so. + def image_path(source, options = {}) + path_to_asset(source, {type: :image}.merge!(options)) + end + alias_method :path_to_image, :image_path # aliased to avoid conflicts with an image_path named route + + # Computes the full URL to an image asset. + # This will use +image_path+ internally, so most of their behaviors will be the same. + def image_url(source, options = {}) + url_to_asset(source, {type: :image}.merge!(options)) + end + alias_method :url_to_image, :image_url # aliased to avoid conflicts with an image_url named route + + # Computes the path to a video asset in the public videos directory. + # Full paths from the document root will be passed through. + # Used internally by +video_tag+ to build the video path. + # + # video_path("hd") # => /videos/hd + # video_path("hd.avi") # => /videos/hd.avi + # video_path("trailers/hd.avi") # => /videos/trailers/hd.avi + # video_path("/trailers/hd.avi") # => /trailers/hd.avi + # video_path("http://www.example.com/vid/hd.avi") # => http://www.example.com/vid/hd.avi + def video_path(source, options = {}) + path_to_asset(source, {type: :video}.merge!(options)) + end + alias_method :path_to_video, :video_path # aliased to avoid conflicts with a video_path named route + + # 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. + def video_url(source, options = {}) + url_to_asset(source, {type: :video}.merge!(options)) + end + alias_method :url_to_video, :video_url # aliased to avoid conflicts with an video_url named route + + # Computes the path to an audio asset in the public audios directory. + # Full paths from the document root will be passed through. + # Used internally by +audio_tag+ to build the audio path. + # + # audio_path("horse") # => /audios/horse + # audio_path("horse.wav") # => /audios/horse.wav + # audio_path("sounds/horse.wav") # => /audios/sounds/horse.wav + # audio_path("/sounds/horse.wav") # => /sounds/horse.wav + # audio_path("http://www.example.com/sounds/horse.wav") # => http://www.example.com/sounds/horse.wav + def audio_path(source, options = {}) + path_to_asset(source, {type: :audio}.merge!(options)) + end + alias_method :path_to_audio, :audio_path # aliased to avoid conflicts with an audio_path named route + + # 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. + def audio_url(source, options = {}) + url_to_asset(source, {type: :audio}.merge!(options)) + end + alias_method :url_to_audio, :audio_url # aliased to avoid conflicts with an audio_url named route + + # Computes the path to a font asset. + # Full paths from the document root will be passed through. + # + # font_path("font") # => /assets/font + # font_path("font.ttf") # => /assets/font.ttf + # font_path("dir/font.ttf") # => /assets/dir/font.ttf + # font_path("/dir/font.ttf") # => /dir/font.ttf + # font_path("http://www.example.com/dir/font.ttf") # => http://www.example.com/dir/font.ttf + def font_path(source, options = {}) + path_to_asset(source, {type: :font}.merge!(options)) + end + alias_method :path_to_font, :font_path # aliased to avoid conflicts with an font_path named route + + # Computes the full URL to a font asset. + # This will use +font_path+ internally, so most of their behaviors will be the same. + def font_url(source, options = {}) + url_to_asset(source, {type: :font}.merge!(options)) + end + alias_method :url_to_font, :font_url # aliased to avoid conflicts with an font_url named route + end + end +end diff --git a/actionview/lib/action_view/helpers/atom_feed_helper.rb b/actionview/lib/action_view/helpers/atom_feed_helper.rb new file mode 100644 index 0000000000..42b1dd8933 --- /dev/null +++ b/actionview/lib/action_view/helpers/atom_feed_helper.rb @@ -0,0 +1,203 @@ +require 'set' + +module ActionView + # = Action View Atom Feed Helpers + module Helpers + module AtomFeedHelper + # Adds easy defaults to writing Atom feeds with the Builder template engine (this does not work on ERB or any other + # template languages). + # + # Full usage example: + # + # config/routes.rb: + # Basecamp::Application.routes.draw do + # resources :posts + # root to: "posts#index" + # end + # + # app/controllers/posts_controller.rb: + # class PostsController < ApplicationController::Base + # # GET /posts.html + # # GET /posts.atom + # def index + # @posts = Post.all + # + # respond_to do |format| + # format.html + # format.atom + # end + # end + # end + # + # app/views/posts/index.atom.builder: + # atom_feed do |feed| + # feed.title("My great blog!") + # feed.updated(@posts[0].created_at) if @posts.length > 0 + # + # @posts.each do |post| + # feed.entry(post) do |entry| + # entry.title(post.title) + # entry.content(post.body, type: 'html') + # + # entry.author do |author| + # author.name("DHH") + # end + # end + # end + # end + # + # The options for atom_feed are: + # + # * <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>: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). + # * <tt>:instruct</tt>: Hash of XML processing instructions in the form {target => {attribute => value, }} or {target => [{attribute => value, }, ]} + # + # Other namespaces can be added to the root element: + # + # app/views/posts/index.atom.builder: + # 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((@posts.first.created_at)) + # feed.tag!(openSearch:totalResults, 10) + # + # @posts.each do |post| + # feed.entry(post) do |entry| + # entry.title(post.title) + # entry.content(post.body, type: 'html') + # entry.tag!('app:edited', Time.now) + # + # entry.author do |author| + # author.name("DHH") + # end + # end + # end + # end + # + # The Atom spec defines five elements (content rights title subtitle + # summary) which may directly contain xhtml content if type: 'xhtml' + # is specified as an attribute. If so, this helper will take care of + # the enclosing div and xhtml namespace declaration. Example usage: + # + # entry.summary type: 'xhtml' do |xhtml| + # xhtml.p pluralize(order.line_items.count, "line item") + # xhtml.p "Shipped to #{order.address}" + # xhtml.p "Paid by #{order.pay_type}" + # end + # + # + # <tt>atom_feed</tt> yields an +AtomFeedBuilder+ instance. Nested elements yield + # an +AtomBuilder+ instance. + def atom_feed(options = {}, &block) + if options[:schema_date] + options[:schema_date] = options[:schema_date].strftime("%Y-%m-%d") if options[:schema_date].respond_to?(:strftime) + else + options[:schema_date] = "2005" # The Atom spec copyright date + end + + xml = options.delete(:xml) || eval("xml", block.binding) + xml.instruct! + if options[:instruct] + options[:instruct].each do |target,attrs| + if attrs.respond_to?(:keys) + xml.instruct!(target, attrs) + elsif attrs.respond_to?(:each) + attrs.each { |attr_group| xml.instruct!(target, attr_group) } + end + end + end + + feed_opts = {"xml:lang" => options[:language] || "en-US", "xmlns" => 'http://www.w3.org/2005/Atom'} + feed_opts.merge!(options).reject!{|k,v| !k.to_s.match(/^xml/)} + + xml.feed(feed_opts) do + xml.id(options[:id] || "tag:#{request.host},#{options[:schema_date]}:#{request.fullpath.split(".")[0]}") + xml.link(:rel => 'alternate', :type => 'text/html', :href => options[:root_url] || (request.protocol + request.host_with_port)) + xml.link(:rel => 'self', :type => 'application/atom+xml', :href => options[:url] || request.url) + + yield AtomFeedBuilder.new(xml, self, options) + end + end + + class AtomBuilder #:nodoc: + XHTML_TAG_NAMES = %w(content rights title subtitle summary).to_set + + def initialize(xml) + @xml = xml + end + + private + # Delegate to xml builder, first wrapping the element in a xhtml + # namespaced div element if the method and arguments indicate + # that an xhtml_block? is desired. + def method_missing(method, *arguments, &block) + if xhtml_block?(method, arguments) + @xml.__send__(method, *arguments) do + @xml.div(:xmlns => 'http://www.w3.org/1999/xhtml') do |xhtml| + block.call(xhtml) + end + end + else + @xml.__send__(method, *arguments, &block) + end + end + + # True if the method name matches one of the five elements defined + # in the Atom spec as potentially containing XHTML content and + # if type: 'xhtml' is, in fact, specified. + def xhtml_block?(method, arguments) + if XHTML_TAG_NAMES.include?(method.to_s) + last = arguments.last + last.is_a?(Hash) && last[:type].to_s == 'xhtml' + end + end + end + + class AtomFeedBuilder < AtomBuilder #:nodoc: + def initialize(xml, view, feed_options = {}) + @xml, @view, @feed_options = xml, view, feed_options + end + + # Accepts a Date or Time object and inserts it in the proper format. If nil is passed, current time in UTC is used. + def updated(date_or_time = nil) + @xml.updated((date_or_time || Time.now.utc).xmlschema) + end + + # Creates an entry tag for a specific record and prefills the id using class and id. + # + # Options: + # + # * <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>: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 = {}) + @xml.entry do + @xml.id(options[:id] || "tag:#{@view.request.host},#{@feed_options[:schema_date]}:#{record.class}/#{record.id}") + + if options[:published] || (record.respond_to?(:created_at) && record.created_at) + @xml.published((options[:published] || record.created_at).xmlschema) + end + + if options[:updated] || (record.respond_to?(:updated_at) && record.updated_at) + @xml.updated((options[:updated] || record.updated_at).xmlschema) + end + + type = options.fetch(:type, 'text/html') + + @xml.link(:rel => 'alternate', :type => type, :href => options[:url] || @view.polymorphic_url(record)) + + yield AtomBuilder.new(@xml) + end + end + end + + end + end +end diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb new file mode 100644 index 0000000000..8fc78ea7fb --- /dev/null +++ b/actionview/lib/action_view/helpers/cache_helper.rb @@ -0,0 +1,196 @@ +module ActionView + # = Action View Cache Helper + module Helpers + module CacheHelper + # This helper exposes a method for caching fragments of a view + # rather than an entire action or page. This technique is useful + # caching pieces like menus, lists of newstopics, static HTML + # fragments, and so on. This method takes a block that contains + # the content you wish to cache. + # + # The best way to use this is by doing key-based cache expiration + # on top of a cache store like Memcached that'll automatically + # kick out old entries. For more on key-based expiration, see: + # http://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works + # + # When using this method, you list the cache dependency as the name of the cache, like so: + # + # <% cache project do %> + # <b>All the topics on this project</b> + # <%= render project.topics %> + # <% end %> + # + # This approach will assume that when a new topic is added, you'll touch + # the project. The cache key generated from this call will be something like: + # + # views/projects/123-20120806214154/7a1156131a6928cb0026877f8b749ac9 + # ^class ^id ^updated_at ^template tree digest + # + # The cache is thus automatically bumped whenever the project updated_at is touched. + # + # If your template cache depends on multiple sources (try to avoid this to keep things simple), + # you can name all these dependencies as part of an array: + # + # <% cache [ project, current_user ] do %> + # <b>All the topics on this project</b> + # <%= render project.topics %> + # <% end %> + # + # This will include both records as part of the cache key and updating either of them will + # expire the cache. + # + # ==== 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 + # expire when you change the template file. + # + # Note that the md5 is taken of the entire template file, not just what's within the + # cache do/end call. So it's possible that changing something outside of that call will + # still expire the cache. + # + # Additionally, the digestor will automatically look through your template file for + # explicit and implicit dependencies, and include those as part of the digest. + # + # The digestor can be bypassed by passing skip_digest: true as an option to the cache call: + # + # <% cache project, skip_digest: true do %> + # <b>All the topics on this project</b> + # <%= render project.topics %> + # <% end %> + # + # ==== Implicit dependencies + # + # Most template dependencies can be derived from calls to render in the template itself. + # Here are some examples of render calls that Cache Digests knows how to decode: + # + # render partial: "comments/comment", collection: commentable.comments + # render "comments/comments" + # render 'comments/comments' + # render('comments/comments') + # + # render "header" => render("comments/header") + # + # render(@topic) => render("topics/topic") + # 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: + # + # render group_of_attachments + # render @project.documents.where(published: true).order('created_at') + # + # You will have to rewrite those to the explicit form: + # + # render partial: 'attachments/attachment', collection: group_of_attachments + # render partial: 'documents/document', collection: @project.documents.where(published: true).order('created_at') + # + # === Explicit dependencies + # + # Some times you'll have template dependencies that can't be derived at all. This is typically + # the case when you have template rendering that happens in helpers. Here's an example: + # + # <%= render_sortable_todolists @project.todolists %> + # + # You'll need to use a special comment format to call those out: + # + # <%# 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. + # 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 + # 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 + safe_concat(fragment_for(cache_fragment_name(name, options), options, &block)) + else + yield + end + + nil + end + + # Cache fragments of a view if +condition+ is true + # + # <%= 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) + if condition + cache(name, options, &block) + else + yield + end + + nil + end + + # Cache fragments of a view unless +condition+ is true + # + # <%= 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) + 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 + # 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] + + if skip_digest + name + else + fragment_name_with_digest(name) + end + end + + private + + def fragment_name_with_digest(name) #:nodoc: + if @virtual_path + [ + *Array(name.is_a?(Hash) ? controller.url_for(name).split("://").last : name), + Digestor.digest(@virtual_path, formats.last.to_sym, lookup_context, dependencies: view_cache_dependencies) + ] + else + name + end + end + + # TODO: Create an object that has caching read/write on it + def fragment_for(name = {}, options = nil, &block) #:nodoc: + if fragment = controller.read_fragment(name, options) + fragment + else + # VIEW TODO: Make #capture usable outside of ERB + # This dance is needed because Builder can't use capture + pos = output_buffer.length + yield + output_safe = output_buffer.html_safe? + fragment = output_buffer.slice!(pos..-1) + if output_safe + self.output_buffer = output_buffer.class.new(output_buffer) + end + controller.write_fragment(name, fragment, options) + end + end + end + end +end diff --git a/actionview/lib/action_view/helpers/capture_helper.rb b/actionview/lib/action_view/helpers/capture_helper.rb new file mode 100644 index 0000000000..5afe435459 --- /dev/null +++ b/actionview/lib/action_view/helpers/capture_helper.rb @@ -0,0 +1,216 @@ +require 'active_support/core_ext/string/output_safety' + +module ActionView + # = Action View Capture Helper + module Helpers + # CaptureHelper exposes methods to let you extract generated markup which + # can be used in other parts of a template or layout file. + # + # It provides a method to capture blocks into variables through capture and + # a way to capture a block of markup for use in a layout through content_for. + module CaptureHelper + # The capture method allows you to extract part of a template into a + # variable. You can then use this variable anywhere in your templates or layout. + # + # The capture method can be used in ERB templates... + # + # <% @greeting = capture do %> + # Welcome to my shiny new web page! The date and time is + # <%= Time.now %> + # <% end %> + # + # ...and Builder (RXML) templates. + # + # @timestamp = capture do + # "The current timestamp is #{Time.now}." + # end + # + # You can then use that variable anywhere else. For example: + # + # <html> + # <head><title><%= @greeting %></title></head> + # <body> + # <b><%= @greeting %></b> + # </body></html> + # + def capture(*args) + value = nil + buffer = with_output_buffer { value = yield(*args) } + if string = buffer.presence || value and string.is_a?(String) + ERB::Util.html_escape string + end + end + + # Calling content_for stores a block of markup in an identifier for later use. + # In order to access this stored content in other templates, helper modules + # or the layout, you would pass the identifier as an argument to <tt>content_for</tt>. + # + # Note: <tt>yield</tt> can still be used to retrieve the stored content, but calling + # <tt>yield</tt> doesn't work in helper modules, while <tt>content_for</tt> does. + # + # <% content_for :not_authorized do %> + # alert('You are not authorized to do that!') + # <% end %> + # + # You can then use <tt>content_for :not_authorized</tt> anywhere in your templates. + # + # <%= content_for :not_authorized if current_user.nil? %> + # + # This is equivalent to: + # + # <%= yield :not_authorized if current_user.nil? %> + # + # <tt>content_for</tt>, however, can also be used in helper modules. + # + # module StorageHelper + # def stored_content + # content_for(:storage) || "Your storage is empty" + # end + # end + # + # This helper works just like normal helpers. + # + # <%= stored_content %> + # + # You can also use the <tt>yield</tt> syntax alongside an existing call to + # <tt>yield</tt> in a layout. For example: + # + # <%# This is the layout %> + # <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> + # <head> + # <title>My Website</title> + # <%= yield :script %> + # </head> + # <body> + # <%= yield %> + # </body> + # </html> + # + # And now, we'll create a view that has a <tt>content_for</tt> call that + # creates the <tt>script</tt> identifier. + # + # <%# This is our view %> + # Please login! + # + # <% content_for :script do %> + # <script>alert('You are not authorized to view this page!')</script> + # <% end %> + # + # Then, in another view, you could to do something like this: + # + # <%= link_to 'Logout', action: 'logout', remote: true %> + # + # <% content_for :script do %> + # <%= javascript_include_tag :defaults %> + # <% end %> + # + # That will place +script+ tags for your default set of JavaScript files on the page; + # this technique is useful if you'll only be using these scripts in a few views. + # + # Note that content_for concatenates (default) the blocks it is given for a particular + # identifier in order. For example: + # + # <% content_for :navigation do %> + # <li><%= link_to 'Home', action: 'index' %></li> + # <% end %> + # + # And in other place: + # + # <% content_for :navigation do %> + # <li><%= link_to 'Login', action: 'login' %></li> + # <% end %> + # + # Then, in another template or layout, this code would render both links in order: + # + # <ul><%= content_for :navigation %></ul> + # + # If the flush parameter is true content_for replaces the blocks it is given. For example: + # + # <% content_for :navigation do %> + # <li><%= link_to 'Home', action: 'index' %></li> + # <% end %> + # + # <%# Add some other content, or use a different template: %> + # + # <% content_for :navigation, flush: true do %> + # <li><%= link_to 'Login', action: 'login' %></li> + # <% end %> + # + # Then, in another template or layout, this code would render only the last link: + # + # <ul><%= content_for :navigation %></ul> + # + # Lastly, simple content can be passed as a parameter: + # + # <% content_for :script, javascript_include_tag(:defaults) %> + # + # WARNING: content_for is ignored in caches. So you shouldn't use it for elements that will be fragment cached. + def content_for(name, content = nil, options = {}, &block) + if content || block_given? + if block_given? + options = content if content + content = capture(&block) + end + if content + options[:flush] ? @view_flow.set(name, content) : @view_flow.append(name, content) + end + nil + else + @view_flow.get(name).presence + end + end + + # The same as +content_for+ but when used with streaming flushes + # straight back to the layout. In other words, if you want to + # concatenate several times to the same buffer when rendering a given + # template, you should use +content_for+, if not, use +provide+ to tell + # the layout to stop looking for more contents. + def provide(name, content = nil, &block) + content = capture(&block) if block_given? + result = @view_flow.append!(name, content) if content + result unless content + end + + # content_for? checks whether any content has been captured yet using `content_for`. + # Useful to render parts of your layout differently based on what is in your views. + # + # <%# This is the layout %> + # <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> + # <head> + # <title>My Website</title> + # <%= yield :script %> + # </head> + # <body class="<%= content_for?(:right_col) ? 'two-column' : 'one-column' %>"> + # <%= yield %> + # <%= yield :right_col %> + # </body> + # </html> + def content_for?(name) + @view_flow.get(name).present? + end + + # Use an alternate output buffer for the duration of the block. + # Defaults to a new empty string. + def with_output_buffer(buf = nil) #:nodoc: + unless buf + buf = ActionView::OutputBuffer.new + buf.force_encoding(output_buffer.encoding) if output_buffer + end + self.output_buffer, old_buffer = buf, output_buffer + yield + output_buffer + ensure + self.output_buffer = old_buffer + end + + # Add the output buffer to the response body and start a new one. + def flush_output_buffer #:nodoc: + if output_buffer && !output_buffer.empty? + response.stream.write output_buffer + self.output_buffer = output_buffer.respond_to?(:clone_empty) ? output_buffer.clone_empty : output_buffer[0, 0] + nil + end + end + end + end +end diff --git a/actionview/lib/action_view/helpers/controller_helper.rb b/actionview/lib/action_view/helpers/controller_helper.rb new file mode 100644 index 0000000000..74ef25f7c1 --- /dev/null +++ b/actionview/lib/action_view/helpers/controller_helper.rb @@ -0,0 +1,25 @@ +require 'active_support/core_ext/module/attr_internal' + +module ActionView + module Helpers + # This module keeps all methods and behavior in ActionView + # that simply delegates to the controller. + module ControllerHelper #:nodoc: + attr_internal :controller, :request + + delegate :request_forgery_protection_token, :params, :session, :cookies, :response, :headers, + :flash, :action_name, :controller_name, :controller_path, :to => :controller + + def assign_controller(controller) + if @_controller = controller + @_request = controller.request if controller.respond_to?(:request) + @_config = controller.config.inheritable_copy if controller.respond_to?(:config) + end + end + + def logger + controller.logger if controller.respond_to?(:logger) + end + end + end +end diff --git a/actionview/lib/action_view/helpers/csrf_helper.rb b/actionview/lib/action_view/helpers/csrf_helper.rb new file mode 100644 index 0000000000..eeb0ed94b9 --- /dev/null +++ b/actionview/lib/action_view/helpers/csrf_helper.rb @@ -0,0 +1,30 @@ +module ActionView + # = Action View CSRF Helper + module Helpers + module CsrfHelper + # Returns meta tags "csrf-param" and "csrf-token" with the name of the cross-site + # request forgery protection parameter and token, respectively. + # + # <head> + # <%= csrf_meta_tags %> + # </head> + # + # These are used to generate the dynamic forms that implement non-remote links with + # <tt>:method</tt>. + # + # Note that regular forms generate hidden fields, and that Ajax calls are whitelisted, + # so they do not use these tags. + def csrf_meta_tags + if protect_against_forgery? + [ + tag('meta', :name => 'csrf-param', :content => request_forgery_protection_token), + tag('meta', :name => 'csrf-token', :content => form_authenticity_token) + ].join("\n").html_safe + end + end + + # For backwards compatibility. + alias csrf_meta_tag csrf_meta_tags + end + end +end diff --git a/actionview/lib/action_view/helpers/date_helper.rb b/actionview/lib/action_view/helpers/date_helper.rb new file mode 100644 index 0000000000..8fb5eb1548 --- /dev/null +++ b/actionview/lib/action_view/helpers/date_helper.rb @@ -0,0 +1,1083 @@ +require 'date' +require 'action_view/helpers/tag_helper' +require 'active_support/core_ext/array/extract_options' +require 'active_support/core_ext/date/conversions' +require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/object/with_options' + +module ActionView + module Helpers + # = Action View Date Helpers + # + # The Date Helper primarily creates select/option tags for different kinds of dates and times or date and time + # elements. All of the select-type methods share a number of common options that are as follows: + # + # * <tt>:prefix</tt> - overwrites the default prefix of "date" used for the select names. So specifying "birthday" + # would give \birthday[month] instead of \date[month] if passed to the <tt>select_month</tt> method. + # * <tt>:include_blank</tt> - set to true if it should be possible to set an empty date. + # * <tt>:discard_type</tt> - set to true if you want to discard the type part of the select name. If set to true, + # the <tt>select_month</tt> method would use simply "date" (which can be overwritten using <tt>:prefix</tt>) instead + # of \date[month]. + module DateHelper + # Reports the approximate distance in time between two Time, Date or DateTime objects or integers as seconds. + # Pass <tt>include_seconds: true</tt> if you want more detailed approximations when distance < 1 min, 29 secs. + # Distances are reported based on the following table: + # + # 0 <-> 29 secs # => less than a minute + # 30 secs <-> 1 min, 29 secs # => 1 minute + # 1 min, 30 secs <-> 44 mins, 29 secs # => [2..44] minutes + # 44 mins, 30 secs <-> 89 mins, 29 secs # => about 1 hour + # 89 mins, 30 secs <-> 23 hrs, 59 mins, 29 secs # => about [2..24] hours + # 23 hrs, 59 mins, 30 secs <-> 41 hrs, 59 mins, 29 secs # => 1 day + # 41 hrs, 59 mins, 30 secs <-> 29 days, 23 hrs, 59 mins, 29 secs # => [2..29] days + # 29 days, 23 hrs, 59 mins, 30 secs <-> 44 days, 23 hrs, 59 mins, 29 secs # => about 1 month + # 44 days, 23 hrs, 59 mins, 30 secs <-> 59 days, 23 hrs, 59 mins, 29 secs # => about 2 months + # 59 days, 23 hrs, 59 mins, 30 secs <-> 1 yr minus 1 sec # => [2..12] months + # 1 yr <-> 1 yr, 3 months # => about 1 year + # 1 yr, 3 months <-> 1 yr, 9 months # => over 1 year + # 1 yr, 9 months <-> 2 yr minus 1 sec # => almost 2 years + # 2 yrs <-> max time or date # => (same rules as 1 yr) + # + # With <tt>include_seconds: true</tt> and the difference < 1 minute 29 seconds: + # 0-4 secs # => less than 5 seconds + # 5-9 secs # => less than 10 seconds + # 10-19 secs # => less than 20 seconds + # 20-39 secs # => half a minute + # 40-59 secs # => less than a minute + # 60-89 secs # => 1 minute + # + # from_time = Time.now + # distance_of_time_in_words(from_time, from_time + 50.minutes) # => about 1 hour + # distance_of_time_in_words(from_time, 50.minutes.from_now) # => about 1 hour + # distance_of_time_in_words(from_time, from_time + 15.seconds) # => less than a minute + # distance_of_time_in_words(from_time, from_time + 15.seconds, include_seconds: true) # => less than 20 seconds + # distance_of_time_in_words(from_time, 3.years.from_now) # => about 3 years + # distance_of_time_in_words(from_time, from_time + 60.hours) # => 3 days + # distance_of_time_in_words(from_time, from_time + 45.seconds, include_seconds: true) # => less than a minute + # distance_of_time_in_words(from_time, from_time - 45.seconds, include_seconds: true) # => less than a minute + # distance_of_time_in_words(from_time, 76.seconds.from_now) # => 1 minute + # distance_of_time_in_words(from_time, from_time + 1.year + 3.days) # => about 1 year + # distance_of_time_in_words(from_time, from_time + 3.years + 6.months) # => over 3 years + # distance_of_time_in_words(from_time, from_time + 4.years + 9.days + 30.minutes + 5.seconds) # => about 4 years + # + # to_time = Time.now + 6.years + 19.days + # 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 + def distance_of_time_in_words(from_time, to_time = 0, include_seconds_or_options = {}, options = {}) + if include_seconds_or_options.is_a?(Hash) + options = include_seconds_or_options + else + ActiveSupport::Deprecation.warn "distance_of_time_in_words and time_ago_in_words now accept :include_seconds " + + "as a part of options hash, not a boolean argument" + options[:include_seconds] ||= !!include_seconds_or_options + end + + options = { + scope: :'datetime.distance_in_words' + }.merge!(options) + + from_time = from_time.to_time if from_time.respond_to?(:to_time) + to_time = to_time.to_time if to_time.respond_to?(:to_time) + from_time, to_time = to_time, from_time if from_time > to_time + distance_in_minutes = ((to_time - from_time)/60.0).round + distance_in_seconds = (to_time - from_time).round + + I18n.with_options :locale => options[:locale], :scope => options[:scope] do |locale| + case distance_in_minutes + when 0..1 + return distance_in_minutes == 0 ? + locale.t(:less_than_x_minutes, :count => 1) : + locale.t(:x_minutes, :count => distance_in_minutes) unless options[:include_seconds] + + case distance_in_seconds + when 0..4 then locale.t :less_than_x_seconds, :count => 5 + when 5..9 then locale.t :less_than_x_seconds, :count => 10 + when 10..19 then locale.t :less_than_x_seconds, :count => 20 + when 20..39 then locale.t :half_a_minute + when 40..59 then locale.t :less_than_x_minutes, :count => 1 + else locale.t :x_minutes, :count => 1 + end + + when 2...45 then locale.t :x_minutes, :count => distance_in_minutes + when 45...90 then locale.t :about_x_hours, :count => 1 + # 90 mins up to 24 hours + when 90...1440 then locale.t :about_x_hours, :count => (distance_in_minutes.to_f / 60.0).round + # 24 hours up to 42 hours + when 1440...2520 then locale.t :x_days, :count => 1 + # 42 hours up to 30 days + when 2520...43200 then locale.t :x_days, :count => (distance_in_minutes.to_f / 1440.0).round + # 30 days up to 60 days + when 43200...86400 then locale.t :about_x_months, :count => (distance_in_minutes.to_f / 43200.0).round + # 60 days up to 365 days + when 86400...525600 then locale.t :x_months, :count => (distance_in_minutes.to_f / 43200.0).round + else + if from_time.acts_like?(:time) && to_time.acts_like?(:time) + fyear = from_time.year + fyear += 1 if from_time.month >= 3 + tyear = to_time.year + tyear -= 1 if to_time.month < 3 + leap_years = (fyear > tyear) ? 0 : (fyear..tyear).count{|x| Date.leap?(x)} + minute_offset_for_leap_year = leap_years * 1440 + # Discount the leap year days when calculating year distance. + # e.g. if there are 20 leap year days between 2 dates having the same day + # and month then the based on 365 days calculation + # the distance in years will come out to over 80 years when in written + # english it would read better as about 80 years. + minutes_with_offset = distance_in_minutes - minute_offset_for_leap_year + else + minutes_with_offset = distance_in_minutes + end + remainder = (minutes_with_offset % 525600) + distance_in_years = (minutes_with_offset.div 525600) + if remainder < 131400 + locale.t(:about_x_years, :count => distance_in_years) + elsif remainder < 394200 + locale.t(:over_x_years, :count => distance_in_years) + else + locale.t(:almost_x_years, :count => distance_in_years + 1) + end + end + end + end + + # Like <tt>distance_of_time_in_words</tt>, but where <tt>to_time</tt> is fixed to <tt>Time.now</tt>. + # + # time_ago_in_words(3.minutes.from_now) # => 3 minutes + # time_ago_in_words(3.minutes.ago) # => 3 minutes + # time_ago_in_words(Time.now - 15.hours) # => about 15 hours + # time_ago_in_words(Time.now) # => less than a minute + # time_ago_in_words(Time.now, include_seconds: true) # => less than 5 seconds + # + # from_time = Time.now - 3.days - 14.minutes - 25.seconds + # time_ago_in_words(from_time) # => 3 days + # + # from_time = (3.days + 14.minutes + 25.seconds).ago + # time_ago_in_words(from_time) # => 3 days + # + # Note that you cannot pass a <tt>Numeric</tt> value to <tt>time_ago_in_words</tt>. + # + def time_ago_in_words(from_time, include_seconds_or_options = {}) + distance_of_time_in_words(from_time, Time.now, include_seconds_or_options) + end + + alias_method :distance_of_time_in_words_to_now, :time_ago_in_words + + # Returns a set of select tags (one for year, month, and day) pre-selected for accessing a specified date-based + # attribute (identified by +method+) on an object assigned to the template (identified by +object+). + # + # ==== Options + # * <tt>:use_month_numbers</tt> - Set to true if you want to use month numbers rather than month names (e.g. + # "2" instead of "February"). + # * <tt>:use_two_digit_numbers</tt> - Set to true if you want to display two digit month and day numbers (e.g. + # "02" instead of "February" and "08" instead of "8"). + # * <tt>:use_short_month</tt> - Set to true if you want to use abbreviated month names instead of full + # month names (e.g. "Feb" instead of "February"). + # * <tt>:add_month_numbers</tt> - Set to true if you want to use both month numbers and month names (e.g. + # "2 - February" instead of "February"). + # * <tt>:use_month_names</tt> - Set to an array with 12 month names if you want to customize month names. + # Note: You can also use Rails' i18n functionality for this. + # * <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>Time.now.year - 5</tt>. + # * <tt>:end_year</tt> - Set the end year for the year select. Default is <tt>Time.now.year + 5</tt>. + # * <tt>:discard_day</tt> - Set to true if you don't want to show a day select. This includes the day + # as a hidden field instead of showing a select field. Also note that this implicitly sets the day to be the + # first of the given month in order to not create invalid dates like 31 February. + # * <tt>:discard_month</tt> - Set to true if you don't want to show a month select. This includes the month + # as a hidden field instead of showing a select field. Also note that this implicitly sets :discard_day to true. + # * <tt>:discard_year</tt> - Set to true if you don't want to show a year select. This includes the year + # as a hidden field instead of showing a select field. + # * <tt>:order</tt> - Set to an array containing <tt>:day</tt>, <tt>:month</tt> and <tt>:year</tt> to + # customize the order in which the select fields are shown. If you leave out any of the symbols, the respective + # select will not be shown (like when you set <tt>discard_xxx: true</tt>. Defaults to the order defined in + # the respective locale (e.g. [:year, :month, :day] in the en locale that ships with Rails). + # * <tt>:include_blank</tt> - Include a blank option in every select field so it's possible to set empty + # dates. + # * <tt>:default</tt> - Set a default date if the affected date isn't set or is nil. + # * <tt>:selected</tt> - Set a date that overrides the actual value. + # * <tt>:disabled</tt> - Set to true if you want show the select fields as disabled. + # * <tt>:prompt</tt> - Set to true (for a generic prompt), a prompt string or a hash of prompt strings + # for <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>, <tt>:hour</tt>, <tt>:minute</tt> and <tt>:second</tt>. + # Setting this option prepends a select option with a generic prompt (Day, Month, Year, Hour, Minute, Seconds) + # 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. + # + # If anything is passed in the +html_options+ hash it will be applied to every select tag in the set. + # + # NOTE: Discarded selects will default to 1. So if no month select is available, January will be assumed. + # + # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute. + # date_select("article", "written_on") + # + # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute, + # # with the year in the year drop down box starting at 1995. + # date_select("article", "written_on", start_year: 1995) + # + # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute, + # # with the year in the year drop down box starting at 1995, numbers used for months instead of words, + # # and without a day select box. + # date_select("article", "written_on", start_year: 1995, use_month_numbers: true, + # discard_day: true, include_blank: true) + # + # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute, + # # with two digit numbers used for months and days. + # date_select("article", "written_on", use_two_digit_numbers: true) + # + # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute + # # with the fields ordered as day, month, year rather than month, day, year. + # date_select("article", "written_on", order: [:day, :month, :year]) + # + # # Generates a date select that when POSTed is stored in the user variable, in the birthday attribute + # # lacking a year field. + # date_select("user", "birthday", order: [:month, :day]) + # + # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute + # # which is initially set to the date 3 days from the current date + # date_select("article", "written_on", default: 3.days.from_now) + # + # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute + # # which is set in the form with todays date, regardless of the value in the Active Record object. + # date_select("article", "written_on", selected: Date.today) + # + # # Generates a date select that when POSTed is stored in the credit_card variable, in the bill_due attribute + # # that will have a default day of 20. + # date_select("credit_card", "bill_due", default: { day: 20 }) + # + # # Generates a date select with custom prompts. + # date_select("article", "written_on", prompt: { day: 'Select day', month: 'Select month', year: 'Select year' }) + # + # The selects are prepared for multi-parameter assignment to an Active Record object. + # + # Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that + # all month choices are valid. + def date_select(object_name, method, options = {}, html_options = {}) + Tags::DateSelect.new(object_name, method, self, options, html_options).render + end + + # Returns a set of select tags (one for hour, minute and optionally second) pre-selected for accessing a + # specified time-based attribute (identified by +method+) on an object assigned to the template (identified by + # +object+). You can include the seconds with <tt>:include_seconds</tt>. You can get hours in the AM/PM format + # with <tt>:ampm</tt> option. + # + # This method will also generate 3 input hidden tags, for the actual year, month and day unless the option + # <tt>:ignore_date</tt> is set to +true+. If you set the <tt>:ignore_date</tt> to +true+, you must have a + # +date_select+ on the same method within the form otherwise an exception will be raised. + # + # If anything is passed in the html_options hash it will be applied to every select tag in the set. + # + # # Creates a time select tag that, when POSTed, will be stored in the article variable in the sunrise attribute. + # time_select("article", "sunrise") + # + # # Creates a time select tag with a seconds field that, when POSTed, will be stored in the article variables in + # # the sunrise attribute. + # time_select("article", "start_time", include_seconds: true) + # + # # You can set the <tt>:minute_step</tt> to 15 which will give you: 00, 15, 30 and 45. + # time_select 'game', 'game_time', {minute_step: 15} + # + # # Creates a time select tag with a custom prompt. Use <tt>prompt: true</tt> for generic prompts. + # time_select("article", "written_on", prompt: {hour: 'Choose hour', minute: 'Choose minute', second: 'Choose seconds'}) + # time_select("article", "written_on", prompt: {hour: true}) # generic prompt for hours + # time_select("article", "written_on", prompt: true) # generic prompts for all + # + # # You can set :ampm option to true which will show the hours as: 12 PM, 01 AM .. 11 PM. + # time_select 'game', 'game_time', {ampm: true} + # + # The selects are prepared for multi-parameter assignment to an Active Record object. + # + # Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that + # all month choices are valid. + def time_select(object_name, method, options = {}, html_options = {}) + Tags::TimeSelect.new(object_name, method, self, options, html_options).render + end + + # Returns a set of select tags (one for year, month, day, hour, and minute) pre-selected for accessing a + # specified datetime-based attribute (identified by +method+) on an object assigned to the template (identified + # by +object+). + # + # If anything is passed in the html_options hash it will be applied to every select tag in the set. + # + # # Generates a datetime select that, when POSTed, will be stored in the article variable in the written_on + # # attribute. + # datetime_select("article", "written_on") + # + # # Generates a datetime select with a year select that starts at 1995 that, when POSTed, will be stored in the + # # article variable in the written_on attribute. + # datetime_select("article", "written_on", start_year: 1995) + # + # # Generates a datetime select with a default value of 3 days from the current time that, when POSTed, will + # # be stored in the trip variable in the departing attribute. + # datetime_select("trip", "departing", default: 3.days.from_now) + # + # # Generate a datetime select with hours in the AM/PM format + # datetime_select("article", "written_on", ampm: true) + # + # # Generates a datetime select that discards the type that, when POSTed, will be stored in the article variable + # # as the written_on attribute. + # datetime_select("article", "written_on", discard_type: true) + # + # # Generates a datetime select with a custom prompt. Use <tt>prompt: true</tt> for generic prompts. + # datetime_select("article", "written_on", prompt: {day: 'Choose day', month: 'Choose month', year: 'Choose year'}) + # datetime_select("article", "written_on", prompt: {hour: true}) # generic prompt for hours + # datetime_select("article", "written_on", prompt: true) # generic prompts for all + # + # The selects are prepared for multi-parameter assignment to an Active Record object. + def datetime_select(object_name, method, options = {}, html_options = {}) + 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 + # +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 + # <tt>:date_separator</tt>, <tt>:datetime_separator</tt> and <tt>:time_separator</tt> keys to the +options+ to + # control visual display of the elements. + # + # If anything is passed in the html_options hash it will be applied to every select tag in the set. + # + # my_date_time = Time.now + 4.days + # + # # Generates a datetime select that defaults to the datetime in my_date_time (four days after today). + # select_datetime(my_date_time) + # + # # Generates a datetime select that defaults to today (no specified datetime) + # select_datetime() + # + # # Generates a datetime select that defaults to the datetime in my_date_time (four days after today) + # # with the fields ordered year, month, day rather than month, day, year. + # select_datetime(my_date_time, order: [:year, :month, :day]) + # + # # Generates a datetime select that defaults to the datetime in my_date_time (four days after today) + # # with a '/' between each date field. + # select_datetime(my_date_time, date_separator: '/') + # + # # Generates a datetime select that defaults to the datetime in my_date_time (four days after today) + # # with a date fields separated by '/', time fields separated by '' and the date and time fields + # # separated by a comma (','). + # select_datetime(my_date_time, date_separator: '/', time_separator: '', datetime_separator: ',') + # + # # Generates a datetime select that discards the type of the field and defaults to the datetime in + # # my_date_time (four days after today) + # select_datetime(my_date_time, discard_type: true) + # + # # Generate a datetime field with hours in the AM/PM format + # select_datetime(my_date_time, ampm: true) + # + # # Generates a datetime select that defaults to the datetime in my_date_time (four days after today) + # # prefixed with 'payday' rather than 'date' + # select_datetime(my_date_time, prefix: 'payday') + # + # # Generates a datetime select with a custom prompt. Use <tt>prompt: true</tt> for generic prompts. + # select_datetime(my_date_time, prompt: {day: 'Choose day', month: 'Choose month', year: 'Choose year'}) + # select_datetime(my_date_time, prompt: {hour: true}) # generic prompt for hours + # select_datetime(my_date_time, prompt: true) # generic prompts for all + def select_datetime(datetime = Time.current, options = {}, html_options = {}) + 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+. + # 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. + # + # If anything is passed in the html_options hash it will be applied to every select tag in the set. + # + # my_date = Time.now + 6.days + # + # # Generates a date select that defaults to the date in my_date (six days after today). + # select_date(my_date) + # + # # Generates a date select that defaults to today (no specified date). + # select_date() + # + # # Generates a date select that defaults to the date in my_date (six days after today) + # # with the fields ordered year, month, day rather than month, day, year. + # select_date(my_date, order: [:year, :month, :day]) + # + # # Generates a date select that discards the type of the field and defaults to the date in + # # my_date (six days after today). + # select_date(my_date, discard_type: true) + # + # # Generates a date select that defaults to the date in my_date, + # # which has fields separated by '/'. + # select_date(my_date, date_separator: '/') + # + # # Generates a date select that defaults to the datetime in my_date (six days after today) + # # prefixed with 'payday' rather than 'date'. + # select_date(my_date, prefix: 'payday') + # + # # Generates a date select with a custom prompt. Use <tt>prompt: true</tt> for generic prompts. + # select_date(my_date, prompt: {day: 'Choose day', month: 'Choose month', year: 'Choose year'}) + # select_date(my_date, prompt: {hour: true}) # generic prompt for hours + # select_date(my_date, prompt: true) # generic prompts for all + def select_date(date = Date.current, options = {}, html_options = {}) + DateTimeSelector.new(date, options, html_options).select_date + end + + # 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. + # + # If anything is passed in the html_options hash it will be applied to every select tag in the set. + # + # my_time = Time.now + 5.days + 7.hours + 3.minutes + 14.seconds + # + # # Generates a time select that defaults to the time in my_time. + # select_time(my_time) + # + # # Generates a time select that defaults to the current time (no specified time). + # select_time() + # + # # Generates a time select that defaults to the time in my_time, + # # which has fields separated by ':'. + # select_time(my_time, time_separator: ':') + # + # # Generates a time select that defaults to the time in my_time, + # # that also includes an input for seconds. + # select_time(my_time, include_seconds: true) + # + # # Generates a time select that defaults to the time in my_time, that has fields + # # separated by ':' and includes an input for seconds. + # select_time(my_time, time_separator: ':', include_seconds: true) + # + # # Generate a time select field with hours in the AM/PM format + # select_time(my_time, ampm: true) + # + # # Generates a time select field with hours that range from 2 to 14 + # select_time(my_time, start_hour: 2, end_hour: 14) + # + # # Generates a time select with a custom prompt. Use <tt>:prompt</tt> to true for generic prompts. + # select_time(my_time, prompt: {day: 'Choose day', month: 'Choose month', year: 'Choose year'}) + # select_time(my_time, prompt: {hour: true}) # generic prompt for hours + # select_time(my_time, prompt: true) # generic prompts for all + def select_time(datetime = Time.current, options = {}, html_options = {}) + DateTimeSelector.new(datetime, options, html_options).select_time + end + + # Returns a select tag with options for each of the seconds 0 through 59 with the current second 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, 'second' by default. + # + # my_time = Time.now + 16.minutes + # + # # Generates a select field for seconds that defaults to the seconds for the time in my_time. + # select_second(my_time) + # + # # Generates a select field for seconds that defaults to the number given. + # select_second(33) + # + # # Generates a select field for seconds that defaults to the seconds for the time in my_time + # # that is named 'interval' rather than 'second'. + # select_second(my_time, field_name: 'interval') + # + # # Generates a select field for seconds with a custom prompt. Use <tt>prompt: true</tt> for a + # # generic prompt. + # select_second(14, prompt: 'Choose seconds') + def select_second(datetime, options = {}, html_options = {}) + DateTimeSelector.new(datetime, options, html_options).select_second + end + + # Returns a select tag with options for each of the minutes 0 through 59 with the current minute selected. + # Also can return a select tag with options by <tt>minute_step</tt> from 0 through 59 with the 00 minute + # 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 + # + # # Generates a select field for minutes that defaults to the minutes for the time in my_time. + # select_minute(my_time) + # + # # Generates a select field for minutes that defaults to the number given. + # select_minute(14) + # + # # Generates a select field for minutes that defaults to the minutes for the time in my_time + # # that is named 'moment' rather than 'minute'. + # select_minute(my_time, field_name: 'moment') + # + # # Generates a select field for minutes with a custom prompt. Use <tt>prompt: true</tt> for a + # # generic prompt. + # select_minute(14, prompt: 'Choose minutes') + def select_minute(datetime, options = {}, html_options = {}) + DateTimeSelector.new(datetime, options, html_options).select_minute + end + + # Returns a select tag with options for each of the hours 0 through 23 with the current hour 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, 'hour' by default. + # + # my_time = Time.now + 6.hours + # + # # Generates a select field for hours that defaults to the hour for the time in my_time. + # select_hour(my_time) + # + # # Generates a select field for hours that defaults to the number given. + # select_hour(13) + # + # # Generates a select field for hours that defaults to the hour for the time in my_time + # # that is named 'stride' rather than 'hour'. + # select_hour(my_time, field_name: 'stride') + # + # # Generates a select field for hours with a custom prompt. Use <tt>prompt: true</tt> for a + # # generic prompt. + # select_hour(13, prompt: 'Choose hour') + # + # # Generate a select field for hours in the AM/PM format + # select_hour(my_time, ampm: true) + # + # # Generates a select field that includes options for hours from 2 to 14. + # select_hour(my_time, start_hour: 2, end_hour: 14) + def select_hour(datetime, options = {}, html_options = {}) + DateTimeSelector.new(datetime, options, html_options).select_hour + end + + # Returns a select tag with options for each of the days 1 through 31 with the current day selected. + # The <tt>date</tt> can also be substituted for a day number. + # If you want to display days with a leading zero set the <tt>:use_two_digit_numbers</tt> key in +options+ to true. + # Override the field name using the <tt>:field_name</tt> option, 'day' by default. + # + # my_date = Time.now + 2.days + # + # # Generates a select field for days that defaults to the day for the date in my_date. + # select_day(my_time) + # + # # Generates a select field for days that defaults to the number given. + # select_day(5) + # + # # Generates a select field for days that defaults to the number given, but displays it with two digits. + # select_day(5, use_two_digit_numbers: true) + # + # # Generates a select field for days that defaults to the day for the date in my_date + # # that is named 'due' rather than 'day'. + # select_day(my_time, field_name: 'due') + # + # # Generates a select field for days with a custom prompt. Use <tt>prompt: true</tt> for a + # # generic prompt. + # select_day(5, prompt: 'Choose day') + def select_day(date, options = {}, html_options = {}) + DateTimeSelector.new(date, options, html_options).select_day + end + + # Returns a select tag with options for each of the months January through December with the current month + # selected. The month names are presented as keys (what's shown to the user) and the month numbers (1-12) are + # used as values (what's submitted to the server). It's also possible to use month numbers for the presentation + # instead of names -- set the <tt>:use_month_numbers</tt> key in +options+ to true for this to happen. If you + # want both numbers and names, set the <tt>:add_month_numbers</tt> key in +options+ to true. If you would prefer + # to show month names as abbreviations, set the <tt>:use_short_month</tt> key in +options+ to true. If you want + # to use your own month names, set the <tt>:use_month_names</tt> key in +options+ to an array of 12 month names. + # If you want to display months with a leading zero set the <tt>:use_two_digit_numbers</tt> key in +options+ to true. + # Override the field name using the <tt>:field_name</tt> option, 'month' by default. + # + # # Generates a select field for months that defaults to the current month that + # # will use keys like "January", "March". + # select_month(Date.today) + # + # # Generates a select field for months that defaults to the current month that + # # is named "start" rather than "month". + # select_month(Date.today, field_name: 'start') + # + # # Generates a select field for months that defaults to the current month that + # # will use keys like "1", "3". + # select_month(Date.today, use_month_numbers: true) + # + # # Generates a select field for months that defaults to the current month that + # # will use keys like "1 - January", "3 - March". + # select_month(Date.today, add_month_numbers: true) + # + # # Generates a select field for months that defaults to the current month that + # # will use keys like "Jan", "Mar". + # select_month(Date.today, use_short_month: true) + # + # # Generates a select field for months that defaults to the current month that + # # will use keys like "Januar", "Marts." + # select_month(Date.today, use_month_names: %w(Januar Februar Marts ...)) + # + # # Generates a select field for months that defaults to the current month that + # # will use keys with two digit numbers like "01", "03". + # select_month(Date.today, use_two_digit_numbers: true) + # + # # Generates a select field for months with a custom prompt. Use <tt>prompt: true</tt> for a + # # generic prompt. + # select_month(14, prompt: 'Choose month') + def select_month(date, options = {}, html_options = {}) + DateTimeSelector.new(date, options, html_options).select_month + end + + # Returns a select tag with options for each of the five years on each side of the current, which is selected. + # The five year radius can be changed using the <tt>:start_year</tt> and <tt>:end_year</tt> keys in the + # +options+. Both ascending and descending year lists are supported by making <tt>:start_year</tt> less than or + # greater than <tt>:end_year</tt>. The <tt>date</tt> can also be substituted for a year given as a number. + # Override the field name using the <tt>:field_name</tt> option, 'year' by default. + # + # # Generates a select field for years that defaults to the current year that + # # has ascending year values. + # select_year(Date.today, start_year: 1992, end_year: 2007) + # + # # Generates a select field for years that defaults to the current year that + # # is named 'birth' rather than 'year'. + # select_year(Date.today, field_name: 'birth') + # + # # Generates a select field for years that defaults to the current year that + # # has descending year values. + # select_year(Date.today, start_year: 2005, end_year: 1900) + # + # # Generates a select field for years that defaults to the year 2006 that + # # has ascending year values. + # select_year(2006, start_year: 2000, end_year: 2010) + # + # # Generates a select field for years with a custom prompt. Use <tt>prompt: true</tt> for a + # # generic prompt. + # select_year(14, prompt: 'Choose year') + def select_year(date, options = {}, html_options = {}) + DateTimeSelector.new(date, options, html_options).select_year + end + + # Returns an html time tag for the given date or time. + # + # time_tag Date.today # => + # <time datetime="2010-11-04">November 04, 2010</time> + # time_tag Time.now # => + # <time datetime="2010-11-04T17:55:45+01:00">November 04, 2010 17:55</time> + # time_tag Date.yesterday, 'Yesterday' # => + # <time datetime="2010-11-03">Yesterday</time> + # time_tag Date.today, pubdate: true # => + # <time datetime="2010-11-04" pubdate="pubdate">November 04, 2010</time> + # time_tag Date.today, datetime: Date.today.strftime('%G-W%V') # => + # <time datetime="2010-W44">November 04, 2010</time> + # + # <%= time_tag Time.now do %> + # <span>Right now</span> + # <% end %> + # # => <time datetime="2010-11-04T17:55:45+01:00"><span>Right now</span></time> + def time_tag(date_or_time, *args, &block) + options = args.extract_options! + format = options.delete(:format) || :long + 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) + end + end + + class DateTimeSelector #:nodoc: + include ActionView::Helpers::TagHelper + + DEFAULT_PREFIX = 'date'.freeze + POSITION = { + :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6 + }.freeze + + AMPM_TRANSLATION = Hash[ + [[0, "12 AM"], [1, "01 AM"], [2, "02 AM"], [3, "03 AM"], + [4, "04 AM"], [5, "05 AM"], [6, "06 AM"], [7, "07 AM"], + [8, "08 AM"], [9, "09 AM"], [10, "10 AM"], [11, "11 AM"], + [12, "12 PM"], [13, "01 PM"], [14, "02 PM"], [15, "03 PM"], + [16, "04 PM"], [17, "05 PM"], [18, "06 PM"], [19, "07 PM"], + [20, "08 PM"], [21, "09 PM"], [22, "10 PM"], [23, "11 PM"]] + ].freeze + + def initialize(datetime, options = {}, html_options = {}) + @options = options.dup + @html_options = html_options.dup + @datetime = datetime + @options[:datetime_separator] ||= ' — ' + @options[:time_separator] ||= ' : ' + end + + def select_datetime + order = date_order.dup + order -= [:hour, :minute, :second] + @options[:discard_year] ||= true unless order.include?(:year) + @options[:discard_month] ||= true unless order.include?(:month) + @options[:discard_day] ||= true if @options[:discard_month] || !order.include?(:day) + @options[:discard_minute] ||= true if @options[:discard_hour] + @options[:discard_second] ||= true unless @options[:include_seconds] && !@options[:discard_minute] + + set_day_if_discarded + + if @options[:tag] && @options[:ignore_date] + select_time + else + [:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) } + order += [:hour, :minute, :second] unless @options[:discard_hour] + + build_selects_from_types(order) + end + end + + def select_date + order = date_order.dup + + @options[:discard_hour] = true + @options[:discard_minute] = true + @options[:discard_second] = true + + @options[:discard_year] ||= true unless order.include?(:year) + @options[:discard_month] ||= true unless order.include?(:month) + @options[:discard_day] ||= true if @options[:discard_month] || !order.include?(:day) + + set_day_if_discarded + + [:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) } + + build_selects_from_types(order) + end + + def select_time + order = [] + + @options[:discard_month] = true + @options[:discard_year] = true + @options[:discard_day] = true + @options[:discard_second] ||= true unless @options[:include_seconds] + + order += [:year, :month, :day] unless @options[:ignore_date] + + order += [:hour, :minute] + order << :second if @options[:include_seconds] + + build_selects_from_types(order) + end + + def select_second + if @options[:use_hidden] || @options[:discard_second] + build_hidden(:second, sec) if @options[:include_seconds] + else + build_options_and_select(:second, sec) + end + end + + def select_minute + if @options[:use_hidden] || @options[:discard_minute] + build_hidden(:minute, min) + else + build_options_and_select(:minute, min, :step => @options[:minute_step]) + end + end + + def select_hour + if @options[:use_hidden] || @options[:discard_hour] + build_hidden(:hour, hour) + else + options = {} + options[:ampm] = @options[:ampm] || false + options[:start] = @options[:start_hour] || 0 + options[:end] = @options[:end_hour] || 23 + build_options_and_select(:hour, hour, options) + end + end + + def select_day + if @options[:use_hidden] || @options[:discard_day] + build_hidden(:day, day || 1) + else + build_options_and_select(:day, day, :start => 1, :end => 31, :leading_zeros => false, :use_two_digit_numbers => @options[:use_two_digit_numbers]) + end + end + + def select_month + if @options[:use_hidden] || @options[:discard_month] + build_hidden(:month, month || 1) + else + month_options = [] + 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" + end + build_select(:month, month_options.join) + end + end + + def select_year + if !@datetime || @datetime == 0 + val = '1' + middle_year = Date.today.year + else + val = middle_year = year + end + + if @options[:use_hidden] || @options[:discard_year] + build_hidden(:year, val) + else + options = {} + options[:start] = @options[:start_year] || middle_year - 5 + options[:end] = @options[:end_year] || middle_year + 5 + options[:step] = options[:start] < options[:end] ? 1 : -1 + options[:leading_zeros] = false + options[:max_years_allowed] = @options[:max_years_allowed] || 1000 + + if (options[:end] - options[:start]).abs > options[:max_years_allowed] + raise ArgumentError, "There are too many years options to be built. Are you sure you haven't mistyped something? You can provide the :max_years_allowed parameter." + end + + build_options_and_select(:year, val, options) + end + end + + private + %w( sec min hour day month year ).each do |method| + define_method(method) do + @datetime.kind_of?(Numeric) ? @datetime : @datetime.send(method) if @datetime + end + end + + # If the day is hidden, the day should be set to the 1st so all month and year choices are + # valid. Otherwise, February 31st or February 29th, 2011 can be selected, which are invalid. + def set_day_if_discarded + if @datetime && @options[:discard_day] + @datetime = @datetime.change(:day => 1) + end + end + + # Returns translated month names, but also ensures that a custom month + # name array has a leading nil element. + def month_names + @month_names ||= begin + month_names = @options[:use_month_names] || translated_month_names + month_names.unshift(nil) if month_names.size < 13 + month_names + end + end + + # Returns translated month names. + # => [nil, "January", "February", "March", + # "April", "May", "June", "July", + # "August", "September", "October", + # "November", "December"] + # + # If <tt>:use_short_month</tt> option is set + # => [nil, "Jan", "Feb", "Mar", "Apr", "May", "Jun", + # "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + def translated_month_names + key = @options[:use_short_month] ? :'date.abbr_month_names' : :'date.month_names' + I18n.translate(key, :locale => @options[:locale]) + end + + # Lookup month name for number. + # month_name(1) => "January" + # + # If <tt>:use_month_numbers</tt> option is passed + # month_name(1) => 1 + # + # If <tt>:use_two_month_numbers</tt> option is passed + # month_name(1) => '01' + # + # If <tt>:add_month_numbers</tt> option is passed + # month_name(1) => "1 - January" + def month_name(number) + if @options[:use_month_numbers] + number + elsif @options[:use_two_digit_numbers] + sprintf "%02d", number + elsif @options[:add_month_numbers] + "#{number} - #{month_names[number]}" + else + month_names[number] + end + end + + def date_order + @date_order ||= @options[:order] || translated_date_order + end + + def translated_date_order + date_order = I18n.translate(:'date.order', :locale => @options[:locale], :default => []) + date_order = date_order.map { |element| element.to_sym } + + forbidden_elements = date_order - [:year, :month, :day] + if forbidden_elements.any? + raise StandardError, + "#{@options[:locale]}.date.order only accepts :year, :month and :day" + end + + date_order + end + + # Build full select tag from date type and options. + def build_options_and_select(type, selected, options = {}) + build_select(type, build_options(selected, options)) + end + + # 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> + # <option value="3">3</option>..." + # + # If <tt>use_two_digit_numbers: true</tt> option is passed + # build_options(15, start: 1, end: 31, use_two_digit_numbers: true) + # => "<option value="1">01</option> + # <option value="2">02</option> + # <option value="3">03</option>..." + # + # If <tt>:step</tt> options is passed + # build_options(15, start: 1, end: 31, step: 2) + # => "<option value="1">1</option> + # <option value="3">3</option> + # <option value="5">5</option>..." + def build_options(selected, options = {}) + options = { + leading_zeros: true, ampm: false, use_two_digit_numbers: false + }.merge!(options) + + start = options.delete(:start) || 0 + stop = options.delete(:end) || 59 + step = options.delete(:step) || 1 + leading_zeros = options.delete(:leading_zeros) + + select_options = [] + start.step(stop, step) do |i| + value = leading_zeros ? sprintf("%02d", i) : i + tag_options = { :value => value } + 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) + end + + (select_options.join("\n") + "\n").html_safe + end + + # 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>... + # </select>" + def build_select(type, select_options_as_html) + select_options = { + :id => input_id_from_type(type), + :name => input_name_from_type(type) + }.merge!(@html_options) + select_options[:disabled] = 'disabled' if @options[:disabled] + select_options[:class] = type if @options[:with_css_classes] + + select_html = "\n" + select_html << content_tag(:option, '', :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 + end + + # Builds a prompt option tag with supplied options or from default options. + # prompt_option_tag(:month, prompt: 'Select month') + # => "<option value="">Select month</option>" + def prompt_option_tag(type, options) + prompt = case options + when Hash + default_options = {:year => false, :month => false, :day => false, :hour => false, :minute => false, :second => false} + default_options.merge!(options)[type.to_sym] + when String + options + else + I18n.translate(:"datetime.prompts.#{type}", :locale => @options[:locale]) + end + + prompt ? content_tag(:option, prompt, :value => '') : '' + end + + # Builds hidden input tag for date part and value. + # build_hidden(:year, 2008) + # => "<input id="post_written_on_1i" name="post[written_on(1i)]" type="hidden" value="2008" />" + def build_hidden(type, value) + select_options = { + :type => "hidden", + :id => input_id_from_type(type), + :name => input_name_from_type(type), + :value => value + }.merge!(@html_options.slice(:disabled)) + select_options[:disabled] = 'disabled' if @options[:disabled] + + tag(:input, select_options) + "\n".html_safe + end + + # Returns the name attribute for the input tag. + # => post[written_on(1i)] + def input_name_from_type(type) + prefix = @options[:prefix] || ActionView::Helpers::DateTimeSelector::DEFAULT_PREFIX + prefix += "[#{@options[:index]}]" if @options.has_key?(:index) + + field_name = @options[:field_name] || type + if @options[:include_position] + field_name += "(#{ActionView::Helpers::DateTimeSelector::POSITION[type]}i)" + end + + @options[:discard_type] ? prefix : "#{prefix}[#{field_name}]" + end + + # Returns the id attribute for the input tag. + # => "post_written_on_1i" + def input_id_from_type(type) + id = input_name_from_type(type).gsub(/([\[\(])|(\]\[)/, '_').gsub(/[\]\)]/, '') + id = @options[:namespace] + '_' + id if @options[:namespace] + + id + end + + # Given an ordering of datetime components, create the selection HTML + # and join them with their appropriate separators. + def build_selects_from_types(order) + select = '' + first_visible = order.find { |type| !@options[:"discard_#{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 + select.html_safe + end + + # Returns the separator for a given datetime component. + def separator(type) + return "" if @options[:use_hidden] + + case type + when :year, :month, :day + @options[:"discard_#{type}"] ? "" : @options[:date_separator] + when :hour + (@options[:discard_year] && @options[:discard_day]) ? "" : @options[:datetime_separator] + when :minute, :second + @options[:"discard_#{type}"] ? "" : @options[:time_separator] + end + end + end + + class FormBuilder + # Wraps ActionView::Helpers::DateHelper#date_select for form builders: + # + # <%= form_for @person do |f| %> + # <%= f.date_select :birth_date %> + # <%= f.submit %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + def date_select(method, options = {}, html_options = {}) + @template.date_select(@object_name, method, objectify_options(options), html_options) + end + + # Wraps ActionView::Helpers::DateHelper#time_select for form builders: + # + # <%= form_for @race do |f| %> + # <%= f.time_select :average_lap %> + # <%= f.submit %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + def time_select(method, options = {}, html_options = {}) + @template.time_select(@object_name, method, objectify_options(options), html_options) + end + + # Wraps ActionView::Helpers::DateHelper#datetime_select for form builders: + # + # <%= form_for @person do |f| %> + # <%= f.time_select :last_request_at %> + # <%= f.submit %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + def datetime_select(method, options = {}, html_options = {}) + @template.datetime_select(@object_name, method, objectify_options(options), html_options) + end + end + end +end diff --git a/actionview/lib/action_view/helpers/debug_helper.rb b/actionview/lib/action_view/helpers/debug_helper.rb new file mode 100644 index 0000000000..c29c1b1eea --- /dev/null +++ b/actionview/lib/action_view/helpers/debug_helper.rb @@ -0,0 +1,39 @@ +module ActionView + # = Action View Debug Helper + # + # Provides a set of methods for making it easier to debug Rails objects. + module Helpers + module DebugHelper + + include TagHelper + + # Returns a YAML representation of +object+ wrapped with <pre> and </pre>. + # If the object cannot be converted to YAML using +to_yaml+, +inspect+ will be called instead. + # Useful for inspecting an object at the time of rendering. + # + # @user = User.new({ username: 'testing', password: 'xyz', age: 42}) %> + # debug(@user) + # # => + # <pre class='debug_dump'>--- !ruby/object:User + # attributes: + # updated_at: + # username: testing + # + # age: 42 + # password: xyz + # created_at: + # attributes_cache: {} + # + # new_record: true + # </pre> + def debug(object) + Marshal::dump(object) + object = ERB::Util.html_escape(object.to_yaml).gsub(" ", " ").html_safe + content_tag(:pre, object, :class => "debug_dump") + rescue Exception # 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 + end + end +end diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb new file mode 100644 index 0000000000..f64c0ca30b --- /dev/null +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -0,0 +1,1880 @@ +require 'cgi' +require 'action_view/helpers/date_helper' +require 'action_view/helpers/tag_helper' +require 'action_view/helpers/form_tag_helper' +require 'action_view/helpers/active_model_helper' +require 'action_view/helpers/tags' +require 'action_view/model_naming' +require 'active_support/core_ext/class/attribute_accessors' +require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/string/output_safety' +require 'active_support/core_ext/string/inflections' + +module ActionView + # = Action View Form Helpers + module Helpers + # Form helpers are designed to make working with resources much easier + # compared to using vanilla HTML. + # + # Typically, a form designed to create or update a resource reflects the + # identity of the resource in several ways: (i) the url that the form is + # sent to (the form element's +action+ attribute) should result in a request + # being routed to the appropriate controller action (with the appropriate <tt>:id</tt> + # parameter in the case of an existing resource), (ii) input fields should + # be named in such a way that in the controller their values appear in the + # appropriate places within the +params+ hash, and (iii) for an existing record, + # when the form is initially displayed, input fields corresponding to attributes + # of the resource should show the current values of those attributes. + # + # In Rails, this is usually achieved by creating the form using +form_for+ and + # a number of related helper methods. +form_for+ generates an appropriate <tt>form</tt> + # tag and yields a form builder object that knows the model the form is about. + # Input fields are created by calling methods defined on the form builder, which + # means they are able to generate the appropriate names and default values + # corresponding to the model attributes, as well as convenient IDs, etc. + # Conventions in the generated field names allow controllers to receive form data + # nicely structured in +params+ with no effort on your side. + # + # For example, to create a new person you typically set up a new instance of + # +Person+ in the <tt>PeopleController#new</tt> action, <tt>@person</tt>, and + # in the view template pass that object to +form_for+: + # + # <%= form_for @person do |f| %> + # <%= f.label :first_name %>: + # <%= f.text_field :first_name %><br /> + # + # <%= f.label :last_name %>: + # <%= f.text_field :last_name %><br /> + # + # <%= f.submit %> + # <% end %> + # + # The HTML generated for this would be (modulus formatting): + # + # <form action="/people" class="new_person" id="new_person" method="post"> + # <div style="margin:0;padding:0;display:inline"> + # <input name="authenticity_token" type="hidden" value="NrOp5bsjoLRuK8IW5+dQEYjKGUJDe7TQoZVvq95Wteg=" /> + # </div> + # <label for="person_first_name">First name</label>: + # <input id="person_first_name" name="person[first_name]" type="text" /><br /> + # + # <label for="person_last_name">Last name</label>: + # <input id="person_last_name" name="person[last_name]" type="text" /><br /> + # + # <input name="commit" type="submit" value="Create Person" /> + # </form> + # + # As you see, the HTML reflects knowledge about the resource in several spots, + # like the path the form should be submitted to, or the names of the input fields. + # + # 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>: + # + # if @person = Person.create(params[:person]) + # # success + # else + # # error handling + # end + # + # Interestingly, the exact same view code in the previous example can be used to edit + # a person. If <tt>@person</tt> is an existing record with name "John Smith" and ID 256, + # the code above as is would yield instead: + # + # <form action="/people/256" class="edit_person" id="edit_person_256" method="post"> + # <div style="margin:0;padding:0;display:inline"> + # <input name="_method" type="hidden" value="patch" /> + # <input name="authenticity_token" type="hidden" value="NrOp5bsjoLRuK8IW5+dQEYjKGUJDe7TQoZVvq95Wteg=" /> + # </div> + # <label for="person_first_name">First name</label>: + # <input id="person_first_name" name="person[first_name]" type="text" value="John" /><br /> + # + # <label for="person_last_name">Last name</label>: + # <input id="person_last_name" name="person[last_name]" type="text" value="Smith" /><br /> + # + # <input name="commit" type="submit" value="Update Person" /> + # </form> + # + # Note that the endpoint, default values, and submit button label are tailored for <tt>@person</tt>. + # That works that way because the involved helpers know whether the resource is a new record or not, + # and generate HTML accordingly. + # + # The controller would receive the form data again in <tt>params[:person]</tt>, ready to be + # passed to <tt>Person#update</tt>: + # + # if @person.update(params[:person]) + # # success + # else + # # error handling + # end + # + # That's how you typically work with resources. + module FormHelper + extend ActiveSupport::Concern + + include FormTagHelper + include UrlHelper + include ModelNaming + + # Creates a form that allows the user to create or update the attributes + # of a specific model object. + # + # The method can be used in several slightly different ways, depending on + # how much you wish to rely on Rails to infer automatically from the model + # how the form should be constructed. For a generic model object, a form + # can be created by passing +form_for+ a string or symbol representing + # the object we are concerned with: + # + # <%= form_for :person do |f| %> + # First name: <%= f.text_field :first_name %><br /> + # Last name : <%= f.text_field :last_name %><br /> + # Biography : <%= f.text_area :biography %><br /> + # Admin? : <%= f.check_box :admin %><br /> + # <%= f.submit %> + # <% end %> + # + # The variable +f+ yielded to the block is a FormBuilder object that + # incorporates the knowledge about the model object represented by + # <tt>:person</tt> passed to +form_for+. Methods defined on the FormBuilder + # are used to generate fields bound to this model. Thus, for example, + # + # <%= f.text_field :first_name %> + # + # will get expanded to + # + # <%= text_field :person, :first_name %> + # 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>. + # + # For fields generated in this way using the FormBuilder, + # if <tt>:person</tt> also happens to be the name of an instance variable + # <tt>@person</tt>, the default value of the field shown when the form is + # initially displayed (e.g. in the situation where you are editing an + # existing record) will be the value of the corresponding attribute of + # <tt>@person</tt>. + # + # The rightmost argument to +form_for+ is an + # optional hash of options - + # + # * <tt>:url</tt> - The URL the form is to be submitted to. This may be + # represented in the same way as values passed to +url_for+ or +link_to+. + # So for example you may use a named route directly. When the model is + # represented by a string or symbol, as in the example above, if the + # <tt>:url</tt> option is not specified, by default the form will be + # sent back to the current url (We will describe below an alternative + # resource-oriented usage of +form_for+ in which the URL does not need + # to be specified explicitly). + # * <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>:html</tt> - Optional HTML attributes for the form tag. + # + # Also note that +form_for+ doesn't create an exclusive scope. It's still + # possible to use both the stand-alone FormHelper methods and methods + # from FormTagHelper. For example: + # + # <%= form_for :person do |f| %> + # First name: <%= f.text_field :first_name %> + # Last name : <%= f.text_field :last_name %> + # Biography : <%= text_area :person, :biography %> + # Admin? : <%= check_box_tag "person[admin]", "1", @person.company.admin? %> + # <%= f.submit %> + # <% end %> + # + # This also works for the methods in FormOptionHelper and DateHelper that + # are designed to work with an object as base, like + # FormOptionHelper#collection_select and DateHelper#datetime_select. + # + # === #form_for with a model object + # + # In the examples above, the object to be created or edited was + # represented by a symbol passed to +form_for+, and we noted that + # a string can also be used equivalently. It is also possible, however, + # to pass a model object itself to +form_for+. For example, if <tt>@post</tt> + # is an existing record you wish to edit, you can create the form using + # + # <%= form_for @post do |f| %> + # ... + # <% end %> + # + # This behaves in almost the same way as outlined previously, with a + # couple of small exceptions. First, the prefix used to name the input + # elements within the form (hence the key that denotes them in the +params+ + # hash) is actually derived from the object's _class_, e.g. <tt>params[:post]</tt> + # if the object's class is +Post+. However, this can be overwritten using + # the <tt>:as</tt> option, e.g. - + # + # <%= form_for(@person, as: :client) do |f| %> + # ... + # <% end %> + # + # would result in <tt>params[:client]</tt>. + # + # Secondly, the field values shown when the form is initially displayed + # are taken from the attributes of the object passed to +form_for+, + # regardless of whether the object is an instance + # variable. So, for example, if we had a _local_ variable +post+ + # representing an existing record, + # + # <%= form_for post do |f| %> + # ... + # <% end %> + # + # would produce a form with fields whose initial state reflect the current + # values of the attributes of +post+. + # + # === Resource-oriented style + # + # In the examples just shown, although not indicated explicitly, we still + # need to use the <tt>:url</tt> option in order to specify where the + # form is going to be sent. However, further simplification is possible + # if the record passed to +form_for+ is a _resource_, i.e. it corresponds + # to a set of RESTful routes, e.g. defined using the +resources+ method + # in <tt>config/routes.rb</tt>. In this case Rails will simply infer the + # appropriate URL from the record itself. For example, + # + # <%= form_for @post do |f| %> + # ... + # <% end %> + # + # is then equivalent to something like: + # + # <%= form_for @post, as: :post, url: post_path(@post), method: :patch, html: { class: "edit_post", id: "edit_post_45" } do |f| %> + # ... + # <% end %> + # + # And for a new record + # + # <%= form_for(Post.new) do |f| %> + # ... + # <% end %> + # + # is equivalent to something like: + # + # <%= form_for @post, as: :post, url: posts_path, html: { class: "new_post", id: "new_post" } do |f| %> + # ... + # <% end %> + # + # However you can still overwrite individual conventions, such as: + # + # <%= form_for(@post, url: super_posts_path) do |f| %> + # ... + # <% end %> + # + # You can also set the answer format, like this: + # + # <%= form_for(@post, format: :json) do |f| %> + # ... + # <% end %> + # + # For namespaced routes, like +admin_post_url+: + # + # <%= form_for([:admin, @post]) do |f| %> + # ... + # <% end %> + # + # If your resource has associations defined, for example, you want to add comments + # to the document given that the routes are set correctly: + # + # <%= form_for([@document, @comment]) do |f| %> + # ... + # <% end %> + # + # Where <tt>@document = Document.find(params[:id])</tt> and + # <tt>@comment = Comment.new</tt>. + # + # === Setting the method + # + # You can force the form to use the full array of HTTP verbs by setting + # + # method: (:get|:post|:patch|:put|:delete) + # + # in the options hash. If the verb is not GET or POST, which are natively + # supported by HTML forms, the form will be set to POST and a hidden input + # called _method will carry the intended verb for the server to interpret. + # + # === Unobtrusive JavaScript + # + # Specifying: + # + # remote: true + # + # in the options hash creates a form that will allow the unobtrusive JavaScript drivers to modify its + # behavior. The expected default behavior is an XMLHttpRequest in the background instead of the regular + # POST arrangement, but ultimately the behavior is the choice of the JavaScript driver implementor. + # Even though it's using JavaScript to serialize the form elements, the form submission will work just like + # a regular submission as viewed by the receiving side (all elements available in <tt>params</tt>). + # + # Example: + # + # <%= form_for(@post, remote: true) do |f| %> + # ... + # <% end %> + # + # The HTML generated for this would be: + # + # <form action='http://www.example.com' method='post' data-remote='true'> + # <div style='margin:0;padding:0;display:inline'> + # <input name='_method' type='hidden' value='patch' /> + # </div> + # ... + # </form> + # + # === Setting HTML options + # + # You can set data attributes directly by passing in a data hash, but all other HTML options must be wrapped in + # the HTML key. Example: + # + # <%= form_for(@post, data: { behavior: "autosave" }, html: { name: "go" }) do |f| %> + # ... + # <% end %> + # + # The HTML generated for this would be: + # + # <form action='http://www.example.com' method='post' data-behavior='autosave' name='go'> + # <div style='margin:0;padding:0;display:inline'> + # <input name='_method' type='hidden' value='patch' /> + # </div> + # ... + # </form> + # + # === Removing hidden model id's + # + # The form_for method automatically includes the model id as a hidden field in the form. + # This is used to maintain the correlation between the form data and its associated model. + # Some ORM systems do not use IDs on nested models so in this case you want to be able + # to disable the hidden id. + # + # In the following example the Post model has many Comments stored within it in a NoSQL database, + # thus there is no primary key for comments. + # + # Example: + # + # <%= form_for(@post) do |f| %> + # <%= f.fields_for(:comments, include_id: false) do |cf| %> + # ... + # <% end %> + # <% end %> + # + # === Customized form builders + # + # You can also build forms using a customized FormBuilder class. Subclass + # FormBuilder and override or define some more helpers, then use your + # custom builder. For example, let's say you made a helper to + # automatically add labels to form inputs. + # + # <%= form_for @person, url: { action: "create" }, builder: LabellingFormBuilder do |f| %> + # <%= f.text_field :first_name %> + # <%= f.text_field :last_name %> + # <%= f.text_area :biography %> + # <%= f.check_box :admin %> + # <%= f.submit %> + # <% end %> + # + # In this case, if you use this: + # + # <%= render f %> + # + # The rendered template is <tt>people/_labelling_form</tt> and the local + # variable referencing the form builder is called + # <tt>labelling_form</tt>. + # + # The custom FormBuilder class is automatically merged with the options + # of a nested fields_for call, unless it's explicitly set. + # + # In many cases you will want to wrap the above in another helper, so you + # could do something like the following: + # + # def labelled_form_for(record_or_name_or_array, *args, &block) + # options = args.extract_options! + # form_for(record_or_name_or_array, *(args << options.merge(builder: LabellingFormBuilder)), &block) + # end + # + # If you don't need to attach a form to a model instance, then check out + # FormTagHelper#form_tag. + # + # === Form to external resources + # + # When you build forms to external resources sometimes you need to set an authenticity token or just render a form + # without it, for example when you submit data to a payment gateway number and types of fields could be limited. + # + # To set an authenticity token you need to pass an <tt>:authenticity_token</tt> parameter + # + # <%= form_for @invoice, url: external_url, authenticity_token: 'external_token' do |f| + # ... + # <% end %> + # + # If you don't want to an authenticity token field be rendered at all just pass <tt>false</tt>: + # + # <%= form_for @invoice, url: external_url, authenticity_token: false do |f| + # ... + # <% end %> + def form_for(record, options = {}, &block) + raise ArgumentError, "Missing block" unless block_given? + html_options = options[:html] ||= {} + + case record + when String, Symbol + object_name = record + object = nil + else + object = record.is_a?(Array) ? record.last : record + raise ArgumentError, "First argument in form cannot contain nil or be empty" unless object + object_name = options[:as] || model_name_from_record_or_class(object).param_key + apply_form_for_options!(record, object, options) + end + + 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[:authenticity_token] = options.delete(:authenticity_token) + + builder = instantiate_builder(object_name, object, options) + output = capture(builder, &block) + html_options[:multipart] ||= builder.multipart? + + form_tag(options[:url] || {}, html_options) { output } + end + + def apply_form_for_options!(record, object, options) #:nodoc: + object = convert_to_model(object) + + as = options[:as] + action, method = object.respond_to?(:persisted?) && object.persisted? ? [:edit, :patch] : [:new, :post] + options[:html].reverse_merge!( + class: as ? "#{action}_#{as}" : dom_class(object, action), + id: as ? "#{action}_#{as}" : [options[:namespace], dom_id(object, action)].compact.join("_").presence, + method: method + ) + + options[:url] ||= polymorphic_path(record, format: options.delete(:format)) + end + private :apply_form_for_options! + + # Creates a scope around a specific model object like form_for, but + # doesn't create the form tags themselves. This makes fields_for suitable + # for specifying additional model objects in the same form. + # + # Although the usage and purpose of +field_for+ is similar to +form_for+'s, + # its method signature is slightly different. Like +form_for+, it yields + # a FormBuilder object associated with a particular model object to a block, + # and within the block allows methods to be called on the builder to + # generate fields associated with the model object. Fields may reflect + # a model object in two ways - how they are named (hence how submitted + # values appear within the +params+ hash in the controller) and what + # default values are shown when the form the fields appear in is first + # displayed. In order for both of these features to be specified independently, + # both an object name (represented by either a symbol or string) and the + # object itself can be passed to the method separately - + # + # <%= form_for @person do |person_form| %> + # First name: <%= person_form.text_field :first_name %> + # Last name : <%= person_form.text_field :last_name %> + # + # <%= fields_for :permission, @person.permission do |permission_fields| %> + # Admin? : <%= permission_fields.check_box :admin %> + # <% end %> + # + # <%= f.submit %> + # <% end %> + # + # In this case, the checkbox field will be represented by an HTML +input+ + # tag with the +name+ attribute <tt>permission[admin]</tt>, and the submitted + # value will appear in the controller as <tt>params[:permission][:admin]</tt>. + # If <tt>@person.permission</tt> is an existing record with an attribute + # +admin+, the initial state of the checkbox when first displayed will + # reflect the value of <tt>@person.permission.admin</tt>. + # + # Often this can be simplified by passing just the name of the model + # object to +fields_for+ - + # + # <%= fields_for :permission do |permission_fields| %> + # Admin?: <%= permission_fields.check_box :admin %> + # <% end %> + # + # ...in which case, if <tt>:permission</tt> also happens to be the name of an + # instance variable <tt>@permission</tt>, the initial state of the input + # field will reflect the value of that variable's attribute <tt>@permission.admin</tt>. + # + # Alternatively, you can pass just the model object itself (if the first + # argument isn't a string or symbol +fields_for+ will realize that the + # name has been omitted) - + # + # <%= fields_for @person.permission do |permission_fields| %> + # Admin?: <%= permission_fields.check_box :admin %> + # <% end %> + # + # and +fields_for+ will derive the required name of the field from the + # _class_ of the model object, e.g. if <tt>@person.permission</tt>, is + # of class +Permission+, the field will still be named <tt>permission[admin]</tt>. + # + # Note: This also works for the methods in FormOptionHelper and + # DateHelper that are designed to work with an object as base, like + # FormOptionHelper#collection_select and DateHelper#datetime_select. + # + # === Nested Attributes Examples + # + # When the object belonging to the current scope has a nested attribute + # writer for a certain attribute, fields_for will yield a new scope + # for that attribute. This allows you to create forms that set or change + # the attributes of a parent object and its associations in one go. + # + # Nested attribute writers are normal setter methods named after an + # association. The most common way of defining these writers is either + # with +accepts_nested_attributes_for+ in a model definition or by + # defining a method with the proper name. For example: the attribute + # writer for the association <tt>:address</tt> is called + # <tt>address_attributes=</tt>. + # + # Whether a one-to-one or one-to-many style form builder will be yielded + # depends on whether the normal reader method returns a _single_ object + # or an _array_ of objects. + # + # ==== One-to-one + # + # Consider a Person class which returns a _single_ Address from the + # <tt>address</tt> reader method and responds to the + # <tt>address_attributes=</tt> writer method: + # + # class Person + # def address + # @address + # end + # + # def address_attributes=(attributes) + # # Process the attributes hash + # end + # end + # + # This model can now be used with a nested fields_for, like so: + # + # <%= form_for @person do |person_form| %> + # ... + # <%= person_form.fields_for :address do |address_fields| %> + # Street : <%= address_fields.text_field :street %> + # Zip code: <%= address_fields.text_field :zip_code %> + # <% end %> + # ... + # <% end %> + # + # When address is already an association on a Person you can use + # +accepts_nested_attributes_for+ to define the writer method for you: + # + # class Person < ActiveRecord::Base + # has_one :address + # accepts_nested_attributes_for :address + # end + # + # If you want to destroy the associated model through the form, you have + # to enable it first using the <tt>:allow_destroy</tt> option for + # +accepts_nested_attributes_for+: + # + # class Person < ActiveRecord::Base + # has_one :address + # accepts_nested_attributes_for :address, allow_destroy: true + # end + # + # Now, when you use a form element with the <tt>_destroy</tt> parameter, + # with a value that evaluates to +true+, you will destroy the associated + # model (eg. 1, '1', true, or 'true'): + # + # <%= form_for @person do |person_form| %> + # ... + # <%= person_form.fields_for :address do |address_fields| %> + # ... + # Delete: <%= address_fields.check_box :_destroy %> + # <% end %> + # ... + # <% end %> + # + # ==== One-to-many + # + # Consider a Person class which returns an _array_ of Project instances + # from the <tt>projects</tt> reader method and responds to the + # <tt>projects_attributes=</tt> writer method: + # + # class Person + # def projects + # [@project1, @project2] + # end + # + # def projects_attributes=(attributes) + # # Process the attributes hash + # end + # end + # + # Note that the <tt>projects_attributes=</tt> writer method is in fact + # required for fields_for to correctly identify <tt>:projects</tt> as a + # collection, and the correct indices to be set in the form markup. + # + # When projects is already an association on Person you can use + # +accepts_nested_attributes_for+ to define the writer method for you: + # + # class Person < ActiveRecord::Base + # has_many :projects + # accepts_nested_attributes_for :projects + # end + # + # This model can now be used with a nested fields_for. The block given to + # the nested fields_for call will be repeated for each instance in the + # collection: + # + # <%= form_for @person do |person_form| %> + # ... + # <%= person_form.fields_for :projects do |project_fields| %> + # <% if project_fields.object.active? %> + # Name: <%= project_fields.text_field :name %> + # <% end %> + # <% end %> + # ... + # <% end %> + # + # It's also possible to specify the instance to be used: + # + # <%= form_for @person do |person_form| %> + # ... + # <% @person.projects.each do |project| %> + # <% if project.active? %> + # <%= person_form.fields_for :projects, project do |project_fields| %> + # Name: <%= project_fields.text_field :name %> + # <% end %> + # <% end %> + # <% end %> + # ... + # <% end %> + # + # Or a collection to be used: + # + # <%= form_for @person do |person_form| %> + # ... + # <%= person_form.fields_for :projects, @active_projects do |project_fields| %> + # Name: <%= project_fields.text_field :name %> + # <% end %> + # ... + # <% end %> + # + # If you want to destroy any of the associated models through the + # form, you have to enable it first using the <tt>:allow_destroy</tt> + # option for +accepts_nested_attributes_for+: + # + # class Person < ActiveRecord::Base + # has_many :projects + # accepts_nested_attributes_for :projects, allow_destroy: true + # end + # + # This will allow you to specify which models to destroy in the + # attributes hash by adding a form element for the <tt>_destroy</tt> + # parameter with a value that evaluates to +true+ + # (eg. 1, '1', true, or 'true'): + # + # <%= form_for @person do |person_form| %> + # ... + # <%= person_form.fields_for :projects do |project_fields| %> + # Delete: <%= project_fields.check_box :_destroy %> + # <% end %> + # ... + # <% end %> + # + # When a collection is used you might want to know the index of each + # object into the array. For this purpose, the <tt>index</tt> method + # is available in the FormBuilder object. + # + # <%= form_for @person do |person_form| %> + # ... + # <%= person_form.fields_for :projects do |project_fields| %> + # Project #<%= project_fields.index %> + # ... + # <% end %> + # ... + # <% end %> + # + # Note that fields_for will automatically generate a hidden field + # to store the ID of the record. There are circumstances where this + # hidden field is not needed and you can pass <tt>include_id: false</tt> + # to prevent fields_for from rendering it automatically. + def fields_for(record_name, record_object = nil, options = {}, &block) + builder = instantiate_builder(record_name, record_object, options) + capture(builder, &block) + end + + # Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object + # assigned to the template (identified by +object+). The text of label will default to the attribute name unless a translation + # is found in the current I18n locale (through helpers.label.<modelname>.<attribute>) or you specify it explicitly. + # Additional options on the label tag can be passed as a hash with +options+. These options will be tagged + # onto the HTML as an HTML element attribute as in the example shown, except for the <tt>:value</tt> option, which is designed to + # target labels for radio_button tags (where the value is used in the ID of the input tag). + # + # ==== Examples + # label(:post, :title) + # # => <label for="post_title">Title</label> + # + # You can localize your labels based on model and attribute names. + # For example you can define the following in your locale (e.g. en.yml) + # + # helpers: + # label: + # post: + # body: "Write your entire text here" + # + # Which then will result in + # + # label(:post, :body) + # # => <label for="post_body">Write your entire text here</label> + # + # Localization can also be based purely on the translation of the attribute-name + # (if you are using ActiveRecord): + # + # activerecord: + # attributes: + # post: + # cost: "Total cost" + # + # label(:post, :cost) + # # => <label for="post_cost">Total cost</label> + # + # label(:post, :title, "A short title") + # # => <label for="post_title">A short title</label> + # + # label(:post, :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 for="post_privacy_public">Public Post</label> + # + # label(:post, :terms) do + # 'Accept <a href="/terms">Terms</a>.'.html_safe + # end + def label(object_name, method, content_or_options = nil, options = nil, &block) + Tags::Label.new(object_name, method, self, content_or_options, options).render(&block) + end + + # Returns an input tag of the "text" type tailored for accessing a specified attribute (identified by +method+) on an object + # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a + # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example + # shown. + # + # ==== Examples + # text_field(:post, :title, size: 20) + # # => <input type="text" id="post_title" name="post[title]" size="20" value="#{@post.title}" /> + # + # text_field(:post, :title, class: "create_input") + # # => <input type="text" id="post_title" name="post[title]" value="#{@post.title}" class="create_input" /> + # + # text_field(:session, :user, onchange: "if ($('#session_user').val() === 'admin') { alert('Your login can not be admin!'); }") + # # => <input type="text" id="session_user" name="session[user]" value="#{@session.user}" onchange="if ($('#session_user').val() === 'admin') { alert('Your login can not be admin!'); }"/> + # + # text_field(:snippet, :code, size: 20, class: 'code_input') + # # => <input type="text" id="snippet_code" name="snippet[code]" size="20" value="#{@snippet.code}" class="code_input" /> + def text_field(object_name, method, options = {}) + Tags::TextField.new(object_name, method, self, options).render + end + + # Returns an input tag of the "password" type tailored for accessing a specified attribute (identified by +method+) on an object + # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a + # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example + # shown. For security reasons this field is blank by default; pass in a value via +options+ if this is not desired. + # + # ==== Examples + # password_field(:login, :pass, size: 20) + # # => <input type="password" id="login_pass" name="login[pass]" size="20" /> + # + # password_field(:account, :secret, class: "form_input", value: @account.secret) + # # => <input type="password" id="account_secret" name="account[secret]" value="#{@account.secret}" class="form_input" /> + # + # password_field(:user, :password, onchange: "if ($('#user_password').val().length > 30) { alert('Your password needs to be shorter!'); }") + # # => <input type="password" id="user_password" name="user[password]" onchange="if ($('#user_password').val().length > 30) { alert('Your password needs to be shorter!'); }"/> + # + # password_field(:account, :pin, size: 20, class: 'form_input') + # # => <input type="password" id="account_pin" name="account[pin]" size="20" class="form_input" /> + def password_field(object_name, method, options = {}) + Tags::PasswordField.new(object_name, method, self, options).render + end + + # Returns a hidden input tag tailored for accessing a specified attribute (identified by +method+) on an object + # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a + # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example + # shown. + # + # ==== Examples + # hidden_field(:signup, :pass_confirm) + # # => <input type="hidden" id="signup_pass_confirm" name="signup[pass_confirm]" value="#{@signup.pass_confirm}" /> + # + # hidden_field(:post, :tag_list) + # # => <input type="hidden" id="post_tag_list" name="post[tag_list]" value="#{@post.tag_list}" /> + # + # hidden_field(:user, :token) + # # => <input type="hidden" id="user_token" name="user[token]" value="#{@user.token}" /> + def hidden_field(object_name, method, options = {}) + Tags::HiddenField.new(object_name, method, self, options).render + end + + # Returns a file upload input tag tailored for accessing a specified attribute (identified by +method+) on an object + # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a + # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example + # shown. + # + # Using this method inside a +form_for+ block will set the enclosing form's encoding to <tt>multipart/form-data</tt>. + # + # ==== Options + # * Creates standard HTML attributes for the tag. + # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input. + # * <tt>:multiple</tt> - If set to true, *in most updated browsers* the user will be allowed to select multiple files. + # * <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) + # # => <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, :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') + # # => <input type="file" id="post_image" name="post[image]" accept="image/png,image/gif,image/jpeg" /> + # + # file_field(:attachment, :file, class: 'file_input') + # # => <input type="file" id="attachment_file" name="attachment[file]" class="file_input" /> + def file_field(object_name, method, options = {}) + Tags::FileField.new(object_name, method, self, options).render + end + + # Returns a textarea opening and closing tag set tailored for accessing a specified attribute (identified by +method+) + # on an object assigned to the template (identified by +object+). Additional options on the input tag can be passed as a + # hash with +options+. + # + # ==== Examples + # text_area(:post, :body, cols: 20, rows: 40) + # # => <textarea cols="20" rows="40" id="post_body" name="post[body]"> + # # #{@post.body} + # # </textarea> + # + # text_area(:comment, :text, size: "20x30") + # # => <textarea cols="20" rows="30" id="comment_text" name="comment[text]"> + # # #{@comment.text} + # # </textarea> + # + # text_area(:application, :notes, cols: 40, rows: 15, class: 'app_input') + # # => <textarea cols="40" rows="15" id="application_notes" name="application[notes]" class="app_input"> + # # #{@application.notes} + # # </textarea> + # + # text_area(:entry, :body, size: "20x20", disabled: 'disabled') + # # => <textarea cols="20" rows="20" id="entry_body" name="entry[body]" disabled="disabled"> + # # #{@entry.body} + # # </textarea> + def text_area(object_name, method, options = {}) + Tags::TextArea.new(object_name, method, self, options).render + end + + # Returns a checkbox tag tailored for accessing a specified attribute (identified by +method+) on an object + # assigned to the template (identified by +object+). This object must be an instance object (@object) and not a local object. + # It's intended that +method+ returns an integer and if that integer is above zero, then the checkbox is checked. + # Additional options on the input tag can be passed as a hash with +options+. The +checked_value+ defaults to 1 + # while the default +unchecked_value+ is set to 0 which is convenient for boolean values. + # + # ==== Gotcha + # + # The HTML specification says unchecked check boxes are not successful, and + # thus web browsers do not send them. Unfortunately this introduces a gotcha: + # if an +Invoice+ model has a +paid+ flag, and in the form that edits a paid + # invoice the user unchecks its check box, no +paid+ parameter is sent. So, + # any mass-assignment idiom like + # + # @invoice.update(params[:invoice]) + # + # wouldn't update the flag. + # + # To prevent this the helper generates an auxiliary hidden field before + # the very check box. The hidden field has the same name and its + # attributes mimic an unchecked check box. + # + # This way, the client either sends only the hidden field (representing + # the check box is unchecked), or both fields. Since the HTML specification + # says key/value pairs have to be sent in the same order they appear in the + # form, and parameters extraction gets the last occurrence of any repeated + # key in the query string, that works for ordinary forms. + # + # Unfortunately that workaround does not work when the check box goes + # within an array-like parameter, as in + # + # <%= fields_for "project[invoice_attributes][]", invoice, index: nil do |form| %> + # <%= form.check_box :paid %> + # ... + # <% end %> + # + # because parameter name repetition is precisely what Rails seeks to distinguish + # the elements of the array. For each item with a checked check box you + # get an extra ghost item with only that attribute, assigned to "0". + # + # In that case it is preferable to either use +check_box_tag+ or to use + # hashes instead of arrays. + # + # # Let's say that @post.validated? is 1: + # check_box("post", "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") + # # => <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") + # # => <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(object_name, method, options = {}, checked_value = "1", unchecked_value = "0") + Tags::CheckBox.new(object_name, method, self, checked_value, unchecked_value, options).render + end + + # Returns a radio button tag for accessing a specified attribute (identified by +method+) on an object + # assigned to the template (identified by +object+). If the current value of +method+ is +tag_value+ the + # radio button will be checked. + # + # To force the radio button to be checked pass <tt>checked: true</tt> in the + # +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") + # # => <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") + # # => <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(object_name, method, tag_value, options = {}) + Tags::RadioButton.new(object_name, method, self, tag_value, options).render + end + + # Returns a text_field of type "color". + # + # color_field("car", "color") + # # => <input id="car_color" name="car[color]" type="color" value="#000000" /> + def color_field(object_name, method, options = {}) + Tags::ColorField.new(object_name, method, self, options).render + end + + # Returns an input of type "search" for accessing a specified attribute (identified by +method+) on an object + # assigned to the template (identified by +object_name+). Inputs of type "search" may be styled differently by + # some browsers. + # + # search_field(:user, :name) + # # => <input id="user_name" name="user[name]" type="search" /> + # search_field(:user, :name, autosave: false) + # # => <input autosave="false" id="user_name" name="user[name]" type="search" /> + # search_field(:user, :name, results: 3) + # # => <input id="user_name" name="user[name]" results="3" type="search" /> + # # Assume request.host returns "www.example.com" + # search_field(:user, :name, autosave: true) + # # => <input autosave="com.example.www" id="user_name" name="user[name]" results="10" type="search" /> + # search_field(:user, :name, onsearch: true) + # # => <input id="user_name" incremental="true" name="user[name]" onsearch="true" type="search" /> + # search_field(:user, :name, autosave: false, onsearch: true) + # # => <input autosave="false" id="user_name" incremental="true" name="user[name]" onsearch="true" type="search" /> + # search_field(:user, :name, autosave: true, onsearch: true) + # # => <input autosave="com.example.www" id="user_name" incremental="true" name="user[name]" onsearch="true" results="10" type="search" /> + def search_field(object_name, method, options = {}) + Tags::SearchField.new(object_name, method, self, options).render + end + + # Returns a text_field of type "tel". + # + # telephone_field("user", "phone") + # # => <input id="user_phone" name="user[phone]" type="tel" /> + # + def telephone_field(object_name, method, options = {}) + Tags::TelField.new(object_name, method, self, options).render + end + # aliases telephone_field + alias phone_field telephone_field + + # Returns a text_field of type "date". + # + # 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" + # 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. + # + # @user.born_on = Date.new(1984, 1, 27) + # date_field("user", "born_on", value: "1984-05-12") + # # => <input id="user_born_on" name="user[born_on]" type="date" value="1984-05-12" /> + # + def date_field(object_name, method, options = {}) + Tags::DateField.new(object_name, method, self, options).render + end + + # Returns a text_field of type "time". + # + # The default value is generated by trying to call +strftime+ with "%T.%L" + # on the objects's value. It is still possible to override that + # by passing the "value" option. + # + # === Options + # * Accepts same options as time_field_tag + # + # === Example + # time_field("task", "started_at") + # # => <input id="task_started_at" name="task[started_at]" type="time" /> + # + def time_field(object_name, method, options = {}) + Tags::TimeField.new(object_name, method, self, options).render + end + + # Returns a text_field of type "datetime". + # + # datetime_field("user", "born_on") + # # => <input id="user_born_on" name="user[born_on]" type="datetime" /> + # + # The default value is generated by trying to call +strftime+ with "%Y-%m-%dT%T.%L%z" + # on the object's value, which makes it behave as expected for instances + # of DateTime and ActiveSupport::TimeWithZone. + # + # @user.born_on = Date.new(1984, 1, 12) + # datetime_field("user", "born_on") + # # => <input id="user_born_on" name="user[born_on]" type="datetime" value="1984-01-12T00:00:00.000+0000" /> + # + def datetime_field(object_name, method, options = {}) + Tags::DatetimeField.new(object_name, method, self, options).render + end + + # Returns a text_field of type "datetime-local". + # + # datetime_local_field("user", "born_on") + # # => <input id="user_born_on" name="user[born_on]" type="datetime-local" /> + # + # The default value is generated by trying to call +strftime+ with "%Y-%m-%dT%T" + # on the object's value, which makes it behave as expected for instances + # of DateTime and ActiveSupport::TimeWithZone. + # + # @user.born_on = Date.new(1984, 1, 12) + # datetime_local_field("user", "born_on") + # # => <input id="user_born_on" name="user[born_on]" type="datetime-local" value="1984-01-12T00:00:00" /> + # + def datetime_local_field(object_name, method, options = {}) + Tags::DatetimeLocalField.new(object_name, method, self, options).render + end + + # Returns a text_field of type "month". + # + # month_field("user", "born_on") + # # => <input id="user_born_on" name="user[born_on]" type="month" /> + # + # The default value is generated by trying to call +strftime+ with "%Y-%m" + # on the object's value, which makes it behave as expected for instances + # of DateTime and ActiveSupport::TimeWithZone. + # + # @user.born_on = Date.new(1984, 1, 27) + # month_field("user", "born_on") + # # => <input id="user_born_on" name="user[born_on]" type="date" value="1984-01" /> + # + def month_field(object_name, method, options = {}) + Tags::MonthField.new(object_name, method, self, options).render + end + + # Returns a text_field of type "week". + # + # week_field("user", "born_on") + # # => <input id="user_born_on" name="user[born_on]" type="week" /> + # + # The default value is generated by trying to call +strftime+ with "%Y-W%W" + # on the object's value, which makes it behave as expected for instances + # of DateTime and ActiveSupport::TimeWithZone. + # + # @user.born_on = Date.new(1984, 5, 12) + # week_field("user", "born_on") + # # => <input id="user_born_on" name="user[born_on]" type="date" value="1984-W19" /> + # + def week_field(object_name, method, options = {}) + Tags::WeekField.new(object_name, method, self, options).render + end + + # Returns a text_field of type "url". + # + # url_field("user", "homepage") + # # => <input id="user_homepage" name="user[homepage]" type="url" /> + # + def url_field(object_name, method, options = {}) + Tags::UrlField.new(object_name, method, self, options).render + end + + # Returns a text_field of type "email". + # + # email_field("user", "address") + # # => <input id="user_address" name="user[address]" type="email" /> + # + def email_field(object_name, method, options = {}) + Tags::EmailField.new(object_name, method, self, options).render + end + + # Returns an input tag of type "number". + # + # ==== Options + # * Accepts same options as number_field_tag + def number_field(object_name, method, options = {}) + Tags::NumberField.new(object_name, method, self, options).render + end + + # Returns an input tag of type "range". + # + # ==== Options + # * Accepts same options as range_field_tag + def range_field(object_name, method, options = {}) + Tags::RangeField.new(object_name, method, self, options).render + end + + private + + def instantiate_builder(record_name, record_object, options) + case record_name + when String, Symbol + object = record_object + object_name = record_name + else + object = record_name + object_name = model_name_from_record_or_class(object).param_key + end + + builder = options[:builder] || default_form_builder + builder.new(object_name, object, self, options) + end + + def default_form_builder + builder = ActionView::Base.default_form_builder + builder.respond_to?(:constantize) ? builder.constantize : builder + end + end + + # A +FormBuilder+ object is associated with a particular model object and + # allows you to generate fields associated with the model object. The + # +FormBuilder+ object is yielded when using +form_for+ or +fields_for+. + # For example: + # + # <%= form_for @person do |person_form| %> + # Name: <%= person_form.text_field :name %> + # Admin: <%= person_form.check_box :admin %> + # <% end %> + # + # In the above block, the 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 + # with the form. + # + # The +FormBuilder+ object can be thought of as serving as a proxy for the + # methods in the +FormHelper+ module. This class, however, allows you to + # call methods with the model object you are building the form for. + # + # You can create your own custom FormBuilder templates by subclasses this + # class. For example: + # + # class MyFormBuilder < ActionView::Helpers::FormBuilder + # def div_radio_button(method, tag_value, options = {}) + # @template.content_tag(:div, + # @template.radio_button( + # @object_name, method, tag_value, objectify_options(options) + # ) + # ) + # 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 + # correctly passed to the method. If +objectify_options+ is not called, + # then the newly created helper will not be linked back to the model. + # + # The +div_radio_button+ code from above can now be used as follows: + # + # <%= form_for @person, :builder => MyFormBuilder do |f| %> + # I am a child: <%= f.div_radio_button(:admin, "child") %> + # I am an adult: <%= f.div_radio_button(:admin, "adult") %> + # <% end -%> + # + # The standard set of helper methods for form building are located in the + # +field_helpers+ class attribute. + class FormBuilder + include ModelNaming + + # The methods which wrap a form helper call. + class_attribute :field_helpers + self.field_helpers = [:fields_for, :label, :text_field, :password_field, + :hidden_field, :file_field, :text_area, :check_box, + :radio_button, :color_field, :search_field, + :telephone_field, :phone_field, :date_field, + :time_field, :datetime_field, :datetime_local_field, + :month_field, :week_field, :url_field, :email_field, + :number_field, :range_field] + + attr_accessor :object_name, :object, :options + + attr_reader :multipart, :index + alias :multipart? :multipart + + def multipart=(multipart) + @multipart = multipart + + if parent_builder = @options[:parent_builder] + parent_builder.multipart = multipart + end + end + + def self._to_partial_path + @_to_partial_path ||= name.demodulize.underscore.sub!(/_builder$/, '') + end + + def to_partial_path + self.class._to_partial_path + end + + def to_model + self + end + + def initialize(object_name, object, template, options, block=nil) + if block + ActiveSupport::Deprecation.warn "Giving a block to FormBuilder is deprecated and has no effect anymore." + end + + @nested_child_index = {} + @object_name, @object, @template, @options = object_name, object, template, options + @default_options = @options ? @options.slice(:index, :namespace) : {} + if @object_name.to_s.match(/\[\]$/) + if object ||= @template.instance_variable_get("@#{Regexp.last_match.pre_match}") and object.respond_to?(:to_param) + @auto_index = object.to_param + else + raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}" + end + end + @multipart = nil + @index = options[:index] || options[:child_index] + end + + (field_helpers - [:label, :check_box, :radio_button, :fields_for, :hidden_field, :file_field]).each do |selector| + class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 + def #{selector}(method, options = {}) # def text_field(method, options = {}) + @template.send( # @template.send( + #{selector.inspect}, # "text_field", + @object_name, # @object_name, + method, # method, + objectify_options(options)) # objectify_options(options)) + end # end + RUBY_EVAL + end + + # Creates a scope around a specific model object like form_for, but + # doesn't create the form tags themselves. This makes fields_for suitable + # for specifying additional model objects in the same form. + # + # Although the usage and purpose of +field_for+ is similar to +form_for+'s, + # its method signature is slightly different. Like +form_for+, it yields + # a FormBuilder object associated with a particular model object to a block, + # and within the block allows methods to be called on the builder to + # generate fields associated with the model object. Fields may reflect + # a model object in two ways - how they are named (hence how submitted + # values appear within the +params+ hash in the controller) and what + # default values are shown when the form the fields appear in is first + # displayed. In order for both of these features to be specified independently, + # both an object name (represented by either a symbol or string) and the + # object itself can be passed to the method separately - + # + # <%= form_for @person do |person_form| %> + # First name: <%= person_form.text_field :first_name %> + # Last name : <%= person_form.text_field :last_name %> + # + # <%= fields_for :permission, @person.permission do |permission_fields| %> + # Admin? : <%= permission_fields.check_box :admin %> + # <% end %> + # + # <%= person_form.submit %> + # <% end %> + # + # In this case, the checkbox field will be represented by an HTML +input+ + # tag with the +name+ attribute <tt>permission[admin]</tt>, and the submitted + # value will appear in the controller as <tt>params[:permission][:admin]</tt>. + # If <tt>@person.permission</tt> is an existing record with an attribute + # +admin+, the initial state of the checkbox when first displayed will + # reflect the value of <tt>@person.permission.admin</tt>. + # + # Often this can be simplified by passing just the name of the model + # object to +fields_for+ - + # + # <%= fields_for :permission do |permission_fields| %> + # Admin?: <%= permission_fields.check_box :admin %> + # <% end %> + # + # ...in which case, if <tt>:permission</tt> also happens to be the name of an + # instance variable <tt>@permission</tt>, the initial state of the input + # field will reflect the value of that variable's attribute <tt>@permission.admin</tt>. + # + # Alternatively, you can pass just the model object itself (if the first + # argument isn't a string or symbol +fields_for+ will realize that the + # name has been omitted) - + # + # <%= fields_for @person.permission do |permission_fields| %> + # Admin?: <%= permission_fields.check_box :admin %> + # <% end %> + # + # and +fields_for+ will derive the required name of the field from the + # _class_ of the model object, e.g. if <tt>@person.permission</tt>, is + # of class +Permission+, the field will still be named <tt>permission[admin]</tt>. + # + # Note: This also works for the methods in FormOptionHelper and + # DateHelper that are designed to work with an object as base, like + # FormOptionHelper#collection_select and DateHelper#datetime_select. + # + # === Nested Attributes Examples + # + # When the object belonging to the current scope has a nested attribute + # writer for a certain attribute, fields_for will yield a new scope + # for that attribute. This allows you to create forms that set or change + # the attributes of a parent object and its associations in one go. + # + # Nested attribute writers are normal setter methods named after an + # association. The most common way of defining these writers is either + # with +accepts_nested_attributes_for+ in a model definition or by + # defining a method with the proper name. For example: the attribute + # writer for the association <tt>:address</tt> is called + # <tt>address_attributes=</tt>. + # + # Whether a one-to-one or one-to-many style form builder will be yielded + # depends on whether the normal reader method returns a _single_ object + # or an _array_ of objects. + # + # ==== One-to-one + # + # Consider a Person class which returns a _single_ Address from the + # <tt>address</tt> reader method and responds to the + # <tt>address_attributes=</tt> writer method: + # + # class Person + # def address + # @address + # end + # + # def address_attributes=(attributes) + # # Process the attributes hash + # end + # end + # + # This model can now be used with a nested fields_for, like so: + # + # <%= form_for @person do |person_form| %> + # ... + # <%= person_form.fields_for :address do |address_fields| %> + # Street : <%= address_fields.text_field :street %> + # Zip code: <%= address_fields.text_field :zip_code %> + # <% end %> + # ... + # <% end %> + # + # When address is already an association on a Person you can use + # +accepts_nested_attributes_for+ to define the writer method for you: + # + # class Person < ActiveRecord::Base + # has_one :address + # accepts_nested_attributes_for :address + # end + # + # If you want to destroy the associated model through the form, you have + # to enable it first using the <tt>:allow_destroy</tt> option for + # +accepts_nested_attributes_for+: + # + # class Person < ActiveRecord::Base + # has_one :address + # accepts_nested_attributes_for :address, allow_destroy: true + # end + # + # Now, when you use a form element with the <tt>_destroy</tt> parameter, + # with a value that evaluates to +true+, you will destroy the associated + # model (eg. 1, '1', true, or 'true'): + # + # <%= form_for @person do |person_form| %> + # ... + # <%= person_form.fields_for :address do |address_fields| %> + # ... + # Delete: <%= address_fields.check_box :_destroy %> + # <% end %> + # ... + # <% end %> + # + # ==== One-to-many + # + # Consider a Person class which returns an _array_ of Project instances + # from the <tt>projects</tt> reader method and responds to the + # <tt>projects_attributes=</tt> writer method: + # + # class Person + # def projects + # [@project1, @project2] + # end + # + # def projects_attributes=(attributes) + # # Process the attributes hash + # end + # end + # + # Note that the <tt>projects_attributes=</tt> writer method is in fact + # required for fields_for to correctly identify <tt>:projects</tt> as a + # collection, and the correct indices to be set in the form markup. + # + # When projects is already an association on Person you can use + # +accepts_nested_attributes_for+ to define the writer method for you: + # + # class Person < ActiveRecord::Base + # has_many :projects + # accepts_nested_attributes_for :projects + # end + # + # This model can now be used with a nested fields_for. The block given to + # the nested fields_for call will be repeated for each instance in the + # collection: + # + # <%= form_for @person do |person_form| %> + # ... + # <%= person_form.fields_for :projects do |project_fields| %> + # <% if project_fields.object.active? %> + # Name: <%= project_fields.text_field :name %> + # <% end %> + # <% end %> + # ... + # <% end %> + # + # It's also possible to specify the instance to be used: + # + # <%= form_for @person do |person_form| %> + # ... + # <% @person.projects.each do |project| %> + # <% if project.active? %> + # <%= person_form.fields_for :projects, project do |project_fields| %> + # Name: <%= project_fields.text_field :name %> + # <% end %> + # <% end %> + # <% end %> + # ... + # <% end %> + # + # Or a collection to be used: + # + # <%= form_for @person do |person_form| %> + # ... + # <%= person_form.fields_for :projects, @active_projects do |project_fields| %> + # Name: <%= project_fields.text_field :name %> + # <% end %> + # ... + # <% end %> + # + # If you want to destroy any of the associated models through the + # form, you have to enable it first using the <tt>:allow_destroy</tt> + # option for +accepts_nested_attributes_for+: + # + # class Person < ActiveRecord::Base + # has_many :projects + # accepts_nested_attributes_for :projects, allow_destroy: true + # end + # + # This will allow you to specify which models to destroy in the + # attributes hash by adding a form element for the <tt>_destroy</tt> + # parameter with a value that evaluates to +true+ + # (eg. 1, '1', true, or 'true'): + # + # <%= form_for @person do |person_form| %> + # ... + # <%= person_form.fields_for :projects do |project_fields| %> + # Delete: <%= project_fields.check_box :_destroy %> + # <% end %> + # ... + # <% end %> + # + # When a collection is used you might want to know the index of each + # object into the array. For this purpose, the <tt>index</tt> method + # is available in the FormBuilder object. + # + # <%= form_for @person do |person_form| %> + # ... + # <%= person_form.fields_for :projects do |project_fields| %> + # Project #<%= project_fields.index %> + # ... + # <% end %> + # ... + # <% end %> + # + # Note that fields_for will automatically generate a hidden field + # to store the ID of the record. There are circumstances where this + # hidden field is not needed and you can pass <tt>include_id: false</tt> + # to prevent fields_for from rendering it automatically. + def fields_for(record_name, record_object = nil, fields_options = {}, &block) + fields_options, record_object = record_object, nil if record_object.is_a?(Hash) && record_object.extractable_options? + fields_options[:builder] ||= options[:builder] + fields_options[:namespace] = options[:namespace] + fields_options[:parent_builder] = self + + case record_name + when String, Symbol + if nested_attributes_association?(record_name) + return fields_for_with_nested_attributes(record_name, record_object, fields_options, block) + end + else + record_object = record_name.is_a?(Array) ? record_name.last : record_name + record_name = model_name_from_record_or_class(record_object).param_key + end + + index = if options.has_key?(:index) + options[:index] + elsif defined?(@auto_index) + self.object_name = @object_name.to_s.sub(/\[\]$/,"") + @auto_index + end + + record_name = index ? "#{object_name}[#{index}][#{record_name}]" : "#{object_name}[#{record_name}]" + fields_options[:child_index] = index + + @template.fields_for(record_name, record_object, fields_options, &block) + end + + # Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object + # assigned to the template (identified by +object+). The text of label will default to the attribute name unless a translation + # is found in the current I18n locale (through helpers.label.<modelname>.<attribute>) or you specify it explicitly. + # Additional options on the label tag can be passed as a hash with +options+. These options will be tagged + # onto the HTML as an HTML element attribute as in the example shown, except for the <tt>:value</tt> option, which is designed to + # target labels for radio_button tags (where the value is used in the ID of the input tag). + # + # ==== Examples + # label(:post, :title) + # # => <label for="post_title">Title</label> + # + # You can localize your labels based on model and attribute names. + # For example you can define the following in your locale (e.g. en.yml) + # + # helpers: + # label: + # post: + # body: "Write your entire text here" + # + # Which then will result in + # + # label(:post, :body) + # # => <label for="post_body">Write your entire text here</label> + # + # Localization can also be based purely on the translation of the attribute-name + # (if you are using ActiveRecord): + # + # activerecord: + # attributes: + # post: + # cost: "Total cost" + # + # label(:post, :cost) + # # => <label for="post_cost">Total cost</label> + # + # label(:post, :title, "A short title") + # # => <label for="post_title">A short title</label> + # + # label(:post, :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 for="post_privacy_public">Public Post</label> + # + # label(:post, :terms) do + # 'Accept <a href="/terms">Terms</a>.'.html_safe + # end + def label(method, text = nil, options = {}, &block) + @template.label(@object_name, method, text, objectify_options(options), &block) + end + + # Returns a checkbox tag tailored for accessing a specified attribute (identified by +method+) on an object + # assigned to the template (identified by +object+). This object must be an instance object (@object) and not a local object. + # It's intended that +method+ returns an integer and if that integer is above zero, then the checkbox is checked. + # Additional options on the input tag can be passed as a hash with +options+. The +checked_value+ defaults to 1 + # while the default +unchecked_value+ is set to 0 which is convenient for boolean values. + # + # ==== Gotcha + # + # The HTML specification says unchecked check boxes are not successful, and + # thus web browsers do not send them. Unfortunately this introduces a gotcha: + # if an +Invoice+ model has a +paid+ flag, and in the form that edits a paid + # invoice the user unchecks its check box, no +paid+ parameter is sent. So, + # any mass-assignment idiom like + # + # @invoice.update(params[:invoice]) + # + # wouldn't update the flag. + # + # To prevent this the helper generates an auxiliary hidden field before + # the very check box. The hidden field has the same name and its + # attributes mimic an unchecked check box. + # + # This way, the client either sends only the hidden field (representing + # the check box is unchecked), or both fields. Since the HTML specification + # says key/value pairs have to be sent in the same order they appear in the + # form, and parameters extraction gets the last occurrence of any repeated + # key in the query string, that works for ordinary forms. + # + # Unfortunately that workaround does not work when the check box goes + # within an array-like parameter, as in + # + # <%= fields_for "project[invoice_attributes][]", invoice, index: nil do |form| %> + # <%= form.check_box :paid %> + # ... + # <% end %> + # + # because parameter name repetition is precisely what Rails seeks to distinguish + # the elements of the array. For each item with a checked check box you + # get an extra ghost item with only that attribute, assigned to "0". + # + # In that case it is preferable to either use +check_box_tag+ or to use + # hashes instead of arrays. + # + # # Let's say that @post.validated? is 1: + # check_box("post", "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") + # # => <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") + # # => <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") + @template.check_box(@object_name, method, objectify_options(options), checked_value, unchecked_value) + end + + # Returns a radio button tag for accessing a specified attribute (identified by +method+) on an object + # assigned to the template (identified by +object+). If the current value of +method+ is +tag_value+ the + # radio button will be checked. + # + # To force the radio button to be checked pass <tt>checked: true</tt> in the + # +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") + # # => <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") + # # => <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 = {}) + @template.radio_button(@object_name, method, tag_value, objectify_options(options)) + end + + # Returns a hidden input tag tailored for accessing a specified attribute (identified by +method+) on an object + # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a + # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example + # shown. + # + # ==== Examples + # hidden_field(:signup, :pass_confirm) + # # => <input type="hidden" id="signup_pass_confirm" name="signup[pass_confirm]" value="#{@signup.pass_confirm}" /> + # + # hidden_field(:post, :tag_list) + # # => <input type="hidden" id="post_tag_list" name="post[tag_list]" value="#{@post.tag_list}" /> + # + # hidden_field(:user, :token) + # # => <input type="hidden" id="user_token" name="user[token]" value="#{@user.token}" /> + # + def hidden_field(method, options = {}) + @emitted_hidden_id = true if method == :id + @template.hidden_field(@object_name, method, objectify_options(options)) + end + + # Returns a file upload input tag tailored for accessing a specified attribute (identified by +method+) on an object + # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a + # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example + # shown. + # + # Using this method inside a +form_for+ block will set the enclosing form's encoding to <tt>multipart/form-data</tt>. + # + # ==== Options + # * Creates standard HTML attributes for the tag. + # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input. + # * <tt>:multiple</tt> - If set to true, *in most updated browsers* the user will be allowed to select multiple files. + # * <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) + # # => <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, :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') + # # => <input type="file" id="post_image" name="post[image]" accept="image/png,image/gif,image/jpeg" /> + # + # file_field(:attachment, :file, class: 'file_input') + # # => <input type="file" id="attachment_file" name="attachment[file]" class="file_input" /> + def file_field(method, options = {}) + self.multipart = true + @template.file_field(@object_name, method, objectify_options(options)) + end + + # Add the submit button for the given form. When no value is given, it checks + # if the object is a new resource or not to create the proper label: + # + # <%= form_for @post do |f| %> + # <%= f.submit %> + # <% end %> + # + # In the example above, if @post is a new record, it will use "Create Post" as + # submit button label, otherwise, it uses "Update Post". + # + # Those labels can be customized using I18n, under the helpers.submit key and accept + # the %{model} as translation interpolation: + # + # en: + # helpers: + # submit: + # create: "Create a %{model}" + # update: "Confirm changes to %{model}" + # + # It also searches for a key specific for the given object: + # + # en: + # helpers: + # submit: + # post: + # create: "Add %{model}" + # + def submit(value=nil, options={}) + value, options = nil, value if value.is_a?(Hash) + value ||= submit_default_value + @template.submit_tag(value, options) + end + + # Add the submit button for the given form. When no value is given, it checks + # if the object is a new resource or not to create the proper label: + # + # <%= form_for @post do |f| %> + # <%= f.button %> + # <% end %> + # + # In the example above, if @post is a new record, it will use "Create Post" as + # button label, otherwise, it uses "Update Post". + # + # Those labels can be customized using I18n, under the helpers.submit key + # (the same as submit helper) and accept the %{model} as translation interpolation: + # + # en: + # helpers: + # submit: + # create: "Create a %{model}" + # update: "Confirm changes to %{model}" + # + # It also searches for a key specific for the given object: + # + # en: + # helpers: + # submit: + # post: + # create: "Add %{model}" + # + # ==== Examples + # button("Create a post") + # # => <button name='button' type='submit'>Create post</button> + # + # button do + # content_tag(:strong, 'Ask me!') + # end + # # => <button name='button' type='submit'> + # # <strong>Ask me!</strong> + # # </button> + # + def button(value = nil, options = {}, &block) + value, options = nil, value if value.is_a?(Hash) + value ||= submit_default_value + @template.button_tag(value, options, &block) + end + + def emitted_hidden_id? + @emitted_hidden_id ||= nil + end + + private + def objectify_options(options) + @default_options.merge(options.merge(object: @object)) + end + + def submit_default_value + 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 + else + @object_name.to_s.humanize + end + + defaults = [] + defaults << :"helpers.submit.#{object_name}.#{key}" + defaults << :"helpers.submit.#{key}" + defaults << "#{key.to_s.humanize} #{model}" + + I18n.t(defaults.shift, model: model, default: defaults) + end + + def nested_attributes_association?(association_name) + @object.respond_to?("#{association_name}_attributes=") + end + + def fields_for_with_nested_attributes(association_name, association, options, block) + name = "#{object_name}[#{association_name}_attributes]" + association = convert_to_model(association) + + if association.respond_to?(:persisted?) + association = [association] if @object.send(association_name).respond_to?(:to_ary) + elsif !association.respond_to?(:to_ary) + association = @object.send(association_name) + end + + if association.respond_to?(:to_ary) + 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 + output << fields_for_nested_model("#{name}[#{options[:child_index]}]", child, options, block) + end + output + elsif association + fields_for_nested_model(name, association, options, block) + end + end + + def fields_for_nested_model(name, object, fields_options, block) + object = convert_to_model(object) + emit_hidden_id = object.persisted? && fields_options.fetch(:include_id) { + options.fetch(:include_id, true) + } + + @template.fields_for(name, object, fields_options) do |f| + output = @template.capture(f, &block) + output.concat f.hidden_field(:id) if output && emit_hidden_id && !f.emitted_hidden_id? + output + end + end + + def nested_child_index(name) + @nested_child_index[name] ||= -1 + @nested_child_index[name] += 1 + end + end + end + + ActiveSupport.on_load(:action_view) do + cattr_accessor(:default_form_builder) { ::ActionView::Helpers::FormBuilder } + end +end diff --git a/actionview/lib/action_view/helpers/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb new file mode 100644 index 0000000000..ad26505086 --- /dev/null +++ b/actionview/lib/action_view/helpers/form_options_helper.rb @@ -0,0 +1,832 @@ +require 'cgi' +require 'erb' +require 'action_view/helpers/form_helper' +require 'active_support/core_ext/string/output_safety' +require 'active_support/core_ext/array/extract_options' +require 'active_support/core_ext/array/wrap' + +module ActionView + # = Action View Form Option Helpers + module Helpers + # Provides a number of methods for turning different kinds of containers into a set of option tags. + # + # The <tt>collection_select</tt>, <tt>select</tt> and <tt>time_zone_select</tt> methods take an <tt>options</tt> parameter, a hash: + # + # * <tt>:include_blank</tt> - set to true or a prompt string if the first option element of the select element is a blank. Useful if there is not a default value required for the select element. + # + # select("post", "category", Post::CATEGORIES, {include_blank: true}) + # + # could become: + # + # <select name="post[category]"> + # <option></option> + # <option>joke</option> + # <option>poem</option> + # </select> + # + # Another common case is a select tag for a <tt>belongs_to</tt>-associated object. + # + # Example with @post.person_id => 2: + # + # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {include_blank: 'None'}) + # + # could become: + # + # <select name="post[person_id]"> + # <option value="">None</option> + # <option value="1">David</option> + # <option value="2" selected="selected">Sam</option> + # <option value="3">Tobias</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. + # + # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {prompt: 'Select Person'}) + # + # could become: + # + # <select name="post[person_id]"> + # <option value="">Select Person</option> + # <option value="1">David</option> + # <option value="2">Sam</option> + # <option value="3">Tobias</option> + # </select> + # + # 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 + # option to be in the +html_options+ parameter. + # + # select("album[]", "genre", %w[rap rock country], {}, { index: nil }) + # + # becomes: + # + # <select name="album[][genre]" id="album__genre"> + # <option value="rap">rap</option> + # <option value="rock">rock</option> + # <option value="country">country</option> + # </select> + # + # * <tt>:disabled</tt> - can be a single value or an array of values that will be disabled options in the final output. + # + # select("post", "category", Post::CATEGORIES, {disabled: 'restricted'}) + # + # could become: + # + # <select name="post[category]"> + # <option></option> + # <option>joke</option> + # <option>poem</option> + # <option disabled="disabled">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? }}) + # + # 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]"> + # <option value="1" disabled="disabled">2008 stuff</option> + # <option value="2" disabled="disabled">Christmas</option> + # <option value="3">Jokes</option> + # <option value="4">Poems</option> + # </select> + # + module FormOptionsHelper + # ERB::Util can mask some helpers like textilize. Make sure to include them. + include TextHelper + + # Create a select tag and a series of contained option tags for the provided object and method. + # The option currently held by the object will be selected, provided that the object is available. + # + # There are two possible formats for the choices parameter, corresponding to other helpers' output: + # * A flat collection: see options_for_select + # * A nested collection: see grouped_options_for_select + # + # Example with @post.person_id => 1: + # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, { include_blank: true }) + # + # could become: + # + # <select name="post[person_id]"> + # <option value=""></option> + # <option value="1" selected="selected">David</option> + # <option value="2">Sam</option> + # <option value="3">Tobias</option> + # </select> + # + # This can be used to provide a default set of options in the standard way: before rendering the create form, a + # new model instance is assigned the default options and bound to @model_name. Usually this model is not saved + # to the database. Instead, a second model object is created when the create request is received. + # This allows the user to submit a form page more than once with the expected results of creating multiple records. + # In addition, this allows a single partial to be used to generate form inputs for both edit and create forms. + # + # By default, <tt>post.person_id</tt> is the selected option. Specify <tt>selected: value</tt> to use a different selection + # or <tt>selected: nil</tt> to leave all options unselected. Similarly, you can specify values to be disabled in the option + # tags by specifying the <tt>:disabled</tt> option. This can either be a single value or an array of values to be disabled. + # + # ==== Gotcha + # + # The HTML specification says when +multiple+ parameter passed to select and all options got deselected + # web browsers do not send any value to server. Unfortunately this introduces a gotcha: + # if an +User+ model has many +roles+ and have +role_ids+ accessor, and in the form that edits roles of the user + # the user deselects all roles from +role_ids+ multiple select box, no +role_ids+ parameter is sent. So, + # any mass-assignment idiom like + # + # @user.update(params[:user]) + # + # wouldn't update roles. + # + # To prevent this the helper generates an auxiliary hidden field before + # every multiple select. The hidden field has the same name as multiple select and blank value. + # + # This way, the client either sends only the hidden field (representing + # the deselected multiple select box), or both fields. Since the HTML specification + # says key/value pairs have to be sent in the same order they appear in the + # form, and parameters extraction gets the last occurrence of any repeated + # key in the query string, that works for ordinary forms. + # + # In case if you don't want the helper to generate this hidden field you can specify + # <tt>include_hidden: false</tt> option. + # + def select(object, method, choices, options = {}, html_options = {}) + Tags::Select.new(object, method, self, choices, options, html_options).render + end + + # Returns <tt><select></tt> and <tt><option></tt> tags for the collection of existing return values of + # +method+ for +object+'s class. The value returned from calling +method+ on the instance +object+ will + # be selected. If calling +method+ returns +nil+, no selection is made without including <tt>:prompt</tt> + # or <tt>:include_blank</tt> in the +options+ hash. + # + # The <tt>:value_method</tt> and <tt>:text_method</tt> parameters are methods to be called on each member + # of +collection+. The return values are used as the +value+ attribute and contents of each + # <tt><option></tt> tag, respectively. They can also be any object that responds to +call+, such + # as a +proc+, that will be called for each member of the +collection+ to + # retrieve the value/text. + # + # Example object structure for use with this method: + # + # class Post < ActiveRecord::Base + # belongs_to :author + # end + # + # class Author < ActiveRecord::Base + # has_many :posts + # def name_with_initial + # "#{first_name.first}. #{last_name}" + # end + # end + # + # Sample usage (selecting the associated Author for an instance of Post, <tt>@post</tt>): + # + # 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]"> + # <option value="">Please select</option> + # <option value="1" selected="selected">D. Heinemeier Hansson</option> + # <option value="2">D. Thomas</option> + # <option value="3">M. Clark</option> + # </select> + def collection_select(object, method, collection, value_method, text_method, options = {}, html_options = {}) + Tags::CollectionSelect.new(object, method, self, collection, value_method, text_method, options, html_options).render + end + + # Returns <tt><select></tt>, <tt><optgroup></tt> and <tt><option></tt> tags for the collection of existing return values of + # +method+ for +object+'s class. The value returned from calling +method+ on the instance +object+ will + # be selected. If calling +method+ returns +nil+, no selection is made without including <tt>:prompt</tt> + # or <tt>:include_blank</tt> in the +options+ hash. + # + # Parameters: + # * +object+ - The instance of the class to be used for the select tag + # * +method+ - The attribute of +object+ corresponding to the select tag + # * +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 + # 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. + # * +option_value_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 contents of its <tt><option></tt> tag. + # + # Example object structure for use with this method: + # + # class Continent < ActiveRecord::Base + # has_many :countries + # # attribs: id, name + # end + # + # class Country < ActiveRecord::Base + # belongs_to :continent + # # attribs: id, name, continent_id + # end + # + # class City < ActiveRecord::Base + # belongs_to :country + # # attribs: id, name, country_id + # end + # + # Sample usage: + # + # grouped_collection_select(:city, :country_id, @continents, :countries, :name, :id, :name) + # + # Possible output: + # + # <select name="city[country_id]"> + # <optgroup label="Africa"> + # <option value="1">South Africa</option> + # <option value="3">Somalia</option> + # </optgroup> + # <optgroup label="Europe"> + # <option value="7" selected="selected">Denmark</option> + # <option value="2">Ireland</option> + # </optgroup> + # </select> + # + def grouped_collection_select(object, method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {}) + Tags::GroupedCollectionSelect.new(object, method, self, collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options).render + end + + # Return select and option tags for the given object and method, using + # #time_zone_options_for_select to generate the list of option tags. + # + # In addition to the <tt>:include_blank</tt> option documented above, + # this method also supports a <tt>:model</tt> option, which defaults + # to ActiveSupport::TimeZone. This may be used by users to specify a + # different time zone model object. (See +time_zone_options_for_select+ + # for more information.) + # + # 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 ActiveSupport::TimeZone.us_zones as a convenience + # for obtaining a list of the US time zones, or a Regexp to select the zones + # of your choice) + # + # Finally, this method supports a <tt>:default</tt> option, which selects + # a default ActiveSupport::TimeZone if the object's time zone is +nil+. + # + # time_zone_select( "user", "time_zone", nil, include_blank: true) + # + # time_zone_select( "user", "time_zone", nil, default: "Pacific Time (US & Canada)" ) + # + # time_zone_select( "user", 'time_zone', ActiveSupport::TimeZone.us_zones, default: "Pacific Time (US & Canada)") + # + # time_zone_select( "user", 'time_zone', [ ActiveSupport::TimeZone['Alaska'], ActiveSupport::TimeZone['Hawaii'] ]) + # + # time_zone_select( "user", 'time_zone', /Australia/) + # + # time_zone_select( "user", "time_zone", ActiveSupport::TimeZone.all.sort, model: ActiveSupport::TimeZone) + def time_zone_select(object, method, priority_zones = nil, options = {}, html_options = {}) + Tags::TimeZoneSelect.new(object, method, self, priority_zones, options, html_options).render + end + + # Accepts a container (hash, array, enumerable, your type) and returns a string of option tags. Given a container + # where the elements respond to first and last (such as a two-element array), the "lasts" serve as option values and + # the "firsts" as option text. Hashes are turned into this form automatically, so the keys become "firsts" and values + # become lasts. If +selected+ is specified, the matching "last" or element will get the selected option-tag. +selected+ + # may also be an array of values to be selected when using a multiple select. + # + # options_for_select([["Dollar", "$"], ["Kroner", "DKK"]]) + # # => <option value="$">Dollar</option> + # # => <option value="DKK">Kroner</option> + # + # options_for_select([ "VISA", "MasterCard" ], "MasterCard") + # # => <option>VISA</option> + # # => <option selected="selected">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> + # + # 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> + # # => <option value="USA" class="bold" selected="selected">USA</option> + # # => <option value="Sweden" selected="selected">Sweden</option> + # + # options_for_select([["Dollar", "$", {class: "bold"}], ["Kroner", "DKK", {onclick: "alert('HI');"}]]) + # # => <option value="$" class="bold">Dollar</option> + # # => <option value="DKK" onclick="alert('HI');">Kroner</option> + # + # If you wish to specify disabled option tags, set +selected+ to be a hash, with <tt>:disabled</tt> being either a value + # or array of values to be disabled. In this case, you can use <tt>:selected</tt> to specify selected option tags. + # + # options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], disabled: "Super Platinum") + # # => <option value="Free">Free</option> + # # => <option value="Basic">Basic</option> + # # => <option value="Advanced">Advanced</option> + # # => <option value="Super Platinum" disabled="disabled">Super Platinum</option> + # + # options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], disabled: ["Advanced", "Super Platinum"]) + # # => <option value="Free">Free</option> + # # => <option value="Basic">Basic</option> + # # => <option value="Advanced" disabled="disabled">Advanced</option> + # # => <option value="Super Platinum" disabled="disabled">Super Platinum</option> + # + # options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], selected: "Free", disabled: "Super Platinum") + # # => <option value="Free" selected="selected">Free</option> + # # => <option value="Basic">Basic</option> + # # => <option value="Advanced">Advanced</option> + # # => <option value="Super Platinum" disabled="disabled">Super Platinum</option> + # + # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag. + def options_for_select(container, selected = nil) + return container if String === container + + selected, disabled = extract_selected_and_disabled(selected).map do |r| + Array(r).map { |item| item.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 } + + html_attributes[:selected] = 'selected' if option_value_selected?(value, selected) + html_attributes[:disabled] = 'disabled' if disabled && option_value_selected?(value, disabled) + html_attributes[:value] = value + + content_tag_string(:option, text, html_attributes) + end.join("\n").html_safe + end + + # Returns a string of option tags that have been compiled by iterating over the +collection+ and assigning + # the result of a call to the +value_method+ as the option value and the +text_method+ as the option text. + # + # options_from_collection_for_select(@people, 'id', 'name') + # # => <option value="#{person.id}">#{person.name}</option> + # + # This is more often than not used inside a #select_tag like this example: + # + # select_tag 'person', options_from_collection_for_select(@people, 'id', 'name') + # + # If +selected+ is specified as a value or array of values, the element(s) returning a match on +value_method+ + # will be selected option tag(s). + # + # If +selected+ is specified as a Proc, those members of the collection that return true for the anonymous + # function are the selected values. + # + # +selected+ can also be a hash, specifying both <tt>:selected</tt> and/or <tt>:disabled</tt> values as required. + # + # Be sure to specify the same class as the +value_method+ when specifying selected or disabled options. + # Failure to do this will produce undesired results. Example: + # options_from_collection_for_select(@people, 'id', 'name', '1') + # Will not select a person with the id of 1 because 1 (an Integer) is not the same as '1' (a string) + # options_from_collection_for_select(@people, 'id', 'name', 1) + # should produce the desired results. + def options_from_collection_for_select(collection, value_method, text_method, selected = nil) + options = collection.map do |element| + [value_for_collection(element, text_method), value_for_collection(element, value_method), option_html_attributes(element)] + end + selected, disabled = extract_selected_and_disabled(selected) + select_deselect = { + :selected => extract_values_from_collection(collection, value_method, selected), + :disabled => extract_values_from_collection(collection, value_method, disabled) + } + + options_for_select(options, select_deselect) + end + + # Returns a string of <tt><option></tt> tags, like <tt>options_from_collection_for_select</tt>, but + # groups them by <tt><optgroup></tt> tags based on the object relationships of the arguments. + # + # Parameters: + # * +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 + # 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. + # * +option_value_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 contents of its <tt><option></tt> tag. + # * +selected_key+ - A value equal to the +value+ attribute for one of the <tt><option></tt> tags, + # which will have the +selected+ attribute set. Corresponds to the return value of one of the calls + # to +option_key_method+. If +nil+, no selection is made. Can also be a hash if disabled values are + # to be specified. + # + # Example object structure for use with this method: + # + # class Continent < ActiveRecord::Base + # has_many :countries + # # attribs: id, name + # end + # + # class Country < ActiveRecord::Base + # belongs_to :continent + # # attribs: id, name, continent_id + # end + # + # Sample usage: + # option_groups_from_collection_for_select(@continents, :countries, :name, :id, :name, 3) + # + # Possible output: + # <optgroup label="Africa"> + # <option value="1">Egypt</option> + # <option value="4">Rwanda</option> + # ... + # </optgroup> + # <optgroup label="Asia"> + # <option value="3" selected="selected">China</option> + # <option value="12">India</option> + # <option value="5">Japan</option> + # ... + # </optgroup> + # + # <b>Note:</b> Only the <tt><optgroup></tt> and <tt><option></tt> tags are returned, so you still have to + # wrap the output in an appropriate <tt><select></tt> tag. + def option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key = nil) + collection.map do |group| + 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)) + end.join.html_safe + end + + # Returns a string of <tt><option></tt> tags, like <tt>options_for_select</tt>, but + # wraps them with <tt><optgroup></tt> tags. + # + # Parameters: + # * +grouped_options+ - Accepts a nested array or hash of strings. The first value serves as the + # <tt><optgroup></tt> label while the second value must be an array of options. The second value can be a + # nested array of text-value pairs. See <tt>options_for_select</tt> for more info. + # Ex. ["North America",[["United States","US"],["Canada","CA"]]] + # * +selected_key+ - A value equal to the +value+ attribute for one of the <tt><option></tt> tags, + # which will have the +selected+ attribute set. Note: It is possible for this value to match multiple options + # as you might have the same option in multiple groups. Each will then get <tt>selected="selected"</tt>. + # + # Options: + # * <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. + # * <tt>:divider</tt> - the divider for the options groups. + # + # grouped_options = [ + # ['North America', + # [['United States','US'],'Canada']], + # ['Europe', + # ['Denmark','Germany','France']] + # ] + # grouped_options_for_select(grouped_options) + # + # grouped_options = { + # 'North America' => [['United States','US'], 'Canada'], + # 'Europe' => ['Denmark','Germany','France'] + # } + # grouped_options_for_select(grouped_options) + # + # Possible output: + # <optgroup label="North America"> + # <option value="US">United States</option> + # <option value="Canada">Canada</option> + # </optgroup> + # <optgroup label="Europe"> + # <option value="Denmark">Denmark</option> + # <option value="Germany">Germany</option> + # <option value="France">France</option> + # </optgroup> + # + # grouped_options = [ + # [['United States','US'], 'Canada'], + # ['Denmark','Germany','France'] + # ] + # grouped_options_for_select(grouped_options, nil, divider: '---------') + # + # Possible output: + # <optgroup label="---------"> + # <option value="US">United States</option> + # <option value="Canada">Canada</option> + # </optgroup> + # <optgroup label="---------"> + # <option value="Denmark">Denmark</option> + # <option value="Germany">Germany</option> + # <option value="France">France</option> + # </optgroup> + # + # <b>Note:</b> Only the <tt><optgroup></tt> and <tt><option></tt> tags are returned, so you still have to + # wrap the output in an appropriate <tt><select></tt> tag. + def grouped_options_for_select(grouped_options, selected_key = nil, options = {}) + if options.is_a?(Hash) + prompt = options[:prompt] + divider = options[:divider] + else + prompt = options + message = "Passing the prompt to grouped_options_for_select as an argument is deprecated. " \ + "Please use an options hash like `{ prompt: #{prompt.inspect} }`." + ActiveSupport::Deprecation.warn message + end + + body = "".html_safe + + if prompt + body.safe_concat content_tag(:option, prompt_text(prompt), :value => "") + end + + grouped_options.each do |container| + if divider + label = divider + else + label, container = container + end + body.safe_concat content_tag(:optgroup, options_for_select(container, selected_key), :label => label) + 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 + # 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 + # ActiveSupport::TimeZone.us_zones as a convenience for obtaining a list + # 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. + # + # By default, +model+ is the ActiveSupport::TimeZone constant (which can + # be obtained in Active Record as a value object). The only requirement + # is that the +model+ parameter be an object that responds to +all+, and + # returns an array of objects that represent time zones. + # + # NOTE: Only the option tags are returned, you have to wrap this call in + # a regular HTML select tag. + def time_zone_options_for_select(selected = nil, priority_zones = nil, model = ::ActiveSupport::TimeZone) + zone_options = "".html_safe + + zones = model.all + convert_zones = lambda { |list| list.map { |z| [ z.to_s, z.name ] } } + + if priority_zones + if priority_zones.is_a?(Regexp) + priority_zones = zones.select { |z| z =~ priority_zones } + end + + zone_options.safe_concat options_for_select(convert_zones[priority_zones], selected) + zone_options.safe_concat content_tag(:option, '-------------', :value => '', :disabled => 'disabled') + zone_options.safe_concat "\n" + + zones = zones - priority_zones + end + + zone_options.safe_concat options_for_select(convert_zones[zones], selected) + end + + # Returns radio button tags for the collection of existing return values + # of +method+ for +object+'s class. The value returned from calling + # +method+ on the instance +object+ will be selected. If calling +method+ + # returns +nil+, no selection is made. + # + # The <tt>:value_method</tt> and <tt>:text_method</tt> parameters are + # methods to be called on each member of +collection+. The return values + # are used as the +value+ attribute and contents of each radio button tag, + # respectively. They can also be any object that responds to +call+, such + # as a +proc+, that will be called for each member of the +collection+ to + # retrieve the value/text. + # + # Example object structure for use with this method: + # class Post < ActiveRecord::Base + # belongs_to :author + # end + # class Author < ActiveRecord::Base + # has_many :posts + # def name_with_initial + # "#{first_name.first}. #{last_name}" + # end + # end + # + # Sample usage (selecting the associated Author for an instance of Post, <tt>@post</tt>): + # collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) + # + # If <tt>@post.author_id</tt> is already <tt>1</tt>, this would return: + # <input id="post_author_id_1" name="post[author_id]" type="radio" value="1" checked="checked" /> + # <label for="post_author_id_1">D. Heinemeier Hansson</label> + # <input id="post_author_id_2" name="post[author_id]" type="radio" value="2" /> + # <label for="post_author_id_2">D. Thomas</label> + # <input id="post_author_id_3" name="post[author_id]" type="radio" value="3" /> + # <label for="post_author_id_3">M. Clark</label> + # + # It is also possible to customize the way the elements will be shown by + # giving a block to the method: + # collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) do |b| + # b.label { b.radio_button } + # end + # + # The argument passed to the block is a special kind of builder for this + # collection, which has the ability to generate the label and radio button + # for the current item in the collection, with proper text and value. + # Using it, you can change the label and radio button display order or + # 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: + # 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 + # + # There are also three special methods available: <tt>object</tt>, <tt>text</tt> and + # <tt>value</tt>, which are the current item being rendered, its text and value methods, + # respectively. You can use them like this: + # 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 + 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 + + # Returns check box tags for the collection of existing return values of + # +method+ for +object+'s class. The value returned from calling +method+ + # on the instance +object+ will be selected. If calling +method+ returns + # +nil+, no selection is made. + # + # The <tt>:value_method</tt> and <tt>:text_method</tt> parameters are + # methods to be called on each member of +collection+. The return values + # are used as the +value+ attribute and contents of each check box tag, + # respectively. They can also be any object that responds to +call+, such + # as a +proc+, that will be called for each member of the +collection+ to + # retrieve the value/text. + # + # Example object structure for use with this method: + # class Post < ActiveRecord::Base + # has_and_belongs_to_many :author + # end + # class Author < ActiveRecord::Base + # has_and_belongs_to_many :posts + # def name_with_initial + # "#{first_name.first}. #{last_name}" + # end + # end + # + # Sample usage (selecting the associated Author for an instance of Post, <tt>@post</tt>): + # collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) + # + # If <tt>@post.author_ids</tt> is already <tt>[1]</tt>, this would return: + # <input id="post_author_ids_1" name="post[author_ids][]" type="checkbox" value="1" checked="checked" /> + # <label for="post_author_ids_1">D. Heinemeier Hansson</label> + # <input id="post_author_ids_2" name="post[author_ids][]" type="checkbox" value="2" /> + # <label for="post_author_ids_2">D. Thomas</label> + # <input id="post_author_ids_3" name="post[author_ids][]" type="checkbox" value="3" /> + # <label for="post_author_ids_3">M. Clark</label> + # <input name="post[author_ids][]" type="hidden" value="" /> + # + # It is also possible to customize the way the elements will be shown by + # giving a block to the method: + # collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) do |b| + # b.label { b.check_box } + # end + # + # The argument passed to the block is a special kind of builder for this + # collection, which has the ability to generate the label and check box + # for the current item in the collection, with proper text and value. + # Using it, you can change the label and check box display order or even + # 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: + # 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 + # + # There are also three special methods available: <tt>object</tt>, <tt>text</tt> and + # <tt>value</tt>, which are the current item being rendered, its text and value methods, + # respectively. You can use them like this: + # 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 + 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 + + private + def option_html_attributes(element) + if Array === element + element.select { |e| Hash === e }.reduce({}, :merge!) + else + {} + end + end + + def option_text_and_value(option) + # Options are [text, value] pairs or strings used for both. + if !option.is_a?(String) && option.respond_to?(:first) && option.respond_to?(:last) + option = option.reject { |e| Hash === e } if Array === option + [option.first, option.last] + else + [option, option] + end + end + + def option_value_selected?(value, selected) + Array(selected).include? value + end + + def extract_selected_and_disabled(selected) + if selected.is_a?(Proc) + [selected, nil] + else + selected = Array.wrap(selected) + options = selected.extract_options!.symbolize_keys + selected_items = options.fetch(:selected, selected) + [selected_items, options[:disabled]] + end + end + + def extract_values_from_collection(collection, value_method, selected) + if selected.is_a?(Proc) + collection.map do |element| + element.send(value_method) if selected.call(element) + end.compact + else + selected + end + end + + def value_for_collection(item, value) + value.respond_to?(:call) ? value.call(item) : item.send(value) + end + + def prompt_text(prompt) + prompt.kind_of?(String) ? prompt : I18n.translate('helpers.select.prompt', :default => 'Please select') + end + end + + class FormBuilder + # Wraps ActionView::Helpers::FormOptionsHelper#select for form builders: + # + # <%= form_for @post do |f| %> + # <%= f.select :person_id, Person.all.collect {|p| [ p.name, p.id ] }, { include_blank: true }) %> + # <%= f.submit %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + def select(method, choices, options = {}, html_options = {}) + @template.select(@object_name, method, choices, objectify_options(options), @default_options.merge(html_options)) + end + + # Wraps ActionView::Helpers::FormOptionsHelper#collection_select for form builders: + # + # <%= form_for @post do |f| %> + # <%= f.collection_select :person_id, Author.all, :id, :name_with_initial, prompt: true %> + # <%= f.submit %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + def collection_select(method, collection, value_method, text_method, options = {}, html_options = {}) + @template.collection_select(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options)) + end + + # Wraps ActionView::Helpers::FormOptionsHelper#grouped_collection_select for form builders: + # + # <%= form_for @city do |f| %> + # <%= f.grouped_collection_select :country_id, :country_id, @continents, :countries, :name, :id, :name %> + # <%= f.submit %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + def grouped_collection_select(method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {}) + @template.grouped_collection_select(@object_name, method, collection, group_method, group_label_method, option_key_method, option_value_method, objectify_options(options), @default_options.merge(html_options)) + end + + # Wraps ActionView::Helpers::FormOptionsHelper#time_zone_select for form builders: + # + # <%= form_for @user do |f| %> + # <%= f.time_zone_select :time_zone, nil, include_blank: true %> + # <%= f.submit %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + def time_zone_select(method, priority_zones = nil, options = {}, html_options = {}) + @template.time_zone_select(@object_name, method, priority_zones, objectify_options(options), @default_options.merge(html_options)) + end + + # Wraps ActionView::Helpers::FormOptionsHelper#collection_check_boxes for form builders: + # + # <%= form_for @post do |f| %> + # <%= f.collection_check_boxes :author_ids, Author.all, :id, :name_with_initial %> + # <%= f.submit %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + def collection_check_boxes(method, collection, value_method, text_method, options = {}, html_options = {}, &block) + @template.collection_check_boxes(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options), &block) + end + + # Wraps ActionView::Helpers::FormOptionsHelper#collection_radio_buttons for form builders: + # + # <%= form_for @post do |f| %> + # <%= f.collection_radio_buttons :author_id, Author.all, :id, :name_with_initial %> + # <%= f.submit %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + def collection_radio_buttons(method, collection, value_method, text_method, options = {}, html_options = {}, &block) + @template.collection_radio_buttons(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options), &block) + end + end + end +end diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb new file mode 100644 index 0000000000..3fa7696b83 --- /dev/null +++ b/actionview/lib/action_view/helpers/form_tag_helper.rb @@ -0,0 +1,744 @@ +require 'cgi' +require 'action_view/helpers/tag_helper' +require 'active_support/core_ext/string/output_safety' +require 'active_support/core_ext/module/attribute_accessors' + +module ActionView + # = Action View Form Tag Helpers + module Helpers + # Provides a number of methods for creating form tags that don't rely on an Active Record object assigned to the template like + # FormHelper does. Instead, you provide the names and values manually. + # + # NOTE: The HTML options <tt>disabled</tt>, <tt>readonly</tt>, and <tt>multiple</tt> can all be treated as booleans. So specifying + # <tt>disabled: true</tt> will give <tt>disabled="disabled"</tt>. + module FormTagHelper + extend ActiveSupport::Concern + + include UrlHelper + include TextHelper + + 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 + # ActionController::Base#url_for. The method for the form defaults to POST. + # + # ==== Options + # * <tt>:multipart</tt> - If set to true, the enctype is set to "multipart/form-data". + # * <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. + # * 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. + # + # ==== Examples + # form_tag('/posts') + # # => <form action="/posts" method="post"> + # + # form_tag('/posts/1', method: :put) + # # => <form action="/posts/1" method="post"> ... <input name="_method" type="hidden" value="put" /> ... + # + # form_tag('/upload', multipart: true) + # # => <form action="/upload" method="post" enctype="multipart/form-data"> + # + # <%= form_tag('/posts') do -%> + # <div><%= submit_tag 'Save' %></div> + # <% end -%> + # # => <form action="/posts" method="post"><div><input type="submit" name="commit" value="Save" /></div></form> + # + # <%= form_tag('/posts', remote: true) %> + # # => <form action="/posts" method="post" data-remote="true"> + # + # form_tag('http://far.away.com/form', authenticity_token: false) + # # form without authenticity token + # + # form_tag('http://far.away.com/form', authenticity_token: "cf50faa3fe97702ca1ae") + # # form with custom authenticity token + # + def form_tag(url_for_options = {}, options = {}, &block) + html_options = html_options_for_form(url_for_options, options) + if block_given? + form_tag_in_block(html_options, &block) + else + form_tag_html(html_options) + end + end + + # Creates a dropdown selection box, or if the <tt>:multiple</tt> option is set to true, a multiple + # choice selection box. + # + # Helpers::FormOptions can be used to create common select boxes such as countries, time zones, or + # 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>: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. + # * <tt>:prompt</tt> - Create a prompt option with blank value and the text asking user to select something + # * 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", "<option>David</option>".html_safe + # # => <select id="people" name="people"><option>David</option></select> + # + # select_tag "count", "<option>1</option><option>2</option><option>3</option><option>4</option>".html_safe + # # => <select id="count" name="count"><option>1</option><option>2</option> + # # <option>3</option><option>4</option></select> + # + # select_tag "colors", "<option>Red</option><option>Green</option><option>Blue</option>".html_safe, multiple: true + # # => <select id="colors" multiple="multiple" name="colors[]"><option>Red</option> + # # <option>Green</option><option>Blue</option></select> + # + # select_tag "locations", "<option>Home</option><option selected='selected'>Work</option><option>Out</option>".html_safe + # # => <select id="locations" name="locations"><option>Home</option><option selected='selected'>Work</option> + # # <option>Out</option></select> + # + # select_tag "access", "<option>Read</option><option>Write</option>".html_safe, multiple: true, class: 'form_input' + # # => <select class="form_input" id="access" multiple="multiple" name="access[]"><option>Read</option> + # # <option>Write</option></select> + # + # select_tag "people", options_from_collection_for_select(@people, "id", "name"), include_blank: true + # # => <select id="people" name="people"><option value=""></option><option value="1">David</option></select> + # + # select_tag "people", options_from_collection_for_select(@people, "id", "name"), prompt: "Select something" + # # => <select id="people" name="people"><option value="">Select something</option><option value="1">David</option></select> + # + # select_tag "destination", "<option>NYC</option><option>Paris</option><option>Rome</option>".html_safe, disabled: true + # # => <select disabled="disabled" id="destination" name="destination"><option>NYC</option> + # # <option>Paris</option><option>Rome</option></select> + # + # select_tag "credit_card", options_for_select([ "VISA", "MasterCard" ], "MasterCard") + # # => <select id="credit_card" name="credit_card"><option>VISA</option> + # # <option selected="selected">MasterCard</option></select> + def select_tag(name, option_tags = nil, options = {}) + 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) + end + + if prompt = options.delete(:prompt) + option_tags = content_tag(:option, prompt, :value => '').safe_concat(option_tags) + end + + content_tag :select, 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 + # or a search query. + # + # ==== Options + # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input. + # * <tt>:size</tt> - The number of visible characters that will fit in the input. + # * <tt>:maxlength</tt> - The maximum number of characters that the browser will allow the user to enter. + # * <tt>:placeholder</tt> - The text contained in the field by default which is removed when the field receives focus. + # * Any other key creates standard HTML attributes for the tag. + # + # ==== Examples + # text_field_tag 'name' + # # => <input id="name" name="name" type="text" /> + # + # text_field_tag 'query', 'Enter your search query here' + # # => <input id="query" name="query" type="text" value="Enter your search query here" /> + # + # text_field_tag 'search', nil, placeholder: 'Enter search term...' + # # => <input id="search" name="search" placeholder="Enter search term..." type="text" /> + # + # text_field_tag 'request', nil, class: 'special_input' + # # => <input class="special_input" id="request" name="request" type="text" /> + # + # text_field_tag 'address', '', size: 75 + # # => <input id="address" name="address" size="75" type="text" value="" /> + # + # text_field_tag 'zip', nil, maxlength: 5 + # # => <input id="zip" maxlength="5" name="zip" type="text" /> + # + # text_field_tag 'payment_amount', '$0.00', disabled: true + # # => <input disabled="disabled" id="payment_amount" name="payment_amount" type="text" value="$0.00" /> + # + # text_field_tag 'ip', '0.0.0.0', maxlength: 15, size: 20, class: "ip-input" + # # => <input class="ip-input" id="ip" maxlength="15" name="ip" size="20" type="text" value="0.0.0.0" /> + def text_field_tag(name, value = nil, options = {}) + tag :input, { "type" => "text", "name" => name, "id" => sanitize_to_id(name), "value" => value }.update(options.stringify_keys) + end + + # Creates a label element. Accepts a block. + # + # ==== Options + # * Creates standard HTML attributes for the tag. + # + # ==== Examples + # label_tag 'name' + # # => <label for="name">Name</label> + # + # label_tag 'name', 'Your name' + # # => <label for="name">Your name</label> + # + # label_tag 'name', nil, class: 'small_label' + # # => <label for="name" class="small_label">Name</label> + def label_tag(name = nil, content_or_options = nil, options = nil, &block) + if block_given? && content_or_options.is_a?(Hash) + options = content_or_options = content_or_options.stringify_keys + else + options ||= {} + options = options.stringify_keys + end + options["for"] = sanitize_to_id(name) unless name.blank? || options.has_key?("for") + content_tag :label, content_or_options || name.to_s.humanize, options, &block + end + + # Creates a hidden form input field used to transmit data that would be lost due to HTTP's statelessness or + # data that should be hidden from the user. + # + # ==== Options + # * Creates standard HTML attributes for the tag. + # + # ==== Examples + # hidden_field_tag 'tags_list' + # # => <input id="tags_list" name="tags_list" type="hidden" /> + # + # hidden_field_tag 'token', 'VUBJKB23UIVI1UU1VOBVI@' + # # => <input id="token" name="token" type="hidden" value="VUBJKB23UIVI1UU1VOBVI@" /> + # + # hidden_field_tag 'collected_input', '', onchange: "alert('Input collected!')" + # # => <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")) + end + + # Creates a file upload field. If you are using file uploads then you will also need + # to set the multipart option for the form tag: + # + # <%= form_tag '/upload', multipart: true do %> + # <label for="file">File to Upload</label> <%= file_field_tag "file" %> + # <%= submit_tag %> + # <% end %> + # + # The specified URL will then be passed a File object containing the selected file, or if the field + # was left blank, a StringIO object. + # + # ==== Options + # * Creates standard HTML attributes for the tag. + # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input. + # * <tt>:multiple</tt> - If set to true, *in most updated browsers* the user will be allowed to select multiple files. + # * <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_tag 'attachment' + # # => <input id="attachment" name="attachment" type="file" /> + # + # file_field_tag 'avatar', class: 'profile_input' + # # => <input class="profile_input" id="avatar" name="avatar" type="file" /> + # + # file_field_tag 'picture', disabled: true + # # => <input disabled="disabled" id="picture" name="picture" type="file" /> + # + # file_field_tag 'resume', value: '~/resume.doc' + # # => <input id="resume" name="resume" type="file" value="~/resume.doc" /> + # + # file_field_tag 'user_pic', accept: 'image/png,image/gif,image/jpeg' + # # => <input accept="image/png,image/gif,image/jpeg" id="user_pic" name="user_pic" type="file" /> + # + # 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")) + end + + # Creates a password field, a masked text field that will hide the users input behind a mask character. + # + # ==== Options + # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input. + # * <tt>:size</tt> - The number of visible characters that will fit in the input. + # * <tt>:maxlength</tt> - The maximum number of characters that the browser will allow the user to enter. + # * Any other key creates standard HTML attributes for the tag. + # + # ==== Examples + # password_field_tag 'pass' + # # => <input id="pass" name="pass" type="password" /> + # + # password_field_tag 'secret', 'Your secret here' + # # => <input id="secret" name="secret" type="password" value="Your secret here" /> + # + # password_field_tag 'masked', nil, class: 'masked_input_field' + # # => <input class="masked_input_field" id="masked" name="masked" type="password" /> + # + # password_field_tag 'token', '', size: 15 + # # => <input id="token" name="token" size="15" type="password" value="" /> + # + # password_field_tag 'key', nil, maxlength: 16 + # # => <input id="key" maxlength="16" name="key" type="password" /> + # + # password_field_tag 'confirm_pass', nil, disabled: true + # # => <input disabled="disabled" id="confirm_pass" name="confirm_pass" type="password" /> + # + # 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")) + end + + # Creates a text input area; use a textarea for longer text inputs such as blog posts or descriptions. + # + # ==== Options + # * <tt>:size</tt> - A string specifying the dimensions (columns by rows) of the textarea (e.g., "25x10"). + # * <tt>:rows</tt> - Specify the number of rows in the textarea + # * <tt>:cols</tt> - Specify the number of columns in the textarea + # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input. + # * <tt>:escape</tt> - By default, the contents of the text input are HTML escaped. + # If you need unescaped contents, set this to false. + # * Any other key creates standard HTML attributes for the tag. + # + # ==== Examples + # text_area_tag 'post' + # # => <textarea id="post" name="post"></textarea> + # + # text_area_tag 'bio', @user.bio + # # => <textarea id="bio" name="bio">This is my biography.</textarea> + # + # text_area_tag 'body', nil, rows: 10, cols: 25 + # # => <textarea cols="25" id="body" name="body" rows="10"></textarea> + # + # text_area_tag 'body', nil, size: "25x10" + # # => <textarea name="body" id="body" cols="25" rows="10"></textarea> + # + # text_area_tag 'description', "Description goes here.", disabled: true + # # => <textarea disabled="disabled" id="description" name="description">Description goes here.</textarea> + # + # text_area_tag 'comment', nil, class: 'comment_input' + # # => <textarea class="comment_input" id="comment" name="comment"></textarea> + def text_area_tag(name, content = nil, options = {}) + options = options.stringify_keys + + if size = options.delete("size") + options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split) + end + + escape = options.delete("escape") { true } + content = ERB::Util.html_escape(content) if escape + + content_tag :textarea, content.to_s.html_safe, { "name" => name, "id" => sanitize_to_id(name) }.update(options) + end + + # Creates a check box form input tag. + # + # ==== Options + # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input. + # * Any other key creates standard HTML options for the tag. + # + # ==== Examples + # check_box_tag 'accept' + # # => <input id="accept" name="accept" type="checkbox" value="1" /> + # + # check_box_tag 'rock', 'rock music' + # # => <input id="rock" name="rock" type="checkbox" value="rock music" /> + # + # check_box_tag 'receive_email', 'yes', true + # # => <input checked="checked" id="receive_email" name="receive_email" type="checkbox" value="yes" /> + # + # check_box_tag 'tos', 'yes', false, class: 'accept_tos' + # # => <input class="accept_tos" id="tos" name="tos" type="checkbox" value="yes" /> + # + # check_box_tag 'eula', 'accepted', false, disabled: true + # # => <input disabled="disabled" id="eula" name="eula" type="checkbox" value="accepted" /> + def check_box_tag(name, value = "1", checked = false, options = {}) + html_options = { "type" => "checkbox", "name" => name, "id" => sanitize_to_id(name), "value" => value }.update(options.stringify_keys) + html_options["checked"] = "checked" if checked + tag :input, html_options + end + + # Creates a radio button; use groups of radio buttons named the same to allow users to + # select from a group of options. + # + # ==== Options + # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input. + # * Any other key creates standard HTML options for the tag. + # + # ==== Examples + # radio_button_tag 'gender', 'male' + # # => <input id="gender_male" name="gender" type="radio" value="male" /> + # + # radio_button_tag 'receive_updates', 'no', true + # # => <input checked="checked" id="receive_updates_no" name="receive_updates" type="radio" value="no" /> + # + # radio_button_tag 'time_slot', "3:00 p.m.", false, disabled: true + # # => <input disabled="disabled" id="time_slot_300_pm" name="time_slot" type="radio" value="3:00 p.m." /> + # + # radio_button_tag 'color', "green", true, class: "color_input" + # # => <input checked="checked" class="color_input" id="color_green" name="color" type="radio" value="green" /> + def radio_button_tag(name, value, checked = false, options = {}) + html_options = { "type" => "radio", "name" => name, "id" => "#{sanitize_to_id(name)}_#{sanitize_to_id(value)}", "value" => value }.update(options.stringify_keys) + html_options["checked"] = "checked" if checked + tag :input, html_options + end + + # Creates a submit button with the text <tt>value</tt> as the caption. + # + # ==== Options + # * <tt>:data</tt> - This option can be used to add custom data attributes. + # * <tt>:disabled</tt> - If true, the user will not be able to use this input. + # * Any other key creates standard HTML options for the tag. + # + # ==== Data attributes + # + # * <tt>confirm: 'question?'</tt> - If present the unobtrusive JavaScript + # drivers will provide a prompt with the question specified. If the user accepts, + # 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. + # + # ==== Examples + # submit_tag + # # => <input name="commit" type="submit" value="Save changes" /> + # + # submit_tag "Edit this article" + # # => <input name="commit" type="submit" value="Edit this article" /> + # + # submit_tag "Save edits", disabled: true + # # => <input disabled="disabled" name="commit" 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 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" /> + # + # submit_tag "Save", data: { confirm: "Are you sure?" } + # # => <input name='commit' type='submit' value='Save' data-confirm="Are you sure?" /> + # + def submit_tag(value = "Save changes", options = {}) + options = options.stringify_keys + + tag :input, { "type" => "submit", "name" => "commit", "value" => value }.update(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. + # + # ==== Options + # * <tt>:data</tt> - This option can be used to add custom data attributes. + # * <tt>:disabled</tt> - If true, the user will not be able to + # use this input. + # * Any other key creates standard HTML options for the tag. + # + # ==== Data attributes + # + # * <tt>confirm: 'question?'</tt> - If present, the + # unobtrusive JavaScript drivers will provide a prompt with + # the question specified. If the user accepts, 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. + # + # ==== Examples + # button_tag + # # => <button name="button" type="submit">Button</button> + # + # button_tag(type: 'button') do + # content_tag(:strong, 'Ask me!') + # end + # # => <button name="button" type="button"> + # # <strong>Ask me!</strong> + # # </button> + # + # button_tag "Checkout", data: { disable_with => "Please wait..." } + # # => <button data-disable-with="Please wait..." name="button" type="submit">Checkout</button> + # + def button_tag(content_or_options = nil, options = nil, &block) + options = content_or_options if block_given? && content_or_options.is_a?(Hash) + options ||= {} + options = options.stringify_keys + + options.reverse_merge! 'name' => 'button', 'type' => 'submit' + + content_tag :button, content_or_options || 'Button', options, &block + end + + # Displays an image which when clicked will submit the form. + # + # <tt>source</tt> is passed to AssetTagHelper#path_to_image + # + # ==== Options + # * <tt>:data</tt> - This option can be used to add custom data attributes. + # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input. + # * Any other key creates standard HTML options for the tag. + # + # ==== Data attributes + # + # * <tt>confirm: 'question?'</tt> - This will add a JavaScript confirm + # prompt with the question specified. If the user accepts, the form is + # processed normally, otherwise no action is taken. + # + # ==== Examples + # image_submit_tag("login.png") + # # => <input alt="Login" src="/images/login.png" type="image" /> + # + # image_submit_tag("purchase.png", disabled: true) + # # => <input alt="Purchase" disabled="disabled" src="/images/purchase.png" type="image" /> + # + # image_submit_tag("search.png", class: 'search_button', alt: 'Find') + # # => <input alt="Find" class="search_button" src="/images/search.png" type="image" /> + # + # image_submit_tag("agree.png", disabled: true, class: "agree_disagree_button") + # # => <input alt="Agree" class="agree_disagree_button" disabled="disabled" src="/images/agree.png" type="image" /> + # + # image_submit_tag("save.png", data: { confirm: "Are you sure?" }) + # # => <input alt="Save" src="/images/save.png" data-confirm="Are you sure?" type="image" /> + def image_submit_tag(source, options = {}) + options = options.stringify_keys + tag :input, { "alt" => image_alt(source), "type" => "image", "src" => path_to_image(source) }.update(options) + end + + # Creates a field set for grouping HTML form elements. + # + # <tt>legend</tt> will become the fieldset's title (optional as per W3C). + # <tt>options</tt> accept the same values as tag. + # + # ==== Examples + # <%= field_set_tag do %> + # <p><%= text_field_tag 'name' %></p> + # <% end %> + # # => <fieldset><p><input id="name" name="name" type="text" /></p></fieldset> + # + # <%= field_set_tag 'Your details' do %> + # <p><%= text_field_tag 'name' %></p> + # <% end %> + # # => <fieldset><legend>Your details</legend><p><input id="name" name="name" type="text" /></p></fieldset> + # + # <%= field_set_tag nil, class: 'format' do %> + # <p><%= text_field_tag 'name' %></p> + # <% end %> + # # => <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.concat(capture(&block)) if block_given? + output.safe_concat("</fieldset>") + end + + # Creates a text field of type "color". + # + # ==== Options + # * Accepts the same options as text_field_tag. + def color_field_tag(name, value = nil, options = {}) + text_field_tag(name, value, options.stringify_keys.update("type" => "color")) + end + + # Creates a text field of type "search". + # + # ==== Options + # * Accepts the same options as text_field_tag. + def search_field_tag(name, value = nil, options = {}) + text_field_tag(name, value, options.stringify_keys.update("type" => "search")) + end + + # Creates a text field of type "tel". + # + # ==== Options + # * Accepts the same options as text_field_tag. + def telephone_field_tag(name, value = nil, options = {}) + text_field_tag(name, value, options.stringify_keys.update("type" => "tel")) + end + alias phone_field_tag telephone_field_tag + + # Creates a text field of type "date". + # + # ==== Options + # * Accepts the same options as text_field_tag. + def date_field_tag(name, value = nil, options = {}) + text_field_tag(name, value, options.stringify_keys.update("type" => "date")) + end + + # Creates a text field of type "time". + # + # === Options + # * <tt>:min</tt> - The minimum acceptable value. + # * <tt>:max</tt> - The maximum acceptable value. + # * <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")) + end + + # Creates a text field of type "datetime". + # + # === Options + # * <tt>:min</tt> - The minimum acceptable value. + # * <tt>:max</tt> - The maximum acceptable value. + # * <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")) + end + + # Creates a text field of type "datetime-local". + # + # === Options + # * <tt>:min</tt> - The minimum acceptable value. + # * <tt>:max</tt> - The maximum acceptable value. + # * <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")) + end + + # Creates a text field of type "month". + # + # === Options + # * <tt>:min</tt> - The minimum acceptable value. + # * <tt>:max</tt> - The maximum acceptable value. + # * <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")) + end + + # Creates a text field of type "week". + # + # === Options + # * <tt>:min</tt> - The minimum acceptable value. + # * <tt>:max</tt> - The maximum acceptable value. + # * <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")) + end + + # Creates a text field of type "url". + # + # ==== Options + # * Accepts the same options as text_field_tag. + def url_field_tag(name, value = nil, options = {}) + text_field_tag(name, value, options.stringify_keys.update("type" => "url")) + end + + # Creates a text field of type "email". + # + # ==== Options + # * Accepts the same options as text_field_tag. + def email_field_tag(name, value = nil, options = {}) + text_field_tag(name, value, options.stringify_keys.update("type" => "email")) + end + + # Creates a number field. + # + # ==== Options + # * <tt>:min</tt> - The minimum acceptable value. + # * <tt>:max</tt> - The maximum acceptable value. + # * <tt>:in</tt> - A range specifying the <tt>:min</tt> and + # <tt>:max</tt> values. + # * <tt>:step</tt> - The acceptable value granularity. + # * Otherwise accepts the same options as text_field_tag. + # + # ==== Examples + # number_field_tag 'quantity', nil, in: 1...10 + # # => <input id="quantity" name="quantity" min="1" max="9" type="number" /> + def number_field_tag(name, value = nil, options = {}) + options = options.stringify_keys + options["type"] ||= "number" + if range = options.delete("in") || options.delete("within") + options.update("min" => range.min, "max" => range.max) + end + text_field_tag(name, value, options) + end + + # Creates a range form element. + # + # ==== 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")) + end + + # Creates the hidden UTF8 enforcer tag. Override this method in a helper + # to customize the tag. + def utf8_enforcer_tag + tag(:input, :type => "hidden", :name => "utf8", :value => "✓".html_safe) + end + + private + def html_options_for_form(url_for_options, options) + options.stringify_keys.tap do |html_options| + html_options["enctype"] = "multipart/form-data" if html_options.delete("multipart") + # The following URL is unescaped, this is just a hash of options, and it is the + # responsibility of the caller to escape all the values. + html_options["action"] = url_for(url_for_options) + html_options["accept-charset"] = "UTF-8" + + html_options["data-remote"] = true if html_options.delete("remote") + + if html_options["data-remote"] && + !embed_authenticity_token_in_remote_forms && + html_options["authenticity_token"].blank? + # The authenticity token is taken from the meta tag in this case + html_options["authenticity_token"] = false + elsif html_options["authenticity_token"] == true + # Include the default authenticity_token, which is only generated when its set to nil, + # but we needed the true value to override the default of no authenticity_token on data-remote. + html_options["authenticity_token"] = nil + end + end + end + + def extra_tags_for_form(html_options) + authenticity_token = html_options.delete("authenticity_token") + method = html_options.delete("method").to_s + + method_tag = case method + when /^get$/i # must be case-insensitive, but can't use downcase as might be nil + html_options["method"] = "get" + '' + when /^post$/i, "", nil + html_options["method"] = "post" + token_tag(authenticity_token) + else + html_options["method"] = "post" + method_tag(method) + token_tag(authenticity_token) + end + + tags = utf8_enforcer_tag << method_tag + content_tag(:div, tags, :style => 'margin:0;padding:0;display:inline') + end + + def form_tag_html(html_options) + extra_tags = extra_tags_for_form(html_options) + tag(:form, html_options, true) + extra_tags + end + + def form_tag_in_block(html_options, &block) + content = capture(&block) + output = form_tag_html(html_options) + output << content + output.safe_concat("</form>") + end + + # 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:.]/, "_") + end + end + end +end diff --git a/actionview/lib/action_view/helpers/javascript_helper.rb b/actionview/lib/action_view/helpers/javascript_helper.rb new file mode 100644 index 0000000000..e475d5b018 --- /dev/null +++ b/actionview/lib/action_view/helpers/javascript_helper.rb @@ -0,0 +1,75 @@ +require 'action_view/helpers/tag_helper' + +module ActionView + module Helpers + module JavaScriptHelper + JS_ESCAPE_MAP = { + '\\' => '\\\\', + '</' => '<\/', + "\r\n" => '\n', + "\n" => '\n', + "\r" => '\n', + '"' => '\\"', + "'" => "\\'" + } + + JS_ESCAPE_MAP["\342\200\250".force_encoding(Encoding::UTF_8).encode!] = '
' + JS_ESCAPE_MAP["\342\200\251".force_encoding(Encoding::UTF_8).encode!] = '
' + + # Escapes carriage returns and single and double quotes for JavaScript segments. + # + # Also available through the alias j(). This is particularly helpful in JavaScript + # responses, like: + # + # $('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] } + javascript.html_safe? ? result.html_safe : result + else + '' + end + end + + alias_method :j, :escape_javascript + + # Returns a JavaScript tag with the +content+ inside. Example: + # javascript_tag "alert('All is good')" + # + # Returns: + # <script> + # //<![CDATA[ + # alert('All is good') + # //]]> + # </script> + # + # +html_options+ may be a hash of attributes for the <tt>\<script></tt> + # tag. + # + # javascript_tag "alert('All is good')", defer: 'defer' + # # => <script defer="defer">alert('All is good')</script> + # + # Instead of passing the content as an argument, you can also use a block + # in which case, you pass your +html_options+ as the first parameter. + # + # <%= javascript_tag defer: 'defer' do -%> + # alert('All is good') + # <% end -%> + def javascript_tag(content_or_options_with_block = nil, html_options = {}, &block) + content = + if block_given? + html_options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash) + capture(&block) + else + content_or_options_with_block + end + + content_tag(:script, javascript_cdata_section(content), html_options) + end + + def javascript_cdata_section(content) #:nodoc: + "\n//#{cdata_section("\n#{content}\n//")}\n".html_safe + end + end + end +end diff --git a/actionview/lib/action_view/helpers/number_helper.rb b/actionview/lib/action_view/helpers/number_helper.rb new file mode 100644 index 0000000000..fda7038a5d --- /dev/null +++ b/actionview/lib/action_view/helpers/number_helper.rb @@ -0,0 +1,441 @@ +# encoding: utf-8 + +require 'active_support/core_ext/hash/keys' +require 'active_support/core_ext/string/output_safety' +require 'active_support/number_helper' + +module ActionView + # = Action View Number Helpers + module Helpers #:nodoc: + + # Provides methods for converting numbers into formatted strings. + # Methods are provided for phone numbers, currency, percentage, + # precision, positional notation, file size and pretty printing. + # + # Most methods expect a +number+ argument, and will return it + # unchanged if can't be converted into a valid number. + module NumberHelper + + # Raised when argument +number+ param given to the helpers is invalid and + # the option :raise is set to +true+. + class InvalidNumberError < StandardError + attr_accessor :number + def initialize(number) + @number = number + end + end + + # Formats a +number+ into a US phone number (e.g., (555) + # 123-9876). You can customize the format in the +options+ hash. + # + # ==== Options + # + # * <tt>:area_code</tt> - Adds parentheses around the area code. + # * <tt>:delimiter</tt> - Specifies the delimiter to use + # (defaults to "-"). + # * <tt>:extension</tt> - Specifies an extension to add to the + # end of the generated number. + # * <tt>:country_code</tt> - Sets the country code for the phone + # number. + # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when + # the argument is invalid. + # + # ==== Examples + # + # number_to_phone(5551234) # => 555-1234 + # number_to_phone("5551234") # => 555-1234 + # number_to_phone(1235551234) # => 123-555-1234 + # number_to_phone(1235551234, area_code: true) # => (123) 555-1234 + # number_to_phone(1235551234, delimiter: " ") # => 123 555 1234 + # number_to_phone(1235551234, area_code: true, extension: 555) # => (123) 555-1234 x 555 + # number_to_phone(1235551234, country_code: 1) # => +1-123-555-1234 + # number_to_phone("123a456") # => 123a456 + # number_to_phone("1234a567", raise: true) # => InvalidNumberError + # + # number_to_phone(1235551234, country_code: 1, extension: 1343, delimiter: ".") + # # => +1.123.555.1234 x 1343 + def number_to_phone(number, options = {}) + return unless number + options = options.symbolize_keys + + parse_float(number, true) if options.delete(:raise) + ERB::Util.html_escape(ActiveSupport::NumberHelper.number_to_phone(number, options)) + end + + # Formats a +number+ into a currency string (e.g., $13.65). You + # can customize the format in the +options+ hash. + # + # ==== Options + # + # * <tt>:locale</tt> - Sets the locale to be used for formatting + # (defaults to current locale). + # * <tt>:precision</tt> - Sets the level of precision (defaults + # to 2). + # * <tt>:unit</tt> - Sets the denomination of the currency + # (defaults to "$"). + # * <tt>:separator</tt> - Sets the separator between the units + # (defaults to "."). + # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults + # to ","). + # * <tt>:format</tt> - Sets the format for non-negative numbers + # (defaults to "%u%n"). Fields are <tt>%u</tt> for the + # currency, and <tt>%n</tt> for the number. + # * <tt>:negative_format</tt> - Sets the format for negative + # numbers (defaults to prepending an hyphen to the formatted + # number given by <tt>:format</tt>). Accepts the same fields + # than <tt>:format</tt>, except <tt>%n</tt> is here the + # absolute value of the number. + # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when + # the argument is invalid. + # + # ==== Examples + # + # number_to_currency(1234567890.50) # => $1,234,567,890.50 + # number_to_currency(1234567890.506) # => $1,234,567,890.51 + # number_to_currency(1234567890.506, precision: 3) # => $1,234,567,890.506 + # number_to_currency(1234567890.506, locale: :fr) # => 1 234 567 890,51 € + # number_to_currency("123a456") # => $123a456 + # + # number_to_currency("123a456", raise: true) # => InvalidNumberError + # + # number_to_currency(-1234567890.50, negative_format: "(%u%n)") + # # => ($1,234,567,890.50) + # number_to_currency(1234567890.50, unit: "£", separator: ",", delimiter: "") + # # => £1234567890,50 + # number_to_currency(1234567890.50, unit: "£", separator: ",", delimiter: "", format: "%n %u") + # # => 1234567890,50 £ + def number_to_currency(number, options = {}) + return unless number + options = escape_unsafe_delimiters_and_separators(options.symbolize_keys) + + wrap_with_output_safety_handling(number, options.delete(:raise)) { + ActiveSupport::NumberHelper.number_to_currency(number, options) + } + end + + # Formats a +number+ as a percentage string (e.g., 65%). You can + # customize the format in the +options+ hash. + # + # ==== Options + # + # * <tt>:locale</tt> - Sets the locale to be used for formatting + # (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 + # digits (defaults to +false+). + # * <tt>:separator</tt> - Sets the separator between the + # fractional and integer digits (defaults to "."). + # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults + # to ""). + # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes + # insignificant zeros after the decimal separator (defaults to + # +false+). + # * <tt>:format</tt> - Specifies the format of the percentage + # string The number field is <tt>%n</tt> (defaults to "%n%"). + # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when + # the argument is invalid. + # + # ==== Examples + # + # number_to_percentage(100) # => 100.000% + # number_to_percentage("98") # => 98.000% + # number_to_percentage(100, precision: 0) # => 100% + # number_to_percentage(1000, delimiter: '.', separator: ',') # => 1.000,000% + # 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("98a", raise: true) # => InvalidNumberError + def number_to_percentage(number, options = {}) + return unless number + options = escape_unsafe_delimiters_and_separators(options.symbolize_keys) + + wrap_with_output_safety_handling(number, options.delete(:raise)) { + ActiveSupport::NumberHelper.number_to_percentage(number, options) + } + end + + # Formats a +number+ with grouped thousands using +delimiter+ + # (e.g., 12,324). You can customize the format in the +options+ + # hash. + # + # ==== Options + # + # * <tt>:locale</tt> - Sets the locale to be used for formatting + # (defaults to current locale). + # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults + # to ","). + # * <tt>:separator</tt> - Sets the separator between the + # fractional and integer digits (defaults to "."). + # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when + # the argument is invalid. + # + # ==== Examples + # + # number_with_delimiter(12345678) # => 12,345,678 + # number_with_delimiter("123456") # => 123,456 + # number_with_delimiter(12345678.05) # => 12,345,678.05 + # number_with_delimiter(12345678, delimiter: ".") # => 12.345.678 + # number_with_delimiter(12345678, delimiter: ",") # => 12,345,678 + # number_with_delimiter(12345678.05, separator: " ") # => 12,345,678 05 + # number_with_delimiter(12345678.05, locale: :fr) # => 12 345 678,05 + # number_with_delimiter("112a") # => 112a + # number_with_delimiter(98765432.98, delimiter: " ", separator: ",") + # # => 98 765 432,98 + # + # number_with_delimiter("112a", raise: true) # => raise InvalidNumberError + def number_with_delimiter(number, options = {}) + options = escape_unsafe_delimiters_and_separators(options.symbolize_keys) + + wrap_with_output_safety_handling(number, options.delete(:raise)) { + ActiveSupport::NumberHelper.number_to_delimited(number, options) + } + end + + # Formats a +number+ with the specified level of + # <tt>:precision</tt> (e.g., 112.32 has a precision of 2 if + # +:significant+ is +false+, and 5 if +:significant+ is +true+). + # You can customize the format in the +options+ hash. + # + # ==== Options + # + # * <tt>:locale</tt> - Sets the locale to be used for formatting + # (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 + # digits (defaults to +false+). + # * <tt>:separator</tt> - Sets the separator between the + # fractional and integer digits (defaults to "."). + # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults + # to ""). + # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes + # insignificant zeros after the decimal separator (defaults to + # +false+). + # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when + # the argument is invalid. + # + # ==== Examples + # + # number_with_precision(111.2345) # => 111.235 + # number_with_precision(111.2345, precision: 2) # => 111.23 + # number_with_precision(13, precision: 5) # => 13.00000 + # number_with_precision(389.32314, precision: 0) # => 389 + # number_with_precision(111.2345, significant: true) # => 111 + # number_with_precision(111.2345, precision: 1, significant: true) # => 100 + # number_with_precision(13, precision: 5, significant: true) # => 13.000 + # number_with_precision(111.234, locale: :fr) # => 111,234 + # + # number_with_precision(13, precision: 5, significant: true, strip_insignificant_zeros: true) + # # => 13 + # + # number_with_precision(389.32314, precision: 4, significant: true) # => 389.3 + # number_with_precision(1111.2345, precision: 2, separator: ',', delimiter: '.') + # # => 1.111,23 + def number_with_precision(number, options = {}) + options = escape_unsafe_delimiters_and_separators(options.symbolize_keys) + + wrap_with_output_safety_handling(number, options.delete(:raise)) { + ActiveSupport::NumberHelper.number_to_rounded(number, options) + } + end + + # Formats the bytes in +number+ into a more understandable + # representation (e.g., giving it 1500 yields 1.5 KB). This + # method is useful for reporting file sizes to users. You can + # customize the format in the +options+ hash. + # + # See <tt>number_to_human</tt> if you want to pretty-print a + # generic number. + # + # ==== Options + # + # * <tt>:locale</tt> - Sets the locale to be used for formatting + # (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 + # digits (defaults to +true+) + # * <tt>:separator</tt> - Sets the separator between the + # fractional and integer digits (defaults to "."). + # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults + # to ""). + # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes + # insignificant zeros after the decimal separator (defaults to + # +true+) + # * <tt>:prefix</tt> - If +:si+ formats the number using the SI + # prefix (defaults to :binary) + # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when + # the argument is invalid. + # + # ==== Examples + # + # number_to_human_size(123) # => 123 Bytes + # number_to_human_size(1234) # => 1.21 KB + # number_to_human_size(12345) # => 12.1 KB + # number_to_human_size(1234567) # => 1.18 MB + # number_to_human_size(1234567890) # => 1.15 GB + # number_to_human_size(1234567890123) # => 1.12 TB + # number_to_human_size(1234567, precision: 2) # => 1.2 MB + # number_to_human_size(483989, precision: 2) # => 470 KB + # number_to_human_size(1234567, precision: 2, separator: ',') # => 1,2 MB + # + # Non-significant zeros after the fractional separator are + # stripped out by default (set + # <tt>:strip_insignificant_zeros</tt> to +false+ to change + # that): + # + # number_to_human_size(1234567890123, precision: 5) # => "1.1229 TB" + # number_to_human_size(524288000, precision: 5) # => "500 MB" + def number_to_human_size(number, options = {}) + options = escape_unsafe_delimiters_and_separators(options.symbolize_keys) + + wrap_with_output_safety_handling(number, options.delete(:raise)) { + ActiveSupport::NumberHelper.number_to_human_size(number, options) + } + end + + # Pretty prints (formats and approximates) a number in a way it + # is more readable by humans (eg.: 1200000000 becomes "1.2 + # Billion"). This is useful for numbers that can get very large + # (and too hard to read). + # + # 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 + # 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 + # (centi, deci, mili, etc). + # + # ==== Options + # + # * <tt>:locale</tt> - Sets the locale to be used for formatting + # (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 + # digits (defaults to +true+) + # * <tt>:separator</tt> - Sets the separator between the + # fractional and integer digits (defaults to "."). + # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults + # to ""). + # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes + # insignificant zeros after the decimal separator (defaults to + # +true+) + # * <tt>:units</tt> - A Hash of unit quantifier names. Or a + # 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> + # * *fractionals*: <tt>:deci</tt>, <tt>:centi</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') + # * %n - The number + # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when + # the argument is invalid. + # + # ==== Examples + # + # number_to_human(123) # => "123" + # number_to_human(1234) # => "1.23 Thousand" + # number_to_human(12345) # => "12.3 Thousand" + # number_to_human(1234567) # => "1.23 Million" + # number_to_human(1234567890) # => "1.23 Billion" + # number_to_human(1234567890123) # => "1.23 Trillion" + # number_to_human(1234567890123456) # => "1.23 Quadrillion" + # number_to_human(1234567890123456789) # => "1230 Quadrillion" + # number_to_human(489939, precision: 2) # => "490 Thousand" + # number_to_human(489939, precision: 4) # => "489.9 Thousand" + # number_to_human(1234567, precision: 4, + # significant: false) # => "1.2346 Million" + # number_to_human(1234567, precision: 1, + # separator: ',', + # significant: false) # => "1,2 Million" + # + # Non-significant zeros after the decimal separator are stripped + # out by default (set <tt>:strip_insignificant_zeros</tt> to + # +false+ to change that): + # number_to_human(12345012345, significant_digits: 6) # => "12.345 Billion" + # number_to_human(500000000, precision: 5) # => "500 Million" + # + # ==== Custom Unit Quantifiers + # + # You can also use your own custom unit quantifiers: + # number_to_human(500000, units: {unit: "ml", thousand: "lt"}) # => "500 lt" + # + # If in your I18n locale you have: + # distance: + # centi: + # one: "centimeter" + # other: "centimeters" + # unit: + # one: "meter" + # other: "meters" + # thousand: + # one: "kilometer" + # other: "kilometers" + # billion: "gazillion-distance" + # + # Then you could do: + # + # number_to_human(543934, units: :distance) # => "544 kilometers" + # number_to_human(54393498, units: :distance) # => "54400 kilometers" + # number_to_human(54393498000, units: :distance) # => "54.4 gazillion-distance" + # number_to_human(343, units: :distance, precision: 1) # => "300 meters" + # number_to_human(1, units: :distance) # => "1 meter" + # number_to_human(0.34, units: :distance) # => "34 centimeters" + # + def number_to_human(number, options = {}) + options = escape_unsafe_delimiters_and_separators(options.symbolize_keys) + + wrap_with_output_safety_handling(number, options.delete(:raise)) { + ActiveSupport::NumberHelper.number_to_human(number, options) + } + end + + private + + def escape_unsafe_delimiters_and_separators(options) + options[:separator] = ERB::Util.html_escape(options[:separator]) if options[:separator] && !options[:separator].html_safe? + options[:delimiter] = ERB::Util.html_escape(options[:delimiter]) if options[:delimiter] && !options[:delimiter].html_safe? + options + end + + def wrap_with_output_safety_handling(number, raise_on_invalid, &block) + valid_float = valid_float?(number) + raise InvalidNumberError, number if raise_on_invalid && !valid_float + + formatted_number = yield + + if valid_float || number.html_safe? + formatted_number.html_safe + else + formatted_number + end + end + + def valid_float?(number) + !parse_float(number, false).nil? + end + + def parse_float(number, raise_error) + Float(number) + rescue ArgumentError, TypeError + raise InvalidNumberError, number if raise_error + end + end + end +end diff --git a/actionview/lib/action_view/helpers/output_safety_helper.rb b/actionview/lib/action_view/helpers/output_safety_helper.rb new file mode 100644 index 0000000000..60a4478c26 --- /dev/null +++ b/actionview/lib/action_view/helpers/output_safety_helper.rb @@ -0,0 +1,38 @@ +require 'active_support/core_ext/string/output_safety' + +module ActionView #:nodoc: + # = Action View Raw Output Helper + module Helpers #:nodoc: + module OutputSafetyHelper + # This method outputs without escaping a string. Since escaping tags is + # now default, this can be used when you don't want Rails to automatically + # escape tags. This is not recommended if the data is coming from the user's + # input. + # + # For example: + # + # raw @user.name + # # => 'Jimmy <alert>Tables</alert>' + def raw(stringish) + stringish.to_s.html_safe + end + + # This method returns a html safe string similar to what <tt>Array#join</tt> + # would return. All items in the array, including 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><br /><p>bar</p>" + # + # safe_join(["<p>foo</p>".html_safe, "<p>bar</p>".html_safe], "<br />".html_safe) + # # => "<p>foo</p><br /><p>bar</p>" + # + def safe_join(array, sep=$,) + sep = ERB::Util.html_escape(sep) + + array.map { |i| ERB::Util.html_escape(i) }.join(sep).html_safe + end + end + end +end diff --git a/actionview/lib/action_view/helpers/record_tag_helper.rb b/actionview/lib/action_view/helpers/record_tag_helper.rb new file mode 100644 index 0000000000..f767957fa9 --- /dev/null +++ b/actionview/lib/action_view/helpers/record_tag_helper.rb @@ -0,0 +1,106 @@ +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) + 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 + 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 new file mode 100644 index 0000000000..458086de96 --- /dev/null +++ b/actionview/lib/action_view/helpers/rendering_helper.rb @@ -0,0 +1,90 @@ +module ActionView + module Helpers + # = Action View Rendering + # + # Implements methods that allow rendering from a view context. + # In order to use this module, all you need is to implement + # view_renderer that returns an ActionView::Renderer object. + module RenderingHelper + # Returns the result of a render that's dictated by the options hash. The primary options are: + # + # * <tt>:partial</tt> - See <tt>ActionView::PartialRenderer</tt>. + # * <tt>:file</tt> - Renders an explicit template file (this used to be the old default), add :locals to pass in those. + # * <tt>:inline</tt> - Renders an inline template similar to how it's done in the controller. + # * <tt>:text</tt> - Renders the text passed in out. + # + # If no options hash is passed or :update specified, the default is to render a partial and use the second parameter + # as the locals hash. + def render(options = {}, locals = {}, &block) + case options + when Hash + if block_given? + view_renderer.render_partial(self, options.merge(:partial => options[:layout]), &block) + else + view_renderer.render(self, options) + end + else + view_renderer.render_partial(self, :partial => options, :locals => locals) + end + end + + # Overwrites _layout_for in the context object so it supports the case a block is + # passed to a partial. Returns the contents that are yielded to a layout, given a + # name or a block. + # + # You can think of a layout as a method that is called with a block. If the user calls + # <tt>yield :some_name</tt>, the block, by default, returns <tt>content_for(:some_name)</tt>. + # If the user calls simply +yield+, the default block returns <tt>content_for(:layout)</tt>. + # + # The user can override this default by passing a block to the layout: + # + # # The template + # <%= render layout: "my_layout" do %> + # Content + # <% end %> + # + # # The layout + # <html> + # <%= yield %> + # </html> + # + # In this case, instead of the default block, which would return <tt>content_for(:layout)</tt>, + # this method returns the block that was passed in to <tt>render :layout</tt>, and the response + # would be + # + # <html> + # Content + # </html> + # + # Finally, the block can take block arguments, which can be passed in by +yield+: + # + # # The template + # <%= render layout: "my_layout" do |customer| %> + # Hello <%= customer.name %> + # <% end %> + # + # # The layout + # <html> + # <%= yield Struct.new(:name).new("David") %> + # </html> + # + # In this case, the layout would receive the block passed into <tt>render :layout</tt>, + # and the struct specified would be passed into the block as an argument. The result + # would be + # + # <html> + # Hello David + # </html> + # + def _layout_for(*args, &block) + name = args.first + + if block && !name.is_a?(Symbol) + capture(*args, &block) + else + super + end + end + end + end +end diff --git a/actionview/lib/action_view/helpers/sanitize_helper.rb b/actionview/lib/action_view/helpers/sanitize_helper.rb new file mode 100644 index 0000000000..e5cb843670 --- /dev/null +++ b/actionview/lib/action_view/helpers/sanitize_helper.rb @@ -0,0 +1,256 @@ +require 'active_support/core_ext/object/try' +require 'action_view/vendor/html-scanner' + +module ActionView + # = Action View Sanitize Helpers + module Helpers + # The SanitizeHelper module provides a set of methods for scrubbing text of undesired HTML elements. + # 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. + # + # 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. + # + # <%= sanitize @article.body %> + # + # 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: + # + # Normal Use + # + # <%= sanitize @article.body %> + # + # Custom Use (only the mentioned tags and attributes are allowed, nothing else) + # + # <%= sanitize @article.body, tags: %w(table tr td), attributes: %w(id class style) %> + # + # Add table tags to the default allowed tags + # + # class Application < Rails::Application + # config.action_view.sanitized_allowed_tags = 'table', 'tr', 'td' + # end + # + # Remove tags to the default allowed tags + # + # class Application < Rails::Application + # config.after_initialize do + # ActionView::Base.sanitized_allowed_tags.delete 'div' + # end + # end + # + # Change allowed default attributes + # + # class Application < Rails::Application + # config.action_view.sanitized_allowed_attributes = 'id', 'class', 'style' + # 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. + # + def sanitize(html, options = {}) + self.class.white_list_sanitizer.sanitize(html, options).try(:html_safe) + end + + # Sanitizes a block of CSS code. Used by +sanitize+ when it comes across a style attribute. + def sanitize_css(style) + 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. + # + # strip_tags("Strip <i>these</i> tags!") + # # => Strip these tags! + # + # strip_tags("<b>Bold</b> no more! <a href='more.html'>See more here</a>...") + # # => Bold no more! See more here... + # + # 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) + end + + # Strips all link tags from +text+ leaving just the link text. + # + # strip_links('<a href="http://www.rubyonrails.org">Ruby on Rails</a>') + # # => Ruby on Rails + # + # strip_links('Please e-mail me at <a href="mailto:me@email.com">me@email.com</a>.') + # # => Please e-mail me at me@email.com. + # + # strip_links('Blog: <a href="http://www.myblog.com/" class="nav" target=\"_blank\">Visit</a>.') + # # => Blog: Visit. + def strip_links(html) + self.class.link_sanitizer.sanitize(html) + end + + 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 + end + + def sanitized_allowed_tags + white_list_sanitizer.allowed_tags + end + + def sanitized_allowed_attributes + 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 + # any object that responds to +sanitize+. + # + # class Application < Rails::Application + # config.action_view.full_sanitizer = MySpecialSanitizer.new + # end + # + def full_sanitizer + @full_sanitizer ||= HTML::FullSanitizer.new + end + + # Gets the 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 + end + + # Gets the HTML::WhiteListSanitizer instance used by sanitize and +sanitize_css+. + # Replace with any object that responds to +sanitize+. + # + # class Application < Rails::Application + # config.action_view.white_list_sanitizer = MySpecialSanitizer.new + # 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) + end + end + end + end +end diff --git a/actionview/lib/action_view/helpers/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb new file mode 100644 index 0000000000..732f35643a --- /dev/null +++ b/actionview/lib/action_view/helpers/tag_helper.rb @@ -0,0 +1,176 @@ +require 'active_support/core_ext/string/output_safety' +require 'set' + +module ActionView + # = Action View Tag Helpers + module Helpers #:nodoc: + # Provides methods to generate HTML tags programmatically when you can't use + # a Builder. By default, they output XHTML compliant tags. + module TagHelper + extend ActiveSupport::Concern + include CaptureHelper + + BOOLEAN_ATTRIBUTES = %w(disabled readonly multiple checked autobuffer + autoplay controls loop selected hidden scoped async + defer reversed ismap seamless muted required + autofocus novalidate formnovalidate open pubdate + itemscope allowfullscreen default inert sortable + truespeed typemustmatch).to_set + + BOOLEAN_ATTRIBUTES.merge(BOOLEAN_ATTRIBUTES.map {|attribute| attribute.to_sym }) + + 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 + # with HTML 4.0 and below. Add HTML attributes by passing an attributes + # hash to +options+. Set +escape+ to false to disable attribute value + # escaping. + # + # ==== Options + # You can use symbols or strings for the attribute names. + # + # Use +true+ with boolean attributes that can render with no value, like + # +disabled+ and +readonly+. + # + # HTML5 <tt>data-*</tt> attributes can be set with a single +data+ key + # pointing to a hash of sub-attributes. + # + # To play nicely with JavaScript conventions sub-attributes are dasherized. + # For example, a key +user_id+ would render as <tt>data-user-id</tt> and + # thus accessed as <tt>dataset.userId</tt>. + # + # Values are encoded to JSON, with the exception of strings and symbols. + # This may come in handy when using jQuery's HTML5-aware <tt>.data()</tt> + # from 1.4.3. + # + # ==== Examples + # tag("br") + # # => <br /> + # + # tag("br", nil, true) + # # => <br> + # + # tag("input", type: 'text', disabled: true) + # # => <input type="text" disabled="disabled" /> + # + # tag("img", src: "open & shut.png") + # # => <img src="open & shut.png" /> + # + # tag("img", {src: "open & shut.png"}, false, false) + # # => <img src="open & shut.png" /> + # + # tag("div", data: {name: 'Stephen', city_state: %w(Chicago IL)}) + # # => <div data-name="Stephen" data-city-state="["Chicago","IL"]" /> + def tag(name, options = nil, open = false, escape = true) + "<#{name}#{tag_options(options, escape) if options}#{open ? ">" : " />"}".html_safe + end + + # Returns an HTML block tag of type +name+ surrounding the +content+. Add + # HTML attributes by passing an attributes hash to +options+. + # Instead of passing the content as an argument, you can also use a block + # in which case, you pass your +options+ as the second parameter. + # Set escape to false to disable attribute value escaping. + # + # ==== Options + # The +options+ hash is used with attributes with no value like (<tt>disabled</tt> and + # <tt>readonly</tt>), which you can give a value of true in the +options+ hash. You can use + # symbols or strings for the attribute names. + # + # ==== Examples + # content_tag(:p, "Hello world!") + # # => <p>Hello world!</p> + # content_tag(:div, content_tag(:p, "Hello world!"), class: "strong") + # # => <div class="strong"><p>Hello world!</p></div> + # content_tag("select", options, multiple: true) + # # => <select multiple="multiple">...options...</select> + # + # <%= content_tag :div, class: "strong" do -%> + # Hello world! + # <% end -%> + # # => <div class="strong">Hello world!</div> + def content_tag(name, content_or_options_with_block = nil, options = nil, escape = true, &block) + if block_given? + options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash) + content_tag_string(name, capture(&block), options, escape) + else + content_tag_string(name, content_or_options_with_block, options, escape) + end + end + + # Returns a CDATA section with the given +content+. CDATA sections + # are used to escape blocks of text containing characters which would + # otherwise be recognized as markup. CDATA sections begin with the string + # <tt><![CDATA[</tt> and end with (and may not contain) the string <tt>]]></tt>. + # + # cdata_section("<hello world>") + # # => <![CDATA[<hello world>]]> + # + # cdata_section(File.read("hello_world.txt")) + # # => <![CDATA[<hello from a text file]]> + # + # cdata_section("hello]]>world") + # # => <![CDATA[hello]]]]><![CDATA[>world]]> + def cdata_section(content) + splitted = content.gsub(']]>', ']]]]><![CDATA[>') + "<![CDATA[#{splitted}]]>".html_safe + end + + # Returns an escaped version of +html+ without affecting existing escaped entities. + # + # escape_once("1 < 2 & 3") + # # => "1 < 2 & 3" + # + # escape_once("<< Accept & Checkout") + # # => "<< Accept & Checkout" + def escape_once(html) + ERB::Util.html_escape_once(html) + end + + private + + def content_tag_string(name, content, options, escape = true) + tag_options = tag_options(options, escape) if options + content = ERB::Util.h(content) if escape + "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name.to_sym]}#{content}</#{name}>".html_safe + end + + def tag_options(options, escape = true) + return if options.blank? + attrs = [] + options.each_pair do |key, value| + if key.to_s == 'data' && value.is_a?(Hash) + value.each_pair do |k, v| + attrs << data_tag_option(k, v, escape) + end + elsif BOOLEAN_ATTRIBUTES.include?(key) + attrs << boolean_tag_option(key) if value + elsif !value.nil? + attrs << tag_option(key, value, escape) + end + end + " #{attrs.sort! * ' '}".html_safe unless attrs.empty? + end + + def data_tag_option(key, value, escape) + key = "data-#{key.to_s.dasherize}" + unless value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(BigDecimal) + value = value.to_json + end + tag_option(key, value, escape) + end + + def boolean_tag_option(key) + %(#{key}="#{key}") + end + + def tag_option(key, value, escape) + value = value.join(" ") if value.is_a?(Array) + value = ERB::Util.h(value) if escape + %(#{key}="#{value}") + end + end + end +end diff --git a/actionview/lib/action_view/helpers/tags.rb b/actionview/lib/action_view/helpers/tags.rb new file mode 100644 index 0000000000..a05e16979a --- /dev/null +++ b/actionview/lib/action_view/helpers/tags.rb @@ -0,0 +1,39 @@ +module ActionView + module Helpers + module Tags #:nodoc: + extend ActiveSupport::Autoload + + autoload :Base + autoload :CheckBox + autoload :CollectionCheckBoxes + autoload :CollectionRadioButtons + autoload :CollectionSelect + autoload :ColorField + autoload :DateField + autoload :DateSelect + autoload :DatetimeField + autoload :DatetimeLocalField + autoload :DatetimeSelect + autoload :EmailField + autoload :FileField + autoload :GroupedCollectionSelect + autoload :HiddenField + autoload :Label + autoload :MonthField + autoload :NumberField + autoload :PasswordField + autoload :RadioButton + autoload :RangeField + autoload :SearchField + autoload :Select + autoload :TelField + autoload :TextArea + autoload :TextField + autoload :TimeField + autoload :TimeSelect + autoload :TimeZoneSelect + autoload :UrlField + autoload :WeekField + end + end +end diff --git a/actionview/lib/action_view/helpers/tags/base.rb b/actionview/lib/action_view/helpers/tags/base.rb new file mode 100644 index 0000000000..3fe3f4e9df --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/base.rb @@ -0,0 +1,147 @@ +module ActionView + module Helpers + module Tags # :nodoc: + class Base # :nodoc: + include Helpers::ActiveModelInstanceTag, Helpers::TagHelper, Helpers::FormTagHelper + include FormOptionsHelper + + attr_reader :object + + def initialize(object_name, method_name, template_object, options = {}) + @object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup + @template_object = template_object + + @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 + end + + # This is what child classes implement. + def render + raise NotImplementedError, "Subclasses must implement a render method" + end + + private + + def value(object) + object.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) : + value(object) + end + end + + def retrieve_object(object) + if object + object + elsif @template_object.instance_variable_defined?("@#{@object_name}") + @template_object.instance_variable_get("@#{@object_name}") + end + rescue NameError + # As @object_name may contain the nested syntax (item[subobject]) we need to fallback to nil. + nil + end + + def retrieve_autoindex(pre_match) + object = self.object || @template_object.instance_variable_get("@#{pre_match}") + if object && object.respond_to?(:to_param) + object.to_param + else + raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}" + end + end + + def add_default_name_and_id_for_value(tag_value, options) + if tag_value.nil? + add_default_name_and_id(options) + else + specified_id = options["id"] + add_default_name_and_id(options) + + if specified_id.blank? && options["id"].present? + options["id"] += "_#{sanitized_value(tag_value)}" + end + end + 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 } + 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}" + end + + def tag_id_with_index(index) + "#{sanitized_object_name}_#{index}_#{sanitized_method_name}" + end + + def sanitized_object_name + @sanitized_object_name ||= @object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "") + end + + def sanitized_method_name + @sanitized_method_name ||= @method_name.sub(/\?$/,"") + end + + def sanitized_value(value) + value.to_s.gsub(/\s/, "_").gsub(/[^-\w]/, "").downcase + end + + 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) + select = content_tag("select", add_options(option_tags, options, value(object)), html_options) + + if html_options["multiple"] && options.fetch(:include_hidden, true) + tag("input", :disabled => html_options["disabled"], :name => html_options["name"], :type => "hidden", :value => "") + select + else + select + end + end + + def select_not_required?(html_options) + !html_options["required"] || html_options["multiple"] || html_options["size"].to_i > 1 + end + + def add_options(option_tags, options, value = nil) + if options[:include_blank] + option_tags = content_tag_string('option', options[:include_blank].kind_of?(String) ? options[:include_blank] : nil, :value => '') + "\n" + option_tags + end + if value.blank? && options[:prompt] + option_tags = content_tag_string('option', prompt_text(options[:prompt]), :value => '') + "\n" + option_tags + end + option_tags + end + end + end + end +end diff --git a/actionview/lib/action_view/helpers/tags/check_box.rb b/actionview/lib/action_view/helpers/tags/check_box.rb new file mode 100644 index 0000000000..6d51f2629a --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/check_box.rb @@ -0,0 +1,64 @@ +require 'action_view/helpers/tags/checkable' + +module ActionView + module Helpers + module Tags # :nodoc: + class CheckBox < Base #:nodoc: + include Checkable + + def initialize(object_name, method_name, template_object, checked_value, unchecked_value, options) + @checked_value = checked_value + @unchecked_value = unchecked_value + super(object_name, method_name, template_object, options) + end + + def render + options = @options.stringify_keys + options["type"] = "checkbox" + options["value"] = @checked_value + options["checked"] = "checked" if input_checked?(object, options) + + if options["multiple"] + add_default_name_and_id_for_value(@checked_value, options) + options.delete("multiple") + else + add_default_name_and_id(options) + end + + include_hidden = options.delete("include_hidden") { true } + checkbox = tag("input", options) + + if include_hidden + hidden = hidden_field_for_checkbox(options) + hidden + checkbox + else + checkbox + end + end + + private + + def checked?(value) + case value + when TrueClass, FalseClass + value == !!@checked_value + when NilClass + false + when String + value == @checked_value + else + if value.respond_to?(:include?) + value.include?(@checked_value) + else + value.to_i == @checked_value.to_i + end + end + end + + def hidden_field_for_checkbox(options) + @unchecked_value ? tag("input", options.slice("name", "disabled", "form").merge!("type" => "hidden", "value" => @unchecked_value)) : "".html_safe + end + end + end + end +end diff --git a/actionview/lib/action_view/helpers/tags/checkable.rb b/actionview/lib/action_view/helpers/tags/checkable.rb new file mode 100644 index 0000000000..052e9df662 --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/checkable.rb @@ -0,0 +1,16 @@ +module ActionView + module Helpers + module Tags # :nodoc: + module Checkable # :nodoc: + def input_checked?(object, options) + if options.has_key?("checked") + checked = options.delete "checked" + checked == true || checked == "checked" + else + checked?(value(object)) + end + end + 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 new file mode 100644 index 0000000000..52006d856b --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb @@ -0,0 +1,43 @@ +require 'action_view/helpers/tags/collection_helpers' + +module ActionView + module Helpers + module Tags # :nodoc: + class CollectionCheckBoxes < Base # :nodoc: + include CollectionHelpers + + class CheckBoxBuilder < Builder # :nodoc: + def check_box(extra_html_options={}) + html_options = extra_html_options.merge(@input_html_options) + @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. + hidden = @template_object.hidden_field_tag("#{tag_name}[]", "", :id => nil) + + rendered_collection + hidden + end + + private + + def render_component(builder) + builder.check_box + builder.label + end + 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 new file mode 100644 index 0000000000..388dcf1f13 --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/collection_helpers.rb @@ -0,0 +1,84 @@ +module ActionView + module Helpers + module Tags # :nodoc: + module CollectionHelpers # :nodoc: + class Builder # :nodoc: + attr_reader :object, :text, :value + + def initialize(template_object, object_name, method_name, object, + sanitized_attribute_name, text, value, input_html_options) + @template_object = template_object + @object_name = object_name + @method_name = method_name + @object = object + @sanitized_attribute_name = sanitized_attribute_name + @text = text + @value = value + @input_html_options = input_html_options + end + + def label(label_html_options={}, &block) + @template_object.label(@object_name, @sanitized_attribute_name, @text, label_html_options, &block) + end + end + + def initialize(object_name, method_name, template_object, collection, value_method, text_method, options, html_options) + @collection = collection + @value_method = value_method + @text_method = text_method + @html_options = html_options + + super(object_name, method_name, template_object, options) + end + + private + + def instantiate_builder(builder_class, item, value, text, html_options) + builder_class.new(@template_object, @object_name, @method_name, item, + sanitize_attribute_name(value), text, value, html_options) + end + + # Generate default options for collection helpers, such as :checked and + # :disabled. + def default_html_options_for_collection(item, value) #:nodoc: + html_options = @html_options.dup + + [:checked, :selected, :disabled].each do |option| + current_value = @options[option] + next if current_value.nil? + + accept = if current_value.respond_to?(:call) + current_value.call(item) + else + Array(current_value).map(&:to_s).include?(value.to_s) + end + + if accept + html_options[option] = true + elsif option == :checked + html_options[option] = false + end + end + + html_options[:object] = @object + html_options + end + + def sanitize_attribute_name(value) #:nodoc: + "#{sanitized_method_name}_#{sanitized_value(value)}" + end + + def render_collection #:nodoc: + @collection.map do |item| + value = value_for_collection(item, @value_method) + text = value_for_collection(item, @text_method) + default_html_options = default_html_options_for_collection(item, value) + additional_html_options = option_html_attributes(item) + + yield item, value, text, default_html_options.merge(additional_html_options) + end.join.html_safe + end + 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 new file mode 100644 index 0000000000..20be34c1f2 --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb @@ -0,0 +1,36 @@ +require 'action_view/helpers/tags/collection_helpers' + +module ActionView + module Helpers + module Tags # :nodoc: + class CollectionRadioButtons < Base # :nodoc: + include CollectionHelpers + + class RadioButtonBuilder < Builder # :nodoc: + def radio_button(extra_html_options={}) + html_options = extra_html_options.merge(@input_html_options) + @template_object.radio_button(@object_name, @method_name, @value, html_options) + end + 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 + end + + private + + def render_component(builder) + builder.radio_button + builder.label + end + end + end + end +end diff --git a/actionview/lib/action_view/helpers/tags/collection_select.rb b/actionview/lib/action_view/helpers/tags/collection_select.rb new file mode 100644 index 0000000000..6cb2b2e0d3 --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/collection_select.rb @@ -0,0 +1,28 @@ +module ActionView + module Helpers + module Tags # :nodoc: + class CollectionSelect < Base #:nodoc: + def initialize(object_name, method_name, template_object, collection, value_method, text_method, options, html_options) + @collection = collection + @value_method = value_method + @text_method = text_method + @html_options = html_options + + super(object_name, method_name, template_object, options) + end + + def render + option_tags_options = { + :selected => @options.fetch(:selected) { value(@object) }, + :disabled => @options[:disabled] + } + + select_content_tag( + options_from_collection_for_select(@collection, @value_method, @text_method, option_tags_options), + @options, @html_options + ) + end + end + end + end +end diff --git a/actionview/lib/action_view/helpers/tags/color_field.rb b/actionview/lib/action_view/helpers/tags/color_field.rb new file mode 100644 index 0000000000..d8fc797035 --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/color_field.rb @@ -0,0 +1,25 @@ +module ActionView + module Helpers + module Tags # :nodoc: + class ColorField < TextField # :nodoc: + def render + options = @options.stringify_keys + options["value"] = @options.fetch("value") { validate_color_string(value(object)) } + @options = options + super + end + + private + + def validate_color_string(string) + regex = /#[0-9a-fA-F]{6}/ + if regex.match(string) + string.downcase + else + "#000000" + end + end + end + end + end +end diff --git a/actionview/lib/action_view/helpers/tags/date_field.rb b/actionview/lib/action_view/helpers/tags/date_field.rb new file mode 100644 index 0000000000..c22be0db29 --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/date_field.rb @@ -0,0 +1,13 @@ +module ActionView + module Helpers + module Tags # :nodoc: + class DateField < DatetimeField # :nodoc: + private + + def format_date(value) + value.try(:strftime, "%Y-%m-%d") + end + end + end + end +end diff --git a/actionview/lib/action_view/helpers/tags/date_select.rb b/actionview/lib/action_view/helpers/tags/date_select.rb new file mode 100644 index 0000000000..0c4ac40070 --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/date_select.rb @@ -0,0 +1,72 @@ +require 'active_support/core_ext/time/calculations' + +module ActionView + module Helpers + module Tags # :nodoc: + class DateSelect < Base # :nodoc: + def initialize(object_name, method_name, template_object, options, html_options) + @html_options = html_options + + super(object_name, method_name, template_object, options) + end + + def render + error_wrapping(datetime_selector(@options, @html_options).send("select_#{select_type}").html_safe) + end + + class << self + def select_type + @select_type ||= self.name.split("::").last.sub("Select", "").downcase + end + end + + private + + def select_type + self.class.select_type + end + + def datetime_selector(options, html_options) + datetime = options.fetch(:selected) { value(object) || default_datetime(options) } + @auto_index ||= nil + + options = options.dup + options[:field_name] = @method_name + options[:include_position] = true + options[:prefix] ||= @object_name + options[:index] = @auto_index if @auto_index && !options.has_key?(:index) + + DateTimeSelector.new(datetime, options, html_options) + end + + def default_datetime(options) + return if options[:include_blank] || options[:prompt] + + case options[:default] + when nil + Time.current + when Date, Time + options[:default] + else + default = options[:default].dup + + # Rename :minute and :second to :min and :sec + default[:min] ||= default[:minute] + default[:sec] ||= default[:second] + + time = Time.current + + [:year, :month, :day, :hour, :min, :sec].each do |key| + default[key] ||= time.send(key) + end + + Time.utc( + default[:year], default[:month], default[:day], + default[:hour], default[:min], default[:sec] + ) + end + end + end + end + end +end diff --git a/actionview/lib/action_view/helpers/tags/datetime_field.rb b/actionview/lib/action_view/helpers/tags/datetime_field.rb new file mode 100644 index 0000000000..9a2279c611 --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/datetime_field.rb @@ -0,0 +1,22 @@ +module ActionView + module Helpers + module Tags # :nodoc: + class DatetimeField < TextField # :nodoc: + def render + options = @options.stringify_keys + options["value"] = @options.fetch("value") { format_date(value(object)) } + options["min"] = format_date(options["min"]) + options["max"] = format_date(options["max"]) + @options = options + super + end + + private + + def format_date(value) + value.try(:strftime, "%Y-%m-%dT%T.%L%z") + end + end + end + end +end diff --git a/actionview/lib/action_view/helpers/tags/datetime_local_field.rb b/actionview/lib/action_view/helpers/tags/datetime_local_field.rb new file mode 100644 index 0000000000..b4a74185d1 --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/datetime_local_field.rb @@ -0,0 +1,19 @@ +module ActionView + module Helpers + module Tags # :nodoc: + class DatetimeLocalField < DatetimeField # :nodoc: + class << self + def field_type + @field_type ||= "datetime-local" + end + end + + private + + def format_date(value) + value.try(:strftime, "%Y-%m-%dT%T") + end + end + end + end +end diff --git a/actionview/lib/action_view/helpers/tags/datetime_select.rb b/actionview/lib/action_view/helpers/tags/datetime_select.rb new file mode 100644 index 0000000000..563de1840e --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/datetime_select.rb @@ -0,0 +1,8 @@ +module ActionView + module Helpers + module Tags # :nodoc: + class DatetimeSelect < DateSelect # :nodoc: + end + end + end +end diff --git a/actionview/lib/action_view/helpers/tags/email_field.rb b/actionview/lib/action_view/helpers/tags/email_field.rb new file mode 100644 index 0000000000..7ce3ccb9bf --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/email_field.rb @@ -0,0 +1,8 @@ +module ActionView + module Helpers + module Tags # :nodoc: + class EmailField < TextField # :nodoc: + end + end + end +end diff --git a/actionview/lib/action_view/helpers/tags/file_field.rb b/actionview/lib/action_view/helpers/tags/file_field.rb new file mode 100644 index 0000000000..476b820d84 --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/file_field.rb @@ -0,0 +1,8 @@ +module ActionView + module Helpers + module Tags # :nodoc: + class FileField < TextField # :nodoc: + end + end + end +end diff --git a/actionview/lib/action_view/helpers/tags/grouped_collection_select.rb b/actionview/lib/action_view/helpers/tags/grouped_collection_select.rb new file mode 100644 index 0000000000..2ed4712dac --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/grouped_collection_select.rb @@ -0,0 +1,29 @@ +module ActionView + module Helpers + module Tags # :nodoc: + class GroupedCollectionSelect < Base # :nodoc: + def initialize(object_name, method_name, template_object, collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options) + @collection = collection + @group_method = group_method + @group_label_method = group_label_method + @option_key_method = option_key_method + @option_value_method = option_value_method + @html_options = html_options + + super(object_name, method_name, template_object, options) + end + + def render + option_tags_options = { + :selected => @options.fetch(:selected) { value(@object) }, + :disabled => @options[:disabled] + } + + select_content_tag( + option_groups_from_collection_for_select(@collection, @group_method, @group_label_method, @option_key_method, @option_value_method, option_tags_options), @options, @html_options + ) + end + end + end + end +end diff --git a/actionview/lib/action_view/helpers/tags/hidden_field.rb b/actionview/lib/action_view/helpers/tags/hidden_field.rb new file mode 100644 index 0000000000..c3757c2461 --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/hidden_field.rb @@ -0,0 +1,8 @@ +module ActionView + module Helpers + module Tags # :nodoc: + class HiddenField < TextField # :nodoc: + end + end + end +end diff --git a/actionview/lib/action_view/helpers/tags/label.rb b/actionview/lib/action_view/helpers/tags/label.rb new file mode 100644 index 0000000000..35d3ba8434 --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/label.rb @@ -0,0 +1,65 @@ +module ActionView + module Helpers + module Tags # :nodoc: + class Label < Base # :nodoc: + def initialize(object_name, method_name, template_object, content_or_options = nil, options = nil) + options ||= {} + + content_is_options = content_or_options.is_a?(Hash) + if content_is_options + options.merge! content_or_options + @content = nil + else + @content = content_or_options + end + + super(object_name, method_name, template_object, options) + end + + def render(&block) + options = @options.stringify_keys + tag_value = options.delete("value") + name_and_id = options.dup + + if name_and_id["for"] + name_and_id["id"] = name_and_id["for"] + else + name_and_id.delete("id") + end + + add_default_name_and_id_for_value(tag_value, name_and_id) + options.delete("index") + options.delete("namespace") + options["for"] = name_and_id["id"] unless options.key?("for") + + if block_given? + content = @template_object.capture(&block) + else + content = if @content.blank? + @object_name.gsub!(/\[(.*)_attributes\]\[\d\]/, '.\1') + method_and_value = tag_value.present? ? "#{@method_name}.#{tag_value}" : @method_name + + 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 + + content ||= if object && object.class.respond_to?(:human_attribute_name) + object.class.human_attribute_name(@method_name) + end + + content ||= @method_name.humanize + end + + label_tag(name_and_id["id"], content, options) + end + end + end + end +end diff --git a/actionview/lib/action_view/helpers/tags/month_field.rb b/actionview/lib/action_view/helpers/tags/month_field.rb new file mode 100644 index 0000000000..4c0fb846ee --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/month_field.rb @@ -0,0 +1,13 @@ +module ActionView + module Helpers + module Tags # :nodoc: + class MonthField < DatetimeField # :nodoc: + private + + def format_date(value) + value.try(:strftime, "%Y-%m") + end + end + end + end +end diff --git a/actionview/lib/action_view/helpers/tags/number_field.rb b/actionview/lib/action_view/helpers/tags/number_field.rb new file mode 100644 index 0000000000..4f95b1b4de --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/number_field.rb @@ -0,0 +1,18 @@ +module ActionView + module Helpers + module Tags # :nodoc: + class NumberField < TextField # :nodoc: + def render + options = @options.stringify_keys + + if range = options.delete("in") || options.delete("within") + options.update("min" => range.min, "max" => range.max) + end + + @options = options + super + end + end + end + end +end diff --git a/actionview/lib/action_view/helpers/tags/password_field.rb b/actionview/lib/action_view/helpers/tags/password_field.rb new file mode 100644 index 0000000000..6099fa6f19 --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/password_field.rb @@ -0,0 +1,12 @@ +module ActionView + module Helpers + module Tags # :nodoc: + class PasswordField < TextField # :nodoc: + def render + @options = {:value => nil}.merge!(@options) + super + end + end + end + end +end diff --git a/actionview/lib/action_view/helpers/tags/radio_button.rb b/actionview/lib/action_view/helpers/tags/radio_button.rb new file mode 100644 index 0000000000..4849c537a5 --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/radio_button.rb @@ -0,0 +1,31 @@ +require 'action_view/helpers/tags/checkable' + +module ActionView + module Helpers + module Tags # :nodoc: + class RadioButton < Base # :nodoc: + include Checkable + + def initialize(object_name, method_name, template_object, tag_value, options) + @tag_value = tag_value + super(object_name, method_name, template_object, options) + end + + def render + options = @options.stringify_keys + options["type"] = "radio" + options["value"] = @tag_value + options["checked"] = "checked" if input_checked?(object, options) + add_default_name_and_id_for_value(@tag_value, options) + tag("input", options) + end + + private + + def checked?(value) + value.to_s == @tag_value.to_s + end + end + end + end +end diff --git a/actionview/lib/action_view/helpers/tags/range_field.rb b/actionview/lib/action_view/helpers/tags/range_field.rb new file mode 100644 index 0000000000..f98ae88043 --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/range_field.rb @@ -0,0 +1,8 @@ +module ActionView + module Helpers + module Tags # :nodoc: + class RangeField < NumberField # :nodoc: + 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 new file mode 100644 index 0000000000..c09e2f1be7 --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/search_field.rb @@ -0,0 +1,24 @@ +module ActionView + module Helpers + module Tags # :nodoc: + class SearchField < TextField # :nodoc: + def render + options = @options.stringify_keys + + if options["autosave"] + if options["autosave"] == true + options["autosave"] = request.host.split(".").reverse.join(".") + end + options["results"] ||= 10 + end + + if options["onsearch"] + options["incremental"] = true unless options.has_key?("incremental") + end + + super + end + end + end + end +end diff --git a/actionview/lib/action_view/helpers/tags/select.rb b/actionview/lib/action_view/helpers/tags/select.rb new file mode 100644 index 0000000000..d64e2f68ef --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/select.rb @@ -0,0 +1,40 @@ +module ActionView + module Helpers + module Tags # :nodoc: + class Select < Base # :nodoc: + def initialize(object_name, method_name, template_object, choices, options, html_options) + @choices = choices + @choices = @choices.to_a if @choices.is_a?(Range) + @html_options = html_options + + super(object_name, method_name, template_object, options) + end + + def render + option_tags_options = { + :selected => @options.fetch(:selected) { value(@object) }, + :disabled => @options[:disabled] + } + + option_tags = if grouped_choices? + grouped_options_for_select(@choices, option_tags_options) + else + options_for_select(@choices, option_tags_options) + end + + select_content_tag(option_tags, @options, @html_options) + end + + private + + # Grouped choices look like this: + # + # [nil, []] + # { nil => [] } + def grouped_choices? + !@choices.empty? && @choices.first.respond_to?(:last) && Array === @choices.first.last + end + end + end + end +end diff --git a/actionview/lib/action_view/helpers/tags/tel_field.rb b/actionview/lib/action_view/helpers/tags/tel_field.rb new file mode 100644 index 0000000000..987bb9e67a --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/tel_field.rb @@ -0,0 +1,8 @@ +module ActionView + module Helpers + module Tags # :nodoc: + class TelField < TextField # :nodoc: + end + end + end +end diff --git a/actionview/lib/action_view/helpers/tags/text_area.rb b/actionview/lib/action_view/helpers/tags/text_area.rb new file mode 100644 index 0000000000..c81156c0c8 --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/text_area.rb @@ -0,0 +1,18 @@ +module ActionView + module Helpers + module Tags # :nodoc: + class TextArea < Base # :nodoc: + def render + options = @options.stringify_keys + add_default_name_and_id(options) + + if size = options.delete("size") + options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split) + end + + content_tag("textarea", options.delete('value') || value_before_type_cast(object), options) + end + end + end + end +end diff --git a/actionview/lib/action_view/helpers/tags/text_field.rb b/actionview/lib/action_view/helpers/tags/text_field.rb new file mode 100644 index 0000000000..baa5ff768e --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/text_field.rb @@ -0,0 +1,29 @@ +module ActionView + module Helpers + module Tags # :nodoc: + class TextField < Base # :nodoc: + def render + options = @options.stringify_keys + options["size"] = options["maxlength"] unless options.key?("size") + options["type"] ||= field_type + options["value"] = options.fetch("value"){ value_before_type_cast(object) } unless field_type == "file" + options["value"] &&= ERB::Util.html_escape(options["value"]) + add_default_name_and_id(options) + tag("input", options) + end + + class << self + def field_type + @field_type ||= self.name.split("::").last.sub("Field", "").downcase + end + end + + private + + def field_type + self.class.field_type + end + end + end + end +end diff --git a/actionview/lib/action_view/helpers/tags/time_field.rb b/actionview/lib/action_view/helpers/tags/time_field.rb new file mode 100644 index 0000000000..0e90a3aed7 --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/time_field.rb @@ -0,0 +1,13 @@ +module ActionView + module Helpers + module Tags # :nodoc: + class TimeField < DatetimeField # :nodoc: + private + + def format_date(value) + value.try(:strftime, "%T.%L") + end + end + end + end +end diff --git a/actionview/lib/action_view/helpers/tags/time_select.rb b/actionview/lib/action_view/helpers/tags/time_select.rb new file mode 100644 index 0000000000..0b06311d25 --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/time_select.rb @@ -0,0 +1,8 @@ +module ActionView + module Helpers + module Tags # :nodoc: + class TimeSelect < DateSelect # :nodoc: + end + end + end +end diff --git a/actionview/lib/action_view/helpers/tags/time_zone_select.rb b/actionview/lib/action_view/helpers/tags/time_zone_select.rb new file mode 100644 index 0000000000..80d165ec7e --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/time_zone_select.rb @@ -0,0 +1,20 @@ +module ActionView + module Helpers + module Tags # :nodoc: + class TimeZoneSelect < Base # :nodoc: + def initialize(object_name, method_name, template_object, priority_zones, options, html_options) + @priority_zones = priority_zones + @html_options = html_options + + super(object_name, method_name, template_object, options) + end + + def render + select_content_tag( + time_zone_options_for_select(value(@object) || @options[:default], @priority_zones, @options[:model] || ActiveSupport::TimeZone), @options, @html_options + ) + end + end + end + end +end diff --git a/actionview/lib/action_view/helpers/tags/url_field.rb b/actionview/lib/action_view/helpers/tags/url_field.rb new file mode 100644 index 0000000000..d76340178d --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/url_field.rb @@ -0,0 +1,8 @@ +module ActionView + module Helpers + module Tags # :nodoc: + class UrlField < TextField # :nodoc: + end + end + end +end diff --git a/actionview/lib/action_view/helpers/tags/week_field.rb b/actionview/lib/action_view/helpers/tags/week_field.rb new file mode 100644 index 0000000000..5b3d0494e9 --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/week_field.rb @@ -0,0 +1,13 @@ +module ActionView + module Helpers + module Tags # :nodoc: + class WeekField < DatetimeField # :nodoc: + private + + def format_date(value) + value.try(:strftime, "%Y-W%W") + 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 new file mode 100644 index 0000000000..147f9fd8ed --- /dev/null +++ b/actionview/lib/action_view/helpers/text_helper.rb @@ -0,0 +1,442 @@ +require 'active_support/core_ext/string/filters' +require 'active_support/core_ext/array/extract_options' + +module ActionView + # = Action View Text Helpers + module Helpers #:nodoc: + # The TextHelper module provides a set of methods for filtering, formatting + # and transforming strings, which can reduce the amount of inline Ruby code in + # your views. These helper methods extend Action View making them callable + # within your template files. + # + # ==== Sanitization + # + # Most text helpers by default sanitize the given content, but do not escape it. + # This means HTML tags will appear in the page but all malicious code will be removed. + # Let's look at some examples using the +simple_format+ method: + # + # simple_format('<a href="http://example.com/">Example</a>') + # # => "<p><a href=\"http://example.com/\">Example</a></p>" + # + # simple_format('<a href="javascript:alert(\'no!\')">Example</a>') + # # => "<p><a>Example</a></p>" + # + # If you want to escape all content, you should invoke the +h+ method before + # calling the text helper. + # + # simple_format h('<a href="http://example.com/">Example</a>') + # # => "<p><a href=\"http://example.com/\">Example</a></p>" + module TextHelper + extend ActiveSupport::Concern + + include SanitizeHelper + include TagHelper + # The preferred method of outputting text in your views is to use the + # <%= "text" %> eRuby syntax. The regular _puts_ and _print_ methods + # do not operate as expected in an eRuby code block. If you absolutely must + # output text within a non-output code block (i.e., <% %>), you can use the concat method. + # + # <% + # concat "hello" + # # is the equivalent of <%= "hello" %> + # + # if logged_in + # concat "Logged in!" + # else + # concat link_to('login', action: :login) + # end + # # will either display "Logged in!" or a login link + # %> + def concat(string) + output_buffer << string + end + + def safe_concat(string) + output_buffer.respond_to?(:safe_concat) ? output_buffer.safe_concat(string) : concat(string) + end + + # Truncates a given +text+ after a given <tt>:length</tt> if +text+ is longer than <tt>:length</tt> + # (defaults to 30). The last characters will be replaced with the <tt>:omission</tt> (defaults to "...") + # for a total length not exceeding <tt>:length</tt>. + # + # Pass a <tt>:separator</tt> to truncate +text+ at a natural break. + # + # Pass a block if you want to show extra content when the text is truncated. + # + # The result is marked as HTML-safe, but it is escaped by default, unless <tt>:escape</tt> is + # +false+. Care should be taken if +text+ contains HTML tags or entities, because truncation + # may produce invalid HTML (such as unbalanced or incomplete tags). + # + # truncate("Once upon a time in a world far far away") + # # => "Once upon a time in a world..." + # + # truncate("Once upon a time in a world far far away", length: 17) + # # => "Once upon a ti..." + # + # truncate("Once upon a time in a world far far away", length: 17, separator: ' ') + # # => "Once upon a..." + # + # truncate("And they found that many people were sleeping better.", length: 25, omission: '... (continued)') + # # => "And they f... (continued)" + # + # truncate("<p>Once upon a time in a world far far away</p>") + # # => "<p>Once upon a time in a wo..." + # + # truncate("Once upon a time in a world far far away") { link_to "Continue", "#" } + # # => "Once upon a time in a wo...<a href="#">Continue</a>" + def truncate(text, options = {}, &block) + if text + length = options.fetch(:length, 30) + + content = text.truncate(length, options) + content = options[:escape] == false ? content.html_safe : ERB::Util.html_escape(content) + content << capture(&block) if block_given? && text.length > length + content + end + end + + # 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>') + # + # highlight('You searched for: rails', 'rails') + # # => You searched for: <mark>rails</mark> + # + # highlight('You searched for: ruby, rails, dhh', 'actionpack') + # # => You searched for: ruby, rails, dhh + # + # highlight('You searched for: rails', ['for', 'rails'], highlighter: '<em>\1</em>') + # # => You searched <em>for</em>: <em>rails</em> + # + # highlight('You searched for: rails', 'rails', highlighter: '<a href="search?q=\1">\1</a>') + # # => You searched for: <a href="search?q=rails">rails</a> + def highlight(text, phrases, options = {}) + text = sanitize(text) if options.fetch(:sanitize, true) + + if text.blank? || phrases.blank? + text + else + highlighter = options.fetch(:highlighter, '<mark>\1</mark>') + match = Array(phrases).map { |p| Regexp.escape(p) }.join('|') + text.gsub(/(#{match})(?![^<]*?>)/i, highlighter) + end.html_safe + end + + # Extracts an excerpt from +text+ that matches the first instance of +phrase+. + # The <tt>:radius</tt> option expands the excerpt on each side of the first occurrence of +phrase+ by the number of characters + # defined in <tt>:radius</tt> (which defaults to 100). If the excerpt radius overflows the beginning or end of the +text+, + # then the <tt>:omission</tt> option (which defaults to "...") will be prepended/appended accordingly. Use the + # <tt>:separator</tt> option to choose the delimitation. The resulting string will be stripped in any case. If the +phrase+ + # isn't found, nil is returned. + # + # excerpt('This is an example', 'an', radius: 5) + # # => ...s is an exam... + # + # excerpt('This is an example', 'is', radius: 5) + # # => This is a... + # + # excerpt('This is an example', 'is') + # # => This is an example + # + # excerpt('This next thing is an example', 'ex', radius: 2) + # # => ...next... + # + # excerpt('This is also an example', 'an', radius: 8, omission: '<chop> ') + # # => <chop> is also an example + # + # excerpt('This is a very beautiful morning', 'very', separator: ' ', radius: 1) + # # => ...a very beautiful... + def excerpt(text, phrase, options = {}) + return unless text && phrase + + separator = options.fetch(:separator, "") + phrase = Regexp.escape(phrase) + regex = /#{phrase}/i + + return unless matches = text.match(regex) + phrase = matches[0] + + text.split(separator).each do |value| + if value.match(regex) + regex = phrase = value + break + end + end + + first_part, second_part = text.split(regex, 2) + + prefix, first_part = cut_excerpt_part(:first, first_part, separator, options) + postfix, second_part = cut_excerpt_part(:second, second_part, separator, options) + + prefix + (first_part + separator + phrase + separator + second_part).strip + postfix + end + + # Attempts to pluralize the +singular+ word unless +count+ is 1. If + # +plural+ is supplied, it will use that when count is > 1, otherwise + # it will use the Inflector to determine the plural form. + # + # pluralize(1, 'person') + # # => 1 person + # + # pluralize(2, 'person') + # # => 2 people + # + # pluralize(3, 'person', 'users') + # # => 3 users + # + # pluralize(0, 'person') + # # => 0 people + def pluralize(count, singular, plural = nil) + word = if (count == 1 || count =~ /^1(\.0+)?$/) + singular + else + plural || singular.pluralize + end + + "#{count || 0} #{word}" + end + + # Wraps the +text+ into lines no longer than +line_width+ width. This method + # breaks on the first whitespace character that does not exceed +line_width+ + # (which is 80 by default). + # + # word_wrap('Once upon a time') + # # => Once upon a time + # + # word_wrap('Once upon a time, in a kingdom called Far Far Away, a king fell ill, and finding a successor to the throne turned out to be more trouble than anyone could have imagined...') + # # => Once upon a time, in a kingdom called Far Far Away, a king fell ill, and finding\na successor to the throne turned out to be more trouble than anyone could have\nimagined... + # + # word_wrap('Once upon a time', line_width: 8) + # # => Once\nupon a\ntime + # + # 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) + + text.split("\n").collect do |line| + line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1\n").strip : line + end * "\n" + end + + # Returns +text+ transformed into HTML using simple formatting rules. + # Two or more consecutive newlines(<tt>\n\n</tt>) are considered as a + # paragraph and wrapped in <tt><p></tt> tags. One newline (<tt>\n</tt>) is + # considered as a linebreak and a <tt><br /></tt> tag is appended. This + # method does not remove the newlines from the +text+. + # + # You can pass any HTML attributes into <tt>html_options</tt>. These + # will be added to all created paragraphs. + # + # ==== Options + # * <tt>:sanitize</tt> - If +false+, does not sanitize +text+. + # * <tt>:wrapper_tag</tt> - String representing the wrapper tag, defaults to <tt>"p"</tt> + # + # ==== Examples + # my_text = "Here is some basic text...\n...with a line break." + # + # simple_format(my_text) + # # => "<p>Here is some basic text...\n<br />...with a line break.</p>" + # + # simple_format(my_text, {}, wrapper_tag: "div") + # # => "<div>Here is some basic text...\n<br />...with a line break.</div>" + # + # more_text = "We want to put a paragraph...\n\n...right there." + # + # simple_format(more_text) + # # => "<p>We want to put a paragraph...</p>\n\n<p>...right there.</p>" + # + # simple_format("Look ma! A class!", class: 'description') + # # => "<p class='description'>Look ma! A class!</p>" + # + # simple_format("<blink>Unblinkable.</blink>") + # # => "<p>Unblinkable.</p>" + # + # simple_format("<blink>Blinkable!</blink> It's true.", {}, sanitize: false) + # # => "<p><blink>Blinkable!</span> It's true.</p>" + def simple_format(text, html_options = {}, options = {}) + wrapper_tag = options.fetch(:wrapper_tag, :p) + + text = sanitize(text) if options.fetch(:sanitize, true) + paragraphs = split_paragraphs(text) + + if paragraphs.empty? + content_tag(wrapper_tag, nil, html_options) + else + paragraphs.map { |paragraph| + content_tag(wrapper_tag, paragraph, html_options, options[:sanitize]) + }.join("\n\n").html_safe + end + end + + # Creates a Cycle object whose _to_s_ method cycles through elements of an + # array every time it is called. This can be used for example, to alternate + # classes for table rows. You can use named cycles to allow nesting in loops. + # Passing a Hash as the last parameter with a <tt>:name</tt> key will create a + # named cycle. The default name for a cycle without a +:name+ key is + # <tt>"default"</tt>. You can manually reset a cycle by calling reset_cycle + # and passing the name of the cycle. The current cycle string can be obtained + # anytime using the current_cycle method. + # + # # Alternate CSS classes for even and odd numbers... + # @items = [1,2,3,4] + # <table> + # <% @items.each do |item| %> + # <tr class="<%= cycle("odd", "even") -%>"> + # <td>item</td> + # </tr> + # <% end %> + # </table> + # + # + # # Cycle CSS classes for rows, and text colors for values within each row + # @items = x = [{first: 'Robert', middle: 'Daniel', last: 'James'}, + # {first: 'Emily', middle: 'Shannon', maiden: 'Pike', last: 'Hicks'}, + # {first: 'June', middle: 'Dae', last: 'Jones'}] + # <% @items.each do |item| %> + # <tr class="<%= cycle("odd", "even", name: "row_class") -%>"> + # <td> + # <% item.values.each do |value| %> + # <%# Create a named cycle "colors" %> + # <span style="color:<%= cycle("red", "green", "blue", name: "colors") -%>"> + # <%= value %> + # </span> + # <% end %> + # <% reset_cycle("colors") %> + # </td> + # </tr> + # <% end %> + def cycle(first_value, *values) + options = values.extract_options! + name = options.fetch(:name, 'default') + + values.unshift(first_value) + + cycle = get_cycle(name) + unless cycle && cycle.values == values + cycle = set_cycle(name, Cycle.new(*values)) + end + cycle.to_s + end + + # Returns the current cycle string after a cycle has been started. Useful + # for complex table highlighting or any other design need which requires + # the current cycle string in more than one place. + # + # # Alternate background colors + # @items = [1,2,3,4] + # <% @items.each do |item| %> + # <div style="background-color:<%= cycle("red","white","blue") %>"> + # <span style="background-color:<%= current_cycle %>"><%= item %></span> + # </div> + # <% end %> + def current_cycle(name = "default") + cycle = get_cycle(name) + cycle.current_value if cycle + end + + # Resets a cycle so that it starts from the first element the next time + # it is called. Pass in +name+ to reset a named cycle. + # + # # Alternate CSS classes for even and odd numbers... + # @items = [[1,2,3,4], [5,6,3], [3,4,5,6,7,4]] + # <table> + # <% @items.each do |item| %> + # <tr class="<%= cycle("even", "odd") -%>"> + # <% item.each do |value| %> + # <span style="color:<%= cycle("#333", "#666", "#999", name: "colors") -%>"> + # <%= value %> + # </span> + # <% end %> + # + # <% reset_cycle("colors") %> + # </tr> + # <% end %> + # </table> + def reset_cycle(name = "default") + cycle = get_cycle(name) + cycle.reset if cycle + end + + class Cycle #:nodoc: + attr_reader :values + + def initialize(first_value, *values) + @values = values.unshift(first_value) + reset + end + + def reset + @index = 0 + end + + def current_value + @values[previous_index].to_s + end + + def to_s + value = @values[@index].to_s + @index = next_index + return value + end + + private + + def next_index + step_index(1) + end + + def previous_index + step_index(-1) + end + + def step_index(n) + (@index + n) % @values.size + end + end + + private + # The cycle helpers need to store the cycles in a place that is + # guaranteed to be reset every time a page is rendered, so it + # uses an instance variable of ActionView::Base. + def get_cycle(name) + @_cycles = Hash.new unless defined?(@_cycles) + return @_cycles[name] + end + + def set_cycle(name, cycle_object) + @_cycles = Hash.new unless defined?(@_cycles) + @_cycles[name] = cycle_object + end + + def split_paragraphs(text) + return [] if text.blank? + + text.to_str.gsub(/\r\n?/, "\n").split(/\n\n+/).map! do |t| + t.gsub!(/([^\n]\n)(?=[^\n])/, '\1<br />') || t + end + end + + def cut_excerpt_part(part_position, part, separator, options) + return "", "" unless part + + radius = options.fetch(:radius, 100) + omission = options.fetch(:omission, "...") + + part = part.split(separator) + part.delete("") + affix = part.size > radius ? omission : "" + + part = if part_position == :first + drop_index = [part.length - radius, 0].max + part.drop(drop_index) + else + part.first(radius) + end + + return affix, part.join(separator) + end + end + end +end diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb new file mode 100644 index 0000000000..ad8eb47f1f --- /dev/null +++ b/actionview/lib/action_view/helpers/translation_helper.rb @@ -0,0 +1,107 @@ +require 'action_view/helpers/tag_helper' +require 'i18n/exceptions' + +module I18n + class ExceptionHandler + include Module.new { + def call(exception, locale, key, options) + exception.is_a?(MissingTranslation) && options[:rescue_format] == :html ? super.html_safe : super + end + } + end +end + +module ActionView + # = Action View Translation Helpers + module Helpers + module TranslationHelper + # Delegates to <tt>I18n#translate</tt> but also performs three additional functions. + # + # First, it'll pass the <tt>rescue_format: :html</tt> option to I18n so that any + # thrown +MissingTranslation+ messages will be turned into 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. + # + # 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. + # + # 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. + # + # 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. + def translate(key, options = {}) + options.merge!(:rescue_format => :html) unless options.key?(:rescue_format) + options[:default] = wrap_translate_defaults(options[:default]) if options[:default] + if html_safe_translation_key?(key) + html_safe_options = options.dup + options.except(*I18n::RESERVED_KEYS).each do |name, value| + unless name == :count && value.is_a?(Numeric) + 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.respond_to?(:html_safe) ? translation.html_safe : translation + else + I18n.translate(scope_key_by_partial(key), options) + end + end + alias :t :translate + + # Delegates to <tt>I18n.localize</tt> with no additional functionality. + # + # See http://rubydoc.info/github/svenfuchs/i18n/master/I18n/Backend/Base:localize + # for more information. + def localize(*args) + I18n.localize(*args) + end + alias :l :localize + + private + def scope_key_by_partial(key) + if key.to_s.first == "." + if @virtual_path + @virtual_path.gsub(%r{/_?}, ".") + key.to_s + else + raise "Cannot use t(#{key.inspect}) shortcut because path is not available" + end + else + key + end + end + + 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 new file mode 100644 index 0000000000..19e5941971 --- /dev/null +++ b/actionview/lib/action_view/helpers/url_helper.rb @@ -0,0 +1,616 @@ +require 'action_view/helpers/javascript_helper' +require 'active_support/core_ext/array/access' +require 'active_support/core_ext/hash/keys' +require 'active_support/core_ext/string/output_safety' + +module ActionView + # = Action View URL Helpers + module Helpers #:nodoc: + # Provides a set of methods for making links and getting URLs that + # depend on the routing subsystem (see ActionDispatch::Routing). + # This allows you to use the same format for links in views + # and controllers. + module UrlHelper + # This helper may be included in any class that includes the + # URL helpers of a routes (routes.url_helpers). Some methods + # provided here will only work in the context of a request + # (link_to_unless_current, for instance), which must be provided + # as a method called #request on the context. + + extend ActiveSupport::Concern + + include TagHelper + + module ClassMethods + def _url_for_modules + ActionView::RoutingUrlFor + end + end + + # Basic implementation of url_for to allow use helpers without routes existence + def url_for(options = nil) # :nodoc: + case options + when String + options + when :back + _back_url + else + raise ArgumentError, "arguments passed to url_for can't be handled. Please require " + + "routes or provide your own implementation" + end + end + + def _back_url # :nodoc: + referrer = controller.respond_to?(:request) && controller.request.env["HTTP_REFERER"] + referrer || 'javascript:history.back()' + end + protected :_back_url + + # Creates a link tag 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 + # 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 + # the value of the link itself will become the name. + # + # ==== Signatures + # + # link_to(body, url, html_options = {}) + # # url is a String; you can use URL helpers like + # # posts_path + # + # link_to(body, url_options = {}, html_options = {}) + # # url_options, except :method, is passed to url_for + # + # link_to(options = {}, html_options = {}) do + # # name + # end + # + # link_to(url, html_options = {}) do + # # name + # end + # + # ==== Options + # * <tt>:data</tt> - This option can be used to add custom data attributes. + # * <tt>method: symbol of HTTP verb</tt> - This modifier will dynamically + # create an HTML form and immediately submit the form for processing using + # the HTTP verb specified. Useful for having links perform a POST operation + # in dangerous actions like deleting a record (which search bots can follow + # while spidering your site). Supported verbs are <tt>:post</tt>, <tt>:delete</tt>, <tt>:patch</tt>, and <tt>:put</tt>. + # Note that if the user has JavaScript disabled, the request will fall back + # to using GET. If <tt>href: '#'</tt> is used and the user has JavaScript + # disabled clicking the link will have no effect. If you are relying on the + # POST behavior, you should check for it in your controller's action by using + # the request object's methods for <tt>post?</tt>, <tt>delete?</tt>, <tt>:patch</tt>, or <tt>put?</tt>. + # * <tt>remote: true</tt> - This will allow the unobtrusive JavaScript + # driver to make an Ajax request to the URL in question instead of following + # the link. The drivers each provide mechanisms for listening for the + # completion of the Ajax request and performing JavaScript operations once + # they're complete + # + # ==== Data attributes + # + # * <tt>confirm: 'question?'</tt> - This will allow the unobtrusive JavaScript + # driver to prompt with the question specified. If the user accepts, the link 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. + # + # ==== Examples + # Because it relies on +url_for+, +link_to+ supports both older-style controller/action/id arguments + # and newer RESTful routes. Current Rails style favors RESTful routes whenever possible, so base + # your application on resources and use + # + # link_to "Profile", profile_path(@profile) + # # => <a href="/profiles/1">Profile</a> + # + # or the even pithier + # + # link_to "Profile", @profile + # # => <a href="/profiles/1">Profile</a> + # + # in place of the older more verbose, non-resource-oriented + # + # link_to "Profile", controller: "profiles", action: "show", id: @profile + # # => <a href="/profiles/show/1">Profile</a> + # + # Similarly, + # + # link_to "Profiles", profiles_path + # # => <a href="/profiles">Profiles</a> + # + # is better than + # + # link_to "Profiles", controller: "profiles" + # # => <a href="/profiles">Profiles</a> + # + # You can use a block as well if your link target is hard to fit into the name parameter. ERB example: + # + # <%= link_to(@profile) do %> + # <strong><%= @profile.name %></strong> -- <span>Check it out!</span> + # <% end %> + # # => <a href="/profiles/1"> + # <strong>David</strong> -- <span>Check it out!</span> + # </a> + # + # Classes and ids for CSS are easy to produce: + # + # link_to "Articles", articles_path, id: "news", class: "article" + # # => <a href="/articles" class="article" id="news">Articles</a> + # + # Be careful when using the older argument style, as an extra literal hash is needed: + # + # link_to "Articles", { controller: "articles" }, id: "news", class: "article" + # # => <a href="/articles" class="article" id="news">Articles</a> + # + # Leaving the hash off gives the wrong link: + # + # link_to "WRONG!", controller: "articles", id: "news", class: "article" + # # => <a href="/articles/index/news?class=article">WRONG!</a> + # + # +link_to+ can also produce links with anchors or query strings: + # + # link_to "Comment wall", profile_path(@profile, anchor: "wall") + # # => <a href="/profiles/1#wall">Comment wall</a> + # + # link_to "Ruby on Rails search", controller: "searches", query: "ruby on rails" + # # => <a href="/searches?query=ruby+on+rails">Ruby on Rails search</a> + # + # link_to "Nonsense search", searches_path(foo: "bar", baz: "quux") + # # => <a href="/searches?foo=bar&baz=quux">Nonsense search</a> + # + # The only option specific to +link_to+ (<tt>:method</tt>) is used as follows: + # + # link_to("Destroy", "http://www.example.com", method: :delete) + # # => <a href='http://www.example.com' rel="nofollow" data-method="delete">Destroy</a> + # + # You can also use custom data attributes using the <tt>:data</tt> option: + # + # 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> + def link_to(name = nil, options = nil, html_options = nil, &block) + html_options, options = options, name if block_given? + options ||= {} + + html_options = convert_options_to_data_attributes(options, html_options) + + url = url_for(options) + html_options['href'] ||= url + + content_tag(:a, name || url, html_options, &block) + end + + # Generates a form containing a single button that submits to the URL created + # by the set of +options+. This is the safest method to ensure links that + # cause changes to your data are not triggered by search bots or accelerators. + # If the HTML button does not work with your layout, you can also consider + # using the +link_to+ method with the <tt>:method</tt> modifier as described in + # the +link_to+ documentation. + # + # By default, the generated form element has a class name of <tt>button_to</tt> + # to allow styling of the form itself and its children. This can be changed + # using the <tt>:form_class</tt> modifier within +html_options+. You can control + # the form submission and input element behavior using +html_options+. + # This method accepts the <tt>:method</tt> modifier described in the +link_to+ documentation. + # If no <tt>:method</tt> modifier is given, it will default to performing a POST operation. + # You can also disable the button by passing <tt>disabled: true</tt> in +html_options+. + # If you are using RESTful routes, you can pass the <tt>:method</tt> + # to change the HTTP verb used to submit the form. + # + # ==== Options + # The +options+ hash accepts the same options as +url_for+. + # + # There are a few special +html_options+: + # * <tt>:method</tt> - Symbol of HTTP verb. Supported verbs are <tt>:post</tt>, <tt>:get</tt>, + # <tt>:delete</tt>, <tt>:patch</tt>, and <tt>:put</tt>. By default it will be <tt>:post</tt>. + # * <tt>:disabled</tt> - If set to true, it will generate a disabled button. + # * <tt>:data</tt> - This option can be used to add custom data attributes. + # * <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>:form</tt> - This hash will be form attributes + # * <tt>:form_class</tt> - This controls the class of the form within which the submit button will + # be placed + # + # ==== Data attributes + # + # * <tt>:confirm</tt> - This will use the unobtrusive JavaScript driver to + # prompt with the question specified. If the user accepts, the link 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. + # + # ==== Examples + # <%= button_to "New", action: "new" %> + # # => "<form method="post" action="/controller/new" class="button_to"> + # # <div><input value="New" type="submit" /></div> + # # </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> + # # </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> + # # </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> + # # </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> + # # </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> + # # </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') + + method = html_options.delete('method').to_s + method_tag = %w{patch put delete}.include?(method) ? method_tag(method) : ''.html_safe + + 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 + + request_token_tag = form_method == 'post' ? token_tag : '' + + html_options = convert_options_to_data_attributes(options, html_options) + html_options['type'] = 'submit' + + button = if block_given? + content_tag('button', html_options, &block) + else + html_options['value'] = name || url + tag('input', html_options) + end + + inner_tags = method_tag.safe_concat(button).safe_concat(request_token_tag) + content_tag('form', content_tag('div', inner_tags), form_options) + end + + # Creates a link tag of the given +name+ using a URL created by the set of + # +options+ unless the current request URI is the same as the links, in + # which case only the name is returned (or the given block is yielded, if + # one exists). You can give +link_to_unless_current+ a block which will + # specialize the default behavior (e.g., show a "Start Here" link rather + # than the link's text). + # + # ==== Examples + # Let's say you have a navigation menu... + # + # <ul id="navbar"> + # <li><%= link_to_unless_current("Home", { action: "index" }) %></li> + # <li><%= link_to_unless_current("About Us", { action: "about" }) %></li> + # </ul> + # + # If in the "about" action, it will render... + # + # <ul id="navbar"> + # <li><a href="/controller/index">Home</a></li> + # <li>About Us</li> + # </ul> + # + # ...but if in the "index" action, it will render: + # + # <ul id="navbar"> + # <li>Home</li> + # <li><a href="/controller/about">About Us</a></li> + # </ul> + # + # The implicit block given to +link_to_unless_current+ is evaluated if the current + # action is the action given. So, if we had a comments page and wanted to render a + # "Go Back" link instead of a link to the comments page, we could do something like this... + # + # <%= + # link_to_unless_current("Comment", { controller: "comments", action: "new" }) do + # link_to("Go back", { controller: "posts", action: "index" }) + # end + # %> + def link_to_unless_current(name, options = {}, html_options = {}, &block) + link_to_unless current_page?(options), name, options, html_options, &block + end + + # Creates a link tag of the given +name+ using a URL created by the set of + # +options+ unless +condition+ is true, in which case only the name is + # returned. To specialize the default behavior (i.e., show a login link rather + # than just the plaintext link text), you can pass a block that + # accepts the name or the full argument list for +link_to_unless+. + # + # ==== Examples + # <%= link_to_unless(@current_user.nil?, "Reply", { action: "reply" }) %> + # # If the user is logged in... + # # => <a href="/controller/reply/">Reply</a> + # + # <%= + # link_to_unless(@current_user.nil?, "Reply", { action: "reply" }) do |name| + # link_to(name, { controller: "accounts", action: "signup" }) + # end + # %> + # # If the user is logged in... + # # => <a href="/controller/reply/">Reply</a> + # # If not... + # # => <a href="/accounts/signup">Reply</a> + def link_to_unless(condition, name, options = {}, html_options = {}, &block) + if condition + if block_given? + block.arity <= 1 ? capture(name, &block) : capture(name, options, html_options, &block) + else + ERB::Util.html_escape(name) + end + else + link_to(name, options, html_options) + end + end + + # Creates a link tag of the given +name+ using a URL created by the set of + # +options+ if +condition+ is true, otherwise only the name is + # returned. To specialize the default behavior, you can pass a block that + # accepts the name or the full argument list for +link_to_unless+ (see the examples + # in +link_to_unless+). + # + # ==== Examples + # <%= link_to_if(@current_user.nil?, "Login", { controller: "sessions", action: "new" }) %> + # # If the user isn't logged in... + # # => <a href="/sessions/new/">Login</a> + # + # <%= + # link_to_if(@current_user.nil?, "Login", { controller: "sessions", action: "new" }) do + # link_to(@current_user.login, { controller: "accounts", action: "show", id: @current_user }) + # end + # %> + # # If the user isn't logged in... + # # => <a href="/sessions/new/">Login</a> + # # If they are logged in... + # # => <a href="/accounts/show/3">my_username</a> + def link_to_if(condition, name, options = {}, html_options = {}, &block) + link_to_unless !condition, name, options, html_options, &block + end + + # Creates a mailto link tag to the specified +email_address+, which is + # also used as the name of the link unless +name+ is specified. Additional + # HTML attributes for the link can be passed in +html_options+. + # + # +mail_to+ has several methods for customizing the email itself by + # passing special keys to +html_options+. + # + # ==== Options + # * <tt>:subject</tt> - Preset the subject line of the email. + # * <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. + # + # ==== Obfuscation + # Prior to Rails 4.0, +mail_to+ provided options for encoding the address + # in order to hinder email harvesters. To take advantage of these options, + # install the +actionview-encoded_mail_to+ gem. + # + # ==== Examples + # mail_to "me@domain.com" + # # => <a href="mailto:me@domain.com">me@domain.com</a> + # + # mail_to "me@domain.com", "My email" + # # => <a href="mailto:me@domain.com">My email</a> + # + # mail_to "me@domain.com", "My email", cc: "ccaddress@domain.com", + # subject: "This is an example email" + # # => <a href="mailto:me@domain.com?cc=ccaddress@domain.com&subject=This%20is%20an%20example%20email">My email</a> + # + # You can use a block as well if your link target is hard to fit into the name parameter. ERB example: + # + # <%= mail_to "me@domain.com" do %> + # <strong>Email me:</strong> <span>me@domain.com</span> + # <% end %> + # # => <a href="mailto:me@domain.com"> + # <strong>Email me:</strong> <span>me@domain.com</span> + # </a> + def mail_to(email_address, name = nil, html_options = {}, &block) + email_address = ERB::Util.html_escape(email_address) + + 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)}" + }.compact + extras = extras.empty? ? '' : '?' + ERB::Util.html_escape(extras.join('&')) + + html_options["href"] = "mailto:#{email_address}#{extras}".html_safe + + content_tag(:a, name || email_address.html_safe, 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. + # + # 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') + # # => 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') + # # => 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?(action: 'checkout') + # # => true + # + # current_page?(controller: 'library', action: 'checkout') + # # => false + # + # 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 + # + def current_page?(options) + unless request + raise "You cannot use helpers that need to determine the current " \ + "page unless your view context provides a Request object " \ + "in a #request method" + end + + return false unless request.get? || request.head? + + url_string = url_for(options) + + # We ignore any extra parameters in the request_uri if the + # submitted url doesn't have any either. This lets the function + # work with things like ?order=asc + request_uri = url_string.index("?") ? request.fullpath : request.path + + if url_string =~ /^\w+:\/\// + url_string == "#{request.protocol}#{request.host_with_port}#{request_uri}" + else + url_string == request_uri + end + end + + private + def convert_options_to_data_attributes(options, html_options) + if html_options + html_options = html_options.stringify_keys + html_options['data-remote'] = 'true' if link_to_remote_options?(options) || link_to_remote_options?(html_options) + + method = html_options.delete('method') + + add_method_to_attributes!(html_options, method) if method + + html_options + else + link_to_remote_options?(options) ? {'data-remote' => 'true'} : {} + end + end + + def link_to_remote_options?(options) + if options.is_a?(Hash) + options.delete('remote') || options.delete(:remote) + end + end + + def add_method_to_attributes!(html_options, method) + if method && method.to_s.downcase != "get" && html_options["rel"] !~ /nofollow/ + html_options["rel"] = "#{html_options["rel"]} nofollow".lstrip + end + 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 + tag(:input, type: "hidden", name: request_forgery_protection_token.to_s, value: token) + else + '' + end + end + + def method_tag(method) + tag('input', type: 'hidden', name: '_method', value: method.to_s) + end + end + end +end diff --git a/actionview/lib/action_view/locale/en.yml b/actionview/lib/action_view/locale/en.yml new file mode 100644 index 0000000000..8a56f147b8 --- /dev/null +++ b/actionview/lib/action_view/locale/en.yml @@ -0,0 +1,56 @@ +"en": + # Used in distance_of_time_in_words(), distance_of_time_in_words_to_now(), time_ago_in_words() + datetime: + distance_in_words: + half_a_minute: "half a minute" + less_than_x_seconds: + one: "less than 1 second" + other: "less than %{count} seconds" + x_seconds: + one: "1 second" + other: "%{count} seconds" + less_than_x_minutes: + one: "less than a minute" + other: "less than %{count} minutes" + x_minutes: + one: "1 minute" + other: "%{count} minutes" + about_x_hours: + one: "about 1 hour" + other: "about %{count} hours" + x_days: + one: "1 day" + other: "%{count} days" + about_x_months: + one: "about 1 month" + other: "about %{count} months" + x_months: + one: "1 month" + other: "%{count} months" + about_x_years: + one: "about 1 year" + other: "about %{count} years" + over_x_years: + one: "over 1 year" + other: "over %{count} years" + almost_x_years: + one: "almost 1 year" + other: "almost %{count} years" + prompts: + year: "Year" + month: "Month" + day: "Day" + hour: "Hour" + minute: "Minute" + second: "Seconds" + + helpers: + select: + # Default value for :prompt => true in FormOptionsHelper + prompt: "Please select" + + # Default translation keys for submit and button FormHelper + submit: + create: 'Create %{model}' + update: 'Update %{model}' + submit: 'Save %{model}' diff --git a/actionview/lib/action_view/log_subscriber.rb b/actionview/lib/action_view/log_subscriber.rb new file mode 100644 index 0000000000..fd9a543e0a --- /dev/null +++ b/actionview/lib/action_view/log_subscriber.rb @@ -0,0 +1,30 @@ +module ActionView + # = Action View Log Subscriber + # + # Provides functionality so that Rails can output logs from Action View. + class LogSubscriber < ActiveSupport::LogSubscriber + VIEWS_PATTERN = /^app\/views\//.freeze + + def render_template(event) + return unless logger.info? + message = " Rendered #{from_rails_root(event.payload[:identifier])}" + message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout] + message << " (#{event.duration.round(1)}ms)" + info(message) + end + alias :render_partial :render_template + alias :render_collection :render_template + + def logger + ActionView::Base.logger + end + + protected + + def from_rails_root(string) + string.sub("#{Rails.root}/", "").sub(VIEWS_PATTERN, "") + end + end +end + +ActionView::LogSubscriber.attach_to :action_view diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb new file mode 100644 index 0000000000..f9d5b97fe3 --- /dev/null +++ b/actionview/lib/action_view/lookup_context.rb @@ -0,0 +1,241 @@ +require 'thread_safe' +require 'active_support/core_ext/module/remove_method' +require 'active_support/core_ext/module/attribute_accessors' + +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. + class LookupContext #:nodoc: + attr_accessor :prefixes, :rendered_format + + mattr_accessor :fallbacks + @@fallbacks = FallbackFileSystemResolver.instances + + mattr_accessor :registered_details + self.registered_details = [] + + def self.register_detail(name, options = {}, &block) + self.registered_details << name + initialize = registered_details.map { |n| "@details[:#{n}] = details[:#{n}] || default_#{n}" } + + Accessors.send :define_method, :"default_#{name}", &block + Accessors.module_eval <<-METHOD, __FILE__, __LINE__ + 1 + def #{name} + @details.fetch(:#{name}, []) + end + + def #{name}=(value) + value = value.present? ? Array(value) : default_#{name} + _set_detail(:#{name}, value) if value != @details[:#{name}] + end + + remove_possible_method :initialize_details + def initialize_details(details) + #{initialize.join("\n")} + end + METHOD + end + + # Holds accessors for the registered details. + module Accessors #:nodoc: + end + + register_detail(:locale) do + locales = [I18n.locale] + locales.concat(I18n.fallbacks[I18n.locale]) if I18n.respond_to? :fallbacks + locales << I18n.default_locale + locales.uniq! + locales + end + register_detail(:formats) { ActionView::Base.default_formats || [:html, :text, :js, :css, :xml, :json] } + register_detail(:handlers){ Template::Handlers.extensions } + + class DetailsKey #:nodoc: + alias :eql? :equal? + alias :object_hash :hash + + attr_reader :hash + @details_keys = ThreadSafe::Cache.new + + def self.get(details) + @details_keys[details] ||= new + end + + def self.clear + @details_keys.clear + end + + def initialize + @hash = object_hash + end + end + + # Add caching behavior on top of Details. + module DetailsCache + attr_accessor :cache + + # Calculate the details key. Remove the handlers from calculation to improve performance + # since the user cannot modify it explicitly. + def details_key #:nodoc: + @details_key ||= DetailsKey.get(@details) if @cache + end + + # Temporary skip passing the details_key forward. + def disable_cache + old_value, @cache = @cache, false + yield + ensure + @cache = old_value + end + + protected + + def _set_detail(key, value) + @details = @details.dup if @details_key + @details_key = nil + @details[key] = value + end + end + + # Helpers related to template lookup using the lookup context information. + module ViewPaths + attr_reader :view_paths, :html_fallback_for_js + + # Whenever setting view paths, makes a copy so we can manipulate then in + # instance objects as we wish. + def view_paths=(paths) + @view_paths = ActionView::PathSet.new(Array(paths)) + end + + def find(name, prefixes = [], partial = false, keys = [], options = {}) + @view_paths.find(*args_for_lookup(name, prefixes, partial, keys, options)) + end + alias :find_template :find + + def find_all(name, prefixes = [], partial = false, keys = [], options = {}) + @view_paths.find_all(*args_for_lookup(name, prefixes, partial, keys, options)) + end + + def exists?(name, prefixes = [], partial = false, keys = [], options = {}) + @view_paths.exists?(*args_for_lookup(name, prefixes, partial, keys, options)) + end + alias :template_exists? :exists? + + # Add fallbacks to the view paths. Useful in cases you are rendering a :file. + def with_fallbacks + added_resolvers = 0 + self.class.fallbacks.each do |resolver| + next if view_paths.include?(resolver) + view_paths.push(resolver) + added_resolvers += 1 + end + yield + ensure + added_resolvers.times { view_paths.pop } + end + + protected + + def args_for_lookup(name, prefixes, partial, keys, details_options) #:nodoc: + name, prefixes = normalize_name(name, prefixes) + details, details_key = detail_args_for(details_options) + [name, prefixes, partial || false, details, details_key, keys] + end + + # Compute details hash and key according to user options (e.g. passed from #render). + def detail_args_for(options) + return @details, details_key if options.empty? # most common path. + user_details = @details.merge(options) + [user_details, DetailsKey.get(user_details)] + end + + # Support legacy foo.erb names even though we now ignore .erb + # as well as incorrectly putting part of the path in the template + # name instead of the prefix. + def normalize_name(name, prefixes) #:nodoc: + prefixes = prefixes.presence + parts = name.to_s.split('/') + parts.shift if parts.first.empty? + name = parts.pop + + return name, prefixes || [""] if parts.empty? + + parts = parts.join('/') + prefixes = prefixes ? prefixes.map { |p| "#{p}/#{parts}" } : [parts] + + return name, prefixes + end + end + + include Accessors + include DetailsCache + include ViewPaths + + def initialize(view_paths, details = {}, prefixes = []) + @details, @details_key = {}, nil + @skip_default_locale = false + @cache = true + @prefixes = prefixes + @rendered_format = nil + + self.view_paths = view_paths + initialize_details(details) + end + + # Override formats= to expand ["*/*"] values and automatically + # add :html as fallback to :js. + def formats=(values) + if values + values.concat(default_formats) if values.delete "*/*" + if values == [:js] + values << :html + @html_fallback_for_js = true + end + end + 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 + end + + # Overload locale= to also set the I18n.locale. If the current I18n.config object responds + # to original_config, it means that it's has a copy of the original I18n configuration and it's + # acting as proxy, which we need to skip. + def locale=(value) + if value + config = I18n.config.respond_to?(:original_config) ? I18n.config.original_config : I18n.config + config.locale = value + end + + super(@skip_default_locale ? I18n.locale : default_locale) + end + + # A method which only 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 + end + end +end diff --git a/actionview/lib/action_view/model_naming.rb b/actionview/lib/action_view/model_naming.rb new file mode 100644 index 0000000000..e09ebd60df --- /dev/null +++ b/actionview/lib/action_view/model_naming.rb @@ -0,0 +1,12 @@ +module ActionView + module ModelNaming + # 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 + end + end +end diff --git a/actionview/lib/action_view/path_set.rb b/actionview/lib/action_view/path_set.rb new file mode 100644 index 0000000000..91ee2ea8f5 --- /dev/null +++ b/actionview/lib/action_view/path_set.rb @@ -0,0 +1,77 @@ +module ActionView #:nodoc: + # = Action View PathSet + # + # This class is used to store and access paths in Action View. A number of + # operations are defined so that you can search among the paths in this + # set and also perform operations on other +PathSet+ objects. + # + # A +LookupContext+ will use a +PathSet+ to store the paths in its context. + class PathSet #:nodoc: + include Enumerable + + attr_reader :paths + + delegate :[], :include?, :pop, :size, :each, to: :paths + + def initialize(paths = []) + @paths = typecast paths + end + + def initialize_copy(other) + @paths = other.paths.dup + self + end + + def to_ary + paths.dup + end + + def compact + PathSet.new paths.compact + end + + def +(array) + PathSet.new(paths + array) + end + + %w(<< concat push insert unshift).each do |method| + class_eval <<-METHOD, __FILE__, __LINE__ + 1 + def #{method}(*args) + paths.#{method}(*typecast(args)) + end + METHOD + end + + def find(*args) + find_all(*args).first || raise(MissingTemplate.new(self, *args)) + end + + def find_all(path, prefixes = [], *args) + prefixes = [prefixes] if String === prefixes + prefixes.each do |prefix| + paths.each do |resolver| + templates = resolver.find_all(path, prefix, *args) + return templates unless templates.empty? + end + end + [] + end + + def exists?(path, prefixes, *args) + find_all(path, prefixes, *args).any? + end + + private + + def typecast(paths) + paths.map do |path| + case path + when Pathname, String + OptimizedFileSystemResolver.new path.to_s + else + path + end + end + end + end +end diff --git a/actionview/lib/action_view/railtie.rb b/actionview/lib/action_view/railtie.rb new file mode 100644 index 0000000000..e80e0ed9b0 --- /dev/null +++ b/actionview/lib/action_view/railtie.rb @@ -0,0 +1,39 @@ +require "action_view" +require "rails" + +module ActionView + # = Action View Railtie + class Railtie < Rails::Railtie # :nodoc: + config.action_view = ActiveSupport::OrderedOptions.new + config.action_view.embed_authenticity_token_in_remote_forms = false + + config.eager_load_namespaces << ActionView + + initializer "action_view.embed_authenticity_token_in_remote_forms" do |app| + ActiveSupport.on_load(:action_view) do + ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = + app.config.action_view.delete(:embed_authenticity_token_in_remote_forms) + end + end + + initializer "action_view.logger" do + ActiveSupport.on_load(:action_view) { self.logger ||= Rails.logger } + end + + initializer "action_view.set_configs" do |app| + ActiveSupport.on_load(:action_view) do + app.config.action_view.each do |k,v| + send "#{k}=", v + end + end + end + + initializer "action_view.caching" do |app| + ActiveSupport.on_load(:action_view) do + if app.config.action_view.cache_template_loading.nil? + ActionView::Resolver.caching = app.config.cache_classes + end + end + end + end +end diff --git a/actionview/lib/action_view/record_identifier.rb b/actionview/lib/action_view/record_identifier.rb new file mode 100644 index 0000000000..63f645431a --- /dev/null +++ b/actionview/lib/action_view/record_identifier.rb @@ -0,0 +1,84 @@ +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. + # + # # routes + # resources :posts + # + # # view + # <%= div_for(post) do %> <div id="post_45" class="post"> + # <%= post.body %> What a wonderful world! + # <% end %> </div> + # + # # controller + # def update + # post = Post.find(params[:id]) + # post.update(params[:post]) + # + # redirect_to(post) # Calls polymorphic_url(post) which in turn calls post_url(post) + # end + # + # 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. + module RecordIdentifier + extend self + extend ModelNaming + + include ModelNaming + + JOIN = '_'.freeze + NEW = 'new'.freeze + + # The DOM class convention is to use the singular form of an object or class. + # + # dom_class(post) # => "post" + # dom_class(Person) # => "person" + # + # If you need to address multiple instances of the same class in the same view, you can prefix the dom_class: + # + # dom_class(post, :edit) # => "edit_post" + # dom_class(Person, :edit) # => "edit_person" + def dom_class(record_or_class, prefix = nil) + singular = model_name_from_record_or_class(record_or_class).param_key + prefix ? "#{prefix}#{JOIN}#{singular}" : singular + end + + # The DOM id convention is to use the singular form of an object or class with the id following an underscore. + # If no id is found, prefix with "new_" instead. + # + # dom_id(Post.find(45)) # => "post_45" + # dom_id(Post.new) # => "new_post" + # + # If you need to address multiple instances of the same class in the same view, you can prefix the dom_id: + # + # dom_id(Post.find(45), :edit) # => "edit_post_45" + # dom_id(Post.new, :custom) # => "custom_post" + def dom_id(record, prefix = nil) + if record_id = record_key_for_dom_id(record) + "#{dom_class(record, prefix)}#{JOIN}#{record_id}" + else + dom_class(record, prefix || NEW) + end + end + + protected + + # Returns a string representation of the key attribute(s) that is suitable for use in an HTML DOM id. + # This can be overwritten to customize the default generated string representation if desired. + # If you need to read back a key from a dom_id in order to query for the underlying database record, + # you should write a helper like 'person_record_from_dom_id' that will extract the key either based + # on the default implementation (which just joins all key attributes with '_') or on your own + # overwritten version of the method. By default, this implementation passes the key string through a + # method that replaces all characters that are invalid inside DOM ids, with valid ones. You need to + # 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 + end + end +end diff --git a/actionview/lib/action_view/renderer/abstract_renderer.rb b/actionview/lib/action_view/renderer/abstract_renderer.rb new file mode 100644 index 0000000000..73c19a0ae2 --- /dev/null +++ b/actionview/lib/action_view/renderer/abstract_renderer.rb @@ -0,0 +1,47 @@ +module ActionView + # This class defines the interface for a renderer. Each class that + # subclasses +AbstractRenderer+ is used by the base +Renderer+ class to + # render a specific type of object. + # + # The base +Renderer+ class uses its +render+ method to delegate to the + # renderers. These currently consist of + # + # PartialRenderer - Used for rendering partials + # TemplateRenderer - Used for rendering other types of templates + # StreamingTemplateRenderer - Used for streaming + # + # Whenever the +render+ method is called on the base +Renderer+ class, a new + # renderer object of the correct type is created, and the +render+ method on + # that new object is called in turn. This abstracts the setup and rendering + # into a separate classes for partials and templates. + class AbstractRenderer #:nodoc: + delegate :find_template, :template_exists?, :with_fallbacks, :with_layout_format, :formats, :to => :@lookup_context + + def initialize(lookup_context) + @lookup_context = lookup_context + end + + def render + raise NotImplementedError + end + + protected + + def extract_details(options) + @lookup_context.registered_details.each_with_object({}) do |key, details| + next unless value = options[key] + details[key] = Array(value) + end + end + + def instrument(name, options={}) + ActiveSupport::Notifications.instrument("render_#{name}.action_view", options){ yield } + end + + def prepend_formats(formats) + formats = Array(formats) + return if formats.empty? || @lookup_context.html_fallback_for_js + @lookup_context.formats = formats | @lookup_context.formats + end + end +end diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb new file mode 100644 index 0000000000..821026268a --- /dev/null +++ b/actionview/lib/action_view/renderer/partial_renderer.rb @@ -0,0 +1,492 @@ +require 'thread_safe' + +module ActionView + # = Action View Partials + # + # There's also a convenience method for rendering sub templates within the current controller that depends on a + # single object (we call this kind of sub templates for partials). It relies on the fact that partials should + # follow the naming convention of being prefixed with an underscore -- as to separate them from regular + # templates that could be rendered on their own. + # + # In a template for Advertiser#account: + # + # <%= render partial: "account" %> + # + # This would render "advertiser/_account.html.erb". + # + # In another template for Advertiser#buy, we could have: + # + # <%= render partial: "account", locals: { account: @buyer } %> + # + # <% @advertisements.each do |ad| %> + # <%= render partial: "ad", locals: { ad: ad } %> + # <% end %> + # + # This would first render "advertiser/_account.html.erb" with @buyer passed in as the local variable +account+, then + # render "advertiser/_ad.html.erb" and pass the local variable +ad+ to the template for display. + # + # == The :as and :object options + # + # By default <tt>ActionView::PartialRenderer</tt> doesn't have any local variables. + # The <tt>:object</tt> option can be used to pass an object to the partial. For instance: + # + # <%= render partial: "account", object: @buyer %> + # + # would provide the <tt>@buyer</tt> object to the partial, available under the local variable +account+ and is + # equivalent to: + # + # <%= render partial: "account", locals: { account: @buyer } %> + # + # With the <tt>:as</tt> option we can specify a different name for said local variable. For example, if we + # wanted it to be +user+ instead of +account+ we'd do: + # + # <%= render partial: "account", object: @buyer, as: 'user' %> + # + # This is equivalent to + # + # <%= render partial: "account", locals: { user: @buyer } %> + # + # == 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 + # accepts an array and renders a partial by the same name as the elements contained within. So the three-lined + # example in "Using partials" can be rewritten with a single line: + # + # <%= render partial: "ad", collection: @advertisements %> + # + # This will render "advertiser/_ad.html.erb" and pass the local variable +ad+ to the template for display. An + # iteration counter will automatically be made available to the template with a name of the form + # +partial_name_counter+. In the case of the example above, the template would be fed +ad_counter+. + # + # The <tt>:as</tt> option may be used when rendering partials. + # + # You can specify a partial to be rendered between elements via the <tt>:spacer_template</tt> option. + # The following example will render <tt>advertiser/_ad_divider.html.erb</tt> between each ad partial: + # + # <%= render partial: "ad", collection: @advertisements, spacer_template: "ad_divider" %> + # + # If the given <tt>:collection</tt> is nil or empty, <tt>render</tt> will return nil. This will allow you + # to specify a text which will displayed instead by using this form: + # + # <%= render(partial: "ad", collection: @advertisements) || "There's no ad to be displayed" %> + # + # 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 + # + # Two controllers can share a set of partials and render them like this: + # + # <%= render partial: "advertisement/ad", locals: { ad: @advertisement } %> + # + # 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` + # + # 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. + # + # # @account.to_partial_path returns 'accounts/account', so it can be used to replace: + # # <%= render partial: "accounts/account", locals: { account: @account} %> + # <%= render partial: @account %> + # + # # @posts is an array of Post instances, so every post record returns 'posts/post' on `to_partial_path`, + # # that's why we can replace: + # # <%= render partial: "posts/post", collection: @posts %> + # <%= render partial: @posts %> + # + # == 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: + # + # # Instead of <%= render partial: "account" %> + # <%= render "account" %> + # + # # Instead of <%= render partial: "account", locals: { account: @buyer } %> + # <%= render "account", account: @buyer %> + # + # # @account.to_partial_path returns 'accounts/account', so it can be used to replace: + # # <%= render partial: "accounts/account", locals: { account: @account} %> + # <%= render @account %> + # + # # @posts is an array of Post instances, so every post record returns 'posts/post' on `to_partial_path`, + # # that's why we can replace: + # # <%= render partial: "posts/post", collection: @posts %> + # <%= render @posts %> + # + # == 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 &> + # 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 &> + # Name: <%= user.name %> + # + # <%# app/views/users/_administrator.html.erb &> + # <div id="administrator"> + # Budget: $<%= user.budget %> + # <%= yield %> + # </div> + # + # <%# app/views/users/_editor.html.erb &> + # <div id="editor"> + # Deadline: <%= user.deadline %> + # <%= yield %> + # </div> + # + # ...this will return: + # + # Here's the administrator: + # <div id="administrator"> + # Budget: $<%= user.budget %> + # Name: <%= user.name %> + # </div> + # + # Here's the editor: + # <div id="editor"> + # Deadline: <%= user.deadline %> + # Name: <%= user.name %> + # </div> + # + # If a collection is given, the layout will be rendered once for each item in + # the collection. Just think these two snippets have the same output: + # + # <%# app/views/users/_user.html.erb %> + # Name: <%= user.name %> + # + # <%# app/views/users/index.html.erb %> + # <%# This does not use layouts %> + # <ul> + # <% users.each do |user| -%> + # <li> + # <%= render partial: "user", locals: { user: user } %> + # </li> + # <% end -%> + # </ul> + # + # <%# app/views/users/_li_layout.html.erb %> + # <li> + # <%= yield %> + # </li> + # + # <%# app/views/users/index.html.erb %> + # <ul> + # <%= render partial: "user", layout: "li_layout", collection: users %> + # </ul> + # + # Given two users whose names are Alice and Bob, these snippets return: + # + # <ul> + # <li> + # Name: Alice + # </li> + # <li> + # Name: Bob + # </li> + # </ul> + # + # The current object being rendered, as well as the object_counter, will be + # available as local variables inside the layout template under the same names + # as available in the partial. + # + # You can also apply a layout to a block within any template: + # + # <%# app/views/users/_chief.html.erb &> + # <%= render(layout: "administrator", locals: { user: chief }) do %> + # Title: <%= chief.title %> + # <% end %> + # + # ...this will return: + # + # <div id="administrator"> + # Budget: $<%= user.budget %> + # Title: <%= chief.name %> + # </div> + # + # As you can see, the <tt>:locals</tt> hash is shared between both the partial and its layout. + # + # 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 &> + # <div class="user"> + # Budget: $<%= user.budget %> + # <%= yield user %> + # </div> + # + # <%# app/views/users/index.html.erb &> + # <%= render layout: @users do |user| %> + # Title: <%= user.title %> + # <% end %> + # + # This will render the layout for each user and yield to the block, passing the user, each time. + # + # You can also yield multiple times in one layout and use block arguments to differentiate the sections. + # + # <%# 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 &> + # <%= render layout: @users do |user, section| %> + # <%- case section when :header -%> + # Title: <%= user.title %> + # <%- when :footer -%> + # Deadline: <%= user.deadline %> + # <%- end -%> + # <% end %> + class PartialRenderer < AbstractRenderer + PREFIXED_PARTIAL_NAMES = ThreadSafe::Cache.new do |h, k| + h[k] = ThreadSafe::Cache.new + end + + def initialize(*) + super + @context_prefix = @lookup_context.prefixes.first + end + + def render(context, options, block) + setup(context, options, block) + identifier = (@template = find_partial) ? @template.identifier : @path + + @lookup_context.rendered_format ||= begin + if @template && @template.formats.present? + @template.formats.first + else + formats.first + end + end + + if @collection + instrument(:collection, :identifier => identifier || "collection", :count => @collection.size) do + render_collection + end + else + instrument(:partial, :identifier => identifier) do + render_partial + end + end + end + + def render_collection + return nil if @collection.blank? + + if @options.key?(:spacer_template) + 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 + end + + def render_partial + view, locals, block = @view, @locals, @block + object, as = @object, @variable + + if !block && (layout = @options[:layout]) + layout = find_template(layout.to_s, @template_keys) + end + + object ||= locals[as] + locals[as] = object + + content = @template.render(view, locals) do |*name| + view._layout_for(*name, &block) + end + + content = layout.render(view, locals){ content } if layout + 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. + # + # If +options[:partial]+ is a string, then the +@path+ instance variable is + # set to that string. Otherwise, the +options[:partial]+ object must + # respond to +to_partial_path+ in order to setup the path. + def setup(context, options, block) + @view = context + partial = options[:partial] + + @options = options + @locals = options[:locals] || {} + @block = block + @details = extract_details(options) + + prepend_formats(options[:formats]) + + if String === partial + @object = options[:object] + @path = partial + @collection = collection + else + @object = partial + + if @collection = collection_from_object || collection + paths = @collection_data = @collection.map { |o| partial_path(o) } + @path = paths.uniq.size == 1 ? paths.first : nil + else + @path = partial_path + end + end + + if as = options[:as] + raise_invalid_identifier(as) unless as.to_s =~ /\A[a-z_]\w*\z/ + as = as.to_sym + end + + if @path + @variable, @variable_counter = retrieve_variable(@path, as) + @template_keys = retrieve_template_keys + else + paths.map! { |path| retrieve_variable(path, as).unshift(path) } + end + + self + end + + def collection + if @options.key?(:collection) + collection = @options[:collection] + collection.respond_to?(:to_ary) ? collection.to_ary : [] + end + end + + def collection_from_object + @object.to_ary if @object.respond_to?(:to_ary) + end + + def find_partial + if path = @path + find_template(path, @template_keys) + end + end + + def find_template(path, locals) + prefixes = path.include?(?/) ? [] : @lookup_context.prefixes + @lookup_context.find_template(path, prefixes, true, locals, @details) + end + + def collection_with_template + view, locals, template = @view, @locals, @template + as, counter = @variable, @variable_counter + + if layout = @options[:layout] + layout = find_template(layout, @template_keys) + end + + index = -1 + @collection.map do |object| + locals[as] = object + locals[counter] = (index += 1) + + content = template.render(view, locals) + content = layout.render(view, locals) { content } if layout + content + end + end + + def collection_without_template + view, locals, collection_data = @view, @locals, @collection_data + cache = {} + keys = @locals.keys + + index = -1 + @collection.map do |object| + index += 1 + path, as, counter = collection_data[index] + + locals[as] = object + locals[counter] = index + + template = (cache[path] ||= find_template(path, keys + [as, counter])) + template.render(view, locals) + end + end + + # Obtains the path to where the object's partial is located. If the object + # responds to +to_partial_path+, then +to_partial_path+ will be called and + # will provide the path. If the object does not respond to +to_partial_path+, + # then an +ArgumentError+ is raised. + # + # If +prefix_partial_path_with_controller_namespace+ is true, then this + # method will prefix the partial paths with a namespace. + def partial_path(object = @object) + object = object.to_model if object.respond_to?(:to_model) + + path = if object.respond_to?(:to_partial_path) + object.to_partial_path + else + raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.") + end + + if @view.prefix_partial_path_with_controller_namespace + prefixed_partial_names[path] ||= merge_prefix_into_object_path(@context_prefix, path.dup) + else + path + end + end + + def prefixed_partial_names + @prefixed_partial_names ||= PREFIXED_PARTIAL_NAMES[@context_prefix] + end + + def merge_prefix_into_object_path(prefix, object_path) + if prefix.include?(?/) && object_path.include?(?/) + prefixes = [] + prefix_array = File.dirname(prefix).split('/') + object_path_array = object_path.split('/')[0..-3] # skip model dir & partial + + prefix_array.each_with_index do |dir, index| + break if dir == object_path_array[index] + prefixes << dir + end + + (prefixes << object_path).join("/") + else + object_path + end + end + + def retrieve_template_keys + keys = @locals.keys + keys << @variable if @object || @collection + keys << @variable_counter if @collection + keys + end + + 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/ + $1.to_sym + end + variable_counter = :"#{variable}_counter" if @collection + [variable, variable_counter] + 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, " + + "and is followed by any combination of letters, numbers and underscores." + + def raise_invalid_identifier(path) + raise ArgumentError.new(IDENTIFIER_ERROR_MESSAGE % (path)) + end + end +end diff --git a/actionview/lib/action_view/renderer/renderer.rb b/actionview/lib/action_view/renderer/renderer.rb new file mode 100644 index 0000000000..964b18337e --- /dev/null +++ b/actionview/lib/action_view/renderer/renderer.rb @@ -0,0 +1,50 @@ +module ActionView + # This is the main entry point for rendering. It basically delegates + # to other objects like TemplateRenderer and PartialRenderer which + # actually renders the template. + # + # The Renderer will parse the options from the +render+ or +render_body+ + # method and render a partial or a template based on the options. The + # +TemplateRenderer+ and +PartialRenderer+ objects are wrappers which do all + # the setup and logic necessary to render a view and a new object is created + # each time +render+ is called. + class Renderer + attr_accessor :lookup_context + + def initialize(lookup_context) + @lookup_context = lookup_context + end + + # Main render entry point shared by AV and AC. + def render(context, options) + if options.key?(:partial) + render_partial(context, options) + else + render_template(context, options) + end + end + + # Render but returns a valid Rack body. If fibers are defined, we return + # a streaming body that renders the template piece by piece. + # + # Note that partials are not supported to be rendered with streaming, + # so in such cases, we just wrap them in an array. + def render_body(context, options) + if options.key?(:partial) + [render_partial(context, options)] + else + StreamingTemplateRenderer.new(@lookup_context).render(context, options) + end + end + + # Direct accessor to template rendering. + def render_template(context, options) #:nodoc: + TemplateRenderer.new(@lookup_context).render(context, options) + end + + # Direct access to partial rendering. + def render_partial(context, options, &block) #:nodoc: + PartialRenderer.new(@lookup_context).render(context, options, block) + end + end +end diff --git a/actionview/lib/action_view/renderer/streaming_template_renderer.rb b/actionview/lib/action_view/renderer/streaming_template_renderer.rb new file mode 100644 index 0000000000..9cf6eb0c65 --- /dev/null +++ b/actionview/lib/action_view/renderer/streaming_template_renderer.rb @@ -0,0 +1,103 @@ +require 'fiber' + +module ActionView + # == TODO + # + # * Support streaming from child templates, partials and so on. + # * Integrate exceptions with exceptron + # * Rack::Cache needs to support streaming bodies + class StreamingTemplateRenderer < TemplateRenderer #:nodoc: + # A valid Rack::Body (i.e. it responds to each). + # It is initialized with a block that, when called, starts + # rendering the template. + class Body #:nodoc: + def initialize(&start) + @start = start + end + + def each(&block) + begin + @start.call(block) + rescue Exception => exception + log_error(exception) + block.call ActionView::Base.streaming_completion_on_exception + end + self + end + + private + + # This is the same logging logic as in ShowExceptions middleware. + # TODO Once "exceptron" is in, refactor this piece to simply re-use exceptron. + def log_error(exception) #:nodoc: + logger = ActionView::Base.logger + return unless logger + + message = "\n#{exception.class} (#{exception.message}):\n" + message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code) + message << " " << exception.backtrace.join("\n ") + logger.fatal("#{message}\n\n") + end + end + + # For streaming, instead of rendering a given a template, we return a Body + # object that responds to each. This object is initialized with a block + # that knows how to render the template. + def render_template(template, layout_name = nil, locals = {}) #:nodoc: + return [super] unless layout_name && template.supports_streaming? + + locals ||= {} + layout = layout_name && find_layout(layout_name, locals.keys) + + Body.new do |buffer| + delayed_render(buffer, template, layout, @view, locals) + end + end + + private + + def delayed_render(buffer, template, layout, view, locals) + # Wrap the given buffer in the StreamingBuffer and pass it to the + # underlying template handler. Now, everytime something is concatenated + # to the buffer, it is not appended to an array, but streamed straight + # to the client. + output = ActionView::StreamingBuffer.new(buffer) + yielder = lambda { |*name| view._layout_for(*name) } + + instrument(:template, :identifier => template.identifier, :layout => layout.try(:virtual_path)) do + fiber = Fiber.new do + if layout + layout.render(view, locals, output, &yielder) + else + # If you don't have a layout, just render the thing + # and concatenate the final result. This is the same + # as a layout with just <%= yield %> + output.safe_concat view._layout_for + end + end + + # Set the view flow to support streaming. It will be aware + # when to stop rendering the layout because it needs to search + # something in the template and vice-versa. + view.view_flow = StreamingFlow.new(view, fiber) + + # Yo! Start the fiber! + fiber.resume + + # If the fiber is still alive, it means we need something + # from the template, so start rendering it. If not, it means + # the layout exited without requiring anything from the template. + if fiber.alive? + content = template.render(view, locals, &yielder) + + # Once rendering the template is done, sets its content in the :layout key. + view.view_flow.set(:layout, content) + + # In case the layout continues yielding, we need to resume + # the fiber until all yields are handled. + fiber.resume while fiber.alive? + end + end + end + end +end diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb new file mode 100644 index 0000000000..4d5c5db80c --- /dev/null +++ b/actionview/lib/action_view/renderer/template_renderer.rb @@ -0,0 +1,96 @@ +require 'active_support/core_ext/object/try' + +module ActionView + class TemplateRenderer < AbstractRenderer #:nodoc: + def render(context, options) + @view = context + @details = extract_details(options) + template = determine_template(options) + context = @lookup_context + + prepend_formats(template.formats) + + unless context.rendered_format + context.rendered_format = template.formats.first || formats.last + end + + render_template(template, options[:layout], options[:locals]) + end + + # Determine the template to be rendered using the given options. + def determine_template(options) #:nodoc: + keys = options.fetch(:locals, {}).keys + + if options.key?(:text) + Template::Text.new(options[:text], formats.first) + elsif options.key?(:file) + with_fallbacks { find_template(options[:file], nil, false, keys, @details) } + elsif options.key?(:inline) + handler = Template.handler_for_extension(options[:type] || "erb") + Template.new(options[:inline], "inline template", handler, :locals => keys) + elsif options.key?(:template) + if options[:template].respond_to?(:render) + options[:template] + else + 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 or :text option." + end + end + + # Renders the given template. A string representing the layout can be + # supplied as well. + def render_template(template, layout_name = nil, locals = nil) #:nodoc: + view, locals = @view, locals || {} + + render_with_layout(layout_name, locals) do |layout| + instrument(:template, :identifier => template.identifier, :layout => layout.try(:virtual_path)) do + template.render(view, locals) { |*name| view._layout_for(*name) } + end + end + end + + def render_with_layout(path, locals) #:nodoc: + layout = path && find_layout(path, locals.keys) + content = yield(layout) + + if layout + view = @view + view.view_flow.set(:layout, content) + layout.render(view, locals){ |*name| view._layout_for(*name) } + else + content + end + end + + # 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) } + end + + def resolve_layout(layout, keys) + case layout + when String + begin + if layout =~ /^\// + with_fallbacks { find_template(layout, nil, false, keys, @details) } + else + 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 + else + layout + end + end + end +end diff --git a/actionview/lib/action_view/routing_url_for.rb b/actionview/lib/action_view/routing_url_for.rb new file mode 100644 index 0000000000..f10e7e88ba --- /dev/null +++ b/actionview/lib/action_view/routing_url_for.rb @@ -0,0 +1,107 @@ +module ActionView + module RoutingUrlFor + + # Returns the URL for the set of +options+ provided. This takes the + # same options as +url_for+ in Action Controller (see the + # documentation for <tt>ActionController::Base#url_for</tt>). Note that by default + # <tt>:only_path</tt> is <tt>true</tt> so you'll get the relative "/controller/action" + # instead of the fully qualified URL like "http://example.com/controller/action". + # + # ==== Options + # * <tt>:anchor</tt> - Specifies the anchor name to be appended to the path. + # * <tt>:only_path</tt> - If true, returns the relative URL (omitting the protocol, host name, and port) (<tt>true</tt> by default unless <tt>:host</tt> is specified). + # * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2005/". Note that this + # is currently not recommended since it breaks caching. + # * <tt>:host</tt> - Overrides the default (current) host if provided. + # * <tt>:protocol</tt> - Overrides the default (current) protocol if provided. + # * <tt>:user</tt> - Inline HTTP authentication (only plucked out if <tt>:password</tt> is also present). + # * <tt>:password</tt> - Inline HTTP authentication (only plucked out if <tt>:user</tt> is also present). + # + # ==== Relying on named routes + # + # Passing a record (like an Active Record) instead of a hash as the options parameter will + # trigger the named route for that record. The lookup will happen on the name of the class. So passing a + # Workshop object will attempt to use the +workshop_path+ route. If you have a nested route, such as + # +admin_workshop_path+ you'll have to call that explicitly (it's impossible for +url_for+ to guess that route). + # + # ==== Implicit Controller Namespacing + # + # Controllers passed in using the +:controller+ option will retain their namespace unless it is an absolute one. + # + # ==== Examples + # <%= url_for(action: 'index') %> + # # => /blog/ + # + # <%= url_for(action: 'find', controller: 'books') %> + # # => /books/find + # + # <%= url_for(action: 'login', controller: 'members', only_path: false, protocol: 'https') %> + # # => https://www.example.com/members/login/ + # + # <%= url_for(action: 'play', anchor: 'player') %> + # # => /messages/play/#player + # + # <%= url_for(action: 'jump', anchor: 'tax&ship') %> + # # => /testing/jump/#tax&ship + # + # <%= url_for(Workshop.new) %> + # # relies on Workshop answering a persisted? call (and in this case returning false) + # # => /workshops + # + # <%= url_for(@workshop) %> + # # calls @workshop.to_param which by default returns the id + # # => /workshops/5 + # + # # to_param can be re-defined in a model to provide different URL names: + # # => /workshops/1-workshop-name + # + # <%= url_for("http://www.example.com") %> + # # => http://www.example.com + # + # <%= url_for(:back) %> + # # if request.env["HTTP_REFERER"] is set to "http://www.example.com" + # # => http://www.example.com + # + # <%= url_for(:back) %> + # # if request.env["HTTP_REFERER"] is not set or is blank + # # => javascript:history.back() + # + # <%= url_for(action: 'index', controller: 'users') %> + # # Assuming an "admin" namespace + # # => /admin/users + # + # <%= url_for(action: 'index', controller: '/users') %> + # # Specify absolute path with beginning slash + # # => /users + def url_for(options = nil) + case options + when String + options + when nil, Hash + options ||= {} + options = { :only_path => options[:host].nil? }.merge!(options.symbolize_keys) + super + when :back + _back_url + else + polymorphic_path(options) + end + end + + def url_options #:nodoc: + return super unless controller.respond_to?(:url_options) + controller.url_options + end + + def _routes_context #:nodoc: + controller + end + protected :_routes_context + + def optimize_routes_generation? #:nodoc: + controller.respond_to?(:optimize_routes_generation?, true) ? + controller.optimize_routes_generation? : super + end + protected :optimize_routes_generation? + end +end diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb new file mode 100644 index 0000000000..e2c50fec47 --- /dev/null +++ b/actionview/lib/action_view/template.rb @@ -0,0 +1,340 @@ +require 'active_support/core_ext/object/try' +require 'active_support/core_ext/kernel/singleton_class' +require 'thread' + +module ActionView + # = Action View Template + class Template + extend ActiveSupport::Autoload + + # === Encodings in ActionView::Template + # + # ActionView::Template is one of a few sources of potential + # encoding issues in Rails. This is because the source for + # templates are usually read from disk, and Ruby (like most + # encoding-aware programming languages) assumes that the + # String retrieved through File IO is encoded in the + # <tt>default_external</tt> encoding. In Rails, the default + # <tt>default_external</tt> encoding is UTF-8. + # + # As a result, if a user saves their template as ISO-8859-1 + # (for instance, using a non-Unicode-aware text editor), + # and uses characters outside of the ASCII range, their + # users will see diamonds with question marks in them in + # the browser. + # + # For the rest of this documentation, when we say "UTF-8", + # we mean "UTF-8 or whatever the default_internal encoding + # is set to". By default, it will be UTF-8. + # + # To mitigate this problem, we use a few strategies: + # 1. If the source is not valid UTF-8, we raise an exception + # when the template is compiled to alert the user + # to the problem. + # 2. The user can specify the encoding using Ruby-style + # encoding comments in any template engine. If such + # a comment is supplied, Rails will apply that encoding + # to the resulting compiled source returned by the + # template handler. + # 3. In all cases, we transcode the resulting String to + # the UTF-8. + # + # This means that other parts of Rails can always assume + # that templates are encoded in UTF-8, even if the original + # source of the template was not UTF-8. + # + # From a user's perspective, the easiest thing to do is + # to save your templates as UTF-8. If you do this, you + # do not need to do anything else for things to "just work". + # + # === Instructions for template handlers + # + # The easiest thing for you to do is to simply ignore + # encodings. Rails will hand you the template source + # as the default_internal (generally UTF-8), raising + # an exception for the user before sending the template + # to you if it could not determine the original encoding. + # + # For the greatest simplicity, you can support only + # UTF-8 as the <tt>default_internal</tt>. This means + # that from the perspective of your handler, the + # entire pipeline is just UTF-8. + # + # === Advanced: Handlers with alternate metadata sources + # + # If you want to provide an alternate mechanism for + # specifying encodings (like ERB does via <%# encoding: ... %>), + # you may indicate that you will handle encodings yourself + # by implementing <tt>self.handles_encoding?</tt> + # on your handler. + # + # If you do, Rails will not try to encode the String + # into the default_internal, passing you the unaltered + # bytes tagged with the assumed encoding (from + # default_external). + # + # In this case, make sure you return a String from + # your handler encoded in the default_internal. Since + # you are handling out-of-band metadata, you are + # also responsible for alerting the user to any + # problems with converting the user's data to + # the <tt>default_internal</tt>. + # + # To do so, simply raise +WrongEncodingError+ as follows: + # + # raise WrongEncodingError.new( + # problematic_string, + # expected_encoding + # ) + + eager_autoload do + autoload :Error + autoload :Handlers + autoload :Text + autoload :Types + end + + extend Template::Handlers + + attr_accessor :locals, :formats, :virtual_path + + attr_reader :source, :identifier, :handler, :original_encoding, :updated_at + + # This finalizer is needed (and exactly with a proc inside another proc) + # otherwise templates leak in development. + Finalizer = proc do |method_name, mod| + proc do + mod.module_eval do + remove_possible_method method_name + end + end + end + + def initialize(source, identifier, handler, details) + format = details[:format] || (handler.default_format if handler.respond_to?(:default_format)) + + @source = source + @identifier = identifier + @handler = handler + @compiled = false + @original_encoding = nil + @locals = details[:locals] || [] + @virtual_path = details[:virtual_path] + @updated_at = details[:updated_at] || Time.now + @formats = Array(format).map { |f| f.respond_to?(:ref) ? f.ref : f } + @compile_mutex = Mutex.new + end + + # Returns if 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? + end + + # Render a template. If the template was not compiled yet, it is done + # exactly before rendering. + # + # This method is instrumented as "!render_template.action_view". Notice that + # 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 + compile!(view) + view.send(method_name, locals, buffer, &block) + end + rescue Exception => e + handle_render_error(view, e) + end + + def type + @type ||= Types[@formats.first] if @formats.first + 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 + # anymore since it was already compiled. In such cases, all you need to do is to call + # refresh passing in the view object. + # + # Notice this method raises an error if the template to be refreshed does not have a + # virtual path set (true just for inline templates). + def refresh(view) + raise "A template needs to have a virtual path in order to be refreshed" unless @virtual_path + lookup = view.lookup_context + pieces = @virtual_path.split("/") + name = pieces.pop + partial = !!name.sub!(/^_/, "") + lookup.disable_cache do + lookup.find_template(name, [ pieces.join('/') ], partial, @locals) + end + end + + def inspect + @inspect ||= defined?(Rails.root) ? identifier.sub("#{Rails.root}/", '') : identifier + end + + # This method is responsible for properly setting the encoding of the + # source. Until this point, we assume that the source is BINARY data. + # If no additional information is supplied, we assume the encoding is + # the same as <tt>Encoding.default_external</tt>. + # + # The user can also specify the encoding via a comment on the first + # line of the template (# encoding: NAME-OF-ENCODING). This will work + # with any template engine, as we process out the encoding comment + # before passing the source on to the template engine, leaving a + # blank line in its stead. + def encode! + return unless source.encoding == Encoding::BINARY + + # Look for # encoding: *. If we find one, we'll encode the + # String in that encoding, otherwise, we'll use the + # default external encoding. + if source.sub!(/\A#{ENCODING_FLAG}/, '') + encoding = magic_encoding = $1 + else + encoding = Encoding.default_external + end + + # Tag the source with the default external encoding + # or the encoding specified in the file + source.force_encoding(encoding) + + # If the user didn't specify an encoding, and the handler + # handles encodings, we simply pass the String as is to + # the handler (with the default_external tag) + if !magic_encoding && @handler.respond_to?(:handles_encoding?) && @handler.handles_encoding? + source + # Otherwise, if the String is valid in the encoding, + # encode immediately to default_internal. This means + # that if a handler doesn't handle encodings, it will + # always get Strings in the default_internal + elsif source.valid_encoding? + source.encode! + # Otherwise, since the String is invalid in the encoding + # specified, raise an exception + else + raise WrongEncodingError.new(source, encoding) + end + end + + protected + + # Compile a template. This method ensures a template is compiled + # just once and removes the source after it is compiled. + def compile!(view) #:nodoc: + return if @compiled + + # Templates can be used concurrently in threaded environments + # so compilation and any instance variable modification must + # be synchronized + @compile_mutex.synchronize do + # Any thread holding this lock will be compiling the template needed + # by the threads waiting. So re-check the @compiled flag to avoid + # re-compilation + return if @compiled + + if view.is_a?(ActionView::CompiledTemplates) + mod = ActionView::CompiledTemplates + else + mod = view.singleton_class + end + + instrument("!compile_template") do + compile(view, mod) + end + + # Just discard the source if we have a virtual path. This + # means we can get the template back. + @source = nil if @virtual_path + @compiled = true + end + end + + # Among other things, this method is responsible for properly setting + # the encoding of the compiled template. + # + # If the template engine handles encodings, we send the encoded + # String to the engine without further processing. This allows + # the template engine to support additional mechanisms for + # specifying the encoding. For instance, ERB supports <%# encoding: %> + # + # Otherwise, after we figure out the correct encoding, we then + # 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: + encode! + method_name = self.method_name + code = @handler.call(self) + + # Make sure that the resulting String to be eval'd is in the + # encoding of the code + source = <<-end_src + def #{method_name}(local_assigns, output_buffer) + _old_virtual_path, @virtual_path = @virtual_path, #{@virtual_path.inspect};_old_output_buffer = @output_buffer;#{locals_code};#{code} + ensure + @virtual_path, @output_buffer = _old_virtual_path, _old_output_buffer + end + end_src + + # Make sure the source is in the encoding of the returned code + source.force_encoding(code.encoding) + + # In case we get back a String from a handler that is not in + # BINARY or the default_internal, encode it to the default_internal + source.encode! + + # Now, validate that the source we got back from the template + # handler is valid in the default_internal. This is for handlers + # that handle encoding but screw up + unless source.valid_encoding? + raise WrongEncodingError.new(@source, Encoding.default_internal) + end + + begin + mod.module_eval(source, identifier, 0) + ObjectSpace.define_finalizer(self, Finalizer[method_name, mod]) + rescue Exception => 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 + end + + def handle_render_error(view, e) #:nodoc: + if e.is_a?(Template::Error) + e.sub_template_of(self) + raise e + else + template = self + unless template.source + template = refresh(view) + template.encode! + end + raise Template::Error.new(template, e) + end + end + + def locals_code #:nodoc: + # Double assign to suppress the dreaded 'assigned but unused variable' warning + @locals.map { |key| "#{key} = #{key} = local_assigns[:#{key}];" }.join + end + + def method_name #:nodoc: + @method_name ||= "_#{identifier_method_name}__#{@identifier.hash}_#{__id__}".gsub('-', "_") + end + + def identifier_method_name #:nodoc: + inspect.gsub(/[^a-z_]/, '_') + end + + def instrument(action, &block) + payload = { virtual_path: @virtual_path, identifier: @identifier } + ActiveSupport::Notifications.instrument("#{action}.action_view", payload, &block) + end + end +end diff --git a/actionview/lib/action_view/template/error.rb b/actionview/lib/action_view/template/error.rb new file mode 100644 index 0000000000..a89d51221e --- /dev/null +++ b/actionview/lib/action_view/template/error.rb @@ -0,0 +1,138 @@ +require "active_support/core_ext/enumerable" + +module ActionView + # = Action View Errors + class ActionViewError < StandardError #:nodoc: + end + + class EncodingError < StandardError #:nodoc: + end + + class MissingRequestError < StandardError #:nodoc: + end + + class WrongEncodingError < EncodingError #:nodoc: + def initialize(string, encoding) + @string, @encoding = string, encoding + end + + def message + @string.force_encoding(Encoding::ASCII_8BIT) + "Your template was not saved as valid #{@encoding}. Please " \ + "either specify #{@encoding} as the encoding for your template " \ + "in your text editor, or mark the template with its " \ + "encoding by inserting the following as the first line " \ + "of the template:\n\n# encoding: <name of correct encoding>.\n\n" \ + "The source of your template was:\n\n#{@string}" + end + end + + class MissingTemplate < ActionViewError #:nodoc: + attr_reader :path + + def initialize(paths, path, prefixes, partial, details, *) + @path = path + prefixes = Array(prefixes) + template_type = if partial + "partial" + elsif path =~ /layouts/i + 'layout' + else + 'template' + end + + searched_paths = prefixes.map { |prefix| [prefix, path].join("/") } + + out = "Missing #{template_type} #{searched_paths.join(", ")} with #{details.inspect}. Searched in:\n" + out += paths.compact.map { |p| " * #{p.to_s.inspect}\n" }.join + super out + end + end + + class Template + # The Template::Error exception is raised when the compilation or rendering of the template + # fails. This exception then gathers a bunch of intimate details and uses it to report a + # precise exception message. + class Error < ActionViewError #:nodoc: + SOURCE_CODE_RADIUS = 3 + + attr_reader :original_exception, :backtrace + + def initialize(template, original_exception) + super(original_exception.message) + @template, @original_exception = template, original_exception + @sub_templates = nil + @backtrace = original_exception.backtrace + end + + def file_name + @template.identifier + end + + def sub_template_message + if @sub_templates + "Trace of template inclusion: " + + @sub_templates.collect { |template| template.inspect }.join(", ") + else + "" + end + end + + def source_extract(indentation = 0, output = :console) + return unless num = line_number + num = num.to_i + + source_code = @template.source.split("\n") + + start_on_line = [ num - SOURCE_CODE_RADIUS - 1, 0 ].max + end_on_line = [ num + SOURCE_CODE_RADIUS - 1, source_code.length].min + + indent = end_on_line.to_s.size + indentation + return unless source_code = source_code[start_on_line..end_on_line] + + formatted_code_for(source_code, start_on_line, indent, output) + end + + def sub_template_of(template_path) + @sub_templates ||= [] + @sub_templates << template_path + end + + def line_number + @line_number ||= + if file_name + regexp = /#{Regexp.escape File.basename(file_name)}:(\d+)/ + $1 if message =~ regexp || backtrace.find { |line| line =~ regexp } + end + end + + def annoted_source_code + source_extract(4) + end + + private + + def source_location + if line_number + "on line ##{line_number} of " + else + 'in ' + end + file_name + end + + def formatted_code_for(source_code, line_counter, indent, output) + start_value = (output == :html) ? {} : "" + source_code.inject(start_value) do |result, line| + line_counter += 1 + if output == :html + result.update(line_counter.to_s => "%#{indent}s %s\n" % ["", line]) + else + result << "%#{indent}s: %s\n" % [line_counter, line] + end + end + end + end + end + + TemplateError = Template::Error +end diff --git a/actionview/lib/action_view/template/handlers.rb b/actionview/lib/action_view/template/handlers.rb new file mode 100644 index 0000000000..d9cddc0040 --- /dev/null +++ b/actionview/lib/action_view/template/handlers.rb @@ -0,0 +1,53 @@ +module ActionView #:nodoc: + # = Action View Template Handlers + class Template + module Handlers #:nodoc: + autoload :ERB, 'action_view/template/handlers/erb' + autoload :Builder, 'action_view/template/handlers/builder' + autoload :Raw, 'action_view/template/handlers/raw' + + def self.extended(base) + base.register_default_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 + + @@template_handlers = {} + @@default_template_handlers = nil + + def self.extensions + @@template_extensions ||= @@template_handlers.keys + end + + # 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 + # and should return the rendered template as a String. + def register_template_handler(*extensions, handler) + raise(ArgumentError, "Extension is required") if extensions.empty? + extensions.each do |extension| + @@template_handlers[extension.to_sym] = handler + end + @@template_extensions = nil + end + + def template_handler_extensions + @@template_handlers.keys.map {|key| key.to_s }.sort + end + + def registered_template_handler(extension) + extension && @@template_handlers[extension.to_sym] + end + + def register_default_template_handler(extension, klass) + register_template_handler(extension, klass) + @@default_template_handlers = klass + end + + def handler_for_extension(extension) + registered_template_handler(extension) || @@default_template_handlers + end + end + end +end diff --git a/actionview/lib/action_view/template/handlers/builder.rb b/actionview/lib/action_view/template/handlers/builder.rb new file mode 100644 index 0000000000..d90b0c6378 --- /dev/null +++ b/actionview/lib/action_view/template/handlers/builder.rb @@ -0,0 +1,26 @@ +module ActionView + module Template::Handlers + class Builder + # Default format used by Builder. + class_attribute :default_format + self.default_format = :xml + + def call(template) + require_engine + "xml = ::Builder::XmlMarkup.new(:indent => 2);" + + "self.output_buffer = xml.target!;" + + template.source + + ";xml.target!;" + end + + protected + + def require_engine + @required ||= begin + require "builder" + true + end + end + end + end +end diff --git a/actionview/lib/action_view/template/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb new file mode 100644 index 0000000000..c8a0059596 --- /dev/null +++ b/actionview/lib/action_view/template/handlers/erb.rb @@ -0,0 +1,145 @@ +require 'erubis' + +module ActionView + class Template + module Handlers + class Erubis < ::Erubis::Eruby + def add_preamble(src) + @newline_pending = 0 + src << "@output_buffer = output_buffer || ActionView::OutputBuffer.new;" + end + + def add_text(src, text) + return if text.empty? + + if text == "\n" + @newline_pending += 1 + else + src << "@output_buffer.safe_append='" + src << "\n" * @newline_pending if @newline_pending > 0 + src << escape_text(text) + src << "';" + + @newline_pending = 0 + end + end + + # Erubis toggles <%= and <%== behavior when escaping is enabled. + # We override to always treat <%== as escaped. + def add_expr(src, code, indicator) + case indicator + when '==' + add_expr_escaped(src, code) + else + super + end + end + + BLOCK_EXPR = /\s+(do|\{)(\s*\|[^|]*\|)?\s*\Z/ + + def add_expr_literal(src, code) + flush_newline_if_pending(src) + if code =~ BLOCK_EXPR + src << '@output_buffer.append= ' << code + else + src << '@output_buffer.append=(' << code << ');' + end + end + + def add_expr_escaped(src, code) + flush_newline_if_pending(src) + if code =~ BLOCK_EXPR + src << "@output_buffer.safe_append= " << code + else + src << "@output_buffer.safe_append=(" << code << ");" + end + end + + def add_stmt(src, code) + flush_newline_if_pending(src) + super + end + + def add_postamble(src) + flush_newline_if_pending(src) + src << '@output_buffer.to_s' + end + + def flush_newline_if_pending(src) + if @newline_pending > 0 + src << "@output_buffer.safe_append='#{"\n" * @newline_pending}';" + @newline_pending = 0 + end + end + end + + class ERB + # Specify trim mode for the ERB compiler. Defaults to '-'. + # See ERB documentation for suitable values. + class_attribute :erb_trim_mode + self.erb_trim_mode = '-' + + # Default implementation used. + class_attribute :erb_implementation + self.erb_implementation = Erubis + + # Do not escape templates of these mime types. + class_attribute :escape_whitelist + self.escape_whitelist = ["text/plain"] + + ENCODING_TAG = Regexp.new("\\A(<%#{ENCODING_FLAG}-?%>)[ \\t]*") + + def self.call(template) + new.call(template) + end + + def supports_streaming? + true + end + + def handles_encoding? + true + end + + def call(template) + # First, convert to BINARY, so in case the encoding is + # wrong, we can still find an encoding tag + # (<%# encoding %>) inside the String using a regular + # expression + template_source = template.source.dup.force_encoding(Encoding::ASCII_8BIT) + + erb = template_source.gsub(ENCODING_TAG, '') + encoding = $2 + + erb.force_encoding valid_encoding(template.source.dup, encoding) + + # Always make sure we return a String in the default_internal + erb.encode! + + self.class.erb_implementation.new( + erb, + :escape => (self.class.escape_whitelist.include? template.type), + :trim => (self.class.erb_trim_mode == "-") + ).src + end + + private + + def valid_encoding(string, encoding) + # If a magic encoding comment was found, tag the + # String with this encoding. This is for a case + # where the original String was assumed to be, + # for instance, UTF-8, but a magic comment + # proved otherwise + string.force_encoding(encoding) if encoding + + # If the String is valid, return the encoding we found + return string.encoding if string.valid_encoding? + + # Otherwise, raise an exception + raise WrongEncodingError.new(string, string.encoding) + end + end + end + end +end diff --git a/actionview/lib/action_view/template/handlers/raw.rb b/actionview/lib/action_view/template/handlers/raw.rb new file mode 100644 index 0000000000..0c0d1fffcb --- /dev/null +++ b/actionview/lib/action_view/template/handlers/raw.rb @@ -0,0 +1,11 @@ +module ActionView + module Template::Handlers + class Raw + def call(template) + escaped = template.source.gsub(':', '\:') + + '%q:' + escaped + ':;' + end + end + end +end diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb new file mode 100644 index 0000000000..3304605c1a --- /dev/null +++ b/actionview/lib/action_view/template/resolver.rb @@ -0,0 +1,326 @@ +require "pathname" +require "active_support/core_ext/class" +require "active_support/core_ext/class/attribute_accessors" +require "action_view/template" +require "thread" +require "thread_safe" + +module ActionView + # = Action View Resolver + class Resolver + # Keeps all information about view path and builds virtual path. + class Path + attr_reader :name, :prefix, :partial, :virtual + alias_method :partial?, :partial + + def self.build(name, prefix, partial) + virtual = "" + virtual << "#{prefix}/" unless prefix.empty? + virtual << (partial ? "_#{name}" : name) + new name, prefix, partial, virtual + end + + def initialize(name, prefix, partial, virtual) + @name = name + @prefix = prefix + @partial = partial + @virtual = virtual + end + + def to_str + @virtual + end + alias :to_s :to_str + end + + # Threadsafe template cache + class Cache #:nodoc: + class SmallCache < ThreadSafe::Cache + def initialize(options = {}) + super(options.merge(:initial_capacity => 2)) + end + end + + # preallocate all the default blocks for performance/memory consumption reasons + PARTIAL_BLOCK = lambda {|cache, partial| cache[partial] = SmallCache.new} + PREFIX_BLOCK = lambda {|cache, prefix| cache[prefix] = SmallCache.new(&PARTIAL_BLOCK)} + NAME_BLOCK = lambda {|cache, name| cache[name] = SmallCache.new(&PREFIX_BLOCK)} + KEY_BLOCK = lambda {|cache, key| cache[key] = SmallCache.new(&NAME_BLOCK)} + + # usually a majority of template look ups return nothing, use this canonical preallocated array to save memory + NO_TEMPLATES = [].freeze + + def initialize + @data = SmallCache.new(&KEY_BLOCK) + end + + # Cache the templates returned by the block + def cache(key, name, prefix, partial, locals) + if Resolver.caching? + @data[key][name][prefix][partial][locals] ||= canonical_no_templates(yield) + else + fresh_templates = yield + cached_templates = @data[key][name][prefix][partial][locals] + + if templates_have_changed?(cached_templates, fresh_templates) + @data[key][name][prefix][partial][locals] = canonical_no_templates(fresh_templates) + else + cached_templates || NO_TEMPLATES + end + end + end + + def clear + @data.clear + end + + private + + def canonical_no_templates(templates) + templates.empty? ? NO_TEMPLATES : templates + end + + def templates_have_changed?(cached_templates, fresh_templates) + # if either the old or new template list is empty, we don't need to (and can't) + # compare modification times, and instead just check whether the lists are different + if cached_templates.blank? || fresh_templates.blank? + return fresh_templates.blank? != cached_templates.blank? + end + + cached_templates_max_updated_at = cached_templates.map(&:updated_at).max + + # if a template has changed, it will be now be newer than all the cached templates + fresh_templates.any? { |t| t.updated_at > cached_templates_max_updated_at } + end + end + + cattr_accessor :caching + self.caching = true + + class << self + alias :caching? :caching + end + + def initialize + @cache = Cache.new + end + + def clear_cache + @cache.clear + end + + # Normalizes the arguments and passes it on to find_templates. + def find_all(name, prefix=nil, partial=false, details={}, key=nil, locals=[]) + cached(key, [name, prefix, partial], details, locals) do + find_templates(name, prefix, partial, details) + end + end + + private + + delegate :caching?, to: :class + + # This is what child classes implement. No defaults are needed + # because Resolver guarantees that the arguments are present and + # normalized. + def find_templates(name, prefix, partial, details) + raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details) method" + end + + # Helpers that builds a path. Useful for building virtual paths. + def build_path(name, prefix, partial) + Path.build(name, prefix, partial) + end + + # Handles templates caching. If a key is given and caching is on + # always check the cache before hitting the resolver. Otherwise, + # it always hits the resolver but if the key is present, check if the + # 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! + + if key + @cache.cache(key, name, prefix, partial, locals) do + decorate(yield, path_info, details, locals) + end + else + decorate(yield, path_info, details, locals) + end + end + + # Ensures all the resolver information is set in the template. + def decorate(templates, path_info, details, locals) #:nodoc: + cached = nil + templates.each do |t| + t.locals = locals + t.formats = details[:formats] || [:html] if t.formats.empty? + t.virtual_path ||= (cached ||= build_path(*path_info)) + end + end + end + + # An abstract class that implements a Resolver with path semantics. + class PathResolver < Resolver #:nodoc: + EXTENSIONS = [:locale, :formats, :handlers] + DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{.:handlers,}" + + def initialize(pattern=nil) + @pattern = pattern || DEFAULT_PATTERN + super() + end + + private + + def find_templates(name, prefix, partial, details) + path = Path.build(name, prefix, partial) + query(path, details, details[:formats]) + end + + def query(path, details, formats) + query = build_query(path, details) + + # deals with case-insensitive file systems. + sanitizer = Hash.new { |h,dir| h[dir] = Dir["#{dir}/*"] } + + template_paths = Dir[query].reject { |filename| + File.directory?(filename) || + !sanitizer[File.dirname(filename)].include?(filename) + } + + template_paths.map { |template| + handler, format = extract_handler_and_format(template, formats) + contents = File.binread template + + Template.new(contents, File.expand_path(template), handler, + :virtual_path => path.virtual, + :format => format, + :updated_at => mtime(template)) + } + end + + # Helper for building query glob string based on resolver's pattern. + def build_query(path, details) + query = @pattern.dup + + 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) + + details.each do |ext, variants| + query.gsub!(/\:#{ext}/, "{#{variants.compact.uniq.join(',')}}") + end + + File.expand_path(query, @path) + end + + def escape_entry(entry) + entry.gsub(/[*?{}\[\]]/, '\\\\\\&') + end + + # Returns the file mtime from the filesystem. + def mtime(p) + File.mtime(p) + end + + # Extract handler and formats from path. If a format cannot be a found neither + # from the path, or the handler, we should return the array of formats given + # to the resolver. + def extract_handler_and_format(path, default_formats) + pieces = File.basename(path).split(".") + 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 = pieces.last && Template::Types[pieces.last] + [handler, format] + end + end + + # A resolver that loads files from the filesystem. It allows setting your own + # resolving pattern. Such pattern can be a glob string supported by some variables. + # + # ==== Examples + # + # 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,}") + # + # 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,}") + # + # If you don't specify a pattern then the default will be used. + # + # In order to use any of the customized resolvers above in a Rails application, you just need + # to configure ActionController::Base.view_paths in an initializer, for example: + # + # ActionController::Base.view_paths = FileSystemResolver.new( + # Rails.root.join("app/views"), + # ":prefix{/:locale}/:action{.:formats,}{.:handlers,}" + # ) + # + # ==== Pattern format and variables + # + # Pattern has to be a valid glob string, and it allows you to use the + # following variables: + # + # * <tt>:prefix</tt> - usually the controller path + # * <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>:handlers</tt> - possible handlers (for example erb, haml, builder...) + # + class FileSystemResolver < PathResolver + def initialize(path, pattern=nil) + raise ArgumentError, "path already is a Resolver class" if path.is_a?(Resolver) + super(pattern) + @path = File.expand_path(path) + end + + def to_s + @path.to_s + end + alias :to_path :to_s + + def eql?(resolver) + self.class.equal?(resolver.class) && to_path == resolver.to_path + end + alias :== :eql? + end + + # An Optimized resolver for Rails' most common case. + class OptimizedFileSystemResolver < FileSystemResolver #:nodoc: + def build_query(path, details) + exts = EXTENSIONS.map { |ext| details[ext] } + query = escape_entry(File.join(@path, path)) + + query + exts.map { |ext| + "{#{ext.compact.uniq.map { |e| ".#{e}," }.join}}" + }.join + end + end + + # The same as FileSystemResolver but does not allow templates to store + # a virtual path since it is invalid for such resolvers. + class FallbackFileSystemResolver < FileSystemResolver #:nodoc: + def self.instances + [new(""), new("/")] + end + + def decorate(*) + super.each { |t| t.virtual_path = nil } + end + end +end diff --git a/actionview/lib/action_view/template/text.rb b/actionview/lib/action_view/template/text.rb new file mode 100644 index 0000000000..859c7bc3ce --- /dev/null +++ b/actionview/lib/action_view/template/text.rb @@ -0,0 +1,34 @@ +module ActionView #:nodoc: + # = Action View Text Template + class Template + class Text #:nodoc: + attr_accessor :type + + def initialize(string, type = nil) + @string = string.to_s + @type = Types[type] || type if type + @type ||= Types[:text] + end + + def identifier + 'text template' + end + + def inspect + 'text template' + end + + def to_str + @string + end + + def render(*args) + to_str + end + + def formats + [@type.to_sym] + end + end + end +end diff --git a/actionview/lib/action_view/template/types.rb b/actionview/lib/action_view/template/types.rb new file mode 100644 index 0000000000..db77cb5d19 --- /dev/null +++ b/actionview/lib/action_view/template/types.rb @@ -0,0 +1,57 @@ +require 'set' +require 'active_support/core_ext/class/attribute_accessors' + +module ActionView + class Template + class Types + class Type + cattr_accessor :types + self.types = Set.new + + def self.register(*t) + types.merge(t.map { |type| type.to_s }) + end + + register :html, :text, :js, :css, :xml, :json + + def self.[](type) + return type if type.is_a?(self) + + if type.is_a?(Symbol) || types.member?(type.to_s) + new(type) + end + end + + attr_reader :symbol + + def initialize(symbol) + @symbol = symbol.to_sym + end + + delegate :to_s, :to_sym, :to => :symbol + alias to_str to_s + + def ref + to_sym || to_s + end + + def ==(type) + return false if type.blank? + symbol.to_sym == type.to_sym + end + end + + cattr_accessor :type_klass + + def self.delegate_to(klass) + self.type_klass = klass + end + + delegate_to Type + + def self.[](type) + type_klass[type] + end + end + end +end diff --git a/actionview/lib/action_view/test_case.rb b/actionview/lib/action_view/test_case.rb new file mode 100644 index 0000000000..3145446114 --- /dev/null +++ b/actionview/lib/action_view/test_case.rb @@ -0,0 +1,272 @@ +require 'active_support/core_ext/module/remove_method' +require 'action_controller' +require 'action_controller/test_case' +require 'action_view' + +module ActionView + # = Action View Test Case + class TestCase < ActiveSupport::TestCase + class TestController < ActionController::Base + include ActionDispatch::TestProcess + + attr_accessor :request, :response, :params + + class << self + attr_writer :controller_path + end + + def controller_path=(path) + self.class.controller_path=(path) + end + + def initialize + super + self.class.controller_path = "" + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + + @request.env.delete('PATH_INFO') + @params = {} + end + end + + module Behavior + extend ActiveSupport::Concern + + include ActionDispatch::Assertions, ActionDispatch::TestProcess + include ActionController::TemplateAssertions + include ActionView::Context + + include ActionDispatch::Routing::PolymorphicRoutes + + include AbstractController::Helpers + include ActionView::Helpers + include ActionView::RecordIdentifier + include ActionView::RoutingUrlFor + + include ActiveSupport::Testing::ConstantLookup + + delegate :lookup_context, :to => :controller + attr_accessor :controller, :output_buffer, :rendered + + module ClassMethods + def tests(helper_class) + case helper_class + when String, Symbol + self.helper_class = "#{helper_class.to_s.underscore}_helper".camelize.safe_constantize + when Module + self.helper_class = helper_class + end + end + + def determine_default_helper_class(name) + determine_constant_from_test_name(name) do |constant| + Module === constant && !(Class === constant) + end + end + + def helper_method(*methods) + # Almost a duplicate from ActionController::Helpers + methods.flatten.each do |method| + _helpers.module_eval <<-end_eval + def #{method}(*args, &block) # def current_user(*args, &block) + _test_case.send(%(#{method}), *args, &block) # _test_case.send(%(current_user), *args, &block) + end # end + end_eval + end + end + + attr_writer :helper_class + + def helper_class + @helper_class ||= determine_default_helper_class(name) + end + + def new(*) + include_helper_modules! + super + end + + private + + def include_helper_modules! + helper(helper_class) if helper_class + include _helpers + end + + end + + def setup_with_controller + @controller = ActionView::TestCase::TestController.new + @request = @controller.request + @output_buffer = ActiveSupport::SafeBuffer.new + @rendered = '' + + make_test_case_available_to_view! + say_no_to_protect_against_forgery! + end + + def config + @controller.config if @controller.respond_to?(:config) + end + + def render(options = {}, local_assigns = {}, &block) + view.assign(view_assigns) + @rendered << output = view.render(options, local_assigns, &block) + output + end + + def rendered_views + @_rendered_views ||= RenderedViewsCollection.new + end + + class RenderedViewsCollection + def initialize + @rendered_views ||= Hash.new { |hash, key| hash[key] = [] } + end + + def add(view, locals) + @rendered_views[view] ||= [] + @rendered_views[view] << locals + end + + def locals_for(view) + @rendered_views[view] + end + + def rendered_views + @rendered_views.keys + end + + def view_rendered?(view, expected_locals) + locals_for(view).any? do |actual_locals| + expected_locals.all? {|key, value| value == actual_locals[key] } + end + end + end + + included do + setup :setup_with_controller + end + + 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 + end + + def say_no_to_protect_against_forgery! + _helpers.module_eval do + remove_possible_method :protect_against_forgery? + def protect_against_forgery? + false + end + end + end + + def make_test_case_available_to_view! + test_case_instance = self + _helpers.module_eval do + unless private_method_defined?(:_test_case) + define_method(:_test_case) { test_case_instance } + private :_test_case + end + end + end + + module Locals + attr_accessor :rendered_views + + def render(options = {}, local_assigns = {}) + case options + when Hash + if block_given? + rendered_views.add options[:layout], options[:locals] + elsif options.key?(:partial) + rendered_views.add options[:partial], options[:locals] + end + else + rendered_views.add options, local_assigns + end + + super + end + end + + # The instance of ActionView::Base that is used by +render+. + def view + @view ||= begin + view = @controller.view_context + view.singleton_class.send :include, _helpers + view.extend(Locals) + view.rendered_views = self.rendered_views + view.output_buffer = self.output_buffer + view + end + end + + alias_method :_view, :view + + INTERNAL_IVARS = [ + :@NAME, + :@failures, + :@assertions, + :@__io__, + :@_assertion_wrapped, + :@_assertions, + :@_result, + :@_routes, + :@controller, + :@_layouts, + :@_files, + :@_rendered_views, + :@method_name, + :@output_buffer, + :@_partials, + :@passed, + :@rendered, + :@request, + :@routes, + :@tagged_logger, + :@_templates, + :@options, + :@test_passed, + :@view, + :@view_context_class + ] + + def _user_defined_ivars + instance_variables - INTERNAL_IVARS + end + + # Returns a Hash of instance variables and their values, as defined by + # the user in the test case, which are then assigned to the view being + # rendered. This is generally intended for internal use and extension + # frameworks. + def view_assigns + Hash[_user_defined_ivars.map do |ivar| + [ivar[1..-1].to_sym, instance_variable_get(ivar)] + end] + end + + def _routes + @controller._routes if @controller.respond_to?(:_routes) + end + + def method_missing(selector, *args) + if @controller.respond_to?(:_routes) && + ( @controller._routes.named_routes.helpers.include?(selector) || + @controller._routes.mounted_helpers.method_defined?(selector) ) + @controller.__send__(selector, *args) + else + super + end + end + end + + include Behavior + end +end diff --git a/actionview/lib/action_view/testing/resolvers.rb b/actionview/lib/action_view/testing/resolvers.rb new file mode 100644 index 0000000000..7afa2fa613 --- /dev/null +++ b/actionview/lib/action_view/testing/resolvers.rb @@ -0,0 +1,50 @@ +require 'action_view/template/resolver' + +module ActionView #:nodoc: + # Use FixtureResolver in your tests to simulate the presence of files on the + # file system. This is used internally by Rails' own test suite, and is + # useful for testing extensions that have no way of knowing what the file + # system will look like at runtime. + class FixtureResolver < PathResolver + attr_reader :hash + + def initialize(hash = {}, pattern=nil) + super(pattern) + @hash = hash + end + + def to_s + @hash.keys.join(', ') + end + + private + + def query(path, exts, formats) + query = "" + EXTENSIONS.each do |ext| + query << '(' << exts[ext].map {|e| e && Regexp.escape(".#{e}") }.join('|') << '|)' + end + query = /^(#{Regexp.escape(path)})#{query}$/ + + templates = [] + @hash.each do |_path, array| + source, updated_at = array + next unless _path =~ query + handler, format = extract_handler_and_format(_path, formats) + templates << Template.new(source, _path, handler, + :virtual_path => path.virtual, :format => format, :updated_at => updated_at) + end + + templates.sort_by {|t| -t.identifier.match(/^#{query}$/).captures.reject(&:blank?).size } + end + end + + class NullResolver < PathResolver + def query(path, exts, formats) + handler, format = extract_handler_and_format(path, formats) + [ActionView::Template.new("Template generated by Null Resolver", path, handler, :virtual_path => path, :format => format)] + end + end + +end + diff --git a/actionview/lib/action_view/vendor/html-scanner.rb b/actionview/lib/action_view/vendor/html-scanner.rb new file mode 100644 index 0000000000..775b827529 --- /dev/null +++ b/actionview/lib/action_view/vendor/html-scanner.rb @@ -0,0 +1,20 @@ +$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 new file mode 100644 index 0000000000..386820300a --- /dev/null +++ b/actionview/lib/action_view/vendor/html-scanner/html/document.rb @@ -0,0 +1,68 @@ +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 new file mode 100644 index 0000000000..7e7cd4f7b6 --- /dev/null +++ b/actionview/lib/action_view/vendor/html-scanner/html/node.rb @@ -0,0 +1,532 @@ +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 + + # Return a textual representation of the node. + def to_s + @children.join() + end + + # Return 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 new file mode 100644 index 0000000000..30b6b8b141 --- /dev/null +++ b/actionview/lib/action_view/vendor/html-scanner/html/sanitizer.rb @@ -0,0 +1,188 @@ +require 'set' +require 'cgi' +require 'active_support/core_ext/class/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 = /:|(�*58)|(p)|(�*3a)|(%|%)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 <font> + 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(/</, "<") + 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 =~ /(^[^\/:]*):|(�*58)|(p)|(�*3a)|(%|%)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 new file mode 100644 index 0000000000..7f8609c408 --- /dev/null +++ b/actionview/lib/action_view/vendor/html-scanner/html/selector.rb @@ -0,0 +1,830 @@ +#-- +# 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 + + + # Return 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 new file mode 100644 index 0000000000..8ac8d34430 --- /dev/null +++ b/actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb @@ -0,0 +1,107 @@ +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 + + # Return 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 new file mode 100644 index 0000000000..6d645c3e14 --- /dev/null +++ b/actionview/lib/action_view/vendor/html-scanner/html/version.rb @@ -0,0 +1,11 @@ +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/version.rb b/actionview/lib/action_view/version.rb new file mode 100644 index 0000000000..094dd474df --- /dev/null +++ b/actionview/lib/action_view/version.rb @@ -0,0 +1,11 @@ +module ActionView + # Returns the version of the currently loaded ActionView as a Gem::Version + def self.version + Gem::Version.new "4.1.0.beta" + end + + module VERSION #:nodoc: + MAJOR, MINOR, TINY, PRE = ActionView.version.segments + STRING = ActionView.version.to_s + end +end diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb new file mode 100644 index 0000000000..8213997f4e --- /dev/null +++ b/actionview/test/abstract_unit.rb @@ -0,0 +1,382 @@ +require File.expand_path('../../../load_paths', __FILE__) + +$:.unshift(File.dirname(__FILE__) + '/lib') +$:.unshift(File.dirname(__FILE__) + '/fixtures/helpers') +$:.unshift(File.dirname(__FILE__) + '/fixtures/alternate_helpers') + +ENV['TMPDIR'] = File.join(File.dirname(__FILE__), 'tmp') + +require 'active_support/core_ext/kernel/reporting' + +# These are the normal settings that will be set up by Railties +# TODO: Have these tests support other combinations of these values +silence_warnings do + Encoding.default_internal = "UTF-8" + Encoding.default_external = "UTF-8" +end + +require 'active_support/testing/autorun' +require 'abstract_controller' +require 'action_controller' +require 'action_view' +require 'action_view/testing/resolvers' +require 'action_dispatch' +require 'active_support/dependencies' +require 'active_model' +require 'active_record' +require 'action_controller/caching' + +require 'pp' # require 'pp' early to prevent hidden_methods from not picking up the pretty-print methods until too late + +module Rails + class << self + def env + @_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "test") + end + end +end + +ActiveSupport::Dependencies.hook! + +Thread.abort_on_exception = true + +# Show backtraces for deprecated behavior for quicker cleanup. +ActiveSupport::Deprecation.debug = true + +# 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 + +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 + @view ||= begin + path = ActionView::FileSystemResolver.new(FIXTURE_LOAD_PATH) + view_paths = ActionView::PathSet.new([path]) + ActionView::Base.new(view_paths) + end + end + + def render_erb(string) + @virtual_path = nil + + template = ActionView::Template.new( + string.strip, + "test template", + ActionView::Template::Handlers::ERB, + {}) + + template.render(self, {}).strip + end +end + +SharedTestRoutes = ActionDispatch::Routing::RouteSet.new + +module ActionDispatch + module SharedRoutes + def before_setup + @routes = SharedTestRoutes + super + end + end + + # Hold off drawing routes until all the possible controller classes + # have been loaded. + module DrawOnce + class << self + attr_accessor :drew + end + self.drew = false + + def before_setup + super + return if DrawOnce.drew + + SharedTestRoutes.draw do + get ':controller(/:action)' + end + + ActionDispatch::IntegrationTest.app.routes.draw do + get ':controller(/:action)' + end + + DrawOnce.drew = true + end + end +end + +module ActiveSupport + class TestCase + include ActionDispatch::DrawOnce + end +end + +class RoutedRackApp + attr_reader :routes + + def initialize(routes, &blk) + @routes = routes + @stack = ActionDispatch::MiddlewareStack.new(&blk).build(@routes) + end + + def call(env) + @stack.call(env) + end +end + +class BasicController + attr_accessor :request + + def config + @config ||= ActiveSupport::InheritableOptions.new(ActionController::Base.config).tap do |config| + # VIEW TODO: View tests should not require a controller + public_dir = File.expand_path("../fixtures/public", __FILE__) + config.assets_dir = public_dir + config.javascripts_dir = "#{public_dir}/javascripts" + config.stylesheets_dir = "#{public_dir}/stylesheets" + config.assets = ActiveSupport::InheritableOptions.new({ :prefix => "assets" }) + config + end + end +end + +class ActionDispatch::IntegrationTest < ActiveSupport::TestCase + include ActionDispatch::SharedRoutes + + 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" + yield(middleware) if block_given? + end + end + + self.app = build_app + + # Stub Rails dispatcher so it does not get controller references and + # simply return the controller#action as Rack::Body. + class StubDispatcher < ::ActionDispatch::Routing::RouteSet::Dispatcher + protected + def controller_reference(controller_param) + controller_param + end + + def dispatch(controller, action, env) + [200, {'Content-Type' => 'text/html'}, ["#{controller}##{action}"]] + end + end + + def self.stub_controllers + old_dispatcher = ActionDispatch::Routing::RouteSet::Dispatcher + ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher } + ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, StubDispatcher } + yield ActionDispatch::Routing::RouteSet.new + ensure + ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher } + ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, old_dispatcher } + end + + def with_routing(&block) + temporary_routes = ActionDispatch::Routing::RouteSet.new + old_app, self.class.app = self.class.app, self.class.build_app(temporary_routes) + old_routes = SharedTestRoutes + silence_warnings { Object.const_set(:SharedTestRoutes, temporary_routes) } + + yield temporary_routes + ensure + self.class.app = old_app + silence_warnings { Object.const_set(:SharedTestRoutes, old_routes) } + end + + def with_autoload_path(path) + path = File.join(File.dirname(__FILE__), "fixtures", path) + if ActiveSupport::Dependencies.autoload_paths.include?(path) + yield + else + begin + ActiveSupport::Dependencies.autoload_paths << path + yield + ensure + ActiveSupport::Dependencies.autoload_paths.reject! {|p| p == path} + ActiveSupport::Dependencies.clear + end + end + 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 + +module ActionController + class Base + include ActionController::Testing + # This stub emulates the Railtie including the URL helpers from a Rails application + include SharedTestRoutes.url_helpers + include SharedTestRoutes.mounted_helpers + + self.view_paths = FIXTURE_LOAD_PATH + + def self.test_routes(&block) + routes = ActionDispatch::Routing::RouteSet.new + routes.draw(&block) + include routes.url_helpers + end + end + + class TestCase + include ActionDispatch::TestProcess + include ActionDispatch::SharedRoutes + end +end + +class ::ApplicationController < ActionController::Base +end + +module ActionView + class TestCase + # Must repeat the setup because AV::TestCase is a duplication + # of AC::TestCase + include ActionDispatch::SharedRoutes + end +end + +class Workshop + extend ActiveModel::Naming + include ActiveModel::Conversion + attr_accessor :id + + def initialize(id) + @id = id + end + + def persisted? + id.present? + end + + def to_s + id.to_s + end +end + +module ActionDispatch + class DebugExceptions + private + remove_method :stderr_logger + # Silence logger + def stderr_logger + nil + end + end +end + +module ActionDispatch + module RoutingVerbs + def get(uri_or_host, path = nil) + host = uri_or_host.host unless path + path ||= uri_or_host.path + + params = {'PATH_INFO' => path, + 'REQUEST_METHOD' => 'GET', + 'HTTP_HOST' => host} + + routes.call(params)[2].join + end + end +end + +module RoutingTestHelpers + def url_for(set, options, recall = nil) + set.send(:url_for, options.merge(:only_path => true, :_recall => recall)) + end +end + +class ResourcesController < ActionController::Base + def index() render :nothing => true end + alias_method :show, :index +end + +class ThreadsController < ResourcesController; end +class MessagesController < ResourcesController; end +class CommentsController < ResourcesController; end +class ReviewsController < ResourcesController; end +class AuthorsController < ResourcesController; end +class LogosController < ResourcesController; end + +class AccountsController < ResourcesController; end +class AdminController < ResourcesController; end +class ProductsController < ResourcesController; end +class ImagesController < ResourcesController; end +class PreferencesController < ResourcesController; end + +module Backoffice + class ProductsController < ResourcesController; end + class TagsController < ResourcesController; end + class ManufacturersController < ResourcesController; end + class ImagesController < ResourcesController; end + + module Admin + class ProductsController < ResourcesController; end + class ImagesController < ResourcesController; end + end +end diff --git a/actionview/test/active_record_unit.rb b/actionview/test/active_record_unit.rb new file mode 100644 index 0000000000..95fbb112c0 --- /dev/null +++ b/actionview/test/active_record_unit.rb @@ -0,0 +1,91 @@ +require 'abstract_unit' + +# Define the essentials +class ActiveRecordTestConnector + cattr_accessor :able_to_connect + cattr_accessor :connected + + # Set our defaults + self.connected = false + self.able_to_connect = true +end + +# Try to grab AR +unless defined?(ActiveRecord) && defined?(FixtureSet) + begin + PATH_TO_AR = "#{File.dirname(__FILE__)}/../../activerecord/lib" + raise LoadError, "#{PATH_TO_AR} doesn't exist" unless File.directory?(PATH_TO_AR) + $LOAD_PATH.unshift PATH_TO_AR + require 'active_record' + rescue LoadError => e + $stderr.print "Failed to load Active Record. Skipping Active Record assertion tests: #{e}" + ActiveRecordTestConnector.able_to_connect = false + end +end +$stderr.flush + + +# Define the rest of the connector +class ActiveRecordTestConnector + class << self + def setup + unless self.connected || !self.able_to_connect + setup_connection + load_schema + require_fixture_models + self.connected = true + end + rescue Exception => e # errors from ActiveRecord setup + $stderr.puts "\nSkipping ActiveRecord assertion tests: #{e}" + #$stderr.puts " #{e.backtrace.join("\n ")}\n" + self.able_to_connect = false + end + + private + def setup_connection + if Object.const_defined?(:ActiveRecord) + defaults = { :database => ':memory:' } + adapter = defined?(JRUBY_VERSION) ? 'jdbcsqlite3' : 'sqlite3' + options = defaults.merge :adapter => adapter, :timeout => 500 + ActiveRecord::Base.establish_connection(options) + ActiveRecord::Base.configurations = { 'sqlite3_ar_integration' => options } + ActiveRecord::Base.connection + + Object.send(:const_set, :QUOTED_TYPE, ActiveRecord::Base.connection.quote_column_name('type')) unless Object.const_defined?(:QUOTED_TYPE) + else + raise "Can't setup connection since ActiveRecord isn't loaded." + end + end + + # Load actionpack sqlite tables + def load_schema + File.read(File.dirname(__FILE__) + "/fixtures/db_definitions/sqlite.sql").split(';').each do |sql| + ActiveRecord::Base.connection.execute(sql) unless sql.blank? + end + end + + def require_fixture_models + Dir.glob(File.dirname(__FILE__) + "/fixtures/*.rb").each {|f| require f} + end + end +end + +class ActiveRecordTestCase < ActionController::TestCase + include ActiveRecord::TestFixtures + + # Set our fixture path + if ActiveRecordTestConnector.able_to_connect + self.fixture_path = [FIXTURE_LOAD_PATH] + self.use_transactional_fixtures = false + end + + def self.fixtures(*args) + super if ActiveRecordTestConnector.connected + end + + def run(*args) + super if ActiveRecordTestConnector.connected + end +end + +ActiveRecordTestConnector.setup diff --git a/actionview/test/activerecord/controller_runtime_test.rb b/actionview/test/activerecord/controller_runtime_test.rb new file mode 100644 index 0000000000..368bec1c70 --- /dev/null +++ b/actionview/test/activerecord/controller_runtime_test.rb @@ -0,0 +1,95 @@ +require 'active_record_unit' +require 'active_record/railties/controller_runtime' +require 'fixtures/project' +require 'active_support/log_subscriber/test_helper' +require 'action_controller/log_subscriber' + +ActionController::Base.send :include, ActiveRecord::Railties::ControllerRuntime + +class ControllerRuntimeLogSubscriberTest < ActionController::TestCase + class LogSubscriberController < ActionController::Base + respond_to :html + + def show + render :inline => "<%= Project.all %>" + end + + def zero + render :inline => "Zero DB runtime" + end + + def create + ActiveRecord::LogSubscriber.runtime += 100 + project = Project.last + respond_with(project, location: url_for(action: :show)) + end + + def redirect + Project.all + redirect_to :action => 'show' + end + + def db_after_render + render :inline => "Hello world" + Project.all + ActiveRecord::LogSubscriber.runtime += 100 + end + end + + include ActiveSupport::LogSubscriber::TestHelper + tests LogSubscriberController + + def setup + super + @old_logger = ActionController::Base.logger + ActionController::LogSubscriber.attach_to :action_controller + end + + def teardown + super + ActiveSupport::LogSubscriber.log_subscribers.clear + ActionController::Base.logger = @old_logger + end + + def set_logger(logger) + ActionController::Base.logger = logger + end + + def test_log_with_active_record + get :show + wait + + assert_equal 2, @logger.logged(:info).size + assert_match(/\(Views: [\d.]+ms \| ActiveRecord: [\d.]+ms\)/, @logger.logged(:info)[1]) + end + + def test_runtime_reset_before_requests + ActiveRecord::LogSubscriber.runtime += 12345 + get :zero + wait + + assert_equal 2, @logger.logged(:info).size + assert_match(/\(Views: [\d.]+ms \| ActiveRecord: 0.0ms\)/, @logger.logged(:info)[1]) + end + + def test_log_with_active_record_when_post + post :create + wait + assert_match(/ActiveRecord: ([1-9][\d.]+)ms\)/, @logger.logged(:info)[2]) + end + + def test_log_with_active_record_when_redirecting + get :redirect + wait + assert_equal 3, @logger.logged(:info).size + assert_match(/\(ActiveRecord: [\d.]+ms\)/, @logger.logged(:info)[2]) + end + + def test_include_time_query_time_after_rendering + get :db_after_render + wait + + assert_equal 2, @logger.logged(:info).size + assert_match(/\(Views: [\d.]+ms \| ActiveRecord: ([1-9][\d.]+)ms\)/, @logger.logged(:info)[1]) + end +end diff --git a/actionview/test/activerecord/form_helper_activerecord_test.rb b/actionview/test/activerecord/form_helper_activerecord_test.rb new file mode 100644 index 0000000000..2e302c65a7 --- /dev/null +++ b/actionview/test/activerecord/form_helper_activerecord_test.rb @@ -0,0 +1,91 @@ +require 'active_record_unit' +require 'fixtures/project' +require 'fixtures/developer' + +class FormHelperActiveRecordTest < ActionView::TestCase + tests ActionView::Helpers::FormHelper + + def form_for(*) + @output_buffer = super + end + + def setup + @developer = Developer.new + @developer.id = 123 + @developer.name = "developer #123" + + @project = Project.new + @project.id = 321 + @project.name = "project #321" + @project.save + + @developer.projects << @project + @developer.save + end + + def teardown + Project.delete(321) + Developer.delete(123) + end + + Routes = ActionDispatch::Routing::RouteSet.new + Routes.draw do + resources :developers do + resources :projects + 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 + form_for(@developer) do |f| + concat f.fields_for(:projects, @developer.projects.first, :child_index => 'abc') { |cf| + concat cf.text_field(:name) + } + end + + expected = whole_form('/developers/123', 'edit_developer_123', 'edit_developer', :method => 'patch') do + '<input id="developer_projects_attributes_abc_name" name="developer[projects_attributes][abc][name]" type="text" value="project #321" />' + + '<input id="developer_projects_attributes_abc_id" name="developer[projects_attributes][abc][id]" type="hidden" value="321" />' + end + + assert_dom_equal expected, output_buffer + end + + protected + + def hidden_fields(method = nil) + txt = %{<div style="margin:0;padding:0;display:inline">} + txt << %{<input name="utf8" type="hidden" value="✓" />} + if method && !%w(get post).include?(method.to_s) + txt << %{<input name="_method" type="hidden" value="#{method}" />} + end + txt << %{</div>} + end + + def form_text(action = "/", id = nil, html_class = nil, remote = nil, multipart = nil, method = nil) + txt = %{<form accept-charset="UTF-8" action="#{action}"} + txt << %{ enctype="multipart/form-data"} if multipart + txt << %{ data-remote="true"} if remote + txt << %{ class="#{html_class}"} if html_class + txt << %{ id="#{id}"} if id + method = method.to_s == "get" ? "get" : "post" + txt << %{ method="#{method}">} + end + + def whole_form(action = "/", id = nil, html_class = nil, options = nil) + contents = block_given? ? yield : "" + + if options.is_a?(Hash) + method, remote, multipart = options.values_at(:method, :remote, :multipart) + else + method = options + end + + form_text(action, id, html_class, remote, multipart, method) + hidden_fields(method) + contents + "</form>" + end +end
\ No newline at end of file diff --git a/actionview/test/activerecord/polymorphic_routes_test.rb b/actionview/test/activerecord/polymorphic_routes_test.rb new file mode 100644 index 0000000000..afb714484b --- /dev/null +++ b/actionview/test/activerecord/polymorphic_routes_test.rb @@ -0,0 +1,546 @@ +require 'active_record_unit' +require 'fixtures/project' + +class Task < ActiveRecord::Base + self.table_name = 'projects' +end + +class Step < ActiveRecord::Base + self.table_name = 'projects' +end + +class Bid < ActiveRecord::Base + self.table_name = 'projects' +end + +class Tax < ActiveRecord::Base + self.table_name = 'projects' +end + +class Fax < ActiveRecord::Base + self.table_name = 'projects' +end + +class Series < ActiveRecord::Base + self.table_name = 'projects' +end + +class ModelDelegator < ActiveRecord::Base + self.table_name = 'projects' + + def to_model + ModelDelegate.new + end +end + +class ModelDelegate + def self.model_name + ActiveModel::Name.new(self) + end + + def to_param + 'overridden' + end +end + +module Blog + class Post < ActiveRecord::Base + self.table_name = 'projects' + end + + class Blog < ActiveRecord::Base + self.table_name = 'projects' + end + + def self.use_relative_model_naming? + true + end +end + +class PolymorphicRoutesTest < ActionController::TestCase + include SharedTestRoutes.url_helpers + self.default_url_options[:host] = 'example.com' + + def setup + @project = Project.new + @task = Task.new + @step = Step.new + @bid = Bid.new + @tax = Tax.new + @fax = Fax.new + @delegator = ModelDelegator.new + @series = Series.new + @blog_post = Blog::Post.new + @blog_blog = Blog::Blog.new + end + + def test_passing_routes_proxy + with_namespaced_routes(:blog) do + proxy = ActionDispatch::Routing::RoutesProxy.new(_routes, self) + @blog_post.save + assert_equal "http://example.com/posts/#{@blog_post.id}", polymorphic_url([proxy, @blog_post]) + end + end + + def test_namespaced_model + with_namespaced_routes(:blog) do + @blog_post.save + assert_equal "http://example.com/posts/#{@blog_post.id}", polymorphic_url(@blog_post) + end + end + + def test_namespaced_model_with_name_the_same_as_namespace + with_namespaced_routes(:blog) do + @blog_blog.save + assert_equal "http://example.com/blogs/#{@blog_blog.id}", polymorphic_url(@blog_blog) + end + end + + def test_namespaced_model_with_nested_resources + with_namespaced_routes(:blog) do + @blog_post.save + @blog_blog.save + assert_equal "http://example.com/blogs/#{@blog_blog.id}/posts/#{@blog_post.id}", polymorphic_url([@blog_blog, @blog_post]) + end + end + + def test_with_nil + with_test_routes do + assert_raise ArgumentError, "Nil location provided. Can't build URI." do + polymorphic_url(nil) + end + end + end + + def test_with_record + with_test_routes do + @project.save + assert_equal "http://example.com/projects/#{@project.id}", polymorphic_url(@project) + end + end + + def test_with_class + with_test_routes do + assert_equal "http://example.com/projects", polymorphic_url(@project.class) + end + end + + def test_with_new_record + with_test_routes do + assert_equal "http://example.com/projects", polymorphic_url(@project) + end + end + + def test_with_destroyed_record + with_test_routes do + @project.destroy + assert_equal "http://example.com/projects", polymorphic_url(@project) + end + end + + def test_with_record_and_action + with_test_routes do + assert_equal "http://example.com/projects/new", polymorphic_url(@project, :action => 'new') + end + end + + def test_url_helper_prefixed_with_new + with_test_routes do + assert_equal "http://example.com/projects/new", new_polymorphic_url(@project) + end + end + + def test_url_helper_prefixed_with_edit + with_test_routes do + @project.save + assert_equal "http://example.com/projects/#{@project.id}/edit", edit_polymorphic_url(@project) + end + end + + def test_url_helper_prefixed_with_edit_with_url_options + with_test_routes do + @project.save + assert_equal "http://example.com/projects/#{@project.id}/edit?param1=10", edit_polymorphic_url(@project, :param1 => '10') + end + end + + def test_url_helper_with_url_options + with_test_routes do + @project.save + assert_equal "http://example.com/projects/#{@project.id}?param1=10", polymorphic_url(@project, :param1 => '10') + end + end + + def test_format_option + with_test_routes do + @project.save + assert_equal "http://example.com/projects/#{@project.id}.pdf", polymorphic_url(@project, :format => :pdf) + end + end + + def test_format_option_with_url_options + with_test_routes do + @project.save + assert_equal "http://example.com/projects/#{@project.id}.pdf?param1=10", polymorphic_url(@project, :format => :pdf, :param1 => '10') + end + end + + def test_id_and_format_option + with_test_routes do + @project.save + assert_equal "http://example.com/projects/#{@project.id}.pdf", polymorphic_url(:id => @project, :format => :pdf) + end + end + + def test_with_nested + with_test_routes do + @project.save + @task.save + assert_equal "http://example.com/projects/#{@project.id}/tasks/#{@task.id}", polymorphic_url([@project, @task]) + end + end + + def test_with_nested_unsaved + with_test_routes do + @project.save + assert_equal "http://example.com/projects/#{@project.id}/tasks", polymorphic_url([@project, @task]) + end + end + + def test_with_nested_destroyed + with_test_routes do + @project.save + @task.destroy + assert_equal "http://example.com/projects/#{@project.id}/tasks", polymorphic_url([@project, @task]) + end + end + + def test_with_nested_class + with_test_routes do + @project.save + assert_equal "http://example.com/projects/#{@project.id}/tasks", polymorphic_url([@project, @task.class]) + end + end + + def test_class_with_array_and_namespace + with_admin_test_routes do + assert_equal "http://example.com/admin/projects", polymorphic_url([:admin, @project.class]) + end + end + + def test_new_with_array_and_namespace + with_admin_test_routes do + assert_equal "http://example.com/admin/projects/new", polymorphic_url([:admin, @project], :action => 'new') + end + end + + def test_unsaved_with_array_and_namespace + with_admin_test_routes do + assert_equal "http://example.com/admin/projects", polymorphic_url([:admin, @project]) + end + end + + def test_nested_unsaved_with_array_and_namespace + with_admin_test_routes do + @project.save + assert_equal "http://example.com/admin/projects/#{@project.id}/tasks", polymorphic_url([:admin, @project, @task]) + end + end + + def test_nested_with_array_and_namespace + with_admin_test_routes do + @project.save + @task.save + assert_equal "http://example.com/admin/projects/#{@project.id}/tasks/#{@task.id}", polymorphic_url([:admin, @project, @task]) + end + end + + def test_ordering_of_nesting_and_namespace + with_admin_and_site_test_routes do + @project.save + @task.save + @step.save + assert_equal "http://example.com/admin/projects/#{@project.id}/site/tasks/#{@task.id}/steps/#{@step.id}", polymorphic_url([:admin, @project, :site, @task, @step]) + end + end + + def test_nesting_with_array_ending_in_singleton_resource + with_test_routes do + @project.save + assert_equal "http://example.com/projects/#{@project.id}/bid", polymorphic_url([@project, :bid]) + end + end + + def test_nesting_with_array_containing_singleton_resource + with_test_routes do + @project.save + @task.save + assert_equal "http://example.com/projects/#{@project.id}/bid/tasks/#{@task.id}", polymorphic_url([@project, :bid, @task]) + end + end + + def test_nesting_with_array_containing_singleton_resource_and_format + with_test_routes do + @project.save + @task.save + assert_equal "http://example.com/projects/#{@project.id}/bid/tasks/#{@task.id}.pdf", polymorphic_url([@project, :bid, @task], :format => :pdf) + end + end + + def test_nesting_with_array_containing_namespace_and_singleton_resource + with_admin_test_routes do + @project.save + @task.save + assert_equal "http://example.com/admin/projects/#{@project.id}/bid/tasks/#{@task.id}", polymorphic_url([:admin, @project, :bid, @task]) + end + end + + def test_nesting_with_array_containing_nil + with_test_routes do + @project.save + assert_equal "http://example.com/projects/#{@project.id}/bid", polymorphic_url([@project, nil, :bid]) + end + end + + def test_with_array_containing_single_object + with_test_routes do + @project.save + assert_equal "http://example.com/projects/#{@project.id}", polymorphic_url([nil, @project]) + end + end + + def test_with_array_containing_single_name + with_test_routes do + @project.save + assert_equal "http://example.com/projects", polymorphic_url([:projects]) + end + end + + def test_with_array_containing_symbols + with_test_routes do + assert_equal "http://example.com/series/new", polymorphic_url([:new, :series]) + end + end + + def test_with_hash + with_test_routes do + @project.save + assert_equal "http://example.com/projects/#{@project.id}", polymorphic_url(:id => @project) + end + end + + def test_polymorphic_path_accepts_options + with_test_routes do + assert_equal "/projects/new", polymorphic_path(@project, :action => 'new') + end + end + + def test_polymorphic_path_does_not_modify_arguments + with_admin_test_routes do + @project.save + @task.save + + options = {} + object_array = [:admin, @project, @task] + original_args = [object_array.dup, options.dup] + + assert_no_difference('object_array.size') { polymorphic_path(object_array, options) } + assert_equal original_args, [object_array, options] + end + end + + # Tests for names where .plural.singular doesn't round-trip + def test_with_irregular_plural_record + with_test_routes do + @tax.save + assert_equal "http://example.com/taxes/#{@tax.id}", polymorphic_url(@tax) + end + end + + def test_with_irregular_plural_class + with_test_routes do + assert_equal "http://example.com/taxes", polymorphic_url(@tax.class) + end + end + + def test_with_irregular_plural_new_record + with_test_routes do + assert_equal "http://example.com/taxes", polymorphic_url(@tax) + end + end + + def test_with_irregular_plural_destroyed_record + with_test_routes do + @tax.destroy + assert_equal "http://example.com/taxes", polymorphic_url(@tax) + end + end + + def test_with_irregular_plural_record_and_action + with_test_routes do + assert_equal "http://example.com/taxes/new", polymorphic_url(@tax, :action => 'new') + end + end + + def test_irregular_plural_url_helper_prefixed_with_new + with_test_routes do + assert_equal "http://example.com/taxes/new", new_polymorphic_url(@tax) + end + end + + def test_irregular_plural_url_helper_prefixed_with_edit + with_test_routes do + @tax.save + assert_equal "http://example.com/taxes/#{@tax.id}/edit", edit_polymorphic_url(@tax) + end + end + + def test_with_nested_irregular_plurals + with_test_routes do + @tax.save + @fax.save + assert_equal "http://example.com/taxes/#{@tax.id}/faxes/#{@fax.id}", polymorphic_url([@tax, @fax]) + end + end + + def test_with_nested_unsaved_irregular_plurals + with_test_routes do + @tax.save + assert_equal "http://example.com/taxes/#{@tax.id}/faxes", polymorphic_url([@tax, @fax]) + end + end + + def test_new_with_irregular_plural_array_and_namespace + with_admin_test_routes do + assert_equal "http://example.com/admin/taxes/new", polymorphic_url([:admin, @tax], :action => 'new') + end + end + + def test_class_with_irregular_plural_array_and_namespace + with_admin_test_routes do + assert_equal "http://example.com/admin/taxes", polymorphic_url([:admin, @tax.class]) + end + end + + def test_unsaved_with_irregular_plural_array_and_namespace + with_admin_test_routes do + assert_equal "http://example.com/admin/taxes", polymorphic_url([:admin, @tax]) + end + end + + def test_nesting_with_irregular_plurals_and_array_ending_in_singleton_resource + with_test_routes do + @tax.save + assert_equal "http://example.com/taxes/#{@tax.id}/bid", polymorphic_url([@tax, :bid]) + end + end + + def test_with_array_containing_single_irregular_plural_object + with_test_routes do + @tax.save + assert_equal "http://example.com/taxes/#{@tax.id}", polymorphic_url([nil, @tax]) + end + end + + def test_with_array_containing_single_name_irregular_plural + with_test_routes do + @tax.save + assert_equal "http://example.com/taxes", polymorphic_url([:taxes]) + end + end + + # Tests for uncountable names + def test_uncountable_resource + with_test_routes do + @series.save + assert_equal "http://example.com/series/#{@series.id}", polymorphic_url(@series) + assert_equal "http://example.com/series", polymorphic_url(Series.new) + end + end + + def test_routing_a_to_model_delegate + with_test_routes do + @delegator.save + assert_equal "http://example.com/model_delegates/overridden", polymorphic_url(@delegator) + end + end + + def with_namespaced_routes(name) + with_routing do |set| + set.draw do + scope(:module => name) do + resources :blogs do + resources :posts + end + resources :posts + end + end + + self.class.send(:include, @routes.url_helpers) + yield + end + end + + def with_test_routes(options = {}) + with_routing do |set| + set.draw do + resources :projects do + resources :tasks + resource :bid do + resources :tasks + end + end + resources :taxes do + resources :faxes + resource :bid + end + resources :series + resources :model_delegates + end + + self.class.send(:include, @routes.url_helpers) + yield + end + end + + def with_admin_test_routes(options = {}) + with_routing do |set| + set.draw do + namespace :admin do + resources :projects do + resources :tasks + resource :bid do + resources :tasks + end + end + resources :taxes do + resources :faxes + end + resources :series + end + end + + self.class.send(:include, @routes.url_helpers) + yield + end + end + + def with_admin_and_site_test_routes(options = {}) + with_routing do |set| + set.draw do + namespace :admin do + resources :projects do + namespace :site do + resources :tasks do + resources :steps + end + end + end + end + end + + self.class.send(:include, @routes.url_helpers) + yield + end + 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 new file mode 100644 index 0000000000..409370104d --- /dev/null +++ b/actionview/test/activerecord/render_partial_with_record_identification_test.rb @@ -0,0 +1,210 @@ +require 'active_record_unit' + +class RenderPartialWithRecordIdentificationController < ActionController::Base + def render_with_has_many_and_belongs_to_association + @developer = Developer.find(1) + render :partial => @developer.projects + end + + def render_with_has_many_association + @topic = Topic.find(1) + render :partial => @topic.replies + end + + def render_with_scope + render :partial => Reply.base + end + + def render_with_has_many_through_association + @developer = Developer.first + render :partial => @developer.topics + end + + def render_with_has_one_association + @company = Company.find(1) + render :partial => @company.mascot + end + + def render_with_belongs_to_association + @reply = Reply.find(1) + render :partial => @reply.topic + end + + def render_with_record + @developer = Developer.first + render :partial => @developer + end + + def render_with_record_collection + @developers = Developer.all + render :partial => @developers + end + + def render_with_record_collection_and_spacer_template + @developer = Developer.find(1) + render :partial => @developer.projects, :spacer_template => 'test/partial_only' + end +end + +class RenderPartialWithRecordIdentificationTest < ActiveRecordTestCase + tests RenderPartialWithRecordIdentificationController + fixtures :developers, :projects, :developers_projects, :topics, :replies, :companies, :mascots + + 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 + 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 + 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 + +class Game < Struct.new(:name, :id) + extend ActiveModel::Naming + include ActiveModel::Conversion + def to_param + id.to_s + end +end + +module Fun + class NestedController < ActionController::Base + def render_with_record_in_nested_controller + render :partial => Game.new("Pong") + end + + def render_with_record_collection_in_nested_controller + render :partial => [ Game.new("Pong"), Game.new("Tank") ] + end + end + + module Serious + class NestedDeeperController < ActionController::Base + def render_with_record_in_deeper_nested_controller + render :partial => Game.new("Chess") + end + + def render_with_record_collection_in_deeper_nested_controller + render :partial => [ Game.new("Chess"), Game.new("Sudoku"), Game.new("Solitaire") ] + end + end + end +end + +class RenderPartialWithRecordIdentificationAndNestedControllersTest < ActiveRecordTestCase + tests Fun::NestedController + + 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 + +class RenderPartialWithRecordIdentificationAndNestedControllersWithoutPrefixTest < ActiveRecordTestCase + tests Fun::NestedController + + def test_render_with_record_in_nested_controller + old_config = ActionView::Base.prefix_partial_path_with_controller_namespace + 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 + end + + def test_render_with_record_collection_in_nested_controller + old_config = ActionView::Base.prefix_partial_path_with_controller_namespace + 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 + end +end + +class RenderPartialWithRecordIdentificationAndNestedDeeperControllersTest < ActiveRecordTestCase + tests Fun::Serious::NestedDeeperController + + 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 + +class RenderPartialWithRecordIdentificationAndNestedDeeperControllersWithoutPrefixTest < ActiveRecordTestCase + tests Fun::Serious::NestedDeeperController + + def test_render_with_record_in_deeper_nested_controller + old_config = ActionView::Base.prefix_partial_path_with_controller_namespace + 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 + end + + def test_render_with_record_collection_in_deeper_nested_controller + old_config = ActionView::Base.prefix_partial_path_with_controller_namespace + 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 + end +end diff --git a/actionview/test/fixtures/_top_level_partial.html.erb b/actionview/test/fixtures/_top_level_partial.html.erb new file mode 100644 index 0000000000..0b1c2e46e0 --- /dev/null +++ b/actionview/test/fixtures/_top_level_partial.html.erb @@ -0,0 +1 @@ +top level partial html
\ No newline at end of file diff --git a/actionview/test/fixtures/_top_level_partial_only.erb b/actionview/test/fixtures/_top_level_partial_only.erb new file mode 100644 index 0000000000..44f25b61d0 --- /dev/null +++ b/actionview/test/fixtures/_top_level_partial_only.erb @@ -0,0 +1 @@ +top level partial
\ No newline at end of file diff --git a/actionview/test/fixtures/blog_public/.gitignore b/actionview/test/fixtures/blog_public/.gitignore new file mode 100644 index 0000000000..312e635ee6 --- /dev/null +++ b/actionview/test/fixtures/blog_public/.gitignore @@ -0,0 +1 @@ +absolute/* diff --git a/actionview/test/fixtures/blog_public/blog.html b/actionview/test/fixtures/blog_public/blog.html new file mode 100644 index 0000000000..79ad44c010 --- /dev/null +++ b/actionview/test/fixtures/blog_public/blog.html @@ -0,0 +1 @@ +/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 new file mode 100644 index 0000000000..2de3825481 --- /dev/null +++ b/actionview/test/fixtures/blog_public/index.html @@ -0,0 +1 @@ +/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 new file mode 100644 index 0000000000..517bded335 --- /dev/null +++ b/actionview/test/fixtures/blog_public/subdir/index.html @@ -0,0 +1 @@ +/blog/subdir/index.html
\ No newline at end of file diff --git a/actionview/test/fixtures/comments/empty.de.html.erb b/actionview/test/fixtures/comments/empty.de.html.erb new file mode 100644 index 0000000000..cffd90dd26 --- /dev/null +++ b/actionview/test/fixtures/comments/empty.de.html.erb @@ -0,0 +1 @@ +<h1>Kein Kommentar</h1>
\ No newline at end of file diff --git a/actionview/test/fixtures/comments/empty.html.builder b/actionview/test/fixtures/comments/empty.html.builder new file mode 100644 index 0000000000..2b0c7207a3 --- /dev/null +++ b/actionview/test/fixtures/comments/empty.html.builder @@ -0,0 +1 @@ +xml.h1 'No Comment'
\ No newline at end of file diff --git a/actionview/test/fixtures/comments/empty.html.erb b/actionview/test/fixtures/comments/empty.html.erb new file mode 100644 index 0000000000..827f3861de --- /dev/null +++ b/actionview/test/fixtures/comments/empty.html.erb @@ -0,0 +1 @@ +<h1>No Comment</h1>
\ No newline at end of file diff --git a/actionview/test/fixtures/comments/empty.xml.erb b/actionview/test/fixtures/comments/empty.xml.erb new file mode 100644 index 0000000000..db1027cd7d --- /dev/null +++ b/actionview/test/fixtures/comments/empty.xml.erb @@ -0,0 +1 @@ +<error>No Comment</error>
\ No newline at end of file diff --git a/actionview/test/fixtures/companies.yml b/actionview/test/fixtures/companies.yml new file mode 100644 index 0000000000..ed2992e0b1 --- /dev/null +++ b/actionview/test/fixtures/companies.yml @@ -0,0 +1,24 @@ +thirty_seven_signals: + id: 1 + name: 37Signals + rating: 4 + +TextDrive: + id: 2 + name: TextDrive + rating: 4 + +PlanetArgon: + id: 3 + name: Planet Argon + rating: 4 + +Google: + id: 4 + name: Google + rating: 4 + +Ionist: + id: 5 + name: Ioni.st + rating: 4
\ No newline at end of file diff --git a/actionview/test/fixtures/company.rb b/actionview/test/fixtures/company.rb new file mode 100644 index 0000000000..f3ac3642fa --- /dev/null +++ b/actionview/test/fixtures/company.rb @@ -0,0 +1,9 @@ +class Company < ActiveRecord::Base + has_one :mascot + self.sequence_name = :companies_nonstd_seq + + validates_presence_of :name + def validate + errors.add('rating', 'rating should not be 2') if rating == 2 + end +end diff --git a/actionview/test/fixtures/custom_pattern/another.html.erb b/actionview/test/fixtures/custom_pattern/another.html.erb new file mode 100644 index 0000000000..6d7f3bafbb --- /dev/null +++ b/actionview/test/fixtures/custom_pattern/another.html.erb @@ -0,0 +1 @@ +Hello custom patterns!
\ No newline at end of file diff --git a/actionview/test/fixtures/custom_pattern/html/another.erb b/actionview/test/fixtures/custom_pattern/html/another.erb new file mode 100644 index 0000000000..dbd7e96ab6 --- /dev/null +++ b/actionview/test/fixtures/custom_pattern/html/another.erb @@ -0,0 +1 @@ +Another template!
\ No newline at end of file diff --git a/actionview/test/fixtures/custom_pattern/html/path.erb b/actionview/test/fixtures/custom_pattern/html/path.erb new file mode 100644 index 0000000000..6d7f3bafbb --- /dev/null +++ b/actionview/test/fixtures/custom_pattern/html/path.erb @@ -0,0 +1 @@ +Hello custom patterns!
\ No newline at end of file diff --git a/actionview/test/fixtures/customers/_customer.html.erb b/actionview/test/fixtures/customers/_customer.html.erb new file mode 100644 index 0000000000..483571e22a --- /dev/null +++ b/actionview/test/fixtures/customers/_customer.html.erb @@ -0,0 +1 @@ +<%= greeting %>: <%= customer.name %>
\ No newline at end of file diff --git a/actionview/test/fixtures/db_definitions/sqlite.sql b/actionview/test/fixtures/db_definitions/sqlite.sql new file mode 100644 index 0000000000..99df4b3e61 --- /dev/null +++ b/actionview/test/fixtures/db_definitions/sqlite.sql @@ -0,0 +1,49 @@ +CREATE TABLE 'companies' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'name' TEXT DEFAULT NULL, + 'rating' INTEGER DEFAULT 1 +); + +CREATE TABLE 'replies' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'content' text, + 'created_at' datetime, + 'updated_at' datetime, + 'topic_id' integer, + 'developer_id' integer +); + +CREATE TABLE 'topics' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'title' varchar(255), + 'subtitle' varchar(255), + 'content' text, + 'created_at' datetime, + 'updated_at' datetime +); + +CREATE TABLE 'developers' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'name' TEXT DEFAULT NULL, + 'salary' INTEGER DEFAULT 70000, + 'created_at' DATETIME DEFAULT NULL, + 'updated_at' DATETIME DEFAULT NULL +); + +CREATE TABLE 'projects' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'name' TEXT DEFAULT NULL +); + +CREATE TABLE 'developers_projects' ( + 'developer_id' INTEGER NOT NULL, + 'project_id' INTEGER NOT NULL, + 'joined_on' DATE DEFAULT NULL, + 'access_level' INTEGER DEFAULT 1 +); + +CREATE TABLE 'mascots' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'company_id' INTEGER NOT NULL, + 'name' TEXT DEFAULT NULL +);
\ No newline at end of file diff --git a/actionview/test/fixtures/developer.rb b/actionview/test/fixtures/developer.rb new file mode 100644 index 0000000000..4941463015 --- /dev/null +++ b/actionview/test/fixtures/developer.rb @@ -0,0 +1,10 @@ +class Developer < ActiveRecord::Base + has_and_belongs_to_many :projects + has_many :replies + has_many :topics, :through => :replies + accepts_nested_attributes_for :projects +end + +class DeVeLoPeR < ActiveRecord::Base + self.table_name = "developers" +end diff --git a/actionview/test/fixtures/developers.yml b/actionview/test/fixtures/developers.yml new file mode 100644 index 0000000000..3656564f63 --- /dev/null +++ b/actionview/test/fixtures/developers.yml @@ -0,0 +1,21 @@ +david: + id: 1 + name: David + salary: 80000 + +jamis: + id: 2 + name: Jamis + salary: 150000 + +<% (3..10).each do |digit| %> +dev_<%= digit %>: + id: <%= digit %> + name: fixture_<%= digit %> + salary: 100000 +<% end %> + +poor_jamis: + id: 11 + name: Jamis + salary: 9000
\ No newline at end of file diff --git a/actionview/test/fixtures/developers/_developer.erb b/actionview/test/fixtures/developers/_developer.erb new file mode 100644 index 0000000000..904a3137e7 --- /dev/null +++ b/actionview/test/fixtures/developers/_developer.erb @@ -0,0 +1 @@ +<%= developer.name %>
\ No newline at end of file diff --git a/actionview/test/fixtures/developers_projects.yml b/actionview/test/fixtures/developers_projects.yml new file mode 100644 index 0000000000..cee359c7cf --- /dev/null +++ b/actionview/test/fixtures/developers_projects.yml @@ -0,0 +1,13 @@ +david_action_controller: + developer_id: 1 + project_id: 2 + joined_on: 2004-10-10 + +david_active_record: + developer_id: 1 + project_id: 1 + joined_on: 2004-10-10 + +jamis_active_record: + developer_id: 2 + project_id: 1
\ 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 new file mode 100644 index 0000000000..f172e749da --- /dev/null +++ b/actionview/test/fixtures/digestor/comments/_comment.html.erb @@ -0,0 +1 @@ +Great story, bro! diff --git a/actionview/test/fixtures/digestor/comments/_comments.html.erb b/actionview/test/fixtures/digestor/comments/_comments.html.erb new file mode 100644 index 0000000000..c28646a283 --- /dev/null +++ b/actionview/test/fixtures/digestor/comments/_comments.html.erb @@ -0,0 +1 @@ +<%= render partial: "comments/comment", collection: commentable.comments %> diff --git a/actionview/test/fixtures/digestor/events/_event.html.erb b/actionview/test/fixtures/digestor/events/_event.html.erb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/actionview/test/fixtures/digestor/events/_event.html.erb diff --git a/actionview/test/fixtures/digestor/level/below/_header.html.erb b/actionview/test/fixtures/digestor/level/below/_header.html.erb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/actionview/test/fixtures/digestor/level/below/_header.html.erb diff --git a/actionview/test/fixtures/digestor/level/below/index.html.erb b/actionview/test/fixtures/digestor/level/below/index.html.erb new file mode 100644 index 0000000000..b92f49a8f8 --- /dev/null +++ b/actionview/test/fixtures/digestor/level/below/index.html.erb @@ -0,0 +1 @@ +<%= render partial: "header" %> diff --git a/actionview/test/fixtures/digestor/messages/_form.html.erb b/actionview/test/fixtures/digestor/messages/_form.html.erb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/actionview/test/fixtures/digestor/messages/_form.html.erb diff --git a/actionview/test/fixtures/digestor/messages/_header.html.erb b/actionview/test/fixtures/digestor/messages/_header.html.erb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/actionview/test/fixtures/digestor/messages/_header.html.erb diff --git a/actionview/test/fixtures/digestor/messages/_message.html.erb b/actionview/test/fixtures/digestor/messages/_message.html.erb new file mode 100644 index 0000000000..406a0fb848 --- /dev/null +++ b/actionview/test/fixtures/digestor/messages/_message.html.erb @@ -0,0 +1 @@ +THIS BE WHERE THEM MESSAGE GO, YO!
\ No newline at end of file diff --git a/actionview/test/fixtures/digestor/messages/actions/_move.html.erb b/actionview/test/fixtures/digestor/messages/actions/_move.html.erb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/actionview/test/fixtures/digestor/messages/actions/_move.html.erb diff --git a/actionview/test/fixtures/digestor/messages/edit.html.erb b/actionview/test/fixtures/digestor/messages/edit.html.erb new file mode 100644 index 0000000000..a9e0a88e32 --- /dev/null +++ b/actionview/test/fixtures/digestor/messages/edit.html.erb @@ -0,0 +1,5 @@ +<%= render "header" %> +<%= render partial: "form" %> +<%= render @message %> +<%= render ( @message.events ) %> +<%= render :partial => "comments/comment", :collection => @message.comments %> diff --git a/actionview/test/fixtures/digestor/messages/index.html.erb b/actionview/test/fixtures/digestor/messages/index.html.erb new file mode 100644 index 0000000000..1937b652e4 --- /dev/null +++ b/actionview/test/fixtures/digestor/messages/index.html.erb @@ -0,0 +1,2 @@ +<%= render @messages %> +<%= render @events %> diff --git a/actionview/test/fixtures/digestor/messages/show.html.erb b/actionview/test/fixtures/digestor/messages/show.html.erb new file mode 100644 index 0000000000..42aa2363dd --- /dev/null +++ b/actionview/test/fixtures/digestor/messages/show.html.erb @@ -0,0 +1,14 @@ +<%# Template Dependency: messages/message %> +<%= render "header" %> +<%= render "comments/comments" %> + +<%= render "messages/actions/move" %> + +<%= render @message.history.events %> + +<%# render "something_missing" %> +<%# render "something_missing_1" %> + +<% + # Template Dependency: messages/form +%>
\ No newline at end of file diff --git a/actionview/test/fixtures/fun/games/_game.erb b/actionview/test/fixtures/fun/games/_game.erb new file mode 100644 index 0000000000..f0f542ff92 --- /dev/null +++ b/actionview/test/fixtures/fun/games/_game.erb @@ -0,0 +1 @@ +Fun <%= game.name %> diff --git a/actionview/test/fixtures/fun/games/hello_world.erb b/actionview/test/fixtures/fun/games/hello_world.erb new file mode 100644 index 0000000000..1ebfbe2539 --- /dev/null +++ b/actionview/test/fixtures/fun/games/hello_world.erb @@ -0,0 +1 @@ +Living in a nested world
\ No newline at end of file diff --git a/actionview/test/fixtures/fun/serious/games/_game.erb b/actionview/test/fixtures/fun/serious/games/_game.erb new file mode 100644 index 0000000000..523bc55bd7 --- /dev/null +++ b/actionview/test/fixtures/fun/serious/games/_game.erb @@ -0,0 +1 @@ +Serious <%= game.name %> 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 new file mode 100644 index 0000000000..3125583a28 --- /dev/null +++ b/actionview/test/fixtures/functional_caching/fragment_cached_without_digest.html.erb @@ -0,0 +1,3 @@ +<body> +<%= cache 'nodigest', skip_digest: true do %><p>ERB</p><% end %> +</body> diff --git a/actionview/test/fixtures/games/_game.erb b/actionview/test/fixtures/games/_game.erb new file mode 100644 index 0000000000..1aeb81fcba --- /dev/null +++ b/actionview/test/fixtures/games/_game.erb @@ -0,0 +1 @@ +Just <%= game.name %> diff --git a/actionview/test/fixtures/good_customers/_good_customer.html.erb b/actionview/test/fixtures/good_customers/_good_customer.html.erb new file mode 100644 index 0000000000..a2d97ebc6d --- /dev/null +++ b/actionview/test/fixtures/good_customers/_good_customer.html.erb @@ -0,0 +1 @@ +<%= greeting %> good customer: <%= good_customer.name %><%= good_customer_counter %>
\ No newline at end of file diff --git a/actionview/test/fixtures/happy_path/render_action/hello_world.erb b/actionview/test/fixtures/happy_path/render_action/hello_world.erb new file mode 100644 index 0000000000..6769dd60bd --- /dev/null +++ b/actionview/test/fixtures/happy_path/render_action/hello_world.erb @@ -0,0 +1 @@ +Hello world!
\ No newline at end of file diff --git a/actionview/test/fixtures/layout_tests/alt/hello.erb b/actionview/test/fixtures/layout_tests/alt/hello.erb new file mode 100644 index 0000000000..1055c36659 --- /dev/null +++ b/actionview/test/fixtures/layout_tests/alt/hello.erb @@ -0,0 +1 @@ +alt/hello.erb diff --git a/actionview/test/fixtures/layouts/_column.html.erb b/actionview/test/fixtures/layouts/_column.html.erb new file mode 100644 index 0000000000..96db002b8a --- /dev/null +++ b/actionview/test/fixtures/layouts/_column.html.erb @@ -0,0 +1,2 @@ +<div id="column"><%= yield :column %></div> +<div id="content"><%= yield %></div>
\ No newline at end of file diff --git a/actionview/test/fixtures/layouts/_customers.erb b/actionview/test/fixtures/layouts/_customers.erb new file mode 100644 index 0000000000..ae63f13cd3 --- /dev/null +++ b/actionview/test/fixtures/layouts/_customers.erb @@ -0,0 +1 @@ +<title><%= yield Struct.new(:name).new("David") %></title>
\ No newline at end of file diff --git a/actionview/test/fixtures/layouts/_partial_and_yield.erb b/actionview/test/fixtures/layouts/_partial_and_yield.erb new file mode 100644 index 0000000000..74cc428ffa --- /dev/null +++ b/actionview/test/fixtures/layouts/_partial_and_yield.erb @@ -0,0 +1,2 @@ +<%= render :partial => 'test/partial' %> +<%= yield %> diff --git a/actionview/test/fixtures/layouts/_yield_only.erb b/actionview/test/fixtures/layouts/_yield_only.erb new file mode 100644 index 0000000000..37f0bddbd7 --- /dev/null +++ b/actionview/test/fixtures/layouts/_yield_only.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/actionview/test/fixtures/layouts/_yield_with_params.erb b/actionview/test/fixtures/layouts/_yield_with_params.erb new file mode 100644 index 0000000000..68e6557fb8 --- /dev/null +++ b/actionview/test/fixtures/layouts/_yield_with_params.erb @@ -0,0 +1 @@ +<%= yield 'Yield!' %> diff --git a/actionview/test/fixtures/layouts/streaming.erb b/actionview/test/fixtures/layouts/streaming.erb new file mode 100644 index 0000000000..d3f896a6ca --- /dev/null +++ b/actionview/test/fixtures/layouts/streaming.erb @@ -0,0 +1,4 @@ +<%= yield :header -%> +<%= yield -%> +<%= yield :footer -%> +<%= yield(:unknown).presence || "." -%>
\ No newline at end of file diff --git a/actionview/test/fixtures/layouts/yield.erb b/actionview/test/fixtures/layouts/yield.erb new file mode 100644 index 0000000000..482dc9022e --- /dev/null +++ b/actionview/test/fixtures/layouts/yield.erb @@ -0,0 +1,2 @@ +<title><%= yield :title %></title> +<%= yield %> diff --git a/actionview/test/fixtures/layouts/yield_with_render_inline_inside.erb b/actionview/test/fixtures/layouts/yield_with_render_inline_inside.erb new file mode 100644 index 0000000000..7298d79690 --- /dev/null +++ b/actionview/test/fixtures/layouts/yield_with_render_inline_inside.erb @@ -0,0 +1,2 @@ +<%= render :inline => 'welcome' %> +<%= yield %> diff --git a/actionview/test/fixtures/layouts/yield_with_render_partial_inside.erb b/actionview/test/fixtures/layouts/yield_with_render_partial_inside.erb new file mode 100644 index 0000000000..74cc428ffa --- /dev/null +++ b/actionview/test/fixtures/layouts/yield_with_render_partial_inside.erb @@ -0,0 +1,2 @@ +<%= render :partial => 'test/partial' %> +<%= yield %> diff --git a/actionview/test/fixtures/mascot.rb b/actionview/test/fixtures/mascot.rb new file mode 100644 index 0000000000..f9f1448b8f --- /dev/null +++ b/actionview/test/fixtures/mascot.rb @@ -0,0 +1,3 @@ +class Mascot < ActiveRecord::Base + belongs_to :company +end
\ No newline at end of file diff --git a/actionview/test/fixtures/mascots.yml b/actionview/test/fixtures/mascots.yml new file mode 100644 index 0000000000..17b7dff454 --- /dev/null +++ b/actionview/test/fixtures/mascots.yml @@ -0,0 +1,4 @@ +upload_bird: + id: 1 + company_id: 1 + name: The Upload Bird
\ No newline at end of file diff --git a/actionview/test/fixtures/mascots/_mascot.html.erb b/actionview/test/fixtures/mascots/_mascot.html.erb new file mode 100644 index 0000000000..432773a1da --- /dev/null +++ b/actionview/test/fixtures/mascots/_mascot.html.erb @@ -0,0 +1 @@ +<%= mascot.name %>
\ No newline at end of file diff --git a/actionview/test/fixtures/multipart/bracketed_utf8_param b/actionview/test/fixtures/multipart/bracketed_utf8_param new file mode 100644 index 0000000000..df9cecea08 --- /dev/null +++ b/actionview/test/fixtures/multipart/bracketed_utf8_param @@ -0,0 +1,5 @@ +--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 new file mode 100644 index 0000000000..1d9fae7b17 --- /dev/null +++ b/actionview/test/fixtures/multipart/single_utf8_param @@ -0,0 +1,5 @@ +--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/plain_text.raw b/actionview/test/fixtures/plain_text.raw new file mode 100644 index 0000000000..b13985337f --- /dev/null +++ b/actionview/test/fixtures/plain_text.raw @@ -0,0 +1 @@ +<%= hello_world %> diff --git a/actionview/test/fixtures/plain_text_with_characters.raw b/actionview/test/fixtures/plain_text_with_characters.raw new file mode 100644 index 0000000000..1e86e44fb4 --- /dev/null +++ b/actionview/test/fixtures/plain_text_with_characters.raw @@ -0,0 +1 @@ +Here are some characters: !@#$%^&*()-="'}{` diff --git a/actionview/test/fixtures/project.rb b/actionview/test/fixtures/project.rb new file mode 100644 index 0000000000..c124a9e605 --- /dev/null +++ b/actionview/test/fixtures/project.rb @@ -0,0 +1,3 @@ +class Project < ActiveRecord::Base + has_and_belongs_to_many :developers, -> { uniq } +end diff --git a/actionview/test/fixtures/projects.yml b/actionview/test/fixtures/projects.yml new file mode 100644 index 0000000000..02800c7824 --- /dev/null +++ b/actionview/test/fixtures/projects.yml @@ -0,0 +1,7 @@ +action_controller: + id: 2 + name: Active Controller + +active_record: + id: 1 + name: Active Record diff --git a/actionview/test/fixtures/projects/_project.erb b/actionview/test/fixtures/projects/_project.erb new file mode 100644 index 0000000000..480c4c2af3 --- /dev/null +++ b/actionview/test/fixtures/projects/_project.erb @@ -0,0 +1 @@ +<%= project.name %>
\ No newline at end of file diff --git a/actionview/test/fixtures/public/.gitignore b/actionview/test/fixtures/public/.gitignore new file mode 100644 index 0000000000..312e635ee6 --- /dev/null +++ b/actionview/test/fixtures/public/.gitignore @@ -0,0 +1 @@ +absolute/* diff --git a/actionview/test/fixtures/public/elsewhere/cools.js b/actionview/test/fixtures/public/elsewhere/cools.js new file mode 100644 index 0000000000..6e12fe29c4 --- /dev/null +++ b/actionview/test/fixtures/public/elsewhere/cools.js @@ -0,0 +1 @@ +// cools.js
\ No newline at end of file diff --git a/actionview/test/fixtures/public/elsewhere/file.css b/actionview/test/fixtures/public/elsewhere/file.css new file mode 100644 index 0000000000..6aea0733b1 --- /dev/null +++ b/actionview/test/fixtures/public/elsewhere/file.css @@ -0,0 +1 @@ +/*file.css*/
\ No newline at end of file diff --git a/actionview/test/fixtures/public/foo/baz.css b/actionview/test/fixtures/public/foo/baz.css new file mode 100644 index 0000000000..b5173fbef2 --- /dev/null +++ b/actionview/test/fixtures/public/foo/baz.css @@ -0,0 +1,3 @@ +body { +background: #000; +} diff --git a/actionview/test/fixtures/public/javascripts/application.js b/actionview/test/fixtures/public/javascripts/application.js new file mode 100644 index 0000000000..9702692980 --- /dev/null +++ b/actionview/test/fixtures/public/javascripts/application.js @@ -0,0 +1 @@ +// application js
\ No newline at end of file diff --git a/actionview/test/fixtures/public/javascripts/bank.js b/actionview/test/fixtures/public/javascripts/bank.js new file mode 100644 index 0000000000..4a1bee7182 --- /dev/null +++ b/actionview/test/fixtures/public/javascripts/bank.js @@ -0,0 +1 @@ +// bank js
\ No newline at end of file diff --git a/actionview/test/fixtures/public/javascripts/common.javascript b/actionview/test/fixtures/public/javascripts/common.javascript new file mode 100644 index 0000000000..2ae1929056 --- /dev/null +++ b/actionview/test/fixtures/public/javascripts/common.javascript @@ -0,0 +1 @@ +// common.javascript
\ No newline at end of file diff --git a/actionview/test/fixtures/public/javascripts/controls.js b/actionview/test/fixtures/public/javascripts/controls.js new file mode 100644 index 0000000000..88168d9f13 --- /dev/null +++ b/actionview/test/fixtures/public/javascripts/controls.js @@ -0,0 +1 @@ +// controls js
\ No newline at end of file diff --git a/actionview/test/fixtures/public/javascripts/dragdrop.js b/actionview/test/fixtures/public/javascripts/dragdrop.js new file mode 100644 index 0000000000..c07061ac0c --- /dev/null +++ b/actionview/test/fixtures/public/javascripts/dragdrop.js @@ -0,0 +1 @@ +// dragdrop js
\ No newline at end of file diff --git a/actionview/test/fixtures/public/javascripts/effects.js b/actionview/test/fixtures/public/javascripts/effects.js new file mode 100644 index 0000000000..b555d63034 --- /dev/null +++ b/actionview/test/fixtures/public/javascripts/effects.js @@ -0,0 +1 @@ +// effects js
\ No newline at end of file diff --git a/actionview/test/fixtures/public/javascripts/prototype.js b/actionview/test/fixtures/public/javascripts/prototype.js new file mode 100644 index 0000000000..9780064a0e --- /dev/null +++ b/actionview/test/fixtures/public/javascripts/prototype.js @@ -0,0 +1 @@ +// prototype js
\ No newline at end of file diff --git a/actionview/test/fixtures/public/javascripts/robber.js b/actionview/test/fixtures/public/javascripts/robber.js new file mode 100644 index 0000000000..eb82fcbdf4 --- /dev/null +++ b/actionview/test/fixtures/public/javascripts/robber.js @@ -0,0 +1 @@ +// robber js
\ No newline at end of file diff --git a/actionview/test/fixtures/public/javascripts/subdir/subdir.js b/actionview/test/fixtures/public/javascripts/subdir/subdir.js new file mode 100644 index 0000000000..9d23a67aa1 --- /dev/null +++ b/actionview/test/fixtures/public/javascripts/subdir/subdir.js @@ -0,0 +1 @@ +// subdir js diff --git a/actionview/test/fixtures/public/javascripts/version.1.0.js b/actionview/test/fixtures/public/javascripts/version.1.0.js new file mode 100644 index 0000000000..cfd5fce70e --- /dev/null +++ b/actionview/test/fixtures/public/javascripts/version.1.0.js @@ -0,0 +1 @@ +// version.1.0 js
\ No newline at end of file diff --git a/actionview/test/fixtures/public/stylesheets/bank.css b/actionview/test/fixtures/public/stylesheets/bank.css new file mode 100644 index 0000000000..ea161b12b2 --- /dev/null +++ b/actionview/test/fixtures/public/stylesheets/bank.css @@ -0,0 +1 @@ +/* bank.css */
\ No newline at end of file diff --git a/actionview/test/fixtures/public/stylesheets/random.styles b/actionview/test/fixtures/public/stylesheets/random.styles new file mode 100644 index 0000000000..d4eeead95c --- /dev/null +++ b/actionview/test/fixtures/public/stylesheets/random.styles @@ -0,0 +1 @@ +/* random.styles */
\ No newline at end of file diff --git a/actionview/test/fixtures/public/stylesheets/robber.css b/actionview/test/fixtures/public/stylesheets/robber.css new file mode 100644 index 0000000000..0fdd00a6a5 --- /dev/null +++ b/actionview/test/fixtures/public/stylesheets/robber.css @@ -0,0 +1 @@ +/* robber.css */
\ No newline at end of file diff --git a/actionview/test/fixtures/public/stylesheets/subdir/subdir.css b/actionview/test/fixtures/public/stylesheets/subdir/subdir.css new file mode 100644 index 0000000000..241152a905 --- /dev/null +++ b/actionview/test/fixtures/public/stylesheets/subdir/subdir.css @@ -0,0 +1 @@ +/* subdir.css */ diff --git a/actionview/test/fixtures/public/stylesheets/version.1.0.css b/actionview/test/fixtures/public/stylesheets/version.1.0.css new file mode 100644 index 0000000000..30f5f9ba6e --- /dev/null +++ b/actionview/test/fixtures/public/stylesheets/version.1.0.css @@ -0,0 +1 @@ +/* version.1.0.css */
\ No newline at end of file diff --git a/actionview/test/fixtures/replies.yml b/actionview/test/fixtures/replies.yml new file mode 100644 index 0000000000..2a3454b8bf --- /dev/null +++ b/actionview/test/fixtures/replies.yml @@ -0,0 +1,15 @@ +witty_retort: + id: 1 + topic_id: 1 + developer_id: 1 + content: Birdman is better! + created_at: <%= 6.hours.ago.to_s(:db) %> + updated_at: nil + +another: + id: 2 + topic_id: 2 + developer_id: 1 + content: Nuh uh! + created_at: <%= 1.hour.ago.to_s(:db) %> + updated_at: nil diff --git a/actionview/test/fixtures/replies/_reply.erb b/actionview/test/fixtures/replies/_reply.erb new file mode 100644 index 0000000000..68baf548d8 --- /dev/null +++ b/actionview/test/fixtures/replies/_reply.erb @@ -0,0 +1 @@ +<%= reply.content %>
\ No newline at end of file diff --git a/actionview/test/fixtures/reply.rb b/actionview/test/fixtures/reply.rb new file mode 100644 index 0000000000..047522c55b --- /dev/null +++ b/actionview/test/fixtures/reply.rb @@ -0,0 +1,7 @@ +class Reply < ActiveRecord::Base + scope :base, -> { all } + belongs_to :topic, -> { includes(:replies) } + belongs_to :developer + + validates_presence_of :content +end diff --git a/actionview/test/fixtures/respond_to/using_defaults_with_all.html.erb b/actionview/test/fixtures/respond_to/using_defaults_with_all.html.erb new file mode 100644 index 0000000000..9f1f855269 --- /dev/null +++ b/actionview/test/fixtures/respond_to/using_defaults_with_all.html.erb @@ -0,0 +1 @@ +HTML! diff --git a/actionview/test/fixtures/ruby_template.ruby b/actionview/test/fixtures/ruby_template.ruby new file mode 100644 index 0000000000..5097bce47c --- /dev/null +++ b/actionview/test/fixtures/ruby_template.ruby @@ -0,0 +1,2 @@ +body = "" +body << ["Hello", "from", "Ruby", "code"].join(" ") diff --git a/actionview/test/fixtures/scope/test/modgreet.erb b/actionview/test/fixtures/scope/test/modgreet.erb new file mode 100644 index 0000000000..8947726e89 --- /dev/null +++ b/actionview/test/fixtures/scope/test/modgreet.erb @@ -0,0 +1 @@ +<p>Beautiful modules!</p>
\ No newline at end of file diff --git a/actionview/test/fixtures/shared.html.erb b/actionview/test/fixtures/shared.html.erb new file mode 100644 index 0000000000..af262fc9f8 --- /dev/null +++ b/actionview/test/fixtures/shared.html.erb @@ -0,0 +1 @@ +Elastica
\ No newline at end of file diff --git a/actionview/test/fixtures/test/_200.html.erb b/actionview/test/fixtures/test/_200.html.erb new file mode 100644 index 0000000000..c9f45675dc --- /dev/null +++ b/actionview/test/fixtures/test/_200.html.erb @@ -0,0 +1 @@ +<h1>Invalid partial</h1> diff --git a/actionview/test/fixtures/test/_b_layout_for_partial.html.erb b/actionview/test/fixtures/test/_b_layout_for_partial.html.erb new file mode 100644 index 0000000000..e918ba8f83 --- /dev/null +++ b/actionview/test/fixtures/test/_b_layout_for_partial.html.erb @@ -0,0 +1 @@ +<b><%= yield %></b>
\ No newline at end of file diff --git a/actionview/test/fixtures/test/_b_layout_for_partial_with_object.html.erb b/actionview/test/fixtures/test/_b_layout_for_partial_with_object.html.erb new file mode 100644 index 0000000000..bdd53014cd --- /dev/null +++ b/actionview/test/fixtures/test/_b_layout_for_partial_with_object.html.erb @@ -0,0 +1 @@ +<b class="<%= customer.name.downcase %>"><%= yield %></b>
\ No newline at end of file diff --git a/actionview/test/fixtures/test/_b_layout_for_partial_with_object_counter.html.erb b/actionview/test/fixtures/test/_b_layout_for_partial_with_object_counter.html.erb new file mode 100644 index 0000000000..44d6121297 --- /dev/null +++ b/actionview/test/fixtures/test/_b_layout_for_partial_with_object_counter.html.erb @@ -0,0 +1 @@ +<b data-counter="<%= customer_counter %>"><%= yield %></b>
\ No newline at end of file diff --git a/actionview/test/fixtures/test/_changing_priority.html.erb b/actionview/test/fixtures/test/_changing_priority.html.erb new file mode 100644 index 0000000000..3225efc49a --- /dev/null +++ b/actionview/test/fixtures/test/_changing_priority.html.erb @@ -0,0 +1 @@ +HTML
\ No newline at end of file diff --git a/actionview/test/fixtures/test/_changing_priority.json.erb b/actionview/test/fixtures/test/_changing_priority.json.erb new file mode 100644 index 0000000000..7fa41dce66 --- /dev/null +++ b/actionview/test/fixtures/test/_changing_priority.json.erb @@ -0,0 +1 @@ +JSON
\ No newline at end of file diff --git a/actionview/test/fixtures/test/_content_tag_nested_in_content_tag.erb b/actionview/test/fixtures/test/_content_tag_nested_in_content_tag.erb new file mode 100644 index 0000000000..2f21a75dd9 --- /dev/null +++ b/actionview/test/fixtures/test/_content_tag_nested_in_content_tag.erb @@ -0,0 +1,3 @@ +<%= content_tag 'p' do %> + <%= content_tag 'b', 'Hello' %> +<% end %> diff --git a/actionview/test/fixtures/test/_counter.html.erb b/actionview/test/fixtures/test/_counter.html.erb new file mode 100644 index 0000000000..fd245bfc70 --- /dev/null +++ b/actionview/test/fixtures/test/_counter.html.erb @@ -0,0 +1 @@ +<%= counter_counter %>
\ No newline at end of file diff --git a/actionview/test/fixtures/test/_customer.erb b/actionview/test/fixtures/test/_customer.erb new file mode 100644 index 0000000000..d8220afeda --- /dev/null +++ b/actionview/test/fixtures/test/_customer.erb @@ -0,0 +1 @@ +Hello: <%= customer.name rescue "Anonymous" %>
\ No newline at end of file diff --git a/actionview/test/fixtures/test/_customer_greeting.erb b/actionview/test/fixtures/test/_customer_greeting.erb new file mode 100644 index 0000000000..6acbcb20c4 --- /dev/null +++ b/actionview/test/fixtures/test/_customer_greeting.erb @@ -0,0 +1 @@ +<%= greeting %>: <%= customer_greeting.name %>
\ No newline at end of file diff --git a/actionview/test/fixtures/test/_customer_with_var.erb b/actionview/test/fixtures/test/_customer_with_var.erb new file mode 100644 index 0000000000..00047dd20e --- /dev/null +++ b/actionview/test/fixtures/test/_customer_with_var.erb @@ -0,0 +1 @@ +<%= customer.name %> <%= customer.name %> <%= customer.name %>
\ No newline at end of file diff --git a/actionview/test/fixtures/test/_directory/_partial_with_locales.html.erb b/actionview/test/fixtures/test/_directory/_partial_with_locales.html.erb new file mode 100644 index 0000000000..1cc8d41475 --- /dev/null +++ b/actionview/test/fixtures/test/_directory/_partial_with_locales.html.erb @@ -0,0 +1 @@ +Hello <%= name %> diff --git a/actionview/test/fixtures/test/_first_json_partial.json.erb b/actionview/test/fixtures/test/_first_json_partial.json.erb new file mode 100644 index 0000000000..790ee896db --- /dev/null +++ b/actionview/test/fixtures/test/_first_json_partial.json.erb @@ -0,0 +1 @@ +<%= render :partial => "test/second_json_partial" %>
\ No newline at end of file diff --git a/actionview/test/fixtures/test/_from_helper.erb b/actionview/test/fixtures/test/_from_helper.erb new file mode 100644 index 0000000000..16de7c0f8a --- /dev/null +++ b/actionview/test/fixtures/test/_from_helper.erb @@ -0,0 +1 @@ +<%= render_from_helper %>
\ No newline at end of file diff --git a/actionview/test/fixtures/test/_json_change_priority.json.erb b/actionview/test/fixtures/test/_json_change_priority.json.erb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/actionview/test/fixtures/test/_json_change_priority.json.erb diff --git a/actionview/test/fixtures/test/_label_with_block.erb b/actionview/test/fixtures/test/_label_with_block.erb new file mode 100644 index 0000000000..40117e594e --- /dev/null +++ b/actionview/test/fixtures/test/_label_with_block.erb @@ -0,0 +1,4 @@ +<%= label 'post', 'message' do %> + Message + <%= text_field 'post', 'message' %> +<% end %> diff --git a/actionview/test/fixtures/test/_layout_for_block_with_args.html.erb b/actionview/test/fixtures/test/_layout_for_block_with_args.html.erb new file mode 100644 index 0000000000..307533208d --- /dev/null +++ b/actionview/test/fixtures/test/_layout_for_block_with_args.html.erb @@ -0,0 +1,3 @@ +Before +<%= yield 'arg1', 'arg2' %> +After
\ No newline at end of file diff --git a/actionview/test/fixtures/test/_layout_for_partial.html.erb b/actionview/test/fixtures/test/_layout_for_partial.html.erb new file mode 100644 index 0000000000..666efadbb6 --- /dev/null +++ b/actionview/test/fixtures/test/_layout_for_partial.html.erb @@ -0,0 +1,3 @@ +Before (<%= name %>) +<%= yield %> +After
\ No newline at end of file diff --git a/actionview/test/fixtures/test/_layout_with_partial_and_yield.html.erb b/actionview/test/fixtures/test/_layout_with_partial_and_yield.html.erb new file mode 100644 index 0000000000..820e7db789 --- /dev/null +++ b/actionview/test/fixtures/test/_layout_with_partial_and_yield.html.erb @@ -0,0 +1,4 @@ +Before +<%= render :partial => "test/partial" %> +<%= yield %> +After diff --git a/actionview/test/fixtures/test/_local_inspector.html.erb b/actionview/test/fixtures/test/_local_inspector.html.erb new file mode 100644 index 0000000000..e6765c0882 --- /dev/null +++ b/actionview/test/fixtures/test/_local_inspector.html.erb @@ -0,0 +1 @@ +<%= local_assigns.keys.map {|k| k.to_s }.sort.join(",") -%>
\ No newline at end of file diff --git a/actionview/test/fixtures/test/_object_inspector.erb b/actionview/test/fixtures/test/_object_inspector.erb new file mode 100644 index 0000000000..53af593821 --- /dev/null +++ b/actionview/test/fixtures/test/_object_inspector.erb @@ -0,0 +1 @@ +<%= object_inspector.inspect -%>
\ No newline at end of file diff --git a/actionview/test/fixtures/test/_one.html.erb b/actionview/test/fixtures/test/_one.html.erb new file mode 100644 index 0000000000..f796291cb4 --- /dev/null +++ b/actionview/test/fixtures/test/_one.html.erb @@ -0,0 +1 @@ +<%= render :partial => "two" %> world diff --git a/actionview/test/fixtures/test/_partial.erb b/actionview/test/fixtures/test/_partial.erb new file mode 100644 index 0000000000..e466dcbd8e --- /dev/null +++ b/actionview/test/fixtures/test/_partial.erb @@ -0,0 +1 @@ +invalid
\ No newline at end of file diff --git a/actionview/test/fixtures/test/_partial.html.erb b/actionview/test/fixtures/test/_partial.html.erb new file mode 100644 index 0000000000..e39f6c9827 --- /dev/null +++ b/actionview/test/fixtures/test/_partial.html.erb @@ -0,0 +1 @@ +partial html
\ No newline at end of file diff --git a/actionview/test/fixtures/test/_partial.js.erb b/actionview/test/fixtures/test/_partial.js.erb new file mode 100644 index 0000000000..b350cdd7ef --- /dev/null +++ b/actionview/test/fixtures/test/_partial.js.erb @@ -0,0 +1 @@ +partial js
\ No newline at end of file diff --git a/actionview/test/fixtures/test/_partial_for_use_in_layout.html.erb b/actionview/test/fixtures/test/_partial_for_use_in_layout.html.erb new file mode 100644 index 0000000000..3a03a64e31 --- /dev/null +++ b/actionview/test/fixtures/test/_partial_for_use_in_layout.html.erb @@ -0,0 +1 @@ +Inside from partial (<%= name %>)
\ No newline at end of file diff --git a/actionview/test/fixtures/test/_partial_name_local_variable.erb b/actionview/test/fixtures/test/_partial_name_local_variable.erb new file mode 100644 index 0000000000..cc3a91c89f --- /dev/null +++ b/actionview/test/fixtures/test/_partial_name_local_variable.erb @@ -0,0 +1 @@ +<%= partial_name_local_variable %> diff --git a/actionview/test/fixtures/test/_partial_only.erb b/actionview/test/fixtures/test/_partial_only.erb new file mode 100644 index 0000000000..a44b3eed40 --- /dev/null +++ b/actionview/test/fixtures/test/_partial_only.erb @@ -0,0 +1 @@ +only partial
\ No newline at end of file diff --git a/actionview/test/fixtures/test/_partial_with_layout.erb b/actionview/test/fixtures/test/_partial_with_layout.erb new file mode 100644 index 0000000000..2a50c834fe --- /dev/null +++ b/actionview/test/fixtures/test/_partial_with_layout.erb @@ -0,0 +1,2 @@ +<%= render :partial => 'test/partial', :layout => 'test/layout_for_partial', :locals => { :name => 'Bar!' } %> +partial with layout diff --git a/actionview/test/fixtures/test/_partial_with_layout_block_content.erb b/actionview/test/fixtures/test/_partial_with_layout_block_content.erb new file mode 100644 index 0000000000..65dafd93a8 --- /dev/null +++ b/actionview/test/fixtures/test/_partial_with_layout_block_content.erb @@ -0,0 +1,4 @@ +<%= render :layout => 'test/layout_for_partial', :locals => { :name => 'Bar!' } do %> + Content from inside layout! +<% end %> +partial with layout diff --git a/actionview/test/fixtures/test/_partial_with_layout_block_partial.erb b/actionview/test/fixtures/test/_partial_with_layout_block_partial.erb new file mode 100644 index 0000000000..444197a7d0 --- /dev/null +++ b/actionview/test/fixtures/test/_partial_with_layout_block_partial.erb @@ -0,0 +1,4 @@ +<%= render :layout => 'test/layout_for_partial', :locals => { :name => 'Bar!' } do %> + <%= render 'test/partial' %> +<% end %> +partial with layout diff --git a/actionview/test/fixtures/test/_partial_with_only_html_version.html.erb b/actionview/test/fixtures/test/_partial_with_only_html_version.html.erb new file mode 100644 index 0000000000..00e6b6d6da --- /dev/null +++ b/actionview/test/fixtures/test/_partial_with_only_html_version.html.erb @@ -0,0 +1 @@ +partial with only html version
\ No newline at end of file diff --git a/actionview/test/fixtures/test/_partial_with_partial.erb b/actionview/test/fixtures/test/_partial_with_partial.erb new file mode 100644 index 0000000000..ee0d5037b6 --- /dev/null +++ b/actionview/test/fixtures/test/_partial_with_partial.erb @@ -0,0 +1,2 @@ +<%= render 'test/partial' %> +partial with partial diff --git a/actionview/test/fixtures/test/_raise.html.erb b/actionview/test/fixtures/test/_raise.html.erb new file mode 100644 index 0000000000..68b08181d3 --- /dev/null +++ b/actionview/test/fixtures/test/_raise.html.erb @@ -0,0 +1 @@ +<%= doesnt_exist %>
\ No newline at end of file diff --git a/actionview/test/fixtures/test/_raise_indentation.html.erb b/actionview/test/fixtures/test/_raise_indentation.html.erb new file mode 100644 index 0000000000..f9a93728fe --- /dev/null +++ b/actionview/test/fixtures/test/_raise_indentation.html.erb @@ -0,0 +1,13 @@ +<p>First paragraph</p> +<p>Second paragraph</p> +<p>Third paragraph</p> +<p>Fourth paragraph</p> +<p>Fifth paragraph</p> +<p>Sixth paragraph</p> +<p>Seventh paragraph</p> +<p>Eight paragraph</p> +<p>Ninth paragraph</p> +<p>Tenth paragraph</p> +<%= raise "error here!" %> +<p>Eleventh paragraph</p> +<p>Twelfth paragraph</p>
\ No newline at end of file diff --git a/actionview/test/fixtures/test/_second_json_partial.json.erb b/actionview/test/fixtures/test/_second_json_partial.json.erb new file mode 100644 index 0000000000..5ebb7f1afd --- /dev/null +++ b/actionview/test/fixtures/test/_second_json_partial.json.erb @@ -0,0 +1 @@ +Third level
\ No newline at end of file diff --git a/actionview/test/fixtures/test/_two.html.erb b/actionview/test/fixtures/test/_two.html.erb new file mode 100644 index 0000000000..5ab2f8a432 --- /dev/null +++ b/actionview/test/fixtures/test/_two.html.erb @@ -0,0 +1 @@ +Hello
\ No newline at end of file diff --git a/actionview/test/fixtures/test/_utf8_partial.html.erb b/actionview/test/fixtures/test/_utf8_partial.html.erb new file mode 100644 index 0000000000..8d717fd427 --- /dev/null +++ b/actionview/test/fixtures/test/_utf8_partial.html.erb @@ -0,0 +1 @@ +<%= "текст" %> diff --git a/actionview/test/fixtures/test/_utf8_partial_magic.html.erb b/actionview/test/fixtures/test/_utf8_partial_magic.html.erb new file mode 100644 index 0000000000..4e2224610a --- /dev/null +++ b/actionview/test/fixtures/test/_utf8_partial_magic.html.erb @@ -0,0 +1,2 @@ +<%# encoding: utf-8 -%> +<%= "текст" %> diff --git a/actionview/test/fixtures/test/basic.html.erb b/actionview/test/fixtures/test/basic.html.erb new file mode 100644 index 0000000000..ea696d7e01 --- /dev/null +++ b/actionview/test/fixtures/test/basic.html.erb @@ -0,0 +1 @@ +Hello from basic.html.erb
\ No newline at end of file diff --git a/actionview/test/fixtures/test/calling_partial_with_layout.html.erb b/actionview/test/fixtures/test/calling_partial_with_layout.html.erb new file mode 100644 index 0000000000..ac44bc0d81 --- /dev/null +++ b/actionview/test/fixtures/test/calling_partial_with_layout.html.erb @@ -0,0 +1 @@ +<%= render(:layout => "layout_for_partial", :partial => "partial_for_use_in_layout", :locals => { :name => "David" }) %>
\ No newline at end of file diff --git a/actionview/test/fixtures/test/change_priority.html.erb b/actionview/test/fixtures/test/change_priority.html.erb new file mode 100644 index 0000000000..5618977d05 --- /dev/null +++ b/actionview/test/fixtures/test/change_priority.html.erb @@ -0,0 +1,2 @@ +<%= render :partial => "test/json_change_priority", formats: :json %> +HTML Template, but <%= render :partial => "test/changing_priority" %> partial
\ No newline at end of file diff --git a/actionview/test/fixtures/test/dont_pick_me b/actionview/test/fixtures/test/dont_pick_me new file mode 100644 index 0000000000..0157c9e503 --- /dev/null +++ b/actionview/test/fixtures/test/dont_pick_me @@ -0,0 +1 @@ +non-template file
\ No newline at end of file diff --git a/actionview/test/fixtures/test/dot.directory/render_file_with_ivar.erb b/actionview/test/fixtures/test/dot.directory/render_file_with_ivar.erb new file mode 100644 index 0000000000..8b8a449236 --- /dev/null +++ b/actionview/test/fixtures/test/dot.directory/render_file_with_ivar.erb @@ -0,0 +1 @@ +The secret is <%= @secret %> diff --git a/actionview/test/fixtures/test/greeting.xml.erb b/actionview/test/fixtures/test/greeting.xml.erb new file mode 100644 index 0000000000..62fb0293f0 --- /dev/null +++ b/actionview/test/fixtures/test/greeting.xml.erb @@ -0,0 +1 @@ +<p>This is grand!</p> diff --git a/actionview/test/fixtures/test/hello.builder b/actionview/test/fixtures/test/hello.builder new file mode 100644 index 0000000000..a471553941 --- /dev/null +++ b/actionview/test/fixtures/test/hello.builder @@ -0,0 +1,4 @@ +xml.html do + xml.p "Hello #{@name}" + xml << render(:file => "test/greeting") +end
\ No newline at end of file diff --git a/actionview/test/fixtures/test/hello/hello.erb b/actionview/test/fixtures/test/hello/hello.erb new file mode 100644 index 0000000000..6769dd60bd --- /dev/null +++ b/actionview/test/fixtures/test/hello/hello.erb @@ -0,0 +1 @@ +Hello world!
\ No newline at end of file diff --git a/actionview/test/fixtures/test/hello_world.da.html.erb b/actionview/test/fixtures/test/hello_world.da.html.erb new file mode 100644 index 0000000000..10ec443291 --- /dev/null +++ b/actionview/test/fixtures/test/hello_world.da.html.erb @@ -0,0 +1 @@ +Hey verden
\ No newline at end of file diff --git a/actionview/test/fixtures/test/hello_world.erb b/actionview/test/fixtures/test/hello_world.erb new file mode 100644 index 0000000000..6769dd60bd --- /dev/null +++ b/actionview/test/fixtures/test/hello_world.erb @@ -0,0 +1 @@ +Hello world!
\ No newline at end of file diff --git a/actionview/test/fixtures/test/hello_world.erb~ b/actionview/test/fixtures/test/hello_world.erb~ new file mode 100644 index 0000000000..21934a1c95 --- /dev/null +++ b/actionview/test/fixtures/test/hello_world.erb~ @@ -0,0 +1 @@ +Don't pick me!
\ No newline at end of file diff --git a/actionview/test/fixtures/test/hello_world.pt-BR.html.erb b/actionview/test/fixtures/test/hello_world.pt-BR.html.erb new file mode 100644 index 0000000000..773b3c8c6e --- /dev/null +++ b/actionview/test/fixtures/test/hello_world.pt-BR.html.erb @@ -0,0 +1 @@ +Ola mundo
\ No newline at end of file diff --git a/actionview/test/fixtures/test/hello_world_with_partial.html.erb b/actionview/test/fixtures/test/hello_world_with_partial.html.erb new file mode 100644 index 0000000000..ec31545356 --- /dev/null +++ b/actionview/test/fixtures/test/hello_world_with_partial.html.erb @@ -0,0 +1,2 @@ +Hello world! +<%= render '/test/partial' %> diff --git a/actionview/test/fixtures/test/html_template.html.erb b/actionview/test/fixtures/test/html_template.html.erb new file mode 100644 index 0000000000..1bbc2b7f09 --- /dev/null +++ b/actionview/test/fixtures/test/html_template.html.erb @@ -0,0 +1 @@ +<%= render :partial => "test/first_json_partial", formats: :json %>
\ No newline at end of file diff --git a/actionview/test/fixtures/test/layout_render_file.erb b/actionview/test/fixtures/test/layout_render_file.erb new file mode 100644 index 0000000000..2f8e921c5f --- /dev/null +++ b/actionview/test/fixtures/test/layout_render_file.erb @@ -0,0 +1,2 @@ +<% content_for :title do %>title<% end -%> +<%= render :file => 'layouts/yield' -%>
\ No newline at end of file diff --git a/actionview/test/fixtures/test/layout_render_object.erb b/actionview/test/fixtures/test/layout_render_object.erb new file mode 100644 index 0000000000..acc4453c08 --- /dev/null +++ b/actionview/test/fixtures/test/layout_render_object.erb @@ -0,0 +1 @@ +<%= render :layout => "layouts/customers" do |customer| %><%= customer.name %><% end %>
\ No newline at end of file diff --git a/actionview/test/fixtures/test/list.erb b/actionview/test/fixtures/test/list.erb new file mode 100644 index 0000000000..0a4bda58ee --- /dev/null +++ b/actionview/test/fixtures/test/list.erb @@ -0,0 +1 @@ +<%= @test_unchanged = 'goodbye' %><%= render :partial => 'customer', :collection => @customers %><%= @test_unchanged %> diff --git a/actionview/test/fixtures/test/malformed/malformed.en.html.erb~ b/actionview/test/fixtures/test/malformed/malformed.en.html.erb~ new file mode 100644 index 0000000000..d009950384 --- /dev/null +++ b/actionview/test/fixtures/test/malformed/malformed.en.html.erb~ @@ -0,0 +1 @@ +Don't render me!
\ No newline at end of file diff --git a/actionview/test/fixtures/test/malformed/malformed.erb~ b/actionview/test/fixtures/test/malformed/malformed.erb~ new file mode 100644 index 0000000000..d009950384 --- /dev/null +++ b/actionview/test/fixtures/test/malformed/malformed.erb~ @@ -0,0 +1 @@ +Don't render me!
\ No newline at end of file diff --git a/actionview/test/fixtures/test/malformed/malformed.html.erb~ b/actionview/test/fixtures/test/malformed/malformed.html.erb~ new file mode 100644 index 0000000000..d009950384 --- /dev/null +++ b/actionview/test/fixtures/test/malformed/malformed.html.erb~ @@ -0,0 +1 @@ +Don't render me!
\ No newline at end of file diff --git a/actionview/test/fixtures/test/malformed/malformed~ b/actionview/test/fixtures/test/malformed/malformed~ new file mode 100644 index 0000000000..d009950384 --- /dev/null +++ b/actionview/test/fixtures/test/malformed/malformed~ @@ -0,0 +1 @@ +Don't render me!
\ No newline at end of file diff --git a/actionview/test/fixtures/test/nested_layout.erb b/actionview/test/fixtures/test/nested_layout.erb new file mode 100644 index 0000000000..6078f74b4c --- /dev/null +++ b/actionview/test/fixtures/test/nested_layout.erb @@ -0,0 +1,3 @@ +<% content_for :title, "title" -%> +<% content_for :column do -%>column<% end -%> +<%= render :layout => 'layouts/column' do -%>content<% end -%>
\ No newline at end of file diff --git a/actionview/test/fixtures/test/nested_streaming.erb b/actionview/test/fixtures/test/nested_streaming.erb new file mode 100644 index 0000000000..55525e0c92 --- /dev/null +++ b/actionview/test/fixtures/test/nested_streaming.erb @@ -0,0 +1,3 @@ +<%- content_for :header do -%>?<%- end -%> +<%= render :template => "test/streaming" %> +?
\ No newline at end of file diff --git a/actionview/test/fixtures/test/one.html.erb b/actionview/test/fixtures/test/one.html.erb new file mode 100644 index 0000000000..0151874809 --- /dev/null +++ b/actionview/test/fixtures/test/one.html.erb @@ -0,0 +1 @@ +<%= render :partial => "test/two" %> world
\ No newline at end of file diff --git a/actionview/test/fixtures/test/render_file_with_ivar.erb b/actionview/test/fixtures/test/render_file_with_ivar.erb new file mode 100644 index 0000000000..8b8a449236 --- /dev/null +++ b/actionview/test/fixtures/test/render_file_with_ivar.erb @@ -0,0 +1 @@ +The secret is <%= @secret %> diff --git a/actionview/test/fixtures/test/render_file_with_locals.erb b/actionview/test/fixtures/test/render_file_with_locals.erb new file mode 100644 index 0000000000..ebe09faee6 --- /dev/null +++ b/actionview/test/fixtures/test/render_file_with_locals.erb @@ -0,0 +1 @@ +The secret is <%= secret %> diff --git a/actionview/test/fixtures/test/render_file_with_locals_and_default.erb b/actionview/test/fixtures/test/render_file_with_locals_and_default.erb new file mode 100644 index 0000000000..9b4900acc5 --- /dev/null +++ b/actionview/test/fixtures/test/render_file_with_locals_and_default.erb @@ -0,0 +1 @@ +<%= secret ||= 'one' %>
\ No newline at end of file diff --git a/actionview/test/fixtures/test/render_partial_inside_directory.html.erb b/actionview/test/fixtures/test/render_partial_inside_directory.html.erb new file mode 100644 index 0000000000..1461b95186 --- /dev/null +++ b/actionview/test/fixtures/test/render_partial_inside_directory.html.erb @@ -0,0 +1 @@ +<%= render partial: 'test/_directory/partial_with_locales', locals: {'name' => 'Jane'} %> diff --git a/actionview/test/fixtures/test/render_two_partials.html.erb b/actionview/test/fixtures/test/render_two_partials.html.erb new file mode 100644 index 0000000000..3db6025860 --- /dev/null +++ b/actionview/test/fixtures/test/render_two_partials.html.erb @@ -0,0 +1,2 @@ +<%= render :partial => 'partial', :locals => {'first' => '1'} %> +<%= render :partial => 'partial', :locals => {'second' => '2'} %> diff --git a/actionview/test/fixtures/test/streaming.erb b/actionview/test/fixtures/test/streaming.erb new file mode 100644 index 0000000000..fb9b8b1ade --- /dev/null +++ b/actionview/test/fixtures/test/streaming.erb @@ -0,0 +1,3 @@ +<%- provide :header do -%>Yes, <%- end -%> +this works +<%- content_for :footer, " like a charm" -%> diff --git a/actionview/test/fixtures/test/streaming_buster.erb b/actionview/test/fixtures/test/streaming_buster.erb new file mode 100644 index 0000000000..4221d56dcc --- /dev/null +++ b/actionview/test/fixtures/test/streaming_buster.erb @@ -0,0 +1,3 @@ +<%= yield :foo -%> +This won't look +<% provide :unknown, " good." -%> diff --git a/actionview/test/fixtures/test/sub_template_raise.html.erb b/actionview/test/fixtures/test/sub_template_raise.html.erb new file mode 100644 index 0000000000..f38c0bda90 --- /dev/null +++ b/actionview/test/fixtures/test/sub_template_raise.html.erb @@ -0,0 +1 @@ +<%= render :partial => "test/raise" %>
\ No newline at end of file diff --git a/actionview/test/fixtures/test/template.erb b/actionview/test/fixtures/test/template.erb new file mode 100644 index 0000000000..785afa8f6a --- /dev/null +++ b/actionview/test/fixtures/test/template.erb @@ -0,0 +1 @@ +<%= template.path %>
\ No newline at end of file diff --git a/actionview/test/fixtures/test/update_element_with_capture.erb b/actionview/test/fixtures/test/update_element_with_capture.erb new file mode 100644 index 0000000000..fa3ef200f9 --- /dev/null +++ b/actionview/test/fixtures/test/update_element_with_capture.erb @@ -0,0 +1,9 @@ +<% replacement_function = update_element_function("products", :action => :update) do %> + <p>Product 1</p> + <p>Product 2</p> +<% end %> +<%= javascript_tag(replacement_function) %> + +<% update_element_function("status", :action => :update, :binding => binding) do %> + <b>You bought something!</b> +<% end %> diff --git a/actionview/test/fixtures/test/utf8.html.erb b/actionview/test/fixtures/test/utf8.html.erb new file mode 100644 index 0000000000..ac98c2f012 --- /dev/null +++ b/actionview/test/fixtures/test/utf8.html.erb @@ -0,0 +1,4 @@ +Русский <%= render :partial => 'test/utf8_partial' %> +<%= "日".encoding %> +<%= @output_buffer.encoding %> +<%= __ENCODING__ %> diff --git a/actionview/test/fixtures/test/utf8_magic.html.erb b/actionview/test/fixtures/test/utf8_magic.html.erb new file mode 100644 index 0000000000..257279c29f --- /dev/null +++ b/actionview/test/fixtures/test/utf8_magic.html.erb @@ -0,0 +1,5 @@ +<%# encoding: utf-8 -%> +Русский <%= render :partial => 'test/utf8_partial_magic' %> +<%= "日".encoding %> +<%= @output_buffer.encoding %> +<%= __ENCODING__ %> diff --git a/actionview/test/fixtures/test/utf8_magic_with_bare_partial.html.erb b/actionview/test/fixtures/test/utf8_magic_with_bare_partial.html.erb new file mode 100644 index 0000000000..cb22692f9a --- /dev/null +++ b/actionview/test/fixtures/test/utf8_magic_with_bare_partial.html.erb @@ -0,0 +1,5 @@ +<%# encoding: utf-8 -%> +Русский <%= render :partial => 'test/utf8_partial' %> +<%= "日".encoding %> +<%= @output_buffer.encoding %> +<%= __ENCODING__ %> diff --git a/actionview/test/fixtures/topic.rb b/actionview/test/fixtures/topic.rb new file mode 100644 index 0000000000..9fa9746535 --- /dev/null +++ b/actionview/test/fixtures/topic.rb @@ -0,0 +1,3 @@ +class Topic < ActiveRecord::Base + has_many :replies, :dependent => :destroy +end diff --git a/actionview/test/fixtures/topics.yml b/actionview/test/fixtures/topics.yml new file mode 100644 index 0000000000..7fdd49d54e --- /dev/null +++ b/actionview/test/fixtures/topics.yml @@ -0,0 +1,22 @@ +futurama: + id: 1 + title: Isnt futurama awesome? + subtitle: It really is, isnt it. + content: I like futurama + created_at: <%= 1.day.ago.to_s(:db) %> + updated_at: + +harvey_birdman: + id: 2 + title: Harvey Birdman is the king of all men + subtitle: yup + content: It really is + created_at: <%= 2.hours.ago.to_s(:db) %> + updated_at: + +rails: + id: 3 + title: Rails is nice + subtitle: It makes me happy + content: except when I have to hack internals to fix pagination. even then really. + created_at: <%= 20.minutes.ago.to_s(:db) %> diff --git a/actionview/test/fixtures/topics/_topic.html.erb b/actionview/test/fixtures/topics/_topic.html.erb new file mode 100644 index 0000000000..98659ca098 --- /dev/null +++ b/actionview/test/fixtures/topics/_topic.html.erb @@ -0,0 +1 @@ +<%= topic.title %>
\ No newline at end of file diff --git a/actionview/test/fixtures/translations/templates/array.erb b/actionview/test/fixtures/translations/templates/array.erb new file mode 100644 index 0000000000..d86045a172 --- /dev/null +++ b/actionview/test/fixtures/translations/templates/array.erb @@ -0,0 +1 @@ +<%= t('.foo.bar') %> diff --git a/actionview/test/fixtures/translations/templates/default.erb b/actionview/test/fixtures/translations/templates/default.erb new file mode 100644 index 0000000000..8b70031071 --- /dev/null +++ b/actionview/test/fixtures/translations/templates/default.erb @@ -0,0 +1 @@ +<%= t('.missing', :default => :'.foo') %> diff --git a/actionview/test/fixtures/translations/templates/found.erb b/actionview/test/fixtures/translations/templates/found.erb new file mode 100644 index 0000000000..080c9c0aee --- /dev/null +++ b/actionview/test/fixtures/translations/templates/found.erb @@ -0,0 +1 @@ +<%= t('.foo') %> diff --git a/actionview/test/fixtures/translations/templates/missing.erb b/actionview/test/fixtures/translations/templates/missing.erb new file mode 100644 index 0000000000..0f3f17f8ef --- /dev/null +++ b/actionview/test/fixtures/translations/templates/missing.erb @@ -0,0 +1 @@ +<%= t('.missing') %> diff --git a/actionview/test/fixtures/with_format.json.erb b/actionview/test/fixtures/with_format.json.erb new file mode 100644 index 0000000000..a7f480ab1d --- /dev/null +++ b/actionview/test/fixtures/with_format.json.erb @@ -0,0 +1 @@ +<%= render :partial => 'missing', :formats => [:json] %> diff --git a/actionview/test/lib/controller/fake_controllers.rb b/actionview/test/lib/controller/fake_controllers.rb new file mode 100644 index 0000000000..1a2863b689 --- /dev/null +++ b/actionview/test/lib/controller/fake_controllers.rb @@ -0,0 +1,35 @@ +class ContentController < ActionController::Base; end + +module Admin + class AccountsController < ActionController::Base; end + class PostsController < ActionController::Base; end + class StuffController < ActionController::Base; end + class UserController < ActionController::Base; end + class UsersController < ActionController::Base; end +end + +module Api + class UsersController < ActionController::Base; end + class ProductsController < ActionController::Base; end +end + +class AccountController < ActionController::Base; end +class ArchiveController < ActionController::Base; end +class ArticlesController < ActionController::Base; end +class BarController < ActionController::Base; end +class BlogController < ActionController::Base; end +class BooksController < ActionController::Base; end +class CarsController < ActionController::Base; end +class CcController < ActionController::Base; end +class CController < ActionController::Base; end +class FooController < ActionController::Base; end +class GeocodeController < ActionController::Base; end +class NewsController < ActionController::Base; end +class NotesController < ActionController::Base; end +class PagesController < ActionController::Base; end +class PeopleController < ActionController::Base; end +class PostsController < ActionController::Base; end +class SubpathBooksController < ActionController::Base; end +class SymbolsController < ActionController::Base; end +class UserController < ActionController::Base; end +class UsersController < ActionController::Base; end diff --git a/actionview/test/lib/controller/fake_models.rb b/actionview/test/lib/controller/fake_models.rb new file mode 100644 index 0000000000..a463a08bb6 --- /dev/null +++ b/actionview/test/lib/controller/fake_models.rb @@ -0,0 +1,185 @@ +require "active_model" + +class Customer < Struct.new(:name, :id) + extend ActiveModel::Naming + include ActiveModel::Conversion + + undef_method :to_json + + def to_xml(options={}) + if options[:builder] + options[:builder].name name + else + "<name>#{name}</name>" + end + end + + def to_js(options={}) + "name: #{name.inspect}" + end + alias :to_text :to_js + + def errors + [] + end + + def persisted? + id.present? + end +end + +class GoodCustomer < Customer +end + +class Post < Struct.new(:title, :author_name, :body, :secret, :persisted, :written_on, :cost) + extend ActiveModel::Naming + include ActiveModel::Conversion + extend ActiveModel::Translation + + alias_method :secret?, :secret + alias_method :persisted?, :persisted + + def initialize(*args) + super + @persisted = false + end + + attr_accessor :author + def author_attributes=(attributes); end + + attr_accessor :comments, :comment_ids + def comments_attributes=(attributes); end + + attr_accessor :tags + def tags_attributes=(attributes); end +end + +class Comment + extend ActiveModel::Naming + include ActiveModel::Conversion + + attr_reader :id + attr_reader :post_id + def initialize(id = nil, post_id = nil); @id, @post_id = id, post_id end + def to_key; id ? [id] : nil end + def save; @id = 1; @post_id = 1 end + def persisted?; @id.present? end + def to_param; @id.to_s; end + def name + @id.nil? ? "new #{self.class.name.downcase}" : "#{self.class.name.downcase} ##{@id}" + end + + attr_accessor :relevances + def relevances_attributes=(attributes); end + + attr_accessor :body +end + +class Tag + extend ActiveModel::Naming + include ActiveModel::Conversion + + attr_reader :id + attr_reader :post_id + def initialize(id = nil, post_id = nil); @id, @post_id = id, post_id end + def to_key; id ? [id] : nil end + def save; @id = 1; @post_id = 1 end + def persisted?; @id.present? end + def to_param; @id; end + def value + @id.nil? ? "new #{self.class.name.downcase}" : "#{self.class.name.downcase} ##{@id}" + end + + attr_accessor :relevances + def relevances_attributes=(attributes); end + +end + +class CommentRelevance + extend ActiveModel::Naming + include ActiveModel::Conversion + + attr_reader :id + attr_reader :comment_id + def initialize(id = nil, comment_id = nil); @id, @comment_id = id, comment_id end + def to_key; id ? [id] : nil end + def save; @id = 1; @comment_id = 1 end + def persisted?; @id.present? end + def to_param; @id; end + def value + @id.nil? ? "new #{self.class.name.downcase}" : "#{self.class.name.downcase} ##{@id}" + 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 + + attr_reader :id + attr_reader :tag_id + def initialize(id = nil, tag_id = nil); @id, @tag_id = id, tag_id end + def to_key; id ? [id] : nil end + def save; @id = 1; @tag_id = 1 end + def persisted?; @id.present? end + def to_param; @id; end + def value + @id.nil? ? "new #{self.class.name.downcase}" : "#{self.class.name.downcase} ##{@id}" + end +end + +class Author < Comment + attr_accessor :post + def post_attributes=(attributes); end +end + +class HashBackedAuthor < Hash + extend ActiveModel::Naming + include ActiveModel::Conversion + + def persisted?; false; end + + def name + "hash backed author" + end +end + +module Blog + def self.use_relative_model_naming? + true + end + + class Post < Struct.new(:title, :id) + extend ActiveModel::Naming + include ActiveModel::Conversion + + def persisted? + id.present? + end + end +end + +class ArelLike + def to_ary + true + end + def each + a = Array.new(2) { |id| Comment.new(id + 1) } + a.each { |i| yield i } + end +end + +class Car < Struct.new(:color) +end diff --git a/actionview/test/template/active_model_helper_test.rb b/actionview/test/template/active_model_helper_test.rb new file mode 100644 index 0000000000..86bccdfade --- /dev/null +++ b/actionview/test/template/active_model_helper_test.rb @@ -0,0 +1,99 @@ +require 'abstract_unit' + +class ActiveModelHelperTest < ActionView::TestCase + tests ActionView::Helpers::ActiveModelHelper + + silence_warnings do + class Post < Struct.new(:author_name, :body, :updated_at) + include ActiveModel::Conversion + include ActiveModel::Validations + + def persisted? + false + end + end + end + + def setup + super + + @post = Post.new + @post.errors[:author_name] << "can't be empty" + @post.errors[:body] << "foo" + @post.errors[:updated_at] << "bar" + + @post.author_name = "" + @post.body = "Back to the hill and over it again!" + @post.updated_at = Date.new(2004, 6, 15) + end + + def test_text_area_with_errors + assert_dom_equal( + %(<div class="field_with_errors"><textarea id="post_body" name="post[body]">\nBack to the hill and over it again!</textarea></div>), + text_area("post", "body") + ) + end + + def test_text_field_with_errors + assert_dom_equal( + %(<div class="field_with_errors"><input id="post_author_name" name="post[author_name]" type="text" value="" /></div>), + text_field("post", "author_name") + ) + end + + def test_select_with_errors + assert_dom_equal( + %(<div class="field_with_errors"><select name="post[author_name]" id="post_author_name"><option value="a">a</option>\n<option value="b">b</option></select></div>), + select("post", "author_name", [:a, :b]) + ) + end + + def test_select_with_errors_and_blank_option + expected_dom = %(<div class="field_with_errors"><select name="post[author_name]" id="post_author_name"><option value="">Choose one...</option>\n<option value="a">a</option>\n<option value="b">b</option></select></div>) + assert_dom_equal(expected_dom, select("post", "author_name", [:a, :b], :include_blank => 'Choose one...')) + assert_dom_equal(expected_dom, select("post", "author_name", [:a, :b], :prompt => 'Choose one...')) + end + + def test_date_select_with_errors + assert_dom_equal( + %(<div class="field_with_errors"><select id="post_updated_at_1i" name="post[updated_at(1i)]">\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n</select>\n<input id="post_updated_at_2i" name="post[updated_at(2i)]" type="hidden" value="6" />\n<input id="post_updated_at_3i" name="post[updated_at(3i)]" type="hidden" value="1" />\n</div>), + date_select("post", "updated_at", :discard_month => true, :discard_day => true, :start_year => 2004, :end_year => 2005) + ) + end + + def test_datetime_select_with_errors + assert_dom_equal( + %(<div class="field_with_errors"><input id="post_updated_at_1i" name="post[updated_at(1i)]" type="hidden" value="2004" />\n<input id="post_updated_at_2i" name="post[updated_at(2i)]" type="hidden" value="6" />\n<input id="post_updated_at_3i" name="post[updated_at(3i)]" type="hidden" value="1" />\n<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n<option selected="selected" 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</select>\n : <select id="post_updated_at_5i" name="post[updated_at(5i)]">\n<option selected="selected" value="00">00</option>\n</select>\n</div>), + datetime_select("post", "updated_at", :discard_year => true, :discard_month => true, :discard_day => true, :minute_step => 60) + ) + end + + def test_time_select_with_errors + assert_dom_equal( + %(<div class="field_with_errors"><input id="post_updated_at_1i" name="post[updated_at(1i)]" type="hidden" value="2004" />\n<input id="post_updated_at_2i" name="post[updated_at(2i)]" type="hidden" value="6" />\n<input id="post_updated_at_3i" name="post[updated_at(3i)]" type="hidden" value="15" />\n<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n<option selected="selected" 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</select>\n : <select id="post_updated_at_5i" name="post[updated_at(5i)]">\n<option selected="selected" value="00">00</option>\n</select>\n</div>), + time_select("post", "updated_at", :minute_step => 60) + ) + end + + def test_hidden_field_does_not_render_errors + assert_dom_equal( + %(<input id="post_author_name" name="post[author_name]" type="hidden" value="" />), + hidden_field("post", "author_name") + ) + end + + def test_field_error_proc + old_proc = ActionView::Base.field_error_proc + ActionView::Base.field_error_proc = Proc.new do |html_tag, instance| + %(<div class=\"field_with_errors\">#{html_tag} <span class="error">#{[instance.error_message].join(', ')}</span></div>).html_safe + end + + assert_dom_equal( + %(<div class="field_with_errors"><input id="post_author_name" name="post[author_name]" type="text" value="" /> <span class="error">can't be empty</span></div>), + text_field("post", "author_name") + ) + ensure + ActionView::Base.field_error_proc = old_proc if old_proc + end + +end diff --git a/actionview/test/template/asset_tag_helper_test.rb b/actionview/test/template/asset_tag_helper_test.rb new file mode 100644 index 0000000000..214a13654e --- /dev/null +++ b/actionview/test/template/asset_tag_helper_test.rb @@ -0,0 +1,762 @@ +require 'zlib' +require 'abstract_unit' +require 'active_support/ordered_options' + +class FakeController + attr_accessor :request + + def config + @config ||= ActiveSupport::InheritableOptions.new(ActionController::Base.config) + end +end + +class AssetTagHelperTest < ActionView::TestCase + tests ActionView::Helpers::AssetTagHelper + + attr_reader :request + + def setup + super + + @controller = BasicController.new + + @request = Class.new do + attr_accessor :script_name + def protocol() 'http://' end + def ssl?() false end + def host_with_port() 'localhost' end + def base_url() 'http://www.example.com' end + end.new + + @controller.request = @request + end + + def url_for(*args) + "http://www.example.com" + end + + AssetPathToTag = { + %(asset_path("foo")) => %(/foo), + %(asset_path("style.css")) => %(/style.css), + %(asset_path("xmlhr.js")) => %(/xmlhr.js), + %(asset_path("xml.png")) => %(/xml.png), + %(asset_path("dir/xml.png")) => %(/dir/xml.png), + %(asset_path("/dir/xml.png")) => %(/dir/xml.png), + + %(asset_path("script.min")) => %(/script.min), + %(asset_path("script.min.js")) => %(/script.min.js), + %(asset_path("style.min")) => %(/style.min), + %(asset_path("style.min.css")) => %(/style.min.css), + + %(asset_path("http://www.outside.com/image.jpg")) => %(http://www.outside.com/image.jpg), + %(asset_path("HTTP://www.outside.com/image.jpg")) => %(HTTP://www.outside.com/image.jpg), + + %(asset_path("style", type: :stylesheet)) => %(/stylesheets/style.css), + %(asset_path("xmlhr", type: :javascript)) => %(/javascripts/xmlhr.js), + %(asset_path("xml.png", type: :image)) => %(/images/xml.png) + } + + AutoDiscoveryToTag = { + %(auto_discovery_link_tag) => %(<link href="http://www.example.com" rel="alternate" title="RSS" type="application/rss+xml" />), + %(auto_discovery_link_tag(:rss)) => %(<link href="http://www.example.com" rel="alternate" title="RSS" type="application/rss+xml" />), + %(auto_discovery_link_tag(:atom)) => %(<link href="http://www.example.com" rel="alternate" title="ATOM" type="application/atom+xml" />), + %(auto_discovery_link_tag(:rss, :action => "feed")) => %(<link href="http://www.example.com" rel="alternate" title="RSS" type="application/rss+xml" />), + %(auto_discovery_link_tag(:rss, "http://localhost/feed")) => %(<link href="http://localhost/feed" rel="alternate" title="RSS" type="application/rss+xml" />), + %(auto_discovery_link_tag(:rss, "//localhost/feed")) => %(<link href="//localhost/feed" rel="alternate" title="RSS" type="application/rss+xml" />), + %(auto_discovery_link_tag(:rss, {:action => "feed"}, {:title => "My RSS"})) => %(<link href="http://www.example.com" rel="alternate" title="My RSS" type="application/rss+xml" />), + %(auto_discovery_link_tag(:rss, {}, {:title => "My RSS"})) => %(<link href="http://www.example.com" rel="alternate" title="My RSS" type="application/rss+xml" />), + %(auto_discovery_link_tag(nil, {}, {:type => "text/html"})) => %(<link href="http://www.example.com" rel="alternate" title="" type="text/html" />), + %(auto_discovery_link_tag(nil, {}, {:title => "No stream.. really", :type => "text/html"})) => %(<link href="http://www.example.com" rel="alternate" title="No stream.. really" type="text/html" />), + %(auto_discovery_link_tag(:rss, {}, {:title => "My RSS", :type => "text/html"})) => %(<link href="http://www.example.com" rel="alternate" title="My RSS" type="text/html" />), + %(auto_discovery_link_tag(:atom, {}, {:rel => "Not so alternate"})) => %(<link href="http://www.example.com" rel="Not so alternate" title="ATOM" type="application/atom+xml" />), + } + + JavascriptPathToTag = { + %(javascript_path("xmlhr")) => %(/javascripts/xmlhr.js), + %(javascript_path("super/xmlhr")) => %(/javascripts/super/xmlhr.js), + %(javascript_path("/super/xmlhr.js")) => %(/super/xmlhr.js), + %(javascript_path("xmlhr.min")) => %(/javascripts/xmlhr.min.js), + %(javascript_path("xmlhr.min.js")) => %(/javascripts/xmlhr.min.js), + + %(javascript_path("xmlhr.js?123")) => %(/javascripts/xmlhr.js?123), + %(javascript_path("xmlhr.js?body=1")) => %(/javascripts/xmlhr.js?body=1), + %(javascript_path("xmlhr.js#hash")) => %(/javascripts/xmlhr.js#hash), + %(javascript_path("xmlhr.js?123#hash")) => %(/javascripts/xmlhr.js?123#hash) + } + + PathToJavascriptToTag = { + %(path_to_javascript("xmlhr")) => %(/javascripts/xmlhr.js), + %(path_to_javascript("super/xmlhr")) => %(/javascripts/super/xmlhr.js), + %(path_to_javascript("/super/xmlhr.js")) => %(/super/xmlhr.js) + } + + JavascriptUrlToTag = { + %(javascript_url("xmlhr")) => %(http://www.example.com/javascripts/xmlhr.js), + %(javascript_url("super/xmlhr")) => %(http://www.example.com/javascripts/super/xmlhr.js), + %(javascript_url("/super/xmlhr.js")) => %(http://www.example.com/super/xmlhr.js) + } + + UrlToJavascriptToTag = { + %(url_to_javascript("xmlhr")) => %(http://www.example.com/javascripts/xmlhr.js), + %(url_to_javascript("super/xmlhr")) => %(http://www.example.com/javascripts/super/xmlhr.js), + %(url_to_javascript("/super/xmlhr.js")) => %(http://www.example.com/super/xmlhr.js) + } + + JavascriptIncludeToTag = { + %(javascript_include_tag("bank")) => %(<script src="/javascripts/bank.js" ></script>), + %(javascript_include_tag("bank.js")) => %(<script src="/javascripts/bank.js" ></script>), + %(javascript_include_tag("bank", :lang => "vbscript")) => %(<script lang="vbscript" src="/javascripts/bank.js" ></script>), + + %(javascript_include_tag("http://example.com/all")) => %(<script src="http://example.com/all"></script>), + %(javascript_include_tag("http://example.com/all.js")) => %(<script src="http://example.com/all.js"></script>), + %(javascript_include_tag("//example.com/all.js")) => %(<script src="//example.com/all.js"></script>), + } + + StylePathToTag = { + %(stylesheet_path("bank")) => %(/stylesheets/bank.css), + %(stylesheet_path("bank.css")) => %(/stylesheets/bank.css), + %(stylesheet_path('subdir/subdir')) => %(/stylesheets/subdir/subdir.css), + %(stylesheet_path('/subdir/subdir.css')) => %(/subdir/subdir.css), + %(stylesheet_path("style.min")) => %(/stylesheets/style.min.css), + %(stylesheet_path("style.min.css")) => %(/stylesheets/style.min.css) + } + + PathToStyleToTag = { + %(path_to_stylesheet("style")) => %(/stylesheets/style.css), + %(path_to_stylesheet("style.css")) => %(/stylesheets/style.css), + %(path_to_stylesheet('dir/file')) => %(/stylesheets/dir/file.css), + %(path_to_stylesheet('/dir/file.rcss', :extname => false)) => %(/dir/file.rcss), + %(path_to_stylesheet('/dir/file', :extname => '.rcss')) => %(/dir/file.rcss) + } + + StyleUrlToTag = { + %(stylesheet_url("bank")) => %(http://www.example.com/stylesheets/bank.css), + %(stylesheet_url("bank.css")) => %(http://www.example.com/stylesheets/bank.css), + %(stylesheet_url('subdir/subdir')) => %(http://www.example.com/stylesheets/subdir/subdir.css), + %(stylesheet_url('/subdir/subdir.css')) => %(http://www.example.com/subdir/subdir.css) + } + + UrlToStyleToTag = { + %(url_to_stylesheet("style")) => %(http://www.example.com/stylesheets/style.css), + %(url_to_stylesheet("style.css")) => %(http://www.example.com/stylesheets/style.css), + %(url_to_stylesheet('dir/file')) => %(http://www.example.com/stylesheets/dir/file.css), + %(url_to_stylesheet('/dir/file.rcss', :extname => false)) => %(http://www.example.com/dir/file.rcss), + %(url_to_stylesheet('/dir/file', :extname => '.rcss')) => %(http://www.example.com/dir/file.rcss) + } + + StyleLinkToTag = { + %(stylesheet_link_tag("bank")) => %(<link href="/stylesheets/bank.css" media="screen" rel="stylesheet" />), + %(stylesheet_link_tag("bank.css")) => %(<link href="/stylesheets/bank.css" media="screen" rel="stylesheet" />), + %(stylesheet_link_tag("/elsewhere/file")) => %(<link href="/elsewhere/file.css" media="screen" rel="stylesheet" />), + %(stylesheet_link_tag("subdir/subdir")) => %(<link href="/stylesheets/subdir/subdir.css" media="screen" rel="stylesheet" />), + %(stylesheet_link_tag("bank", :media => "all")) => %(<link href="/stylesheets/bank.css" media="all" rel="stylesheet" />), + + %(stylesheet_link_tag("http://www.example.com/styles/style")) => %(<link href="http://www.example.com/styles/style" media="screen" rel="stylesheet" />), + %(stylesheet_link_tag("http://www.example.com/styles/style.css")) => %(<link href="http://www.example.com/styles/style.css" media="screen" rel="stylesheet" />), + %(stylesheet_link_tag("//www.example.com/styles/style.css")) => %(<link href="//www.example.com/styles/style.css" media="screen" rel="stylesheet" />), + } + + ImagePathToTag = { + %(image_path("xml")) => %(/images/xml), + %(image_path("xml.png")) => %(/images/xml.png), + %(image_path("dir/xml.png")) => %(/images/dir/xml.png), + %(image_path("/dir/xml.png")) => %(/dir/xml.png) + } + + PathToImageToTag = { + %(path_to_image("xml")) => %(/images/xml), + %(path_to_image("xml.png")) => %(/images/xml.png), + %(path_to_image("dir/xml.png")) => %(/images/dir/xml.png), + %(path_to_image("/dir/xml.png")) => %(/dir/xml.png) + } + + ImageUrlToTag = { + %(image_url("xml")) => %(http://www.example.com/images/xml), + %(image_url("xml.png")) => %(http://www.example.com/images/xml.png), + %(image_url("dir/xml.png")) => %(http://www.example.com/images/dir/xml.png), + %(image_url("/dir/xml.png")) => %(http://www.example.com/dir/xml.png) + } + + UrlToImageToTag = { + %(url_to_image("xml")) => %(http://www.example.com/images/xml), + %(url_to_image("xml.png")) => %(http://www.example.com/images/xml.png), + %(url_to_image("dir/xml.png")) => %(http://www.example.com/images/dir/xml.png), + %(url_to_image("/dir/xml.png")) => %(http://www.example.com/dir/xml.png) + } + + ImageLinkToTag = { + %(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 => "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" />), + %(image_tag("error.png", "size" => "x")) => %(<img alt="Error" src="/images/error.png" />), + %(image_tag("google.com.png")) => %(<img alt="Google.com" src="/images/google.com.png" />), + %(image_tag("slash..png")) => %(<img alt="Slash." src="/images/slash..png" />), + %(image_tag(".pdf.png")) => %(<img alt=".pdf" src="/images/.pdf.png" />), + %(image_tag("http://www.rubyonrails.com/images/rails.png")) => %(<img alt="Rails" src="http://www.rubyonrails.com/images/rails.png" />), + %(image_tag("//www.rubyonrails.com/images/rails.png")) => %(<img alt="Rails" src="//www.rubyonrails.com/images/rails.png" />), + %(image_tag("mouse.png", :alt => nil)) => %(<img src="/images/mouse.png" />), + %(image_tag("data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==", :alt => nil)) => %(<img src="data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" />), + %(image_tag("")) => %(<img src="" />) + } + + FaviconLinkToTag = { + %(favicon_link_tag) => %(<link href="/images/favicon.ico" rel="shortcut icon" type="image/vnd.microsoft.icon" />), + %(favicon_link_tag 'favicon.ico') => %(<link href="/images/favicon.ico" rel="shortcut icon" type="image/vnd.microsoft.icon" />), + %(favicon_link_tag 'favicon.ico', :rel => 'foo') => %(<link href="/images/favicon.ico" rel="foo" type="image/vnd.microsoft.icon" />), + %(favicon_link_tag 'favicon.ico', :rel => 'foo', :type => 'bar') => %(<link href="/images/favicon.ico" rel="foo" type="bar" />), + %(favicon_link_tag 'mb-icon.png', :rel => 'apple-touch-icon', :type => 'image/png') => %(<link href="/images/mb-icon.png" rel="apple-touch-icon" type="image/png" />) + } + + VideoPathToTag = { + %(video_path("xml")) => %(/videos/xml), + %(video_path("xml.ogg")) => %(/videos/xml.ogg), + %(video_path("dir/xml.ogg")) => %(/videos/dir/xml.ogg), + %(video_path("/dir/xml.ogg")) => %(/dir/xml.ogg) + } + + PathToVideoToTag = { + %(path_to_video("xml")) => %(/videos/xml), + %(path_to_video("xml.ogg")) => %(/videos/xml.ogg), + %(path_to_video("dir/xml.ogg")) => %(/videos/dir/xml.ogg), + %(path_to_video("/dir/xml.ogg")) => %(/dir/xml.ogg) + } + + VideoUrlToTag = { + %(video_url("xml")) => %(http://www.example.com/videos/xml), + %(video_url("xml.ogg")) => %(http://www.example.com/videos/xml.ogg), + %(video_url("dir/xml.ogg")) => %(http://www.example.com/videos/dir/xml.ogg), + %(video_url("/dir/xml.ogg")) => %(http://www.example.com/dir/xml.ogg) + } + + UrlToVideoToTag = { + %(url_to_video("xml")) => %(http://www.example.com/videos/xml), + %(url_to_video("xml.ogg")) => %(http://www.example.com/videos/xml.ogg), + %(url_to_video("dir/xml.ogg")) => %(http://www.example.com/videos/dir/xml.ogg), + %(url_to_video("/dir/xml.ogg")) => %(http://www.example.com/dir/xml.ogg) + } + + VideoLinkToTag = { + %(video_tag("xml.ogg")) => %(<video src="/videos/xml.ogg"></video>), + %(video_tag("rss.m4v", :autoplay => true, :controls => true)) => %(<video autoplay="autoplay" controls="controls" src="/videos/rss.m4v"></video>), + %(video_tag("rss.m4v", :autobuffer => true)) => %(<video autobuffer="autobuffer" src="/videos/rss.m4v"></video>), + %(video_tag("gold.m4v", :size => "160x120")) => %(<video height="120" src="/videos/gold.m4v" width="160"></video>), + %(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 src="/videos/error.avi"></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>), + %(video_tag("//media.rubyonrails.org/video/rails_blog_2.mov")) => %(<video src="//media.rubyonrails.org/video/rails_blog_2.mov"></video>), + %(video_tag("multiple.ogg", "multiple.avi")) => %(<video><source src="/videos/multiple.ogg" /><source src="/videos/multiple.avi" /></video>), + %(video_tag(["multiple.ogg", "multiple.avi"])) => %(<video><source src="/videos/multiple.ogg" /><source src="/videos/multiple.avi" /></video>), + %(video_tag(["multiple.ogg", "multiple.avi"], :size => "160x120", :controls => true)) => %(<video controls="controls" height="120" width="160"><source src="/videos/multiple.ogg" /><source src="/videos/multiple.avi" /></video>) + } + + AudioPathToTag = { + %(audio_path("xml")) => %(/audios/xml), + %(audio_path("xml.wav")) => %(/audios/xml.wav), + %(audio_path("dir/xml.wav")) => %(/audios/dir/xml.wav), + %(audio_path("/dir/xml.wav")) => %(/dir/xml.wav) + } + + PathToAudioToTag = { + %(path_to_audio("xml")) => %(/audios/xml), + %(path_to_audio("xml.wav")) => %(/audios/xml.wav), + %(path_to_audio("dir/xml.wav")) => %(/audios/dir/xml.wav), + %(path_to_audio("/dir/xml.wav")) => %(/dir/xml.wav) + } + + AudioUrlToTag = { + %(audio_url("xml")) => %(http://www.example.com/audios/xml), + %(audio_url("xml.wav")) => %(http://www.example.com/audios/xml.wav), + %(audio_url("dir/xml.wav")) => %(http://www.example.com/audios/dir/xml.wav), + %(audio_url("/dir/xml.wav")) => %(http://www.example.com/dir/xml.wav) + } + + UrlToAudioToTag = { + %(url_to_audio("xml")) => %(http://www.example.com/audios/xml), + %(url_to_audio("xml.wav")) => %(http://www.example.com/audios/xml.wav), + %(url_to_audio("dir/xml.wav")) => %(http://www.example.com/audios/dir/xml.wav), + %(url_to_audio("/dir/xml.wav")) => %(http://www.example.com/dir/xml.wav) + } + + AudioLinkToTag = { + %(audio_tag("xml.wav")) => %(<audio src="/audios/xml.wav"></audio>), + %(audio_tag("rss.wav", :autoplay => true, :controls => true)) => %(<audio autoplay="autoplay" controls="controls" src="/audios/rss.wav"></audio>), + %(audio_tag("http://media.rubyonrails.org/audio/rails_blog_2.mov")) => %(<audio src="http://media.rubyonrails.org/audio/rails_blog_2.mov"></audio>), + %(audio_tag("//media.rubyonrails.org/audio/rails_blog_2.mov")) => %(<audio src="//media.rubyonrails.org/audio/rails_blog_2.mov"></audio>), + %(audio_tag("audio.mp3", "audio.ogg")) => %(<audio><source src="/audios/audio.mp3" /><source src="/audios/audio.ogg" /></audio>), + %(audio_tag(["audio.mp3", "audio.ogg"])) => %(<audio><source src="/audios/audio.mp3" /><source src="/audios/audio.ogg" /></audio>), + %(audio_tag(["audio.mp3", "audio.ogg"], :autobuffer => true, :controls => true)) => %(<audio autobuffer="autobuffer" controls="controls"><source src="/audios/audio.mp3" /><source src="/audios/audio.ogg" /></audio>) + } + + FontPathToTag = { + %(font_path("font.eot")) => %(/fonts/font.eot), + %(font_path("font.eot#iefix")) => %(/fonts/font.eot#iefix), + %(font_path("font.woff")) => %(/fonts/font.woff), + %(font_path("font.ttf")) => %(/fonts/font.ttf), + %(font_path("font.ttf?123")) => %(/fonts/font.ttf?123) + } + + def test_autodiscovery_link_tag_with_unknown_type_but_not_pass_type_option_key + assert_raise(ArgumentError) do + auto_discovery_link_tag(:xml) + end + end + + 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 + end + + def test_asset_path_tag + AssetPathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_compute_asset_public_path + assert_equal "/robots.txt", compute_asset_path("robots.txt") + assert_equal "/robots.txt", compute_asset_path("/robots.txt") + assert_equal "/javascripts/foo.js", compute_asset_path("foo.js", :type => :javascript) + assert_equal "/javascripts/foo.js", compute_asset_path("/foo.js", :type => :javascript) + assert_equal "/stylesheets/foo.css", compute_asset_path("foo.css", :type => :stylesheet) + end + + def test_auto_discovery_link_tag + AutoDiscoveryToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_javascript_path + JavascriptPathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_path_to_javascript_alias_for_javascript_path + PathToJavascriptToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_javascript_url + JavascriptUrlToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_url_to_javascript_alias_for_javascript_url + UrlToJavascriptToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_javascript_include_tag + JavascriptIncludeToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_javascript_include_tag_with_missing_source + assert_nothing_raised { + javascript_include_tag('missing_security_guard') + } + + assert_nothing_raised { + javascript_include_tag('http://example.com/css/missing_security_guard') + } + end + + def test_javascript_include_tag_is_html_safe + assert javascript_include_tag("prototype").html_safe? + end + + def test_javascript_include_tag_relative_protocol + @controller.config.asset_host = "assets.example.com" + assert_dom_equal %(<script src="//assets.example.com/javascripts/prototype.js"></script>), javascript_include_tag('prototype', protocol: :relative) + end + + def test_javascript_include_tag_default_protocol + @controller.config.asset_host = "assets.example.com" + @controller.config.default_asset_host_protocol = :relative + assert_dom_equal %(<script src="//assets.example.com/javascripts/prototype.js"></script>), javascript_include_tag('prototype') + end + + def test_stylesheet_path + StylePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_path_to_stylesheet_alias_for_stylesheet_path + PathToStyleToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_stylesheet_url + StyleUrlToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_url_to_stylesheet_alias_for_stylesheet_url + UrlToStyleToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_stylesheet_link_tag + StyleLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_stylesheet_link_tag_with_missing_source + assert_nothing_raised { + stylesheet_link_tag('missing_security_guard') + } + + assert_nothing_raised { + stylesheet_link_tag('http://example.com/css/missing_security_guard') + } + end + + def test_stylesheet_link_tag_is_html_safe + assert stylesheet_link_tag('dir/file').html_safe? + assert stylesheet_link_tag('dir/other/file', 'dir/file2').html_safe? + end + + def test_stylesheet_link_tag_escapes_options + assert_dom_equal %(<link href="/file.css" media="<script>" rel="stylesheet" />), stylesheet_link_tag('/file', :media => '<script>') + end + + def test_stylesheet_link_tag_should_not_output_the_same_asset_twice + assert_dom_equal %(<link href="/stylesheets/wellington.css" media="screen" rel="stylesheet" />\n<link href="/stylesheets/amsterdam.css" media="screen" rel="stylesheet" />), stylesheet_link_tag('wellington', 'wellington', 'amsterdam') + end + + def test_stylesheet_link_tag_with_relative_protocol + @controller.config.asset_host = "assets.example.com" + assert_dom_equal %(<link href="//assets.example.com/stylesheets/wellington.css" media="screen" rel="stylesheet" />), stylesheet_link_tag('wellington', protocol: :relative) + end + + def test_stylesheet_link_tag_with_default_protocol + @controller.config.asset_host = "assets.example.com" + @controller.config.default_asset_host_protocol = :relative + assert_dom_equal %(<link href="//assets.example.com/stylesheets/wellington.css" media="screen" rel="stylesheet" />), stylesheet_link_tag('wellington') + end + + def test_image_path + ImagePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_path_to_image_alias_for_image_path + PathToImageToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_image_url + ImageUrlToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_url_to_image_alias_for_image_url + UrlToImageToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_image_alt + [nil, '/', '/foo/bar/', 'foo/bar/'].each do |prefix| + assert_equal 'Rails', image_alt("#{prefix}rails.png") + assert_equal 'Rails', image_alt("#{prefix}rails-9c0a079bdd7701d7e729bd956823d153.png") + assert_equal 'Long file name with hyphens', image_alt("#{prefix}long-file-name-with-hyphens.png") + assert_equal 'Long file name with underscores', image_alt("#{prefix}long_file_name_with_underscores.png") + end + end + + def test_image_tag + ImageLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_image_tag_does_not_modify_options + options = {:size => '16x10'} + image_tag('icon', options) + assert_equal({:size => '16x10'}, options) + end + + def test_favicon_link_tag + FaviconLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_video_path + VideoPathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_path_to_video_alias_for_video_path + PathToVideoToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_video_url + VideoUrlToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_url_to_video_alias_for_video_url + UrlToVideoToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_video_tag + VideoLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_audio_path + AudioPathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_path_to_audio_alias_for_audio_path + PathToAudioToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_audio_url + AudioUrlToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_url_to_audio_alias_for_audio_url + UrlToAudioToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_audio_tag + AudioLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_font_path + FontPathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } + end + + def test_video_audio_tag_does_not_modify_options + options = {:autoplay => true} + video_tag('video', options) + assert_equal({:autoplay => true}, options) + audio_tag('audio', options) + assert_equal({:autoplay => true}, options) + end + + def test_image_tag_interpreting_email_cid_correctly + # An inline image has no need for an alt tag to be automatically generated from the cid: + assert_equal '<img src="cid:thi%25%25sis@acontentid" />', image_tag("cid:thi%25%25sis@acontentid") + end + + def test_image_tag_interpreting_email_adding_optional_alt_tag + assert_equal '<img alt="Image" src="cid:thi%25%25sis@acontentid" />', image_tag("cid:thi%25%25sis@acontentid", :alt => "Image") + end + + def test_should_not_modify_source_string + source = '/images/rails.png' + copy = source.dup + image_tag(source) + assert_equal copy, source + end + + def test_image_path_with_asset_host_proc_returning_nil + @controller.config.asset_host = Proc.new do |source| + unless source.end_with?("tiff") + "cdn.example.com" + end + end + + assert_equal "/images/file.tiff", image_path("file.tiff") + assert_equal "http://cdn.example.com/images/file.png", image_path("file.png") + end + + def test_caching_image_path_with_caching_and_proc_asset_host_using_request + @controller.config.asset_host = Proc.new do |source, request| + if request.ssl? + "#{request.protocol}#{request.host_with_port}" + else + "#{request.protocol}assets#{source.length}.example.com" + end + end + + @controller.request.stubs(:ssl?).returns(false) + assert_equal "http://assets15.example.com/images/xml.png", image_path("xml.png") + + @controller.request.stubs(:ssl?).returns(true) + assert_equal "http://localhost/images/xml.png", image_path("xml.png") + end +end + +class AssetTagHelperNonVhostTest < ActionView::TestCase + tests ActionView::Helpers::AssetTagHelper + + attr_reader :request + + def setup + super + @controller = BasicController.new + @controller.config.relative_url_root = "/collaboration/hieraki" + + @request = Struct.new(:protocol, :base_url).new("gopher://", "gopher://www.example.com") + @controller.request = @request + end + + def url_for(options) + "http://www.example.com/collaboration/hieraki" + end + + def test_should_compute_proper_path + assert_dom_equal(%(<link href="http://www.example.com/collaboration/hieraki" rel="alternate" title="RSS" type="application/rss+xml" />), auto_discovery_link_tag) + assert_dom_equal(%(/collaboration/hieraki/javascripts/xmlhr.js), javascript_path("xmlhr")) + assert_dom_equal(%(/collaboration/hieraki/stylesheets/style.css), stylesheet_path("style")) + assert_dom_equal(%(/collaboration/hieraki/images/xml.png), image_path("xml.png")) + end + + def test_should_return_nothing_if_asset_host_isnt_configured + assert_equal nil, compute_asset_host("foo") + end + + def test_should_current_request_host_is_always_returned_for_request + assert_equal "gopher://www.example.com", compute_asset_host("foo", :protocol => :request) + end + + def test_should_ignore_relative_root_path_on_complete_url + assert_dom_equal(%(http://www.example.com/images/xml.png), image_path("http://www.example.com/images/xml.png")) + end + + def test_should_return_simple_string_asset_host + @controller.config.asset_host = "assets.example.com" + assert_equal "gopher://assets.example.com", compute_asset_host("foo") + end + + def test_should_return_relative_asset_host + @controller.config.asset_host = "assets.example.com" + assert_equal "//assets.example.com", compute_asset_host("foo", :protocol => :relative) + end + + def test_should_return_custom_protocol_asset_host + @controller.config.asset_host = "assets.example.com" + assert_equal "ftp://assets.example.com", compute_asset_host("foo", :protocol => "ftp") + end + + def test_should_compute_proper_path_with_asset_host + @controller.config.asset_host = "assets.example.com" + assert_dom_equal(%(<link href="http://www.example.com/collaboration/hieraki" rel="alternate" title="RSS" type="application/rss+xml" />), auto_discovery_link_tag) + assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/javascripts/xmlhr.js), javascript_path("xmlhr")) + assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/stylesheets/style.css), stylesheet_path("style")) + assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/images/xml.png), image_path("xml.png")) + end + + def test_should_compute_proper_path_with_asset_host_and_default_protocol + @controller.config.asset_host = "assets.example.com" + @controller.config.default_asset_host_protocol = :request + assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/javascripts/xmlhr.js), javascript_path("xmlhr")) + assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/stylesheets/style.css), stylesheet_path("style")) + assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/images/xml.png), image_path("xml.png")) + end + + def test_should_compute_proper_url_with_asset_host + @controller.config.asset_host = "assets.example.com" + assert_dom_equal(%(<link href="http://www.example.com/collaboration/hieraki" rel="alternate" title="RSS" type="application/rss+xml" />), auto_discovery_link_tag) + assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/javascripts/xmlhr.js), javascript_url("xmlhr")) + assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/stylesheets/style.css), stylesheet_url("style")) + assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/images/xml.png), image_url("xml.png")) + end + + def test_should_compute_proper_url_with_asset_host_and_default_protocol + @controller.config.asset_host = "assets.example.com" + @controller.config.default_asset_host_protocol = :request + assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/javascripts/xmlhr.js), javascript_url("xmlhr")) + assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/stylesheets/style.css), stylesheet_url("style")) + assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/images/xml.png), image_url("xml.png")) + end + + def test_should_return_asset_host_with_protocol + @controller.config.asset_host = "http://assets.example.com" + assert_equal "http://assets.example.com", compute_asset_host("foo") + end + + def test_should_ignore_asset_host_on_complete_url + @controller.config.asset_host = "http://assets.example.com" + assert_dom_equal(%(<link href="http://bar.example.com/stylesheets/style.css" media="screen" rel="stylesheet" />), stylesheet_link_tag("http://bar.example.com/stylesheets/style.css")) + end + + def test_should_ignore_asset_host_on_scheme_relative_url + @controller.config.asset_host = "http://assets.example.com" + assert_dom_equal(%(<link href="//bar.example.com/stylesheets/style.css" media="screen" rel="stylesheet" />), stylesheet_link_tag("//bar.example.com/stylesheets/style.css")) + end + + def test_should_wildcard_asset_host + @controller.config.asset_host = 'http://a%d.example.com' + assert_match(%r(http://a[0123].example.com), compute_asset_host("foo")) + end + + def test_should_wildcard_asset_host_between_zero_and_four + @controller.config.asset_host = 'http://a%d.example.com' + assert_match(%r(http://a[0123].example.com/collaboration/hieraki/images/xml.png), image_path('xml.png')) + assert_match(%r(http://a[0123].example.com/collaboration/hieraki/images/xml.png), image_url('xml.png')) + end + + def test_asset_host_without_protocol_should_be_protocol_relative + @controller.config.asset_host = 'a.example.com' + assert_equal 'gopher://a.example.com/collaboration/hieraki/images/xml.png', image_path('xml.png') + assert_equal 'gopher://a.example.com/collaboration/hieraki/images/xml.png', image_url('xml.png') + end + + def test_asset_host_without_protocol_should_be_protocol_relative_even_if_path_present + @controller.config.asset_host = 'a.example.com/files/go/here' + assert_equal 'gopher://a.example.com/files/go/here/collaboration/hieraki/images/xml.png', image_path('xml.png') + assert_equal 'gopher://a.example.com/files/go/here/collaboration/hieraki/images/xml.png', image_url('xml.png') + end + + def test_assert_css_and_js_of_the_same_name_return_correct_extension + assert_dom_equal(%(/collaboration/hieraki/javascripts/foo.js), javascript_path("foo")) + assert_dom_equal(%(/collaboration/hieraki/stylesheets/foo.css), stylesheet_path("foo")) + end +end + +class AssetUrlHelperControllerTest < ActionView::TestCase + tests ActionView::Helpers::AssetUrlHelper + + def setup + super + + @controller = BasicController.new + @controller.extend ActionView::Helpers::AssetUrlHelper + + @request = Class.new do + attr_accessor :script_name + def protocol() 'http://' end + def ssl?() false end + def host_with_port() 'www.example.com' end + def base_url() 'http://www.example.com' end + end.new + + @controller.request = @request + end + + def test_asset_path + assert_equal "/foo", @controller.asset_path("foo") + end + + def test_asset_url + assert_equal "http://www.example.com/foo", @controller.asset_url("foo") + end +end + +class AssetUrlHelperEmptyModuleTest < ActionView::TestCase + tests ActionView::Helpers::AssetUrlHelper + + def setup + super + + @module = Module.new + @module.extend ActionView::Helpers::AssetUrlHelper + end + + def test_asset_path + assert_equal "/foo", @module.asset_path("foo") + end + + def test_asset_url + assert_equal "/foo", @module.asset_url("foo") + end + + def test_asset_url_with_request + @module.instance_eval do + def request + Struct.new(:base_url, :script_name).new("http://www.example.com", nil) + end + end + + assert @module.request + assert_equal "http://www.example.com/foo", @module.asset_url("foo") + end + + def test_asset_url_with_config_asset_host + @module.instance_eval do + def config + Struct.new(:asset_host).new("http://www.example.com") + end + end + + assert @module.config.asset_host + assert_equal "http://www.example.com/foo", @module.asset_url("foo") + end +end diff --git a/actionview/test/template/atom_feed_helper_test.rb b/actionview/test/template/atom_feed_helper_test.rb new file mode 100644 index 0000000000..63b5ac0fab --- /dev/null +++ b/actionview/test/template/atom_feed_helper_test.rb @@ -0,0 +1,349 @@ +require 'abstract_unit' + +class Scroll < Struct.new(:id, :to_param, :title, :body, :updated_at, :created_at) + extend ActiveModel::Naming + include ActiveModel::Conversion + + def persisted? + false + end +end + +class ScrollsController < ActionController::Base + FEEDS = {} + FEEDS["defaults"] = <<-EOT + atom_feed(:schema_date => '2008') do |feed| + feed.title("My great blog!") + feed.updated((@scrolls.first.created_at)) + + @scrolls.each do |scroll| + feed.entry(scroll) 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["entry_options"] = <<-EOT + atom_feed do |feed| + feed.title("My great blog!") + 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| + entry.title(scroll.title) + entry.content(scroll.body, :type => 'html') + + entry.author do |author| + author.name("DHH") + end + end + end + end + EOT + FEEDS["entry_type_options"] = <<-EOT + atom_feed(:schema_date => '2008') do |feed| + feed.title("My great blog!") + feed.updated((@scrolls.first.created_at)) + + @scrolls.each do |scroll| + feed.entry(scroll, :type => 'text/xml') 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.author do |author| + author.name("DHH") + end + + @scrolls.each do |scroll| + feed.entry(scroll, :url => "/otherstuff/" + scroll.to_param.to_s, :updated => Time.utc(2007, 1, scroll.id)) do |entry| + entry.title(scroll.title) + entry.content(scroll.body, :type => 'html') + end + end + end + EOT + FEEDS["feed_with_atomPub_namespace"] = <<-EOT + 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)) + + @scrolls.each do |scroll| + feed.entry(scroll) do |entry| + entry.title(scroll.title) + entry.content(scroll.body, :type => 'html') + entry.tag!('app:edited', Time.now) + + entry.author do |author| + author.name("DHH") + end + end + end + end + EOT + 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)) + + @scrolls.each do |scroll| + feed.entry(scroll, :id => "tag:test.rubyonrails.org,2008:"+scroll.id.to_s) do |entry| + entry.title(scroll.title) + entry.content(scroll.body, :type => 'html') + entry.tag!('app:edited', Time.now) + + entry.author do |author| + author.name("DHH") + end + end + end + end + EOT + FEEDS["feed_with_xml_processing_instructions"] = <<-EOT + 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)) + + @scrolls.each do |scroll| + feed.entry(scroll) 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["feed_with_xml_processing_instructions_duplicate_targets"] = <<-EOT + 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)) + + @scrolls.each do |scroll| + feed.entry(scroll) 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["feed_with_xhtml_content"] = <<-'EOT' + atom_feed do |feed| + feed.title("My great blog!") + feed.updated((@scrolls.first.created_at)) + + @scrolls.each do |scroll| + feed.entry(scroll) do |entry| + entry.title(scroll.title) + entry.summary(:type => 'xhtml') do |xhtml| + xhtml.p "before #{scroll.id}" + xhtml.p {xhtml << scroll.body} + xhtml.p "after #{scroll.id}" + end + entry.tag!('app:edited', Time.now) + + entry.author do |author| + author.name("DHH") + end + end + end + end + EOT + FEEDS["provide_builder"] = <<-'EOT' + # we pass in the new_xml to the helper so it doesn't + # call anything on the original builder + new_xml = Builder::XmlMarkup.new(:target=>'') + atom_feed(:xml => new_xml) do |feed| + feed.title("My great blog!") + feed.updated((@scrolls.first.created_at)) + + @scrolls.each do |scroll| + feed.entry(scroll) do |entry| + entry.title(scroll.title) + entry.content(scroll.body, :type => 'html') + + entry.author do |author| + author.name("DHH") + end + end + end + end + EOT + def index + @scrolls = [ + Scroll.new(1, "1", "Hello One", "Something <i>COOL!</i>", Time.utc(2007, 12, 12, 15), Time.utc(2007, 12, 12, 15)), + Scroll.new(2, "2", "Hello Two", "Something Boring", Time.utc(2007, 12, 12, 15)), + ] + + render :inline => FEEDS[params[:id]], :type => :builder + end +end + +class AtomFeedTest < ActionController::TestCase + tests ScrollsController + + def setup + super + @request.host = "www.nextangle.com" + end + + def test_feed_should_use_default_language_if_none_is_given + with_restful_routing(:scrolls) do + get :index, :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" + 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" + assert_select "published", 1 + end + end + + def test_providing_builder_to_atom_feed + with_restful_routing(:scrolls) do + get :index, :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? + end + end + + def test_entry_with_prefilled_options_should_use_those_instead_of_querying_the_record + with_restful_routing(:scrolls) do + get :index, :id => "entry_options" + + assert_select "updated", Time.utc(2007, 1, 1).xmlschema + assert_select "updated", Time.utc(2007, 1, 2).xmlschema + end + end + + 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]" + end + end + + def test_feed_id_should_be_a_valid_tag + with_restful_routing(:scrolls) do + get :index, :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" + 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 + end + + def test_feed_should_allow_nested_xml_blocks + with_restful_routing(:scrolls) do + get :index, :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" + 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 + end + end + + def test_feed_should_allow_overriding_ids + with_restful_routing(:scrolls) do + get :index, :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" + end + end + + def test_feed_xml_processing_instructions + with_restful_routing(:scrolls) do + get :index, :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 + end + + def test_feed_xml_processing_instructions_duplicate_targets + with_restful_routing(:scrolls) do + get :index, :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 + end + + def test_feed_xhtml + with_restful_routing(:scrolls) do + get :index, :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" + 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]" + 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]" + end + end + + private + def with_restful_routing(resources) + with_routing do |set| + set.draw do + resources(resources) + end + yield + end + end +end diff --git a/actionview/test/template/capture_helper_test.rb b/actionview/test/template/capture_helper_test.rb new file mode 100644 index 0000000000..938f1c3e54 --- /dev/null +++ b/actionview/test/template/capture_helper_test.rb @@ -0,0 +1,242 @@ +require 'abstract_unit' + +class CaptureHelperTest < ActionView::TestCase + def setup + super + @av = ActionView::Base.new + @view_flow = ActionView::OutputFlow.new + end + + def test_capture_captures_the_temporary_output_buffer_in_its_block + assert_nil @av.output_buffer + string = @av.capture do + @av.output_buffer << 'foo' + @av.output_buffer << 'bar' + end + assert_nil @av.output_buffer + assert_equal 'foobar', string + end + + def test_capture_captures_the_value_returned_by_the_block_if_the_temporary_buffer_is_blank + string = @av.capture('foo', 'bar') do |a, b| + a + b + end + assert_equal 'foobar', string + end + + def test_capture_returns_nil_if_the_returned_value_is_not_a_string + assert_nil @av.capture { 1 } + end + + def test_capture_escapes_html + string = @av.capture { '<em>bar</em>' } + assert_equal '<em>bar</em>', string + end + + def test_capture_doesnt_escape_twice + string = @av.capture { '<em>bar</em>'.html_safe } + assert_equal '<em>bar</em>', string + end + + def test_capture_used_for_read + content_for :foo, "foo" + assert_equal "foo", content_for(:foo) + + content_for(:bar){ "bar" } + assert_equal "bar", content_for(:bar) + end + + def test_content_for_with_multiple_calls + assert ! content_for?(:title) + content_for :title, 'foo' + content_for :title, 'bar' + assert_equal 'foobar', content_for(:title) + end + + def test_content_for_with_multiple_calls_and_flush + assert ! content_for?(:title) + content_for :title, 'foo' + content_for :title, 'bar', flush: true + assert_equal 'bar', content_for(:title) + end + + def test_content_for_with_block + assert ! content_for?(:title) + content_for :title do + output_buffer << 'foo' + output_buffer << 'bar' + nil + end + assert_equal 'foobar', content_for(:title) + end + + def test_content_for_with_block_and_multiple_calls_with_flush + assert ! content_for?(:title) + content_for :title do + 'foo' + end + content_for :title, flush: true do + 'bar' + end + assert_equal 'bar', content_for(:title) + end + + def test_content_for_with_block_and_multiple_calls_with_flush_nil_content + assert ! content_for?(:title) + content_for :title do + 'foo' + end + content_for :title, nil, flush: true do + 'bar' + end + assert_equal 'bar', content_for(:title) + end + + def test_content_for_with_block_and_multiple_calls_without_flush + assert ! content_for?(:title) + content_for :title do + 'foo' + end + content_for :title, flush: false do + 'bar' + end + assert_equal 'foobar', content_for(:title) + end + + def test_content_for_with_whitespace_block + assert ! content_for?(:title) + content_for :title, 'foo' + content_for :title do + output_buffer << " \n " + nil + end + content_for :title, 'bar' + assert_equal 'foobar', content_for(:title) + end + + def test_content_for_with_whitespace_block_and_flush + assert ! content_for?(:title) + content_for :title, 'foo' + content_for :title, flush: true do + output_buffer << " \n " + nil + end + content_for :title, 'bar', flush: true + assert_equal 'bar', content_for(:title) + end + + def test_content_for_returns_nil_when_writing + assert ! content_for?(:title) + assert_equal nil, content_for(:title, 'foo') + assert_equal nil, content_for(:title) { output_buffer << 'bar'; nil } + assert_equal nil, content_for(:title) { output_buffer << " \n "; nil } + assert_equal 'foobar', content_for(:title) + assert_equal nil, content_for(:title, 'foo', flush: true) + assert_equal nil, content_for(:title, flush: true) { output_buffer << 'bar'; nil } + assert_equal nil, content_for(:title, flush: true) { output_buffer << " \n "; nil } + assert_equal 'bar', content_for(:title) + end + + def test_content_for_returns_nil_when_content_missing + assert_equal nil, content_for(:some_missing_key) + end + + def test_content_for_question_mark + assert ! content_for?(:title) + content_for :title, 'title' + assert content_for?(:title) + assert ! content_for?(:something_else) + end + + def test_provide + assert !content_for?(:title) + provide :title, "hi" + assert content_for?(:title) + assert_equal "hi", content_for(:title) + provide :title, "<p>title</p>" + assert_equal "hi<p>title</p>", content_for(:title) + + @view_flow = ActionView::OutputFlow.new + provide :title, "hi" + provide :title, "<p>title</p>".html_safe + assert_equal "hi<p>title</p>", content_for(:title) + end + + def test_with_output_buffer_swaps_the_output_buffer_given_no_argument + assert_nil @av.output_buffer + buffer = @av.with_output_buffer do + @av.output_buffer << '.' + end + assert_equal '.', buffer + assert_nil @av.output_buffer + end + + def test_with_output_buffer_swaps_the_output_buffer_with_an_argument + assert_nil @av.output_buffer + buffer = ActionView::OutputBuffer.new('.') + @av.with_output_buffer(buffer) do + @av.output_buffer << '.' + end + assert_equal '..', buffer + assert_nil @av.output_buffer + end + + def test_with_output_buffer_restores_the_output_buffer + buffer = ActionView::OutputBuffer.new + @av.output_buffer = buffer + @av.with_output_buffer do + @av.output_buffer << '.' + end + assert buffer.equal?(@av.output_buffer) + end + + def test_with_output_buffer_sets_proper_encoding + @av.output_buffer = ActionView::OutputBuffer.new + + # Ensure we set the output buffer to an encoding different than the default one. + alt_encoding = alt_encoding(@av.output_buffer) + @av.output_buffer.force_encoding(alt_encoding) + + @av.with_output_buffer do + assert_equal alt_encoding, @av.output_buffer.encoding + end + end + + def test_with_output_buffer_does_not_assume_there_is_an_output_buffer + assert_nil @av.output_buffer + assert_equal "", @av.with_output_buffer {} + end + + def test_flush_output_buffer_concats_output_buffer_to_response + view = view_with_controller + assert_equal [], view.response.body_parts + + view.output_buffer << 'OMG' + view.flush_output_buffer + assert_equal ['OMG'], view.response.body_parts + assert_equal '', view.output_buffer + + view.output_buffer << 'foobar' + view.flush_output_buffer + assert_equal ['OMG', 'foobar'], view.response.body_parts + assert_equal '', view.output_buffer + end + + def test_flush_output_buffer_preserves_the_encoding_of_the_output_buffer + view = view_with_controller + alt_encoding = alt_encoding(view.output_buffer) + view.output_buffer.force_encoding(alt_encoding) + flush_output_buffer + assert_equal alt_encoding, view.output_buffer.encoding + end + + 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 new file mode 100644 index 0000000000..2336321f3e --- /dev/null +++ b/actionview/test/template/compiled_templates_test.rb @@ -0,0 +1,62 @@ +require 'abstract_unit' + +class CompiledTemplatesTest < ActiveSupport::TestCase + def setup + # Clean up any details key cached to expose failures + # that otherwise would appear just on isolated tests + ActionView::LookupContext::DetailsKey.clear + + @compiled_templates = ActionView::CompiledTemplates + @compiled_templates.instance_methods.each do |m| + @compiled_templates.send(:remove_method, m) if m =~ /^_render_template_/ + end + 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" }) + end + + def test_template_changes_are_not_reflected_with_cached_templates + assert_equal "Hello world!", render(:file => "test/hello_world") + modify_template "test/hello_world.erb", "Goodbye world!" do + assert_equal "Hello world!", render(:file => "test/hello_world") + end + assert_equal "Hello world!", render(:file => "test/hello_world") + end + + def test_template_changes_are_reflected_with_uncached_templates + assert_equal "Hello world!", render_without_cache(:file => "test/hello_world") + modify_template "test/hello_world.erb", "Goodbye world!" do + assert_equal "Goodbye world!", render_without_cache(:file => "test/hello_world") + end + assert_equal "Hello world!", render_without_cache(:file => "test/hello_world") + end + + private + def render(*args) + render_with_cache(*args) + end + + def render_with_cache(*args) + view_paths = ActionController::Base.view_paths + ActionView::Base.new(view_paths, {}).render(*args) + end + + def render_without_cache(*args) + path = ActionView::FileSystemResolver.new(FIXTURE_LOAD_PATH) + view_paths = ActionView::PathSet.new([path]) + ActionView::Base.new(view_paths, {}).render(*args) + end + + def modify_template(template, content) + filename = "#{FIXTURE_LOAD_PATH}/#{template}" + old_content = File.read(filename) + begin + File.open(filename, "wb+") { |f| f.write(content) } + yield + ensure + File.open(filename, "wb+") { |f| f.write(old_content) } + end + end +end diff --git a/actionview/test/template/date_helper_i18n_test.rb b/actionview/test/template/date_helper_i18n_test.rb new file mode 100644 index 0000000000..21fca35185 --- /dev/null +++ b/actionview/test/template/date_helper_i18n_test.rb @@ -0,0 +1,148 @@ +require 'abstract_unit' + +class DateHelperDistanceOfTimeInWordsI18nTests < ActiveSupport::TestCase + include ActionView::Helpers::DateHelper + attr_reader :request + + def setup + @from = Time.utc(2004, 6, 6, 21, 45, 0) + end + + # distance_of_time_in_words + + def test_distance_of_time_in_words_calls_i18n + { # with include_seconds + [2.seconds, { :include_seconds => true }] => [:'less_than_x_seconds', 5], + [9.seconds, { :include_seconds => true }] => [:'less_than_x_seconds', 10], + [19.seconds, { :include_seconds => true }] => [:'less_than_x_seconds', 20], + [30.seconds, { :include_seconds => true }] => [:'half_a_minute', nil], + [59.seconds, { :include_seconds => true }] => [:'less_than_x_minutes', 1], + [60.seconds, { :include_seconds => true }] => [:'x_minutes', 1], + + # without include_seconds + [29.seconds, { :include_seconds => false }] => [:'less_than_x_minutes', 1], + [60.seconds, { :include_seconds => false }] => [:'x_minutes', 1], + [44.minutes, { :include_seconds => false }] => [:'x_minutes', 44], + [61.minutes, { :include_seconds => false }] => [:'about_x_hours', 1], + [24.hours, { :include_seconds => false }] => [:'x_days', 1], + [30.days, { :include_seconds => false }] => [:'about_x_months', 1], + [60.days, { :include_seconds => false }] => [:'x_months', 2], + [1.year, { :include_seconds => false }] => [:'about_x_years', 1], + [3.years + 6.months, { :include_seconds => false }] => [:'over_x_years', 3], + [3.years + 10.months, { :include_seconds => false }] => [:'almost_x_years', 4] + + }.each do |passed, expected| + assert_distance_of_time_in_words_translates_key passed, expected + end + end + + def test_distance_of_time_in_words_calls_i18n_with_custom_scope + { + [30.days, { scope: :'datetime.distance_in_words_ago' }] => [:'about_x_months', 1], + [60.days, { scope: :'datetime.distance_in_words_ago' }] => [:'x_months', 2], + }.each do |passed, expected| + assert_distance_of_time_in_words_translates_key(passed, expected, scope: :'datetime.distance_in_words_ago') + end + end + + def test_time_ago_in_words_passes_locale + I18n.expects(:t).with(:less_than_x_minutes, :scope => :'datetime.distance_in_words', :count => 1, :locale => 'ru') + time_ago_in_words(15.seconds.ago, :locale => 'ru') + end + + def test_distance_of_time_pluralizations + { [:'less_than_x_seconds', 1] => 'less than 1 second', + [:'less_than_x_seconds', 2] => 'less than 2 seconds', + [:'less_than_x_minutes', 1] => 'less than a minute', + [:'less_than_x_minutes', 2] => 'less than 2 minutes', + [:'x_minutes', 1] => '1 minute', + [:'x_minutes', 2] => '2 minutes', + [:'about_x_hours', 1] => 'about 1 hour', + [:'about_x_hours', 2] => 'about 2 hours', + [:'x_days', 1] => '1 day', + [:'x_days', 2] => '2 days', + [:'about_x_years', 1] => 'about 1 year', + [:'about_x_years', 2] => 'about 2 years', + [:'over_x_years', 1] => 'over 1 year', + [:'over_x_years', 2] => 'over 2 years' + + }.each do |args, expected| + key, count = *args + assert_equal expected, I18n.t(key, :count => count, :scope => 'datetime.distance_in_words') + end + end + + def assert_distance_of_time_in_words_translates_key(passed, expected, expected_options = {}) + diff, passed_options = *passed + key, count = *expected + to = @from + diff + + options = { locale: 'en', scope: :'datetime.distance_in_words' }.merge!(expected_options) + options[:count] = count if count + + I18n.expects(:t).with(key, options) + distance_of_time_in_words(@from, to, passed_options.merge(locale: 'en')) + end +end + +class DateHelperSelectTagsI18nTests < ActiveSupport::TestCase + include ActionView::Helpers::DateHelper + attr_reader :request + + def setup + @prompt_defaults = {:year => 'Year', :month => 'Month', :day => 'Day', :hour => 'Hour', :minute => 'Minute', :second => 'Seconds'} + + I18n.stubs(:translate).with(:'date.month_names', :locale => 'en').returns Date::MONTHNAMES + end + + # select_month + + def test_select_month_given_use_month_names_option_does_not_translate_monthnames + I18n.expects(:translate).never + select_month(8, :locale => 'en', :use_month_names => Date::MONTHNAMES) + end + + def test_select_month_translates_monthnames + I18n.expects(:translate).with(:'date.month_names', :locale => 'en').returns Date::MONTHNAMES + select_month(8, :locale => 'en') + end + + def test_select_month_given_use_short_month_option_translates_abbr_monthnames + I18n.expects(:translate).with(:'date.abbr_month_names', :locale => 'en').returns Date::ABBR_MONTHNAMES + select_month(8, :locale => 'en', :use_short_month => true) + end + + def test_date_or_time_select_translates_prompts + @prompt_defaults.each do |key, prompt| + I18n.expects(:translate).with(('datetime.prompts.' + key.to_s).to_sym, :locale => 'en').returns prompt + end + + I18n.expects(:translate).with(:'date.order', :locale => 'en', :default => []).returns %w(year month day) + datetime_select('post', 'updated_at', :locale => 'en', :include_seconds => true, :prompt => true) + end + + # date_or_time_select + + def test_date_or_time_select_given_an_order_options_does_not_translate_order + I18n.expects(:translate).never + datetime_select('post', 'updated_at', :order => [:year, :month, :day], :locale => 'en') + end + + def test_date_or_time_select_given_no_order_options_translates_order + I18n.expects(:translate).with(:'date.order', :locale => 'en', :default => []).returns %w(year month day) + datetime_select('post', 'updated_at', :locale => 'en') + end + + def test_date_or_time_select_given_invalid_order + I18n.expects(:translate).with(:'date.order', :locale => 'en', :default => []).returns %w(invalid month day) + + assert_raise StandardError do + datetime_select('post', 'updated_at', :locale => 'en') + end + end + + def test_date_or_time_select_given_symbol_keys + I18n.expects(:translate).with(:'date.order', :locale => 'en', :default => []).returns [:year, :month, :day] + datetime_select('post', 'updated_at', :locale => 'en') + end +end diff --git a/actionview/test/template/date_helper_test.rb b/actionview/test/template/date_helper_test.rb new file mode 100644 index 0000000000..242b56a1fd --- /dev/null +++ b/actionview/test/template/date_helper_test.rb @@ -0,0 +1,3199 @@ +require 'abstract_unit' + +class DateHelperTest < ActionView::TestCase + tests ActionView::Helpers::DateHelper + + silence_warnings do + Post = Struct.new("Post", :id, :written_on, :updated_at) + Post.class_eval do + def id + 123 + end + def id_before_type_cast + 123 + end + def to_param + '123' + end + end + end + + def assert_distance_of_time_in_words(from, to=nil) + Fixnum.send :private, :/ # test we avoid Integer#/ (redefined by mathn) + + to ||= from + + # 0..1 minute with :include_seconds => true + assert_equal "less than 5 seconds", distance_of_time_in_words(from, to + 0.seconds, :include_seconds => true) + assert_equal "less than 5 seconds", distance_of_time_in_words(from, to + 4.seconds, :include_seconds => true) + assert_equal "less than 10 seconds", distance_of_time_in_words(from, to + 5.seconds, :include_seconds => true) + assert_equal "less than 10 seconds", distance_of_time_in_words(from, to + 9.seconds, :include_seconds => true) + assert_equal "less than 20 seconds", distance_of_time_in_words(from, to + 10.seconds, :include_seconds => true) + assert_equal "less than 20 seconds", distance_of_time_in_words(from, to + 19.seconds, :include_seconds => true) + assert_equal "half a minute", distance_of_time_in_words(from, to + 20.seconds, :include_seconds => true) + assert_equal "half a minute", distance_of_time_in_words(from, to + 39.seconds, :include_seconds => true) + assert_equal "less than a minute", distance_of_time_in_words(from, to + 40.seconds, :include_seconds => true) + assert_equal "less than a minute", distance_of_time_in_words(from, to + 59.seconds, :include_seconds => true) + assert_equal "1 minute", distance_of_time_in_words(from, to + 60.seconds, :include_seconds => true) + assert_equal "1 minute", distance_of_time_in_words(from, to + 89.seconds, :include_seconds => true) + + # 0..1 minute with :include_seconds => false + assert_equal "less than a minute", distance_of_time_in_words(from, to + 0.seconds, :include_seconds => false) + assert_equal "less than a minute", distance_of_time_in_words(from, to + 4.seconds, :include_seconds => false) + assert_equal "less than a minute", distance_of_time_in_words(from, to + 5.seconds, :include_seconds => false) + assert_equal "less than a minute", distance_of_time_in_words(from, to + 9.seconds, :include_seconds => false) + assert_equal "less than a minute", distance_of_time_in_words(from, to + 10.seconds, :include_seconds => false) + assert_equal "less than a minute", distance_of_time_in_words(from, to + 19.seconds, :include_seconds => false) + assert_equal "less than a minute", distance_of_time_in_words(from, to + 20.seconds, :include_seconds => false) + assert_equal "1 minute", distance_of_time_in_words(from, to + 39.seconds, :include_seconds => false) + assert_equal "1 minute", distance_of_time_in_words(from, to + 40.seconds, :include_seconds => false) + assert_equal "1 minute", distance_of_time_in_words(from, to + 59.seconds, :include_seconds => false) + assert_equal "1 minute", distance_of_time_in_words(from, to + 60.seconds, :include_seconds => false) + assert_equal "1 minute", distance_of_time_in_words(from, to + 89.seconds, :include_seconds => false) + + # Note that we are including a 30-second boundary around the interval we + # want to test. For instance, "1 minute" is actually 30s to 1m29s. The + # reason for doing this is simple -- in `distance_of_time_to_words`, when we + # take the distance between our two Time objects in seconds and convert it + # to minutes, we round the number. So 29s gets rounded down to 0m, 30s gets + # rounded up to 1m, and 1m29s gets rounded down to 1m. A similar thing + # happens with the other cases. + + # First case 0..1 minute + assert_equal "less than a minute", distance_of_time_in_words(from, to + 0.seconds) + assert_equal "less than a minute", distance_of_time_in_words(from, to + 29.seconds) + assert_equal "1 minute", distance_of_time_in_words(from, to + 30.seconds) + assert_equal "1 minute", distance_of_time_in_words(from, to + 1.minutes + 29.seconds) + + # 2 minutes up to 45 minutes + assert_equal "2 minutes", distance_of_time_in_words(from, to + 1.minutes + 30.seconds) + assert_equal "44 minutes", distance_of_time_in_words(from, to + 44.minutes + 29.seconds) + + # 45 minutes up to 90 minutes + assert_equal "about 1 hour", distance_of_time_in_words(from, to + 44.minutes + 30.seconds) + assert_equal "about 1 hour", distance_of_time_in_words(from, to + 89.minutes + 29.seconds) + + # 90 minutes up to 24 hours + assert_equal "about 2 hours", distance_of_time_in_words(from, to + 89.minutes + 30.seconds) + assert_equal "about 24 hours", distance_of_time_in_words(from, to + 23.hours + 59.minutes + 29.seconds) + + # 24 hours up to 42 hours + assert_equal "1 day", distance_of_time_in_words(from, to + 23.hours + 59.minutes + 30.seconds) + assert_equal "1 day", distance_of_time_in_words(from, to + 41.hours + 59.minutes + 29.seconds) + + # 42 hours up to 30 days + assert_equal "2 days", distance_of_time_in_words(from, to + 41.hours + 59.minutes + 30.seconds) + assert_equal "3 days", distance_of_time_in_words(from, to + 2.days + 12.hours) + assert_equal "30 days", distance_of_time_in_words(from, to + 29.days + 23.hours + 59.minutes + 29.seconds) + + # 30 days up to 60 days + assert_equal "about 1 month", distance_of_time_in_words(from, to + 29.days + 23.hours + 59.minutes + 30.seconds) + assert_equal "about 1 month", distance_of_time_in_words(from, to + 44.days + 23.hours + 59.minutes + 29.seconds) + assert_equal "about 2 months", distance_of_time_in_words(from, to + 44.days + 23.hours + 59.minutes + 30.seconds) + assert_equal "about 2 months", distance_of_time_in_words(from, to + 59.days + 23.hours + 59.minutes + 29.seconds) + + # 60 days up to 365 days + assert_equal "2 months", distance_of_time_in_words(from, to + 59.days + 23.hours + 59.minutes + 30.seconds) + assert_equal "12 months", distance_of_time_in_words(from, to + 1.years - 31.seconds) + + # >= 365 days + assert_equal "about 1 year", distance_of_time_in_words(from, to + 1.years - 30.seconds) + assert_equal "about 1 year", distance_of_time_in_words(from, to + 1.years + 3.months - 1.day) + assert_equal "over 1 year", distance_of_time_in_words(from, to + 1.years + 6.months) + + assert_equal "almost 2 years", distance_of_time_in_words(from, to + 2.years - 3.months + 1.day) + assert_equal "about 2 years", distance_of_time_in_words(from, to + 2.years + 3.months - 1.day) + assert_equal "over 2 years", distance_of_time_in_words(from, to + 2.years + 3.months + 1.day) + assert_equal "over 2 years", distance_of_time_in_words(from, to + 2.years + 9.months - 1.day) + assert_equal "almost 3 years", distance_of_time_in_words(from, to + 2.years + 9.months + 1.day) + + assert_equal "almost 5 years", distance_of_time_in_words(from, to + 5.years - 3.months + 1.day) + assert_equal "about 5 years", distance_of_time_in_words(from, to + 5.years + 3.months - 1.day) + assert_equal "over 5 years", distance_of_time_in_words(from, to + 5.years + 3.months + 1.day) + assert_equal "over 5 years", distance_of_time_in_words(from, to + 5.years + 9.months - 1.day) + assert_equal "almost 6 years", distance_of_time_in_words(from, to + 5.years + 9.months + 1.day) + + assert_equal "almost 10 years", distance_of_time_in_words(from, to + 10.years - 3.months + 1.day) + assert_equal "about 10 years", distance_of_time_in_words(from, to + 10.years + 3.months - 1.day) + assert_equal "over 10 years", distance_of_time_in_words(from, to + 10.years + 3.months + 1.day) + assert_equal "over 10 years", distance_of_time_in_words(from, to + 10.years + 9.months - 1.day) + assert_equal "almost 11 years", distance_of_time_in_words(from, to + 10.years + 9.months + 1.day) + + # test to < from + assert_equal "about 4 hours", distance_of_time_in_words(from + 4.hours, to) + assert_equal "less than 20 seconds", distance_of_time_in_words(from + 19.seconds, to, :include_seconds => true) + assert_equal "less than a minute", distance_of_time_in_words(from + 19.seconds, to, :include_seconds => false) + + ensure + Fixnum.send :public, :/ + end + + def test_distance_in_words + from = Time.utc(2004, 6, 6, 21, 45, 0) + assert_distance_of_time_in_words(from) + end + + def test_time_ago_in_words_passes_include_seconds + assert_equal "less than 20 seconds", time_ago_in_words(15.seconds.ago, :include_seconds => true) + assert_equal "less than a minute", time_ago_in_words(15.seconds.ago, :include_seconds => false) + end + + def test_distance_in_words_with_time_zones + from = Time.mktime(2004, 6, 6, 21, 45, 0) + assert_distance_of_time_in_words(from.in_time_zone('Alaska')) + assert_distance_of_time_in_words(from.in_time_zone('Hawaii')) + end + + def test_distance_in_words_with_different_time_zones + from = Time.mktime(2004, 6, 6, 21, 45, 0) + assert_distance_of_time_in_words( + from.in_time_zone('Alaska'), + from.in_time_zone('Hawaii') + ) + end + + def test_distance_in_words_with_dates + start_date = Date.new 1975, 1, 31 + end_date = Date.new 1977, 1, 31 + assert_equal("about 2 years", distance_of_time_in_words(start_date, end_date)) + + start_date = Date.new 1982, 12, 3 + end_date = Date.new 2010, 11, 30 + assert_equal("almost 28 years", distance_of_time_in_words(start_date, end_date)) + assert_equal("almost 28 years", distance_of_time_in_words(end_date, start_date)) + end + + def test_distance_in_words_with_integers + assert_equal "1 minute", distance_of_time_in_words(59) + assert_equal "about 1 hour", distance_of_time_in_words(60*60) + assert_equal "1 minute", distance_of_time_in_words(0, 59) + assert_equal "about 1 hour", distance_of_time_in_words(60*60, 0) + assert_equal "about 3 years", distance_of_time_in_words(10**8) + assert_equal "about 3 years", distance_of_time_in_words(0, 10**8) + end + + def test_distance_in_words_with_times + assert_equal "1 minute", distance_of_time_in_words(30.seconds) + assert_equal "1 minute", distance_of_time_in_words(59.seconds) + assert_equal "2 minutes", distance_of_time_in_words(119.seconds) + assert_equal "2 minutes", distance_of_time_in_words(1.minute + 59.seconds) + assert_equal "3 minutes", distance_of_time_in_words(2.minute + 30.seconds) + assert_equal "44 minutes", distance_of_time_in_words(44.minutes + 29.seconds) + assert_equal "about 1 hour", distance_of_time_in_words(44.minutes + 30.seconds) + assert_equal "about 1 hour", distance_of_time_in_words(60.minutes) + + # include seconds + assert_equal "half a minute", distance_of_time_in_words(39.seconds, 0, :include_seconds => true) + assert_equal "less than a minute", distance_of_time_in_words(40.seconds, 0, :include_seconds => true) + assert_equal "less than a minute", distance_of_time_in_words(59.seconds, 0, :include_seconds => true) + assert_equal "1 minute", distance_of_time_in_words(60.seconds, 0, :include_seconds => true) + end + + def test_time_ago_in_words + assert_equal "about 1 year", time_ago_in_words(1.year.ago - 1.day) + end + + def test_select_day + expected = %(<select id="date_day" name="date[day]">\n) + expected << %(<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 value="15">15</option>\n<option value="16" selected="selected">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) + expected << "</select>\n" + + assert_dom_equal expected, select_day(Time.mktime(2003, 8, 16)) + assert_dom_equal expected, select_day(16) + end + + def test_select_day_with_blank + expected = %(<select id="date_day" name="date[day]">\n) + expected << %(<option value=""></option>\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 value="15">15</option>\n<option value="16" selected="selected">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) + expected << "</select>\n" + + assert_dom_equal expected, select_day(Time.mktime(2003, 8, 16), :include_blank => true) + assert_dom_equal expected, select_day(16, :include_blank => true) + end + + def test_select_day_nil_with_blank + expected = %(<select id="date_day" name="date[day]">\n) + expected << %(<option value=""></option>\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 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) + expected << "</select>\n" + + assert_dom_equal expected, select_day(nil, :include_blank => true) + end + + def test_select_day_with_two_digit_numbers + expected = %(<select id="date_day" name="date[day]">\n) + expected << %(<option value="1">01</option>\n<option selected="selected" value="2">02</option>\n<option value="3">03</option>\n<option value="4">04</option>\n<option value="5">05</option>\n<option value="6">06</option>\n<option value="7">07</option>\n<option value="8">08</option>\n<option value="9">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) + expected << "</select>\n" + + assert_dom_equal expected, select_day(Time.mktime(2011, 8, 2), :use_two_digit_numbers => true) + assert_dom_equal expected, select_day(2, :use_two_digit_numbers => true) + end + + def test_select_day_with_html_options + expected = %(<select id="date_day" name="date[day]" class="selector">\n) + expected << %(<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 value="15">15</option>\n<option value="16" selected="selected">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) + expected << "</select>\n" + + assert_dom_equal expected, select_day(Time.mktime(2003, 8, 16), {}, :class => 'selector') + assert_dom_equal expected, select_day(16, {}, :class => 'selector') + end + + def test_select_day_with_default_prompt + expected = %(<select id="date_day" name="date[day]">\n) + expected << %(<option value="">Day</option>\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 value="15">15</option>\n<option value="16" selected="selected">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) + expected << "</select>\n" + + assert_dom_equal expected, select_day(16, :prompt => true) + end + + def test_select_day_with_custom_prompt + expected = %(<select id="date_day" name="date[day]">\n) + expected << %(<option value="">Choose day</option>\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 value="15">15</option>\n<option value="16" selected="selected">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) + expected << "</select>\n" + + assert_dom_equal expected, select_day(16, :prompt => 'Choose day') + end + + def test_select_month + expected = %(<select id="date_month" name="date[month]">\n) + expected << %(<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 value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">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) + expected << "</select>\n" + + assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16)) + assert_dom_equal expected, select_month(8) + end + + def test_select_month_with_two_digit_numbers + expected = %(<select id="date_month" name="date[month]">\n) + expected << %(<option value="1">01</option>\n<option value="2">02</option>\n<option value="3">03</option>\n<option value="4">04</option>\n<option value="5">05</option>\n<option value="6">06</option>\n<option value="7">07</option>\n<option value="8" selected="selected">08</option>\n<option value="9">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n) + expected << "</select>\n" + + assert_dom_equal expected, select_month(Time.mktime(2011, 8, 16), :use_two_digit_numbers => true) + assert_dom_equal expected, select_month(8, :use_two_digit_numbers => true) + end + + def test_select_month_with_disabled + expected = %(<select id="date_month" name="date[month]" disabled="disabled">\n) + expected << %(<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 value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">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) + expected << "</select>\n" + + assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :disabled => true) + assert_dom_equal expected, select_month(8, :disabled => true) + end + + def test_select_month_with_field_name_override + expected = %(<select id="date_mois" name="date[mois]">\n) + expected << %(<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 value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">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) + expected << "</select>\n" + + assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :field_name => 'mois') + assert_dom_equal expected, select_month(8, :field_name => 'mois') + end + + def test_select_month_with_blank + expected = %(<select id="date_month" name="date[month]">\n) + expected << %(<option value=""></option>\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 value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">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) + expected << "</select>\n" + + assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :include_blank => true) + assert_dom_equal expected, select_month(8, :include_blank => true) + end + + def test_select_month_nil_with_blank + expected = %(<select id="date_month" name="date[month]">\n) + expected << %(<option value=""></option>\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 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) + expected << "</select>\n" + + assert_dom_equal expected, select_month(nil, :include_blank => true) + end + + def test_select_month_with_numbers + expected = %(<select id="date_month" name="date[month]">\n) + expected << %(<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" selected="selected">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) + expected << "</select>\n" + + assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :use_month_numbers => true) + assert_dom_equal expected, select_month(8, :use_month_numbers => true) + end + + def test_select_month_with_numbers_and_names + expected = %(<select id="date_month" name="date[month]">\n) + expected << %(<option value="1">1 - January</option>\n<option value="2">2 - February</option>\n<option value="3">3 - March</option>\n<option value="4">4 - April</option>\n<option value="5">5 - May</option>\n<option value="6">6 - June</option>\n<option value="7">7 - July</option>\n<option value="8" selected="selected">8 - August</option>\n<option value="9">9 - September</option>\n<option value="10">10 - October</option>\n<option value="11">11 - November</option>\n<option value="12">12 - December</option>\n) + expected << "</select>\n" + + assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :add_month_numbers => true) + assert_dom_equal expected, select_month(8, :add_month_numbers => true) + end + + def test_select_month_with_numbers_and_names_with_abbv + expected = %(<select id="date_month" name="date[month]">\n) + expected << %(<option value="1">1 - Jan</option>\n<option value="2">2 - Feb</option>\n<option value="3">3 - Mar</option>\n<option value="4">4 - Apr</option>\n<option value="5">5 - May</option>\n<option value="6">6 - Jun</option>\n<option value="7">7 - Jul</option>\n<option value="8" selected="selected">8 - Aug</option>\n<option value="9">9 - Sep</option>\n<option value="10">10 - Oct</option>\n<option value="11">11 - Nov</option>\n<option value="12">12 - Dec</option>\n) + expected << "</select>\n" + + assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :add_month_numbers => true, :use_short_month => true) + assert_dom_equal expected, select_month(8, :add_month_numbers => true, :use_short_month => true) + end + + def test_select_month_with_abbv + expected = %(<select id="date_month" name="date[month]">\n) + expected << %(<option value="1">Jan</option>\n<option value="2">Feb</option>\n<option value="3">Mar</option>\n<option value="4">Apr</option>\n<option value="5">May</option>\n<option value="6">Jun</option>\n<option value="7">Jul</option>\n<option value="8" selected="selected">Aug</option>\n<option value="9">Sep</option>\n<option value="10">Oct</option>\n<option value="11">Nov</option>\n<option value="12">Dec</option>\n) + expected << "</select>\n" + + assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :use_short_month => true) + assert_dom_equal expected, select_month(8, :use_short_month => true) + end + + def test_select_month_with_custom_names + month_names = %w(nil Januar Februar Marts April Maj Juni Juli August September Oktober November December) + + expected = %(<select id="date_month" name="date[month]">\n) + 1.upto(12) { |month| expected << %(<option value="#{month}"#{' selected="selected"' if month == 8}>#{month_names[month]}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :use_month_names => month_names) + assert_dom_equal expected, select_month(8, :use_month_names => month_names) + end + + def test_select_month_with_zero_indexed_custom_names + month_names = %w(Januar Februar Marts April Maj Juni Juli August September Oktober November December) + + expected = %(<select id="date_month" name="date[month]">\n) + 1.upto(12) { |month| expected << %(<option value="#{month}"#{' selected="selected"' if month == 8}>#{month_names[month-1]}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :use_month_names => month_names) + assert_dom_equal expected, select_month(8, :use_month_names => month_names) + end + + def test_select_month_with_hidden + assert_dom_equal "<input type=\"hidden\" id=\"date_month\" name=\"date[month]\" value=\"8\" />\n", select_month(8, :use_hidden => true) + end + + def test_select_month_with_hidden_and_field_name + assert_dom_equal "<input type=\"hidden\" id=\"date_mois\" name=\"date[mois]\" value=\"8\" />\n", select_month(8, :use_hidden => true, :field_name => 'mois') + end + + def test_select_month_with_html_options + expected = %(<select id="date_month" name="date[month]" class="selector" accesskey="M">\n) + expected << %(<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 value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">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) + expected << "</select>\n" + + assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), {}, :class => 'selector', :accesskey => 'M') + #result = select_month(Time.mktime(2003, 8, 16), {}, :class => 'selector', :accesskey => 'M') + #assert result.include?('<select id="date_month" name="date[month]"') + #assert result.include?('class="selector"') + #assert result.include?('accesskey="M"') + #assert result.include?('<option value="1">January') + end + + def test_select_month_with_default_prompt + expected = %(<select id="date_month" name="date[month]">\n) + expected << %(<option value="">Month</option>\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 value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">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) + expected << "</select>\n" + + assert_dom_equal expected, select_month(8, :prompt => true) + end + + def test_select_month_with_custom_prompt + expected = %(<select id="date_month" name="date[month]">\n) + expected << %(<option value="">Choose month</option>\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 value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">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) + expected << "</select>\n" + + assert_dom_equal expected, select_month(8, :prompt => 'Choose month') + end + + def test_select_year + expected = %(<select id="date_year" name="date[year]">\n) + expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + assert_dom_equal expected, select_year(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005) + assert_dom_equal expected, select_year(2003, :start_year => 2003, :end_year => 2005) + end + + def test_select_year_with_disabled + expected = %(<select id="date_year" name="date[year]" disabled="disabled">\n) + expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + assert_dom_equal expected, select_year(Time.mktime(2003, 8, 16), :disabled => true, :start_year => 2003, :end_year => 2005) + assert_dom_equal expected, select_year(2003, :disabled => true, :start_year => 2003, :end_year => 2005) + end + + def test_select_year_with_field_name_override + expected = %(<select id="date_annee" name="date[annee]">\n) + expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + assert_dom_equal expected, select_year(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :field_name => 'annee') + assert_dom_equal expected, select_year(2003, :start_year => 2003, :end_year => 2005, :field_name => 'annee') + end + + def test_select_year_with_type_discarding + expected = %(<select id="date_year" name="date_year">\n) + expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + assert_dom_equal expected, select_year( + Time.mktime(2003, 8, 16), :prefix => "date_year", :discard_type => true, :start_year => 2003, :end_year => 2005) + assert_dom_equal expected, select_year( + 2003, :prefix => "date_year", :discard_type => true, :start_year => 2003, :end_year => 2005) + end + + def test_select_year_descending + expected = %(<select id="date_year" name="date[year]">\n) + expected << %(<option value="2005" selected="selected">2005</option>\n<option value="2004">2004</option>\n<option value="2003">2003</option>\n) + expected << "</select>\n" + + assert_dom_equal expected, select_year(Time.mktime(2005, 8, 16), :start_year => 2005, :end_year => 2003) + assert_dom_equal expected, select_year(2005, :start_year => 2005, :end_year => 2003) + end + + def test_select_year_with_hidden + assert_dom_equal "<input type=\"hidden\" id=\"date_year\" name=\"date[year]\" value=\"2007\" />\n", select_year(2007, :use_hidden => true) + end + + def test_select_year_with_hidden_and_field_name + assert_dom_equal "<input type=\"hidden\" id=\"date_anno\" name=\"date[anno]\" value=\"2007\" />\n", select_year(2007, :use_hidden => true, :field_name => 'anno') + end + + def test_select_year_with_html_options + expected = %(<select id="date_year" name="date[year]" class="selector" accesskey="M">\n) + expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + assert_dom_equal expected, select_year(Time.mktime(2003, 8, 16), {:start_year => 2003, :end_year => 2005}, :class => 'selector', :accesskey => 'M') + #result = select_year(Time.mktime(2003, 8, 16), {:start_year => 2003, :end_year => 2005}, :class => 'selector', :accesskey => 'M') + #assert result.include?('<select id="date_year" name="date[year]"') + #assert result.include?('class="selector"') + #assert result.include?('accesskey="M"') + #assert result.include?('<option value="2003"') + end + + def test_select_year_with_default_prompt + expected = %(<select id="date_year" name="date[year]">\n) + expected << %(<option value="">Year</option>\n<option value="2003">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + assert_dom_equal expected, select_year(nil, :start_year => 2003, :end_year => 2005, :prompt => true) + end + + def test_select_year_with_custom_prompt + expected = %(<select id="date_year" name="date[year]">\n) + expected << %(<option value="">Choose year</option>\n<option value="2003">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + assert_dom_equal expected, select_year(nil, :start_year => 2003, :end_year => 2005, :prompt => 'Choose year') + end + + def test_select_hour + expected = %(<select id="date_hour" name="date[hour]">\n) + expected << %(<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" selected="selected">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) + expected << "</select>\n" + + assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18)) + end + + def test_select_hour_with_ampm + expected = %(<select id="date_hour" name="date[hour]">\n) + expected << %(<option value="00">12 AM</option>\n<option value="01">01 AM</option>\n<option value="02">02 AM</option>\n<option value="03">03 AM</option>\n<option value="04">04 AM</option>\n<option value="05">05 AM</option>\n<option value="06">06 AM</option>\n<option value="07">07 AM</option>\n<option value="08" selected="selected">08 AM</option>\n<option value="09">09 AM</option>\n<option value="10">10 AM</option>\n<option value="11">11 AM</option>\n<option value="12">12 PM</option>\n<option value="13">01 PM</option>\n<option value="14">02 PM</option>\n<option value="15">03 PM</option>\n<option value="16">04 PM</option>\n<option value="17">05 PM</option>\n<option value="18">06 PM</option>\n<option value="19">07 PM</option>\n<option value="20">08 PM</option>\n<option value="21">09 PM</option>\n<option value="22">10 PM</option>\n<option value="23">11 PM</option>\n) + expected << "</select>\n" + + assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :ampm => true) + end + + def test_select_hour_with_disabled + expected = %(<select id="date_hour" name="date[hour]" disabled="disabled">\n) + expected << %(<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" selected="selected">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) + expected << "</select>\n" + + assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :disabled => true) + end + + def test_select_hour_with_field_name_override + expected = %(<select id="date_heure" name="date[heure]">\n) + expected << %(<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" selected="selected">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) + expected << "</select>\n" + + assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :field_name => 'heure') + end + + def test_select_hour_with_blank + expected = %(<select id="date_hour" name="date[hour]">\n) + expected << %(<option value=""></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" selected="selected">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) + expected << "</select>\n" + + assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :include_blank => true) + end + + def test_select_hour_nil_with_blank + expected = %(<select id="date_hour" name="date[hour]">\n) + expected << %(<option value=""></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">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) + expected << "</select>\n" + + assert_dom_equal expected, select_hour(nil, :include_blank => true) + end + + def test_select_hour_with_html_options + expected = %(<select id="date_hour" name="date[hour]" class="selector" accesskey="M">\n) + expected << %(<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" selected="selected">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) + expected << "</select>\n" + + assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), {}, :class => 'selector', :accesskey => 'M') + end + + def test_select_hour_with_default_prompt + expected = %(<select id="date_hour" name="date[hour]">\n) + expected << %(<option value="">Hour</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" selected="selected">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) + expected << "</select>\n" + + assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => true) + end + + def test_select_hour_with_custom_prompt + expected = %(<select id="date_hour" name="date[hour]">\n) + expected << %(<option value="">Choose hour</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" selected="selected">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) + expected << "</select>\n" + + assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => 'Choose hour') + end + + def test_select_minute + expected = %(<select id="date_minute" name="date[minute]">\n) + expected << %(<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" selected="selected">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 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_minute(Time.mktime(2003, 8, 16, 8, 4, 18)) + end + + def test_select_minute_with_disabled + expected = %(<select id="date_minute" name="date[minute]" disabled="disabled">\n) + expected << %(<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" selected="selected">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 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_minute(Time.mktime(2003, 8, 16, 8, 4, 18), :disabled => true) + end + + def test_select_minute_with_field_name_override + expected = %(<select id="date_minuto" name="date[minuto]">\n) + expected << %(<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" selected="selected">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 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_minute(Time.mktime(2003, 8, 16, 8, 4, 18), :field_name => 'minuto') + end + + def test_select_minute_with_blank + expected = %(<select id="date_minute" name="date[minute]">\n) + expected << %(<option value=""></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" selected="selected">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 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_minute(Time.mktime(2003, 8, 16, 8, 4, 18), :include_blank => true) + end + + def test_select_minute_with_blank_and_step + expected = %(<select id="date_minute" name="date[minute]">\n) + expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="15">15</option>\n<option value="30">30</option>\n<option value="45">45</option>\n) + expected << "</select>\n" + + assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), { :include_blank => true , :minute_step => 15 }) + end + + def test_select_minute_nil_with_blank + expected = %(<select id="date_minute" name="date[minute]">\n) + expected << %(<option value=""></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">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_minute(nil, :include_blank => true) + end + + def test_select_minute_nil_with_blank_and_step + expected = %(<select id="date_minute" name="date[minute]">\n) + expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="15">15</option>\n<option value="30">30</option>\n<option value="45">45</option>\n) + expected << "</select>\n" + + assert_dom_equal expected, select_minute(nil, { :include_blank => true , :minute_step => 15 }) + end + + def test_select_minute_with_hidden + assert_dom_equal "<input type=\"hidden\" id=\"date_minute\" name=\"date[minute]\" value=\"8\" />\n", select_minute(8, :use_hidden => true) + end + + def test_select_minute_with_hidden_and_field_name + assert_dom_equal "<input type=\"hidden\" id=\"date_minuto\" name=\"date[minuto]\" value=\"8\" />\n", select_minute(8, :use_hidden => true, :field_name => 'minuto') + end + + def test_select_minute_with_html_options + expected = %(<select id="date_minute" name="date[minute]" class="selector" accesskey="M">\n) + expected << %(<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" selected="selected">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 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_minute(Time.mktime(2003, 8, 16, 8, 4, 18), {}, :class => 'selector', :accesskey => 'M') + + #result = select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), {}, :class => 'selector', :accesskey => 'M') + #assert result.include?('<select id="date_minute" name="date[minute]"') + #assert result.include?('class="selector"') + #assert result.include?('accesskey="M"') + #assert result.include?('<option value="00">00') + end + + def test_select_minute_with_default_prompt + expected = %(<select id="date_minute" name="date[minute]">\n) + expected << %(<option value="">Minute</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" selected="selected">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 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_minute(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => true) + end + + def test_select_minute_with_custom_prompt + expected = %(<select id="date_minute" name="date[minute]">\n) + expected << %(<option value="">Choose minute</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" selected="selected">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 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_minute(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => 'Choose minute') + end + + def test_select_second + expected = %(<select id="date_second" name="date[second]">\n) + expected << %(<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_second(Time.mktime(2003, 8, 16, 8, 4, 18)) + end + + def test_select_second_with_disabled + expected = %(<select id="date_second" name="date[second]" disabled="disabled">\n) + expected << %(<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_second(Time.mktime(2003, 8, 16, 8, 4, 18), :disabled => true) + end + + def test_select_second_with_field_name_override + expected = %(<select id="date_segundo" name="date[segundo]">\n) + expected << %(<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_second(Time.mktime(2003, 8, 16, 8, 4, 18), :field_name => 'segundo') + end + + def test_select_second_with_blank + expected = %(<select id="date_second" name="date[second]">\n) + expected << %(<option value=""></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_second(Time.mktime(2003, 8, 16, 8, 4, 18), :include_blank => true) + end + + def test_select_second_nil_with_blank + expected = %(<select id="date_second" name="date[second]">\n) + expected << %(<option value=""></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">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_second(nil, :include_blank => true) + end + + def test_select_second_with_html_options + expected = %(<select id="date_second" name="date[second]" class="selector" accesskey="M">\n) + expected << %(<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_second(Time.mktime(2003, 8, 16, 8, 4, 18), {}, :class => 'selector', :accesskey => 'M') + + #result = select_second(Time.mktime(2003, 8, 16, 8, 4, 18), {}, :class => 'selector', :accesskey => 'M') + #assert result.include?('<select id="date_second" name="date[second]"') + #assert result.include?('class="selector"') + #assert result.include?('accesskey="M"') + #assert result.include?('<option value="00">00') + end + + def test_select_second_with_default_prompt + expected = %(<select id="date_second" name="date[second]">\n) + expected << %(<option value="">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_second(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => true) + end + + def test_select_second_with_custom_prompt + expected = %(<select id="date_second" name="date[second]">\n) + 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_second(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => 'Choose seconds') + end + + def test_select_date + expected = %(<select id="date_first_year" name="date[first][year]">\n) + expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + expected << %(<select id="date_first_month" name="date[first][month]">\n) + expected << %(<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 value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">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) + expected << "</select>\n" + + expected << %(<select id="date_first_day" name="date[first][day]">\n) + expected << %(<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 value="15">15</option>\n<option value="16" selected="selected">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) + expected << "</select>\n" + + assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :prefix => "date[first]") + end + + def test_select_date_with_too_big_range_between_start_year_and_end_year + assert_raise(ArgumentError) { select_date(Time.mktime(2003, 8, 16), :start_year => 2000, :end_year => 20000, :prefix => "date[first]", :order => [:month, :day, :year]) } + assert_raise(ArgumentError) { select_date(Time.mktime(2003, 8, 16), :start_year => Date.today.year - 100.years, :end_year => 2000, :prefix => "date[first]", :order => [:month, :day, :year]) } + end + + def test_select_date_can_have_more_then_1000_years_interval_if_forced_via_parameter + assert_nothing_raised { select_date(Time.mktime(2003, 8, 16), :start_year => 2000, :end_year => 3100, :max_years_allowed => 2000) } + end + + def test_select_date_with_order + expected = %(<select id="date_first_month" name="date[first][month]">\n) + expected << %(<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 value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">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) + expected << "</select>\n" + + expected << %(<select id="date_first_day" name="date[first][day]">\n) + expected << %(<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 value="15">15</option>\n<option value="16" selected="selected">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) + expected << "</select>\n" + + expected << %(<select id="date_first_year" name="date[first][year]">\n) + expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :prefix => "date[first]", :order => [:month, :day, :year]) + end + + def test_select_date_with_incomplete_order + # Since the order is incomplete nothing will be shown + expected = %(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003" />\n) + expected << %(<input id="date_first_month" name="date[first][month]" type="hidden" value="8" />\n) + expected << %(<input id="date_first_day" name="date[first][day]" type="hidden" value="1" />\n) + + assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :prefix => "date[first]", :order => [:day]) + end + + def test_select_date_with_disabled + expected = %(<select id="date_first_year" name="date[first][year]" disabled="disabled">\n) + expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + expected << %(<select id="date_first_month" name="date[first][month]" disabled="disabled">\n) + expected << %(<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 value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">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) + expected << "</select>\n" + + expected << %(<select id="date_first_day" name="date[first][day]" disabled="disabled">\n) + expected << %(<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 value="15">15</option>\n<option value="16" selected="selected">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) + expected << "</select>\n" + + assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :prefix => "date[first]", :disabled => true) + end + + def test_select_date_with_no_start_year + expected = %(<select id="date_first_year" name="date[first][year]">\n) + (Date.today.year-5).upto(Date.today.year+1) do |y| + if y == Date.today.year + expected << %(<option value="#{y}" selected="selected">#{y}</option>\n) + else + expected << %(<option value="#{y}">#{y}</option>\n) + end + end + expected << "</select>\n" + + expected << %(<select id="date_first_month" name="date[first][month]">\n) + expected << %(<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 value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">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) + expected << "</select>\n" + + expected << %(<select id="date_first_day" name="date[first][day]">\n) + expected << %(<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 value="15">15</option>\n<option value="16" selected="selected">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) + expected << "</select>\n" + + assert_dom_equal expected, select_date( + Time.mktime(Date.today.year, 8, 16), :end_year => Date.today.year+1, :prefix => "date[first]" + ) + end + + def test_select_date_with_no_end_year + expected = %(<select id="date_first_year" name="date[first][year]">\n) + 2003.upto(2008) do |y| + if y == 2003 + expected << %(<option value="#{y}" selected="selected">#{y}</option>\n) + else + expected << %(<option value="#{y}">#{y}</option>\n) + end + end + expected << "</select>\n" + + expected << %(<select id="date_first_month" name="date[first][month]">\n) + expected << %(<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 value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">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) + expected << "</select>\n" + + expected << %(<select id="date_first_day" name="date[first][day]">\n) + expected << %(<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 value="15">15</option>\n<option value="16" selected="selected">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) + expected << "</select>\n" + + assert_dom_equal expected, select_date( + Time.mktime(2003, 8, 16), :start_year => 2003, :prefix => "date[first]" + ) + end + + def test_select_date_with_no_start_or_end_year + expected = %(<select id="date_first_year" name="date[first][year]">\n) + (Date.today.year-5).upto(Date.today.year+5) do |y| + if y == Date.today.year + expected << %(<option value="#{y}" selected="selected">#{y}</option>\n) + else + expected << %(<option value="#{y}">#{y}</option>\n) + end + end + expected << "</select>\n" + + expected << %(<select id="date_first_month" name="date[first][month]">\n) + expected << %(<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 value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">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) + expected << "</select>\n" + + expected << %(<select id="date_first_day" name="date[first][day]">\n) + expected << %(<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 value="15">15</option>\n<option value="16" selected="selected">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) + expected << "</select>\n" + + assert_dom_equal expected, select_date( + Time.mktime(Date.today.year, 8, 16), :prefix => "date[first]" + ) + end + + def test_select_date_with_zero_value + expected = %(<select id="date_first_year" name="date[first][year]">\n) + expected << %(<option value="2003">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + expected << %(<select id="date_first_month" name="date[first][month]">\n) + expected << %(<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 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) + expected << "</select>\n" + + expected << %(<select id="date_first_day" name="date[first][day]">\n) + expected << %(<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 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) + expected << "</select>\n" + + assert_dom_equal expected, select_date(0, :start_year => 2003, :end_year => 2005, :prefix => "date[first]") + end + + def test_select_date_with_zero_value_and_no_start_year + expected = %(<select id="date_first_year" name="date[first][year]">\n) + (Date.today.year-5).upto(Date.today.year+1) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } + expected << "</select>\n" + + expected << %(<select id="date_first_month" name="date[first][month]">\n) + expected << %(<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 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) + expected << "</select>\n" + + expected << %(<select id="date_first_day" name="date[first][day]">\n) + expected << %(<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 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) + expected << "</select>\n" + + assert_dom_equal expected, select_date(0, :end_year => Date.today.year+1, :prefix => "date[first]") + end + + def test_select_date_with_zero_value_and_no_end_year + expected = %(<select id="date_first_year" name="date[first][year]">\n) + last_year = Time.now.year + 5 + 2003.upto(last_year) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } + expected << "</select>\n" + + expected << %(<select id="date_first_month" name="date[first][month]">\n) + expected << %(<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 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) + expected << "</select>\n" + + expected << %(<select id="date_first_day" name="date[first][day]">\n) + expected << %(<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 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) + expected << "</select>\n" + + assert_dom_equal expected, select_date(0, :start_year => 2003, :prefix => "date[first]") + end + + def test_select_date_with_zero_value_and_no_start_and_end_year + expected = %(<select id="date_first_year" name="date[first][year]">\n) + (Date.today.year-5).upto(Date.today.year+5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } + expected << "</select>\n" + + expected << %(<select id="date_first_month" name="date[first][month]">\n) + expected << %(<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 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) + expected << "</select>\n" + + expected << %(<select id="date_first_day" name="date[first][day]">\n) + expected << %(<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 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) + expected << "</select>\n" + + assert_dom_equal expected, select_date(0, :prefix => "date[first]") + end + + def test_select_date_with_nil_value_and_no_start_and_end_year + expected = %(<select id="date_first_year" name="date[first][year]">\n) + (Date.today.year-5).upto(Date.today.year+5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } + expected << "</select>\n" + + expected << %(<select id="date_first_month" name="date[first][month]">\n) + expected << %(<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 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) + expected << "</select>\n" + + expected << %(<select id="date_first_day" name="date[first][day]">\n) + expected << %(<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 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) + expected << "</select>\n" + + assert_dom_equal expected, select_date(nil, :prefix => "date[first]") + end + + def test_select_date_with_html_options + expected = %(<select id="date_first_year" name="date[first][year]" class="selector">\n) + expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + expected << %(<select id="date_first_month" name="date[first][month]" class="selector">\n) + expected << %(<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 value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">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) + expected << "</select>\n" + + expected << %(<select id="date_first_day" name="date[first][day]" class="selector">\n) + expected << %(<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 value="15">15</option>\n<option value="16" selected="selected">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) + expected << "</select>\n" + + assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), {:start_year => 2003, :end_year => 2005, :prefix => "date[first]"}, :class => "selector") + end + + def test_select_date_with_separator + expected = %(<select id="date_first_year" name="date[first][year]">\n) + expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + expected << " / " + + expected << %(<select id="date_first_month" name="date[first][month]">\n) + expected << %(<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 value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">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) + expected << "</select>\n" + + expected << " / " + + expected << %(<select id="date_first_day" name="date[first][day]">\n) + expected << %(<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 value="15">15</option>\n<option value="16" selected="selected">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) + expected << "</select>\n" + + assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { :date_separator => " / ", :start_year => 2003, :end_year => 2005, :prefix => "date[first]"}) + end + + def test_select_date_with_separator_and_discard_day + expected = %(<select id="date_first_year" name="date[first][year]">\n) + expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + expected << " / " + + expected << %(<select id="date_first_month" name="date[first][month]">\n) + expected << %(<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 value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">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) + expected << "</select>\n" + + expected << %(<input type="hidden" id="date_first_day" name="date[first][day]" value="1" />\n) + + assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { :date_separator => " / ", :discard_day => true, :start_year => 2003, :end_year => 2005, :prefix => "date[first]"}) + end + + def test_select_date_with_separator_discard_month_and_day + expected = %(<select id="date_first_year" name="date[first][year]">\n) + expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + expected << %(<input type="hidden" id="date_first_month" name="date[first][month]" value="8" />\n) + expected << %(<input type="hidden" id="date_first_day" name="date[first][day]" value="1" />\n) + + assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { :date_separator => " / ", :discard_month => true, :discard_day => true, :start_year => 2003, :end_year => 2005, :prefix => "date[first]"}) + end + + def test_select_date_with_hidden + expected = %(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003"/>\n) + expected << %(<input id="date_first_month" name="date[first][month]" type="hidden" value="8" />\n) + expected << %(<input id="date_first_day" name="date[first][day]" type="hidden" value="16" />\n) + + assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { :prefix => "date[first]", :use_hidden => true }) + assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { :date_separator => " / ", :prefix => "date[first]", :use_hidden => true }) + end + + def test_select_date_with_css_classes_option + expected = %(<select id="date_first_year" name="date[first][year]" class="year">\n) + expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + expected << %(<select id="date_first_month" name="date[first][month]" class="month">\n) + expected << %(<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 value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">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) + expected << "</select>\n" + + expected << %(<select id="date_first_day" name="date[first][day]" class="day">\n) + expected << %(<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 value="15">15</option>\n<option value="16" selected="selected">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) + expected << "</select>\n" + + assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), {:start_year => 2003, :end_year => 2005, :prefix => "date[first]", :with_css_classes => true}) + end + + def test_select_datetime + expected = %(<select id="date_first_year" name="date[first][year]">\n) + expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + expected << %(<select id="date_first_month" name="date[first][month]">\n) + expected << %(<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 value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">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) + expected << "</select>\n" + + expected << %(<select id="date_first_day" name="date[first][day]">\n) + expected << %(<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 value="15">15</option>\n<option value="16" selected="selected">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) + expected << "</select>\n" + + expected << " — " + + expected << %(<select id="date_first_hour" name="date[first][hour]">\n) + expected << %(<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" selected="selected">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) + expected << "</select>\n" + + expected << " : " + + expected << %(<select id="date_first_minute" name="date[first][minute]">\n) + expected << %(<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" selected="selected">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 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_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :start_year => 2003, :end_year => 2005, :prefix => "date[first]") + end + + def test_select_datetime_with_ampm + expected = %(<select id="date_first_year" name="date[first][year]">\n) + expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + expected << %(<select id="date_first_month" name="date[first][month]">\n) + expected << %(<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 value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">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) + expected << "</select>\n" + + expected << %(<select id="date_first_day" name="date[first][day]">\n) + expected << %(<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 value="15">15</option>\n<option value="16" selected="selected">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) + expected << "</select>\n" + + expected << " — " + + expected << %(<select id="date_first_hour" name="date[first][hour]">\n) + expected << %(<option value="00">12 AM</option>\n<option value="01">01 AM</option>\n<option value="02">02 AM</option>\n<option value="03">03 AM</option>\n<option value="04">04 AM</option>\n<option value="05">05 AM</option>\n<option value="06">06 AM</option>\n<option value="07">07 AM</option>\n<option value="08" selected="selected">08 AM</option>\n<option value="09">09 AM</option>\n<option value="10">10 AM</option>\n<option value="11">11 AM</option>\n<option value="12">12 PM</option>\n<option value="13">01 PM</option>\n<option value="14">02 PM</option>\n<option value="15">03 PM</option>\n<option value="16">04 PM</option>\n<option value="17">05 PM</option>\n<option value="18">06 PM</option>\n<option value="19">07 PM</option>\n<option value="20">08 PM</option>\n<option value="21">09 PM</option>\n<option value="22">10 PM</option>\n<option value="23">11 PM</option>\n) + expected << "</select>\n" + + expected << " : " + + expected << %(<select id="date_first_minute" name="date[first][minute]">\n) + expected << %(<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" selected="selected">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 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_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :start_year => 2003, :end_year => 2005, :prefix => "date[first]", :ampm => true) + end + + def test_select_datetime_with_separators + expected = %(<select id="date_first_year" name="date[first][year]">\n) + expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + expected << %(<select id="date_first_month" name="date[first][month]">\n) + expected << %(<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 value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">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) + expected << "</select>\n" + + expected << %(<select id="date_first_day" name="date[first][day]">\n) + expected << %(<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 value="15">15</option>\n<option value="16" selected="selected">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) + expected << "</select>\n" + + expected << " — " + + expected << %(<select id="date_first_hour" name="date[first][hour]">\n) + expected << %(<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" selected="selected">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) + expected << "</select>\n" + + expected << " : " + + expected << %(<select id="date_first_minute" name="date[first][minute]">\n) + expected << %(<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" selected="selected">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 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_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :start_year => 2003, :end_year => 2005, :prefix => "date[first]", :datetime_separator => ' — ', :time_separator => ' : ') + end + + def test_select_datetime_with_nil_value_and_no_start_and_end_year + expected = %(<select id="date_first_year" name="date[first][year]">\n) + (Date.today.year-5).upto(Date.today.year+5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } + expected << "</select>\n" + + expected << %(<select id="date_first_month" name="date[first][month]">\n) + expected << %(<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 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) + expected << "</select>\n" + + expected << %(<select id="date_first_day" name="date[first][day]">\n) + expected << %(<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 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) + expected << "</select>\n" + + expected << " — " + + expected << %(<select id="date_first_hour" name="date[first][hour]">\n) + expected << %(<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) + expected << "</select>\n" + + expected << " : " + + expected << %(<select id="date_first_minute" name="date[first][minute]">\n) + expected << %(<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 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_datetime(nil, :prefix => "date[first]") + end + + def test_select_datetime_with_html_options + expected = %(<select id="date_first_year" name="date[first][year]" class="selector">\n) + expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + + expected << %(<select id="date_first_month" name="date[first][month]" class="selector">\n) + expected << %(<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 value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">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) + expected << "</select>\n" + + expected << %(<select id="date_first_day" name="date[first][day]" class="selector">\n) + expected << %(<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 value="15">15</option>\n<option value="16" selected="selected">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) + expected << "</select>\n" + + expected << " — " + + expected << %(<select id="date_first_hour" name="date[first][hour]" class="selector">\n) + expected << %(<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" selected="selected">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) + expected << "</select>\n" + + expected << " : " + + expected << %(<select id="date_first_minute" name="date[first][minute]" class="selector">\n) + expected << %(<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" selected="selected">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 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_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), {:start_year => 2003, :end_year => 2005, :prefix => "date[first]"}, :class => 'selector') + end + + def test_select_datetime_with_all_separators + expected = %(<select id="date_first_year" name="date[first][year]" class="selector">\n) + expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + expected << "/" + + expected << %(<select id="date_first_month" name="date[first][month]" class="selector">\n) + expected << %(<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 value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">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) + expected << "</select>\n" + + expected << "/" + + expected << %(<select id="date_first_day" name="date[first][day]" class="selector">\n) + expected << %(<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 value="15">15</option>\n<option value="16" selected="selected">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) + expected << "</select>\n" + + expected << "—" + + expected << %(<select id="date_first_hour" name="date[first][hour]" class="selector">\n) + expected << %(<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" selected="selected">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) + expected << "</select>\n" + + expected << ":" + + expected << %(<select id="date_first_minute" name="date[first][minute]" class="selector">\n) + expected << %(<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" selected="selected">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 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_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), { :datetime_separator => "—", :date_separator => "/", :time_separator => ":", :start_year => 2003, :end_year => 2005, :prefix => "date[first]"}, :class => 'selector') + end + + def test_select_datetime_should_work_with_date + assert_nothing_raised { select_datetime(Date.today) } + end + + def test_select_datetime_with_default_prompt + expected = %(<select id="date_first_year" name="date[first][year]">\n) + expected << %(<option value="">Year</option>\n<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + expected << %(<select id="date_first_month" name="date[first][month]">\n) + expected << %(<option value="">Month</option>\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 value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">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) + expected << "</select>\n" + + expected << %(<select id="date_first_day" name="date[first][day]">\n) + expected << %(<option value="">Day</option>\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 value="15">15</option>\n<option value="16" selected="selected">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) + expected << "</select>\n" + + expected << " — " + + expected << %(<select id="date_first_hour" name="date[first][hour]">\n) + expected << %(<option value="">Hour</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" selected="selected">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) + expected << "</select>\n" + + expected << " : " + + expected << %(<select id="date_first_minute" name="date[first][minute]">\n) + expected << %(<option value="">Minute</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" selected="selected">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 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_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :start_year => 2003, :end_year => 2005, + :prefix => "date[first]", :prompt => true) + end + + def test_select_datetime_with_custom_prompt + + expected = %(<select id="date_first_year" name="date[first][year]">\n) + expected << %(<option value="">Choose year</option>\n<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + expected << %(<select id="date_first_month" name="date[first][month]">\n) + expected << %(<option value="">Choose month</option>\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 value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">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) + expected << "</select>\n" + + expected << %(<select id="date_first_day" name="date[first][day]">\n) + expected << %(<option value="">Choose day</option>\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 value="15">15</option>\n<option value="16" selected="selected">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) + expected << "</select>\n" + + expected << " — " + + expected << %(<select id="date_first_hour" name="date[first][hour]">\n) + expected << %(<option value="">Choose hour</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" selected="selected">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) + expected << "</select>\n" + + expected << " : " + + expected << %(<select id="date_first_minute" name="date[first][minute]">\n) + expected << %(<option value="">Choose minute</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" selected="selected">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 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_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :start_year => 2003, :end_year => 2005, :prefix => "date[first]", + :prompt => {:day => 'Choose day', :month => 'Choose month', :year => 'Choose year', :hour => 'Choose hour', :minute => 'Choose minute'}) + end + + def test_select_datetime_with_custom_hours + expected = %(<select id="date_first_year" name="date[first][year]">\n) + expected << %(<option value="">Choose year</option>\n<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + expected << %(<select id="date_first_month" name="date[first][month]">\n) + expected << %(<option value="">Choose month</option>\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 value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">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) + expected << "</select>\n" + + expected << %(<select id="date_first_day" name="date[first][day]">\n) + expected << %(<option value="">Choose day</option>\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 value="15">15</option>\n<option value="16" selected="selected">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) + expected << "</select>\n" + + expected << " — " + + expected << %(<select id="date_first_hour" name="date[first][hour]">\n) + expected << %(<option value="">Choose hour</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" selected="selected">08</option>\n<option value="09">09</option>\n) + expected << "</select>\n" + + expected << " : " + + expected << %(<select id="date_first_minute" name="date[first][minute]">\n) + expected << %(<option value="">Choose minute</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" selected="selected">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 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_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :start_year => 2003, :end_year => 2005, :start_hour => 1, :end_hour => 9, :prefix => "date[first]", + :prompt => {:day => 'Choose day', :month => 'Choose month', :year => 'Choose year', :hour => 'Choose hour', :minute => 'Choose minute'}) + end + + def test_select_datetime_with_hidden + expected = %(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003" />\n) + expected << %(<input id="date_first_month" name="date[first][month]" type="hidden" value="8" />\n) + expected << %(<input id="date_first_day" name="date[first][day]" type="hidden" value="16" />\n) + expected << %(<input id="date_first_hour" name="date[first][hour]" type="hidden" value="8" />\n) + expected << %(<input id="date_first_minute" name="date[first][minute]" type="hidden" value="4" />\n) + + assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :prefix => "date[first]", :use_hidden => true) + assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :datetime_separator => "—", :date_separator => "/", + :time_separator => ":", :prefix => "date[first]", :use_hidden => true) + end + + def test_select_time + expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) + expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) + expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) + + expected << %(<select id="date_hour" name="date[hour]">\n) + expected << %(<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" selected="selected">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) + expected << "</select>\n" + + expected << " : " + + expected << %(<select id="date_minute" name="date[minute]">\n) + expected << %(<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" selected="selected">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 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)) + assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :include_seconds => false) + end + + def test_select_time_with_ampm + expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) + expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) + expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) + + expected << %(<select id="date_hour" name="date[hour]">\n) + expected << %(<option value="00">12 AM</option>\n<option value="01">01 AM</option>\n<option value="02">02 AM</option>\n<option value="03">03 AM</option>\n<option value="04">04 AM</option>\n<option value="05">05 AM</option>\n<option value="06">06 AM</option>\n<option value="07">07 AM</option>\n<option value="08" selected="selected">08 AM</option>\n<option value="09">09 AM</option>\n<option value="10">10 AM</option>\n<option value="11">11 AM</option>\n<option value="12">12 PM</option>\n<option value="13">01 PM</option>\n<option value="14">02 PM</option>\n<option value="15">03 PM</option>\n<option value="16">04 PM</option>\n<option value="17">05 PM</option>\n<option value="18">06 PM</option>\n<option value="19">07 PM</option>\n<option value="20">08 PM</option>\n<option value="21">09 PM</option>\n<option value="22">10 PM</option>\n<option value="23">11 PM</option>\n) + expected << "</select>\n" + + expected << " : " + + expected << %(<select id="date_minute" name="date[minute]">\n) + expected << %(<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" selected="selected">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 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), :include_seconds => false, :ampm => true) + end + + def test_select_time_with_separator + expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) + expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) + expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) + expected << %(<select id="date_hour" name="date[hour]">\n) + expected << %(<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" selected="selected">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) + expected << "</select>\n" + + expected << " : " + + expected << %(<select id="date_minute" name="date[minute]">\n) + expected << %(<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" selected="selected">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 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), :time_separator => ' : ') + assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :time_separator => ' : ', :include_seconds => false) + end + + def test_select_time_with_seconds + expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) + expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) + expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) + + expected << %(<select id="date_hour" name="date[hour]">\n) + expected << %(<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" selected="selected">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) + expected << "</select>\n" + + expected << ' : ' + + expected << %(<select id="date_minute" name="date[minute]">\n) + expected << %(<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" selected="selected">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 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" + + expected << ' : ' + + expected << %(<select id="date_second" name="date[second]">\n) + expected << %(<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), :include_seconds => true) + end + + def test_select_time_with_seconds_and_separator + expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) + expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) + expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) + + expected << %(<select id="date_hour" name="date[hour]">\n) + expected << %(<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" selected="selected">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) + expected << "</select>\n" + + expected << " : " + + expected << %(<select id="date_minute" name="date[minute]">\n) + expected << %(<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" selected="selected">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 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" + + expected << " : " + + expected << %(<select id="date_second" name="date[second]">\n) + expected << %(<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), :include_seconds => true, :time_separator => ' : ') + end + + def test_select_time_with_html_options + expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) + expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) + expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) + + expected << %(<select id="date_hour" name="date[hour]" class="selector">\n) + expected << %(<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" selected="selected">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) + expected << "</select>\n" + + expected << " : " + + expected << %(<select id="date_minute" name="date[minute]" class="selector">\n) + expected << %(<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" selected="selected">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 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), {}, :class => 'selector') + assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), {:include_seconds => false}, :class => 'selector') + end + + def test_select_time_should_work_with_date + assert_nothing_raised { select_time(Date.today) } + end + + def test_select_time_with_default_prompt + expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) + expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) + expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) + + expected << %(<select id="date_hour" name="date[hour]">\n) + expected << %(<option value="">Hour</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" selected="selected">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) + expected << "</select>\n" + + expected << " : " + + expected << %(<select id="date_minute" name="date[minute]">\n) + expected << %(<option value="">Minute</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" selected="selected">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 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" + + expected << " : " + + expected << %(<select id="date_second" name="date[second]">\n) + expected << %(<option value="">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), :include_seconds => true, :prompt => true) + end + + def test_select_time_with_custom_prompt + expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) + expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) + expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) + + expected << %(<select id="date_hour" name="date[hour]">\n) + expected << %(<option value="">Choose hour</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" selected="selected">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) + expected << "</select>\n" + + expected << " : " + + expected << %(<select id="date_minute" name="date[minute]">\n) + expected << %(<option value="">Choose minute</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" selected="selected">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 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" + + expected << " : " + + expected << %(<select id="date_second" name="date[second]">\n) + 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, + :prompt => {:hour => 'Choose hour', :minute => 'Choose minute', :second => 'Choose seconds'}) + end + + def test_select_time_with_hidden + expected = %(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003" />\n) + expected << %(<input id="date_first_month" name="date[first][month]" type="hidden" value="8" />\n) + expected << %(<input id="date_first_day" name="date[first][day]" type="hidden" value="16" />\n) + expected << %(<input id="date_first_hour" name="date[first][hour]" type="hidden" value="8" />\n) + expected << %(<input id="date_first_minute" name="date[first][minute]" type="hidden" value="4" />\n) + + assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :prefix => "date[first]", :use_hidden => true) + assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :time_separator => ":", :prefix => "date[first]", :use_hidden => true) + end + + def test_date_select + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + + expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} + expected << %{<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 value="2004" selected="selected">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} + expected << "</select>\n" + + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n} + expected << %{<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 value="6" selected="selected">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} + expected << "</select>\n" + + expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n} + expected << %{<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 value="15" selected="selected">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} + + expected << "</select>\n" + + assert_dom_equal expected, date_select("post", "written_on") + end + + def test_date_select_with_selected + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + + expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} + expected << %{<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} + expected << "</select>\n" + + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n} + expected << %{<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 value="6">June</option>\n<option value="7" selected="selected">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} + expected << "</select>\n" + + expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n} + expected << %{<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" selected="selected">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} + + expected << "</select>\n" + + assert_dom_equal expected, date_select("post", "written_on", :selected => Date.new(2004, 07, 10)) + end + + def test_date_select_with_selected_nil + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + + expected = '<input id="post_written_on_1i" name="post[written_on(1i)]" type="hidden" value="1"/>' + "\n" + + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n} + expected << %{<option value=""></option>\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 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} + expected << "</select>\n" + + expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n} + expected << %{<option value=""></option>\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 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} + + expected << "</select>\n" + + assert_dom_equal expected, date_select("post", "written_on", include_blank: true, discard_year: true, selected: nil) + end + + def test_date_select_without_day + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + + expected = "<input type=\"hidden\" id=\"post_written_on_3i\" name=\"post[written_on(3i)]\" value=\"1\" />\n" + + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n} + expected << %{<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 value="6" selected="selected">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} + expected << "</select>\n" + + expected << %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} + expected << %{<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 value="2004" selected="selected">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} + expected << "</select>\n" + + assert_dom_equal expected, date_select("post", "written_on", :order => [ :month, :year ]) + end + + def test_date_select_without_day_and_month + @post = Post.new + @post.written_on = Date.new(2004, 2, 29) + + expected = "<input type=\"hidden\" id=\"post_written_on_2i\" name=\"post[written_on(2i)]\" value=\"2\" />\n" + expected << "<input type=\"hidden\" id=\"post_written_on_3i\" name=\"post[written_on(3i)]\" value=\"1\" />\n" + + expected << %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} + expected << %{<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 value="2004" selected="selected">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} + expected << "</select>\n" + + assert_dom_equal expected, date_select("post", "written_on", :order => [ :year ]) + end + + def test_date_select_without_day_with_separator + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + + expected = "<input type=\"hidden\" id=\"post_written_on_3i\" name=\"post[written_on(3i)]\" value=\"1\" />\n" + + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n} + expected << %{<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 value="6" selected="selected">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} + expected << "</select>\n" + + expected << "/" + + expected << %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} + expected << %{<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 value="2004" selected="selected">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} + expected << "</select>\n" + + assert_dom_equal expected, date_select("post", "written_on", :date_separator => '/', :order => [ :month, :year ]) + end + + def test_date_select_without_day_and_with_disabled_html_option + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + + expected = "<input type=\"hidden\" id=\"post_written_on_3i\" disabled=\"disabled\" name=\"post[written_on(3i)]\" value=\"1\" />\n" + + expected << %{<select id="post_written_on_2i" disabled="disabled" name="post[written_on(2i)]">\n} + expected << %{<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 value="6" selected="selected">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} + expected << "</select>\n" + + expected << %{<select id="post_written_on_1i" disabled="disabled" name="post[written_on(1i)]">\n} + expected << %{<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 value="2004" selected="selected">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} + expected << "</select>\n" + + assert_dom_equal expected, date_select("post", "written_on", { :order => [ :month, :year ] }, :disabled => true) + end + + def test_date_select_within_fields_for + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + + output_buffer = fields_for :post, @post do |f| + 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" + + assert_dom_equal(expected, output_buffer) + end + + def test_date_select_within_fields_for_with_index + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + id = 27 + + output_buffer = fields_for :post, @post, :index => id do |f| + 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" + + assert_dom_equal(expected, output_buffer) + end + + def test_date_select_within_fields_for_with_blank_index + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + id = nil + + output_buffer = fields_for :post, @post, :index => id do |f| + 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" + + assert_dom_equal(expected, output_buffer) + end + + def test_date_select_with_index + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + id = 456 + + expected = %{<select id="post_456_written_on_1i" name="post[#{id}][written_on(1i)]">\n} + expected << %{<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 value="2004" selected="selected">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} + expected << "</select>\n" + + expected << %{<select id="post_456_written_on_2i" name="post[#{id}][written_on(2i)]">\n} + expected << %{<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 value="6" selected="selected">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} + expected << "</select>\n" + + expected << %{<select id="post_456_written_on_3i" name="post[#{id}][written_on(3i)]">\n} + expected << %{<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 value="15" selected="selected">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} + expected << "</select>\n" + + assert_dom_equal expected, date_select("post", "written_on", :index => id) + end + + def test_date_select_with_auto_index + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + id = 123 + + expected = %{<select id="post_123_written_on_1i" name="post[#{id}][written_on(1i)]">\n} + expected << %{<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 value="2004" selected="selected">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} + expected << "</select>\n" + + expected << %{<select id="post_123_written_on_2i" name="post[#{id}][written_on(2i)]">\n} + expected << %{<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 value="6" selected="selected">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} + expected << "</select>\n" + + expected << %{<select id="post_123_written_on_3i" name="post[#{id}][written_on(3i)]">\n} + expected << %{<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 value="15" selected="selected">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} + expected << "</select>\n" + + assert_dom_equal expected, date_select("post[]", "written_on") + end + + def test_date_select_with_different_order + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + + expected = %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n} + 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 15}>#{i}</option>\n) } + expected << "</select>\n" + + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n} + 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 6}>#{Date::MONTHNAMES[i]}</option>\n) } + expected << "</select>\n" + + expected << %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} + 1999.upto(2009) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2004}>#{i}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, date_select("post", "written_on", :order => [:day, :month, :year]) + end + + def test_date_select_with_nil + @post = Post.new + + start_year = Time.now.year-5 + end_year = Time.now.year+5 + expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} + start_year.upto(end_year) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == Time.now.year}>#{i}</option>\n) } + expected << "</select>\n" + + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n} + 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == Time.now.month}>#{Date::MONTHNAMES[i]}</option>\n) } + expected << "</select>\n" + + expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n} + 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == Time.now.day}>#{i}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, date_select("post", "written_on") + end + + def test_date_select_with_nil_and_blank + @post = Post.new + + start_year = Time.now.year-5 + end_year = Time.now.year+5 + expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} + expected << "<option value=\"\"></option>\n" + start_year.upto(end_year) { |i| expected << %(<option value="#{i}">#{i}</option>\n) } + expected << "</select>\n" + + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n} + expected << "<option value=\"\"></option>\n" + 1.upto(12) { |i| expected << %(<option value="#{i}">#{Date::MONTHNAMES[i]}</option>\n) } + expected << "</select>\n" + + expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n} + expected << "<option value=\"\"></option>\n" + 1.upto(31) { |i| expected << %(<option value="#{i}">#{i}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, date_select("post", "written_on", :include_blank => true) + end + + def test_date_select_with_nil_and_blank_and_order + @post = Post.new + + start_year = Time.now.year-5 + end_year = Time.now.year+5 + + expected = '<input name="post[written_on(3i)]" type="hidden" id="post_written_on_3i" value="1"/>' + "\n" + expected << %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} + expected << "<option value=\"\"></option>\n" + start_year.upto(end_year) { |i| expected << %(<option value="#{i}">#{i}</option>\n) } + expected << "</select>\n" + + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n} + expected << "<option value=\"\"></option>\n" + 1.upto(12) { |i| expected << %(<option value="#{i}">#{Date::MONTHNAMES[i]}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, date_select("post", "written_on", :order=>[:year, :month], :include_blank=>true) + end + + def test_date_select_with_nil_and_blank_and_discard_month + @post = Post.new + + start_year = Time.now.year-5 + end_year = Time.now.year+5 + + expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} + expected << "<option value=\"\"></option>\n" + start_year.upto(end_year) { |i| expected << %(<option value="#{i}">#{i}</option>\n) } + expected << "</select>\n" + expected << '<input name="post[written_on(2i)]" type="hidden" id="post_written_on_2i" value="1"/>' + "\n" + expected << '<input name="post[written_on(3i)]" type="hidden" id="post_written_on_3i" value="1"/>' + "\n" + + assert_dom_equal expected, date_select("post", "written_on", :discard_month => true, :include_blank=>true) + end + + def test_date_select_with_nil_and_blank_and_discard_year + @post = Post.new + + expected = '<input id="post_written_on_1i" name="post[written_on(1i)]" type="hidden" value="1" />' + "\n" + + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n} + expected << "<option value=\"\"></option>\n" + 1.upto(12) { |i| expected << %(<option value="#{i}">#{Date::MONTHNAMES[i]}</option>\n) } + expected << "</select>\n" + + expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n} + expected << "<option value=\"\"></option>\n" + 1.upto(31) { |i| expected << %(<option value="#{i}">#{i}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, date_select("post", "written_on", :discard_year => true, :include_blank=>true) + end + + def test_date_select_cant_override_discard_hour + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + + expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} + expected << %{<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 value="2004" selected="selected">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} + expected << "</select>\n" + + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n} + expected << %{<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 value="6" selected="selected">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} + expected << "</select>\n" + + expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n} + expected << %{<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 value="15" selected="selected">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} + expected << "</select>\n" + + assert_dom_equal expected, date_select("post", "written_on", :discard_hour => false) + end + + def test_date_select_with_html_options + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + + expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]" class="selector">\n} + expected << %{<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 value="2004" selected="selected">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} + expected << "</select>\n" + + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]" class="selector">\n} + expected << %{<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 value="6" selected="selected">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} + expected << "</select>\n" + + expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]" class="selector">\n} + expected << %{<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 value="15" selected="selected">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} + + expected << "</select>\n" + + assert_dom_equal expected, date_select("post", "written_on", {}, :class => 'selector') + end + + def test_date_select_with_html_options_within_fields_for + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + + output_buffer = fields_for :post, @post do |f| + concat f.date_select(:written_on, {}, :class => 'selector') + end + + expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]" class="selector">\n} + expected << %{<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 value="2004" selected="selected">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} + expected << "</select>\n" + + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]" class="selector">\n} + expected << %{<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 value="6" selected="selected">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} + expected << "</select>\n" + + expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]" class="selector">\n} + expected << %{<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 value="15" selected="selected">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} + + expected << "</select>\n" + + assert_dom_equal expected, output_buffer + end + + def test_date_select_with_separator + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + + expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} + expected << %{<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 value="2004" selected="selected">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} + expected << "</select>\n" + + expected << " / " + + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n} + expected << %{<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 value="6" selected="selected">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} + expected << "</select>\n" + + expected << " / " + + expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n} + expected << %{<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 value="15" selected="selected">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} + + expected << "</select>\n" + + assert_dom_equal expected, date_select("post", "written_on", { :date_separator => " / " }) + end + + def test_date_select_with_separator_and_order + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + + expected = %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n} + expected << %{<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 value="15" selected="selected">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} + expected << "</select>\n" + + expected << " / " + + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n} + expected << %{<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 value="6" selected="selected">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} + expected << "</select>\n" + + expected << " / " + + expected << %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} + expected << %{<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 value="2004" selected="selected">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} + expected << "</select>\n" + + assert_dom_equal expected, date_select("post", "written_on", { :order => [:day, :month, :year], :date_separator => " / " }) + end + + def test_date_select_with_separator_and_order_and_year_discarded + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + + expected = %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n} + expected << %{<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 value="15" selected="selected">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} + expected << "</select>\n" + + expected << " / " + + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n} + expected << %{<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 value="6" selected="selected">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} + expected << "</select>\n" + expected << %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n} + + assert_dom_equal expected, date_select("post", "written_on", { :order => [:day, :month, :year], :discard_year => true, :date_separator => " / " }) + end + + def test_date_select_with_default_prompt + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + + expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} + expected << %{<option value="">Year</option>\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 value="2004" selected="selected">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} + expected << "</select>\n" + + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n} + expected << %{<option value="">Month</option>\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 value="6" selected="selected">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} + expected << "</select>\n" + + expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n} + expected << %{<option value="">Day</option>\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 value="15" selected="selected">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} + + expected << "</select>\n" + + assert_dom_equal expected, date_select("post", "written_on", :prompt => true) + end + + def test_date_select_with_custom_prompt + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + + expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} + expected << %{<option value="">Choose year</option>\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 value="2004" selected="selected">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} + expected << "</select>\n" + + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n} + expected << %{<option value="">Choose month</option>\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 value="6" selected="selected">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} + expected << "</select>\n" + + expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n} + expected << %{<option value="">Choose day</option>\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 value="15" selected="selected">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} + + expected << "</select>\n" + + assert_dom_equal expected, date_select("post", "written_on", :prompt => {:year => 'Choose year', :month => 'Choose month', :day => 'Choose day'}) + end + + def test_time_select + @post = Post.new + @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) + + expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n} + expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n} + expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n} + + expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]">\n) + 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + expected << " : " + expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]">\n) + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, time_select("post", "written_on") + end + + def test_time_select_with_selected + @post = Post.new + @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) + + expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n} + expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n} + expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n} + + expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]">\n) + 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 12}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + expected << " : " + expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]">\n) + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 20}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, time_select("post", "written_on", selected: Time.local(2004, 6, 15, 12, 20, 30)) + end + + def test_time_select_with_selected_nil + @post = Post.new + @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) + + expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="1" />\n} + expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="1" />\n} + expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="1" />\n} + + expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]">\n) + 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}">#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + expected << " : " + expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]">\n) + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}">#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, time_select("post", "written_on", discard_year: true, discard_month: true, discard_day: true, selected: nil) + end + + def test_time_select_without_date_hidden_fields + @post = Post.new + @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) + + expected = %(<select id="post_written_on_4i" name="post[written_on(4i)]">\n) + 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + expected << " : " + expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]">\n) + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, time_select("post", "written_on", :ignore_date => true) + end + + def test_time_select_with_seconds + @post = Post.new + @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) + + expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n} + expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n} + expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n} + + expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]">\n) + 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + expected << " : " + expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]">\n) + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + expected << " : " + expected << %(<select id="post_written_on_6i" name="post[written_on(6i)]">\n) + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 35}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, time_select("post", "written_on", :include_seconds => true) + end + + def test_time_select_with_html_options + @post = Post.new + @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) + + expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n} + expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n} + expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n} + + expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]" class="selector">\n) + 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + expected << " : " + expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]" class="selector">\n) + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, time_select("post", "written_on", {}, :class => 'selector') + end + + def test_time_select_with_html_options_within_fields_for + @post = Post.new + @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) + + output_buffer = fields_for :post, @post do |f| + concat f.time_select(:written_on, {}, :class => 'selector') + end + + expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n} + expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n} + expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n} + + expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]" class="selector">\n) + 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + expected << " : " + expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]" class="selector">\n) + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, output_buffer + end + + def test_time_select_with_separator + @post = Post.new + @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) + + expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n} + expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n} + expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n} + + expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]">\n) + 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + expected << " - " + + expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]">\n) + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + expected << " - " + + expected << %(<select id="post_written_on_6i" name="post[written_on(6i)]">\n) + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 35}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, time_select("post", "written_on", { :time_separator => " - ", :include_seconds => true }) + end + + def test_time_select_with_default_prompt + @post = Post.new + @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) + + expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n} + expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n} + expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n} + + expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]">\n) + expected << %(<option value="">Hour</option>\n) + 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + expected << " : " + expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]">\n) + expected << %(<option value="">Minute</option>\n) + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, time_select("post", "written_on", :prompt => true) + end + + def test_time_select_with_custom_prompt + @post = Post.new + @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) + + expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n} + expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n} + expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n} + + expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]">\n) + expected << %(<option value="">Choose hour</option>\n) + 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + expected << " : " + expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]">\n) + expected << %(<option value="">Choose minute</option>\n) + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, time_select("post", "written_on", :prompt => {:hour => 'Choose hour', :minute => 'Choose minute'}) + end + + def test_time_select_with_disabled_html_option + @post = Post.new + @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) + + expected = %{<input type="hidden" id="post_written_on_1i" disabled="disabled" name="post[written_on(1i)]" value="2004" />\n} + expected << %{<input type="hidden" id="post_written_on_2i" disabled="disabled" name="post[written_on(2i)]" value="6" />\n} + expected << %{<input type="hidden" id="post_written_on_3i" disabled="disabled" name="post[written_on(3i)]" value="15" />\n} + + expected << %(<select id="post_written_on_4i" disabled="disabled" name="post[written_on(4i)]">\n) + 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + expected << " : " + expected << %(<select id="post_written_on_5i" disabled="disabled" name="post[written_on(5i)]">\n) + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, time_select("post", "written_on", {}, :disabled => true) + end + + def test_datetime_select + @post = Post.new + @post.updated_at = Time.local(2004, 6, 15, 16, 35) + + expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n} + expected << %{<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 value="2004" selected="selected">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} + expected << "</select>\n" + + expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n} + expected << %{<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 value="6" selected="selected">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} + expected << "</select>\n" + + expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n} + expected << %{<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 value="15" selected="selected">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} + expected << "</select>\n" + + expected << " — " + + expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n} + expected << %{<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" selected="selected">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} + expected << "</select>\n" + expected << " : " + expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n} + expected << %{<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 value="35" selected="selected">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, datetime_select("post", "updated_at") + end + + def test_datetime_select_with_selected + @post = Post.new + @post.updated_at = Time.local(2004, 6, 15, 16, 35) + + expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n} + expected << %{<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 value="2004" selected="selected">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} + expected << "</select>\n" + + expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n} + expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3" selected="selected">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option 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} + expected << "</select>\n" + + expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n} + expected << %{<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" selected="selected">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} + expected << "</select>\n" + + expected << " — " + + expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n} + expected << %{<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" selected="selected">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} + expected << "</select>\n" + expected << " : " + expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n} + expected << %{<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" selected="selected">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, datetime_select("post", "updated_at", :selected => Time.local(2004, 3, 10, 12, 30)) + end + + def test_datetime_select_with_selected_nil + @post = Post.new + @post.updated_at = Time.local(2004, 6, 15, 16, 35) + + expected = '<input id="post_updated_at_1i" name="post[updated_at(1i)]" type="hidden" value="1"/>' + "\n" + + expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n} + expected << %{<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 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} + expected << "</select>\n" + + expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n} + expected << %{<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 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} + expected << "</select>\n" + + expected << " — " + + expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n} + expected << %{<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} + expected << "</select>\n" + expected << " : " + expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n} + expected << %{<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 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, datetime_select("post", "updated_at", discard_year: true, selected: nil) + end + + def test_datetime_select_defaults_to_time_zone_now_when_config_time_zone_is_set + # The love zone is UTC+0 + mytz = Class.new(ActiveSupport::TimeZone) { + attr_accessor :now + }.create('tenderlove', 0) + + now = Time.mktime(2004, 6, 15, 16, 35, 0) + mytz.now = now + Time.zone = mytz + + assert_equal mytz, Time.zone + + @post = Post.new + + expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n} + expected << %{<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 value="2004" selected="selected">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} + expected << "</select>\n" + + expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n} + expected << %{<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 value="6" selected="selected">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} + expected << "</select>\n" + + expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n} + expected << %{<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 value="15" selected="selected">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} + expected << "</select>\n" + + expected << " — " + + expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n} + expected << %{<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" selected="selected">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} + expected << "</select>\n" + expected << " : " + expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n} + expected << %{<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 value="35" selected="selected">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, datetime_select("post", "updated_at") + ensure + Time.zone = nil + end + + def test_datetime_select_with_html_options_within_fields_for + @post = Post.new + @post.updated_at = Time.local(2004, 6, 15, 16, 35) + + output_buffer = fields_for :post, @post do |f| + 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 << " — <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 + + def test_datetime_select_with_separators + @post = Post.new + @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35) + + expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n} + expected << %{<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 value="2004" selected="selected">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} + expected << "</select>\n" + + expected << " / " + + expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n} + expected << %{<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 value="6" selected="selected">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} + expected << "</select>\n" + + expected << " / " + + expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n} + expected << %{<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 value="15" selected="selected">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} + expected << "</select>\n" + + expected << " , " + + expected << %(<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n) + 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + expected << " - " + + expected << %(<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n) + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + expected << " - " + + expected << %(<select id="post_updated_at_6i" name="post[updated_at(6i)]">\n) + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 35}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, datetime_select("post", "updated_at", { :date_separator => " / ", :datetime_separator => " , ", :time_separator => " - ", :include_seconds => true }) + end + + def test_datetime_select_with_integer + @post = Post.new + @post.updated_at = 3 + datetime_select("post", "updated_at") + end + + def test_datetime_select_with_infinity # Float + @post = Post.new + @post.updated_at = (-1.0/0) + datetime_select("post", "updated_at") + end + + def test_datetime_select_with_default_prompt + @post = Post.new + @post.updated_at = nil + + expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n} + expected << %{<option value="">Year</option>\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 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} + expected << "</select>\n" + + expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n} + expected << %{<option value="">Month</option>\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 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} + expected << "</select>\n" + + expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n} + expected << %{<option value="">Day</option>\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 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} + expected << "</select>\n" + + expected << " — " + + expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n} + expected << %{<option value="">Hour</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">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} + expected << "</select>\n" + expected << " : " + expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n} + expected << %{<option value="">Minute</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">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, datetime_select("post", "updated_at", :start_year=>1999, :end_year=>2009, :prompt => true) + end + + def test_datetime_select_with_custom_prompt + @post = Post.new + @post.updated_at = nil + + expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n} + expected << %{<option value="">Choose year</option>\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 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} + expected << "</select>\n" + + expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n} + expected << %{<option value="">Choose month</option>\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 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} + expected << "</select>\n" + + expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n} + expected << %{<option value="">Choose day</option>\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 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} + expected << "</select>\n" + + expected << " — " + + expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n} + expected << %{<option value="">Choose hour</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">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} + expected << "</select>\n" + expected << " : " + expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n} + expected << %{<option value="">Choose minute</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">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, datetime_select("post", "updated_at", :start_year=>1999, :end_year=>2009, :prompt => {:year => 'Choose year', :month => 'Choose month', :day => 'Choose day', :hour => 'Choose hour', :minute => 'Choose minute'}) + end + + def test_date_select_with_zero_value_and_no_start_year + expected = %(<select id="date_first_year" name="date[first][year]">\n) + (Date.today.year-5).upto(Date.today.year+1) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } + expected << "</select>\n" + + expected << %(<select id="date_first_month" name="date[first][month]">\n) + expected << %(<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 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) + expected << "</select>\n" + + expected << %(<select id="date_first_day" name="date[first][day]">\n) + expected << %(<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 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) + expected << "</select>\n" + + assert_dom_equal expected, select_date(0, :end_year => Date.today.year+1, :prefix => "date[first]") + end + + def test_date_select_with_zero_value_and_no_end_year + expected = %(<select id="date_first_year" name="date[first][year]">\n) + last_year = Time.now.year + 5 + 2003.upto(last_year) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } + expected << "</select>\n" + + expected << %(<select id="date_first_month" name="date[first][month]">\n) + expected << %(<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 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) + expected << "</select>\n" + + expected << %(<select id="date_first_day" name="date[first][day]">\n) + expected << %(<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 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) + expected << "</select>\n" + + assert_dom_equal expected, select_date(0, :start_year => 2003, :prefix => "date[first]") + end + + def test_date_select_with_zero_value_and_no_start_and_end_year + expected = %(<select id="date_first_year" name="date[first][year]">\n) + (Date.today.year-5).upto(Date.today.year+5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } + expected << "</select>\n" + + expected << %(<select id="date_first_month" name="date[first][month]">\n) + expected << %(<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 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) + expected << "</select>\n" + + expected << %(<select id="date_first_day" name="date[first][day]">\n) + expected << %(<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 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) + expected << "</select>\n" + + assert_dom_equal expected, select_date(0, :prefix => "date[first]") + end + + def test_date_select_with_nil_value_and_no_start_and_end_year + expected = %(<select id="date_first_year" name="date[first][year]">\n) + (Date.today.year-5).upto(Date.today.year+5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } + expected << "</select>\n" + + expected << %(<select id="date_first_month" name="date[first][month]">\n) + expected << %(<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 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) + expected << "</select>\n" + + expected << %(<select id="date_first_day" name="date[first][day]">\n) + expected << %(<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 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) + expected << "</select>\n" + + assert_dom_equal expected, select_date(nil, :prefix => "date[first]") + end + + def test_datetime_select_with_nil_value_and_no_start_and_end_year + expected = %(<select id="date_first_year" name="date[first][year]">\n) + (Date.today.year-5).upto(Date.today.year+5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } + expected << "</select>\n" + + expected << %(<select id="date_first_month" name="date[first][month]">\n) + expected << %(<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 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) + expected << "</select>\n" + + expected << %(<select id="date_first_day" name="date[first][day]">\n) + expected << %(<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 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) + expected << "</select>\n" + + expected << " — " + + expected << %(<select id="date_first_hour" name="date[first][hour]">\n) + expected << %(<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) + expected << "</select>\n" + + expected << " : " + + expected << %(<select id="date_first_minute" name="date[first][minute]">\n) + expected << %(<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 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_datetime(nil, :prefix => "date[first]") + end + + def test_datetime_select_with_options_index + @post = Post.new + @post.updated_at = Time.local(2004, 6, 15, 16, 35) + id = 456 + + expected = %{<select id="post_456_updated_at_1i" name="post[#{id}][updated_at(1i)]">\n} + expected << %{<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 value="2004" selected="selected">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} + expected << "</select>\n" + + expected << %{<select id="post_456_updated_at_2i" name="post[#{id}][updated_at(2i)]">\n} + expected << %{<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 value="6" selected="selected">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} + expected << "</select>\n" + + expected << %{<select id="post_456_updated_at_3i" name="post[#{id}][updated_at(3i)]">\n} + expected << %{<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 value="15" selected="selected">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} + expected << "</select>\n" + + expected << " — " + + expected << %{<select id="post_456_updated_at_4i" name="post[#{id}][updated_at(4i)]">\n} + expected << %{<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" selected="selected">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} + expected << "</select>\n" + expected << " : " + expected << %{<select id="post_456_updated_at_5i" name="post[#{id}][updated_at(5i)]">\n} + expected << %{<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 value="35" selected="selected">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, datetime_select("post", "updated_at", :index => id) + end + + def test_datetime_select_within_fields_for_with_options_index + @post = Post.new + @post.updated_at = Time.local(2004, 6, 15, 16, 35) + id = 456 + + output_buffer = fields_for :post, @post, :index => id do |f| + concat f.datetime_select(:updated_at) + end + + expected = %{<select id="post_456_updated_at_1i" name="post[#{id}][updated_at(1i)]">\n} + expected << %{<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 value="2004" selected="selected">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} + expected << "</select>\n" + + expected << %{<select id="post_456_updated_at_2i" name="post[#{id}][updated_at(2i)]">\n} + expected << %{<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 value="6" selected="selected">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} + expected << "</select>\n" + + expected << %{<select id="post_456_updated_at_3i" name="post[#{id}][updated_at(3i)]">\n} + expected << %{<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 value="15" selected="selected">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} + expected << "</select>\n" + + expected << " — " + + expected << %{<select id="post_456_updated_at_4i" name="post[#{id}][updated_at(4i)]">\n} + expected << %{<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" selected="selected">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} + expected << "</select>\n" + expected << " : " + expected << %{<select id="post_456_updated_at_5i" name="post[#{id}][updated_at(5i)]">\n} + expected << %{<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 value="35" selected="selected">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, output_buffer + end + + def test_datetime_select_with_auto_index + @post = Post.new + @post.updated_at = Time.local(2004, 6, 15, 16, 35) + id = @post.id + + expected = %{<select id="post_123_updated_at_1i" name="post[#{id}][updated_at(1i)]">\n} + expected << %{<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 value="2004" selected="selected">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} + expected << "</select>\n" + + expected << %{<select id="post_123_updated_at_2i" name="post[#{id}][updated_at(2i)]">\n} + expected << %{<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 value="6" selected="selected">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} + expected << "</select>\n" + + expected << %{<select id="post_123_updated_at_3i" name="post[#{id}][updated_at(3i)]">\n} + expected << %{<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 value="15" selected="selected">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} + expected << "</select>\n" + + expected << " — " + + expected << %{<select id="post_123_updated_at_4i" name="post[#{id}][updated_at(4i)]">\n} + expected << %{<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" selected="selected">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} + expected << "</select>\n" + expected << " : " + expected << %{<select id="post_123_updated_at_5i" name="post[#{id}][updated_at(5i)]">\n} + expected << %{<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 value="35" selected="selected">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, datetime_select("post[]", "updated_at") + end + + def test_datetime_select_with_seconds + @post = Post.new + @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35) + + expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n} + 1999.upto(2009) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2004}>#{i}</option>\n) } + expected << "</select>\n" + expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n} + 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 6}>#{Date::MONTHNAMES[i]}</option>\n) } + expected << "</select>\n" + expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n} + 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 15}>#{i}</option>\n) } + expected << "</select>\n" + + expected << " — " + + expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n} + 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + expected << " : " + expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n} + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + expected << " : " + expected << %{<select id="post_updated_at_6i" name="post[updated_at(6i)]">\n} + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 35}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, datetime_select("post", "updated_at", :include_seconds => true) + end + + def test_datetime_select_discard_year + @post = Post.new + @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35) + + expected = %{<input type="hidden" id="post_updated_at_1i" name="post[updated_at(1i)]" value="2004" />\n} + expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n} + 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 6}>#{Date::MONTHNAMES[i]}</option>\n) } + expected << "</select>\n" + expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n} + 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 15}>#{i}</option>\n) } + expected << "</select>\n" + + expected << " — " + + expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n} + 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + expected << " : " + expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n} + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, datetime_select("post", "updated_at", :discard_year => true) + end + + def test_datetime_select_discard_month + @post = Post.new + @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35) + + expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n} + 1999.upto(2009) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2004}>#{i}</option>\n) } + expected << "</select>\n" + expected << %{<input type="hidden" id="post_updated_at_2i" name="post[updated_at(2i)]" value="6" />\n} + expected << %{<input type="hidden" id="post_updated_at_3i" name="post[updated_at(3i)]" value="1" />\n} + + expected << " — " + + expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n} + 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + expected << " : " + expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n} + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, datetime_select("post", "updated_at", :discard_month => true) + end + + def test_datetime_select_discard_year_and_month + @post = Post.new + @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35) + + expected = %{<input type="hidden" id="post_updated_at_1i" name="post[updated_at(1i)]" value="2004" />\n} + expected << %{<input type="hidden" id="post_updated_at_2i" name="post[updated_at(2i)]" value="6" />\n} + expected << %{<input type="hidden" id="post_updated_at_3i" name="post[updated_at(3i)]" value="1" />\n} + + expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n} + 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + expected << " : " + expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n} + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, datetime_select("post", "updated_at", :discard_year => true, :discard_month => true) + end + + def test_datetime_select_discard_year_and_month_with_disabled_html_option + @post = Post.new + @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35) + + expected = %{<input type="hidden" id="post_updated_at_1i" disabled="disabled" name="post[updated_at(1i)]" value="2004" />\n} + expected << %{<input type="hidden" id="post_updated_at_2i" disabled="disabled" name="post[updated_at(2i)]" value="6" />\n} + expected << %{<input type="hidden" id="post_updated_at_3i" disabled="disabled" name="post[updated_at(3i)]" value="1" />\n} + + expected << %{<select id="post_updated_at_4i" disabled="disabled" name="post[updated_at(4i)]">\n} + 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + expected << " : " + expected << %{<select id="post_updated_at_5i" disabled="disabled" name="post[updated_at(5i)]">\n} + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, datetime_select("post", "updated_at", { :discard_year => true, :discard_month => true }, :disabled => true) + end + + def test_datetime_select_discard_hour + @post = Post.new + @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35) + + expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n} + 1999.upto(2009) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2004}>#{i}</option>\n) } + expected << "</select>\n" + expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n} + 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 6}>#{Date::MONTHNAMES[i]}</option>\n) } + expected << "</select>\n" + expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n} + 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 15}>#{i}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, datetime_select("post", "updated_at", :discard_hour => true) + end + + def test_datetime_select_discard_minute + @post = Post.new + @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35) + + expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n} + 1999.upto(2009) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2004}>#{i}</option>\n) } + expected << "</select>\n" + expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n} + 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 6}>#{Date::MONTHNAMES[i]}</option>\n) } + expected << "</select>\n" + expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n} + 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 15}>#{i}</option>\n) } + expected << "</select>\n" + + expected << " — " + + expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n} + 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + expected << %{<input type="hidden" id="post_updated_at_5i" name="post[updated_at(5i)]" value="16" />\n} + + assert_dom_equal expected, datetime_select("post", "updated_at", :discard_minute => true) + end + + def test_datetime_select_disabled_and_discard_minute + @post = Post.new + @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35) + + expected = %{<select id="post_updated_at_1i" disabled="disabled" name="post[updated_at(1i)]">\n} + 1999.upto(2009) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2004}>#{i}</option>\n) } + expected << "</select>\n" + expected << %{<select id="post_updated_at_2i" disabled="disabled" name="post[updated_at(2i)]">\n} + 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 6}>#{Date::MONTHNAMES[i]}</option>\n) } + expected << "</select>\n" + expected << %{<select id="post_updated_at_3i" disabled="disabled" name="post[updated_at(3i)]">\n} + 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 15}>#{i}</option>\n) } + expected << "</select>\n" + + expected << " — " + + expected << %{<select id="post_updated_at_4i" disabled="disabled" name="post[updated_at(4i)]">\n} + 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + expected << %{<input type="hidden" id="post_updated_at_5i" disabled="disabled" name="post[updated_at(5i)]" value="16" />\n} + + assert_dom_equal expected, datetime_select("post", "updated_at", :discard_minute => true, :disabled => true) + end + + def test_datetime_select_invalid_order + @post = Post.new + @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35) + + expected = %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n} + 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 15}>#{i}</option>\n) } + expected << "</select>\n" + expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n} + 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 6}>#{Date::MONTHNAMES[i]}</option>\n) } + expected << "</select>\n" + expected << %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n} + 1999.upto(2009) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2004}>#{i}</option>\n) } + expected << "</select>\n" + + expected << " — " + + expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n} + 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + expected << " : " + expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n} + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, datetime_select("post", "updated_at", :order => [:minute, :day, :hour, :month, :year, :second]) + end + + def test_datetime_select_discard_with_order + @post = Post.new + @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35) + + expected = %{<input type="hidden" id="post_updated_at_1i" name="post[updated_at(1i)]" value="2004" />\n} + expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n} + 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 15}>#{i}</option>\n) } + expected << "</select>\n" + expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n} + 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 6}>#{Date::MONTHNAMES[i]}</option>\n) } + expected << "</select>\n" + + expected << " — " + + expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n} + 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + expected << " : " + expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n} + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, datetime_select("post", "updated_at", :order => [:day, :month]) + end + + def test_datetime_select_with_default_value_as_time + @post = Post.new + @post.updated_at = nil + + expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n} + 2001.upto(2011) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2006}>#{i}</option>\n) } + expected << "</select>\n" + expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n} + 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 9}>#{Date::MONTHNAMES[i]}</option>\n) } + expected << "</select>\n" + expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n} + 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 19}>#{i}</option>\n) } + expected << "</select>\n" + + expected << " — " + + expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n} + 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + expected << " : " + expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n} + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, datetime_select("post", "updated_at", :default => Time.local(2006, 9, 19, 15, 16, 35)) + end + + def test_include_blank_overrides_default_option + @post = Post.new + @post.updated_at = nil + + expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n} + expected << %(<option value=""></option>\n) + (Time.now.year - 5).upto(Time.now.year + 5) { |i| expected << %(<option value="#{i}">#{i}</option>\n) } + expected << "</select>\n" + expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n} + expected << %(<option value=""></option>\n) + 1.upto(12) { |i| expected << %(<option value="#{i}">#{Date::MONTHNAMES[i]}</option>\n) } + expected << "</select>\n" + expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n} + expected << %(<option value=""></option>\n) + 1.upto(31) { |i| expected << %(<option value="#{i}">#{i}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, date_select("post", "updated_at", :default => Time.local(2006, 9, 19, 15, 16, 35), :include_blank => true) + end + + def test_datetime_select_with_default_value_as_hash + @post = Post.new + @post.updated_at = nil + + expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n} + (Time.now.year - 5).upto(Time.now.year + 5) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == Time.now.year}>#{i}</option>\n) } + expected << "</select>\n" + expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n} + 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 10}>#{Date::MONTHNAMES[i]}</option>\n) } + expected << "</select>\n" + expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n} + 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == Time.now.day}>#{i}</option>\n) } + expected << "</select>\n" + + expected << " — " + + expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n} + 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 9}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + expected << " : " + expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n} + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 42}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, datetime_select("post", "updated_at", :default => { :month => 10, :minute => 42, :hour => 9 }) + end + + def test_datetime_select_with_html_options + @post = Post.new + @post.updated_at = Time.local(2004, 6, 15, 16, 35) + + expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]" class="selector">\n} + expected << %{<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 value="2004" selected="selected">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} + expected << "</select>\n" + + expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]" class="selector">\n} + expected << %{<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 value="6" selected="selected">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} + expected << "</select>\n" + + expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]" class="selector">\n} + expected << %{<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 value="15" selected="selected">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} + expected << "</select>\n" + + expected << " — " + + expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]" class="selector">\n} + expected << %{<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" selected="selected">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} + expected << "</select>\n" + expected << " : " + expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]" class="selector">\n} + expected << %{<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 value="35" selected="selected">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, datetime_select("post", "updated_at", {}, :class => 'selector') + end + + def test_date_select_should_not_change_passed_options_hash + @post = Post.new + @post.updated_at = Time.local(2008, 7, 16, 23, 30) + + options = { + :order => [ :year, :month, :day ], + :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 }, + :discard_type => false, + :include_blank => false, + :ignore_date => false, + :include_seconds => true + } + date_select(@post, :updated_at, options) + + # note: the literal hash is intentional to show that the actual options hash isn't modified + # don't change this! + assert_equal({ + :order => [ :year, :month, :day ], + :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 }, + :discard_type => false, + :include_blank => false, + :ignore_date => false, + :include_seconds => true + }, options) + end + + def test_datetime_select_should_not_change_passed_options_hash + @post = Post.new + @post.updated_at = Time.local(2008, 7, 16, 23, 30) + + options = { + :order => [ :year, :month, :day ], + :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 }, + :discard_type => false, + :include_blank => false, + :ignore_date => false, + :include_seconds => true + } + datetime_select(@post, :updated_at, options) + + # note: the literal hash is intentional to show that the actual options hash isn't modified + # don't change this! + assert_equal({ + :order => [ :year, :month, :day ], + :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 }, + :discard_type => false, + :include_blank => false, + :ignore_date => false, + :include_seconds => true + }, options) + end + + def test_time_select_should_not_change_passed_options_hash + @post = Post.new + @post.updated_at = Time.local(2008, 7, 16, 23, 30) + + options = { + :order => [ :year, :month, :day ], + :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 }, + :discard_type => false, + :include_blank => false, + :ignore_date => false, + :include_seconds => true + } + time_select(@post, :updated_at, options) + + # note: the literal hash is intentional to show that the actual options hash isn't modified + # don't change this! + assert_equal({ + :order => [ :year, :month, :day ], + :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 }, + :discard_type => false, + :include_blank => false, + :ignore_date => false, + :include_seconds => true + }, options) + end + + def test_select_date_should_not_change_passed_options_hash + options = { + :order => [ :year, :month, :day ], + :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 }, + :discard_type => false, + :include_blank => false, + :ignore_date => false, + :include_seconds => true + } + select_date(Date.today, options) + + # note: the literal hash is intentional to show that the actual options hash isn't modified + # don't change this! + assert_equal({ + :order => [ :year, :month, :day ], + :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 }, + :discard_type => false, + :include_blank => false, + :ignore_date => false, + :include_seconds => true + }, options) + end + + def test_select_datetime_should_not_change_passed_options_hash + options = { + :order => [ :year, :month, :day ], + :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 }, + :discard_type => false, + :include_blank => false, + :ignore_date => false, + :include_seconds => true + } + select_datetime(Time.now, options) + + # note: the literal hash is intentional to show that the actual options hash isn't modified + # don't change this! + assert_equal({ + :order => [ :year, :month, :day ], + :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 }, + :discard_type => false, + :include_blank => false, + :ignore_date => false, + :include_seconds => true + }, options) + end + + def test_select_time_should_not_change_passed_options_hash + options = { + :order => [ :year, :month, :day ], + :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 }, + :discard_type => false, + :include_blank => false, + :ignore_date => false, + :include_seconds => true + } + select_time(Time.now, options) + + # note: the literal hash is intentional to show that the actual options hash isn't modified + # don't change this! + assert_equal({ + :order => [ :year, :month, :day ], + :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 }, + :discard_type => false, + :include_blank => false, + :ignore_date => false, + :include_seconds => true + }, options) + end + + def test_select_html_safety + assert select_day(16).html_safe? + assert select_month(8).html_safe? + assert select_year(Time.mktime(2003, 8, 16, 8, 4, 18)).html_safe? + assert select_minute(Time.mktime(2003, 8, 16, 8, 4, 18)).html_safe? + assert select_second(Time.mktime(2003, 8, 16, 8, 4, 18)).html_safe? + + assert select_minute(8, :use_hidden => true).html_safe? + assert select_month(8, :prompt => 'Choose month').html_safe? + + assert select_time(Time.mktime(2003, 8, 16, 8, 4, 18), {}, :class => 'selector').html_safe? + assert select_date(Time.mktime(2003, 8, 16), :date_separator => " / ", :start_year => 2003, :end_year => 2005, :prefix => "date[first]").html_safe? + end + + def test_object_select_html_safety + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + + assert date_select("post", "written_on", :default => Time.local(2006, 9, 19, 15, 16, 35), :include_blank => true).html_safe? + assert time_select("post", "written_on", :ignore_date => true).html_safe? + end + + def test_time_tag_with_date + date = Date.new(2013, 2, 20) + expected = '<time datetime="2013-02-20">February 20, 2013</time>' + assert_equal expected, time_tag(date) + end + + def test_time_tag_with_time + time = Time.new(2013, 2, 20, 0, 0, 0, '+00:00') + expected = '<time datetime="2013-02-20T00:00:00+00:00">February 20, 2013 00:00</time>' + assert_equal expected, time_tag(time) + end + + def test_time_tag_pubdate_option + assert_match(/<time.*pubdate="pubdate">.*<\/time>/, time_tag(Time.now, :pubdate => true)) + end + + def test_time_tag_with_given_text + assert_match(/<time.*>Right now<\/time>/, time_tag(Time.now, 'Right now')) + end + + def test_time_tag_with_given_block + assert_match(/<time.*><span>Right now<\/span><\/time>/, time_tag(Time.now){ '<span>Right now</span>'.html_safe }) + end + + def test_time_tag_with_different_format + time = Time.new(2013, 2, 20, 0, 0, 0, '+00:00') + 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/debug_helper_test.rb b/actionview/test/template/debug_helper_test.rb new file mode 100644 index 0000000000..42d06bd9ff --- /dev/null +++ b/actionview/test/template/debug_helper_test.rb @@ -0,0 +1,8 @@ +require 'active_record_unit' + +class DebugHelperTest < ActionView::TestCase + def test_debug + company = Company.new(name: "firebase") + assert_match " name: firebase", debug(company) + end +end diff --git a/actionview/test/template/dependency_tracker_test.rb b/actionview/test/template/dependency_tracker_test.rb new file mode 100644 index 0000000000..7a9b4b26ac --- /dev/null +++ b/actionview/test/template/dependency_tracker_test.rb @@ -0,0 +1,74 @@ +require 'abstract_unit' +require 'action_view/dependency_tracker' + +class NeckbeardTracker + def self.call(name, template) + ["foo/#{name}"] + end +end + +class FakeTemplate + attr_reader :source, :handler + + def initialize(source, handler = Neckbeard) + @source, @handler = source, handler + end +end + +Neckbeard = lambda {|template| template.source } +Bowtie = lambda {|template| template.source } + +class DependencyTrackerTest < ActionView::TestCase + def tracker + ActionView::DependencyTracker + end + + def setup + ActionView::Template.register_template_handler :neckbeard, Neckbeard + tracker.register_tracker(:neckbeard, NeckbeardTracker) + end + + def teardown + tracker.remove_tracker(:neckbeard) + end + + def test_finds_tracker_by_template_handler + template = FakeTemplate.new("boo/hoo") + dependencies = tracker.find_dependencies("boo/hoo", template) + assert_equal ["foo/boo/hoo"], dependencies + end + + def test_returns_empty_array_if_no_tracker_is_found + template = FakeTemplate.new("boo/hoo", Bowtie) + dependencies = tracker.find_dependencies("boo/hoo", template) + assert_equal [], dependencies + end +end + +class ERBTrackerTest < Minitest::Test + def make_tracker(name, template) + ActionView::DependencyTracker::ERBTracker.new(name, template) + end + + def test_dependency_of_erb_template_with_number_in_filename + template = FakeTemplate.new("<%# render 'messages/message123' %>", :erb) + tracker = make_tracker('messages/_message123', template) + + assert_equal ["messages/message123"], tracker.dependencies + end + + def test_finds_dependency_in_correct_directory + template = FakeTemplate.new("<%# render(message.topic) %>", :erb) + tracker = make_tracker('messages/_message', template) + + assert_equal ["topics/topic"], tracker.dependencies + end + + def test_finds_dependency_in_correct_directory_with_underscore + template = FakeTemplate.new("<%# render(message_type.messages) %>", :erb) + tracker = make_tracker('message_types/_message_type', template) + + assert_equal ["messages/message"], tracker.dependencies + end +end + diff --git a/actionview/test/template/digestor_test.rb b/actionview/test/template/digestor_test.rb new file mode 100644 index 0000000000..06735c30d3 --- /dev/null +++ b/actionview/test/template/digestor_test.rb @@ -0,0 +1,197 @@ +require 'abstract_unit' +require 'fileutils' + +class FixtureTemplate + attr_reader :source, :handler + + def initialize(template_path) + @source = File.read(template_path) + @handler = ActionView::Template.handler_for_extension(:erb) + rescue Errno::ENOENT + raise ActionView::MissingTemplate.new([], "", [], true, []) + end +end + +class FixtureFinder + FIXTURES_DIR = "#{File.dirname(__FILE__)}/../fixtures/digestor" + + def find(logical_name, keys, partial, options) + FixtureTemplate.new("digestor/#{partial ? logical_name.gsub(%r|/([^/]+)$|, '/_\1') : logical_name}.#{options[:formats].first}.erb") + end +end + +class TemplateDigestorTest < ActionView::TestCase + def setup + @cwd = Dir.pwd + @tmp_dir = Dir.mktmpdir + + FileUtils.cp_r FixtureFinder::FIXTURES_DIR, @tmp_dir + Dir.chdir @tmp_dir + end + + def teardown + Dir.chdir @cwd + FileUtils.rm_r @tmp_dir + ActionView::Digestor.cache.clear + end + + def test_top_level_change_reflected + assert_digest_difference("messages/show") do + change_template("messages/show") + end + end + + def test_explicit_dependency + assert_digest_difference("messages/show") do + change_template("messages/_message") + end + end + + def test_explicit_dependency_in_multiline_erb_tag + assert_digest_difference("messages/show") do + change_template("messages/_form") + end + end + + def test_second_level_dependency + assert_digest_difference("messages/show") do + change_template("comments/_comments") + end + end + + def test_second_level_dependency_within_same_directory + assert_digest_difference("messages/show") do + change_template("messages/_header") + end + end + + def test_third_level_dependency + assert_digest_difference("messages/show") do + change_template("comments/_comment") + end + end + + def test_directory_depth_dependency + assert_digest_difference("level/below/index") do + change_template("level/below/_header") + end + end + + def test_logging_of_missing_template + assert_logged "Couldn't find template for digesting: messages/something_missing.html" do + digest("messages/show") + end + end + + def test_logging_of_missing_template_ending_with_number + assert_logged "Couldn't find template for digesting: messages/something_missing_1.html" do + digest("messages/show") + end + end + + def test_nested_template_directory + assert_digest_difference("messages/show") do + change_template("messages/actions/_move") + end + end + + def test_dont_generate_a_digest_for_missing_templates + assert_equal '', digest("nothing/there") + end + + def test_collection_dependency + assert_digest_difference("messages/index") do + change_template("messages/_message") + end + + assert_digest_difference("messages/index") do + change_template("events/_event") + end + end + + def test_collection_derived_from_record_dependency + assert_digest_difference("messages/show") do + change_template("events/_event") + end + end + + def test_extra_whitespace_in_render_partial + assert_digest_difference("messages/edit") do + change_template("messages/_form") + end + end + + def test_extra_whitespace_in_render_named_partial + assert_digest_difference("messages/edit") do + change_template("messages/_header") + end + end + + def test_extra_whitespace_in_render_record + assert_digest_difference("messages/edit") do + change_template("messages/_message") + end + end + + def test_extra_whitespace_in_render_with_parenthesis + assert_digest_difference("messages/edit") do + change_template("events/_event") + end + end + + def test_old_style_hash_in_render_invocation + assert_digest_difference("messages/edit") do + change_template("comments/_comment") + end + end + + def test_dependencies_via_options_results_in_different_digest + digest_plain = digest("comments/_comment") + digest_fridge = digest("comments/_comment", dependencies: ["fridge"]) + digest_phone = digest("comments/_comment", dependencies: ["phone"]) + digest_fridge_phone = digest("comments/_comment", dependencies: ["fridge", "phone"]) + + assert_not_equal digest_plain, digest_fridge + assert_not_equal digest_plain, digest_phone + assert_not_equal digest_plain, digest_fridge_phone + assert_not_equal digest_fridge, digest_phone + assert_not_equal digest_fridge, digest_fridge_phone + assert_not_equal digest_phone, digest_fridge_phone + end + + private + def assert_logged(message) + old_logger = ActionView::Base.logger + log = StringIO.new + ActionView::Base.logger = Logger.new(log) + + begin + yield + + log.rewind + assert_match message, log.read + ensure + ActionView::Base.logger = old_logger + end + end + + def assert_digest_difference(template_name) + previous_digest = digest(template_name) + ActionView::Digestor.cache.clear + + yield + + assert previous_digest != digest(template_name), "digest didn't change" + ActionView::Digestor.cache.clear + end + + def digest(template_name, options={}) + ActionView::Digestor.digest(template_name, :html, FixtureFinder.new, options) + end + + def change_template(template_name) + File.open("digestor/#{template_name}.html.erb", "w") do |f| + f.write "\nTHIS WAS CHANGED!" + end + end +end diff --git a/actionview/test/template/erb/form_for_test.rb b/actionview/test/template/erb/form_for_test.rb new file mode 100644 index 0000000000..e722b40a9a --- /dev/null +++ b/actionview/test/template/erb/form_for_test.rb @@ -0,0 +1,11 @@ +require "abstract_unit" +require "template/erb/helper" + +module ERBTest + class TagHelperTest < BlockTestCase + test "form_for works" do + output = render_content "form_for(:staticpage, :url => {:controller => 'blah', :action => 'update'})", "" + assert_match %r{<form.*action="/blah/update".*method="post">.*</form>}, output + end + end +end diff --git a/actionview/test/template/erb/helper.rb b/actionview/test/template/erb/helper.rb new file mode 100644 index 0000000000..a1973068d5 --- /dev/null +++ b/actionview/test/template/erb/helper.rb @@ -0,0 +1,24 @@ +module ERBTest + class ViewContext + include ActionView::Helpers::UrlHelper + include SharedTestRoutes.url_helpers + include ActionView::Helpers::TagHelper + include ActionView::Helpers::JavaScriptHelper + include ActionView::Helpers::FormHelper + + attr_accessor :output_buffer, :controller + + def protect_against_forgery?() false end + end + + class BlockTestCase < ActiveSupport::TestCase + def render_content(start, inside) + template = block_helper(start, inside) + ActionView::Template::Handlers::Erubis.new(template).evaluate(ViewContext.new) + end + + def block_helper(str, rest) + "<%= #{str} do %>#{rest}<% end %>" + end + end +end diff --git a/actionview/test/template/erb/tag_helper_test.rb b/actionview/test/template/erb/tag_helper_test.rb new file mode 100644 index 0000000000..84e328d8be --- /dev/null +++ b/actionview/test/template/erb/tag_helper_test.rb @@ -0,0 +1,30 @@ +require "abstract_unit" +require "template/erb/helper" + +module ERBTest + class TagHelperTest < BlockTestCase + test "percent equals works for content_tag and does not require parenthesis on method call" do + assert_equal "<div>Hello world</div>", render_content("content_tag :div", "Hello world") + end + + test "percent equals works for javascript_tag" do + expected_output = "<script>\n//<![CDATA[\nalert('Hello')\n//]]>\n</script>" + assert_equal expected_output, render_content("javascript_tag", "alert('Hello')") + end + + test "percent equals works for javascript_tag with options" do + expected_output = "<script id=\"the_js_tag\">\n//<![CDATA[\nalert('Hello')\n//]]>\n</script>" + assert_equal expected_output, render_content("javascript_tag(:id => 'the_js_tag')", "alert('Hello')") + end + + test "percent equals works with form tags" do + expected_output = %r{<form.*action="foo".*method="post">.*hello*</form>} + assert_match expected_output, render_content("form_tag('foo')", "<%= 'hello' %>") + end + + test "percent equals works with fieldset tags" do + expected_output = "<fieldset><legend>foo</legend>hello</fieldset>" + assert_equal expected_output, render_content("field_set_tag('foo')", "<%= 'hello' %>") + end + end +end diff --git a/actionview/test/template/erb_util_test.rb b/actionview/test/template/erb_util_test.rb new file mode 100644 index 0000000000..3e5b029cea --- /dev/null +++ b/actionview/test/template/erb_util_test.rb @@ -0,0 +1,60 @@ +require 'abstract_unit' + +class ErbUtilTest < ActiveSupport::TestCase + include ERB::Util + + ERB::Util::HTML_ESCAPE.each do |given, expected| + define_method "test_html_escape_#{expected.gsub(/\W/, '')}" do + assert_equal expected, html_escape(given) + end + end + + ERB::Util::JSON_ESCAPE.each do |given, expected| + define_method "test_json_escape_#{expected.gsub(/\W/, '')}" do + assert_equal ERB::Util::JSON_ESCAPE[given], json_escape(given) + end + end + + def test_json_escape_returns_unsafe_strings_when_passed_unsafe_strings + value = json_escape("asdf") + assert !value.html_safe? + end + + def test_json_escape_returns_safe_strings_when_passed_safe_strings + value = json_escape("asdf".html_safe) + assert value.html_safe? + end + + def test_html_escape_is_html_safe + escaped = h("<p>") + assert_equal "<p>", escaped + assert escaped.html_safe? + end + + def test_html_escape_passes_html_escpe_unmodified + escaped = h("<p>".html_safe) + assert_equal "<p>", escaped + assert escaped.html_safe? + end + + def test_rest_in_ascii + (0..127).to_a.map {|int| int.chr }.each do |chr| + next if %('"&<>).include?(chr) + assert_equal chr, html_escape(chr) + end + end + + def test_html_escape_once + assert_equal '1 <>&"' 2 & 3', html_escape_once('1 <>&"\' 2 & 3') + end + + def test_html_escape_once_returns_unsafe_strings_when_passed_unsafe_strings + value = html_escape_once('1 < 2 & 3') + assert !value.html_safe? + end + + def test_html_escape_once_returns_safe_strings_when_passed_safe_strings + value = html_escape_once('1 < 2 & 3'.html_safe) + assert value.html_safe? + end +end diff --git a/actionview/test/template/form_collections_helper_test.rb b/actionview/test/template/form_collections_helper_test.rb new file mode 100644 index 0000000000..bc9c21dfd3 --- /dev/null +++ b/actionview/test/template/form_collections_helper_test.rb @@ -0,0 +1,358 @@ +require 'abstract_unit' + +class Category < Struct.new(:id, :name) +end + +class FormCollectionsHelperTest < ActionView::TestCase + def assert_no_select(selector, value = nil) + assert_select(selector, :text => value, :count => 0) + end + + def with_collection_radio_buttons(*args, &block) + @output_buffer = collection_radio_buttons(*args, &block) + end + + def with_collection_check_boxes(*args, &block) + @output_buffer = collection_check_boxes(*args, &block) + end + + # COLLECTION RADIO BUTTONS + test 'collection radio accepts a collection and generate inputs from value method' do + with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s + + assert_select 'input[type=radio][value=true]#user_active_true' + assert_select 'input[type=radio][value=false]#user_active_false' + end + + test 'collection radio accepts a collection and generate inputs from label method' do + with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s + + assert_select 'label[for=user_active_true]', 'true' + assert_select 'label[for=user_active_false]', 'false' + end + + test 'collection radio handles camelized collection values for labels correctly' do + with_collection_radio_buttons :user, :active, ['Yes', 'No'], :to_s, :to_s + + assert_select 'label[for=user_active_yes]', 'Yes' + assert_select 'label[for=user_active_no]', 'No' + end + + test 'colection radio should sanitize collection values for labels correctly' do + with_collection_radio_buttons :user, :name, ['$0.99', '$1.99'], :to_s, :to_s + assert_select 'label[for=user_name_099]', '$0.99' + assert_select 'label[for=user_name_199]', '$1.99' + end + + test 'collection radio accepts checked item' do + with_collection_radio_buttons :user, :active, [[1, true], [0, false]], :last, :first, :checked => true + + assert_select 'input[type=radio][value=true][checked=checked]' + assert_no_select 'input[type=radio][value=false][checked=checked]' + end + + test 'collection radio accepts multiple disabled items' do + collection = [[1, true], [0, false], [2, 'other']] + with_collection_radio_buttons :user, :active, collection, :last, :first, :disabled => [true, false] + + assert_select 'input[type=radio][value=true][disabled=disabled]' + assert_select 'input[type=radio][value=false][disabled=disabled]' + assert_no_select 'input[type=radio][value=other][disabled=disabled]' + end + + test 'collection radio accepts single disable item' do + collection = [[1, true], [0, false]] + with_collection_radio_buttons :user, :active, collection, :last, :first, :disabled => true + + assert_select 'input[type=radio][value=true][disabled=disabled]' + assert_no_select 'input[type=radio][value=false][disabled=disabled]' + end + + test 'collection radio accepts html options as input' do + collection = [[1, true], [0, false]] + with_collection_radio_buttons :user, :active, collection, :last, :first, {}, :class => 'special-radio' + + assert_select 'input[type=radio][value=true].special-radio#user_active_true' + assert_select 'input[type=radio][value=false].special-radio#user_active_false' + end + + test 'collection radio accepts html options as the last element of array' do + collection = [[1, true, {class: 'foo'}], [0, false, {class: 'bar'}]] + with_collection_radio_buttons :user, :active, collection, :second, :first + + assert_select 'input[type=radio][value=true].foo#user_active_true' + assert_select 'input[type=radio][value=false].bar#user_active_false' + end + + test 'collection radio does not wrap input inside the label' do + with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s + + assert_select 'input[type=radio] + label' + assert_no_select 'label input' + end + + test 'collection radio accepts a block to render the label as radio button wrapper' do + with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s do |b| + b.label { b.radio_button } + end + + assert_select 'label[for=user_active_true] > input#user_active_true[type=radio]' + assert_select 'label[for=user_active_false] > input#user_active_false[type=radio]' + end + + test 'collection radio accepts a block to change the order of label and radio button' do + with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s do |b| + b.label + b.radio_button + end + + assert_select 'label[for=user_active_true] + input#user_active_true[type=radio]' + assert_select 'label[for=user_active_false] + input#user_active_false[type=radio]' + end + + test 'collection radio with block helpers accept extra html options' do + with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s do |b| + b.label(:class => "radio_button") + b.radio_button(:class => "radio_button") + end + + assert_select 'label.radio_button[for=user_active_true] + input#user_active_true.radio_button[type=radio]' + assert_select 'label.radio_button[for=user_active_false] + input#user_active_false.radio_button[type=radio]' + end + + test 'collection radio with block helpers allows access to current text and value' do + with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s do |b| + b.label(:"data-value" => b.value) { b.radio_button + b.text } + end + + assert_select 'label[for=user_active_true][data-value=true]', 'true' do + assert_select 'input#user_active_true[type=radio]' + end + assert_select 'label[for=user_active_false][data-value=false]', 'false' do + assert_select 'input#user_active_false[type=radio]' + end + end + + test 'collection radio with block helpers allows access to the current object item in the collection to access extra properties' do + with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s do |b| + b.label(:class => b.object) { b.radio_button + b.text } + end + + assert_select 'label.true[for=user_active_true]', 'true' do + assert_select 'input#user_active_true[type=radio]' + end + assert_select 'label.false[for=user_active_false]', 'false' do + assert_select 'input#user_active_false[type=radio]' + end + end + + test 'collection radio buttons with fields for' do + collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] + @output_buffer = fields_for(:post) do |p| + 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 'label[for=post_category_id_1]', 'Category 1' + assert_select 'label[for=post_category_id_2]', 'Category 2' + end + + test 'collection radio accepts checked item which has a value of false' do + with_collection_radio_buttons :user, :active, [[1, true], [0, false]], :last, :first, :checked => false + assert_no_select 'input[type=radio][value=true][checked=checked]' + assert_select 'input[type=radio][value=false][checked=checked]' + end + + # COLLECTION CHECK BOXES + test 'collection check boxes accepts a collection and generate a serie 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]' + 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 + end + + test 'collection check boxes accepts a collection and generate a serie of checkboxes with labels for label 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 'label[for=user_category_ids_1]', 'Category 1' + assert_select 'label[for=user_category_ids_2]', 'Category 2' + end + + test 'collection check boxes handles camelized collection values for labels correctly' do + with_collection_check_boxes :user, :active, ['Yes', 'No'], :to_s, :to_s + + assert_select 'label[for=user_active_yes]', 'Yes' + assert_select 'label[for=user_active_no]', 'No' + end + + test 'colection check box should sanitize collection values for labels correctly' do + with_collection_check_boxes :user, :name, ['$0.99', '$1.99'], :to_s, :to_s + assert_select 'label[for=user_name_099]', '$0.99' + assert_select 'label[for=user_name_199]', '$1.99' + end + + test 'collection check boxes accepts html options as the last element of array' do + 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' + end + + test 'collection check boxes accepts selected 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]' + 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]' + 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]' + end + + test 'collection check boxes accepts selected values as :checked option and override the model values' do + user = Struct.new(:category_ids).new(2) + collection = (1..3).map{|i| [i, "Category #{i}"] } + + @output_buffer = fields_for(:user, user) do |p| + 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]' + 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]' + end + + test 'collection check boxes accepts single disable 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]' + 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]' + 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]' + end + + test 'collection check boxes with fields for' do + collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] + @output_buffer = fields_for(:post) do |p| + 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 'label[for=post_category_ids_1]', 'Category 1' + assert_select 'label[for=post_category_ids_2]', 'Category 2' + end + + test 'collection check boxes does not wrap input inside the label' do + with_collection_check_boxes :user, :active, [true, false], :to_s, :to_s + + assert_select 'input[type=checkbox] + label' + assert_no_select 'label input' + end + + test 'collection check boxes accepts a block to render the label as check box wrapper' do + with_collection_check_boxes :user, :active, [true, false], :to_s, :to_s do |b| + b.label { b.check_box } + end + + assert_select 'label[for=user_active_true] > input#user_active_true[type=checkbox]' + assert_select 'label[for=user_active_false] > input#user_active_false[type=checkbox]' + end + + test 'collection check boxes accepts a block to change the order of label and check box' do + with_collection_check_boxes :user, :active, [true, false], :to_s, :to_s do |b| + b.label + b.check_box + end + + assert_select 'label[for=user_active_true] + input#user_active_true[type=checkbox]' + assert_select 'label[for=user_active_false] + input#user_active_false[type=checkbox]' + end + + test 'collection check boxes with block helpers accept extra html options' do + with_collection_check_boxes :user, :active, [true, false], :to_s, :to_s do |b| + b.label(:class => "check_box") + b.check_box(:class => "check_box") + end + + assert_select 'label.check_box[for=user_active_true] + input#user_active_true.check_box[type=checkbox]' + assert_select 'label.check_box[for=user_active_false] + input#user_active_false.check_box[type=checkbox]' + end + + test 'collection check boxes with block helpers allows access to current text and value' do + with_collection_check_boxes :user, :active, [true, false], :to_s, :to_s do |b| + b.label(:"data-value" => b.value) { b.check_box + b.text } + end + + assert_select 'label[for=user_active_true][data-value=true]', 'true' do + assert_select 'input#user_active_true[type=checkbox]' + end + assert_select 'label[for=user_active_false][data-value=false]', 'false' do + assert_select 'input#user_active_false[type=checkbox]' + end + end + + test 'collection check boxes with block helpers allows access to the current object item in the collection to access extra properties' do + with_collection_check_boxes :user, :active, [true, false], :to_s, :to_s do |b| + b.label(:class => b.object) { b.check_box + b.text } + end + + assert_select 'label.true[for=user_active_true]', 'true' do + assert_select 'input#user_active_true[type=checkbox]' + end + assert_select 'label.false[for=user_active_false]', 'false' do + assert_select 'input#user_active_false[type=checkbox]' + end + end +end diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb new file mode 100644 index 0000000000..1ff320224d --- /dev/null +++ b/actionview/test/template/form_helper_test.rb @@ -0,0 +1,2959 @@ +require 'abstract_unit' +require 'controller/fake_models' + +class FormHelperTest < ActionView::TestCase + include RenderERBUtils + + tests ActionView::Helpers::FormHelper + + def form_for(*) + @output_buffer = super + end + + def setup + super + + # Create "label" locale for testing I18n label helpers + I18n.backend.store_translations 'label', { + activemodel: { + attributes: { + post: { + cost: "Total cost" + } + } + }, + helpers: { + label: { + post: { + body: "Write entire text here", + color: { + red: "Rojo" + }, + comments: { + body: "Write body here" + } + }, + tag: { + value: "Tag" + } + } + } + } + + # Create "submit" locale for testing I18n submit helpers + I18n.backend.store_translations 'submit', { + helpers: { + submit: { + create: 'Create %{model}', + update: 'Confirm %{model} changes', + submit: 'Save changes', + another_post: { + update: 'Update your %{model}' + } + } + } + } + + @post = Post.new + @comment = Comment.new + def @post.errors() + Class.new { + def [](field); field == "author_name" ? ["can't be empty"] : [] end + def empty?() false end + def count() 1 end + def full_messages() ["Author name can't be empty"] end + }.new + end + def @post.to_key; [123]; end + def @post.id_before_type_cast; 123; end + def @post.to_param; '123'; end + + @post.persisted = true + @post.title = "Hello World" + @post.author_name = "" + @post.body = "Back to the hill and over it again!" + @post.secret = 1 + @post.written_on = Date.new(2004, 6, 15) + + @post.comments = [] + @post.comments << @comment + + @post.tags = [] + @post.tags << Tag.new + + @car = Car.new("#000FFF") + end + + Routes = ActionDispatch::Routing::RouteSet.new + Routes.draw do + resources :posts do + resources :comments + end + + namespace :admin do + resources :posts do + resources :comments + end + end + + get "/foo", to: "controller#action" + root to: "main#index" + end + + def _routes + Routes + end + + include Routes.url_helpers + + def url_for(object) + @url_for_options = object + + if object.is_a?(Hash) && object[:use_route].blank? && object[:controller].blank? + object.merge!(controller: "main", action: "index") + end + + super + end + + class FooTag < ActionView::Helpers::Tags::Base + def initialize; end + end + + def test_tags_base_child_without_render_method + assert_raise(NotImplementedError) { FooTag.new.render } + end + + def test_label + assert_dom_equal('<label for="post_title">Title</label>', label("post", "title")) + assert_dom_equal( + '<label for="post_title">The title goes here</label>', + label("post", "title", "The title goes here") + ) + assert_dom_equal( + '<label class="title_label" for="post_title">Title</label>', + label("post", "title", nil, class: 'title_label') + ) + assert_dom_equal('<label for="post_secret">Secret?</label>', label("post", "secret?")) + end + + def test_label_with_symbols + assert_dom_equal('<label for="post_title">Title</label>', label(:post, :title)) + assert_dom_equal('<label for="post_secret">Secret?</label>', label(:post, :secret?)) + end + + def test_label_with_locales_strings + old_locale, I18n.locale = I18n.locale, :label + assert_dom_equal('<label for="post_body">Write entire text here</label>', label("post", "body")) + ensure + I18n.locale = old_locale + end + + def test_label_with_human_attribute_name + old_locale, I18n.locale = I18n.locale, :label + assert_dom_equal('<label for="post_cost">Total cost</label>', label(:post, :cost)) + ensure + I18n.locale = old_locale + end + + def test_label_with_locales_symbols + old_locale, I18n.locale = I18n.locale, :label + assert_dom_equal('<label for="post_body">Write entire text here</label>', label(:post, :body)) + ensure + I18n.locale = old_locale + end + + def test_label_with_locales_and_options + old_locale, I18n.locale = I18n.locale, :label + assert_dom_equal( + '<label for="post_body" class="post_body">Write entire text here</label>', + label(:post, :body, class: "post_body") + ) + ensure + I18n.locale = old_locale + end + + def test_label_with_locales_and_value + old_locale, I18n.locale = I18n.locale, :label + assert_dom_equal('<label for="post_color_red">Rojo</label>', label(:post, :color, value: "red")) + ensure + I18n.locale = old_locale + end + + def test_label_with_locales_and_nested_attributes + old_locale, I18n.locale = I18n.locale, :label + form_for(@post, html: { id: 'create-post' }) do |f| + f.fields_for(:comments) do |cf| + concat cf.label(:body) + end + end + + expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do + '<label for="post_comments_attributes_0_body">Write body here</label>' + end + + assert_dom_equal expected, output_buffer + ensure + I18n.locale = old_locale + end + + def test_label_with_locales_fallback_and_nested_attributes + old_locale, I18n.locale = I18n.locale, :label + form_for(@post, html: { id: 'create-post' }) do |f| + f.fields_for(:tags) do |cf| + concat cf.label(:value) + end + end + + expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do + '<label for="post_tags_attributes_0_value">Tag</label>' + end + + assert_dom_equal expected, output_buffer + ensure + I18n.locale = old_locale + 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 + + def test_label_with_for_attribute_as_string + assert_dom_equal('<label for="my_for">Title</label>', label(:post, :title, nil, "for" => "my_for")) + end + + def test_label_does_not_generate_for_attribute_when_given_nil + assert_dom_equal('<label>Title</label>', label(:post, :title, for: nil)) + end + + def test_label_with_id_attribute_as_symbol + assert_dom_equal( + '<label for="post_title" id="my_id">Title</label>', + label(:post, :title, nil, id: "my_id") + ) + end + + def test_label_with_id_attribute_as_string + assert_dom_equal( + '<label for="post_title" id="my_id">Title</label>', + label(:post, :title, nil, "id" => "my_id") + ) + end + + def test_label_with_for_and_id_attributes_as_symbol + assert_dom_equal( + '<label for="my_for" id="my_id">Title</label>', + label(:post, :title, nil, for: "my_for", id: "my_id") + ) + end + + def test_label_with_for_and_id_attributes_as_string + assert_dom_equal( + '<label for="my_for" id="my_id">Title</label>', + label(:post, :title, nil, "for" => "my_for", "id" => "my_id") + ) + end + + def test_label_for_radio_buttons_with_value + assert_dom_equal( + '<label for="post_title_great_title">The title goes here</label>', + label("post", "title", "The title goes here", value: "great_title") + ) + assert_dom_equal( + '<label for="post_title_great_title">The title goes here</label>', + label("post", "title", "The title goes here", value: "great title") + ) + end + + def test_label_with_block + assert_dom_equal( + '<label for="post_title">The title, please:</label>', + label(:post, :title) { "The title, please:" } + ) + end + + def test_label_with_block_and_options + assert_dom_equal( + '<label for="my_for">The title, please:</label>', + label(:post, :title, "for" => "my_for") { "The title, please:" } + ) + end + + def test_label_with_block_in_erb + assert_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_text_field + assert_dom_equal( + '<input id="post_title" name="post[title]" type="text" value="Hello World" />', + text_field("post", "title") + ) + assert_dom_equal( + '<input id="post_title" name="post[title]" type="password" />', + password_field("post", "title") + ) + assert_dom_equal( + '<input id="post_title" name="post[title]" type="password" value="Hello World" />', + password_field("post", "title", value: @post.title) + ) + assert_dom_equal( + '<input id="person_name" name="person[name]" type="password" />', + password_field("person", "name") + ) + end + + def test_text_field_with_escapes + @post.title = "<b>Hello World</b>" + assert_dom_equal( + '<input id="post_title" name="post[title]" type="text" value="<b>Hello World</b>" />', + text_field("post", "title") + ) + end + + def test_text_field_with_html_entities + @post.title = "The HTML Entity for & is &" + assert_dom_equal( + '<input id="post_title" name="post[title]" type="text" value="The HTML Entity for & is &amp;" />', + text_field("post", "title") + ) + end + + def test_text_field_with_options + expected = '<input id="post_title" name="post[title]" size="35" type="text" value="Hello World" />' + assert_dom_equal expected, text_field("post", "title", "size" => 35) + assert_dom_equal expected, text_field("post", "title", size: 35) + end + + def test_text_field_assuming_size + expected = '<input id="post_title" maxlength="35" name="post[title]" size="35" type="text" value="Hello World" />' + assert_dom_equal expected, text_field("post", "title", "maxlength" => 35) + assert_dom_equal expected, text_field("post", "title", maxlength: 35) + end + + def test_text_field_removing_size + expected = '<input id="post_title" maxlength="35" name="post[title]" type="text" value="Hello World" />' + assert_dom_equal expected, text_field("post", "title", "maxlength" => 35, "size" => nil) + assert_dom_equal expected, text_field("post", "title", maxlength: 35, size: nil) + end + + def test_text_field_with_nil_value + expected = '<input id="post_title" name="post[title]" type="text" />' + assert_dom_equal expected, text_field("post", "title", value: nil) + end + + def test_text_field_with_nil_name + expected = '<input id="post_title" type="text" value="Hello World" />' + assert_dom_equal expected, text_field("post", "title", name: nil) + end + + 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[]" + end + + def test_file_field_has_no_size + expected = '<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" />' + 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" />' + assert_dom_equal expected, file_field("import", "file", :multiple => true, :name => "custom") + end + + def test_hidden_field + assert_dom_equal( + '<input id="post_title" name="post[title]" type="hidden" value="Hello World" />', + hidden_field("post", "title") + ) + assert_dom_equal( + '<input id="post_secret" name="post[secret]" type="hidden" value="1" />', + hidden_field("post", "secret?") + ) + end + + def test_hidden_field_with_escapes + @post.title = "<b>Hello World</b>" + assert_dom_equal( + '<input id="post_title" name="post[title]" type="hidden" value="<b>Hello World</b>" />', + hidden_field("post", "title") + ) + end + + def test_hidden_field_with_nil_value + expected = '<input id="post_title" name="post[title]" type="hidden" />' + assert_dom_equal expected, hidden_field("post", "title", value: nil) + end + + def test_hidden_field_with_options + assert_dom_equal( + '<input id="post_title" name="post[title]" type="hidden" value="Something Else" />', + hidden_field("post", "title", value: "Something Else") + ) + end + + def test_text_field_with_custom_type + assert_dom_equal( + '<input id="user_email" name="user[email]" type="email" />', + text_field("user", "email", type: "email") + ) + end + + def test_check_box_is_html_safe + assert check_box("post", "secret").html_safe? + end + + def test_check_box_checked_if_object_value_is_same_that_check_value + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="0" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" />', + check_box("post", "secret") + ) + end + + def test_check_box_not_checked_if_object_value_is_same_that_unchecked_value + @post.secret = 0 + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="0" /><input id="post_secret" name="post[secret]" type="checkbox" value="1" />', + check_box("post", "secret") + ) + end + + def test_check_box_checked_if_option_checked_is_present + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="0" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" />', + check_box("post", "secret", "checked"=>"checked") + ) + end + + def test_check_box_checked_if_object_value_is_true + @post.secret = true + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="0" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" />', + check_box("post", "secret") + ) + + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="0" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" />', + check_box("post", "secret?") + ) + end + + def test_check_box_checked_if_object_value_includes_checked_value + @post.secret = ['0'] + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="0" /><input id="post_secret" name="post[secret]" type="checkbox" value="1" />', + check_box("post", "secret") + ) + + @post.secret = ['1'] + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="0" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" />', + check_box("post", "secret") + ) + + @post.secret = Set.new(['1']) + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="0" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" />', + check_box("post", "secret") + ) + end + + def test_check_box_with_include_hidden_false + @post.secret = false + assert_dom_equal( + '<input id="post_secret" name="post[secret]" type="checkbox" value="1" />', + check_box("post", "secret", include_hidden: false) + ) + end + + def test_check_box_with_explicit_checked_and_unchecked_values_when_object_value_is_string + @post.secret = "on" + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="off" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="on" />', + check_box("post", "secret", {}, "on", "off") + ) + + @post.secret = "off" + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="off" /><input id="post_secret" name="post[secret]" type="checkbox" value="on" />', + check_box("post", "secret", {}, "on", "off") + ) + end + + def test_check_box_with_explicit_checked_and_unchecked_values_when_object_value_is_boolean + @post.secret = false + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="true" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="false" />', + check_box("post", "secret", {}, false, true) + ) + + @post.secret = true + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="true" /><input id="post_secret" name="post[secret]" type="checkbox" value="false" />', + check_box("post", "secret", {}, false, true) + ) + end + + def test_check_box_with_explicit_checked_and_unchecked_values_when_object_value_is_integer + @post.secret = 0 + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="1" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="0" />', + check_box("post", "secret", {}, 0, 1) + ) + + @post.secret = 1 + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="1" /><input id="post_secret" name="post[secret]" type="checkbox" value="0" />', + check_box("post", "secret", {}, 0, 1) + ) + + @post.secret = 2 + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="1" /><input id="post_secret" name="post[secret]" type="checkbox" value="0" />', + check_box("post", "secret", {}, 0, 1) + ) + end + + def test_check_box_with_explicit_checked_and_unchecked_values_when_object_value_is_float + @post.secret = 0.0 + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="1" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="0" />', + check_box("post", "secret", {}, 0, 1) + ) + + @post.secret = 1.1 + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="1" /><input id="post_secret" name="post[secret]" type="checkbox" value="0" />', + check_box("post", "secret", {}, 0, 1) + ) + + @post.secret = 2.2 + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="1" /><input id="post_secret" name="post[secret]" type="checkbox" value="0" />', + check_box("post", "secret", {}, 0, 1) + ) + end + + def test_check_box_with_explicit_checked_and_unchecked_values_when_object_value_is_big_decimal + @post.secret = BigDecimal.new(0) + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="1" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="0" />', + check_box("post", "secret", {}, 0, 1) + ) + + @post.secret = BigDecimal.new(1) + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="1" /><input id="post_secret" name="post[secret]" type="checkbox" value="0" />', + check_box("post", "secret", {}, 0, 1) + ) + + @post.secret = BigDecimal.new(2.2, 1) + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="1" /><input id="post_secret" name="post[secret]" type="checkbox" value="0" />', + check_box("post", "secret", {}, 0, 1) + ) + end + + def test_check_box_with_nil_unchecked_value + @post.secret = "on" + assert_dom_equal( + '<input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="on" />', + check_box("post", "secret", {}, "on", nil) + ) + end + + def test_check_box_with_nil_unchecked_value_is_html_safe + assert check_box("post", "secret", {}, "on", nil).html_safe? + end + + def test_check_box_with_multiple_behavior + @post.comment_ids = [2,3] + assert_dom_equal( + '<input name="post[comment_ids][]" type="hidden" value="0" /><input id="post_comment_ids_1" name="post[comment_ids][]" type="checkbox" value="1" />', + check_box("post", "comment_ids", { multiple: true }, 1) + ) + assert_dom_equal( + '<input name="post[comment_ids][]" type="hidden" value="0" /><input checked="checked" id="post_comment_ids_3" name="post[comment_ids][]" type="checkbox" value="3" />', + check_box("post", "comment_ids", { multiple: true }, 3) + ) + end + + def test_check_box_with_multiple_behavior_and_index + @post.comment_ids = [2,3] + assert_dom_equal( + '<input name="post[foo][comment_ids][]" type="hidden" value="0" /><input id="post_foo_comment_ids_1" name="post[foo][comment_ids][]" type="checkbox" value="1" />', + check_box("post", "comment_ids", { multiple: true, index: "foo" }, 1) + ) + assert_dom_equal( + '<input name="post[bar][comment_ids][]" type="hidden" value="0" /><input checked="checked" id="post_bar_comment_ids_3" name="post[bar][comment_ids][]" type="checkbox" value="3" />', + check_box("post", "comment_ids", { multiple: true, index: "bar" }, 3) + ) + + end + + def test_checkbox_disabled_disables_hidden_field + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="0" disabled="disabled"/><input checked="checked" disabled="disabled" id="post_secret" name="post[secret]" type="checkbox" value="1" />', + check_box("post", "secret", { disabled: true }) + ) + end + + def test_checkbox_form_html5_attribute + assert_dom_equal( + '<input form="new_form" name="post[secret]" type="hidden" value="0" /><input checked="checked" form="new_form" id="post_secret" name="post[secret]" type="checkbox" value="1" />', + check_box("post", "secret", form: "new_form") + ) + end + + def test_radio_button + assert_dom_equal('<input checked="checked" id="post_title_hello_world" name="post[title]" type="radio" value="Hello World" />', + radio_button("post", "title", "Hello World") + ) + assert_dom_equal('<input id="post_title_goodbye_world" name="post[title]" type="radio" value="Goodbye World" />', + radio_button("post", "title", "Goodbye World") + ) + assert_dom_equal('<input id="item_subobject_title_inside_world" name="item[subobject][title]" type="radio" value="inside world"/>', + radio_button("item[subobject]", "title", "inside world") + ) + end + + def test_radio_button_is_checked_with_integers + assert_dom_equal('<input checked="checked" id="post_secret_1" name="post[secret]" type="radio" value="1" />', + radio_button("post", "secret", "1") + ) + end + + def test_radio_button_with_negative_integer_value + assert_dom_equal('<input id="post_secret_-1" name="post[secret]" type="radio" value="-1" />', + radio_button("post", "secret", "-1")) + end + + def test_radio_button_respects_passed_in_id + assert_dom_equal('<input checked="checked" id="foo" name="post[secret]" type="radio" value="1" />', + radio_button("post", "secret", "1", id: "foo") + ) + end + + def test_radio_button_with_booleans + assert_dom_equal('<input id="post_secret_true" name="post[secret]" type="radio" value="true" />', + radio_button("post", "secret", true) + ) + + assert_dom_equal('<input id="post_secret_false" name="post[secret]" type="radio" value="false" />', + radio_button("post", "secret", false) + ) + end + + def test_text_area + assert_dom_equal( + %{<textarea id="post_body" name="post[body]">\nBack to the hill and over it again!</textarea>}, + text_area("post", "body") + ) + end + + def test_text_area_with_escapes + @post.body = "Back to <i>the</i> hill and over it again!" + assert_dom_equal( + %{<textarea id="post_body" name="post[body]">\nBack to <i>the</i> hill and over it again!</textarea>}, + text_area("post", "body") + ) + end + + def test_text_area_with_alternate_value + assert_dom_equal( + %{<textarea id="post_body" name="post[body]">\nTesting alternate values.</textarea>}, + text_area("post", "body", value: "Testing alternate values.") + ) + end + + def test_text_area_with_html_entities + @post.body = "The HTML Entity for & is &" + assert_dom_equal( + %{<textarea id="post_body" name="post[body]">\nThe HTML Entity for & is &amp;</textarea>}, + text_area("post", "body") + ) + end + + def test_text_area_with_size_option + assert_dom_equal( + %{<textarea cols="183" id="post_body" name="post[body]" rows="820">\nBack to the hill and over it again!</textarea>}, + text_area("post", "body", size: "183x820") + ) + end + + def test_color_field_with_valid_hex_color_string + expected = %{<input id="car_color" name="car[color]" type="color" value="#000fff" />} + assert_dom_equal(expected, color_field("car", "color")) + end + + def test_color_field_with_invalid_hex_color_string + expected = %{<input id="car_color" name="car[color]" type="color" value="#000000" />} + @car.color = "#1234TR" + assert_dom_equal(expected, color_field("car", "color")) + end + + def test_search_field + expected = %{<input id="contact_notes_query" name="contact[notes_query]" type="search" />} + assert_dom_equal(expected, search_field("contact", "notes_query")) + end + + def test_telephone_field + expected = %{<input id="user_cell" name="user[cell]" type="tel" />} + assert_dom_equal(expected, telephone_field("user", "cell")) + end + + def test_date_field + expected = %{<input id="post_written_on" name="post[written_on]" type="date" value="2004-06-15" />} + assert_dom_equal(expected, date_field("post", "written_on")) + end + + def test_date_field_with_datetime_value + expected = %{<input id="post_written_on" name="post[written_on]" type="date" value="2004-06-15" />} + @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) + assert_dom_equal(expected, date_field("post", "written_on")) + end + + def test_date_field_with_extra_attrs + expected = %{<input id="post_written_on" step="2" max="2010-08-15" min="2000-06-15" name="post[written_on]" type="date" value="2004-06-15" />} + @post.written_on = DateTime.new(2004, 6, 15) + min_value = DateTime.new(2000, 6, 15) + max_value = DateTime.new(2010, 8, 15) + step = 2 + assert_dom_equal(expected, date_field("post", "written_on", min: min_value, max: max_value, step: step)) + end + + def test_date_field_with_timewithzone_value + previous_time_zone, Time.zone = Time.zone, 'UTC' + expected = %{<input id="post_written_on" name="post[written_on]" type="date" value="2004-06-15" />} + @post.written_on = Time.zone.parse('2004-06-15 15:30:45') + assert_dom_equal(expected, date_field("post", "written_on")) + ensure + Time.zone = previous_time_zone + end + + def test_date_field_with_nil_value + expected = %{<input id="post_written_on" name="post[written_on]" type="date" />} + @post.written_on = nil + assert_dom_equal(expected, date_field("post", "written_on")) + end + + def test_time_field + expected = %{<input id="post_written_on" name="post[written_on]" type="time" value="00:00:00.000" />} + assert_dom_equal(expected, time_field("post", "written_on")) + end + + def test_time_field_with_datetime_value + expected = %{<input id="post_written_on" name="post[written_on]" type="time" value="01:02:03.000" />} + @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) + assert_dom_equal(expected, time_field("post", "written_on")) + end + + def test_time_field_with_extra_attrs + expected = %{<input id="post_written_on" step="60" max="10:25:00.000" min="20:45:30.000" name="post[written_on]" type="time" value="01:02:03.000" />} + @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) + min_value = DateTime.new(2000, 6, 15, 20, 45, 30) + max_value = DateTime.new(2010, 8, 15, 10, 25, 00) + step = 60 + assert_dom_equal(expected, time_field("post", "written_on", min: min_value, max: max_value, step: step)) + end + + def test_time_field_with_timewithzone_value + previous_time_zone, Time.zone = Time.zone, 'UTC' + expected = %{<input id="post_written_on" name="post[written_on]" type="time" value="01:02:03.000" />} + @post.written_on = Time.zone.parse('2004-06-15 01:02:03') + assert_dom_equal(expected, time_field("post", "written_on")) + ensure + Time.zone = previous_time_zone + end + + def test_time_field_with_nil_value + expected = %{<input id="post_written_on" name="post[written_on]" type="time" />} + @post.written_on = nil + assert_dom_equal(expected, time_field("post", "written_on")) + end + + def test_datetime_field + expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2004-06-15T00:00:00.000+0000" />} + assert_dom_equal(expected, datetime_field("post", "written_on")) + end + + def test_datetime_field_with_datetime_value + expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2004-06-15T01:02:03.000+0000" />} + @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) + assert_dom_equal(expected, datetime_field("post", "written_on")) + end + + def test_datetime_field_with_extra_attrs + expected = %{<input id="post_written_on" step="60" max="2010-08-15T10:25:00.000+0000" min="2000-06-15T20:45:30.000+0000" name="post[written_on]" type="datetime" value="2004-06-15T01:02:03.000+0000" />} + @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) + min_value = DateTime.new(2000, 6, 15, 20, 45, 30) + max_value = DateTime.new(2010, 8, 15, 10, 25, 00) + step = 60 + assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value, step: step)) + end + + def test_datetime_field_with_timewithzone_value + previous_time_zone, Time.zone = Time.zone, 'UTC' + expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2004-06-15T15:30:45.000+0000" />} + @post.written_on = Time.zone.parse('2004-06-15 15:30:45') + assert_dom_equal(expected, datetime_field("post", "written_on")) + ensure + Time.zone = previous_time_zone + end + + def test_datetime_field_with_nil_value + expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" />} + @post.written_on = nil + assert_dom_equal(expected, datetime_field("post", "written_on")) + end + + def test_datetime_local_field + expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" value="2004-06-15T00:00:00" />} + assert_dom_equal(expected, datetime_local_field("post", "written_on")) + end + + def test_datetime_local_field_with_datetime_value + expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" value="2004-06-15T01:02:03" />} + @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) + assert_dom_equal(expected, datetime_local_field("post", "written_on")) + end + + def test_datetime_local_field_with_extra_attrs + expected = %{<input id="post_written_on" step="60" max="2010-08-15T10:25:00" min="2000-06-15T20:45:30" name="post[written_on]" type="datetime-local" value="2004-06-15T01:02:03" />} + @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) + min_value = DateTime.new(2000, 6, 15, 20, 45, 30) + max_value = DateTime.new(2010, 8, 15, 10, 25, 00) + step = 60 + assert_dom_equal(expected, datetime_local_field("post", "written_on", min: min_value, max: max_value, step: step)) + end + + def test_datetime_local_field_with_timewithzone_value + previous_time_zone, Time.zone = Time.zone, 'UTC' + expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" value="2004-06-15T15:30:45" />} + @post.written_on = Time.zone.parse('2004-06-15 15:30:45') + assert_dom_equal(expected, datetime_local_field("post", "written_on")) + ensure + Time.zone = previous_time_zone + end + + def test_datetime_local_field_with_nil_value + expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" />} + @post.written_on = nil + assert_dom_equal(expected, datetime_local_field("post", "written_on")) + end + + def test_month_field + expected = %{<input id="post_written_on" name="post[written_on]" type="month" value="2004-06" />} + assert_dom_equal(expected, month_field("post", "written_on")) + end + + def test_month_field_with_nil_value + expected = %{<input id="post_written_on" name="post[written_on]" type="month" />} + @post.written_on = nil + assert_dom_equal(expected, month_field("post", "written_on")) + end + + def test_month_field_with_datetime_value + expected = %{<input id="post_written_on" name="post[written_on]" type="month" value="2004-06" />} + @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) + assert_dom_equal(expected, month_field("post", "written_on")) + end + + def test_month_field_with_extra_attrs + expected = %{<input id="post_written_on" step="2" max="2010-12" min="2000-02" name="post[written_on]" type="month" value="2004-06" />} + @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) + min_value = DateTime.new(2000, 2, 13) + max_value = DateTime.new(2010, 12, 23) + step = 2 + assert_dom_equal(expected, month_field("post", "written_on", min: min_value, max: max_value, step: step)) + end + + def test_month_field_with_timewithzone_value + previous_time_zone, Time.zone = Time.zone, 'UTC' + expected = %{<input id="post_written_on" name="post[written_on]" type="month" value="2004-06" />} + @post.written_on = Time.zone.parse('2004-06-15 15:30:45') + assert_dom_equal(expected, month_field("post", "written_on")) + ensure + Time.zone = previous_time_zone + end + + def test_week_field + expected = %{<input id="post_written_on" name="post[written_on]" type="week" value="2004-W24" />} + assert_dom_equal(expected, week_field("post", "written_on")) + end + + def test_week_field_with_nil_value + expected = %{<input id="post_written_on" name="post[written_on]" type="week" />} + @post.written_on = nil + assert_dom_equal(expected, week_field("post", "written_on")) + end + + def test_week_field_with_datetime_value + expected = %{<input id="post_written_on" name="post[written_on]" type="week" value="2004-W24" />} + @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) + assert_dom_equal(expected, week_field("post", "written_on")) + end + + def test_week_field_with_extra_attrs + expected = %{<input id="post_written_on" step="2" max="2010-W51" min="2000-W06" name="post[written_on]" type="week" value="2004-W24" />} + @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) + min_value = DateTime.new(2000, 2, 13) + max_value = DateTime.new(2010, 12, 23) + step = 2 + assert_dom_equal(expected, week_field("post", "written_on", min: min_value, max: max_value, step: step)) + end + + def test_week_field_with_timewithzone_value + previous_time_zone, Time.zone = Time.zone, 'UTC' + expected = %{<input id="post_written_on" name="post[written_on]" type="week" value="2004-W24" />} + @post.written_on = Time.zone.parse('2004-06-15 15:30:45') + assert_dom_equal(expected, week_field("post", "written_on")) + ensure + Time.zone = previous_time_zone + end + + def test_url_field + expected = %{<input id="user_homepage" name="user[homepage]" type="url" />} + assert_dom_equal(expected, url_field("user", "homepage")) + end + + def test_email_field + expected = %{<input id="user_address" name="user[address]" type="email" />} + assert_dom_equal(expected, email_field("user", "address")) + end + + def test_number_field + expected = %{<input name="order[quantity]" max="9" id="order_quantity" type="number" min="1" />} + assert_dom_equal(expected, number_field("order", "quantity", in: 1...10)) + expected = %{<input name="order[quantity]" size="30" max="9" id="order_quantity" type="number" min="1" />} + assert_dom_equal(expected, number_field("order", "quantity", size: 30, in: 1...10)) + end + + def test_range_input + expected = %{<input name="hifi[volume]" step="0.1" max="11" id="hifi_volume" type="range" min="0" />} + assert_dom_equal(expected, range_field("hifi", "volume", in: 0..11, step: 0.1)) + expected = %{<input name="hifi[volume]" step="0.1" size="30" max="11" id="hifi_volume" type="range" min="0" />} + assert_dom_equal(expected, range_field("hifi", "volume", size: 30, in: 0..11, step: 0.1)) + end + + def test_explicit_name + assert_dom_equal( + '<input id="post_title" name="dont guess" type="text" value="Hello World" />', + text_field("post", "title", "name" => "dont guess") + ) + assert_dom_equal( + %{<textarea id="post_body" name="really!">\nBack to the hill and over it again!</textarea>}, + text_area("post", "body", "name" => "really!") + ) + assert_dom_equal( + '<input name="i mean it" type="hidden" value="0" /><input checked="checked" id="post_secret" name="i mean it" type="checkbox" value="1" />', + check_box("post", "secret", "name" => "i mean it") + ) + assert_dom_equal( + text_field("post", "title", "name" => "dont guess"), + text_field("post", "title", name: "dont guess") + ) + assert_dom_equal( + text_area("post", "body", "name" => "really!"), + text_area("post", "body", name: "really!") + ) + assert_dom_equal( + check_box("post", "secret", "name" => "i mean it"), + check_box("post", "secret", name: "i mean it") + ) + end + + def test_explicit_id + assert_dom_equal( + '<input id="dont guess" name="post[title]" type="text" value="Hello World" />', + text_field("post", "title", "id" => "dont guess") + ) + assert_dom_equal( + %{<textarea id="really!" name="post[body]">\nBack to the hill and over it again!</textarea>}, + text_area("post", "body", "id" => "really!") + ) + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="0" /><input checked="checked" id="i mean it" name="post[secret]" type="checkbox" value="1" />', + check_box("post", "secret", "id" => "i mean it") + ) + assert_dom_equal( + text_field("post", "title", "id" => "dont guess"), + text_field("post", "title", id: "dont guess") + ) + assert_dom_equal( + text_area("post", "body", "id" => "really!"), + text_area("post", "body", id: "really!") + ) + assert_dom_equal( + check_box("post", "secret", "id" => "i mean it"), + check_box("post", "secret", id: "i mean it") + ) + end + + def test_nil_id + assert_dom_equal( + '<input name="post[title]" type="text" value="Hello World" />', + text_field("post", "title", "id" => nil) + ) + assert_dom_equal( + %{<textarea name="post[body]">\nBack to the hill and over it again!</textarea>}, + text_area("post", "body", "id" => nil) + ) + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="0" /><input checked="checked" name="post[secret]" type="checkbox" value="1" />', + check_box("post", "secret", "id" => nil) + ) + assert_dom_equal( + '<input type="radio" name="post[secret]" value="0" />', + radio_button("post", "secret", "0", "id" => nil) + ) + assert_dom_equal( + '<select name="post[secret]"></select>', + select("post", "secret", [], {}, "id" => nil) + ) + assert_dom_equal( + text_field("post", "title", "id" => nil), + text_field("post", "title", id: nil) + ) + assert_dom_equal( + text_area("post", "body", "id" => nil), + text_area("post", "body", id: nil) + ) + assert_dom_equal( + check_box("post", "secret", "id" => nil), + check_box("post", "secret", id: nil) + ) + assert_dom_equal( + radio_button("post", "secret", "0", "id" => nil), + radio_button("post", "secret", "0", id: nil) + ) + end + + def test_index + assert_dom_equal( + '<input name="post[5][title]" id="post_5_title" type="text" value="Hello World" />', + text_field("post", "title", "index" => 5) + ) + assert_dom_equal( + %{<textarea name="post[5][body]" id="post_5_body">\nBack to the hill and over it again!</textarea>}, + text_area("post", "body", "index" => 5) + ) + assert_dom_equal( + '<input name="post[5][secret]" type="hidden" value="0" /><input checked="checked" name="post[5][secret]" type="checkbox" value="1" id="post_5_secret" />', + check_box("post", "secret", "index" => 5) + ) + assert_dom_equal( + text_field("post", "title", "index" => 5), + text_field("post", "title", "index" => 5) + ) + assert_dom_equal( + text_area("post", "body", "index" => 5), + text_area("post", "body", "index" => 5) + ) + assert_dom_equal( + check_box("post", "secret", "index" => 5), + check_box("post", "secret", "index" => 5) + ) + end + + def test_index_with_nil_id + assert_dom_equal( + '<input name="post[5][title]" type="text" value="Hello World" />', + text_field("post", "title", "index" => 5, 'id' => nil) + ) + assert_dom_equal( + %{<textarea name="post[5][body]">\nBack to the hill and over it again!</textarea>}, + text_area("post", "body", "index" => 5, 'id' => nil) + ) + assert_dom_equal( + '<input name="post[5][secret]" type="hidden" value="0" /><input checked="checked" name="post[5][secret]" type="checkbox" value="1" />', + check_box("post", "secret", "index" => 5, 'id' => nil) + ) + assert_dom_equal( + text_field("post", "title", "index" => 5, 'id' => nil), + text_field("post", "title", index: 5, id: nil) + ) + assert_dom_equal( + text_area("post", "body", "index" => 5, 'id' => nil), + text_area("post", "body", index: 5, id: nil) + ) + assert_dom_equal( + check_box("post", "secret", "index" => 5, 'id' => nil), + check_box("post", "secret", index: 5, id: nil) + ) + end + + def test_auto_index + pid = 123 + assert_dom_equal( + %{<label for="post_#{pid}_title">Title</label>}, + label("post[]", "title") + ) + assert_dom_equal( + %{<input id="post_#{pid}_title" name="post[#{pid}][title]" type="text" value="Hello World" />}, + text_field("post[]","title") + ) + assert_dom_equal( + %{<textarea id="post_#{pid}_body" name="post[#{pid}][body]">\nBack to the hill and over it again!</textarea>}, + text_area("post[]", "body") + ) + assert_dom_equal( + %{<input name="post[#{pid}][secret]" type="hidden" value="0" /><input checked="checked" id="post_#{pid}_secret" name="post[#{pid}][secret]" type="checkbox" value="1" />}, + check_box("post[]", "secret") + ) + assert_dom_equal( + %{<input checked="checked" id="post_#{pid}_title_hello_world" name="post[#{pid}][title]" type="radio" value="Hello World" />}, + radio_button("post[]", "title", "Hello World") + ) + assert_dom_equal( + %{<input id="post_#{pid}_title_goodbye_world" name="post[#{pid}][title]" type="radio" value="Goodbye World" />}, + radio_button("post[]", "title", "Goodbye World") + ) + end + + def test_auto_index_with_nil_id + pid = 123 + assert_dom_equal( + %{<input name="post[#{pid}][title]" type="text" value="Hello World" />}, + text_field("post[]", "title", id: nil) + ) + assert_dom_equal( + %{<textarea name="post[#{pid}][body]">\nBack to the hill and over it again!</textarea>}, + text_area("post[]", "body", id: nil) + ) + assert_dom_equal( + %{<input name="post[#{pid}][secret]" type="hidden" value="0" /><input checked="checked" name="post[#{pid}][secret]" type="checkbox" value="1" />}, + check_box("post[]", "secret", id: nil) + ) + assert_dom_equal( + %{<input checked="checked" name="post[#{pid}][title]" type="radio" value="Hello World" />}, + radio_button("post[]", "title", "Hello World", id: nil) + ) + assert_dom_equal( + %{<input name="post[#{pid}][title]" type="radio" value="Goodbye World" />}, + radio_button("post[]", "title", "Goodbye World", id: nil) + ) + end + + def test_form_for_requires_block + assert_raises(ArgumentError) do + form_for(:post, @post, html: { id: 'create-post' }) + end + end + + def test_form_for_requires_arguments + error = assert_raises(ArgumentError) do + form_for(nil, html: { id: 'create-post' }) do + end + end + assert_equal "First argument in form cannot contain nil or be empty", error.message + + error = assert_raises(ArgumentError) do + form_for([nil, nil], html: { id: 'create-post' }) do + end + end + assert_equal "First argument in form cannot contain nil or be empty", error.message + end + + def test_form_for + form_for(@post, html: { id: 'create-post' }) do |f| + concat f.label(:title) { "The Title" } + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + concat f.submit('Create post') + concat f.button('Create post') + concat f.button { + concat content_tag(:span, 'Create post') + } + end + + expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do + "<label for='post_title'>The Title</label>" + + "<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' />" + + "<input name='commit' type='submit' value='Create post' />" + + "<button name='button' type='submit'>Create post</button>" + + "<button name='button' type='submit'><span>Create post</span></button>" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_for_with_collection_radio_buttons + post = Post.new + def post.active; false; end + form_for(post) do |f| + concat f.collection_radio_buttons(:active, [true, false], :to_s, :to_s) + end + + expected = whole_form("/posts", "new_post", "new_post") do + "<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>" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_for_with_collection_radio_buttons_with_custom_builder_block + post = Post.new + def post.active; false; end + + form_for(post) do |f| + rendered_radio_buttons = f.collection_radio_buttons(:active, [true, false], :to_s, :to_s) do |b| + b.label { b.radio_button + b.text } + end + concat rendered_radio_buttons + end + + expected = whole_form("/posts", "new_post", "new_post") do + "<label for='post_active_true'>"+ + "<input id='post_active_true' name='post[active]' type='radio' value='true' />" + + "true</label>" + + "<label for='post_active_false'>"+ + "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" + + "false</label>" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_for_with_collection_radio_buttons_with_custom_builder_block_does_not_leak_the_template + post = Post.new + def post.active; false; end + def post.id; 1; end + + form_for(post) do |f| + rendered_radio_buttons = f.collection_radio_buttons(:active, [true, false], :to_s, :to_s) do |b| + b.label { b.radio_button + b.text } + end + concat rendered_radio_buttons + concat f.hidden_field :id + end + + expected = whole_form("/posts", "new_post_1", "new_post") do + "<label for='post_active_true'>"+ + "<input id='post_active_true' name='post[active]' type='radio' value='true' />" + + "true</label>" + + "<label for='post_active_false'>"+ + "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" + + "false</label>"+ + "<input id='post_id' name='post[id]' type='hidden' value='1' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_for_with_collection_check_boxes + post = Post.new + def post.tag_ids; [1, 3]; end + collection = (1..3).map { |i| [i, "Tag #{i}"] } + form_for(post) do |f| + concat f.collection_check_boxes(:tag_ids, collection, :first, :last) + end + + expected = whole_form("/posts", "new_post", "new_post") do + "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" + + "<label for='post_tag_ids_1'>Tag 1</label>" + + "<input id='post_tag_ids_2' name='post[tag_ids][]' type='checkbox' value='2' />" + + "<label for='post_tag_ids_2'>Tag 2</label>" + + "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" + + "<label for='post_tag_ids_3'>Tag 3</label>" + + "<input name='post[tag_ids][]' type='hidden' value='' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_for_with_collection_check_boxes_with_custom_builder_block + post = Post.new + def post.tag_ids; [1, 3]; end + collection = (1..3).map { |i| [i, "Tag #{i}"] } + form_for(post) do |f| + rendered_check_boxes = f.collection_check_boxes(:tag_ids, collection, :first, :last) do |b| + b.label { b.check_box + b.text } + end + concat rendered_check_boxes + end + + expected = whole_form("/posts", "new_post", "new_post") do + "<label for='post_tag_ids_1'>" + + "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" + + "Tag 1</label>" + + "<label for='post_tag_ids_2'>" + + "<input id='post_tag_ids_2' name='post[tag_ids][]' type='checkbox' value='2' />" + + "Tag 2</label>" + + "<label for='post_tag_ids_3'>" + + "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" + + "Tag 3</label>" + + "<input name='post[tag_ids][]' type='hidden' value='' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_for_with_collection_check_boxes_with_custom_builder_block_does_not_leak_the_template + post = Post.new + def post.tag_ids; [1, 3]; end + def post.id; 1; end + collection = (1..3).map { |i| [i, "Tag #{i}"] } + + form_for(post) do |f| + rendered_check_boxes = f.collection_check_boxes(:tag_ids, collection, :first, :last) do |b| + b.label { b.check_box + b.text } + end + concat rendered_check_boxes + concat f.hidden_field :id + end + + expected = whole_form("/posts", "new_post_1", "new_post") do + "<label for='post_tag_ids_1'>" + + "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" + + "Tag 1</label>" + + "<label for='post_tag_ids_2'>" + + "<input id='post_tag_ids_2' name='post[tag_ids][]' type='checkbox' value='2' />" + + "Tag 2</label>" + + "<label for='post_tag_ids_3'>" + + "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" + + "Tag 3</label>" + + "<input name='post[tag_ids][]' type='hidden' value='' />"+ + "<input id='post_id' name='post[id]' type='hidden' value='1' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_for_with_file_field_generate_multipart + Post.send :attr_accessor, :file + + form_for(@post, html: { id: 'create-post' }) do |f| + concat f.file_field(:file) + end + + expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch", multipart: true) do + "<input name='post[file]' type='file' id='post_file' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_fields_for_with_file_field_generate_multipart + Comment.send :attr_accessor, :file + + form_for(@post) do |f| + concat f.fields_for(:comment, @post) { |c| + concat c.file_field(:file) + } + 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' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_for_with_format + form_for(@post, format: :json, html: { id: "edit_post_123", class: "edit_post" }) do |f| + concat f.label(:title) + end + + expected = whole_form("/posts/123.json", "edit_post_123", "edit_post", method: 'patch') do + "<label for='post_title'>Title</label>" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_for_with_model_using_relative_model_naming + blog_post = Blog::Post.new("And his name will be forty and four.", 44) + + form_for(blog_post) do |f| + concat f.text_field :title + concat f.submit('Edit post') + end + + 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' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_for_with_symbol_object_name + form_for(@post, as: "other_name", html: { id: "create-post" }) do |f| + concat f.label(:title, class: 'post_title') + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + concat f.submit('Create post') + end + + expected = whole_form("/posts/123", "create-post", "edit_other_name", method: "patch") do + "<label for='other_name_title' class='post_title'>Title</label>" + + "<input name='other_name[title]' id='other_name_title' value='Hello World' type='text' />" + + "<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' />" + end + + assert_dom_equal expected, output_buffer + 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) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = whole_form("/", "create-post", "edit_post", method: "delete") 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 + end + + def test_form_for_with_method + form_for(@post, url: '/', method: :delete, html: { id: 'create-post' }) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = whole_form("/", "create-post", "edit_post", method: "delete") 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 + end + + def test_form_for_with_search_field + # Test case for bug which would emit an "object" attribute + # when used with form_for using a search_field form helper + form_for(Post.new, url: "/search", html: { id: "search-post", method: :get }) do |f| + concat f.search_field(:title) + end + + expected = whole_form("/search", "search-post", "new_post", method: "get") do + "<input name='post[title]' type='search' id='post_title' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_for_with_remote + form_for(@post, url: '/', remote: true, html: { id: 'create-post', method: :patch }) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = whole_form("/", "create-post", "edit_post", method: "patch", 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 + 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) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = whole_form("/", "create-post", "edit_post", method: "patch", 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 + end + + 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 + + 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 + end + + def test_form_for_without_object + form_for(:post, html: { id: 'create-post' }) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = whole_form("/", "create-post") 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 + end + + def test_form_for_with_index + form_for(@post, as: "post[]") do |f| + concat f.label(:title) + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = whole_form('/posts/123', 'edit_post[]', 'edit_post[]', method: 'patch') do + "<label for='post_123_title'>Title</label>" + + "<input name='post[123][title]' type='text' id='post_123_title' value='Hello World' />" + + "<textarea name='post[123][body]' id='post_123_body'>\nBack to the hill and over it again!</textarea>" + + "<input name='post[123][secret]' type='hidden' value='0' />" + + "<input name='post[123][secret]' checked='checked' type='checkbox' id='post_123_secret' value='1' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_for_with_nil_index_option_override + form_for(@post, as: "post[]", index: nil) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = whole_form('/posts/123', 'edit_post[]', 'edit_post[]', method: 'patch') 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 + end + + def test_form_for_label_error_wrapping + form_for(@post) do |f| + concat f.label(:author_name, class: 'label') + concat f.text_field(:author_name) + concat f.submit('Create post') + end + + 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' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_for_label_error_wrapping_without_conventional_instance_variable + post = remove_instance_variable :@post + + form_for(post) do |f| + concat f.label(:author_name, class: 'label') + concat f.text_field(:author_name) + concat f.submit('Create post') + end + + 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' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_for_label_error_wrapping_block_and_non_block_versions + form_for(@post) do |f| + concat f.label(:author_name, 'Name', class: 'label') + concat f.label(:author_name, class: 'label') { 'Name' } + end + + 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'>Name</label></div>" + + "<div class='field_with_errors'><label for='post_author_name' class='label'>Name</label></div>" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_for_with_namespace + form_for(@post, namespace: 'namespace') do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = whole_form('/posts/123', 'namespace_edit_post_123', 'edit_post', method: 'patch') do + "<input name='post[title]' type='text' id='namespace_post_title' value='Hello World' />" + + "<textarea name='post[body]' id='namespace_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='namespace_post_secret' value='1' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_for_with_namespace_with_date_select + form_for(@post, namespace: 'namespace') do |f| + concat f.date_select(:written_on) + end + + assert_select 'select#namespace_post_written_on_1i' + end + + def test_form_for_with_namespace_with_label + form_for(@post, namespace: 'namespace') do |f| + concat f.label(:title) + concat f.text_field(:title) + end + + expected = whole_form('/posts/123', 'namespace_edit_post_123', 'edit_post', method: 'patch') do + "<label for='namespace_post_title'>Title</label>" + + "<input name='post[title]' type='text' id='namespace_post_title' value='Hello World' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_two_form_for_with_namespace + form_for(@post, namespace: 'namespace_1') do |f| + concat f.label(:title) + concat f.text_field(:title) + end + + expected_1 = whole_form('/posts/123', 'namespace_1_edit_post_123', 'edit_post', method: 'patch') do + "<label for='namespace_1_post_title'>Title</label>" + + "<input name='post[title]' type='text' id='namespace_1_post_title' value='Hello World' />" + end + + assert_dom_equal expected_1, output_buffer + + form_for(@post, namespace: 'namespace_2') do |f| + concat f.label(:title) + concat f.text_field(:title) + end + + expected_2 = whole_form('/posts/123', 'namespace_2_edit_post_123', 'edit_post', method: 'patch') do + "<label for='namespace_2_post_title'>Title</label>" + + "<input name='post[title]' type='text' id='namespace_2_post_title' value='Hello World' />" + end + + assert_dom_equal expected_2, output_buffer + end + + def test_fields_for_with_namespace + @comment.body = 'Hello World' + form_for(@post, namespace: 'namespace') do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.fields_for(@comment) { |c| + concat c.text_field(:body) + } + end + + expected = whole_form('/posts/123', 'namespace_edit_post_123', 'edit_post', method: 'patch') do + "<input name='post[title]' type='text' id='namespace_post_title' value='Hello World' />" + + "<textarea name='post[body]' id='namespace_post_body'>\nBack to the hill and over it again!</textarea>" + + "<input name='post[comment][body]' type='text' id='namespace_post_comment_body' value='Hello World' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_submit_with_object_as_new_record_and_locale_strings + old_locale, I18n.locale = I18n.locale, :submit + + @post.persisted = false + @post.stubs(:to_key).returns(nil) + 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 + + assert_dom_equal expected, output_buffer + ensure + I18n.locale = old_locale + end + + def test_submit_with_object_as_existing_record_and_locale_strings + old_locale, I18n.locale = I18n.locale, :submit + + form_for(@post) do |f| + concat f.submit + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + "<input name='commit' type='submit' value='Confirm Post changes' />" + end + + assert_dom_equal expected, output_buffer + ensure + I18n.locale = old_locale + end + + def test_submit_without_object_and_locale_strings + old_locale, I18n.locale = I18n.locale, :submit + + form_for(:post) do |f| + concat f.submit class: "extra" + end + + expected = whole_form do + "<input name='commit' class='extra' type='submit' value='Save changes' />" + end + + assert_dom_equal expected, output_buffer + ensure + I18n.locale = old_locale + end + + def test_submit_with_object_and_nested_lookup + old_locale, I18n.locale = I18n.locale, :submit + + form_for(@post, as: :another_post) do |f| + concat f.submit + end + + expected = whole_form('/posts/123', 'edit_another_post', 'edit_another_post', method: 'patch') do + "<input name='commit' type='submit' value='Update your Post' />" + end + + assert_dom_equal expected, output_buffer + ensure + I18n.locale = old_locale + end + + def test_nested_fields_for + @comment.body = 'Hello World' + form_for(@post) do |f| + concat f.fields_for(@comment) { |c| + concat c.text_field(:body) + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + "<input name='post[comment][body]' type='text' id='post_comment_body' value='Hello World' />" + 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) + concat f.fields_for('comment[]', @comment) { |c| + concat c.text_field(:name) + } + end + + expected = whole_form('/posts/123', 'edit_post[]', 'edit_post[]', method: 'patch') do + "<input name='post[123][title]' type='text' id='post_123_title' value='Hello World' />" + + "<input name='post[123][comment][][name]' type='text' id='post_123_comment__name' value='new comment' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_index_and_parent_fields + form_for(@post, index: 1) do |c| + concat c.text_field(:title) + concat c.fields_for('comment', @comment, index: 1) { |r| + concat r.text_field(:name) + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + "<input name='post[1][title]' type='text' id='post_1_title' value='Hello World' />" + + "<input name='post[1][comment][1][name]' type='text' id='post_1_comment_1_name' value='new comment' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_for_with_index_and_nested_fields_for + output_buffer = form_for(@post, index: 1) do |f| + concat f.fields_for(:comment, @post) { |c| + concat c.text_field(:title) + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + "<input name='post[1][comment][title]' type='text' id='post_1_comment_title' value='Hello World' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_index_on_both + form_for(@post, index: 1) do |f| + concat f.fields_for(:comment, @post, index: 5) { |c| + concat c.text_field(:title) + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + "<input name='post[1][comment][5][title]' type='text' id='post_1_comment_5_title' value='Hello World' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_auto_index + form_for(@post, as: "post[]") do |f| + concat f.fields_for(:comment, @post) { |c| + concat c.text_field(:title) + } + end + + expected = whole_form('/posts/123', 'edit_post[]', 'edit_post[]', method: 'patch') do + "<input name='post[123][comment][title]' type='text' id='post_123_comment_title' value='Hello World' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_index_radio_button + form_for(@post) do |f| + concat f.fields_for(:comment, @post, index: 5) { |c| + concat c.radio_button(:title, "hello") + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + "<input name='post[comment][5][title]' type='radio' id='post_comment_5_title_hello' value='hello' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_auto_index_on_both + form_for(@post, as: "post[]") do |f| + concat f.fields_for("comment[]", @post) { |c| + concat c.text_field(:title) + } + end + + expected = whole_form('/posts/123', 'edit_post[]', 'edit_post[]', method: 'patch') do + "<input name='post[123][comment][123][title]' type='text' id='post_123_comment_123_title' value='Hello World' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_index_and_auto_index + output_buffer = form_for(@post, as: "post[]") do |f| + concat f.fields_for(:comment, @post, index: 5) { |c| + concat c.text_field(:title) + } + end + + output_buffer << form_for(@post, as: :post, index: 1) do |f| + concat f.fields_for("comment[]", @post) { |c| + concat c.text_field(:title) + } + end + + expected = whole_form('/posts/123', 'edit_post[]', 'edit_post[]', method: 'patch') do + "<input name='post[123][comment][5][title]' type='text' id='post_123_comment_5_title' value='Hello World' />" + end + whole_form('/posts/123', 'edit_post', 'edit_post', method: 'patch') do + "<input name='post[1][comment][123][title]' type='text' id='post_1_comment_123_title' value='Hello World' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_a_new_record_on_a_nested_attributes_one_to_one_association + @post.author = Author.new + + form_for(@post) do |f| + concat f.text_field(:title) + concat f.fields_for(:author) { |af| + concat af.text_field(:name) + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + + '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="new author" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_explicitly_passed_object_on_a_nested_attributes_one_to_one_association + form_for(@post) do |f| + f.fields_for(:author, Author.new(123)) do |af| + assert_not_nil af.object + assert_equal 123, af.object.id + end + end + end + + def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association + @post.author = Author.new(321) + + form_for(@post) do |f| + concat f.text_field(:title) + concat f.fields_for(:author) { |af| + concat af.text_field(:name) + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + + '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' + + '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association_using_erb_and_inline_block + @post.author = Author.new(321) + + form_for(@post) do |f| + concat f.text_field(:title) + concat f.fields_for(:author) { |af| + af.text_field(:name) + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + + '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' + + '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id + @post.author = Author.new(321) + + form_for(@post) do |f| + concat f.text_field(:title) + concat f.fields_for(:author, include_id: false) { |af| + af.text_field(:name) + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + + '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id_inherited + @post.author = Author.new(321) + + form_for(@post, include_id: false) do |f| + concat f.text_field(:title) + concat f.fields_for(:author) { |af| + af.text_field(:name) + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + + '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id_override + @post.author = Author.new(321) + + form_for(@post, include_id: false) do |f| + concat f.text_field(:title) + concat f.fields_for(:author, include_id: true) { |af| + af.text_field(:name) + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + + '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' + + '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_existing_records_on_a_nested_attributes_one_to_one_association_with_explicit_hidden_field_placement + @post.author = Author.new(321) + + form_for(@post) do |f| + concat f.text_field(:title) + concat f.fields_for(:author) { |af| + concat af.hidden_field(:id) + concat af.text_field(:name) + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + + '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' + + '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + + form_for(@post) do |f| + concat f.text_field(:title) + @post.comments.each do |comment| + concat f.fields_for(:comments, comment) { |cf| + concat cf.text_field(:name) + } + end + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' + + '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + @post.author = Author.new(321) + + form_for(@post) do |f| + concat f.text_field(:title) + concat f.fields_for(:author) { |af| + concat af.text_field(:name) + } + @post.comments.each do |comment| + concat f.fields_for(:comments, comment, include_id: false) { |cf| + concat cf.text_field(:name) + } + end + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + + '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' + + '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' + + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id_inherited + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + @post.author = Author.new(321) + + form_for(@post, include_id: false) do |f| + concat f.text_field(:title) + concat f.fields_for(:author) { |af| + concat af.text_field(:name) + } + @post.comments.each do |comment| + concat f.fields_for(:comments, comment) { |cf| + concat cf.text_field(:name) + } + end + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + + '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' + + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id_override + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + @post.author = Author.new(321) + + form_for(@post, include_id: false) do |f| + concat f.text_field(:title) + concat f.fields_for(:author, include_id: true) { |af| + concat af.text_field(:name) + } + @post.comments.each do |comment| + concat f.fields_for(:comments, comment) { |cf| + concat cf.text_field(:name) + } + end + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + + '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' + + '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' + + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_using_erb_and_inline_block + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + + form_for(@post) do |f| + concat f.text_field(:title) + @post.comments.each do |comment| + concat f.fields_for(:comments, comment) { |cf| + cf.text_field(:name) + } + end + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' + + '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_explicit_hidden_field_placement + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + + form_for(@post) do |f| + concat f.text_field(:title) + @post.comments.each do |comment| + concat f.fields_for(:comments, comment) { |cf| + concat cf.hidden_field(:id) + concat cf.text_field(:name) + } + end + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + + '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' + + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_new_records_on_a_nested_attributes_collection_association + @post.comments = [Comment.new, Comment.new] + + form_for(@post) do |f| + concat f.text_field(:title) + @post.comments.each do |comment| + concat f.fields_for(:comments, comment) { |cf| + concat cf.text_field(:name) + } + end + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="new comment" />' + + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="new comment" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_existing_and_new_records_on_a_nested_attributes_collection_association + @post.comments = [Comment.new(321), Comment.new] + + form_for(@post) do |f| + concat f.text_field(:title) + @post.comments.each do |comment| + concat f.fields_for(:comments, comment) { |cf| + concat cf.text_field(:name) + } + end + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #321" />' + + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' + + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="new comment" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_an_empty_supplied_attributes_collection + form_for(@post) do |f| + concat f.text_field(:title) + f.fields_for(:comments, []) do |cf| + concat cf.text_field(:name) + end + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_existing_records_on_a_supplied_nested_attributes_collection + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + + form_for(@post) do |f| + concat f.text_field(:title) + concat f.fields_for(:comments, @post.comments) { |cf| + concat cf.text_field(:name) + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' + + '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_arel_like + @post.comments = ArelLike.new + + form_for(@post) do |f| + concat f.text_field(:title) + concat f.fields_for(:comments, @post.comments) { |cf| + concat cf.text_field(:name) + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' + + '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_existing_records_on_a_supplied_nested_attributes_collection_different_from_record_one + comments = Array.new(2) { |id| Comment.new(id + 1) } + @post.comments = [] + + form_for(@post) do |f| + concat f.text_field(:title) + concat f.fields_for(:comments, comments) { |cf| + concat cf.text_field(:name) + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' + + '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_on_a_nested_attributes_collection_association_yields_only_builder + @post.comments = [Comment.new(321), Comment.new] + yielded_comments = [] + + form_for(@post) do |f| + concat f.text_field(:title) + concat f.fields_for(:comments) { |cf| + concat cf.text_field(:name) + yielded_comments << cf.object + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #321" />' + + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' + + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="new comment" />' + end + + assert_dom_equal expected, output_buffer + assert_equal yielded_comments, @post.comments + end + + def test_nested_fields_for_with_child_index_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] + end + end + + def test_nested_fields_for_with_child_index_option_override_on_a_nested_attributes_collection_association_with_proxy + @post.comments = FakeAssociationProxy.new + + 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 + + def test_nested_fields_for_index_method_with_existing_records_on_a_nested_attributes_collection_association + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + + form_for(@post) do |f| + expected = 0 + @post.comments.each do |comment| + f.fields_for(:comments, comment) { |cf| + assert_equal cf.index, expected + expected += 1 + } + end + end + end + + def test_nested_fields_for_index_method_with_existing_and_new_records_on_a_nested_attributes_collection_association + @post.comments = [Comment.new(321), Comment.new] + + form_for(@post) do |f| + expected = 0 + @post.comments.each do |comment| + f.fields_for(:comments, comment) { |cf| + assert_equal cf.index, expected + expected += 1 + } + end + end + end + + def test_nested_fields_for_index_method_with_existing_records_on_a_supplied_nested_attributes_collection + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + + form_for(@post) do |f| + expected = 0 + f.fields_for(:comments, @post.comments) { |cf| + assert_equal cf.index, expected + expected += 1 + } + end + end + + def test_nested_fields_for_index_method_with_child_index_option_override_on_a_nested_attributes_collection_association + @post.comments = [] + + form_for(@post) do |f| + f.fields_for(:comments, Comment.new(321), child_index: 'abc') { |cf| + assert_equal cf.index, 'abc' + } + end + end + + def test_nested_fields_uses_unique_indices_for_different_collection_associations + @post.comments = [Comment.new(321)] + @post.tags = [Tag.new(123), Tag.new(456)] + @post.comments[0].relevances = [] + @post.tags[0].relevances = [] + @post.tags[1].relevances = [] + + form_for(@post) do |f| + concat f.fields_for(:comments, @post.comments[0]) { |cf| + concat cf.text_field(:name) + concat cf.fields_for(:relevances, CommentRelevance.new(314)) { |crf| + concat crf.text_field(:value) + } + } + concat f.fields_for(:tags, @post.tags[0]) { |tf| + concat tf.text_field(:value) + concat tf.fields_for(:relevances, TagRelevance.new(3141)) { |trf| + concat trf.text_field(:value) + } + } + concat f.fields_for('tags', @post.tags[1]) { |tf| + concat tf.text_field(:value) + concat tf.fields_for(:relevances, TagRelevance.new(31415)) { |trf| + concat trf.text_field(:value) + } + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #321" />' + + '<input id="post_comments_attributes_0_relevances_attributes_0_value" name="post[comments_attributes][0][relevances_attributes][0][value]" type="text" value="commentrelevance #314" />' + + '<input id="post_comments_attributes_0_relevances_attributes_0_id" name="post[comments_attributes][0][relevances_attributes][0][id]" type="hidden" value="314" />' + + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' + + '<input id="post_tags_attributes_0_value" name="post[tags_attributes][0][value]" type="text" value="tag #123" />' + + '<input id="post_tags_attributes_0_relevances_attributes_0_value" name="post[tags_attributes][0][relevances_attributes][0][value]" type="text" value="tagrelevance #3141" />' + + '<input id="post_tags_attributes_0_relevances_attributes_0_id" name="post[tags_attributes][0][relevances_attributes][0][id]" type="hidden" value="3141" />' + + '<input id="post_tags_attributes_0_id" name="post[tags_attributes][0][id]" type="hidden" value="123" />' + + '<input id="post_tags_attributes_1_value" name="post[tags_attributes][1][value]" type="text" value="tag #456" />' + + '<input id="post_tags_attributes_1_relevances_attributes_0_value" name="post[tags_attributes][1][relevances_attributes][0][value]" type="text" value="tagrelevance #31415" />' + + '<input id="post_tags_attributes_1_relevances_attributes_0_id" name="post[tags_attributes][1][relevances_attributes][0][id]" type="hidden" value="31415" />' + + '<input id="post_tags_attributes_1_id" name="post[tags_attributes][1][id]" type="hidden" value="456" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_hash_like_model + @author = HashBackedAuthor.new + + form_for(@post) do |f| + concat f.fields_for(:author, @author) { |af| + concat af.text_field(:name) + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="hash backed author" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_fields_for + output_buffer = fields_for(:post, @post) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = + "<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' />" + + assert_dom_equal expected, output_buffer + end + + def test_fields_for_with_index + output_buffer = fields_for("post[]", @post) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = + "<input name='post[123][title]' type='text' id='post_123_title' value='Hello World' />" + + "<textarea name='post[123][body]' id='post_123_body'>\nBack to the hill and over it again!</textarea>" + + "<input name='post[123][secret]' type='hidden' value='0' />" + + "<input name='post[123][secret]' checked='checked' type='checkbox' id='post_123_secret' value='1' />" + + assert_dom_equal expected, output_buffer + end + + def test_fields_for_with_nil_index_option_override + output_buffer = fields_for("post[]", @post, index: nil) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = + "<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' />" + + assert_dom_equal expected, output_buffer + end + + def test_fields_for_with_index_option_override + output_buffer = fields_for("post[]", @post, index: "abc") do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = + "<input name='post[abc][title]' type='text' id='post_abc_title' value='Hello World' />" + + "<textarea name='post[abc][body]' id='post_abc_body'>\nBack to the hill and over it again!</textarea>" + + "<input name='post[abc][secret]' type='hidden' value='0' />" + + "<input name='post[abc][secret]' checked='checked' type='checkbox' id='post_abc_secret' value='1' />" + + assert_dom_equal expected, output_buffer + end + + def test_fields_for_without_object + output_buffer = fields_for(:post) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = + "<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' />" + + assert_dom_equal expected, output_buffer + end + + def test_fields_for_with_only_object + output_buffer = fields_for(@post) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = + "<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' />" + + assert_dom_equal expected, output_buffer + end + + def test_fields_for_object_with_bracketed_name + output_buffer = fields_for("author[post]", @post) do |f| + concat f.label(:title) + concat f.text_field(:title) + end + + assert_dom_equal "<label for=\"author_post_title\">Title</label>" + + "<input name='author[post][title]' type='text' id='author_post_title' value='Hello World' />", + output_buffer + end + + def test_fields_for_object_with_bracketed_name_and_index + output_buffer = fields_for("author[post]", @post, index: 1) do |f| + concat f.label(:title) + concat f.text_field(:title) + end + + assert_dom_equal "<label for=\"author_post_1_title\">Title</label>" + + "<input name='author[post][1][title]' type='text' id='author_post_1_title' value='Hello World' />", + output_buffer + end + + def test_form_builder_does_not_have_form_for_method + assert !ActionView::Helpers::FormBuilder.instance_methods.include?(:form_for) + end + + def test_form_for_and_fields_for + form_for(@post, as: :post, html: { id: 'create-post' }) do |post_form| + concat post_form.text_field(:title) + concat post_form.text_area(:body) + + concat fields_for(:parent_post, @post) { |parent_fields| + concat parent_fields.check_box(:secret) + } + end + + expected = whole_form('/posts/123', 'create-post', 'edit_post', method: 'patch') 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='parent_post[secret]' type='hidden' value='0' />" + + "<input name='parent_post[secret]' checked='checked' type='checkbox' id='parent_post_secret' value='1' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_for_and_fields_for_with_object + form_for(@post, as: :post, html: { id: 'create-post' }) do |post_form| + concat post_form.text_field(:title) + concat post_form.text_area(:body) + + concat post_form.fields_for(@comment) { |comment_fields| + concat comment_fields.text_field(:name) + } + end + + expected = whole_form('/posts/123', 'create-post', 'edit_post', method: 'patch') 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[comment][name]' type='text' id='post_comment_name' value='new comment' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_for_and_fields_for_with_non_nested_association_and_without_object + form_for(@post) do |f| + concat f.fields_for(:category) { |c| + concat c.text_field(:name) + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + "<input name='post[category][name]' type='text' id='post_category_name' />" + end + + assert_dom_equal expected, output_buffer + end + + class LabelledFormBuilder < ActionView::Helpers::FormBuilder + (field_helpers - %w(hidden_field)).each do |selector| + class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 + def #{selector}(field, *args, &proc) + ("<label for='\#{field}'>\#{field.to_s.humanize}:</label> " + super + "<br/>").html_safe + end + RUBY_EVAL + end + end + + def test_form_for_with_labelled_builder + form_for(@post, builder: LabelledFormBuilder) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + "<label for='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><br/>" + + "<label for='body'>Body:</label> <textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea><br/>" + + "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' /><br/>" + end + + assert_dom_equal expected, output_buffer + end + + def test_default_form_builder + old_default_form_builder, ActionView::Base.default_form_builder = + ActionView::Base.default_form_builder, LabelledFormBuilder + + form_for(@post) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + "<label for='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><br/>" + + "<label for='body'>Body:</label> <textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea><br/>" + + "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' /><br/>" + end + + assert_dom_equal expected, output_buffer + ensure + ActionView::Base.default_form_builder = old_default_form_builder + end + + def test_lazy_loading_default_form_builder + old_default_form_builder, ActionView::Base.default_form_builder = + ActionView::Base.default_form_builder, "FormHelperTest::LabelledFormBuilder" + + form_for(@post) do |f| + concat f.text_field(:title) + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + "<label for='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><br/>" + end + + assert_dom_equal expected, output_buffer + ensure + ActionView::Base.default_form_builder = old_default_form_builder + end + + def test_fields_for_with_labelled_builder + output_buffer = fields_for(:post, @post, builder: LabelledFormBuilder) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = + "<label for='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><br/>" + + "<label for='body'>Body:</label> <textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea><br/>" + + "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' /><br/>" + + assert_dom_equal expected, output_buffer + end + + def test_form_for_with_labelled_builder_with_nested_fields_for_without_options_hash + klass = nil + + form_for(@post, builder: LabelledFormBuilder) do |f| + f.fields_for(:comments, Comment.new) do |nested_fields| + klass = nested_fields.class + '' + end + end + + assert_equal LabelledFormBuilder, klass + end + + def test_form_for_with_labelled_builder_with_nested_fields_for_with_options_hash + klass = nil + + form_for(@post, builder: LabelledFormBuilder) do |f| + f.fields_for(:comments, Comment.new, index: 'foo') do |nested_fields| + klass = nested_fields.class + '' + end + end + + assert_equal LabelledFormBuilder, klass + end + + def test_form_for_with_labelled_builder_path + path = nil + + form_for(@post, builder: LabelledFormBuilder) do |f| + path = f.to_partial_path + '' + end + + assert_equal 'labelled_form', path + end + + class LabelledFormBuilderSubclass < LabelledFormBuilder; end + + def test_form_for_with_labelled_builder_with_nested_fields_for_with_custom_builder + klass = nil + + form_for(@post, builder: LabelledFormBuilder) do |f| + f.fields_for(:comments, Comment.new, builder: LabelledFormBuilderSubclass) do |nested_fields| + klass = nested_fields.class + '' + end + end + + assert_equal LabelledFormBuilderSubclass, klass + end + + def test_form_for_with_html_options_adds_options_to_form_tag + form_for(@post, html: { id: 'some_form', class: 'some_class', multipart: true }) do |f| end + expected = whole_form("/posts/123", "some_form", "some_class", method: "patch", multipart: "multipart/form-data") + + assert_dom_equal expected, output_buffer + end + + 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 + end + + def test_form_for_with_hash_url_option + form_for(@post, url: { controller: 'controller', action: 'action' }) do |f| end + + assert_equal 'controller', @url_for_options[:controller] + assert_equal 'action', @url_for_options[:action] + end + + def test_form_for_with_record_url_option + 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 + 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 + end + + def test_form_for_with_new_object + post = Post.new + post.persisted = false + def post.to_key; nil; end + + form_for(post) do |f| end + + expected = whole_form("/posts", "new_post", "new_post") + assert_equal expected, output_buffer + end + + def test_form_for_with_existing_object_in_list + @comment.save + form_for([@post, @comment]) {} + + expected = whole_form(post_comment_path(@post, @comment), "edit_comment_1", "edit_comment", method: "patch") + assert_dom_equal expected, output_buffer + end + + def test_form_for_with_new_object_in_list + form_for([@post, @comment]) {} + + expected = whole_form(post_comments_path(@post), "new_comment", "new_comment") + assert_dom_equal expected, output_buffer + end + + def test_form_for_with_existing_object_and_namespace_in_list + @comment.save + form_for([:admin, @post, @comment]) {} + + expected = whole_form(admin_post_comment_path(@post, @comment), "edit_comment_1", "edit_comment", method: "patch") + assert_dom_equal expected, output_buffer + end + + def test_form_for_with_new_object_and_namespace_in_list + form_for([:admin, @post, @comment]) {} + + expected = whole_form(admin_post_comments_path(@post), "new_comment", "new_comment") + assert_dom_equal expected, output_buffer + end + + def test_form_for_with_existing_object_and_custom_url + 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 + end + + def test_form_for_with_default_method_as_patch + form_for(@post) {} + expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") + assert_dom_equal expected, output_buffer + end + + def test_form_for_with_data_attributes + form_for(@post, data: { behavior: "stuff" }, remote: true) {} + assert_match %r|data-behavior="stuff"|, output_buffer + assert_match %r|data-remote="true"|, output_buffer + end + + def test_fields_for_returns_block_result + output = fields_for(Post.new) { |f| "fields" } + assert_equal "fields", output + end + + def test_form_builder_block_argument_deprecation + builder_class = Class.new(ActionView::Helpers::FormBuilder) do + def initialize(object_name, object, template, options, block) + super + end + end + + assert_deprecated(/Giving a block to FormBuilder is deprecated and has no effect anymore/) do + builder_class.new(:foo, nil, nil, {}, proc {}) + end + end + + def test_form_for_only_instantiates_builder_once + initialization_count = 0 + builder_class = Class.new(ActionView::Helpers::FormBuilder) do + define_method :initialize do |*args| + super(*args) + initialization_count += 1 + end + end + + form_for(@post, builder: builder_class) { } + assert_equal 1, initialization_count, 'form builder instantiated more than once' + end + + protected + + def hidden_fields(method = nil) + txt = %{<div style="margin:0;padding:0;display:inline">} + txt << %{<input name="utf8" type="hidden" value="✓" />} + if method && !%w(get post).include?(method.to_s) + txt << %{<input name="_method" type="hidden" value="#{method}" />} + end + txt << %{</div>} + end + + def form_text(action = "/", id = nil, html_class = nil, remote = nil, multipart = nil, method = nil) + txt = %{<form accept-charset="UTF-8" action="#{action}"} + txt << %{ enctype="multipart/form-data"} if multipart + txt << %{ data-remote="true"} if remote + txt << %{ class="#{html_class}"} if html_class + txt << %{ id="#{id}"} if id + method = method.to_s == "get" ? "get" : "post" + txt << %{ method="#{method}">} + end + + def whole_form(action = "/", id = nil, html_class = nil, options = {}) + contents = block_given? ? yield : "" + + method, remote, multipart = options.values_at(:method, :remote, :multipart) + + form_text(action, id, html_class, remote, multipart, method) + hidden_fields(method) + contents + "</form>" + end + + def protect_against_forgery? + false + end +end diff --git a/actionview/test/template/form_options_helper_i18n_test.rb b/actionview/test/template/form_options_helper_i18n_test.rb new file mode 100644 index 0000000000..4972ea6511 --- /dev/null +++ b/actionview/test/template/form_options_helper_i18n_test.rb @@ -0,0 +1,27 @@ +require 'abstract_unit' + +class FormOptionsHelperI18nTests < ActionView::TestCase + tests ActionView::Helpers::FormOptionsHelper + + def setup + @prompt_message = 'Select!' + I18n.backend.send(:init_translations) + I18n.backend.store_translations :en, :helpers => { :select => { :prompt => @prompt_message } } + end + + def teardown + I18n.backend = I18n::Backend::Simple.new + 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) + end + + def test_select_with_translated_prompt + assert_dom_equal( + %Q(<select id="post_category" name="post[category]"><option value="">#{@prompt_message}</option>\n</select>), + select('post', 'category', [], :prompt => true) + ) + end +end diff --git a/actionview/test/template/form_options_helper_test.rb b/actionview/test/template/form_options_helper_test.rb new file mode 100644 index 0000000000..1715902927 --- /dev/null +++ b/actionview/test/template/form_options_helper_test.rb @@ -0,0 +1,1304 @@ +require 'abstract_unit' +require 'tzinfo' + +class Map < Hash + def category + "<mus>" + end +end + +TZInfo::Timezone.cattr_reader :loaded_zones + +class FormOptionsHelperTest < ActionView::TestCase + tests ActionView::Helpers::FormOptionsHelper + + silence_warnings do + Post = Struct.new('Post', :title, :author_name, :body, :secret, :written_on, :category, :origin, :allow_comments) + Continent = Struct.new('Continent', :continent_name, :countries) + Country = Struct.new('Country', :country_id, :country_name) + Firm = Struct.new('Firm', :time_zone) + Album = Struct.new('Album', :id, :title, :genre) + end + + def setup + @fake_timezones = %w(A B C D E).map do |id| + tz = TZInfo::Timezone.loaded_zones[id] = stub(:name => id, :to_s => id) + ActiveSupport::TimeZone.stubs(:[]).with(id).returns(tz) + tz + end + ActiveSupport::TimeZone.stubs(:all).returns(@fake_timezones) + end + + def test_collection_options + assert_dom_equal( + "<option value=\"<Abe>\"><Abe> went home</option>\n<option value=\"Babe\">Babe went home</option>\n<option value=\"Cabe\">Cabe went home</option>", + options_from_collection_for_select(dummy_posts, "author_name", "title") + ) + end + + + def test_collection_options_with_preselected_value + assert_dom_equal( + "<option value=\"<Abe>\"><Abe> went home</option>\n<option value=\"Babe\" selected=\"selected\">Babe went home</option>\n<option value=\"Cabe\">Cabe went home</option>", + options_from_collection_for_select(dummy_posts, "author_name", "title", "Babe") + ) + end + + def test_collection_options_with_preselected_value_array + assert_dom_equal( + "<option value=\"<Abe>\"><Abe> went home</option>\n<option value=\"Babe\" selected=\"selected\">Babe went home</option>\n<option value=\"Cabe\" selected=\"selected\">Cabe went home</option>", + options_from_collection_for_select(dummy_posts, "author_name", "title", [ "Babe", "Cabe" ]) + ) + end + + def test_collection_options_with_proc_for_selected + assert_dom_equal( + "<option value=\"<Abe>\"><Abe> went home</option>\n<option value=\"Babe\" selected=\"selected\">Babe went home</option>\n<option value=\"Cabe\">Cabe went home</option>", + options_from_collection_for_select(dummy_posts, "author_name", "title", lambda{|p| p.author_name == 'Babe' }) + ) + end + + def test_collection_options_with_disabled_value + assert_dom_equal( + "<option value=\"<Abe>\"><Abe> went home</option>\n<option value=\"Babe\" disabled=\"disabled\">Babe went home</option>\n<option value=\"Cabe\">Cabe went home</option>", + options_from_collection_for_select(dummy_posts, "author_name", "title", :disabled => "Babe") + ) + end + + def test_collection_options_with_disabled_array + assert_dom_equal( + "<option value=\"<Abe>\"><Abe> went home</option>\n<option value=\"Babe\" disabled=\"disabled\">Babe went home</option>\n<option value=\"Cabe\" disabled=\"disabled\">Cabe went home</option>", + options_from_collection_for_select(dummy_posts, "author_name", "title", :disabled => [ "Babe", "Cabe" ]) + ) + end + + def test_collection_options_with_preselected_and_disabled_value + assert_dom_equal( + "<option value=\"<Abe>\"><Abe> went home</option>\n<option value=\"Babe\" disabled=\"disabled\">Babe went home</option>\n<option value=\"Cabe\" selected=\"selected\">Cabe went home</option>", + options_from_collection_for_select(dummy_posts, "author_name", "title", :selected => "Cabe", :disabled => "Babe") + ) + end + + def test_collection_options_with_proc_for_disabled + assert_dom_equal( + "<option value=\"<Abe>\"><Abe> went home</option>\n<option value=\"Babe\" disabled=\"disabled\">Babe went home</option>\n<option value=\"Cabe\" disabled=\"disabled\">Cabe went home</option>", + options_from_collection_for_select(dummy_posts, "author_name", "title", :disabled => lambda {|p| %w(Babe Cabe).include?(p.author_name)}) + ) + end + + def test_collection_options_with_proc_for_value_method + assert_dom_equal( + "<option value=\"<Abe>\"><Abe> went home</option>\n<option value=\"Babe\">Babe went home</option>\n<option value=\"Cabe\">Cabe went home</option>", + options_from_collection_for_select(dummy_posts, lambda { |p| p.author_name }, "title") + ) + end + + def test_collection_options_with_proc_for_text_method + assert_dom_equal( + "<option value=\"<Abe>\"><Abe> went home</option>\n<option value=\"Babe\">Babe went home</option>\n<option value=\"Cabe\">Cabe went home</option>", + options_from_collection_for_select(dummy_posts, "author_name", lambda { |p| p.title }) + ) + end + + def test_collection_options_with_element_attributes + assert_dom_equal( + "<option value=\"USA\" class=\"bold\">USA</option>", + options_from_collection_for_select([[ "USA", "USA", { :class => 'bold' } ]], :first, :second) + ) + end + + def test_string_options_for_select + options = "<option value=\"Denmark\">Denmark</option><option value=\"USA\">USA</option><option value=\"Sweden\">Sweden</option>" + assert_dom_equal( + options, + options_for_select(options) + ) + end + + def test_array_options_for_select + assert_dom_equal( + "<option value=\"<Denmark>\"><Denmark></option>\n<option value=\"USA\">USA</option>\n<option value=\"Sweden\">Sweden</option>", + options_for_select([ "<Denmark>", "USA", "Sweden" ]) + ) + end + + def test_array_options_for_select_with_selection + assert_dom_equal( + "<option value=\"Denmark\">Denmark</option>\n<option value=\"<USA>\" selected=\"selected\"><USA></option>\n<option value=\"Sweden\">Sweden</option>", + options_for_select([ "Denmark", "<USA>", "Sweden" ], "<USA>") + ) + end + + def test_array_options_for_select_with_selection_array + assert_dom_equal( + "<option value=\"Denmark\">Denmark</option>\n<option value=\"<USA>\" selected=\"selected\"><USA></option>\n<option value=\"Sweden\" selected=\"selected\">Sweden</option>", + options_for_select([ "Denmark", "<USA>", "Sweden" ], [ "<USA>", "Sweden" ]) + ) + end + + def test_array_options_for_select_with_disabled_value + assert_dom_equal( + "<option value=\"Denmark\">Denmark</option>\n<option value=\"<USA>\" disabled=\"disabled\"><USA></option>\n<option value=\"Sweden\">Sweden</option>", + options_for_select([ "Denmark", "<USA>", "Sweden" ], :disabled => "<USA>") + ) + end + + def test_array_options_for_select_with_disabled_array + assert_dom_equal( + "<option value=\"Denmark\">Denmark</option>\n<option value=\"<USA>\" disabled=\"disabled\"><USA></option>\n<option value=\"Sweden\" disabled=\"disabled\">Sweden</option>", + options_for_select([ "Denmark", "<USA>", "Sweden" ], :disabled => ["<USA>", "Sweden"]) + ) + end + + def test_array_options_for_select_with_selection_and_disabled_value + assert_dom_equal( + "<option value=\"Denmark\" selected=\"selected\">Denmark</option>\n<option value=\"<USA>\" disabled=\"disabled\"><USA></option>\n<option value=\"Sweden\">Sweden</option>", + options_for_select([ "Denmark", "<USA>", "Sweden" ], :selected => "Denmark", :disabled => "<USA>") + ) + end + + def test_boolean_array_options_for_select_with_selection_and_disabled_value + assert_dom_equal( + "<option value=\"true\">true</option>\n<option value=\"false\" selected=\"selected\">false</option>", + options_for_select([ true, false ], :selected => false, :disabled => nil) + ) + end + + def test_range_options_for_select + assert_dom_equal( + "<option value=\"1\">1</option>\n<option value=\"2\">2</option>\n<option value=\"3\">3</option>", + options_for_select(1..3) + ) + end + + def test_array_options_for_string_include_in_other_string_bug_fix + assert_dom_equal( + "<option value=\"ruby\">ruby</option>\n<option value=\"rubyonrails\" selected=\"selected\">rubyonrails</option>", + options_for_select([ "ruby", "rubyonrails" ], "rubyonrails") + ) + assert_dom_equal( + "<option value=\"ruby\" selected=\"selected\">ruby</option>\n<option value=\"rubyonrails\">rubyonrails</option>", + options_for_select([ "ruby", "rubyonrails" ], "ruby") + ) + assert_dom_equal( + %(<option value="ruby" selected="selected">ruby</option>\n<option value="rubyonrails">rubyonrails</option>\n<option value=""></option>), + options_for_select([ "ruby", "rubyonrails", nil ], "ruby") + ) + end + + def test_hash_options_for_select + assert_dom_equal( + "<option value=\"Dollar\">$</option>\n<option value=\"<Kroner>\"><DKR></option>", + options_for_select("$" => "Dollar", "<DKR>" => "<Kroner>").split("\n").join("\n") + ) + assert_dom_equal( + "<option value=\"Dollar\" selected=\"selected\">$</option>\n<option value=\"<Kroner>\"><DKR></option>", + options_for_select({ "$" => "Dollar", "<DKR>" => "<Kroner>" }, "Dollar").split("\n").join("\n") + ) + assert_dom_equal( + "<option value=\"Dollar\" selected=\"selected\">$</option>\n<option value=\"<Kroner>\" selected=\"selected\"><DKR></option>", + options_for_select({ "$" => "Dollar", "<DKR>" => "<Kroner>" }, [ "Dollar", "<Kroner>" ]).split("\n").join("\n") + ) + end + + def test_ducktyped_options_for_select + quack = Struct.new(:first, :last) + assert_dom_equal( + "<option value=\"<Kroner>\"><DKR></option>\n<option value=\"Dollar\">$</option>", + options_for_select([quack.new("<DKR>", "<Kroner>"), quack.new("$", "Dollar")]) + ) + assert_dom_equal( + "<option value=\"<Kroner>\"><DKR></option>\n<option value=\"Dollar\" selected=\"selected\">$</option>", + options_for_select([quack.new("<DKR>", "<Kroner>"), quack.new("$", "Dollar")], "Dollar") + ) + assert_dom_equal( + "<option value=\"<Kroner>\" selected=\"selected\"><DKR></option>\n<option value=\"Dollar\" selected=\"selected\">$</option>", + options_for_select([quack.new("<DKR>", "<Kroner>"), quack.new("$", "Dollar")], ["Dollar", "<Kroner>"]) + ) + end + + def test_collection_options_with_preselected_value_as_string_and_option_value_is_integer + albums = [ Album.new(1, "first","rap"), Album.new(2, "second","pop")] + assert_dom_equal( + %(<option selected="selected" value="1">rap</option>\n<option value="2">pop</option>), + options_from_collection_for_select(albums, "id", "genre", :selected => "1") + ) + end + + def test_collection_options_with_preselected_value_as_integer_and_option_value_is_string + albums = [ Album.new("1", "first","rap"), Album.new("2", "second","pop")] + + assert_dom_equal( + %(<option selected="selected" value="1">rap</option>\n<option value="2">pop</option>), + options_from_collection_for_select(albums, "id", "genre", :selected => 1) + ) + end + + def test_collection_options_with_preselected_value_as_string_and_option_value_is_float + albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop")] + + assert_dom_equal( + %(<option value="1.0">rap</option>\n<option value="2.0" selected="selected">pop</option>), + options_from_collection_for_select(albums, "id", "genre", :selected => "2.0") + ) + end + + def test_collection_options_with_preselected_value_as_nil + albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop")] + + assert_dom_equal( + %(<option value="1.0">rap</option>\n<option value="2.0">pop</option>), + options_from_collection_for_select(albums, "id", "genre", :selected => nil) + ) + end + + def test_collection_options_with_disabled_value_as_nil + albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop")] + + assert_dom_equal( + %(<option value="1.0">rap</option>\n<option value="2.0">pop</option>), + options_from_collection_for_select(albums, "id", "genre", :disabled => nil) + ) + end + + def test_collection_options_with_disabled_value_as_array + albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop")] + + assert_dom_equal( + %(<option disabled="disabled" value="1.0">rap</option>\n<option disabled="disabled" value="2.0">pop</option>), + options_from_collection_for_select(albums, "id", "genre", :disabled => ["1.0", 2.0]) + ) + end + + def test_collection_options_with_preselected_values_as_string_array_and_option_value_is_float + albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop"), Album.new(3.0, "third","country") ] + + assert_dom_equal( + %(<option value="1.0" selected="selected">rap</option>\n<option value="2.0">pop</option>\n<option value="3.0" selected="selected">country</option>), + options_from_collection_for_select(albums, "id", "genre", ["1.0","3.0"]) + ) + end + + def test_option_groups_from_collection_for_select + assert_dom_equal( + "<optgroup label=\"<Africa>\"><option value=\"<sa>\"><South Africa></option>\n<option value=\"so\">Somalia</option></optgroup><optgroup label=\"Europe\"><option value=\"dk\" selected=\"selected\">Denmark</option>\n<option value=\"ie\">Ireland</option></optgroup>", + option_groups_from_collection_for_select(dummy_continents, "countries", "continent_name", "country_id", "country_name", "dk") + ) + end + + def test_option_groups_from_collection_for_select_returns_html_safe_string + assert option_groups_from_collection_for_select(dummy_continents, "countries", "continent_name", "country_id", "country_name", "dk").html_safe? + end + + def test_grouped_options_for_select_with_array + assert_dom_equal( + "<optgroup label=\"North America\"><option value=\"US\">United States</option>\n<option value=\"Canada\">Canada</option></optgroup><optgroup label=\"Europe\"><option value=\"GB\">Great Britain</option>\n<option value=\"Germany\">Germany</option></optgroup>", + grouped_options_for_select([ + ["North America", + [['United States','US'],"Canada"]], + ["Europe", + [["Great Britain","GB"], "Germany"]] + ]) + ) + end + + def test_grouped_options_for_select_with_optional_divider + assert_dom_equal( + "<optgroup label=\"----------\"><option value=\"US\">US</option>\n<option value=\"Canada\">Canada</option></optgroup><optgroup label=\"----------\"><option value=\"GB\">GB</option>\n<option value=\"Germany\">Germany</option></optgroup>", + + grouped_options_for_select([['US',"Canada"] , ["GB", "Germany"]], nil, divider: "----------") + ) + end + + def test_grouped_options_for_select_with_selected_and_prompt_deprecated + assert_deprecated 'Passing the prompt to grouped_options_for_select as an argument is deprecated. Please use an options hash like `{ prompt: "Choose a product..." }`.' do + assert_dom_equal( + "<option value=\"\">Choose a product...</option><optgroup label=\"Hats\"><option value=\"Baseball Cap\">Baseball Cap</option>\n<option selected=\"selected\" value=\"Cowboy Hat\">Cowboy Hat</option></optgroup>", + grouped_options_for_select([["Hats", ["Baseball Cap","Cowboy Hat"]]], "Cowboy Hat", "Choose a product...") + ) + end + end + + def test_grouped_options_for_select_with_selected_and_prompt + assert_dom_equal( + "<option value=\"\">Choose a product...</option><optgroup label=\"Hats\"><option value=\"Baseball Cap\">Baseball Cap</option>\n<option selected=\"selected\" value=\"Cowboy Hat\">Cowboy Hat</option></optgroup>", + grouped_options_for_select([["Hats", ["Baseball Cap","Cowboy Hat"]]], "Cowboy Hat", prompt: "Choose a product...") + ) + end + + def test_grouped_options_for_select_with_selected_and_prompt_true + assert_dom_equal( + "<option value=\"\">Please select</option><optgroup label=\"Hats\"><option value=\"Baseball Cap\">Baseball Cap</option>\n<option selected=\"selected\" value=\"Cowboy Hat\">Cowboy Hat</option></optgroup>", + grouped_options_for_select([["Hats", ["Baseball Cap","Cowboy Hat"]]], "Cowboy Hat", prompt: true) + ) + end + + def test_grouped_options_for_select_returns_html_safe_string + assert grouped_options_for_select([["Hats", ["Baseball Cap","Cowboy Hat"]]]).html_safe? + end + + def test_grouped_options_for_select_with_prompt_returns_html_escaped_string_deprecated + ActiveSupport::Deprecation.silence do + assert_dom_equal( + "<option value=\"\"><Choose One></option><optgroup label=\"Hats\"><option value=\"Baseball Cap\">Baseball Cap</option>\n<option value=\"Cowboy Hat\">Cowboy Hat</option></optgroup>", + grouped_options_for_select([["Hats", ["Baseball Cap","Cowboy Hat"]]], nil, '<Choose One>')) + end + end + + def test_grouped_options_for_select_with_prompt_returns_html_escaped_string + assert_dom_equal( + "<option value=\"\"><Choose One></option><optgroup label=\"Hats\"><option value=\"Baseball Cap\">Baseball Cap</option>\n<option value=\"Cowboy Hat\">Cowboy Hat</option></optgroup>", + grouped_options_for_select([["Hats", ["Baseball Cap","Cowboy Hat"]]], nil, prompt: '<Choose One>')) + end + + def test_optgroups_with_with_options_with_hash + assert_dom_equal( + "<optgroup label=\"North America\"><option value=\"United States\">United States</option>\n<option value=\"Canada\">Canada</option></optgroup><optgroup label=\"Europe\"><option value=\"Denmark\">Denmark</option>\n<option value=\"Germany\">Germany</option></optgroup>", + grouped_options_for_select({'North America' => ['United States','Canada'], 'Europe' => ['Denmark','Germany']}) + ) + end + + def test_time_zone_options_no_params + opts = time_zone_options_for_select + assert_dom_equal "<option value=\"A\">A</option>\n" + + "<option value=\"B\">B</option>\n" + + "<option value=\"C\">C</option>\n" + + "<option value=\"D\">D</option>\n" + + "<option value=\"E\">E</option>", + opts + end + + def test_time_zone_options_with_selected + opts = time_zone_options_for_select( "D" ) + assert_dom_equal "<option value=\"A\">A</option>\n" + + "<option value=\"B\">B</option>\n" + + "<option value=\"C\">C</option>\n" + + "<option value=\"D\" selected=\"selected\">D</option>\n" + + "<option value=\"E\">E</option>", + opts + end + + def test_time_zone_options_with_unknown_selected + opts = time_zone_options_for_select( "K" ) + assert_dom_equal "<option value=\"A\">A</option>\n" + + "<option value=\"B\">B</option>\n" + + "<option value=\"C\">C</option>\n" + + "<option value=\"D\">D</option>\n" + + "<option value=\"E\">E</option>", + opts + end + + def test_time_zone_options_with_priority_zones + zones = [ ActiveSupport::TimeZone.new( "B" ), ActiveSupport::TimeZone.new( "E" ) ] + opts = time_zone_options_for_select( nil, zones ) + assert_dom_equal "<option value=\"B\">B</option>\n" + + "<option value=\"E\">E</option>" + + "<option value=\"\" disabled=\"disabled\">-------------</option>\n" + + "<option value=\"A\">A</option>\n" + + "<option value=\"C\">C</option>\n" + + "<option value=\"D\">D</option>", + opts + end + + def test_time_zone_options_with_selected_priority_zones + zones = [ ActiveSupport::TimeZone.new( "B" ), ActiveSupport::TimeZone.new( "E" ) ] + opts = time_zone_options_for_select( "E", zones ) + assert_dom_equal "<option value=\"B\">B</option>\n" + + "<option value=\"E\" selected=\"selected\">E</option>" + + "<option value=\"\" disabled=\"disabled\">-------------</option>\n" + + "<option value=\"A\">A</option>\n" + + "<option value=\"C\">C</option>\n" + + "<option value=\"D\">D</option>", + opts + end + + def test_time_zone_options_with_unselected_priority_zones + zones = [ ActiveSupport::TimeZone.new( "B" ), ActiveSupport::TimeZone.new( "E" ) ] + opts = time_zone_options_for_select( "C", zones ) + assert_dom_equal "<option value=\"B\">B</option>\n" + + "<option value=\"E\">E</option>" + + "<option value=\"\" disabled=\"disabled\">-------------</option>\n" + + "<option value=\"A\">A</option>\n" + + "<option value=\"C\" selected=\"selected\">C</option>\n" + + "<option value=\"D\">D</option>", + opts + end + + def test_time_zone_options_with_priority_zones_does_not_mutate_time_zones + original_zones = ActiveSupport::TimeZone.all.dup + zones = [ ActiveSupport::TimeZone.new( "B" ), ActiveSupport::TimeZone.new( "E" ) ] + time_zone_options_for_select(nil, zones) + assert_equal original_zones, ActiveSupport::TimeZone.all + end + + def test_time_zone_options_returns_html_safe_string + assert time_zone_options_for_select.html_safe? + end + + def test_select + @post = Post.new + @post.category = "<mus>" + assert_dom_equal( + "<select id=\"post_category\" name=\"post[category]\"><option value=\"abe\">abe</option>\n<option value=\"<mus>\" selected=\"selected\"><mus></option>\n<option value=\"hest\">hest</option></select>", + select("post", "category", %w( abe <mus> hest)) + ) + end + + def test_select_without_multiple + assert_dom_equal( + "<select id=\"post_category\" name=\"post[category]\"></select>", + select(:post, :category, "", {}, :multiple => false) + ) + end + + def test_select_with_grouped_collection_as_nested_array + @post = Post.new + + countries_by_continent = [ + ["<Africa>", [["<South Africa>", "<sa>"], ["Somalia", "so"]]], + ["Europe", [["Denmark", "dk"], ["Ireland", "ie"]]], + ] + + assert_dom_equal( + [ + %Q{<select id="post_origin" name="post[origin]"><optgroup label="<Africa>"><option value="<sa>"><South Africa></option>}, + %Q{<option value="so">Somalia</option></optgroup><optgroup label="Europe"><option value="dk">Denmark</option>}, + %Q{<option value="ie">Ireland</option></optgroup></select>}, + ].join("\n"), + select("post", "origin", countries_by_continent) + ) + end + + def test_select_with_grouped_collection_as_hash + @post = Post.new + + countries_by_continent = { + "<Africa>" => [["<South Africa>", "<sa>"], ["Somalia", "so"]], + "Europe" => [["Denmark", "dk"], ["Ireland", "ie"]], + } + + assert_dom_equal( + [ + %Q{<select id="post_origin" name="post[origin]"><optgroup label="<Africa>"><option value="<sa>"><South Africa></option>}, + %Q{<option value="so">Somalia</option></optgroup><optgroup label="Europe"><option value="dk">Denmark</option>}, + %Q{<option value="ie">Ireland</option></optgroup></select>}, + ].join("\n"), + select("post", "origin", countries_by_continent) + ) + end + + def test_select_with_boolean_method + @post = Post.new + @post.allow_comments = false + assert_dom_equal( + "<select id=\"post_allow_comments\" name=\"post[allow_comments]\"><option value=\"true\">true</option>\n<option value=\"false\" selected=\"selected\">false</option></select>", + select("post", "allow_comments", %w( true false )) + ) + end + + def test_select_under_fields_for + @post = Post.new + @post.category = "<mus>" + + output_buffer = fields_for :post, @post do |f| + concat f.select(:category, %w( abe <mus> hest)) + end + + assert_dom_equal( + "<select id=\"post_category\" name=\"post[category]\"><option value=\"abe\">abe</option>\n<option value=\"<mus>\" selected=\"selected\"><mus></option>\n<option value=\"hest\">hest</option></select>", + output_buffer + ) + end + + def test_fields_for_with_record_inherited_from_hash + map = Map.new + + output_buffer = fields_for :map, map do |f| + concat f.select(:category, %w( abe <mus> hest)) + end + + assert_dom_equal( + "<select id=\"map_category\" name=\"map[category]\"><option value=\"abe\">abe</option>\n<option value=\"<mus>\" selected=\"selected\"><mus></option>\n<option value=\"hest\">hest</option></select>", + output_buffer + ) + end + + def test_select_under_fields_for_with_index + @post = Post.new + @post.category = "<mus>" + + output_buffer = fields_for :post, @post, :index => 108 do |f| + concat f.select(:category, %w( abe <mus> hest)) + end + + assert_dom_equal( + "<select id=\"post_108_category\" name=\"post[108][category]\"><option value=\"abe\">abe</option>\n<option value=\"<mus>\" selected=\"selected\"><mus></option>\n<option value=\"hest\">hest</option></select>", + output_buffer + ) + end + + def test_select_under_fields_for_with_auto_index + @post = Post.new + @post.category = "<mus>" + def @post.to_param; 108; end + + output_buffer = fields_for "post[]", @post do |f| + concat f.select(:category, %w( abe <mus> hest)) + end + + assert_dom_equal( + "<select id=\"post_108_category\" name=\"post[108][category]\"><option value=\"abe\">abe</option>\n<option value=\"<mus>\" selected=\"selected\"><mus></option>\n<option value=\"hest\">hest</option></select>", + output_buffer + ) + end + + def test_select_under_fields_for_with_string_and_given_prompt + @post = Post.new + options = "<option value=\"abe\">abe</option><option value=\"mus\">mus</option><option value=\"hest\">hest</option>".html_safe + + output_buffer = fields_for :post, @post do |f| + concat f.select(:category, options, :prompt => 'The prompt') + end + + assert_dom_equal( + "<select id=\"post_category\" name=\"post[category]\"><option value=\"\">The prompt</option>\n#{options}</select>", + output_buffer + ) + end + + def test_select_with_multiple_to_add_hidden_input + output_buffer = select(:post, :category, "", {}, :multiple => true) + assert_dom_equal( + "<input type=\"hidden\" name=\"post[category][]\" value=\"\"/><select multiple=\"multiple\" id=\"post_category\" name=\"post[category][]\"></select>", + output_buffer + ) + end + + def test_select_with_multiple_and_without_hidden_input + output_buffer = select(:post, :category, "", {:include_hidden => false}, :multiple => true) + assert_dom_equal( + "<select multiple=\"multiple\" id=\"post_category\" name=\"post[category][]\"></select>", + output_buffer + ) + end + + def test_select_with_multiple_and_with_explicit_name_ending_with_brackets + output_buffer = select(:post, :category, [], {include_hidden: false}, multiple: true, name: 'post[category][]') + assert_dom_equal( + "<select multiple=\"multiple\" id=\"post_category\" name=\"post[category][]\"></select>", + output_buffer + ) + end + + def test_select_with_multiple_and_disabled_to_add_disabled_hidden_input + output_buffer = select(:post, :category, "", {}, :multiple => true, :disabled => true) + assert_dom_equal( + "<input disabled=\"disabled\"type=\"hidden\" name=\"post[category][]\" value=\"\"/><select multiple=\"multiple\" disabled=\"disabled\" id=\"post_category\" name=\"post[category][]\"></select>", + output_buffer + ) + end + + def test_select_with_blank + @post = Post.new + @post.category = "<mus>" + assert_dom_equal( + "<select id=\"post_category\" name=\"post[category]\"><option value=\"\"></option>\n<option value=\"abe\">abe</option>\n<option value=\"<mus>\" selected=\"selected\"><mus></option>\n<option value=\"hest\">hest</option></select>", + select("post", "category", %w( abe <mus> hest), :include_blank => true) + ) + end + + def test_select_with_blank_as_string + @post = Post.new + @post.category = "<mus>" + assert_dom_equal( + "<select id=\"post_category\" name=\"post[category]\"><option value=\"\">None</option>\n<option value=\"abe\">abe</option>\n<option value=\"<mus>\" selected=\"selected\"><mus></option>\n<option value=\"hest\">hest</option></select>", + select("post", "category", %w( abe <mus> hest), :include_blank => 'None') + ) + end + + def test_select_with_blank_as_string_escaped + @post = Post.new + @post.category = "<mus>" + assert_dom_equal( + "<select id=\"post_category\" name=\"post[category]\"><option value=\"\"><None></option>\n<option value=\"abe\">abe</option>\n<option value=\"<mus>\" selected=\"selected\"><mus></option>\n<option value=\"hest\">hest</option></select>", + select("post", "category", %w( abe <mus> hest), :include_blank => '<None>') + ) + end + + def test_select_with_default_prompt + @post = Post.new + @post.category = "" + assert_dom_equal( + "<select id=\"post_category\" name=\"post[category]\"><option value=\"\">Please select</option>\n<option value=\"abe\">abe</option>\n<option value=\"<mus>\"><mus></option>\n<option value=\"hest\">hest</option></select>", + select("post", "category", %w( abe <mus> hest), :prompt => true) + ) + end + + def test_select_no_prompt_when_select_has_value + @post = Post.new + @post.category = "<mus>" + assert_dom_equal( + "<select id=\"post_category\" name=\"post[category]\"><option value=\"abe\">abe</option>\n<option value=\"<mus>\" selected=\"selected\"><mus></option>\n<option value=\"hest\">hest</option></select>", + select("post", "category", %w( abe <mus> hest), :prompt => true) + ) + end + + def test_select_with_given_prompt + @post = Post.new + @post.category = "" + assert_dom_equal( + "<select id=\"post_category\" name=\"post[category]\"><option value=\"\">The prompt</option>\n<option value=\"abe\">abe</option>\n<option value=\"<mus>\"><mus></option>\n<option value=\"hest\">hest</option></select>", + select("post", "category", %w( abe <mus> hest), :prompt => 'The prompt') + ) + end + + def test_select_with_given_prompt_escaped + @post = Post.new + assert_dom_equal( + "<select id=\"post_category\" name=\"post[category]\"><option value=\"\"><The prompt></option>\n<option value=\"abe\">abe</option>\n<option value=\"<mus>\"><mus></option>\n<option value=\"hest\">hest</option></select>", + select("post", "category", %w( abe <mus> hest), :prompt => '<The prompt>') + ) + end + + def test_select_with_prompt_and_blank + @post = Post.new + @post.category = "" + assert_dom_equal( + "<select id=\"post_category\" name=\"post[category]\"><option value=\"\">Please select</option>\n<option value=\"\"></option>\n<option value=\"abe\">abe</option>\n<option value=\"<mus>\"><mus></option>\n<option value=\"hest\">hest</option></select>", + select("post", "category", %w( abe <mus> hest), :prompt => true, :include_blank => true) + ) + end + + def test_empty + @post = Post.new + @post.category = "" + assert_dom_equal( + "<select id=\"post_category\" name=\"post[category]\"><option value=\"\">Please select</option>\n<option value=\"\"></option>\n</select>", + select("post", "category", [], :prompt => true, :include_blank => true) + ) + end + + def test_select_with_nil + @post = Post.new + @post.category = "othervalue" + assert_dom_equal( + "<select id=\"post_category\" name=\"post[category]\"><option value=\"\"></option>\n<option value=\"othervalue\" selected=\"selected\">othervalue</option></select>", + select("post", "category", [nil, "othervalue"]) + ) + end + + def test_required_select + assert_dom_equal( + %(<select id="post_category" name="post[category]" required="required"><option value=""></option>\n<option value="abe">abe</option>\n<option value="mus">mus</option>\n<option value="hest">hest</option></select>), + select("post", "category", %w(abe mus hest), {}, required: true) + ) + end + + def test_required_select_with_include_blank_prompt + assert_dom_equal( + %(<select id="post_category" name="post[category]" required="required"><option value="">Select one</option>\n<option value="abe">abe</option>\n<option value="mus">mus</option>\n<option value="hest">hest</option></select>), + select("post", "category", %w(abe mus hest), { include_blank: "Select one" }, required: true) + ) + end + + def test_required_select_with_prompt + assert_dom_equal( + %(<select id="post_category" name="post[category]" required="required"><option value="">Select one</option>\n<option value="abe">abe</option>\n<option value="mus">mus</option>\n<option value="hest">hest</option></select>), + select("post", "category", %w(abe mus hest), { prompt: "Select one" }, required: true) + ) + end + + def test_required_select_display_size_equals_to_one + assert_dom_equal( + %(<select id="post_category" name="post[category]" required="required" size="1"><option value=""></option>\n<option value="abe">abe</option>\n<option value="mus">mus</option>\n<option value="hest">hest</option></select>), + select("post", "category", %w(abe mus hest), {}, required: true, size: 1) + ) + end + + def test_required_select_with_display_size_bigger_than_one + assert_dom_equal( + %(<select id="post_category" name="post[category]" required="required" size="2"><option value="abe">abe</option>\n<option value="mus">mus</option>\n<option value="hest">hest</option></select>), + select("post", "category", %w(abe mus hest), {}, required: true, size: 2) + ) + end + + def test_required_select_with_multiple_option + assert_dom_equal( + %(<input name="post[category][]" type="hidden" value=""/><select id="post_category" multiple="multiple" name="post[category][]" required="required"><option value="abe">abe</option>\n<option value="mus">mus</option>\n<option value="hest">hest</option></select>), + select("post", "category", %w(abe mus hest), {}, required: true, multiple: true) + ) + end + + def test_select_with_fixnum + @post = Post.new + @post.category = "" + assert_dom_equal( + "<select id=\"post_category\" name=\"post[category]\"><option value=\"\">Please select</option>\n<option value=\"\"></option>\n<option value=\"1\">1</option></select>", + select("post", "category", [1], :prompt => true, :include_blank => true) + ) + end + + def test_list_of_lists + @post = Post.new + @post.category = "" + assert_dom_equal( + "<select id=\"post_category\" name=\"post[category]\"><option value=\"\">Please select</option>\n<option value=\"\"></option>\n<option value=\"number\">Number</option>\n<option value=\"text\">Text</option>\n<option value=\"boolean\">Yes/No</option></select>", + select("post", "category", [["Number", "number"], ["Text", "text"], ["Yes/No", "boolean"]], :prompt => true, :include_blank => true) + ) + end + + def test_select_with_selected_value + @post = Post.new + @post.category = "<mus>" + assert_dom_equal( + "<select id=\"post_category\" name=\"post[category]\"><option value=\"abe\" selected=\"selected\">abe</option>\n<option value=\"<mus>\"><mus></option>\n<option value=\"hest\">hest</option></select>", + select("post", "category", %w( abe <mus> hest ), :selected => 'abe') + ) + end + + def test_select_with_index_option + @album = Album.new + @album.id = 1 + + expected = "<select id=\"album__genre\" name=\"album[][genre]\"><option value=\"rap\">rap</option>\n<option value=\"rock\">rock</option>\n<option value=\"country\">country</option></select>" + + assert_dom_equal( + expected, + select("album[]", "genre", %w[rap rock country], {}, { :index => nil }) + ) + end + + def test_select_escapes_options + assert_dom_equal( + '<select id="post_title" name="post[title]"><script>alert(1)</script></select>', + select('post', 'title', '<script>alert(1)</script>') + ) + end + + def test_select_with_selected_nil + @post = Post.new + @post.category = "<mus>" + assert_dom_equal( + "<select id=\"post_category\" name=\"post[category]\"><option value=\"abe\">abe</option>\n<option value=\"<mus>\"><mus></option>\n<option value=\"hest\">hest</option></select>", + select("post", "category", %w( abe <mus> hest ), :selected => nil) + ) + end + + def test_select_with_disabled_value + @post = Post.new + @post.category = "<mus>" + assert_dom_equal( + "<select id=\"post_category\" name=\"post[category]\"><option value=\"abe\">abe</option>\n<option value=\"<mus>\" selected=\"selected\"><mus></option>\n<option value=\"hest\" disabled=\"disabled\">hest</option></select>", + select("post", "category", %w( abe <mus> hest ), :disabled => 'hest') + ) + end + + def test_select_with_disabled_array + @post = Post.new + @post.category = "<mus>" + assert_dom_equal( + "<select id=\"post_category\" name=\"post[category]\"><option value=\"abe\" disabled=\"disabled\">abe</option>\n<option value=\"<mus>\" selected=\"selected\"><mus></option>\n<option value=\"hest\" disabled=\"disabled\">hest</option></select>", + select("post", "category", %w( abe <mus> hest ), :disabled => ['hest', 'abe']) + ) + end + + def test_select_with_range + @post = Post.new + @post.category = 0 + assert_dom_equal( + "<select id=\"post_category\" name=\"post[category]\"><option value=\"1\">1</option>\n<option value=\"2\">2</option>\n<option value=\"3\">3</option></select>", + select("post", "category", 1..3) + ) + end + + def test_collection_select + @post = Post.new + @post.author_name = "Babe" + + assert_dom_equal( + "<select id=\"post_author_name\" name=\"post[author_name]\"><option value=\"<Abe>\"><Abe></option>\n<option value=\"Babe\" selected=\"selected\">Babe</option>\n<option value=\"Cabe\">Cabe</option></select>", + collection_select("post", "author_name", dummy_posts, "author_name", "author_name") + ) + end + + def test_collection_select_under_fields_for + @post = Post.new + @post.author_name = "Babe" + + output_buffer = fields_for :post, @post do |f| + concat f.collection_select(:author_name, dummy_posts, :author_name, :author_name) + end + + assert_dom_equal( + "<select id=\"post_author_name\" name=\"post[author_name]\"><option value=\"<Abe>\"><Abe></option>\n<option value=\"Babe\" selected=\"selected\">Babe</option>\n<option value=\"Cabe\">Cabe</option></select>", + output_buffer + ) + end + + def test_collection_select_under_fields_for_with_index + @post = Post.new + @post.author_name = "Babe" + + output_buffer = fields_for :post, @post, :index => 815 do |f| + concat f.collection_select(:author_name, dummy_posts, :author_name, :author_name) + end + + assert_dom_equal( + "<select id=\"post_815_author_name\" name=\"post[815][author_name]\"><option value=\"<Abe>\"><Abe></option>\n<option value=\"Babe\" selected=\"selected\">Babe</option>\n<option value=\"Cabe\">Cabe</option></select>", + output_buffer + ) + end + + def test_collection_select_under_fields_for_with_auto_index + @post = Post.new + @post.author_name = "Babe" + def @post.to_param; 815; end + + output_buffer = fields_for "post[]", @post do |f| + concat f.collection_select(:author_name, dummy_posts, :author_name, :author_name) + end + + assert_dom_equal( + "<select id=\"post_815_author_name\" name=\"post[815][author_name]\"><option value=\"<Abe>\"><Abe></option>\n<option value=\"Babe\" selected=\"selected\">Babe</option>\n<option value=\"Cabe\">Cabe</option></select>", + output_buffer + ) + end + + def test_collection_select_with_blank_and_style + @post = Post.new + @post.author_name = "Babe" + + assert_dom_equal( + "<select id=\"post_author_name\" name=\"post[author_name]\" style=\"width: 200px\"><option value=\"\"></option>\n<option value=\"<Abe>\"><Abe></option>\n<option value=\"Babe\" selected=\"selected\">Babe</option>\n<option value=\"Cabe\">Cabe</option></select>", + collection_select("post", "author_name", dummy_posts, "author_name", "author_name", { :include_blank => true }, "style" => "width: 200px") + ) + end + + def test_collection_select_with_blank_as_string_and_style + @post = Post.new + @post.author_name = "Babe" + + assert_dom_equal( + "<select id=\"post_author_name\" name=\"post[author_name]\" style=\"width: 200px\"><option value=\"\">No Selection</option>\n<option value=\"<Abe>\"><Abe></option>\n<option value=\"Babe\" selected=\"selected\">Babe</option>\n<option value=\"Cabe\">Cabe</option></select>", + collection_select("post", "author_name", dummy_posts, "author_name", "author_name", { :include_blank => 'No Selection' }, "style" => "width: 200px") + ) + end + + def test_collection_select_with_multiple_option_appends_array_brackets_and_hidden_input + @post = Post.new + @post.author_name = "Babe" + + expected = "<input type=\"hidden\" name=\"post[author_name][]\" value=\"\"/><select id=\"post_author_name\" name=\"post[author_name][]\" multiple=\"multiple\"><option value=\"\"></option>\n<option value=\"<Abe>\"><Abe></option>\n<option value=\"Babe\" selected=\"selected\">Babe</option>\n<option value=\"Cabe\">Cabe</option></select>" + + # Should suffix default name with []. + assert_dom_equal expected, collection_select("post", "author_name", dummy_posts, "author_name", "author_name", { :include_blank => true }, :multiple => true) + + # Shouldn't suffix custom name with []. + assert_dom_equal expected, collection_select("post", "author_name", dummy_posts, "author_name", "author_name", { :include_blank => true, :name => 'post[author_name][]' }, :multiple => true) + end + + def test_collection_select_with_blank_and_selected + @post = Post.new + @post.author_name = "Babe" + + assert_dom_equal( + %{<select id="post_author_name" name="post[author_name]"><option value=""></option>\n<option value="<Abe>" selected="selected"><Abe></option>\n<option value="Babe">Babe</option>\n<option value="Cabe">Cabe</option></select>}, + collection_select("post", "author_name", dummy_posts, "author_name", "author_name", {:include_blank => true, :selected => "<Abe>"}) + ) + end + + def test_collection_select_with_disabled + @post = Post.new + @post.author_name = "Babe" + + assert_dom_equal( + "<select id=\"post_author_name\" name=\"post[author_name]\"><option value=\"<Abe>\"><Abe></option>\n<option value=\"Babe\" selected=\"selected\">Babe</option>\n<option value=\"Cabe\" disabled=\"disabled\">Cabe</option></select>", + collection_select("post", "author_name", dummy_posts, "author_name", "author_name", :disabled => 'Cabe') + ) + end + + def test_collection_select_with_proc_for_value_method + @post = Post.new + + assert_dom_equal( + "<select id=\"post_author_name\" name=\"post[author_name]\"><option value=\"<Abe>\"><Abe> went home</option>\n<option value=\"Babe\">Babe went home</option>\n<option value=\"Cabe\">Cabe went home</option></select>", + collection_select("post", "author_name", dummy_posts, lambda { |p| p.author_name }, "title") + ) + end + + def test_collection_select_with_proc_for_text_method + @post = Post.new + + assert_dom_equal( + "<select id=\"post_author_name\" name=\"post[author_name]\"><option value=\"<Abe>\"><Abe> went home</option>\n<option value=\"Babe\">Babe went home</option>\n<option value=\"Cabe\">Cabe went home</option></select>", + collection_select("post", "author_name", dummy_posts, "author_name", lambda { |p| p.title }) + ) + end + + def test_time_zone_select + @firm = Firm.new("D") + html = time_zone_select( "firm", "time_zone" ) + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + + "<option value=\"A\">A</option>\n" + + "<option value=\"B\">B</option>\n" + + "<option value=\"C\">C</option>\n" + + "<option value=\"D\" selected=\"selected\">D</option>\n" + + "<option value=\"E\">E</option>" + + "</select>", + html + end + + def test_time_zone_select_under_fields_for + @firm = Firm.new("D") + + output_buffer = fields_for :firm, @firm do |f| + concat f.time_zone_select(:time_zone) + end + + assert_dom_equal( + "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + + "<option value=\"A\">A</option>\n" + + "<option value=\"B\">B</option>\n" + + "<option value=\"C\">C</option>\n" + + "<option value=\"D\" selected=\"selected\">D</option>\n" + + "<option value=\"E\">E</option>" + + "</select>", + output_buffer + ) + end + + def test_time_zone_select_under_fields_for_with_index + @firm = Firm.new("D") + + output_buffer = fields_for :firm, @firm, :index => 305 do |f| + concat f.time_zone_select(:time_zone) + end + + assert_dom_equal( + "<select id=\"firm_305_time_zone\" name=\"firm[305][time_zone]\">" + + "<option value=\"A\">A</option>\n" + + "<option value=\"B\">B</option>\n" + + "<option value=\"C\">C</option>\n" + + "<option value=\"D\" selected=\"selected\">D</option>\n" + + "<option value=\"E\">E</option>" + + "</select>", + output_buffer + ) + end + + def test_time_zone_select_under_fields_for_with_auto_index + @firm = Firm.new("D") + def @firm.to_param; 305; end + + output_buffer = fields_for "firm[]", @firm do |f| + concat f.time_zone_select(:time_zone) + end + + assert_dom_equal( + "<select id=\"firm_305_time_zone\" name=\"firm[305][time_zone]\">" + + "<option value=\"A\">A</option>\n" + + "<option value=\"B\">B</option>\n" + + "<option value=\"C\">C</option>\n" + + "<option value=\"D\" selected=\"selected\">D</option>\n" + + "<option value=\"E\">E</option>" + + "</select>", + output_buffer + ) + end + + def test_time_zone_select_with_blank + @firm = Firm.new("D") + html = time_zone_select("firm", "time_zone", nil, :include_blank => true) + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + + "<option value=\"\"></option>\n" + + "<option value=\"A\">A</option>\n" + + "<option value=\"B\">B</option>\n" + + "<option value=\"C\">C</option>\n" + + "<option value=\"D\" selected=\"selected\">D</option>\n" + + "<option value=\"E\">E</option>" + + "</select>", + html + end + + def test_time_zone_select_with_blank_as_string + @firm = Firm.new("D") + html = time_zone_select("firm", "time_zone", nil, :include_blank => 'No Zone') + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + + "<option value=\"\">No Zone</option>\n" + + "<option value=\"A\">A</option>\n" + + "<option value=\"B\">B</option>\n" + + "<option value=\"C\">C</option>\n" + + "<option value=\"D\" selected=\"selected\">D</option>\n" + + "<option value=\"E\">E</option>" + + "</select>", + html + end + + def test_time_zone_select_with_style + @firm = Firm.new("D") + html = time_zone_select("firm", "time_zone", nil, {}, + "style" => "color: red") + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\" style=\"color: red\">" + + "<option value=\"A\">A</option>\n" + + "<option value=\"B\">B</option>\n" + + "<option value=\"C\">C</option>\n" + + "<option value=\"D\" selected=\"selected\">D</option>\n" + + "<option value=\"E\">E</option>" + + "</select>", + html + assert_dom_equal html, time_zone_select("firm", "time_zone", nil, {}, + :style => "color: red") + end + + def test_time_zone_select_with_blank_and_style + @firm = Firm.new("D") + html = time_zone_select("firm", "time_zone", nil, + { :include_blank => true }, "style" => "color: red") + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\" style=\"color: red\">" + + "<option value=\"\"></option>\n" + + "<option value=\"A\">A</option>\n" + + "<option value=\"B\">B</option>\n" + + "<option value=\"C\">C</option>\n" + + "<option value=\"D\" selected=\"selected\">D</option>\n" + + "<option value=\"E\">E</option>" + + "</select>", + html + assert_dom_equal html, time_zone_select("firm", "time_zone", nil, + { :include_blank => true }, :style => "color: red") + end + + def test_time_zone_select_with_blank_as_string_and_style + @firm = Firm.new("D") + html = time_zone_select("firm", "time_zone", nil, + { :include_blank => 'No Zone' }, "style" => "color: red") + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\" style=\"color: red\">" + + "<option value=\"\">No Zone</option>\n" + + "<option value=\"A\">A</option>\n" + + "<option value=\"B\">B</option>\n" + + "<option value=\"C\">C</option>\n" + + "<option value=\"D\" selected=\"selected\">D</option>\n" + + "<option value=\"E\">E</option>" + + "</select>", + html + assert_dom_equal html, time_zone_select("firm", "time_zone", nil, + { :include_blank => 'No Zone' }, :style => "color: red") + end + + def test_time_zone_select_with_priority_zones + @firm = Firm.new("D") + zones = [ ActiveSupport::TimeZone.new("A"), ActiveSupport::TimeZone.new("D") ] + html = time_zone_select("firm", "time_zone", zones ) + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + + "<option value=\"A\">A</option>\n" + + "<option value=\"D\" selected=\"selected\">D</option>" + + "<option value=\"\" disabled=\"disabled\">-------------</option>\n" + + "<option value=\"B\">B</option>\n" + + "<option value=\"C\">C</option>\n" + + "<option value=\"E\">E</option>" + + "</select>", + html + end + + def test_time_zone_select_with_priority_zones_as_regexp + @firm = Firm.new("D") + + @fake_timezones.each_with_index do |tz, i| + tz.stubs(:=~).returns(i.zero? || i == 3) + end + + html = time_zone_select("firm", "time_zone", /A|D/) + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + + "<option value=\"A\">A</option>\n" + + "<option value=\"D\" selected=\"selected\">D</option>" + + "<option value=\"\" disabled=\"disabled\">-------------</option>\n" + + "<option value=\"B\">B</option>\n" + + "<option value=\"C\">C</option>\n" + + "<option value=\"E\">E</option>" + + "</select>", + html + end + + def test_time_zone_select_with_priority_zones_as_regexp_using_grep_finds_no_zones + @firm = Firm.new("D") + + priority_zones = /A|D/ + @fake_timezones.each do |tz| + priority_zones.stubs(:===).with(tz).raises(Exception) + end + + html = time_zone_select("firm", "time_zone", priority_zones) + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + + "<option value=\"\" disabled=\"disabled\">-------------</option>\n" + + "<option value=\"A\">A</option>\n" + + "<option value=\"B\">B</option>\n" + + "<option value=\"C\">C</option>\n" + + "<option value=\"D\" selected=\"selected\">D</option>\n" + + "<option value=\"E\">E</option>" + + "</select>", + html + end + + def test_time_zone_select_with_default_time_zone_and_nil_value + @firm = Firm.new() + @firm.time_zone = nil + + html = time_zone_select( "firm", "time_zone", nil, :default => 'B' ) + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + + "<option value=\"A\">A</option>\n" + + "<option value=\"B\" selected=\"selected\">B</option>\n" + + "<option value=\"C\">C</option>\n" + + "<option value=\"D\">D</option>\n" + + "<option value=\"E\">E</option>" + + "</select>", + html + end + + def test_time_zone_select_with_default_time_zone_and_value + @firm = Firm.new('D') + + html = time_zone_select( "firm", "time_zone", nil, :default => 'B' ) + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + + "<option value=\"A\">A</option>\n" + + "<option value=\"B\">B</option>\n" + + "<option value=\"C\">C</option>\n" + + "<option value=\"D\" selected=\"selected\">D</option>\n" + + "<option value=\"E\">E</option>" + + "</select>", + html + end + + def test_options_for_select_with_element_attributes + assert_dom_equal( + "<option value=\"<Denmark>\" class=\"bold\"><Denmark></option>\n<option value=\"USA\" onclick=\"alert('Hello World');\">USA</option>\n<option value=\"Sweden\">Sweden</option>\n<option value=\"Germany\">Germany</option>", + options_for_select([ [ "<Denmark>", { :class => 'bold' } ], [ "USA", { :onclick => "alert('Hello World');" } ], [ "Sweden" ], "Germany" ]) + ) + end + + def test_options_for_select_with_data_element + assert_dom_equal( + "<option value=\"<Denmark>\" data-test=\"bold\"><Denmark></option>", + options_for_select([ [ "<Denmark>", { :data => { :test => 'bold' } } ] ]) + ) + end + + def test_options_for_select_with_data_element_with_special_characters + assert_dom_equal( + "<option value=\"<Denmark>\" data-test=\"<bold>\"><Denmark></option>", + options_for_select([ [ "<Denmark>", { :data => { :test => '<bold>' } } ] ]) + ) + end + + def test_options_for_select_with_element_attributes_and_selection + assert_dom_equal( + "<option value=\"<Denmark>\"><Denmark></option>\n<option value=\"USA\" class=\"bold\" selected=\"selected\">USA</option>\n<option value=\"Sweden\">Sweden</option>", + options_for_select([ "<Denmark>", [ "USA", { :class => 'bold' } ], "Sweden" ], "USA") + ) + end + + def test_options_for_select_with_element_attributes_and_selection_array + assert_dom_equal( + "<option value=\"<Denmark>\"><Denmark></option>\n<option value=\"USA\" class=\"bold\" selected=\"selected\">USA</option>\n<option value=\"Sweden\" selected=\"selected\">Sweden</option>", + options_for_select([ "<Denmark>", [ "USA", { :class => 'bold' } ], "Sweden" ], [ "USA", "Sweden" ]) + ) + end + + def test_options_for_select_with_special_characters + assert_dom_equal( + "<option value=\"<Denmark>\" onclick=\"alert("<code>")\"><Denmark></option>", + options_for_select([ [ "<Denmark>", { :onclick => %(alert("<code>")) } ] ]) + ) + end + + def test_option_html_attributes_with_no_array_element + assert_equal({}, option_html_attributes('foo')) + end + + def test_option_html_attributes_without_hash + assert_equal({}, option_html_attributes([ 'foo', 'bar' ])) + end + + def test_option_html_attributes_with_single_element_hash + assert_equal( + {:class => 'fancy'}, + option_html_attributes([ 'foo', 'bar', { :class => 'fancy' } ]) + ) + end + + def test_option_html_attributes_with_multiple_element_hash + assert_equal( + {:class => 'fancy', 'onclick' => "alert('Hello World');"}, + option_html_attributes([ 'foo', 'bar', { :class => 'fancy', 'onclick' => "alert('Hello World');" } ]) + ) + end + + def test_option_html_attributes_with_multiple_hashes + assert_equal( + {:class => 'fancy', 'onclick' => "alert('Hello World');"}, + option_html_attributes([ 'foo', 'bar', { :class => 'fancy' }, { 'onclick' => "alert('Hello World');" } ]) + ) + end + + def test_option_html_attributes_with_multiple_hashes_does_not_modify_them + options1 = { class: 'fancy' } + options2 = { onclick: "alert('Hello World');" } + option_html_attributes([ 'foo', 'bar', options1, options2 ]) + + assert_equal({ class: 'fancy' }, options1) + assert_equal({ onclick: "alert('Hello World');" }, options2) + end + + def test_grouped_collection_select + @post = Post.new + @post.origin = 'dk' + + assert_dom_equal( + %Q{<select id="post_origin" name="post[origin]"><optgroup label="<Africa>"><option value="<sa>"><South Africa></option>\n<option value="so">Somalia</option></optgroup><optgroup label="Europe"><option value="dk" selected="selected">Denmark</option>\n<option value="ie">Ireland</option></optgroup></select>}, + grouped_collection_select("post", "origin", dummy_continents, :countries, :continent_name, :country_id, :country_name) + ) + end + + def test_grouped_collection_select_with_selected + @post = Post.new + + assert_dom_equal( + %Q{<select id="post_origin" name="post[origin]"><optgroup label="<Africa>"><option value="<sa>"><South Africa></option>\n<option value="so">Somalia</option></optgroup><optgroup label="Europe"><option value="dk" selected="selected">Denmark</option>\n<option value="ie">Ireland</option></optgroup></select>}, + grouped_collection_select("post", "origin", dummy_continents, :countries, :continent_name, :country_id, :country_name, :selected => 'dk') + ) + end + + def test_grouped_collection_select_with_disabled_value + @post = Post.new + + assert_dom_equal( + %Q{<select id="post_origin" name="post[origin]"><optgroup label="<Africa>"><option value="<sa>"><South Africa></option>\n<option value="so">Somalia</option></optgroup><optgroup label="Europe"><option disabled="disabled" value="dk">Denmark</option>\n<option value="ie">Ireland</option></optgroup></select>}, + grouped_collection_select("post", "origin", dummy_continents, :countries, :continent_name, :country_id, :country_name, :disabled => 'dk') + ) + end + + def test_grouped_collection_select_under_fields_for + @post = Post.new + @post.origin = 'dk' + + output_buffer = fields_for :post, @post do |f| + concat f.grouped_collection_select("origin", dummy_continents, :countries, :continent_name, :country_id, :country_name) + end + + assert_dom_equal( + %Q{<select id="post_origin" name="post[origin]"><optgroup label="<Africa>"><option value="<sa>"><South Africa></option>\n<option value="so">Somalia</option></optgroup><optgroup label="Europe"><option value="dk" selected="selected">Denmark</option>\n<option value="ie">Ireland</option></optgroup></select>}, + output_buffer + ) + end + + private + + def dummy_posts + [ Post.new("<Abe> went home", "<Abe>", "To a little house", "shh!"), + Post.new("Babe went home", "Babe", "To a little house", "shh!"), + Post.new("Cabe went home", "Cabe", "To a little house", "shh!") ] + end + + def dummy_continents + [ Continent.new("<Africa>", [Country.new("<sa>", "<South Africa>"), Country.new("so", "Somalia")]), + Continent.new("Europe", [Country.new("dk", "Denmark"), Country.new("ie", "Ireland")]) ] + end +end diff --git a/actionview/test/template/form_tag_helper_test.rb b/actionview/test/template/form_tag_helper_test.rb new file mode 100644 index 0000000000..70fc6a588b --- /dev/null +++ b/actionview/test/template/form_tag_helper_test.rb @@ -0,0 +1,614 @@ +require 'abstract_unit' + +class FormTagHelperTest < ActionView::TestCase + include RenderERBUtils + + tests ActionView::Helpers::FormTagHelper + + def setup + super + @controller = BasicController.new + end + + def hidden_fields(options = {}) + method = options[:method] + + txt = %{<div style="margin:0;padding:0;display:inline">} + txt << %{<input name="utf8" type="hidden" value="✓" />} + if method && !%w(get post).include?(method.to_s) + txt << %{<input name="_method" type="hidden" value="#{method}" />} + end + txt << %{</div>} + end + + def form_text(action = "http://www.example.com", options = {}) + remote, enctype, html_class, id, method = options.values_at(:remote, :enctype, :html_class, :id, :method) + + method = method.to_s == "get" ? "get" : "post" + + txt = %{<form accept-charset="UTF-8" action="#{action}"} + txt << %{ enctype="multipart/form-data"} if enctype + txt << %{ data-remote="true"} if remote + txt << %{ class="#{html_class}"} if html_class + txt << %{ id="#{id}"} if id + txt << %{ method="#{method}">} + end + + def whole_form(action = "http://www.example.com", options = {}) + out = form_text(action, options) + hidden_fields(options) + + if block_given? + out << yield << "</form>" + end + + out + end + + def url_for(options) + if options.is_a?(Hash) + "http://www.example.com" + else + super + end + end + + VALID_HTML_ID = /^[A-Za-z][-_:.A-Za-z0-9]*$/ # see http://www.w3.org/TR/html4/types.html#type-name + + def test_check_box_tag + actual = check_box_tag "admin" + expected = %(<input id="admin" 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'] + end + + def test_form_tag + actual = form_tag + expected = whole_form + assert_dom_equal expected, actual + end + + def test_form_tag_multipart + actual = form_tag({}, { 'multipart' => true }) + expected = whole_form("http://www.example.com", :enctype => true) + assert_dom_equal expected, actual + end + + def test_form_tag_with_method_patch + actual = form_tag({}, { :method => :patch }) + expected = whole_form("http://www.example.com", :method => :patch) + assert_dom_equal expected, actual + end + + def test_form_tag_with_method_put + actual = form_tag({}, { :method => :put }) + expected = whole_form("http://www.example.com", :method => :put) + assert_dom_equal expected, actual + end + + def test_form_tag_with_method_delete + actual = form_tag({}, { :method => :delete }) + + expected = whole_form("http://www.example.com", :method => :delete) + assert_dom_equal expected, actual + end + + def test_form_tag_with_remote + actual = form_tag({}, :remote => true) + + expected = whole_form("http://www.example.com", :remote => true) + assert_dom_equal expected, actual + end + + def test_form_tag_with_remote_false + actual = form_tag({}, :remote => false) + + expected = whole_form + assert_dom_equal expected, actual + end + + def test_form_tag_with_block_in_erb + output_buffer = render_erb("<%= form_tag('http://www.example.com') do %>Hello world!<% end %>") + + expected = whole_form { "Hello world!" } + assert_dom_equal expected, output_buffer + end + + def test_form_tag_with_block_and_method_in_erb + output_buffer = render_erb("<%= form_tag('http://www.example.com', :method => :put) do %>Hello world!<% end %>") + + expected = whole_form("http://www.example.com", :method => "put") do + "Hello world!" + end + + assert_dom_equal expected, output_buffer + end + + def test_hidden_field_tag + actual = hidden_field_tag "id", 3 + expected = %(<input id="id" name="id" type="hidden" value="3" />) + assert_dom_equal expected, actual + end + + def test_hidden_field_tag_id_sanitized + input_elem = root_elem(hidden_field_tag("item[][title]")) + assert_match VALID_HTML_ID, input_elem['id'] + end + + def test_file_field_tag + assert_dom_equal "<input name=\"picsplz\" type=\"file\" id=\"picsplz\" />", file_field_tag("picsplz") + end + + def test_file_field_tag_with_options + assert_dom_equal "<input name=\"picsplz\" type=\"file\" id=\"picsplz\" class=\"pix\"/>", file_field_tag("picsplz", :class => "pix") + end + + def test_password_field_tag + actual = password_field_tag + expected = %(<input id="password" name="password" type="password" />) + assert_dom_equal expected, actual + end + + def test_radio_button_tag + actual = radio_button_tag "people", "david" + expected = %(<input id="people_david" name="people" type="radio" value="david" />) + assert_dom_equal expected, actual + + actual = radio_button_tag("num_people", 5) + expected = %(<input id="num_people_5" name="num_people" type="radio" value="5" />) + assert_dom_equal expected, actual + + actual = radio_button_tag("gender", "m") + radio_button_tag("gender", "f") + expected = %(<input id="gender_m" name="gender" type="radio" value="m" /><input id="gender_f" name="gender" type="radio" value="f" />) + assert_dom_equal expected, actual + + actual = radio_button_tag("opinion", "-1") + radio_button_tag("opinion", "1") + expected = %(<input id="opinion_-1" name="opinion" type="radio" value="-1" /><input id="opinion_1" name="opinion" type="radio" value="1" />) + assert_dom_equal expected, actual + + actual = radio_button_tag("person[gender]", "m") + expected = %(<input id="person_gender_m" name="person[gender]" type="radio" value="m" />) + assert_dom_equal expected, actual + + actual = radio_button_tag('ctrlname', 'apache2.2') + expected = %(<input id="ctrlname_apache2.2" name="ctrlname" type="radio" value="apache2.2" />) + assert_dom_equal expected, actual + end + + def test_select_tag + actual = select_tag "people", "<option>david</option>".html_safe + expected = %(<select id="people" name="people"><option>david</option></select>) + assert_dom_equal expected, actual + 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>) + 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 + expected = %(<select id="places" disabled="disabled" name="places"><option>Home</option><option>Work</option><option>Pub</option></select>) + assert_dom_equal expected, actual + end + + def test_select_tag_id_sanitized + input_elem = root_elem(select_tag("project[1]people", "<option>david</option>")) + assert_match VALID_HTML_ID, input_elem['id'] + end + + def test_select_tag_with_include_blank + actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, :include_blank => true + expected = %(<select id="places" name="places"><option value=""></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>) + assert_dom_equal expected, actual + end + + def test_select_tag_escapes_prompt + actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, :prompt => "<script>alert(1337)</script>" + expected = %(<select id="places" name="places"><option value=""><script>alert(1337)</script></option><option>Home</option><option>Work</option><option>Pub</option></select>) + assert_dom_equal expected, actual + end + + def test_select_tag_with_prompt_and_include_blank + actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, :prompt => "string", :include_blank => true + expected = %(<select name="places" id="places"><option value="">string</option><option value=""></option><option>Home</option><option>Work</option><option>Pub</option></select>) + assert_dom_equal expected, actual + end + + def test_select_tag_with_nil_option_tags_and_include_blank + actual = select_tag "places", nil, :include_blank => true + expected = %(<select id="places" name="places"><option value=""></option></select>) + assert_dom_equal expected, actual + end + + def test_select_tag_with_nil_option_tags_and_prompt + actual = select_tag "places", nil, :prompt => "string" + expected = %(<select id="places" name="places"><option value="">string</option></select>) + assert_dom_equal expected, actual + end + + def test_text_area_tag_size_string + actual = text_area_tag "body", "hello world", "size" => "20x40" + expected = %(<textarea cols="20" id="body" name="body" rows="40">\nhello world</textarea>) + assert_dom_equal expected, actual + end + + def test_text_area_tag_size_symbol + actual = text_area_tag "body", "hello world", :size => "20x40" + expected = %(<textarea cols="20" id="body" name="body" rows="40">\nhello world</textarea>) + assert_dom_equal expected, actual + end + + def test_text_area_tag_should_disregard_size_if_its_given_as_an_integer + actual = text_area_tag "body", "hello world", :size => 20 + expected = %(<textarea id="body" name="body">\nhello world</textarea>) + assert_dom_equal expected, actual + end + + def test_text_area_tag_id_sanitized + input_elem = root_elem(text_area_tag("item[][description]")) + assert_match VALID_HTML_ID, input_elem['id'] + end + + def test_text_area_tag_escape_content + actual = text_area_tag "body", "<b>hello world</b>", :size => "20x40" + expected = %(<textarea cols="20" id="body" name="body" rows="40">\n<b>hello world</b></textarea>) + assert_dom_equal expected, actual + end + + def test_text_area_tag_unescaped_content + actual = text_area_tag "body", "<b>hello world</b>", :size => "20x40", :escape => false + expected = %(<textarea cols="20" id="body" name="body" rows="40">\n<b>hello world</b></textarea>) + assert_dom_equal expected, actual + end + + def test_text_area_tag_unescaped_nil_content + actual = text_area_tag "body", nil, :escape => false + expected = %(<textarea id="body" name="body">\n</textarea>) + assert_dom_equal expected, actual + end + + def test_text_field_tag + actual = text_field_tag "title", "Hello!" + expected = %(<input id="title" name="title" type="text" value="Hello!" />) + assert_dom_equal expected, actual + end + + def test_text_field_tag_class_string + actual = text_field_tag "title", "Hello!", "class" => "admin" + expected = %(<input class="admin" id="title" name="title" type="text" value="Hello!" />) + assert_dom_equal expected, actual + end + + def test_text_field_tag_size_symbol + actual = text_field_tag "title", "Hello!", :size => 75 + expected = %(<input id="title" name="title" size="75" type="text" value="Hello!" />) + assert_dom_equal expected, actual + end + + def test_text_field_tag_size_string + actual = text_field_tag "title", "Hello!", "size" => "75" + expected = %(<input id="title" name="title" size="75" type="text" value="Hello!" />) + assert_dom_equal expected, actual + end + + def test_text_field_tag_maxlength_symbol + actual = text_field_tag "title", "Hello!", :maxlength => 75 + expected = %(<input id="title" name="title" maxlength="75" type="text" value="Hello!" />) + assert_dom_equal expected, actual + end + + def test_text_field_tag_maxlength_string + actual = text_field_tag "title", "Hello!", "maxlength" => "75" + expected = %(<input id="title" name="title" maxlength="75" type="text" value="Hello!" />) + assert_dom_equal expected, actual + end + + def test_text_field_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_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!" />) + assert_dom_equal expected, actual + end + + def test_text_field_tag_id_sanitized + input_elem = root_elem(text_field_tag("item[][title]")) + assert_match VALID_HTML_ID, input_elem['id'] + end + + def test_label_tag_without_text + actual = label_tag "title" + expected = %(<label for="title">Title</label>) + assert_dom_equal expected, actual + end + + def test_label_tag_with_symbol + actual = label_tag :title + expected = %(<label for="title">Title</label>) + assert_dom_equal expected, actual + end + + def test_label_tag_with_text + actual = label_tag "title", "My Title" + expected = %(<label for="title">My Title</label>) + assert_dom_equal expected, actual + end + + def test_label_tag_class_string + actual = label_tag "title", "My Title", "class" => "small_label" + expected = %(<label for="title" class="small_label">My Title</label>) + assert_dom_equal expected, actual + end + + def test_label_tag_id_sanitized + label_elem = root_elem(label_tag("item[title]")) + assert_match VALID_HTML_ID, label_elem['for'] + end + + def test_label_tag_with_block + assert_dom_equal('<label>Blocked</label>', label_tag { "Blocked" }) + end + + def test_label_tag_with_block_and_argument + output = label_tag("clock") { "Grandfather" } + assert_dom_equal('<label for="clock">Grandfather</label>', output) + end + + def test_label_tag_with_block_and_argument_and_options + output = label_tag("clock", :id => "label_clock") { "Grandfather" } + assert_dom_equal('<label for="clock" id="label_clock">Grandfather</label>', output) + end + + def test_boolean_options + assert_dom_equal %(<input checked="checked" disabled="disabled" id="admin" name="admin" readonly="readonly" type="checkbox" value="1" />), check_box_tag("admin", 1, true, 'disabled' => true, :readonly => "yes") + assert_dom_equal %(<input checked="checked" id="admin" name="admin" type="checkbox" value="1" />), check_box_tag("admin", 1, true, :disabled => false, :readonly => nil) + assert_dom_equal %(<input type="checkbox" />), tag(:input, :type => "checkbox", :checked => false) + assert_dom_equal %(<select id="people" multiple="multiple" name="people[]"><option>david</option></select>), select_tag("people", "<option>david</option>".html_safe, :multiple => true) + assert_dom_equal %(<select id="people_" multiple="multiple" name="people[]"><option>david</option></select>), select_tag("people[]", "<option>david</option>".html_safe, :multiple => true) + assert_dom_equal %(<select id="people" name="people"><option>david</option></select>), select_tag("people", "<option>david</option>".html_safe, :multiple => nil) + end + + def test_stringify_symbol_keys + actual = text_field_tag "title", "Hello!", :id => "admin" + expected = %(<input id="admin" name="title" type="text" value="Hello!" />) + assert_dom_equal expected, actual + end + + def test_submit_tag + assert_dom_equal( + %(<input name='commit' data-disable-with="Saving..." onclick="alert('hello!')" type="submit" value="Save" />), + submit_tag("Save", :onclick => "alert('hello!')", :data => { :disable_with => "Saving..." }) + ) + end + + def test_submit_tag_with_no_onclick_options + assert_dom_equal( + %(<input name='commit' data-disable-with="Saving..." type="submit" value="Save" />), + submit_tag("Save", :data => { :disable_with => "Saving..." }) + ) + end + + def test_submit_tag_with_confirmation + assert_dom_equal( + %(<input name='commit' type='submit' value='Save' data-confirm="Are you sure?" />), + submit_tag("Save", :data => { :confirm => "Are you sure?" }) + ) + end + + def test_button_tag + assert_dom_equal( + %(<button name="button" type="submit">Button</button>), + button_tag + ) + end + + def test_button_tag_with_submit_type + assert_dom_equal( + %(<button name="button" type="submit">Save</button>), + button_tag("Save", :type => "submit") + ) + end + + def test_button_tag_with_button_type + assert_dom_equal( + %(<button name="button" type="button">Button</button>), + button_tag("Button", :type => "button") + ) + end + + def test_button_tag_with_reset_type + assert_dom_equal( + %(<button name="button" type="reset">Reset</button>), + button_tag("Reset", :type => "reset") + ) + end + + def test_button_tag_with_disabled_option + assert_dom_equal( + %(<button name="button" type="reset" disabled="disabled">Reset</button>), + button_tag("Reset", :type => "reset", :disabled => true) + ) + end + + def test_button_tag_escape_content + assert_dom_equal( + %(<button name="button" type="reset" disabled="disabled"><b>Reset</b></button>), + button_tag("<b>Reset</b>", :type => "reset", :disabled => true) + ) + end + + def test_button_tag_with_block + assert_dom_equal('<button name="button" type="submit">Content</button>', button_tag { 'Content' }) + end + + def test_button_tag_with_block_and_options + output = button_tag(:name => 'temptation', :type => 'button') { content_tag(:strong, 'Do not press me') } + assert_dom_equal('<button name="temptation" type="button"><strong>Do not press me</strong></button>', output) + end + + def test_button_tag_with_confirmation + assert_dom_equal( + %(<button name="button" type="submit" data-confirm="Are you sure?">Save</button>), + button_tag("Save", :type => "submit", :data => { :confirm => "Are you sure?" }) + ) + 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?" />), + image_submit_tag("save.gif", :data => { :confirm => "Are you sure?" }) + ) + end + + def test_color_field_tag + expected = %{<input id="car" name="car" type="color" />} + assert_dom_equal(expected, color_field_tag("car")) + end + + def test_search_field_tag + expected = %{<input id="query" name="query" type="search" />} + assert_dom_equal(expected, search_field_tag("query")) + end + + def test_telephone_field_tag + expected = %{<input id="cell" name="cell" type="tel" />} + assert_dom_equal(expected, telephone_field_tag("cell")) + end + + def test_date_field_tag + expected = %{<input id="cell" name="cell" type="date" />} + assert_dom_equal(expected, date_field_tag("cell")) + end + + def test_time_field_tag + expected = %{<input id="cell" name="cell" type="time" />} + assert_dom_equal(expected, time_field_tag("cell")) + end + + def test_datetime_field_tag + expected = %{<input id="appointment" name="appointment" type="datetime" />} + assert_dom_equal(expected, datetime_field_tag("appointment")) + end + + def test_datetime_local_field_tag + expected = %{<input id="appointment" name="appointment" type="datetime-local" />} + assert_dom_equal(expected, datetime_local_field_tag("appointment")) + end + + def test_month_field_tag + expected = %{<input id="birthday" name="birthday" type="month" />} + assert_dom_equal(expected, month_field_tag("birthday")) + end + + def test_week_field_tag + expected = %{<input id="birthday" name="birthday" type="week" />} + assert_dom_equal(expected, week_field_tag("birthday")) + end + + def test_url_field_tag + expected = %{<input id="homepage" name="homepage" type="url" />} + assert_dom_equal(expected, url_field_tag("homepage")) + end + + def test_email_field_tag + expected = %{<input id="address" name="address" type="email" />} + assert_dom_equal(expected, email_field_tag("address")) + end + + def test_number_field_tag + expected = %{<input name="quantity" max="9" id="quantity" type="number" min="1" />} + assert_dom_equal(expected, number_field_tag("quantity", nil, :in => 1...10)) + end + + def test_range_input_tag + expected = %{<input name="volume" step="0.1" max="11" id="volume" type="range" min="0" />} + assert_dom_equal(expected, range_field_tag("volume", nil, :in => 0..11, :step => 0.1)) + end + + def test_field_set_tag_in_erb + output_buffer = render_erb("<%= field_set_tag('Your details') do %>Hello world!<% end %>") + + expected = %(<fieldset><legend>Your details</legend>Hello world!</fieldset>) + assert_dom_equal expected, output_buffer + + output_buffer = render_erb("<%= field_set_tag do %>Hello world!<% end %>") + + expected = %(<fieldset>Hello world!</fieldset>) + assert_dom_equal expected, output_buffer + + output_buffer = render_erb("<%= field_set_tag('') do %>Hello world!<% end %>") + + expected = %(<fieldset>Hello world!</fieldset>) + assert_dom_equal expected, output_buffer + + output_buffer = render_erb("<%= field_set_tag('', :class => 'format') do %>Hello world!<% end %>") + + expected = %(<fieldset class="format">Hello world!</fieldset>) + assert_dom_equal expected, output_buffer + + output_buffer = render_erb("<%= field_set_tag %>") + + expected = %(<fieldset></fieldset>) + assert_dom_equal expected, output_buffer + + output_buffer = render_erb("<%= field_set_tag('You legend!') %>") + + expected = %(<fieldset><legend>You legend!</legend></fieldset>) + assert_dom_equal expected, output_buffer + end + + def test_text_area_tag_options_symbolize_keys_side_effects + options = { :option => "random_option" } + text_area_tag "body", "hello world", options + assert_equal options, { :option => "random_option" } + end + + def test_submit_tag_options_symbolize_keys_side_effects + options = { :option => "random_option" } + submit_tag "submit value", options + assert_equal options, { :option => "random_option" } + end + + def test_button_tag_options_symbolize_keys_side_effects + options = { :option => "random_option" } + button_tag "button value", options + assert_equal options, { :option => "random_option" } + end + + def test_image_submit_tag_options_symbolize_keys_side_effects + options = { :option => "random_option" } + image_submit_tag "submit source", options + assert_equal options, { :option => "random_option" } + end + + def test_image_label_tag_options_symbolize_keys_side_effects + options = { :option => "random_option" } + label_tag "submit source", "title", options + assert_equal options, { :option => "random_option" } + end + + def protect_against_forgery? + false + end + + private + + def root_elem(rendered_content) + HTML::Document.new(rendered_content).root.children[0] + end +end diff --git a/actionview/test/template/html-scanner/cdata_node_test.rb b/actionview/test/template/html-scanner/cdata_node_test.rb new file mode 100644 index 0000000000..9b58174641 --- /dev/null +++ b/actionview/test/template/html-scanner/cdata_node_test.rb @@ -0,0 +1,15 @@ +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 new file mode 100644 index 0000000000..17f045d549 --- /dev/null +++ b/actionview/test/template/html-scanner/document_test.rb @@ -0,0 +1,148 @@ +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 <br> 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 new file mode 100644 index 0000000000..5b5d092036 --- /dev/null +++ b/actionview/test/template/html-scanner/node_test.rb @@ -0,0 +1,89 @@ +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 new file mode 100644 index 0000000000..b1c1b83807 --- /dev/null +++ b/actionview/test/template/html-scanner/sanitizer_test.rb @@ -0,0 +1,330 @@ +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="%6A%61%76%61%73%63%72%69%70%74%3A%61%6C%65%72%74%28%22%58%53%53%22%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("XSS")>), + %(<IMG SRC=javascript:alert(String.fromCharCode(88,83,83))>), + %(<IMG SRC=javascript:alert('XSS')>), + %(<IMG SRC=javascript:alert('XSS')>), + %(<IMG SRC=javascript:alert('XSS')>), + %(<IMG SRC="jav\tascript:alert('XSS');">), + %(<IMG SRC="jav	ascript:alert('XSS');">), + %(<IMG SRC="jav
ascript:alert('XSS');">), + %(<IMG SRC="jav
ascript:alert('XSS');">), + %(<IMG SRC="  javascript:alert('XSS');">), + %(<IMG SRC="javascript: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>), "<" + assert_sanitized %(<iframe src=http://ha.ckers.org/scriptlet.html\n<a), %(<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>]]>", "<![CDATA[<span>section</span>]]>" + end + + def test_should_sanitize_unterminated_cdata_section + assert_sanitized "<![CDATA[<span>neverending...", "<![CDATA[<span>neverending...]]>" + end + + def test_should_not_mangle_urls_with_ampersand + assert_sanitized %{<a href=\"http://www.domain.com?var1=1&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:alert('XSS');">), "<a>" + assert_sanitized %(<a href="javascript:alert('XSS');">), "<a>" + assert_sanitized %(<a href="http://legit">), %(<a href="http://legit">) + assert_sanitized %(<a href="javascript:alert('XSS');">), "<a>" + assert_sanitized %(<a href="javascript:alert('XSS');">), "<a>" + assert_sanitized %(<a href="http://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 new file mode 100644 index 0000000000..a29d2d43d7 --- /dev/null +++ b/actionview/test/template/html-scanner/tag_node_test.rb @@ -0,0 +1,243 @@ +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 new file mode 100644 index 0000000000..cbcb9e78f0 --- /dev/null +++ b/actionview/test/template/html-scanner/text_node_test.rb @@ -0,0 +1,50 @@ +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 new file mode 100644 index 0000000000..1d59de23b6 --- /dev/null +++ b/actionview/test/template/html-scanner/tokenizer_test.rb @@ -0,0 +1,131 @@ +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 new file mode 100644 index 0000000000..de6a6eaab3 --- /dev/null +++ b/actionview/test/template/javascript_helper_test.rb @@ -0,0 +1,62 @@ +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 + + def setup + super + ActiveSupport.escape_html_entities_in_json = true + @template = self + end + + def teardown + ActiveSupport.escape_html_entities_in_json = false + end + + def test_escape_javascript + assert_equal '', escape_javascript(nil) + assert_equal %(This \\"thing\\" is really\\n netos\\'), escape_javascript(%(This "thing" is really\n netos')) + assert_equal %(backslash\\\\test), escape_javascript( %(backslash\\test) ) + assert_equal %(dont <\\/close> tags), escape_javascript(%(dont </close> tags)) + assert_equal %(unicode 
 newline), escape_javascript(%(unicode \342\200\250 newline).force_encoding(Encoding::UTF_8).encode!) + assert_equal %(unicode 
 newline), escape_javascript(%(unicode \342\200\251 newline).force_encoding(Encoding::UTF_8).encode!) + + assert_equal %(dont <\\/close> tags), j(%(dont </close> tags)) + end + + def test_escape_javascript_with_safebuffer + given = %('quoted' "double-quoted" new-line:\n </closed>) + expect = %(\\'quoted\\' \\"double-quoted\\" new-line:\\n <\\/closed>) + assert_equal expect, escape_javascript(given) + assert_equal expect, escape_javascript(ActiveSupport::SafeBuffer.new(given)) + assert_instance_of String, escape_javascript(given) + assert_instance_of ActiveSupport::SafeBuffer, escape_javascript(ActiveSupport::SafeBuffer.new(given)) + end + + def test_javascript_tag + self.output_buffer = 'foo' + + assert_dom_equal "<script>\n//<![CDATA[\nalert('hello')\n//]]>\n</script>", + javascript_tag("alert('hello')") + + assert_equal 'foo', output_buffer, 'javascript_tag without a block should not concat to output_buffer' + end + + def test_javascript_tag_with_options + assert_dom_equal "<script id=\"the_js_tag\">\n//<![CDATA[\nalert('hello')\n//]]>\n</script>", + javascript_tag("alert('hello')", :id => "the_js_tag") + end + + def test_javascript_cdata_section + assert_dom_equal "\n//<![CDATA[\nalert('hello')\n//]]>\n", javascript_cdata_section("alert('hello')") + end +end diff --git a/actionview/test/template/log_subscriber_test.rb b/actionview/test/template/log_subscriber_test.rb new file mode 100644 index 0000000000..7f4c84929f --- /dev/null +++ b/actionview/test/template/log_subscriber_test.rb @@ -0,0 +1,91 @@ +require "abstract_unit" +require "active_support/log_subscriber/test_helper" +require "action_view/log_subscriber" +require "controller/fake_models" + +class AVLogSubscriberTest < ActiveSupport::TestCase + include ActiveSupport::LogSubscriber::TestHelper + + def setup + super + view_paths = ActionController::Base.view_paths + lookup_context = ActionView::LookupContext.new(view_paths, {}, ["test"]) + renderer = ActionView::Renderer.new(lookup_context) + @view = ActionView::Base.new(renderer, {}) + Rails.stubs(:root).returns(File.expand_path(FIXTURE_LOAD_PATH)) + ActionView::LogSubscriber.attach_to :action_view + end + + def teardown + super + ActiveSupport::LogSubscriber.log_subscribers.clear + end + + def set_logger(logger) + ActionView::Base.logger = logger + end + + def test_render_file_template + @view.render(:file => "test/hello_world") + wait + + assert_equal 1, @logger.logged(:info).size + assert_match(/Rendered test\/hello_world\.erb/, @logger.logged(:info).last) + end + + def test_render_text_template + @view.render(:text => "TEXT") + wait + + assert_equal 1, @logger.logged(:info).size + assert_match(/Rendered text template/, @logger.logged(:info).last) + end + + def test_render_inline_template + @view.render(:inline => "<%= 'TEXT' %>") + wait + + assert_equal 1, @logger.logged(:info).size + assert_match(/Rendered inline template/, @logger.logged(:info).last) + end + + def test_render_partial_template + @view.render(:partial => "test/customer") + wait + + assert_equal 1, @logger.logged(:info).size + assert_match(/Rendered test\/_customer.erb/, @logger.logged(:info).last) + end + + def test_render_partial_with_implicit_path + @view.render(Customer.new("david"), :greeting => "hi") + wait + + assert_equal 1, @logger.logged(:info).size + assert_match(/Rendered customers\/_customer\.html\.erb/, @logger.logged(:info).last) + end + + def test_render_collection_template + @view.render(:partial => "test/customer", :collection => [ Customer.new("david"), Customer.new("mary") ]) + wait + + assert_equal 1, @logger.logged(:info).size + assert_match(/Rendered test\/_customer.erb/, @logger.logged(:info).last) + end + + def test_render_collection_with_implicit_path + @view.render([ Customer.new("david"), Customer.new("mary") ], :greeting => "hi") + wait + + assert_equal 1, @logger.logged(:info).size + assert_match(/Rendered customers\/_customer\.html\.erb/, @logger.logged(:info).last) + end + + def test_render_collection_template_without_path + @view.render([ GoodCustomer.new("david"), Customer.new("mary") ], :greeting => "hi") + wait + + assert_equal 1, @logger.logged(:info).size + assert_match(/Rendered collection/, @logger.logged(:info).last) + end +end diff --git a/actionview/test/template/lookup_context_test.rb b/actionview/test/template/lookup_context_test.rb new file mode 100644 index 0000000000..073bd14783 --- /dev/null +++ b/actionview/test/template/lookup_context_test.rb @@ -0,0 +1,263 @@ +require "abstract_unit" +require "abstract_controller/rendering" + +class LookupContextTest < ActiveSupport::TestCase + def setup + @lookup_context = ActionView::LookupContext.new(FIXTURE_LOAD_PATH, {}) + ActionView::LookupContext::DetailsKey.clear + end + + def teardown + I18n.locale = :en + end + + test "allows to override default_formats with ActionView::Base.default_formats" do + begin + formats = ActionView::Base.default_formats + ActionView::Base.default_formats = [:foo, :bar] + + assert_equal [:foo, :bar], ActionView::LookupContext.new([]).default_formats + ensure + ActionView::Base.default_formats = formats + end + end + + test "process view paths on initialization" do + assert_kind_of ActionView::PathSet, @lookup_context.view_paths + end + + test "normalizes details on initialization" do + assert_equal Mime::SET, @lookup_context.formats + assert_equal :en, @lookup_context.locale + end + + test "allows me to freeze and retrieve frozen formats" do + @lookup_context.formats.freeze + assert @lookup_context.formats.frozen? + end + + test "provides getters and setters for formats" do + @lookup_context.formats = [:html] + assert_equal [:html], @lookup_context.formats + end + + test "handles */* formats" do + @lookup_context.formats = ["*/*"] + assert_equal Mime::SET, @lookup_context.formats + end + + test "handles explicitly defined */* formats fallback to :js" do + @lookup_context.formats = [:js, Mime::ALL] + assert_equal [:js, *Mime::SET.symbols], @lookup_context.formats + end + + test "adds :html fallback to :js formats" do + @lookup_context.formats = [:js] + assert_equal [:js, :html], @lookup_context.formats + end + + test "provides getters and setters for locale" do + @lookup_context.locale = :pt + assert_equal :pt, @lookup_context.locale + end + + test "changing lookup_context locale, changes I18n.locale" do + @lookup_context.locale = :pt + assert_equal :pt, I18n.locale + end + + test "delegates changing the locale to the I18n configuration object if it contains a lookup_context object" do + begin + I18n.config = AbstractController::I18nProxy.new(I18n.config, @lookup_context) + @lookup_context.locale = :pt + assert_equal :pt, I18n.locale + assert_equal :pt, @lookup_context.locale + ensure + I18n.config = I18n.config.original_config + end + + assert_equal :pt, I18n.locale + end + + test "find templates using the given view paths and configured details" do + template = @lookup_context.find("hello_world", %w(test)) + assert_equal "Hello world!", template.source + + @lookup_context.locale = :da + template = @lookup_context.find("hello_world", %w(test)) + assert_equal "Hey verden", template.source + 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 + end + + test "adds fallbacks to view paths when required" do + assert_equal 1, @lookup_context.view_paths.size + + @lookup_context.with_fallbacks do + assert_equal 3, @lookup_context.view_paths.size + assert @lookup_context.view_paths.include?(ActionView::FallbackFileSystemResolver.new("")) + assert @lookup_context.view_paths.include?(ActionView::FallbackFileSystemResolver.new("/")) + end + end + + test "add fallbacks just once in nested fallbacks calls" do + @lookup_context.with_fallbacks do + @lookup_context.with_fallbacks do + assert_equal 3, @lookup_context.view_paths.size + end + end + end + + test "generates a new details key for each details hash" do + keys = [] + keys << @lookup_context.details_key + assert_equal 1, keys.uniq.size + + @lookup_context.locale = :da + keys << @lookup_context.details_key + assert_equal 2, keys.uniq.size + + @lookup_context.locale = :en + keys << @lookup_context.details_key + assert_equal 2, keys.uniq.size + + @lookup_context.formats = [:html] + keys << @lookup_context.details_key + assert_equal 3, keys.uniq.size + + @lookup_context.formats = nil + keys << @lookup_context.details_key + assert_equal 3, keys.uniq.size + end + + test "gives the key forward to the resolver, so it can be used as cache key" do + @lookup_context.view_paths = ActionView::FixtureResolver.new("test/_foo.erb" => "Foo") + 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 we will hit the cache. + @lookup_context.view_paths.first.hash["test/_foo.erb"] = "Bar" + template = @lookup_context.find("foo", %w(test), true) + assert_equal "Foo", template.source + + # This time we will change the locale. The updated template should be picked since + # lookup_context generated a new key after we changed the locale. + @lookup_context.locale = :da + template = @lookup_context.find("foo", %w(test), true) + assert_equal "Bar", template.source + + # Now we will change back the locale and it will still pick the old template. + # This is expected because lookup_context will reuse the previous key for :en locale. + @lookup_context.locale = :en + template = @lookup_context.find("foo", %w(test), true) + assert_equal "Foo", template.source + + # Finally, we can expire the cache. And the expected template will be used. + @lookup_context.view_paths.first.clear_cache + template = @lookup_context.find("foo", %w(test), true) + assert_equal "Bar", template.source + end + + test "can disable the cache on demand" do + @lookup_context.view_paths = ActionView::FixtureResolver.new("test/_foo.erb" => "Foo") + old_template = @lookup_context.find("foo", %w(test), true) + + template = @lookup_context.find("foo", %w(test), true) + assert_equal template, old_template + + assert @lookup_context.cache + template = @lookup_context.disable_cache do + assert !@lookup_context.cache + @lookup_context.find("foo", %w(test), true) + end + assert @lookup_context.cache + + assert_not_equal template, old_template + end + + test "responds to #prefixes" do + assert_equal [], @lookup_context.prefixes + @lookup_context.prefixes = ["foo"] + assert_equal ["foo"], @lookup_context.prefixes + end +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 + 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 + + @resolver.hash.clear + assert_raise ActionView::MissingTemplate do + @lookup_context.find("foo", %w(test), true) + 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 + + @resolver.hash["test/_foo.erb"] = ["Foo", Time.utc(2000)] + template = @lookup_context.find("foo", %w(test), true) + assert_equal "Foo", template.source + end +end + +class TestMissingTemplate < ActiveSupport::TestCase + def setup + @lookup_context = ActionView::LookupContext.new("/Path/to/views", {}) + end + + test "if no template was found we get a helpful error message including the inheritance chain" do + e = assert_raise ActionView::MissingTemplate do + @lookup_context.find("foo", %w(parent child)) + end + assert_match %r{Missing template parent/foo, child/foo with .* Searched in:\n \* "/Path/to/views"\n}, e.message + end + + test "if no partial was found we get a helpful error message including the inheritance chain" do + e = assert_raise ActionView::MissingTemplate do + @lookup_context.find("foo", %w(parent child), true) + end + assert_match %r{Missing partial parent/foo, child/foo with .* Searched in:\n \* "/Path/to/views"\n}, e.message + end + + test "if a single prefix is passed as a string and the lookup fails, MissingTemplate accepts it" do + e = assert_raise ActionView::MissingTemplate do + details = {:handlers=>[], :formats=>[], :locale=>[]} + @lookup_context.view_paths.find("foo", "parent", true, details) + end + assert_match %r{Missing partial parent/foo with .* Searched in:\n \* "/Path/to/views"\n}, e.message + end + +end diff --git a/actionview/test/template/number_helper_test.rb b/actionview/test/template/number_helper_test.rb new file mode 100644 index 0000000000..6e640889d2 --- /dev/null +++ b/actionview/test/template/number_helper_test.rb @@ -0,0 +1,151 @@ +require "abstract_unit" + +class NumberHelperTest < ActionView::TestCase + tests ActionView::Helpers::NumberHelper + + def test_number_to_phone + assert_equal nil, number_to_phone(nil) + assert_equal "555-1234", number_to_phone(5551234) + assert_equal "(800) 555-1212 x 123", number_to_phone(8005551212, area_code: true, extension: 123) + assert_equal "+18005551212", number_to_phone(8005551212, country_code: 1, delimiter: "") + end + + def test_number_to_currency + assert_equal nil, number_to_currency(nil) + assert_equal "$1,234,567,890.50", number_to_currency(1234567890.50) + assert_equal "$1,234,567,892", number_to_currency(1234567891.50, precision: 0) + assert_equal "1,234,567,890.50 - Kč", number_to_currency("-1234567890.50", unit: "Kč", format: "%n %u", negative_format: "%n - %u") + end + + def test_number_to_percentage + assert_equal nil, number_to_percentage(nil) + assert_equal "100.000%", number_to_percentage(100) + assert_equal "100%", number_to_percentage(100, precision: 0) + assert_equal "123.4%", number_to_percentage(123.400, precision: 3, strip_insignificant_zeros: true) + assert_equal "1.000,000%", number_to_percentage(1000, delimiter: ".", separator: ",") + end + + def test_number_with_delimiter + assert_equal nil, number_with_delimiter(nil) + assert_equal "12,345,678", number_with_delimiter(12345678) + assert_equal "0", number_with_delimiter(0) + end + + def test_number_with_precision + assert_equal nil, number_with_precision(nil) + assert_equal "-111.235", number_with_precision(-111.2346) + assert_equal "111.00", number_with_precision(111, precision: 2) + assert_equal "0.00100", number_with_precision(0.001, precision: 5) + end + + def test_number_to_human_size + assert_equal nil, number_to_human_size(nil) + assert_equal "3 Bytes", number_to_human_size(3.14159265) + assert_equal "1.2 MB", number_to_human_size(1234567, precision: 2) + end + + def test_number_to_human + assert_equal nil, number_to_human(nil) + assert_equal "0", number_to_human(0) + assert_equal "1.23 Thousand", number_to_human(1234) + assert_equal "489.0 Thousand", number_to_human(489000, precision: 4, strip_insignificant_zeros: false) + end + + def test_number_helpers_escape_delimiter_and_separator + assert_equal "111<script></script>111<script></script>1111", number_to_phone(1111111111, delimiter: "<script></script>") + + assert_equal "$1<script></script>01", number_to_currency(1.01, separator: "<script></script>") + assert_equal "$1<script></script>000.00", number_to_currency(1000, delimiter: "<script></script>") + + assert_equal "1<script></script>010%", number_to_percentage(1.01, separator: "<script></script>") + assert_equal "1<script></script>000.000%", number_to_percentage(1000, delimiter: "<script></script>") + + assert_equal "1<script></script>01", number_with_delimiter(1.01, separator: "<script></script>") + assert_equal "1<script></script>000", number_with_delimiter(1000, delimiter: "<script></script>") + + assert_equal "1<script></script>010", number_with_precision(1.01, separator: "<script></script>") + assert_equal "1<script></script>000.000", number_with_precision(1000, delimiter: "<script></script>") + + assert_equal "9<script></script>86 KB", number_to_human_size(10100, separator: "<script></script>") + + assert_equal "1<script></script>01", number_to_human(1.01, separator: "<script></script>") + assert_equal "100<script></script>000 Quadrillion", number_to_human(10**20, delimiter: "<script></script>") + end + + def test_number_helpers_outputs_are_html_safe + assert number_to_human(1).html_safe? + assert !number_to_human("<script></script>").html_safe? + assert number_to_human("asdf".html_safe).html_safe? + assert number_to_human("1".html_safe).html_safe? + + assert number_to_human_size(1).html_safe? + assert number_to_human_size(1000000).html_safe? + assert !number_to_human_size("<script></script>").html_safe? + assert number_to_human_size("asdf".html_safe).html_safe? + assert number_to_human_size("1".html_safe).html_safe? + + assert number_with_precision(1, strip_insignificant_zeros: false).html_safe? + assert number_with_precision(1, strip_insignificant_zeros: true).html_safe? + assert !number_with_precision("<script></script>").html_safe? + assert number_with_precision("asdf".html_safe).html_safe? + assert number_with_precision("1".html_safe).html_safe? + + assert number_to_currency(1).html_safe? + assert !number_to_currency("<script></script>").html_safe? + assert number_to_currency("asdf".html_safe).html_safe? + assert number_to_currency("1".html_safe).html_safe? + + assert number_to_percentage(1).html_safe? + assert !number_to_percentage("<script></script>").html_safe? + assert number_to_percentage("asdf".html_safe).html_safe? + assert number_to_percentage("1".html_safe).html_safe? + + assert number_to_phone(1).html_safe? + assert_equal "<script></script>", number_to_phone("<script></script>") + assert number_to_phone("<script></script>").html_safe? + assert number_to_phone("asdf".html_safe).html_safe? + assert number_to_phone("1".html_safe).html_safe? + + assert number_with_delimiter(1).html_safe? + assert !number_with_delimiter("<script></script>").html_safe? + assert number_with_delimiter("asdf".html_safe).html_safe? + assert number_with_delimiter("1".html_safe).html_safe? + end + + def test_number_helpers_should_raise_error_if_invalid_when_specified + exception = assert_raise InvalidNumberError do + number_to_human("x", raise: true) + end + assert_equal "x", exception.number + + exception = assert_raise InvalidNumberError do + number_to_human_size("x", raise: true) + end + assert_equal "x", exception.number + + exception = assert_raise InvalidNumberError do + number_with_precision("x", raise: true) + end + assert_equal "x", exception.number + + exception = assert_raise InvalidNumberError do + number_to_currency("x", raise: true) + end + assert_equal "x", exception.number + + exception = assert_raise InvalidNumberError do + number_to_percentage("x", raise: true) + end + assert_equal "x", exception.number + + exception = assert_raise InvalidNumberError do + number_with_delimiter("x", raise: true) + end + assert_equal "x", exception.number + + exception = assert_raise InvalidNumberError do + number_to_phone("x", raise: true) + end + assert_equal "x", exception.number + end +end diff --git a/actionview/test/template/output_buffer_test.rb b/actionview/test/template/output_buffer_test.rb new file mode 100644 index 0000000000..eb0df3d1ab --- /dev/null +++ b/actionview/test/template/output_buffer_test.rb @@ -0,0 +1,59 @@ +require 'abstract_unit' + +class OutputBufferTest < ActionController::TestCase + class TestController < ActionController::Base + def index + render :text => 'foo' + end + end + + tests TestController + + def setup + @vc = @controller.view_context + get :index + assert_equal ['foo'], body_parts + end + + test 'output buffer is nil after rendering' do + assert_nil output_buffer + end + + test 'flushing ignores nil output buffer' do + @controller.view_context.flush_output_buffer + assert_nil output_buffer + assert_equal ['foo'], body_parts + end + + test 'flushing ignores empty output buffer' do + @vc.output_buffer = '' + @vc.flush_output_buffer + assert_equal '', output_buffer + assert_equal ['foo'], body_parts + end + + test 'flushing appends the output buffer to the body parts' do + @vc.output_buffer = 'bar' + @vc.flush_output_buffer + assert_equal '', output_buffer + assert_equal ['foo', 'bar'], body_parts + end + + test 'flushing preserves output buffer encoding' do + original_buffer = ' '.force_encoding(Encoding::EUC_JP) + @vc.output_buffer = original_buffer + @vc.flush_output_buffer + assert_equal ['foo', original_buffer], body_parts + assert_not_equal original_buffer, output_buffer + assert_equal Encoding::EUC_JP, output_buffer.encoding + end + + protected + def output_buffer + @vc.output_buffer + end + + def body_parts + @controller.response.body_parts + end +end diff --git a/actionview/test/template/output_safety_helper_test.rb b/actionview/test/template/output_safety_helper_test.rb new file mode 100644 index 0000000000..76c71c9e6d --- /dev/null +++ b/actionview/test/template/output_safety_helper_test.rb @@ -0,0 +1,28 @@ +require 'abstract_unit' + +class OutputSafetyHelperTest < ActionView::TestCase + tests ActionView::Helpers::OutputSafetyHelper + + def setup + @string = "hello" + end + + test "raw returns the safe string" do + result = raw(@string) + assert_equal @string, result + assert result.html_safe? + end + + test "raw handles nil values correctly" do + assert_equal "", raw(nil) + end + + test "safe_join should html_escape any items, including the separator, if they are not html_safe" do + joined = safe_join(["<p>foo</p>".html_safe, "<p>bar</p>"], "<br />") + assert_equal "<p>foo</p><br /><p>bar</p>", joined + + joined = safe_join(["<p>foo</p>".html_safe, "<p>bar</p>".html_safe], "<br />".html_safe) + assert_equal "<p>foo</p><br /><p>bar</p>", joined + end + +end
\ No newline at end of file diff --git a/actionview/test/template/record_identifier_test.rb b/actionview/test/template/record_identifier_test.rb new file mode 100644 index 0000000000..22038110a5 --- /dev/null +++ b/actionview/test/template/record_identifier_test.rb @@ -0,0 +1,49 @@ +require 'abstract_unit' +require 'controller/fake_models' + +class RecordIdentifierTest < ActiveSupport::TestCase + include ActionView::RecordIdentifier + + def setup + @klass = Comment + @record = @klass.new + @singular = 'comment' + @plural = 'comments' + @uncountable = Sheep + end + + def test_dom_id_with_new_record + assert_equal "new_#{@singular}", dom_id(@record) + end + + def test_dom_id_with_new_record_and_prefix + assert_equal "custom_prefix_#{@singular}", dom_id(@record, :custom_prefix) + end + + def test_dom_id_with_saved_record + @record.save + assert_equal "#{@singular}_1", dom_id(@record) + end + + def test_dom_id_with_prefix + @record.save + assert_equal "edit_#{@singular}_1", dom_id(@record, :edit) + end + + def test_dom_class + assert_equal @singular, dom_class(@record) + end + + def test_dom_class_with_prefix + assert_equal "custom_prefix_#{@singular}", dom_class(@record, :custom_prefix) + end + + def test_dom_id_as_singleton_method + @record.save + assert_equal "#{@singular}_1", ActionView::RecordIdentifier.dom_id(@record) + end + + def test_dom_class_as_singleton_method + assert_equal @singular, 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 new file mode 100644 index 0000000000..ab84bccb56 --- /dev/null +++ b/actionview/test/template/record_tag_helper_test.rb @@ -0,0 +1,117 @@ +require 'abstract_unit' + +class RecordTagPost + extend ActiveModel::Naming + include ActiveModel::Conversion + attr_accessor :id, :body + + def initialize + @id = 45 + @body = "What a wonderful world!" + + yield self if block_given? + end +end + +class RecordTagHelperTest < ActionView::TestCase + include RenderERBUtils + + tests ActionView::Helpers::RecordTagHelper + + def setup + super + @post = RecordTagPost.new + 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? + 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) + end +end diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb new file mode 100644 index 0000000000..8cffe73cce --- /dev/null +++ b/actionview/test/template/render_test.rb @@ -0,0 +1,538 @@ +# encoding: utf-8 +require 'abstract_unit' +require 'controller/fake_models' + +class TestController < ActionController::Base +end + +module RenderTestCases + def setup_view(paths) + @assigns = { :secret => 'in the sauce' } + @view = ActionView::Base.new(paths, @assigns) + @controller_view = TestController.new.view_context + + # Reload and register danish language for testing + I18n.reload! + I18n.backend.store_translations 'da', {} + 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 + end + + def test_render_without_options + e = assert_raises(ArgumentError) { @view.render() } + assert_match "You invoked render but did not give any of :partial, :template, :inline, :file or :text option.", e.message + end + + def test_render_file + assert_equal "Hello world!", @view.render(:file => "test/hello_world") + end + + # Test if :formats, :locale etc. options are passed correctly to the resolvers. + def test_render_file_with_format + assert_match "<h1>No Comment</h1>", @view.render(:file => "comments/empty", :formats => [:html]) + assert_match "<error>No Comment</error>", @view.render(:file => "comments/empty", :formats => [:xml]) + assert_match "<error>No Comment</error>", @view.render(:file => "comments/empty", :formats => :xml) + end + + def test_render_template_with_format + assert_match "<h1>No Comment</h1>", @view.render(:template => "comments/empty", :formats => [:html]) + assert_match "<error>No Comment</error>", @view.render(:template => "comments/empty", :formats => [:xml]) + end + + def test_render_partial_implicitly_use_format_of_the_rendered_template + @view.lookup_context.formats = [:json] + assert_equal "Hello world", @view.render(:template => "test/one", :formats => [:html]) + end + + def test_render_partial_implicitly_use_format_of_the_rendered_partial + @view.lookup_context.formats = [:html] + assert_equal "Third level", @view.render(:template => "test/html_template") + end + + def test_render_partial_use_last_prepended_format_for_partials_with_the_same_names + @view.lookup_context.formats = [:html] + assert_equal "\nHTML Template, but JSON partial", @view.render(:template => "test/change_priority") + end + + 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 + @view.render(:template => "with_format", :formats => [:json]) + end + end + + def test_render_file_with_locale + assert_equal "<h1>Kein Kommentar</h1>", @view.render(:file => "comments/empty", :locale => [:de]) + assert_equal "<h1>Kein Kommentar</h1>", @view.render(:file => "comments/empty", :locale => :de) + end + + def test_render_template_with_locale + assert_equal "<h1>Kein Kommentar</h1>", @view.render(:template => "comments/empty", :locale => [:de]) + end + + def test_render_file_with_handlers + assert_equal "<h1>No Comment</h1>\n", @view.render(:file => "comments/empty", :handlers => [:builder]) + assert_equal "<h1>No Comment</h1>\n", @view.render(:file => "comments/empty", :handlers => :builder) + end + + def test_render_template_with_handlers + assert_equal "<h1>No Comment</h1>\n", @view.render(:template => "comments/empty", :handlers => [:builder]) + end + + def test_render_raw_template_with_handlers + assert_equal "<%= hello_world %>\n", @view.render(:template => "plain_text") + end + + def test_render_raw_template_with_quotes + assert_equal %q;Here are some characters: !@#$%^&*()-="'}{`; + "\n", @view.render(:template => "plain_text_with_characters") + end + + def test_render_ruby_template_with_handlers + assert_equal "Hello from Ruby code", @view.render(:template => "ruby_template") + end + + def test_render_ruby_template_inline + assert_equal '4', @view.render(:inline => "(2**2).to_s", :type => :ruby) + end + + def test_render_file_with_localization_on_context_level + old_locale, @view.locale = @view.locale, :da + assert_equal "Hey verden", @view.render(:file => "test/hello_world") + ensure + @view.locale = old_locale + end + + def test_render_file_with_dashed_locale + old_locale, @view.locale = @view.locale, :"pt-BR" + assert_equal "Ola mundo", @view.render(:file => "test/hello_world") + ensure + @view.locale = old_locale + end + + def test_render_file_at_top_level + assert_equal 'Elastica', @view.render(:file => '/shared') + end + + def test_render_file_with_full_path + template_path = File.join(File.dirname(__FILE__), '../fixtures/test/hello_world') + assert_equal "Hello world!", @view.render(:file => template_path) + end + + def test_render_file_with_instance_variables + assert_equal "The secret is in the sauce\n", @view.render(:file => "test/render_file_with_ivar") + end + + def test_render_file_with_locals + locals = { :secret => 'in the sauce' } + assert_equal "The secret is in the sauce\n", @view.render(:file => "test/render_file_with_locals", :locals => locals) + end + + def test_render_file_not_using_full_path_with_dot_in_path + assert_equal "The secret is in the sauce\n", @view.render(:file => "test/dot.directory/render_file_with_ivar") + end + + def test_render_partial_from_default + assert_equal "only partial", @view.render("test/partial_only") + end + + def test_render_partial + assert_equal "only partial", @view.render(:partial => "test/partial_only") + end + + def test_render_partial_with_format + assert_equal 'partial html', @view.render(:partial => 'test/partial') + end + + def test_render_partial_with_selected_format + assert_equal 'partial html', @view.render(:partial => 'test/partial', :formats => :html) + assert_equal 'partial js', @view.render(:partial => 'test/partial', :formats => [:js]) + end + + def test_render_partial_at_top_level + # file fixtures/_top_level_partial_only (not fixtures/test) + assert_equal 'top level partial', @view.render(:partial => '/top_level_partial_only') + end + + def test_render_partial_with_format_at_top_level + # file fixtures/_top_level_partial.html (not fixtures/test, with format extension) + assert_equal 'top level partial html', @view.render(:partial => '/top_level_partial') + end + + def test_render_partial_with_locals + assert_equal "5", @view.render(:partial => "test/counter", :locals => { :counter_counter => 5 }) + end + + def test_render_partial_with_locals_from_default + 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 + 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 + end + + def test_render_partial_with_incompatible_object + e = assert_raises(ArgumentError) { @view.render(:partial => nil) } + assert_equal "'#{nil.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.", e.message + 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, " + + "and is followed by any combination of letters, numbers and underscores.", e.message + end + + def test_render_partial_with_errors + e = assert_raises(ActionView::Template::Error) { @view.render(:partial => "test/raise") } + assert_match %r!method.*doesnt_exist!, e.message + assert_equal "", e.sub_template_message + assert_equal "1", e.line_number + assert_equal "1: <%= doesnt_exist %>", e.annoted_source_code.strip + assert_equal File.expand_path("#{FIXTURE_LOAD_PATH}/test/_raise.html.erb"), e.file_name + end + + def test_render_error_indentation + e = assert_raises(ActionView::Template::Error) { @view.render(:partial => "test/raise_indentation") } + error_lines = e.annoted_source_code.split("\n") + assert_match %r!error\shere!, e.message + assert_equal "11", e.line_number + assert_equal " 9: <p>Ninth paragraph</p>", error_lines.second + assert_equal " 10: <p>Tenth paragraph</p>", error_lines.third + end + + def test_render_sub_template_with_errors + e = assert_raises(ActionView::Template::Error) { @view.render(:template => "test/sub_template_raise") } + assert_match %r!method.*doesnt_exist!, e.message + assert_equal "Trace of template inclusion: #{File.expand_path("#{FIXTURE_LOAD_PATH}/test/sub_template_raise.html.erb")}", e.sub_template_message + assert_equal "1", e.line_number + assert_equal File.expand_path("#{FIXTURE_LOAD_PATH}/test/_raise.html.erb"), e.file_name + end + + def test_render_file_with_errors + e = assert_raises(ActionView::Template::Error) { @view.render(:file => File.expand_path("test/_raise", FIXTURE_LOAD_PATH)) } + assert_match %r!method.*doesnt_exist!, e.message + assert_equal "", e.sub_template_message + assert_equal "1", e.line_number + assert_equal "1: <%= doesnt_exist %>", e.annoted_source_code.strip + assert_equal File.expand_path("#{FIXTURE_LOAD_PATH}/test/_raise.html.erb"), e.file_name + end + + def test_render_object + assert_equal "Hello: david", @view.render(:partial => "test/customer", :object => Customer.new("david")) + end + + def test_render_object_with_array + assert_equal "[1, 2, 3]", @view.render(:partial => "test/object_inspector", :object => [1, 2, 3]) + end + + def test_render_partial_collection + assert_equal "Hello: davidHello: mary", @view.render(:partial => "test/customer", :collection => [ Customer.new("david"), Customer.new("mary") ]) + end + + def test_render_partial_collection_as_by_string + assert_equal "david david davidmary mary mary", + @view.render(:partial => "test/customer_with_var", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => 'customer') + end + + def test_render_partial_collection_as_by_symbol + assert_equal "david david davidmary mary mary", + @view.render(:partial => "test/customer_with_var", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => :customer) + end + + def test_render_partial_collection_without_as + assert_equal "local_inspector,local_inspector_counter", + @view.render(:partial => "test/local_inspector", :collection => [ Customer.new("mary") ]) + end + + def test_render_partial_with_empty_collection_should_return_nil + assert_nil @view.render(:partial => "test/customer", :collection => []) + end + + def test_render_partial_with_nil_collection_should_return_nil + assert_nil @view.render(:partial => "test/customer", :collection => 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 + + def test_render_partial_with_layout_using_collection_and_template + assert_equal "<b>Hello: Amazon</b><b>Hello: Yahoo</b>", @view.render(:partial => "test/customer", :layout => 'test/b_layout_for_partial', :collection => [ Customer.new("Amazon"), Customer.new("Yahoo") ]) + end + + def test_render_partial_with_layout_using_collection_and_template_makes_current_item_available_in_layout + assert_equal '<b class="amazon">Hello: Amazon</b><b class="yahoo">Hello: Yahoo</b>', + @view.render(:partial => "test/customer", :layout => 'test/b_layout_for_partial_with_object', :collection => [ Customer.new("Amazon"), Customer.new("Yahoo") ]) + end + + def test_render_partial_with_layout_using_collection_and_template_makes_current_item_counter_available_in_layout + assert_equal '<b data-counter="0">Hello: Amazon</b><b data-counter="1">Hello: Yahoo</b>', + @view.render(:partial => "test/customer", :layout => 'test/b_layout_for_partial_with_object_counter', :collection => [ Customer.new("Amazon"), Customer.new("Yahoo") ]) + end + + def test_render_partial_with_layout_using_object_and_template_makes_object_available_in_layout + assert_equal '<b class="amazon">Hello: Amazon</b>', + @view.render(:partial => "test/customer", :layout => 'test/b_layout_for_partial_with_object', :object => Customer.new("Amazon")) + end + + def test_render_partial_with_empty_array_should_return_nil + assert_nil @view.render(:partial => []) + end + + def test_render_partial_using_string + assert_equal "Hello: Anonymous", @controller_view.render('customer') + end + + def test_render_partial_with_locals_using_string + assert_equal "Hola: david", @controller_view.render('customer_greeting', :greeting => 'Hola', :customer_greeting => Customer.new("david")) + end + + def test_render_partial_using_object + assert_equal "Hello: lifo", + @controller_view.render(Customer.new("lifo"), :greeting => "Hello") + end + + def test_render_partial_using_collection + customers = [ Customer.new("Amazon"), Customer.new("Yahoo") ] + assert_equal "Hello: AmazonHello: Yahoo", + @controller_view.render(customers, :greeting => "Hello") + end + + def test_render_partial_without_object_or_collection_does_not_generate_partial_name_local_variable + exception = assert_raises ActionView::Template::Error do + @controller_view.render("partial_name_local_variable") + end + assert_match "undefined local variable or method `partial_name_local_variable'", exception.message + end + + # TODO: The reason for this test is unclear, improve documentation + def test_render_partial_and_fallback_to_layout + assert_equal "Before (Josh)\n\nAfter", @view.render(:partial => "test/layout_for_partial", :locals => { :name => "Josh" }) + end + + # TODO: The reason for this test is unclear, improve documentation + def test_render_missing_xml_partial_and_raise_missing_template + @view.formats = [:xml] + assert_raises(ActionView::MissingTemplate) { @view.render(:partial => "test/layout_for_partial") } + ensure + @view.formats = nil + end + + def test_render_layout_with_block_and_other_partial_inside + render = @view.render(:layout => "test/layout_with_partial_and_yield") { "Yield!" } + assert_equal "Before\npartial html\nYield!\nAfter\n", render + end + + def test_render_inline + assert_equal "Hello, World!", @view.render(:inline => "Hello, World!") + end + + def test_render_inline_with_locals + assert_equal "Hello, Josh!", @view.render(:inline => "Hello, <%= name %>!", :locals => { :name => "Josh" }) + end + + def test_render_fallbacks_to_erb_for_unknown_types + assert_equal "Hello, World!", @view.render(:inline => "Hello, World!", :type => :bar) + end + + CustomHandler = lambda do |template| + "@output_buffer = ''\n" + + "@output_buffer << 'source: #{template.source.inspect}'\n" + end + + def test_render_inline_with_render_from_to_proc + ActionView::Template.register_template_handler :ruby_handler, :source.to_proc + assert_equal '3', @view.render(:inline => "(1 + 2).to_s", :type => :ruby_handler) + end + + def test_render_inline_with_compilable_custom_type + ActionView::Template.register_template_handler :foo, CustomHandler + assert_equal 'source: "Hello, World!"', @view.render(:inline => "Hello, World!", :type => :foo) + end + + def test_render_inline_with_locals_and_compilable_custom_type + ActionView::Template.register_template_handler :foo, CustomHandler + assert_equal 'source: "Hello, <%= name %>!"', @view.render(:inline => "Hello, <%= name %>!", :locals => { :name => "Josh" }, :type => :foo) + end + + def test_render_knows_about_types_registered_when_extensions_are_checked_earlier_in_initialization + ActionView::Template::Handlers.extensions + ActionView::Template.register_template_handler :foo, CustomHandler + assert ActionView::Template::Handlers.extensions.include?(:foo) + end + + def test_render_ignores_templates_with_malformed_template_handlers + ActiveSupport::Deprecation.silence do + %w(malformed malformed.erb malformed.html.erb malformed.en.html.erb).each do |name| + assert File.exists?(File.expand_path("#{FIXTURE_LOAD_PATH}/test/malformed/#{name}~")), "Malformed file (#{name}~) which should be ignored does not exists" + assert_raises(ActionView::MissingTemplate) { @view.render(:file => "test/malformed/#{name}") } + end + end + end + + def test_render_with_layout + assert_equal %(<title></title>\nHello world!\n), + @view.render(:file => "test/hello_world", :layout => "layouts/yield") + end + + def test_render_with_layout_which_has_render_inline + assert_equal %(welcome\nHello world!\n), + @view.render(:file => "test/hello_world", :layout => "layouts/yield_with_render_inline_inside") + end + + def test_render_with_layout_which_renders_another_partial + assert_equal %(partial html\nHello world!\n), + @view.render(:file => "test/hello_world", :layout => "layouts/yield_with_render_partial_inside") + end + + def test_render_layout_with_block_and_yield + assert_equal %(Content from block!\n), + @view.render(:layout => "layouts/yield_only") { "Content from block!" } + end + + def test_render_layout_with_block_and_yield_with_params + assert_equal %(Yield! Content from block!\n), + @view.render(:layout => "layouts/yield_with_params") { |param| "#{param} Content from block!" } + end + + def test_render_layout_with_block_which_renders_another_partial_and_yields + assert_equal %(partial html\nContent from block!\n), + @view.render(:layout => "layouts/partial_and_yield") { "Content from block!" } + end + + def test_render_partial_and_layout_without_block_with_locals + assert_equal %(Before (Foo!)\npartial html\nAfter), + @view.render(:partial => 'test/partial', :layout => 'test/layout_for_partial', :locals => { :name => 'Foo!'}) + end + + def test_render_partial_and_layout_without_block_with_locals_and_rendering_another_partial + assert_equal %(Before (Foo!)\npartial html\npartial with partial\n\nAfter), + @view.render(:partial => 'test/partial_with_partial', :layout => 'test/layout_for_partial', :locals => { :name => 'Foo!'}) + 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!'}) + end + + def test_render_layout_with_a_nested_render_layout_call_using_block_with_render_partial + assert_equal %(Before (Foo!)\nBefore (Bar!)\n\n partial html\n\nAfterpartial with layout\n\nAfter), + @view.render(:partial => 'test/partial_with_layout_block_partial', :layout => 'test/layout_for_partial', :locals => { :name => 'Foo!'}) + end + + def test_render_layout_with_a_nested_render_layout_call_using_block_with_render_content + assert_equal %(Before (Foo!)\nBefore (Bar!)\n\n Content from inside layout!\n\nAfterpartial with layout\n\nAfter), + @view.render(:partial => 'test/partial_with_layout_block_content', :layout => 'test/layout_for_partial', :locals => { :name => 'Foo!'}) + end + + def test_render_partial_with_layout_raises_descriptive_error + e = assert_raises(ActionView::MissingTemplate) { @view.render(partial: 'test/partial', layout: true) } + assert_match "Missing partial /true with", e.message + end + + def test_render_with_nested_layout + assert_equal %(<title>title</title>\n\n<div id="column">column</div>\n<div id="content">content</div>\n), + @view.render(:file => "test/nested_layout", :layout => "layouts/yield") + end + + def test_render_with_file_in_layout + assert_equal %(\n<title>title</title>\n\n), + @view.render(:file => "test/layout_render_file") + end + + def test_render_layout_with_object + assert_equal %(<title>David</title>), + @view.render(:file => "test/layout_render_object") + end + + def test_render_with_passing_couple_extensions_to_one_register_template_handler_function_call + ActionView::Template.register_template_handler :foo1, :foo2, CustomHandler + assert_equal @view.render(:inline => "Hello, World!", :type => :foo1), @view.render(:inline => "Hello, World!", :type => :foo2) + end + + def test_render_throws_exception_when_no_extensions_passed_to_register_template_handler_function_call + assert_raises(ArgumentError) { ActionView::Template.register_template_handler CustomHandler } + end +end + +class CachedViewRenderTest < ActiveSupport::TestCase + include RenderTestCases + + # Ensure view path cache is primed + def setup + view_paths = ActionController::Base.view_paths + assert_equal ActionView::OptimizedFileSystemResolver, view_paths.first.class + setup_view(view_paths) + end + + def teardown + GC.start + end +end + +class LazyViewRenderTest < ActiveSupport::TestCase + include RenderTestCases + + # Test the same thing as above, but make sure the view path + # is not eager loaded + def setup + path = ActionView::FileSystemResolver.new(FIXTURE_LOAD_PATH) + view_paths = ActionView::PathSet.new([path]) + assert_equal ActionView::FileSystemResolver.new(FIXTURE_LOAD_PATH), view_paths.first + setup_view(view_paths) + end + + def teardown + GC.start + end + + def test_render_utf8_template_with_magic_comment + with_external_encoding Encoding::ASCII_8BIT do + result = @view.render(:file => "test/utf8_magic", :formats => [:html], :layouts => "layouts/yield") + assert_equal Encoding::UTF_8, result.encoding + assert_equal "\nРусский \nтекст\n\nUTF-8\nUTF-8\nUTF-8\n", result + end + end + + def test_render_utf8_template_with_default_external_encoding + with_external_encoding Encoding::UTF_8 do + result = @view.render(:file => "test/utf8", :formats => [:html], :layouts => "layouts/yield") + assert_equal Encoding::UTF_8, result.encoding + assert_equal "Русский текст\n\nUTF-8\nUTF-8\nUTF-8\n", result + end + end + + def test_render_utf8_template_with_incompatible_external_encoding + with_external_encoding Encoding::SHIFT_JIS do + e = assert_raises(ActionView::Template::Error) { @view.render(:file => "test/utf8", :formats => [:html], :layouts => "layouts/yield") } + assert_match 'Your template was not saved as valid Shift_JIS', e.original_exception.message + end + end + + def test_render_utf8_template_with_partial_with_incompatible_encoding + with_external_encoding Encoding::SHIFT_JIS do + e = assert_raises(ActionView::Template::Error) { @view.render(:file => "test/utf8_magic_with_bare_partial", :formats => [:html], :layouts => "layouts/yield") } + assert_match 'Your template was not saved as valid Shift_JIS', e.original_exception.message + end + end + + def with_external_encoding(encoding) + old = Encoding.default_external + silence_warnings { Encoding.default_external = encoding } + yield + ensure + silence_warnings { Encoding.default_external = old } + end +end diff --git a/actionview/test/template/resolver_patterns_test.rb b/actionview/test/template/resolver_patterns_test.rb new file mode 100644 index 0000000000..97b1bad055 --- /dev/null +++ b/actionview/test/template/resolver_patterns_test.rb @@ -0,0 +1,31 @@ +require 'abstract_unit' + +class ResolverPatternsTest < ActiveSupport::TestCase + def setup + path = File.expand_path("../../fixtures/", __FILE__) + pattern = ":prefix/{:formats/,}:action{.:formats,}{.:handlers,}" + @resolver = ActionView::FileSystemResolver.new(path, pattern) + end + + def test_should_return_empty_list_for_unknown_path + templates = @resolver.find_all("unknown", "custom_pattern", false, {:locale => [], :formats => [:html], :handlers => [:erb]}) + assert_equal [], templates, "expected an empty list of templates" + end + + def test_should_return_template_for_declared_path + templates = @resolver.find_all("path", "custom_pattern", false, {:locale => [], :formats => [:html], :handlers => [:erb]}) + assert_equal 1, templates.size, "expected one template" + assert_equal "Hello custom patterns!", templates.first.source + assert_equal "custom_pattern/path", templates.first.virtual_path + assert_equal [:html], templates.first.formats + end + + def test_should_return_all_templates_when_ambigous_pattern + templates = @resolver.find_all("another", "custom_pattern", false, {:locale => [], :formats => [:html], :handlers => [:erb]}) + assert_equal 2, templates.size, "expected two templates" + assert_equal "Another template!", templates[0].source + assert_equal "custom_pattern/another", templates[0].virtual_path + assert_equal "Hello custom patterns!", templates[1].source + assert_equal "custom_pattern/another", templates[1].virtual_path + end +end diff --git a/actionview/test/template/sanitize_helper_test.rb b/actionview/test/template/sanitize_helper_test.rb new file mode 100644 index 0000000000..12d5260a9d --- /dev/null +++ b/actionview/test/template/sanitize_helper_test.rb @@ -0,0 +1,51 @@ +require 'abstract_unit' + +# The exhaustive tests are in test/controller/html/sanitizer_test.rb. +# This tests the 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 + assert_equal '', sanitize("<form action=\"/foo/bar\" method=\"post\"><input></form>") + 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_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 <img onerror=alert(1337)", ERB::Util.html_escape(strip_tags("something <img onerror=alert(1337)")) + end + + def test_sanitize_is_marked_safe + assert sanitize("<html><script></script></html>").html_safe? + end +end diff --git a/actionview/test/template/streaming_render_test.rb b/actionview/test/template/streaming_render_test.rb new file mode 100644 index 0000000000..8a24d78e74 --- /dev/null +++ b/actionview/test/template/streaming_render_test.rb @@ -0,0 +1,108 @@ +# encoding: utf-8 +require 'abstract_unit' + +class TestController < ActionController::Base +end + +class FiberedTest < ActiveSupport::TestCase + def setup + view_paths = ActionController::Base.view_paths + @assigns = { :secret => 'in the sauce', :name => nil } + @view = ActionView::Base.new(view_paths, @assigns) + @controller_view = TestController.new.view_context + end + + def render_body(options) + @view.view_renderer.render_body(@view, options) + end + + def buffered_render(options) + body = render_body(options) + string = "" + body.each do |piece| + string << piece + end + string + end + + def test_streaming_works + content = [] + body = render_body(:template => "test/hello_world", :layout => "layouts/yield") + + body.each do |piece| + content << piece + end + + assert_equal "<title>", content[0] + assert_equal "", content[1] + assert_equal "</title>\n", content[2] + assert_equal "Hello world!", content[3] + assert_equal "\n", content[4] + end + + def test_render_file + assert_equal "Hello world!", buffered_render(:file => "test/hello_world") + end + + def test_render_file_with_locals + locals = { :secret => 'in the sauce' } + assert_equal "The secret is in the sauce\n", buffered_render(:file => "test/render_file_with_locals", :locals => locals) + end + + def test_render_partial + assert_equal "only partial", buffered_render(:partial => "test/partial_only") + end + + def test_render_inline + assert_equal "Hello, World!", buffered_render(:inline => "Hello, World!") + end + + def test_render_without_layout + assert_equal "Hello world!", buffered_render(:template => "test/hello_world") + end + + def test_render_with_layout + assert_equal %(<title></title>\nHello world!\n), + buffered_render(:template => "test/hello_world", :layout => "layouts/yield") + end + + def test_render_with_layout_which_has_render_inline + assert_equal %(welcome\nHello world!\n), + buffered_render(:template => "test/hello_world", :layout => "layouts/yield_with_render_inline_inside") + end + + def test_render_with_layout_which_renders_another_partial + assert_equal %(partial html\nHello world!\n), + buffered_render(:template => "test/hello_world", :layout => "layouts/yield_with_render_partial_inside") + end + + def test_render_with_nested_layout + assert_equal %(<title>title</title>\n\n<div id="column">column</div>\n<div id="content">content</div>\n), + buffered_render(:template => "test/nested_layout", :layout => "layouts/yield") + end + + def test_render_with_file_in_layout + assert_equal %(\n<title>title</title>\n\n), + buffered_render(:template => "test/layout_render_file") + end + + def test_render_with_handler_without_streaming_support + assert_match "<p>This is grand!</p>", buffered_render(:template => "test/hello") + end + + def test_render_with_streaming_multiple_yields_provide_and_content_for + assert_equal "Yes, \nthis works\n like a charm.", + buffered_render(:template => "test/streaming", :layout => "layouts/streaming") + end + + def test_render_with_streaming_with_fake_yields_and_streaming_buster + assert_equal "This won't look\n good.", + buffered_render(:template => "test/streaming_buster", :layout => "layouts/streaming") + end + + def test_render_with_nested_streaming_multiple_yields_provide_and_content_for + assert_equal "?Yes, \n\nthis works\n\n? like a charm.", + buffered_render(:template => "test/nested_streaming", :layout => "layouts/streaming") + end + +end diff --git a/actionview/test/template/tag_helper_test.rb b/actionview/test/template/tag_helper_test.rb new file mode 100644 index 0000000000..802da5d566 --- /dev/null +++ b/actionview/test/template/tag_helper_test.rb @@ -0,0 +1,130 @@ +require 'abstract_unit' + +class TagHelperTest < ActionView::TestCase + include RenderERBUtils + + tests ActionView::Helpers::TagHelper + + def test_tag + assert_equal "<br />", tag("br") + assert_equal "<br clear=\"left\" />", tag(:br, :clear => "left") + assert_equal "<br>", tag("br", nil, true) + end + + def test_tag_options + str = tag("p", "class" => "show", :class => "elsewhere") + assert_match(/class="show"/, str) + assert_match(/class="elsewhere"/, str) + end + + def test_tag_options_rejects_nil_option + assert_equal "<p />", tag("p", :ignored => nil) + end + + def test_tag_options_accepts_false_option + assert_equal "<p value=\"false\" />", tag("p", :value => false) + end + + def test_tag_options_accepts_blank_option + assert_equal "<p included=\"\" />", tag("p", :included => '') + end + + def test_tag_options_converts_boolean_option + assert_dom_equal '<p disabled="disabled" itemscope="itemscope" multiple="multiple" readonly="readonly" allowfullscreen="allowfullscreen" seamless="seamless" typemustmatch="typemustmatch" sortable="sortable" default="default" inert="inert" truespeed="truespeed" />', + tag("p", :disabled => true, :itemscope => true, :multiple => true, :readonly => true, :allowfullscreen => true, :seamless => true, :typemustmatch => true, :sortable => true, :default => true, :inert => true, :truespeed => true) + end + + def test_content_tag + assert_equal "<a href=\"create\">Create</a>", content_tag("a", "Create", "href" => "create") + assert content_tag("a", "Create", "href" => "create").html_safe? + assert_equal content_tag("a", "Create", "href" => "create"), + content_tag("a", "Create", :href => "create") + assert_equal "<p><script>evil_js</script></p>", + content_tag(:p, '<script>evil_js</script>') + assert_equal "<p><script>evil_js</script></p>", + content_tag(:p, '<script>evil_js</script>', nil, false) + end + + def test_content_tag_with_block_in_erb + buffer = render_erb("<%= content_tag(:div) do %>Hello world!<% end %>") + assert_dom_equal "<div>Hello world!</div>", 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 + end + + def test_content_tag_with_block_and_options_out_of_erb + assert_dom_equal %(<div class="green">Hello world!</div>), content_tag(:div, :class => "green") { "Hello world!" } + end + + def test_content_tag_with_block_and_options_outside_out_of_erb + assert_equal content_tag("a", "Create", :href => "create"), + content_tag("a", "href" => "create") { "Create" } + 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") }, + output_buffer + end + + def test_content_tag_nested_in_content_tag_in_erb + assert_equal "<p>\n <b>Hello</b>\n</p>", view.render("test/content_tag_nested_in_content_tag") + end + + def test_content_tag_with_escaped_array_class + str = content_tag('p', "limelight", :class => ["song", "play>"]) + assert_equal "<p class=\"song play>\">limelight</p>", str + + str = content_tag('p', "limelight", :class => ["song", "play"]) + assert_equal "<p class=\"song play\">limelight</p>", str + end + + def test_content_tag_with_unescaped_array_class + str = content_tag('p', "limelight", {:class => ["song", "play>"]}, false) + assert_equal "<p class=\"song play>\">limelight</p>", str + end + + def test_content_tag_with_data_attributes + assert_dom_equal '<p data-number="1" data-string="hello" data-string-with-quotes="double"quote"party"">limelight</p>', + content_tag('p', "limelight", data: { number: 1, string: 'hello', string_with_quotes: 'double"quote"party"' }) + end + + def test_cdata_section + assert_equal "<![CDATA[<hello world>]]>", cdata_section("<hello world>") + end + + def test_cdata_section_splitted + assert_equal "<![CDATA[hello]]]]><![CDATA[>world]]>", cdata_section("hello]]>world") + assert_equal "<![CDATA[hello]]]]><![CDATA[>world]]]]><![CDATA[>again]]>", cdata_section("hello]]>world]]>again") + end + + def test_escape_once + assert_equal '1 < 2 & 3', escape_once('1 < 2 & 3') + end + + def test_tag_honors_html_safe_for_param_values + ['1&2', '1 < 2', '“test“'].each do |escaped| + assert_equal %(<a href="#{escaped}" />), tag('a', :href => escaped.html_safe) + end + end + + def test_skip_invalid_escaped_attributes + ['&1;', 'dfa3;', '& #123;'].each do |escaped| + assert_equal %(<a href="#{escaped.gsub(/&/, '&')}" />), tag('a', :href => escaped) + end + end + + def test_disable_escaping + assert_equal '<a href="&" />', tag('a', { :href => '&' }, false, false) + end + + def test_data_attributes + ['data', :data].each { |data| + assert_dom_equal '<a data-a-float="3.14" data-a-big-decimal="-123.456" data-a-number="1" data-array="[1,2,3]" data-hash="{"key":"value"}" data-string-with-quotes="double"quote"party"" data-string="hello" data-symbol="foo" />', + 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 +end diff --git a/actionview/test/template/template_error_test.rb b/actionview/test/template/template_error_test.rb new file mode 100644 index 0000000000..91424daeed --- /dev/null +++ b/actionview/test/template/template_error_test.rb @@ -0,0 +1,13 @@ +require "abstract_unit" + +class TemplateErrorTest < ActiveSupport::TestCase + def test_provides_original_message + error = ActionView::Template::Error.new("test", Exception.new("original")) + assert_equal "original", error.message + end + + def test_provides_useful_inspect + error = ActionView::Template::Error.new("test", Exception.new("original")) + assert_equal "#<ActionView::Template::Error: original>", error.inspect + end +end diff --git a/actionview/test/template/template_test.rb b/actionview/test/template/template_test.rb new file mode 100644 index 0000000000..c94508d678 --- /dev/null +++ b/actionview/test/template/template_test.rb @@ -0,0 +1,200 @@ +# encoding: US-ASCII +require "abstract_unit" +require "logger" + +class TestERBTemplate < ActiveSupport::TestCase + ERBHandler = ActionView::Template::Handlers::ERB.new + + class LookupContext + def disable_cache + yield + end + + def find_template(*args) + end + + attr_accessor :formats + end + + class Context + def initialize + @output_buffer = "original" + @virtual_path = nil + end + + def hello + "Hello" + end + + def apostrophe + "l'apostrophe" + end + + def partial + ActionView::Template.new( + "<%= @virtual_path %>", + "partial", + ERBHandler, + :virtual_path => "partial" + ) + end + + def lookup_context + @lookup_context ||= LookupContext.new + end + + def logger + ActiveSupport::Logger.new(STDERR) + end + + def my_buffer + @output_buffer + end + end + + def new_template(body = "<%= hello %>", details = { format: :html }) + ActionView::Template.new(body, "hello template", details.fetch(:handler) { ERBHandler }, {:virtual_path => "hello"}.merge!(details)) + end + + def render(locals = {}) + @template.render(@context, locals) + end + + def setup + @context = Context.new + end + + def test_basic_template + @template = new_template + assert_equal "Hello", render + end + + def test_basic_template_does_html_escape + @template = new_template("<%= apostrophe %>") + assert_equal "l'apostrophe", render + end + + def test_text_template_does_not_html_escape + @template = new_template("<%= apostrophe %> <%== apostrophe %>", format: :text) + assert_equal "l'apostrophe l'apostrophe", render + end + + def test_raw_template + @template = new_template("<%= hello %>", :handler => ActionView::Template::Handlers::Raw.new) + assert_equal "<%= hello %>", render + end + + def test_template_loses_its_source_after_rendering + @template = new_template + render + assert_nil @template.source + end + + def test_template_does_not_lose_its_source_after_rendering_if_it_does_not_have_a_virtual_path + @template = new_template("Hello", :virtual_path => nil) + render + assert_equal "Hello", @template.source + end + + def test_locals + @template = new_template("<%= my_local %>") + @template.locals = [:my_local] + assert_equal "I am a local", render(:my_local => "I am a local") + end + + def test_restores_buffer + @template = new_template + assert_equal "Hello", render + assert_equal "original", @context.my_buffer + end + + def test_virtual_path + @template = new_template("<%= @virtual_path %>" \ + "<%= partial.render(self, {}) %>" \ + "<%= @virtual_path %>") + assert_equal "hellopartialhello", render + end + + 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) + 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) + end + + def test_refresh_raises_an_error_without_virtual_path + @template = new_template("Hello", :virtual_path => nil) + assert_raise RuntimeError do + @template.refresh(@context) + end + end + + def test_resulting_string_is_utf8 + @template = new_template + assert_equal Encoding::UTF_8, render.encoding + end + + def test_no_magic_comment_word_with_utf_8 + @template = new_template("hello \u{fc}mlat") + assert_equal Encoding::UTF_8, render.encoding + assert_equal "hello \u{fc}mlat", render + end + + # This test ensures that if the default_external + # is set to something other than UTF-8, we don't + # get any errors and get back a UTF-8 String. + def test_default_external_works + with_external_encoding "ISO-8859-1" do + @template = new_template("hello \xFCmlat") + assert_equal Encoding::UTF_8, render.encoding + assert_equal "hello \u{fc}mlat", render + end + end + + def test_encoding_can_be_specified_with_magic_comment + @template = new_template("# encoding: ISO-8859-1\nhello \xFCmlat") + assert_equal Encoding::UTF_8, render.encoding + assert_equal "\nhello \u{fc}mlat", render + end + + # TODO: This is currently handled inside ERB. The case of explicitly + # lying about encodings via the normal Rails API should be handled + # inside Rails. + def test_lying_with_magic_comment + assert_raises(ActionView::Template::Error) do + @template = new_template("# encoding: UTF-8\nhello \xFCmlat", :virtual_path => nil) + render + end + end + + def test_encoding_can_be_specified_with_magic_comment_in_erb + with_external_encoding Encoding::UTF_8 do + @template = new_template("<%# encoding: ISO-8859-1 %>hello \xFCmlat", :virtual_path => nil) + assert_equal Encoding::UTF_8, render.encoding + assert_equal "hello \u{fc}mlat", render + end + end + + def test_error_when_template_isnt_valid_utf8 + assert_raises(ActionView::Template::Error, /\xFC/) do + @template = new_template("hello \xFCmlat", :virtual_path => nil) + render + end + end + + def with_external_encoding(encoding) + old = Encoding.default_external + Encoding::Converter.new old, encoding if old != encoding + silence_warnings { Encoding.default_external = encoding } + yield + ensure + silence_warnings { Encoding.default_external = old } + end +end diff --git a/actionview/test/template/test_case_test.rb b/actionview/test/template/test_case_test.rb new file mode 100644 index 0000000000..4ee0930341 --- /dev/null +++ b/actionview/test/template/test_case_test.rb @@ -0,0 +1,366 @@ +require 'abstract_unit' + +module ActionView + + module ATestHelper + end + + module AnotherTestHelper + def from_another_helper + 'Howdy!' + end + end + + module ASharedTestHelper + def from_shared_helper + 'Holla!' + end + end + + class TestCase + helper ASharedTestHelper + + module SharedTests + def self.included(test_case) + test_case.class_eval do + test "helpers defined on ActionView::TestCase are available" do + assert test_case.ancestors.include?(ASharedTestHelper) + assert_equal 'Holla!', from_shared_helper + end + end + end + end + end + + class GeneralViewTest < ActionView::TestCase + include SharedTests + test_case = self + + test "memoizes the view" do + assert_same view, view + end + + test "exposes view as _view for backwards compatibility" do + assert_same _view, view + end + + test "retrieve non existing config values" do + assert_equal nil, ActionView::Base.new.config.something_odd + end + + test "works without testing a helper module" do + assert_equal 'Eloy', render('developers/developer', :developer => stub(:name => 'Eloy')) + end + + test "can render a layout with block" do + assert_equal "Before (ChrisCruft)\n!\nAfter", + render(:layout => "test/layout_for_partial", :locals => {:name => "ChrisCruft"}) {"!"} + end + + helper AnotherTestHelper + test "additional helper classes can be specified as in a controller" do + assert test_case.ancestors.include?(AnotherTestHelper) + assert_equal 'Howdy!', from_another_helper + end + + test "determine_default_helper_class returns nil if the test name constant resolves to a class" do + assert_nil self.class.determine_default_helper_class("String") + end + + test "delegates notice to request.flash[:notice]" do + view.request.flash.expects(:[]).with(:notice) + view.notice + end + + test "delegates alert to request.flash[:alert]" do + view.request.flash.expects(:[]).with(:alert) + view.alert + end + + test "uses controller lookup context" do + assert_equal self.lookup_context, @controller.lookup_context + end + end + + class ClassMethodsTest < ActionView::TestCase + include SharedTests + test_case = self + + tests ATestHelper + test "tests the specified helper module" do + assert_equal ATestHelper, test_case.helper_class + assert test_case.ancestors.include?(ATestHelper) + end + + helper AnotherTestHelper + test "additional helper classes can be specified as in a controller" do + assert test_case.ancestors.include?(AnotherTestHelper) + assert_equal 'Howdy!', from_another_helper + + test_case.helper_class.module_eval do + def render_from_helper + from_another_helper + end + end + assert_equal 'Howdy!', render(:partial => 'test/from_helper') + end + end + + class HelperInclusionTest < ActionView::TestCase + module RenderHelper + def render_from_helper + render :partial => 'customer', :collection => @customers + end + end + + helper RenderHelper + + 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')] + assert_match(/Hello: EloyHello: Manfred/, render(:partial => 'test/from_helper')) + end + end + + class ControllerHelperMethod < ActionView::TestCase + module SomeHelper + def some_method + render :partial => 'test/from_helper' + end + end + + helper SomeHelper + + test "can call a helper method defined on the current controller from a helper" do + @controller.singleton_class.class_eval <<-EOF, __FILE__, __LINE__ + 1 + def render_from_helper + 'controller_helper_method' + end + EOF + @controller.class.helper_method :render_from_helper + + assert_equal 'controller_helper_method', some_method + end + end + + class ViewAssignsTest < ActionView::TestCase + test "view_assigns returns a Hash of user defined ivars" do + @a = 'b' + @c = 'd' + assert_equal({:a => 'b', :c => 'd'}, view_assigns) + end + + 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" + end + end + end + + class HelperExposureTest < ActionView::TestCase + helper(Module.new do + def render_from_helper + from_test_case + end + end) + test "is able to make methods available to the view" do + assert_equal 'Word!', render(:partial => 'test/from_helper') + end + + def from_test_case; 'Word!'; end + helper_method :from_test_case + end + + class IgnoreProtectAgainstForgeryTest < ActionView::TestCase + module HelperThatInvokesProtectAgainstForgery + def help_me + protect_against_forgery? + end + end + + helper HelperThatInvokesProtectAgainstForgery + + test "protect_from_forgery? in any helpers returns false" do + assert !view.help_me + end + + end + + class ATestHelperTest < ActionView::TestCase + include SharedTests + test_case = self + + test "inflects the name of the helper module to test from the test case class" do + assert_equal ATestHelper, test_case.helper_class + assert test_case.ancestors.include?(ATestHelper) + end + + test "a configured test controller is available" do + assert_kind_of ActionController::Base, controller + assert_equal '', controller.controller_path + end + + test "no additional helpers should shared across test cases" do + assert !test_case.ancestors.include?(AnotherTestHelper) + assert_raise(NoMethodError) { send :from_another_helper } + end + + test "is able to use routes" do + controller.request.assign_parameters(@routes, 'foo', 'index') + assert_equal '/foo', url_for + assert_equal '/bar', url_for(:controller => 'bar') + end + + test "is able to use named routes" do + with_routing do |set| + set.draw { resources :contents } + assert_equal 'http://test.host/contents/new', new_content_url + assert_equal 'http://test.host/contents/1', content_url(:id => 1) + end + end + + test "is able to use mounted routes" do + with_routing do |set| + app = Class.new do + def self.routes + @routes ||= ActionDispatch::Routing::RouteSet.new + end + + routes.draw { get "bar", :to => lambda {} } + + def self.call(*) + end + end + + set.draw { mount app => "/foo", :as => "foo_app" } + + assert_equal '/foo/bar', foo_app.bar_path + end + end + + test "named routes can be used from helper included in view" do + with_routing do |set| + set.draw { resources :contents } + _helpers.module_eval do + def render_from_helper + new_content_url + end + end + + assert_equal 'http://test.host/contents/new', render(:partial => 'test/from_helper') + end + 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(:partial => 'developers/developer', + :locals => { :developer => stub(:name => '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')] + assert_match(/Hello: EloyHello: Manfred/, render(:file => 'test/list')) + end + + test "is able to render partials from templates and also use instance variables after view has been referenced" do + @controller.controller_path = "test" + + view + + @customers = [stub(:name => 'Eloy'), stub(:name => 'Manfred')] + assert_match(/Hello: EloyHello: Manfred/, render(:file => 'test/list')) + end + + end + + class AssertionsTest < ActionView::TestCase + def render_from_helper + form_tag('/foo') do + safe_concat render(:text => '<ul><li>foo</li></ul>') + end + end + helper_method :render_from_helper + + test "uses the output_buffer for assert_select" do + render(:partial => 'test/from_helper') + + assert_select 'form' do + 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 '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 + + 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 + + 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' } + end + end + + module AHelperWithInitialize + def initialize(*) + super + @called_initialize = true + end + end + + class AHelperWithInitializeTest < ActionView::TestCase + test "the helper's initialize was actually called" do + assert @called_initialize + end + end +end diff --git a/actionview/test/template/test_test.rb b/actionview/test/template/test_test.rb new file mode 100644 index 0000000000..108a674d95 --- /dev/null +++ b/actionview/test/template/test_test.rb @@ -0,0 +1,80 @@ +require 'abstract_unit' + +module PeopleHelper + def title(text) + content_tag(:h1, text) + end + + def homepage_path + people_path + end + + def homepage_url + people_url + end + + def link_to_person(person) + link_to person.name, person + end +end + +class PeopleHelperTest < ActionView::TestCase + def test_title + assert_equal "<h1>Ruby on Rails</h1>", title("Ruby on Rails") + end + + def test_homepage_path + with_test_route_set do + assert_equal "/people", homepage_path + end + end + + def test_homepage_url + with_test_route_set do + assert_equal "http://test.host/people", homepage_url + end + end + + def test_link_to_person + with_test_route_set do + person = mock(:name => "David") + person.class.extend ActiveModel::Naming + expects(:mocha_mock_path).with(person).returns("/people/1") + assert_equal '<a href="/people/1">David</a>', link_to_person(person) + end + end + + private + def with_test_route_set + with_routing do |set| + set.draw do + get 'people', :to => 'people#index', :as => :people + end + yield + end + end +end + +class CrazyHelperTest < ActionView::TestCase + tests PeopleHelper + + def test_helper_class_can_be_set_manually_not_just_inferred + assert_equal PeopleHelper, self.class.helper_class + end +end + +class CrazySymbolHelperTest < ActionView::TestCase + tests :people + + def test_set_helper_class_using_symbol + assert_equal PeopleHelper, self.class.helper_class + end +end + +class CrazyStringHelperTest < ActionView::TestCase + tests 'people' + + def test_set_helper_class_using_string + assert_equal PeopleHelper, self.class.helper_class + end +end diff --git a/actionview/test/template/testing/fixture_resolver_test.rb b/actionview/test/template/testing/fixture_resolver_test.rb new file mode 100644 index 0000000000..9649f349cb --- /dev/null +++ b/actionview/test/template/testing/fixture_resolver_test.rb @@ -0,0 +1,18 @@ +require 'abstract_unit' + +class FixtureResolverTest < ActiveSupport::TestCase + def test_should_return_empty_list_for_unknown_path + resolver = ActionView::FixtureResolver.new() + templates = resolver.find_all("path", "arbitrary", false, {:locale => [], :formats => [:html], :handlers => []}) + assert_equal [], templates, "expected an empty list of templates" + end + + def test_should_return_template_for_declared_path + resolver = ActionView::FixtureResolver.new("arbitrary/path.erb" => "this text") + templates = resolver.find_all("path", "arbitrary", false, {:locale => [], :formats => [:html], :handlers => [:erb]}) + assert_equal 1, templates.size, "expected one template" + assert_equal "this text", templates.first.source + assert_equal "arbitrary/path", templates.first.virtual_path + assert_equal [:html], templates.first.formats + end +end diff --git a/actionview/test/template/testing/null_resolver_test.rb b/actionview/test/template/testing/null_resolver_test.rb new file mode 100644 index 0000000000..55ec36e753 --- /dev/null +++ b/actionview/test/template/testing/null_resolver_test.rb @@ -0,0 +1,12 @@ +require 'abstract_unit' + +class NullResolverTest < ActiveSupport::TestCase + def test_should_return_template_for_any_path + resolver = ActionView::NullResolver.new() + templates = resolver.find_all("path.erb", "arbitrary", false, {:locale => [], :formats => [:html], :handlers => []}) + assert_equal 1, templates.size, "expected one template" + assert_equal "Template generated by Null Resolver", templates.first.source + assert_equal "arbitrary/path.erb", templates.first.virtual_path.to_s + assert_equal [:html], templates.first.formats + end +end diff --git a/actionview/test/template/text_helper_test.rb b/actionview/test/template/text_helper_test.rb new file mode 100644 index 0000000000..1b2234f4e2 --- /dev/null +++ b/actionview/test/template/text_helper_test.rb @@ -0,0 +1,467 @@ +# encoding: utf-8 +require 'abstract_unit' + +class TextHelperTest < ActionView::TestCase + tests ActionView::Helpers::TextHelper + + def setup + super + # This simulates the fact that instance variables are reset every time + # a view is rendered. The cycle helper depends on this behavior. + @_cycles = nil if (defined? @_cycles) + end + + def test_concat + self.output_buffer = 'foo' + assert_equal 'foobar', concat('bar') + assert_equal 'foobar', output_buffer + end + + def test_simple_format_should_be_html_safe + assert simple_format("<b> test with html tags </b>").html_safe? + end + + def test_simple_format + assert_equal "<p></p>", simple_format(nil) + + assert_equal "<p>crazy\n<br /> cross\n<br /> platform linebreaks</p>", simple_format("crazy\r\n cross\r platform linebreaks") + assert_equal "<p>A paragraph</p>\n\n<p>and another one!</p>", simple_format("A paragraph\n\nand another one!") + assert_equal "<p>A paragraph\n<br /> With a newline</p>", simple_format("A paragraph\n With a newline") + + text = "A\nB\nC\nD".freeze + assert_equal "<p>A\n<br />B\n<br />C\n<br />D</p>", simple_format(text) + + text = "A\r\n \nB\n\n\r\n\t\nC\nD".freeze + assert_equal "<p>A\n<br /> \n<br />B</p>\n\n<p>\t\n<br />C\n<br />D</p>", simple_format(text) + + assert_equal %q(<p class="test">This is a classy test</p>), simple_format("This is a classy test", :class => 'test') + assert_equal %Q(<p class="test">para 1</p>\n\n<p class="test">para 2</p>), simple_format("para 1\n\npara 2", :class => 'test') + end + + def test_simple_format_should_sanitize_input_when_sanitize_option_is_not_false + assert_equal "<p><b> test with unsafe string </b></p>", simple_format("<b> test with unsafe string </b><script>code!</script>") + end + + def test_simple_format_should_not_sanitize_input_when_sanitize_option_is_false + assert_equal "<p><b> test with unsafe string </b><script>code!</script></p>", simple_format("<b> test with unsafe string </b><script>code!</script>", {}, :sanitize => false) + end + + def test_simple_format_with_custom_wrapper + assert_equal "<div></div>", simple_format(nil, {}, :wrapper_tag => "div") + end + + def test_simple_format_with_custom_wrapper_and_multi_line_breaks + assert_equal "<div>We want to put a wrapper...</div>\n\n<div>...right there.</div>", simple_format("We want to put a wrapper...\n\n...right there.", {}, :wrapper_tag => "div") + end + + def test_simple_format_should_not_change_the_text_passed + text = "<b>Ok</b><script>code!</script>" + text_clone = text.dup + simple_format(text) + assert_equal text_clone, text + end + + def test_simple_format_does_not_modify_the_html_options_hash + options = { :class => "foobar"} + passed_options = options.dup + simple_format("some text", passed_options) + assert_equal options, passed_options + end + + def test_simple_format_does_not_modify_the_options_hash + options = { :wrapper_tag => :div, :sanitize => false } + passed_options = options.dup + simple_format("some text", {}, passed_options) + assert_equal options, passed_options + end + + def test_truncate + assert_equal "Hello World!", truncate("Hello World!", :length => 12) + assert_equal "Hello Wor...", truncate("Hello World!!", :length => 12) + end + + def test_truncate_should_use_default_length_of_30 + str = "This is a string that will go longer then the default truncate length of 30" + assert_equal str[0...27] + "...", truncate(str) + end + + def test_truncate_with_options_hash + assert_equal "This is a string that wil[...]", truncate("This is a string that will go longer then the default truncate length of 30", :omission => "[...]") + assert_equal "Hello W...", truncate("Hello World!", :length => 10) + assert_equal "Hello[...]", truncate("Hello World!", :omission => "[...]", :length => 10) + assert_equal "Hello[...]", truncate("Hello Big World!", :omission => "[...]", :length => 13, :separator => ' ') + assert_equal "Hello Big[...]", truncate("Hello Big World!", :omission => "[...]", :length => 14, :separator => ' ') + assert_equal "Hello Big[...]", truncate("Hello Big World!", :omission => "[...]", :length => 15, :separator => ' ') + end + + def test_truncate_multibyte + assert_equal "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 ...".force_encoding(Encoding::UTF_8), + truncate("\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 \354\225\204\353\235\274\353\246\254\354\230\244".force_encoding(Encoding::UTF_8), :length => 10) + end + + def test_truncate_does_not_modify_the_options_hash + options = { :length => 10 } + passed_options = options.dup + truncate("some text", passed_options) + assert_equal options, passed_options + end + + def test_truncate_with_link_options + assert_equal "Here is a long test and ...<a href=\"#\">Continue</a>", + truncate("Here is a long test and I need a continue to read link", :length => 27) { link_to 'Continue', '#' } + end + + def test_truncate_should_be_html_safe + assert truncate("Hello World!", :length => 12).html_safe? + end + + def test_truncate_should_escape_the_input + assert_equal "Hello <sc...", truncate("Hello <script>code!</script>World!!", :length => 12) + end + + def test_truncate_should_not_escape_the_input_with_escape_false + assert_equal "Hello <sc...", truncate("Hello <script>code!</script>World!!", :length => 12, :escape => false) + end + + def test_truncate_with_escape_false_should_be_html_safe + truncated = truncate("Hello <script>code!</script>World!!", :length => 12, :escape => false) + assert truncated.html_safe? + end + + def test_truncate_with_block_should_be_html_safe + truncated = truncate("Here's a long test and I need a continue to read link", :length => 27) { link_to 'Continue', '#' } + assert truncated.html_safe? + end + + def test_truncate_with_block_should_escape_the_input + assert_equal "<script>code!</script>He...<a href=\"#\">Continue</a>", + truncate("<script>code!</script>Here's a long test and I need a continue to read link", :length => 27) { link_to 'Continue', '#' } + end + + def test_truncate_with_block_should_not_escape_the_input_with_escape_false + assert_equal "<script>code!</script>He...<a href=\"#\">Continue</a>", + truncate("<script>code!</script>Here's a long test and I need a continue to read link", :length => 27, :escape => false) { link_to 'Continue', '#' } + end + + def test_truncate_with_block_with_escape_false_should_be_html_safe + truncated = truncate("<script>code!</script>Here's a long test and I need a continue to read link", :length => 27, :escape => false) { link_to 'Continue', '#' } + assert truncated.html_safe? + end + + def test_truncate_with_block_should_escape_the_block + assert_equal "Here is a long test and ...<script>alert('foo');</script>", + truncate("Here is a long test and I need a continue to read link", :length => 27) { "<script>alert('foo');</script>" } + end + + def test_highlight_should_be_html_safe + assert highlight("This is a beautiful morning", "beautiful").html_safe? + end + + def test_highlight + assert_equal( + "This is a <mark>beautiful</mark> morning", + highlight("This is a beautiful morning", "beautiful") + ) + + assert_equal( + "This is a <mark>beautiful</mark> morning, but also a <mark>beautiful</mark> day", + highlight("This is a beautiful morning, but also a beautiful day", "beautiful") + ) + + assert_equal( + "This is a <b>beautiful</b> morning, but also a <b>beautiful</b> day", + highlight("This is a beautiful morning, but also a beautiful day", "beautiful", :highlighter => '<b>\1</b>') + ) + + assert_equal( + "This text is not changed because we supplied an empty phrase", + highlight("This text is not changed because we supplied an empty phrase", nil) + ) + + assert_equal ' ', highlight(' ', 'blank text is returned verbatim') + end + + def test_highlight_should_sanitize_input + assert_equal( + "This is a <mark>beautiful</mark> morning", + highlight("This is a beautiful morning<script>code!</script>", "beautiful") + ) + end + + def test_highlight_should_not_sanitize_if_sanitize_option_if_false + assert_equal( + "This is a <mark>beautiful</mark> morning<script>code!</script>", + highlight("This is a beautiful morning<script>code!</script>", "beautiful", :sanitize => false) + ) + end + + def test_highlight_with_regexp + assert_equal( + "This is a <mark>beautiful!</mark> morning", + highlight("This is a beautiful! morning", "beautiful!") + ) + + assert_equal( + "This is a <mark>beautiful! morning</mark>", + highlight("This is a beautiful! morning", "beautiful! morning") + ) + + assert_equal( + "This is a <mark>beautiful? morning</mark>", + highlight("This is a beautiful? morning", "beautiful? morning") + ) + end + + def test_highlight_with_multiple_phrases_in_one_pass + assert_equal %(<em>wow</em> <em>em</em>), highlight('wow em', %w(wow em), :highlighter => '<em>\1</em>') + end + + def test_highlight_with_html + assert_equal( + "<p>This is a <mark>beautiful</mark> morning, but also a <mark>beautiful</mark> day</p>", + highlight("<p>This is a beautiful morning, but also a beautiful day</p>", "beautiful") + ) + assert_equal( + "<p>This is a <em><mark>beautiful</mark></em> morning, but also a <mark>beautiful</mark> day</p>", + highlight("<p>This is a <em>beautiful</em> morning, but also a beautiful day</p>", "beautiful") + ) + assert_equal( + "<p>This is a <em class=\"error\"><mark>beautiful</mark></em> morning, but also a <mark>beautiful</mark> <span class=\"last\">day</span></p>", + highlight("<p>This is a <em class=\"error\">beautiful</em> morning, but also a beautiful <span class=\"last\">day</span></p>", "beautiful") + ) + assert_equal( + "<p class=\"beautiful\">This is a <mark>beautiful</mark> morning, but also a <mark>beautiful</mark> day</p>", + highlight("<p class=\"beautiful\">This is a beautiful morning, but also a beautiful day</p>", "beautiful") + ) + assert_equal( + "<p>This is a <mark>beautiful</mark> <a href=\"http://example.com/beautiful#top?what=beautiful%20morning&when=now+then\">morning</a>, but also a <mark>beautiful</mark> day</p>", + highlight("<p>This is a beautiful <a href=\"http://example.com/beautiful\#top?what=beautiful%20morning&when=now+then\">morning</a>, but also a beautiful day</p>", "beautiful") + ) + assert_equal( + "<div>abc <b>div</b></div>", + highlight("<div>abc div</div>", "div", :highlighter => '<b>\1</b>') + ) + end + + def test_highlight_does_not_modify_the_options_hash + options = { :highlighter => '<b>\1</b>', :sanitize => false } + passed_options = options.dup + highlight("<div>abc div</div>", "div", passed_options) + assert_equal options, passed_options + end + + def test_excerpt + assert_equal("...is a beautiful morn...", excerpt("This is a beautiful morning", "beautiful", :radius => 5)) + assert_equal("This is a...", excerpt("This is a beautiful morning", "this", :radius => 5)) + assert_equal("...iful morning", excerpt("This is a beautiful morning", "morning", :radius => 5)) + assert_nil excerpt("This is a beautiful morning", "day") + end + + def test_excerpt_should_not_be_html_safe + assert !excerpt('This is a beautiful! morning', 'beautiful', :radius => 5).html_safe? + end + + def test_excerpt_in_borderline_cases + assert_equal("", excerpt("", "", :radius => 0)) + assert_equal("a", excerpt("a", "a", :radius => 0)) + assert_equal("...b...", excerpt("abc", "b", :radius => 0)) + assert_equal("abc", excerpt("abc", "b", :radius => 1)) + assert_equal("abc...", excerpt("abcd", "b", :radius => 1)) + assert_equal("...abc", excerpt("zabc", "b", :radius => 1)) + assert_equal("...abc...", excerpt("zabcd", "b", :radius => 1)) + assert_equal("zabcd", excerpt("zabcd", "b", :radius => 2)) + + # excerpt strips the resulting string before ap-/prepending excerpt_string. + # whether this behavior is meaningful when excerpt_string is not to be + # appended is questionable. + assert_equal("zabcd", excerpt(" zabcd ", "b", :radius => 4)) + assert_equal("...abc...", excerpt("z abc d", "b", :radius => 1)) + end + + def test_excerpt_with_regex + assert_equal('...is a beautiful! mor...', excerpt('This is a beautiful! morning', 'beautiful', :radius => 5)) + assert_equal('...is a beautiful? mor...', excerpt('This is a beautiful? morning', 'beautiful', :radius => 5)) + end + + def test_excerpt_with_omission + assert_equal("[...]is a beautiful morn[...]", excerpt("This is a beautiful morning", "beautiful", :omission => "[...]",:radius => 5)) + assert_equal( + "This is the ultimate supercalifragilisticexpialidoceous very looooooooooooooooooong looooooooooooong beautiful morning with amazing sunshine and awesome tempera[...]", + excerpt("This is the ultimate supercalifragilisticexpialidoceous very looooooooooooooooooong looooooooooooong beautiful morning with amazing sunshine and awesome temperatures. So what are you gonna do about it?", "very", + :omission => "[...]") + ) + end + + def test_excerpt_with_utf8 + assert_equal("...\357\254\203ciency could not be...".force_encoding(Encoding::UTF_8), excerpt("That's why e\357\254\203ciency could not be helped".force_encoding(Encoding::UTF_8), 'could', :radius => 8)) + end + + def test_excerpt_does_not_modify_the_options_hash + options = { :omission => "[...]",:radius => 5 } + passed_options = options.dup + excerpt("This is a beautiful morning", "beautiful", passed_options) + assert_equal options, passed_options + end + + def test_excerpt_with_separator + options = { :separator => ' ', :radius => 1 } + assert_equal('...a very beautiful...', excerpt('This is a very beautiful morning', 'very', options)) + assert_equal('This is...', excerpt('This is a very beautiful morning', 'this', options)) + assert_equal('...beautiful morning', excerpt('This is a very beautiful morning', 'morning', options)) + + options = { :separator => "\n", :radius => 0 } + assert_equal("...very long...", excerpt("my very\nvery\nvery long\nstring", 'long', options)) + + options = { :separator => "\n", :radius => 1 } + assert_equal("...very\nvery long\nstring", excerpt("my very\nvery\nvery long\nstring", 'long', options)) + end + + def test_word_wrap + assert_equal("my very very\nvery long\nstring", word_wrap("my very very very long string", :line_width => 15)) + end + + def test_word_wrap_with_extra_newlines + assert_equal("my very very\nvery long\nstring\n\nwith another\nline", word_wrap("my very very very long string\n\nwith another line", :line_width => 15)) + end + + def test_word_wrap_does_not_modify_the_options_hash + options = { :line_width => 15 } + passed_options = options.dup + word_wrap("some text", passed_options) + assert_equal options, passed_options + end + + def test_pluralization + assert_equal("1 count", pluralize(1, "count")) + assert_equal("2 counts", pluralize(2, "count")) + assert_equal("1 count", pluralize('1', "count")) + assert_equal("2 counts", pluralize('2', "count")) + assert_equal("1,066 counts", pluralize('1,066', "count")) + assert_equal("1.25 counts", pluralize('1.25', "count")) + assert_equal("1.0 count", pluralize('1.0', "count")) + assert_equal("1.00 count", pluralize('1.00', "count")) + assert_equal("2 counters", pluralize(2, "count", "counters")) + assert_equal("0 counters", pluralize(nil, "count", "counters")) + assert_equal("2 people", pluralize(2, "person")) + assert_equal("10 buffaloes", pluralize(10, "buffalo")) + assert_equal("1 berry", pluralize(1, "berry")) + assert_equal("12 berries", pluralize(12, "berry")) + end + + def test_cycle_class + value = Cycle.new("one", 2, "3") + assert_equal("one", value.to_s) + assert_equal("2", value.to_s) + assert_equal("3", value.to_s) + assert_equal("one", value.to_s) + value.reset + assert_equal("one", value.to_s) + assert_equal("2", value.to_s) + assert_equal("3", value.to_s) + end + + def test_cycle_class_with_no_arguments + assert_raise(ArgumentError) { Cycle.new } + end + + def test_cycle + assert_equal("one", cycle("one", 2, "3")) + assert_equal("2", cycle("one", 2, "3")) + assert_equal("3", cycle("one", 2, "3")) + assert_equal("one", cycle("one", 2, "3")) + assert_equal("2", cycle("one", 2, "3")) + assert_equal("3", cycle("one", 2, "3")) + end + + def test_cycle_with_no_arguments + assert_raise(ArgumentError) { cycle } + end + + def test_cycle_resets_with_new_values + assert_equal("even", cycle("even", "odd")) + assert_equal("odd", cycle("even", "odd")) + assert_equal("even", cycle("even", "odd")) + assert_equal("1", cycle(1, 2, 3)) + assert_equal("2", cycle(1, 2, 3)) + assert_equal("3", cycle(1, 2, 3)) + assert_equal("1", cycle(1, 2, 3)) + end + + def test_named_cycles + assert_equal("1", cycle(1, 2, 3, :name => "numbers")) + assert_equal("red", cycle("red", "blue", :name => "colors")) + assert_equal("2", cycle(1, 2, 3, :name => "numbers")) + assert_equal("blue", cycle("red", "blue", :name => "colors")) + assert_equal("3", cycle(1, 2, 3, :name => "numbers")) + assert_equal("red", cycle("red", "blue", :name => "colors")) + end + + def test_current_cycle_with_default_name + cycle("even","odd") + assert_equal "even", current_cycle + cycle("even","odd") + assert_equal "odd", current_cycle + cycle("even","odd") + assert_equal "even", current_cycle + end + + def test_current_cycle_with_named_cycles + cycle("red", "blue", :name => "colors") + assert_equal "red", current_cycle("colors") + cycle("red", "blue", :name => "colors") + assert_equal "blue", current_cycle("colors") + cycle("red", "blue", :name => "colors") + assert_equal "red", current_cycle("colors") + end + + def test_current_cycle_safe_call + assert_nothing_raised { current_cycle } + assert_nothing_raised { current_cycle("colors") } + end + + def test_current_cycle_with_more_than_two_names + cycle(1,2,3) + assert_equal "1", current_cycle + cycle(1,2,3) + assert_equal "2", current_cycle + cycle(1,2,3) + assert_equal "3", current_cycle + cycle(1,2,3) + assert_equal "1", current_cycle + end + + def test_default_named_cycle + assert_equal("1", cycle(1, 2, 3)) + assert_equal("2", cycle(1, 2, 3, :name => "default")) + assert_equal("3", cycle(1, 2, 3)) + end + + def test_reset_cycle + assert_equal("1", cycle(1, 2, 3)) + assert_equal("2", cycle(1, 2, 3)) + reset_cycle + assert_equal("1", cycle(1, 2, 3)) + end + + def test_reset_unknown_cycle + reset_cycle("colors") + end + + def test_recet_named_cycle + assert_equal("1", cycle(1, 2, 3, :name => "numbers")) + assert_equal("red", cycle("red", "blue", :name => "colors")) + reset_cycle("numbers") + assert_equal("1", cycle(1, 2, 3, :name => "numbers")) + assert_equal("blue", cycle("red", "blue", :name => "colors")) + assert_equal("2", cycle(1, 2, 3, :name => "numbers")) + assert_equal("red", cycle("red", "blue", :name => "colors")) + end + + def test_cycle_no_instance_variable_clashes + @cycles = %w{Specialized Fuji Giant} + assert_equal("red", cycle("red", "blue")) + assert_equal("blue", cycle("red", "blue")) + assert_equal("red", cycle("red", "blue")) + assert_equal(%w{Specialized Fuji Giant}, @cycles) + end +end diff --git a/actionview/test/template/translation_helper_test.rb b/actionview/test/template/translation_helper_test.rb new file mode 100644 index 0000000000..d496dbb35e --- /dev/null +++ b/actionview/test/template/translation_helper_test.rb @@ -0,0 +1,138 @@ +require 'abstract_unit' + +class TranslationHelperTest < ActiveSupport::TestCase + include ActionView::Helpers::TagHelper + include ActionView::Helpers::TranslationHelper + + attr_reader :request, :view + + def setup + I18n.backend.store_translations(:en, + :translations => { + :templates => { + :found => { :foo => 'Foo' }, + :array => { :foo => { :bar => 'Foo Bar' } }, + :default => { :foo => 'Foo' } + }, + :foo => 'Foo', + :hello => '<a>Hello World</a>', + :html => '<a>Hello World</a>', + :hello_html => '<a>Hello World</a>', + :interpolated_html => '<a>Hello %{word}</a>', + :array_html => %w(foo bar), + :array => %w(foo bar), + :count_html => { + :one => '<a>One %{count}</a>', + :other => '<a>Other %{count}</a>' + } + } + ) + @view = ::ActionView::Base.new(ActionController::Base.view_paths, {}) + end + + def test_delegates_to_i18n_setting_the_rescue_format_option_to_html + I18n.expects(:translate).with(:foo, :locale => 'en', :rescue_format => :html).returns("") + translate :foo, :locale => 'en' + end + + def test_delegates_localize_to_i18n + @time = Time.utc(2008, 7, 8, 12, 18, 38) + I18n.expects(:localize).with(@time) + localize @time + end + + def test_returns_missing_translation_message_wrapped_into_span + expected = '<span class="translation_missing" title="translation missing: en.translations.missing">Missing</span>' + assert_equal expected, translate(:"translations.missing") + 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? + 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? + end + + def test_translation_returning_an_array + expected = %w(foo bar) + assert_equal expected, translate(:"translations.array") + end + + def test_finds_translation_scoped_by_partial + assert_equal 'Foo', view.render(:file => 'translations/templates/found').strip + end + + def test_finds_array_of_translations_scoped_by_partial + assert_equal 'Foo Bar', @view.render(:file => 'translations/templates/array').strip + end + + def test_default_lookup_scoped_by_partial + assert_equal 'Foo', view.render(:file => 'translations/templates/default').strip + end + + def test_missing_translation_scoped_by_partial + expected = '<span class="translation_missing" title="translation missing: en.translations.templates.missing.missing">Missing</span>' + assert_equal expected, view.render(:file => 'translations/templates/missing').strip + end + + def test_translate_does_not_mark_plain_text_as_safe_html + assert_equal false, translate(:'translations.hello').html_safe? + end + + def test_translate_marks_translations_named_html_as_safe_html + assert translate(:'translations.html').html_safe? + end + + def test_translate_marks_translations_with_a_html_suffix_as_safe_html + assert translate(:'translations.hello_html').html_safe? + end + + def test_translate_escapes_interpolations_in_translations_with_a_html_suffix + assert_equal '<a>Hello <World></a>', translate(:'translations.interpolated_html', :word => '<World>') + assert_equal '<a>Hello <World></a>', translate(:'translations.interpolated_html', :word => stub(:to_s => "<World>")) + end + + def test_translate_with_html_count + assert_equal '<a>One 1</a>', translate(:'translations.count_html', :count => 1) + assert_equal '<a>Other 2</a>', translate(:'translations.count_html', :count => 2) + assert_equal '<a>Other <One></a>', translate(:'translations.count_html', :count => '<One>') + end + + def test_translation_returning_an_array_ignores_html_suffix + assert_equal ["foo", "bar"], translate(:'translations.array_html') + end + + def test_translate_with_default_named_html + translation = translate(:'translations.missing', :default => :'translations.hello_html') + assert_equal '<a>Hello World</a>', translation + assert_equal true, translation.html_safe? + 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 + assert_equal true, translation.html_safe? + end + + def test_translate_with_last_default_named_html + translation = translate(:'translations.missing', :default => [:'translations.missing', :'translations.hello_html']) + assert_equal '<a>Hello World</a>', translation + assert_equal true, 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_array_of_string_defaults + translation = translate(:'translations.missing', default: ['A Generic String', 'Second generic string']) + assert_equal 'A Generic String', translation + end +end diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb new file mode 100644 index 0000000000..8373d7f992 --- /dev/null +++ b/actionview/test/template/url_helper_test.rb @@ -0,0 +1,758 @@ +# encoding: utf-8 +require 'abstract_unit' + +class UrlHelperTest < ActiveSupport::TestCase + + # In a few cases, the helper proxies to 'controller' + # or request. + # + # In those cases, we'll set up a simple mock + attr_accessor :controller, :request + + cattr_accessor :request_forgery + self.request_forgery = false + + routes = ActionDispatch::Routing::RouteSet.new + routes.draw do + get "/" => "foo#bar" + get "/other" => "foo#other" + get "/article/:id" => "foo#article", :as => :article + end + + include ActionView::Helpers::UrlHelper + include routes.url_helpers + + include ActionView::Helpers::JavaScriptHelper + include ActionDispatch::Assertions::DomAssertions + include ActionView::Context + include RenderERBUtils + + setup :_prepare_context + + def hash_for(options = {}) + { controller: "foo", action: "bar" }.merge!(options) + end + alias url_hash hash_for + + def test_url_for_does_not_escape_urls + assert_equal "/?a=b&c=d", url_for(hash_for(a: :b, c: :d)) + end + + def test_url_for_with_back + referer = 'http://www.example.com/referer' + @controller = Struct.new(:request).new(Struct.new(:env).new("HTTP_REFERER" => referer)) + + assert_equal 'http://www.example.com/referer', url_for(:back) + end + + def test_url_for_with_back_and_no_referer + @controller = Struct.new(:request).new(Struct.new(:env).new({})) + assert_equal 'javascript:history.back()', url_for(:back) + end + + def test_button_to_with_straight_url + assert_dom_equal %{<form method="post" action="http://www.example.com" class="button_to"><div><input type="submit" value="Hello" /></div></form>}, button_to("Hello", "http://www.example.com") + end + + def test_button_to_with_straight_url_and_request_forgery + self.request_forgery = true + + assert_dom_equal( + %{<form method="post" action="http://www.example.com" class="button_to"><div><input type="submit" value="Hello" /><input name="form_token" type="hidden" value="secret" /></div></form>}, + button_to("Hello", "http://www.example.com") + ) + ensure + self.request_forgery = false + end + + def test_button_to_with_form_class + assert_dom_equal %{<form method="post" action="http://www.example.com" class="custom-class"><div><input type="submit" value="Hello" /></div></form>}, button_to("Hello", "http://www.example.com", form_class: 'custom-class') + end + + def test_button_to_with_form_class_escapes + assert_dom_equal %{<form method="post" action="http://www.example.com" class="<script>evil_js</script>"><div><input type="submit" value="Hello" /></div></form>}, button_to("Hello", "http://www.example.com", form_class: '<script>evil_js</script>') + end + + def test_button_to_with_query + assert_dom_equal %{<form method="post" action="http://www.example.com/q1=v1&q2=v2" class="button_to"><div><input type="submit" value="Hello" /></div></form>}, button_to("Hello", "http://www.example.com/q1=v1&q2=v2") + end + + def test_button_to_with_html_safe_URL + assert_dom_equal %{<form method="post" action="http://www.example.com/q1=v1&q2=v2" class="button_to"><div><input type="submit" value="Hello" /></div></form>}, button_to("Hello", "http://www.example.com/q1=v1&q2=v2".html_safe) + end + + def test_button_to_with_query_and_no_name + assert_dom_equal %{<form method="post" action="http://www.example.com?q1=v1&q2=v2" class="button_to"><div><input type="submit" value="http://www.example.com?q1=v1&q2=v2" /></div></form>}, button_to(nil, "http://www.example.com?q1=v1&q2=v2") + end + + def test_button_to_with_javascript_confirm + assert_dom_equal( + %{<form method="post" action="http://www.example.com" class="button_to"><div><input data-confirm="Are you sure?" type="submit" value="Hello" /></div></form>}, + button_to("Hello", "http://www.example.com", data: { confirm: "Are you sure?" }) + ) + end + + def test_button_to_with_javascript_disable_with + assert_dom_equal( + %{<form method="post" action="http://www.example.com" class="button_to"><div><input data-disable-with="Greeting..." type="submit" value="Hello" /></div></form>}, + button_to("Hello", "http://www.example.com", data: { disable_with: "Greeting..." }) + ) + end + + def test_button_to_with_remote_and_form_options + assert_dom_equal( + %{<form method="post" action="http://www.example.com" class="custom-class" data-remote="true" data-type="json"><div><input type="submit" value="Hello" /></div></form>}, + button_to("Hello", "http://www.example.com", remote: true, form: { class: "custom-class", "data-type" => "json" }) + ) + end + + def test_button_to_with_remote_and_javascript_confirm + assert_dom_equal( + %{<form method="post" action="http://www.example.com" class="button_to" data-remote="true"><div><input data-confirm="Are you sure?" type="submit" value="Hello" /></div></form>}, + button_to("Hello", "http://www.example.com", remote: true, data: { confirm: "Are you sure?" }) + ) + end + + def test_button_to_with_remote_and_javascript_disable_with + assert_dom_equal( + %{<form method="post" action="http://www.example.com" class="button_to" data-remote="true"><div><input data-disable-with="Greeting..." type="submit" value="Hello" /></div></form>}, + button_to("Hello", "http://www.example.com", remote: true, data: { disable_with: "Greeting..." }) + ) + end + + def test_button_to_with_remote_false + assert_dom_equal( + %{<form method="post" action="http://www.example.com" class="button_to"><div><input type="submit" value="Hello" /></div></form>}, + button_to("Hello", "http://www.example.com", remote: false) + ) + end + + def test_button_to_enabled_disabled + assert_dom_equal( + %{<form method="post" action="http://www.example.com" class="button_to"><div><input type="submit" value="Hello" /></div></form>}, + button_to("Hello", "http://www.example.com", disabled: false) + ) + assert_dom_equal( + %{<form method="post" action="http://www.example.com" class="button_to"><div><input disabled="disabled" type="submit" value="Hello" /></div></form>}, + button_to("Hello", "http://www.example.com", disabled: true) + ) + end + + def test_button_to_with_method_delete + assert_dom_equal( + %{<form method="post" action="http://www.example.com" class="button_to"><div><input type="hidden" name="_method" value="delete" /><input type="submit" value="Hello" /></div></form>}, + button_to("Hello", "http://www.example.com", method: :delete) + ) + end + + def test_button_to_with_method_get + assert_dom_equal( + %{<form method="get" action="http://www.example.com" class="button_to"><div><input type="submit" value="Hello" /></div></form>}, + button_to("Hello", "http://www.example.com", method: :get) + ) + end + + def test_button_to_with_block + assert_dom_equal( + %{<form method="post" action="http://www.example.com" class="button_to"><div><button type="submit"><span>Hello</span></button></div></form>}, + button_to("http://www.example.com") { content_tag(:span, 'Hello') } + ) + end + + def test_link_tag_with_straight_url + assert_dom_equal %{<a href="http://www.example.com">Hello</a>}, link_to("Hello", "http://www.example.com") + end + + def test_link_tag_without_host_option + assert_dom_equal(%{<a href="/">Test Link</a>}, link_to('Test Link', url_hash)) + end + + def test_link_tag_with_host_option + hash = hash_for(host: "www.example.com") + expected = %{<a href="http://www.example.com/">Test Link</a>} + assert_dom_equal(expected, link_to('Test Link', hash)) + end + + def test_link_tag_with_query + expected = %{<a href="http://www.example.com?q1=v1&q2=v2">Hello</a>} + assert_dom_equal expected, link_to("Hello", "http://www.example.com?q1=v1&q2=v2") + end + + def test_link_tag_with_query_and_no_name + expected = %{<a href="http://www.example.com?q1=v1&q2=v2">http://www.example.com?q1=v1&q2=v2</a>} + assert_dom_equal expected, link_to(nil, "http://www.example.com?q1=v1&q2=v2") + end + + def test_link_tag_with_back + env = {"HTTP_REFERER" => "http://www.example.com/referer"} + @controller = Struct.new(:request).new(Struct.new(:env).new(env)) + expected = %{<a href="#{env["HTTP_REFERER"]}">go back</a>} + assert_dom_equal expected, link_to('go back', :back) + end + + def test_link_tag_with_back_and_no_referer + @controller = Struct.new(:request).new(Struct.new(:env).new({})) + link = link_to('go back', :back) + assert_dom_equal %{<a href="javascript:history.back()">go back</a>}, link + end + + def test_link_tag_with_img + link = link_to("<img src='/favicon.jpg' />".html_safe, "/") + expected = %{<a href="/"><img src='/favicon.jpg' /></a>} + assert_dom_equal expected, link + end + + def test_link_with_nil_html_options + link = link_to("Hello", url_hash, nil) + assert_dom_equal %{<a href="/">Hello</a>}, link + end + + def test_link_tag_with_custom_onclick + link = link_to("Hello", "http://www.example.com", onclick: "alert('yay!')") + expected = %{<a href="http://www.example.com" onclick="alert('yay!')">Hello</a>} + assert_dom_equal expected, link + end + + def test_link_tag_with_javascript_confirm + assert_dom_equal( + %{<a href="http://www.example.com" data-confirm="Are you sure?">Hello</a>}, + link_to("Hello", "http://www.example.com", data: { confirm: "Are you sure?" }) + ) + assert_dom_equal( + %{<a href="http://www.example.com" data-confirm="You cant possibly be sure, can you?">Hello</a>}, + link_to("Hello", "http://www.example.com", data: { confirm: "You cant possibly be sure, can you?" }) + ) + assert_dom_equal( + %{<a href="http://www.example.com" data-confirm="You cant possibly be sure,\n can you?">Hello</a>}, + link_to("Hello", "http://www.example.com", data: { confirm: "You cant possibly be sure,\n can you?" }) + ) + end + + def test_link_to_with_remote + assert_dom_equal( + %{<a href="http://www.example.com" data-remote="true">Hello</a>}, + link_to("Hello", "http://www.example.com", remote: true) + ) + end + + def test_link_to_with_remote_false + assert_dom_equal( + %{<a href="http://www.example.com">Hello</a>}, + link_to("Hello", "http://www.example.com", remote: false) + ) + end + + def test_link_to_with_symbolic_remote_in_non_html_options + assert_dom_equal( + %{<a href="/" data-remote="true">Hello</a>}, + link_to("Hello", hash_for(remote: true), {}) + ) + end + + def test_link_to_with_string_remote_in_non_html_options + assert_dom_equal( + %{<a href="/" data-remote="true">Hello</a>}, + link_to("Hello", hash_for('remote' => true), {}) + ) + end + + def test_link_tag_using_post_javascript + assert_dom_equal( + %{<a href="http://www.example.com" data-method="post" rel="nofollow">Hello</a>}, + link_to("Hello", "http://www.example.com", method: :post) + ) + end + + def test_link_tag_using_delete_javascript + assert_dom_equal( + %{<a href="http://www.example.com" rel="nofollow" data-method="delete">Destroy</a>}, + link_to("Destroy", "http://www.example.com", method: :delete) + ) + end + + def test_link_tag_using_delete_javascript_and_href + assert_dom_equal( + %{<a href="\#" rel="nofollow" data-method="delete">Destroy</a>}, + link_to("Destroy", "http://www.example.com", method: :delete, href: '#') + ) + end + + def test_link_tag_using_post_javascript_and_rel + assert_dom_equal( + %{<a href="http://www.example.com" data-method="post" rel="example nofollow">Hello</a>}, + link_to("Hello", "http://www.example.com", method: :post, rel: 'example') + ) + end + + def test_link_tag_using_post_javascript_and_confirm + assert_dom_equal( + %{<a href="http://www.example.com" data-method="post" rel="nofollow" data-confirm="Are you serious?">Hello</a>}, + link_to("Hello", "http://www.example.com", method: :post, data: { confirm: "Are you serious?" }) + ) + end + + def test_link_tag_using_delete_javascript_and_href_and_confirm + assert_dom_equal( + %{<a href="\#" rel="nofollow" data-confirm="Are you serious?" data-method="delete">Destroy</a>}, + link_to("Destroy", "http://www.example.com", method: :delete, href: '#', data: { confirm: "Are you serious?" }) + ) + end + + def test_link_tag_with_block + assert_dom_equal %{<a href="/"><span>Example site</span></a>}, + link_to('/') { content_tag(:span, 'Example site') } + end + + def test_link_tag_with_block_and_html_options + assert_dom_equal %{<a class="special" href="/"><span>Example site</span></a>}, + link_to('/', class: "special") { content_tag(:span, 'Example site') } + end + + def test_link_tag_using_block_in_erb + out = render_erb %{<%= link_to('/') do %>Example site<% end %>} + assert_equal '<a href="/">Example site</a>', out + end + + def test_link_tag_with_html_safe_string + assert_dom_equal( + %{<a href="/article/Gerd_M%C3%BCller">Gerd Müller</a>}, + link_to("Gerd Müller", article_path("Gerd_Müller".html_safe)) + ) + end + + def test_link_tag_escapes_content + assert_dom_equal %{<a href="/">Malicious <script>content</script></a>}, + link_to("Malicious <script>content</script>", "/") + end + + def test_link_tag_does_not_escape_html_safe_content + assert_dom_equal %{<a href="/">Malicious <script>content</script></a>}, + link_to("Malicious <script>content</script>".html_safe, "/") + end + + def test_link_to_unless + assert_equal "Showing", link_to_unless(true, "Showing", url_hash) + + assert_dom_equal %{<a href="/">Listing</a>}, + link_to_unless(false, "Listing", url_hash) + + assert_equal "Showing", link_to_unless(true, "Showing", url_hash) + + assert_equal "<strong>Showing</strong>", + link_to_unless(true, "Showing", url_hash) { |name| + "<strong>#{name}</strong>".html_safe + } + + assert_equal "test", + link_to_unless(true, "Showing", url_hash) { + "test" + } + + assert_equal %{<b>Showing</b>}, link_to_unless(true, "<b>Showing</b>", url_hash) + assert_equal %{<a href="/"><b>Showing</b></a>}, link_to_unless(false, "<b>Showing</b>", url_hash) + assert_equal %{<b>Showing</b>}, link_to_unless(true, "<b>Showing</b>".html_safe, url_hash) + assert_equal %{<a href="/"><b>Showing</b></a>}, link_to_unless(false, "<b>Showing</b>".html_safe, url_hash) + end + + def test_link_to_if + assert_equal "Showing", link_to_if(false, "Showing", url_hash) + assert_dom_equal %{<a href="/">Listing</a>}, link_to_if(true, "Listing", url_hash) + assert_equal "Showing", link_to_if(false, "Showing", url_hash) + end + + def request_for_url(url, opts = {}) + env = Rack::MockRequest.env_for("http://www.example.com#{url}", opts) + ActionDispatch::Request.new(env) + end + + def test_current_page_with_http_head_method + @request = request_for_url("/", :method => :head) + assert current_page?(url_hash) + assert current_page?("http://www.example.com/") + end + + def test_current_page_with_simple_url + @request = request_for_url("/") + assert current_page?(url_hash) + assert current_page?("http://www.example.com/") + end + + def test_current_page_ignoring_params + @request = request_for_url("/?order=desc&page=1") + + assert current_page?(url_hash) + assert current_page?("http://www.example.com/") + end + + def test_current_page_with_params_that_match + @request = request_for_url("/?order=desc&page=1") + + assert current_page?(hash_for(order: "desc", page: "1")) + assert current_page?("http://www.example.com/?order=desc&page=1") + end + + def test_current_page_with_not_get_verb + @request = request_for_url("/events", method: :post) + + assert !current_page?('/events') + end + + def test_link_unless_current + @request = request_for_url("/") + + assert_equal "Showing", + link_to_unless_current("Showing", url_hash) + assert_equal "Showing", + link_to_unless_current("Showing", "http://www.example.com/") + + @request = request_for_url("/?order=desc") + + assert_equal "Showing", + link_to_unless_current("Showing", url_hash) + assert_equal "Showing", + link_to_unless_current("Showing", "http://www.example.com/") + + @request = request_for_url("/?order=desc&page=1") + + assert_equal "Showing", + link_to_unless_current("Showing", hash_for(order: 'desc', page: '1')) + assert_equal "Showing", + link_to_unless_current("Showing", "http://www.example.com/?order=desc&page=1") + + @request = request_for_url("/?order=desc") + + assert_equal %{<a href="/?order=asc">Showing</a>}, + link_to_unless_current("Showing", hash_for(order: :asc)) + assert_equal %{<a href="http://www.example.com/?order=asc">Showing</a>}, + link_to_unless_current("Showing", "http://www.example.com/?order=asc") + + @request = request_for_url("/?order=desc") + assert_equal %{<a href="/?order=desc&page=2\">Showing</a>}, + link_to_unless_current("Showing", hash_for(order: "desc", page: 2)) + assert_equal %{<a href="http://www.example.com/?order=desc&page=2">Showing</a>}, + link_to_unless_current("Showing", "http://www.example.com/?order=desc&page=2") + + @request = request_for_url("/show") + + assert_equal %{<a href="/">Listing</a>}, + link_to_unless_current("Listing", url_hash) + assert_equal %{<a href="http://www.example.com/">Listing</a>}, + link_to_unless_current("Listing", "http://www.example.com/") + 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") + assert_dom_equal( + %{<a class="admin" href="mailto:david@loudthinking.com">David Heinemeier Hansson</a>}, + mail_to("david@loudthinking.com", "David Heinemeier Hansson", "class" => "admin") + ) + assert_equal mail_to("david@loudthinking.com", "David Heinemeier Hansson", "class" => "admin"), + mail_to("david@loudthinking.com", "David Heinemeier Hansson", class: "admin") + end + + def test_mail_with_options + assert_dom_equal( + %{<a href="mailto:me@example.com?cc=ccaddress%40example.com&bcc=bccaddress%40example.com&body=This%20is%20the%20body%20of%20the%20message.&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.") + ) + end + + def test_mail_to_with_img + assert_dom_equal %{<a href="mailto:feedback@example.com"><img src="/feedback.png" /></a>}, + mail_to('feedback@example.com', '<img src="/feedback.png" />'.html_safe) + end + + def test_mail_to_returns_html_safe_string + assert mail_to("david@loudthinking.com").html_safe? + end + + def test_mail_to_with_block + assert_dom_equal %{<a href="mailto:me@example.com"><span>Email me</span></a>}, + mail_to('me@example.com') { content_tag(:span, 'Email me') } + end + + def test_mail_to_with_block_and_options + assert_dom_equal %{<a class="special" href="mailto:me@example.com?cc=ccaddress%40example.com"><span>Email me</span></a>}, + mail_to('me@example.com', cc: "ccaddress@example.com", class: "special") { content_tag(:span, 'Email me') } + end + + def test_mail_to_does_not_modify_html_options_hash + options = { class: 'special' } + mail_to 'me@example.com', 'ME!', options + assert_equal({ class: 'special' }, options) + end + + def protect_against_forgery? + self.request_forgery + end + + def form_authenticity_token + "secret" + end + + def request_forgery_protection_token + "form_token" + end + + private + def sort_query_string_params(uri) + path, qs = uri.split('?') + qs = qs.split('&').sort.join('&') if qs + qs ? "#{path}?#{qs}" : path + end +end + +class UrlHelperControllerTest < ActionController::TestCase + class UrlHelperController < ActionController::Base + test_routes do + get 'url_helper_controller_test/url_helper/show/:id', + to: 'url_helper_controller_test/url_helper#show', + as: :show + + get 'url_helper_controller_test/url_helper/profile/:name', + to: 'url_helper_controller_test/url_helper#show', + as: :profile + + get 'url_helper_controller_test/url_helper/show_named_route', + to: 'url_helper_controller_test/url_helper#show_named_route', + as: :show_named_route + + get "/:controller(/:action(/:id))" + + get 'url_helper_controller_test/url_helper/normalize_recall_params', + to: UrlHelperController.action(:normalize_recall), + as: :normalize_recall_params + + get '/url_helper_controller_test/url_helper/override_url_helper/default', + to: 'url_helper_controller_test/url_helper#override_url_helper', + as: :override_url_helper + end + + def show + if params[:name] + render inline: 'ok' + else + redirect_to profile_path(params[:id]) + end + end + + def show_url_for + render inline: "<%= url_for controller: 'url_helper_controller_test/url_helper', action: 'show_url_for' %>" + end + + def show_overridden_url_for + render inline: "<%= url_for params.merge(controller: 'url_helper_controller_test/url_helper', action: 'show_url_for') %>" + end + + def show_named_route + render inline: "<%= show_named_route_#{params[:kind]} %>" + end + + def nil_url_for + render inline: '<%= url_for(nil) %>' + end + + def normalize_recall_params + render inline: '<%= normalize_recall_params_path %>' + end + + def recall_params_not_changed + render inline: '<%= url_for(action: :show_url_for) %>' + end + + def override_url_helper + render inline: '<%= override_url_helper_path %>' + end + + def override_url_helper_path + '/url_helper_controller_test/url_helper/override_url_helper/override' + end + helper_method :override_url_helper_path + end + + tests UrlHelperController + + def test_url_for_shows_only_path + get :show_url_for + assert_equal '/url_helper_controller_test/url_helper/show_url_for', @response.body + end + + def test_overridden_url_for_shows_only_path + get :show_overridden_url_for + assert_equal '/url_helper_controller_test/url_helper/show_url_for', @response.body + end + + def test_named_route_url_shows_host_and_path + get :show_named_route, 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' + assert_equal '/url_helper_controller_test/url_helper/show_named_route', @response.body + end + + def test_url_for_nil_returns_current_path + get :nil_url_for + assert_equal '/url_helper_controller_test/url_helper/nil_url_for', @response.body + end + + def test_named_route_should_show_host_and_path_using_controller_default_url_options + class << @controller + def default_url_options + { host: 'testtwo.host' } + end + end + + get :show_named_route, kind: 'url' + assert_equal 'http://testtwo.host/url_helper_controller_test/url_helper/show_named_route', @response.body + end + + def test_recall_params_should_be_normalized + get :normalize_recall_params + assert_equal '/url_helper_controller_test/url_helper/normalize_recall_params', @response.body + end + + def test_recall_params_should_not_be_changed + get :recall_params_not_changed + assert_equal '/url_helper_controller_test/url_helper/show_url_for', @response.body + end + + def test_recall_params_should_normalize_id + get :show, 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' + assert_equal 'ok', @response.body + end + + def test_url_helper_can_be_overridden + get :override_url_helper + assert_equal '/url_helper_controller_test/url_helper/override_url_helper/override', @response.body + end +end + +class TasksController < ActionController::Base + test_routes do + resources :tasks + end + + def index + render_default + end + + def show + render_default + end + + protected + def render_default + render inline: "<%= link_to_unless_current('tasks', tasks_path) %>\n" + + "<%= link_to_unless_current('tasks', tasks_url) %>" + end +end + +class LinkToUnlessCurrentWithControllerTest < ActionController::TestCase + tests TasksController + + def test_link_to_unless_current_to_current + get :index + assert_equal "tasks\ntasks", @response.body + end + + def test_link_to_unless_current_shows_link + get :show, id: 1 + assert_equal %{<a href="/tasks">tasks</a>\n} + + %{<a href="#{@request.protocol}#{@request.host_with_port}/tasks">tasks</a>}, + @response.body + end +end + +class Session + extend ActiveModel::Naming + include ActiveModel::Conversion + attr_accessor :id, :workshop_id + + def initialize(id) + @id = id + end + + def persisted? + id.present? + end + + def to_s + id.to_s + end +end + +class WorkshopsController < ActionController::Base + test_routes do + resources :workshops do + resources :sessions + end + end + + def index + @workshop = Workshop.new(nil) + render inline: "<%= url_for(@workshop) %>\n<%= link_to('Workshop', @workshop) %>" + end + + def show + @workshop = Workshop.new(params[:id]) + render inline: "<%= url_for(@workshop) %>\n<%= link_to('Workshop', @workshop) %>" + end +end + +class SessionsController < ActionController::Base + test_routes do + resources :workshops do + resources :sessions + end + end + + def index + @workshop = Workshop.new(params[:workshop_id]) + @session = Session.new(nil) + render inline: "<%= url_for([@workshop, @session]) %>\n<%= link_to('Session', [@workshop, @session]) %>" + end + + def show + @workshop = Workshop.new(params[:workshop_id]) + @session = Session.new(params[:id]) + render inline: "<%= url_for([@workshop, @session]) %>\n<%= link_to('Session', [@workshop, @session]) %>" + end +end + +class PolymorphicControllerTest < ActionController::TestCase + def test_new_resource + @controller = WorkshopsController.new + + get :index + assert_equal %{/workshops\n<a href="/workshops">Workshop</a>}, @response.body + end + + def test_existing_resource + @controller = WorkshopsController.new + + get :show, 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 + 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 + assert_equal %{/workshops/1/sessions/1\n<a href="/workshops/1/sessions/1">Session</a>}, @response.body + end +end |