\r\r"
end
def nested_multipart(recipient)
recipients recipient
subject "nested multipart"
from "test@example.com"
content_type "multipart/mixed"
part :content_type => "multipart/alternative", :content_disposition => "inline" do |p|
p.part :content_type => "text/plain", :body => "test text\nline #2"
p.part :content_type => "text/html", :body => "test HTML \nline #2"
end
attachment :content_type => "application/octet-stream",:filename => "test.txt", :body => "test abcdefghijklmnopqstuvwxyz"
end
def attachment_with_custom_header(recipient)
recipients recipient
subject "custom header in attachment"
from "test@example.com"
content_type "multipart/related"
part :content_type => "text/html", :body => 'yo'
attachment :content_type => "image/jpeg",:filename => "test.jpeg", :body => "i am not a real picture", :headers => { 'Content-ID' => '' }
end
def unnamed_attachment(recipient)
recipients recipient
subject "nested multipart"
from "test@example.com"
content_type "multipart/mixed"
part :content_type => "text/plain", :body => "hullo"
attachment :content_type => "application/octet-stream", :body => "test abcdefghijklmnopqstuvwxyz"
end
def headers_with_nonalpha_chars(recipient)
recipients recipient
subject "nonalpha chars"
from "One: Two "
cc "Three: Four "
bcc "Five: Six "
body "testing"
end
def custom_content_type_attributes
recipients "no.one@nowhere.test"
subject "custom content types"
from "some.one@somewhere.test"
content_type "text/plain; format=flowed"
body "testing"
end
class < charset }
end
mail
end
def setup
ActionMailer::Base.delivery_method = :test
ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.deliveries = []
@recipient = 'test@localhost'
end
def test_nested_parts
created = nil
assert_nothing_raised { created = TestMailer.create_nested_multipart(@recipient)}
assert_equal 2,created.parts.size
assert_equal 2,created.parts.first.parts.size
assert_equal "multipart/mixed", created.content_type
assert_equal "multipart/alternative", created.parts.first.content_type
assert_equal "text/plain", created.parts.first.parts.first.content_type
assert_equal "text/html", created.parts.first.parts[1].content_type
assert_equal "application/octet-stream", created.parts[1].content_type
end
def test_attachment_with_custom_header
created = nil
assert_nothing_raised { created = TestMailer.create_attachment_with_custom_header(@recipient)}
assert_equal "", created.parts[1].header['content-id'].to_s
end
def test_signed_up
expected = new_mail
expected.to = @recipient
expected.subject = "[Signed up] Welcome #{@recipient}"
expected.body = "Hello there, \n\nMr. #{@recipient}"
expected.from = "system@loudthinking.com"
expected.date = Time.local(2004, 12, 12)
expected.mime_version = nil
created = nil
assert_nothing_raised { created = TestMailer.create_signed_up(@recipient) }
assert_not_nil created
assert_equal expected.encoded, created.encoded
assert_nothing_raised { TestMailer.deliver_signed_up(@recipient) }
assert_not_nil ActionMailer::Base.deliveries.first
assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded
end
def test_custom_template
expected = new_mail
expected.to = @recipient
expected.subject = "[Signed up] Welcome #{@recipient}"
expected.body = "Hello there, \n\nMr. #{@recipient}"
expected.from = "system@loudthinking.com"
expected.date = Time.local(2004, 12, 12)
created = nil
assert_nothing_raised { created = TestMailer.create_custom_template(@recipient) }
assert_not_nil created
assert_equal expected.encoded, created.encoded
end
def test_cancelled_account
expected = new_mail
expected.to = @recipient
expected.subject = "[Cancelled] Goodbye #{@recipient}"
expected.body = "Goodbye, Mr. #{@recipient}"
expected.from = "system@loudthinking.com"
expected.date = Time.local(2004, 12, 12)
created = nil
assert_nothing_raised { created = TestMailer.create_cancelled_account(@recipient) }
assert_not_nil created
assert_equal expected.encoded, created.encoded
assert_nothing_raised { TestMailer.deliver_cancelled_account(@recipient) }
assert_not_nil ActionMailer::Base.deliveries.first
assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded
end
def test_cc_bcc
expected = new_mail
expected.to = @recipient
expected.subject = "testing bcc/cc"
expected.body = "Nothing to see here."
expected.from = "system@loudthinking.com"
expected.cc = "nobody@loudthinking.com"
expected.bcc = "root@loudthinking.com"
expected.date = Time.local 2004, 12, 12
created = nil
assert_nothing_raised do
created = TestMailer.create_cc_bcc @recipient
end
assert_not_nil created
assert_equal expected.encoded, created.encoded
assert_nothing_raised do
TestMailer.deliver_cc_bcc @recipient
end
assert_not_nil ActionMailer::Base.deliveries.first
assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded
end
def test_iso_charset
expected = new_mail( "iso-8859-1" )
expected.to = @recipient
expected.subject = encode "testing isø charsets", "iso-8859-1"
expected.body = "Nothing to see here."
expected.from = "system@loudthinking.com"
expected.cc = "nobody@loudthinking.com"
expected.bcc = "root@loudthinking.com"
expected.date = Time.local 2004, 12, 12
created = nil
assert_nothing_raised do
created = TestMailer.create_iso_charset @recipient
end
assert_not_nil created
assert_equal expected.encoded, created.encoded
assert_nothing_raised do
TestMailer.deliver_iso_charset @recipient
end
assert_not_nil ActionMailer::Base.deliveries.first
assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded
end
def test_unencoded_subject
expected = new_mail
expected.to = @recipient
expected.subject = "testing unencoded subject"
expected.body = "Nothing to see here."
expected.from = "system@loudthinking.com"
expected.cc = "nobody@loudthinking.com"
expected.bcc = "root@loudthinking.com"
expected.date = Time.local 2004, 12, 12
created = nil
assert_nothing_raised do
created = TestMailer.create_unencoded_subject @recipient
end
assert_not_nil created
assert_equal expected.encoded, created.encoded
assert_nothing_raised do
TestMailer.deliver_unencoded_subject @recipient
end
assert_not_nil ActionMailer::Base.deliveries.first
assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded
end
def test_instances_are_nil
assert_nil ActionMailer::Base.new
assert_nil TestMailer.new
end
def test_deliveries_array
assert_not_nil ActionMailer::Base.deliveries
assert_equal 0, ActionMailer::Base.deliveries.size
TestMailer.deliver_signed_up(@recipient)
assert_equal 1, ActionMailer::Base.deliveries.size
assert_not_nil ActionMailer::Base.deliveries.first
end
def test_perform_deliveries_flag
ActionMailer::Base.perform_deliveries = false
TestMailer.deliver_signed_up(@recipient)
assert_equal 0, ActionMailer::Base.deliveries.size
ActionMailer::Base.perform_deliveries = true
TestMailer.deliver_signed_up(@recipient)
assert_equal 1, ActionMailer::Base.deliveries.size
end
def test_unquote_quoted_printable_subject
msg = <"
expected = new_mail "iso-8859-1"
expected.to = quote_address_if_necessary @recipient, "iso-8859-1"
expected.subject = "testing extended headers"
expected.body = "Nothing to see here."
expected.from = quote_address_if_necessary "Grytøyr ", "iso-8859-1"
expected.cc = quote_address_if_necessary "Grytøyr ", "iso-8859-1"
expected.bcc = quote_address_if_necessary "Grytøyr ", "iso-8859-1"
expected.date = Time.local 2004, 12, 12
created = nil
assert_nothing_raised do
created = TestMailer.create_extended_headers @recipient
end
assert_not_nil created
assert_equal expected.encoded, created.encoded
assert_nothing_raised do
TestMailer.deliver_extended_headers @recipient
end
assert_not_nil ActionMailer::Base.deliveries.first
assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded
end
def test_utf8_body_is_not_quoted
@recipient = "Foo áëô îü "
expected = new_mail "utf-8"
expected.to = quote_address_if_necessary @recipient, "utf-8"
expected.subject = "testing utf-8 body"
expected.body = "åœö blah"
expected.from = quote_address_if_necessary @recipient, "utf-8"
expected.cc = quote_address_if_necessary @recipient, "utf-8"
expected.bcc = quote_address_if_necessary @recipient, "utf-8"
expected.date = Time.local 2004, 12, 12
created = TestMailer.create_utf8_body @recipient
assert_match(/åœö blah/, created.encoded)
end
def test_multiple_utf8_recipients
@recipient = ["\"Foo áëô îü\" ", "\"Example Recipient\" "]
expected = new_mail "utf-8"
expected.to = quote_address_if_necessary @recipient, "utf-8"
expected.subject = "testing utf-8 body"
expected.body = "åœö blah"
expected.from = quote_address_if_necessary @recipient.first, "utf-8"
expected.cc = quote_address_if_necessary @recipient, "utf-8"
expected.bcc = quote_address_if_necessary @recipient, "utf-8"
expected.date = Time.local 2004, 12, 12
created = TestMailer.create_utf8_body @recipient
assert_match(/\nFrom: =\?utf-8\?Q\?Foo_.*?\?= \r/, created.encoded)
assert_match(/\nTo: =\?utf-8\?Q\?Foo_.*?\?= , Example Recipient _Google}, mail.body
end
def test_various_newlines
mail = TestMailer.create_various_newlines(@recipient)
assert_equal("line #1\nline #2\nline #3\nline #4\n\n" +
"line #5\n\nline#6\n\nline #7", mail.body)
end
def test_various_newlines_multipart
mail = TestMailer.create_various_newlines_multipart(@recipient)
assert_equal "line #1\nline #2\nline #3\nline #4\n\n", mail.parts[0].body
assert_equal "
line #1
\n
line #2
\n
line #3
\n
line #4
\n\n", mail.parts[1].body
end
def test_headers_removed_on_smtp_delivery
ActionMailer::Base.delivery_method = :smtp
TestMailer.deliver_cc_bcc(@recipient)
assert MockSMTP.deliveries[0][2].include?("root@loudthinking.com")
assert MockSMTP.deliveries[0][2].include?("nobody@loudthinking.com")
assert MockSMTP.deliveries[0][2].include?(@recipient)
assert_match %r{^Cc: nobody@loudthinking.com}, MockSMTP.deliveries[0][0]
assert_match %r{^To: #{@recipient}}, MockSMTP.deliveries[0][0]
assert_no_match %r{^Bcc: root@loudthinking.com}, MockSMTP.deliveries[0][0]
end
def test_recursive_multipart_processing
fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email7")
mail = TMail::Mail.parse(fixture)
assert_equal "This is the first part.\n\nAttachment: test.rb\nAttachment: test.pdf\n\n\nAttachment: smime.p7s\n", mail.body
end
def test_decode_encoded_attachment_filename
fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email8")
mail = TMail::Mail.parse(fixture)
attachment = mail.attachments.last
assert_equal "01QuienTeDijat.Pitbull.mp3", attachment.original_filename
end
def test_wrong_mail_header
fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email9")
assert_raise(TMail::SyntaxError) { TMail::Mail.parse(fixture) }
end
def test_decode_message_with_unknown_charset
fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email10")
mail = TMail::Mail.parse(fixture)
assert_nothing_raised { mail.body }
end
def test_decode_message_with_unquoted_atchar_in_header
fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email11")
mail = TMail::Mail.parse(fixture)
assert_not_nil mail.from
end
def test_empty_header_values_omitted
result = TestMailer.create_unnamed_attachment(@recipient).encoded
assert_match %r{Content-Type: application/octet-stream[^;]}, result
assert_match %r{Content-Disposition: attachment[^;]}, result
end
def test_headers_with_nonalpha_chars
mail = TestMailer.create_headers_with_nonalpha_chars(@recipient)
assert !mail.from_addrs.empty?
assert !mail.cc_addrs.empty?
assert !mail.bcc_addrs.empty?
assert_match(/:/, mail.from_addrs.to_s)
assert_match(/:/, mail.cc_addrs.to_s)
assert_match(/:/, mail.bcc_addrs.to_s)
end
def test_deliver_with_mail_object
mail = TestMailer.create_headers_with_nonalpha_chars(@recipient)
assert_nothing_raised { TestMailer.deliver(mail) }
assert_equal 1, TestMailer.deliveries.length
end
def test_multipart_with_template_path_with_dots
mail = FunkyPathMailer.create_multipart_with_template_path_with_dots(@recipient)
assert_equal 2, mail.parts.length
end
def test_custom_content_type_attributes
mail = TestMailer.create_custom_content_type_attributes
assert_match %r{format=flowed}, mail['content-type'].to_s
assert_match %r{charset=utf-8}, mail['content-type'].to_s
end
end
class InheritableTemplateRootTest < Test::Unit::TestCase
def test_attr
expected = "#{File.dirname(__FILE__)}/fixtures/path.with.dots"
assert_equal expected, FunkyPathMailer.template_root
sub = Class.new(FunkyPathMailer)
sub.template_root = 'test/path'
assert_equal 'test/path', sub.template_root
assert_equal expected, FunkyPathMailer.template_root
end
end
$:.unshift(File.dirname(__FILE__) + "/../lib/")
$:.unshift(File.dirname(__FILE__) + "/../lib/action_mailer/vendor")
require 'test/unit'
require 'tmail'
require 'tempfile'
class QuotingTest < Test::Unit::TestCase
def test_quote_multibyte_chars
original = "\303\246 \303\270 and \303\245"
result = execute_in_sandbox(<<-CODE)
$:.unshift(File.dirname(__FILE__) + "/../lib/")
$KCODE = 'u'
require 'jcode'
require 'action_mailer/quoting'
include ActionMailer::Quoting
quoted_printable(#{original.inspect}, "UTF-8")
CODE
unquoted = TMail::Unquoter.unquote_and_convert_to(result, nil)
assert_equal unquoted, original
end
private
# This whole thing *could* be much simpler, but I don't think Tempfile,
# popen and others exist on all platforms (like Windows).
def execute_in_sandbox(code)
test_name = "#{File.dirname(__FILE__)}/am-quoting-test.#{$$}.rb"
res_name = "#{File.dirname(__FILE__)}/am-quoting-test.#{$$}.out"
File.open(test_name, "w+") do |file|
file.write(<<-CODE)
block = Proc.new do
#{code}
end
puts block.call
CODE
end
system("ruby #{test_name} > #{res_name}") or raise "could not run test in sandbox"
File.read(res_name)
ensure
File.delete(test_name) rescue nil
File.delete(res_name) rescue nil
end
end
$:.unshift(File.dirname(__FILE__) + "/../lib/")
$:.unshift File.dirname(__FILE__) + "/fixtures/helpers"
require 'test/unit'
require 'action_mailer'
class TMailMailTest < Test::Unit::TestCase
def test_body
m = TMail::Mail.new
expected = 'something_with_underscores'
m.encoding = 'quoted-printable'
quoted_body = [expected].pack('*M')
m.body = quoted_body
assert_equal "something_with_underscores=\n", m.quoted_body
assert_equal expected, m.body
end
end
$:.unshift(File.dirname(__FILE__) + "/../lib")
require "action_controller"
require "action_controller/test_process"
Person = Struct.new("Person", :id, :name, :email_address, :phone_number)
class AddressBookService
attr_reader :people
def initialize() @people = [] end
def create_person(data) people.unshift(Person.new(next_person_id, data["name"], data["email_address"], data["phone_number"])) end
def find_person(topic_id) people.select { |person| person.id == person.to_i }.first end
def next_person_id() people.first.id + 1 end
end
class AddressBookController < ActionController::Base
layout "address_book/layout"
before_filter :initialize_session_storage
# Could also have used a proc
# before_filter proc { |c| c.instance_variable_set("@address_book", c.session["address_book"] ||= AddressBookService.new) }
def index
@title = "Address Book"
@people = @address_book.people
end
def person
@person = @address_book.find_person(@params["id"])
end
def create_person
@address_book.create_person(@params["person"])
redirect_to :action => "index"
end
private
def initialize_session_storage
@address_book = @session["address_book"] ||= AddressBookService.new
end
end
ActionController::Base.template_root = File.dirname(__FILE__)
# ActionController::Base.logger = Logger.new("debug.log") # Remove first comment to turn on logging in current dir
begin
AddressBookController.process_cgi(CGI.new) if $0 == __FILE__
rescue => e
CGI.new.out { "#{e.class}: #{e.message}" }
end$:.unshift(File.dirname(__FILE__) + "/../lib")
require "action_controller"
require 'action_controller/test_process'
Person = Struct.new("Person", :name, :address, :age)
class BenchmarkController < ActionController::Base
def message
render_text "hello world"
end
def list
@people = [ Person.new("David"), Person.new("Mary") ]
render_template "hello: <% for person in @people %>Name: <%= person.name %><% end %>"
end
def form_helper
@person = Person.new "david", "hyacintvej", 24
render_template(
"<% person = Person.new 'Mary', 'hyacintvej', 22 %> " +
"change the name <%= text_field 'person', 'name' %> and <%= text_field 'person', 'address' %> and <%= text_field 'person', 'age' %>"
)
end
end
#ActionController::Base.template_root = File.dirname(__FILE__)
require "benchmark"
RUNS = ARGV[0] ? ARGV[0].to_i : 50
require "profile" if ARGV[1]
runtime = Benchmark.measure {
RUNS.times { BenchmarkController.process_test(ActionController::TestRequest.new({ "action" => "list" })) }
}
puts "List: #{RUNS / runtime.real}"
runtime = Benchmark.measure {
RUNS.times { BenchmarkController.process_test(ActionController::TestRequest.new({ "action" => "message" })) }
}
puts "Message: #{RUNS / runtime.real}"
runtime = Benchmark.measure {
RUNS.times { BenchmarkController.process_test(ActionController::TestRequest.new({ "action" => "form_helper" })) }
}
puts "Form helper: #{RUNS / runtime.real}"
require 'rbconfig'
require 'find'
require 'ftools'
include Config
# this was adapted from rdoc's install.rb by ways of Log4r
$sitedir = CONFIG["sitelibdir"]
unless $sitedir
version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"]
$libdir = File.join(CONFIG["libdir"], "ruby", version)
$sitedir = $:.find {|x| x =~ /site_ruby/ }
if !$sitedir
$sitedir = File.join($libdir, "site_ruby")
elsif $sitedir !~ Regexp.quote(version)
$sitedir = File.join($sitedir, version)
end
end
# the acual gruntwork
Dir.chdir("lib")
Find.find("action_controller", "action_controller.rb", "action_view", "action_view.rb") { |f|
if f[-3..-1] == ".rb"
File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true)
else
File::makedirs(File.join($sitedir, *f.split(/\//)))
end
}require 'test/unit'
require 'test/unit/assertions'
require 'rexml/document'
require File.dirname(__FILE__) + "/vendor/html-scanner/html/document"
module Test #:nodoc:
module Unit #:nodoc:
# In addition to these specific assertions, you also have easy access to various collections that the regular test/unit assertions
# can be used against. These collections are:
#
# * assigns: Instance variables assigned in the action that are available for the view.
# * session: Objects being saved in the session.
# * flash: The flash objects currently in the session.
# * cookies: Cookies being sent to the user on this request.
#
# These collections can be used just like any other hash:
#
# assert_not_nil assigns(:person) # makes sure that a @person instance variable was set
# assert_equal "Dave", cookies[:name] # makes sure that a cookie called :name was set as "Dave"
# assert flash.empty? # makes sure that there's nothing in the flash
#
# For historic reasons, the assigns hash uses string-based keys. So assigns[:person] won't work, but assigns["person"] will. To
# appease our yearning for symbols, though, an alternative accessor has been deviced using a method call instead of index referencing.
# So assigns(:person) will work just like assigns["person"], but again, assigns[:person] will not work.
#
# On top of the collections, you have the complete url that a given action redirected to available in redirect_to_url.
#
# For redirects within the same controller, you can even call follow_redirect and the redirect will be followed, triggering another
# action call which can then be asserted against.
#
# == Manipulating the request collections
#
# The collections described above link to the response, so you can test if what the actions were expected to do happened. But
# sometimes you also want to manipulate these collections in the incoming request. This is really only relevant for sessions
# and cookies, though. For sessions, you just do:
#
# @request.session[:key] = "value"
#
# For cookies, you need to manually create the cookie, like this:
#
# @request.cookies["key"] = CGI::Cookie.new("key", "value")
#
# == Testing named routes
#
# If you're using named routes, they can be easily tested using the original named routes methods straight in the test case.
# Example:
#
# assert_redirected_to page_url(:title => 'foo')
module Assertions
# Asserts that the response is one of the following types:
#
# * :success: Status code was 200
# * :redirect: Status code was in the 300-399 range
# * :missing: Status code was 404
# * :error: Status code was in the 500-599 range
#
# You can also pass an explicit status code number as the type, like assert_response(501)
def assert_response(type, message = nil)
clean_backtrace do
if [ :success, :missing, :redirect, :error ].include?(type) && @response.send("#{type}?")
assert_block("") { true } # to count the assertion
elsif type.is_a?(Fixnum) && @response.response_code == type
assert_block("") { true } # to count the assertion
else
assert_block(build_message(message, "Expected response to be a >, but was >", type, @response.response_code)) { false }
end
end
end
# Assert that the redirection options passed in match those of the redirect called in the latest action. This match can be partial,
# such that assert_redirected_to(:controller => "weblog") will also match the redirection of
# redirect_to(:controller => "weblog", :action => "show") and so on.
def assert_redirected_to(options = {}, message=nil)
clean_backtrace do
assert_response(:redirect, message)
if options.is_a?(String)
msg = build_message(message, "expected a redirect to >, found one to >", options, @response.redirect_url)
url_regexp = %r{^(\w+://.*?(/|$|\?))(.*)$}
eurl, epath, url, path = [options, @response.redirect_url].collect do |url|
u, p = (url_regexp =~ url) ? [$1, $3] : [nil, url]
[u, (p[0..0] == '/') ? p : '/' + p]
end.flatten
assert_equal(eurl, url, msg) if eurl && url
assert_equal(epath, path, msg) if epath && path
else
@response_diff = options.diff(@response.redirected_to) if options.is_a?(Hash) && @response.redirected_to.is_a?(Hash)
msg = build_message(message, "response is not a redirection to all of the options supplied (redirection is >)#{', difference: >' if @response_diff}",
@response.redirected_to || @response.redirect_url, @response_diff)
assert_block(msg) do
if options.is_a?(Symbol)
@response.redirected_to == options
else
options.keys.all? do |k|
if k == :controller then options[k] == ActionController::Routing.controller_relative_to(@response.redirected_to[k], @controller.class.controller_path)
else options[k] == (@response.redirected_to[k].respond_to?(:to_param) ? @response.redirected_to[k].to_param : @response.redirected_to[k] unless @response.redirected_to[k].nil?)
end
end
end
end
end
end
end
# Asserts that the request was rendered with the appropriate template file.
def assert_template(expected = nil, message=nil)
clean_backtrace do
rendered = expected ? @response.rendered_file(!expected.include?('/')) : @response.rendered_file
msg = build_message(message, "expecting > but rendering with >", expected, rendered)
assert_block(msg) do
if expected.nil?
!@response.rendered_with_file?
else
expected == rendered
end
end
end
end
# Asserts that the routing of the given path was handled correctly and that the parsed options match.
def assert_recognizes(expected_options, path, extras={}, message=nil)
clean_backtrace do
path = "/#{path}" unless path[0..0] == '/'
# Load routes.rb if it hasn't been loaded.
ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty?
# Assume given controller
request = ActionController::TestRequest.new({}, {}, nil)
request.path = path
ActionController::Routing::Routes.recognize!(request)
expected_options = expected_options.clone
extras.each_key { |key| expected_options.delete key } unless extras.nil?
expected_options.stringify_keys!
msg = build_message(message, "The recognized options > did not match >",
request.path_parameters, expected_options)
assert_block(msg) { request.path_parameters == expected_options }
end
end
# Asserts that the provided options can be used to generate the provided path.
def assert_generates(expected_path, options, defaults={}, extras = {}, message=nil)
clean_backtrace do
expected_path = "/#{expected_path}" unless expected_path[0] == ?/
# Load routes.rb if it hasn't been loaded.
ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty?
generated_path, extra_keys = ActionController::Routing::Routes.generate(options, extras)
found_extras = options.reject {|k, v| ! extra_keys.include? k}
msg = build_message(message, "found extras >, not >", found_extras, extras)
assert_block(msg) { found_extras == extras }
msg = build_message(message, "The generated path > did not match >", generated_path,
expected_path)
assert_block(msg) { expected_path == generated_path }
end
end
# Asserts that path and options match both ways; in other words, the URL generated from
# options is the same as path, and also that the options recognized from path are the same as options
def assert_routing(path, options, defaults={}, extras={}, message=nil)
assert_recognizes(options, path, extras, message)
controller, default_controller = options[:controller], defaults[:controller]
if controller && controller.include?(?/) && default_controller && default_controller.include?(?/)
options[:controller] = "/#{controller}"
end
assert_generates(path, options, defaults, extras, message)
end
# Asserts that there is a tag/node/element in the body of the response
# that meets all of the given conditions. The +conditions+ parameter must
# be a hash of any of the following keys (all are optional):
#
# * :tag: the node type must match the corresponding value
# * :attributes: a hash. The node's attributes must match the
# corresponding values in the hash.
# * :parent: a hash. The node's parent must match the
# corresponding hash.
# * :child: a hash. At least one of the node's immediate children
# must meet the criteria described by the hash.
# * :ancestor: a hash. At least one of the node's ancestors must
# meet the criteria described by the hash.
# * :descendant: a hash. At least one of the node's descendants
# must meet the criteria described by the hash.
# * :sibling: a hash. At least one of the node's siblings must
# meet the criteria described by the hash.
# * :after: a hash. The node must be after any sibling meeting
# the criteria described by the hash, and at least one sibling must match.
# * :before: a hash. The node must be before any sibling meeting
# the criteria described by the hash, and at least one sibling must match.
# * :children: a hash, for counting children of a node. Accepts
# the keys:
# * :count: either a number or a range which must equal (or
# include) the number of children that match.
# * :less_than: the number of matching children must be less
# than this number.
# * :greater_than: the number of matching children must be
# greater than this number.
# * :only: another hash consisting of the keys to use
# to match on the children, and only matching children will be
# counted.
# * :content: the textual content of the node must match the
# given value. This will not match HTML tags in the body of a
# tag--only text.
#
# 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:
#
# # assert that there is a "span" tag
# assert_tag :tag => "span"
#
# # assert that there is a "span" tag with id="x"
# assert_tag :tag => "span", :attributes => { :id => "x" }
#
# # assert that there is a "span" tag using the short-hand
# assert_tag :span
#
# # assert that there is a "span" tag with id="x" using the short-hand
# assert_tag :span, :attributes => { :id => "x" }
#
# # assert that there is a "span" inside of a "div"
# assert_tag :tag => "span", :parent => { :tag => "div" }
#
# # assert that there is a "span" somewhere inside a table
# assert_tag :tag => "span", :ancestor => { :tag => "table" }
#
# # assert that there is a "span" with at least one "em" child
# assert_tag :tag => "span", :child => { :tag => "em" }
#
# # assert that there is a "span" containing a (possibly nested)
# # "strong" tag.
# assert_tag :tag => "span", :descendant => { :tag => "strong" }
#
# # assert that there is a "span" containing between 2 and 4 "em" tags
# # as immediate children
# assert_tag :tag => "span",
# :children => { :count => 2..4, :only => { :tag => "em" } }
#
# # get funky: assert that there is a "div", with an "ul" ancestor
# # and an "li" parent (with "class" = "enum"), and containing a
# # "span" descendant that contains text matching /hello world/
# assert_tag :tag => "div",
# :ancestor => { :tag => "ul" },
# :parent => { :tag => "li",
# :attributes => { :class => "enum" } },
# :descendant => { :tag => "span",
# :child => /hello world/ }
#
# Please noteYou must explicitly
# close all of your tags to use these assertions.
def assert_tag(*opts)
clean_backtrace do
opts = opts.size > 1 ? opts.last.merge({ :tag => opts.first.to_s }) : opts.first
tag = find_tag(opts)
assert tag, "expected tag, but no tag found matching #{opts.inspect} in:\n#{@response.body.inspect}"
end
end
# Identical to #assert_tag, but asserts that a matching tag does _not_
# exist. (See #assert_tag for a full discussion of the syntax.)
def assert_no_tag(*opts)
clean_backtrace do
opts = opts.size > 1 ? opts.last.merge({ :tag => opts.first.to_s }) : opts.first
tag = find_tag(opts)
assert !tag, "expected no tag, but found tag matching #{opts.inspect} in:\n#{@response.body.inspect}"
end
end
# test 2 html strings to be equivalent, i.e. identical up to reordering of attributes
def assert_dom_equal(expected, actual, message="")
clean_backtrace do
expected_dom = HTML::Document.new(expected).root
actual_dom = HTML::Document.new(actual).root
full_message = build_message(message, "> expected to be == to\n>.", expected_dom.to_s, actual_dom.to_s)
assert_block(full_message) { expected_dom == actual_dom }
end
end
# negated form of +assert_dom_equivalent+
def assert_dom_not_equal(expected, actual, message="")
clean_backtrace do
expected_dom = HTML::Document.new(expected).root
actual_dom = HTML::Document.new(actual).root
full_message = build_message(message, "> expected to be != to\n>.", expected_dom.to_s, actual_dom.to_s)
assert_block(full_message) { expected_dom != actual_dom }
end
end
# ensures that the passed record is valid by active record standards. returns the error messages if not
def assert_valid(record)
clean_backtrace do
assert record.valid?, record.errors.full_messages.join("\n")
end
end
def clean_backtrace(&block)
yield
rescue AssertionFailedError => e
path = File.expand_path(__FILE__)
raise AssertionFailedError, e.message, e.backtrace.reject { |line| File.expand_path(line) =~ /#{path}/ }
end
end
end
end
require 'action_controller/mime_type'
require 'action_controller/request'
require 'action_controller/response'
require 'action_controller/routing'
require 'action_controller/code_generation'
require 'action_controller/url_rewriter'
require 'drb'
require 'set'
module ActionController #:nodoc:
class ActionControllerError < StandardError #:nodoc:
end
class SessionRestoreError < ActionControllerError #:nodoc:
end
class MissingTemplate < ActionControllerError #:nodoc:
end
class RoutingError < ActionControllerError #:nodoc:
attr_reader :failures
def initialize(message, failures=[])
super(message)
@failures = failures
end
end
class UnknownController < ActionControllerError #:nodoc:
end
class UnknownAction < ActionControllerError #:nodoc:
end
class MissingFile < ActionControllerError #:nodoc:
end
class SessionOverflowError < ActionControllerError #:nodoc:
DEFAULT_MESSAGE = 'Your session data is larger than the data column in which it is to be stored. You must increase the size of your data column if you intend to store large data.'
def initialize(message = nil)
super(message || DEFAULT_MESSAGE)
end
end
class DoubleRenderError < ActionControllerError #:nodoc:
DEFAULT_MESSAGE = "Render and/or redirect were called multiple times in this action. Please note that you may only call render OR redirect, and only once per action. Also note that neither redirect nor render terminate execution of the action, so if you want to exit an action after redirecting, you need to do something like \"redirect_to(...) and return\". Finally, note that to cause a before filter to halt execution of the rest of the filter chain, the filter must return false, explicitly, so \"render(...) and return false\"."
def initialize(message = nil)
super(message || DEFAULT_MESSAGE)
end
end
class RedirectBackError < ActionControllerError #:nodoc:
DEFAULT_MESSAGE = 'No HTTP_REFERER was set in the request to this action, so redirect_to :back could not be called successfully. If this is a test, make sure to specify @request.env["HTTP_REFERER"].'
def initialize(message = nil)
super(message || DEFAULT_MESSAGE)
end
end
# Action Controllers are the core of a web request in Rails. They are made up of one or more actions that are executed
# on request and then either render a template or redirect to another action. An action is defined as a public method
# on the controller, which will automatically be made accessible to the web-server through Rails Routes.
#
# A sample controller could look like this:
#
# class GuestBookController < ActionController::Base
# def index
# @entries = Entry.find(:all)
# end
#
# def sign
# Entry.create(params[:entry])
# redirect_to :action => "index"
# end
# end
#
# Actions, by default, render a template in the app/views directory corresponding to the name of the controller and action
# after executing code in the action. For example, the +index+ action of the +GuestBookController+ would render the
# template app/views/guestbook/index.rhtml by default after populating the @entries instance variable.
#
# Unlike index, the sign action will not render a template. After performing its main purpose (creating a
# new entry in the guest book), it initiates a redirect instead. This redirect works by returning an external
# "302 Moved" HTTP response that takes the user to the index action.
#
# The index and sign represent the two basic action archetypes used in Action Controllers. Get-and-show and do-and-redirect.
# Most actions are variations of these themes.
#
# == Requests
#
# Requests are processed by the Action Controller framework by extracting the value of the "action" key in the request parameters.
# This value should hold the name of the action to be performed. Once the action has been identified, the remaining
# request parameters, the session (if one is available), and the full request with all the http headers are made available to
# the action through instance variables. Then the action is performed.
#
# The full request object is available with the request accessor and is primarily used to query for http headers. These queries
# are made by accessing the environment hash, like this:
#
# def server_ip
# location = request.env["SERVER_ADDR"]
# render :text => "This server hosted at #{location}"
# end
#
# == Parameters
#
# All request parameters, whether they come from a GET or POST request, or from the URL, are available through the params method
# which returns a hash. For example, an action that was performed through /weblog/list?category=All&limit=5 will include
# { "category" => "All", "limit" => 5 } in params.
#
# It's also possible to construct multi-dimensional parameter hashes by specifying keys using brackets, such as:
#
#
#
#
# A request stemming from a form holding these inputs will include { "post" => { "name" => "david", "address" => "hyacintvej" } }.
# If the address input had been named "post[address][street]", the params would have included
# { "post" => { "address" => { "street" => "hyacintvej" } } }. There's no limit to the depth of the nesting.
#
# == Sessions
#
# Sessions allows you to store objects in between requests. This is useful for objects that are not yet ready to be persisted,
# such as a Signup object constructed in a multi-paged process, or objects that don't change much and are needed all the time, such
# as a User object for a system that requires login. The session should not be used, however, as a cache for objects where it's likely
# they could be changed unknowingly. It's usually too much work to keep it all synchronized -- something databases already excel at.
#
# You can place objects in the session by using the session method, which accesses a hash:
#
# session[:person] = Person.authenticate(user_name, password)
#
# And retrieved again through the same hash:
#
# Hello #{session[:person]}
#
# For removing objects from the session, you can either assign a single key to nil, like session[:person] = nil, or you can
# remove the entire session with reset_session.
#
# By default, sessions are stored on the file system in RAILS_ROOT/tmp/sessions. Any object can be placed in the session
# (as long as it can be Marshalled). But remember that 1000 active sessions each storing a 50kb object could lead to a 50MB store on the filesystem.
# In other words, think carefully about size and caching before resorting to the use of the session on the filesystem.
#
# An alternative to storing sessions on disk is to use ActiveRecordStore to store sessions in your database, which can solve problems
# caused by storing sessions in the file system and may speed up your application. To use ActiveRecordStore, uncomment the line:
#
# config.action_controller.session_store = :active_record_store
#
# in your environment.rb and run rake db:sessions:create.
#
# == Responses
#
# Each action results in a response, which holds the headers and document to be sent to the user's browser. The actual response
# object is generated automatically through the use of renders and redirects and requires no user intervention.
#
# == Renders
#
# Action Controller sends content to the user by using one of five rendering methods. The most versatile and common is the rendering
# of a template. Included in the Action Pack is the Action View, which enables rendering of ERb templates. It's automatically configured.
# The controller passes objects to the view by assigning instance variables:
#
# def show
# @post = Post.find(params[:id])
# end
#
# Which are then automatically available to the view:
#
# Title: <%= @post.title %>
#
# You don't have to rely on the automated rendering. Especially actions that could result in the rendering of different templates will use
# the manual rendering methods:
#
# def search
# @results = Search.find(params[:query])
# case @results
# when 0 then render :action => "no_results"
# when 1 then render :action => "show"
# when 2..10 then render :action => "show_many"
# end
# end
#
# Read more about writing ERb and Builder templates in link:classes/ActionView/Base.html.
#
# == Redirects
#
# Redirects are used to move from one action to another. For example, after a create action, which stores a blog entry to a database,
# we might like to show the user the new entry. Because we're following good DRY principles (Don't Repeat Yourself), we're going to reuse (and redirect to)
# a show action that we'll assume has already been created. The code might look like this:
#
# def create
# @entry = Entry.new(params[:entry])
# if @entry.save
# # The entry was saved correctly, redirect to show
# redirect_to :action => 'show', :id => @entry.id
# else
# # things didn't go so well, do something else
# end
# end
#
# In this case, after saving our new entry to the database, the user is redirected to the show method which is then executed.
#
# == Calling multiple redirects or renders
#
# An action should conclude with a single render or redirect. Attempting to try to do either again will result in a DoubleRenderError:
#
# def do_something
# redirect_to :action => "elsewhere"
# render :action => "overthere" # raises DoubleRenderError
# end
#
# If you need to redirect on the condition of something, then be sure to add "and return" to halt execution.
#
# def do_something
# redirect_to(:action => "elsewhere") and return if monkeys.nil?
# render :action => "overthere" # won't be called unless monkeys is nil
# end
#
class Base
DEFAULT_RENDER_STATUS_CODE = "200 OK"
include Reloadable::Subclasses
# Determines whether the view has access to controller internals @request, @response, @session, and @template.
# By default, it does.
@@view_controller_internals = true
cattr_accessor :view_controller_internals
# Protected instance variable cache
@@protected_variables_cache = nil
cattr_accessor :protected_variables_cache
# Prepends all the URL-generating helpers from AssetHelper. This makes it possible to easily move javascripts, stylesheets,
# and images to a dedicated asset server away from the main web server. Example:
# ActionController::Base.asset_host = "http://assets.example.com"
@@asset_host = ""
cattr_accessor :asset_host
# All requests are considered local by default, so everyone will be exposed to detailed debugging screens on errors.
# When the application is ready to go public, this should be set to false, and the protected method local_request?
# should instead be implemented in the controller to determine when debugging screens should be shown.
@@consider_all_requests_local = true
cattr_accessor :consider_all_requests_local
# Enable or disable the collection of failure information for RoutingErrors.
# This information can be extremely useful when tweaking custom routes, but is
# pointless once routes have been tested and verified.
@@debug_routes = true
cattr_accessor :debug_routes
# Controls whether the application is thread-safe, so multi-threaded servers like WEBrick know whether to apply a mutex
# around the performance of each action. Action Pack and Active Record are by default thread-safe, but many applications
# may not be. Turned off by default.
@@allow_concurrency = false
cattr_accessor :allow_concurrency
# Modern REST web services often need to submit complex data to the web application.
# The param_parsers hash lets you register handlers wich will process the http body and add parameters to the
# params hash. These handlers are invoked for post and put requests.
#
# By default application/xml is enabled. A XmlSimple class with the same param name as the root will be instanciated
# in the params. This allows XML requests to mask themselves as regular form submissions, so you can have one
# action serve both regular forms and web service requests.
#
# Example of doing your own parser for a custom content type:
#
# ActionController::Base.param_parsers[Mime::Type.lookup('application/atom+xml')] = Proc.new do |data|
# node = REXML::Document.new(post)
# { node.root.name => node.root }
# end
#
# Note: Up until release 1.1 of Rails, Action Controller would default to using XmlSimple configured to discard the
# root node for such requests. The new default is to keep the root, such that "David" results
# in params[:r][:name] for "David" instead of params[:name]. To get the old behavior, you can
# re-register XmlSimple as application/xml handler ike this:
#
# ActionController::Base.param_parsers[Mime::XML] =
# Proc.new { |data| XmlSimple.xml_in(data, 'ForceArray' => false) }
#
# A YAML parser is also available and can be turned on with:
#
# ActionController::Base.param_parsers[Mime::YAML] = :yaml
@@param_parsers = { Mime::XML => :xml_simple }
cattr_accessor :param_parsers
# Template root determines the base from which template references will be made. So a call to render("test/template")
# will be converted to "#{template_root}/test/template.rhtml".
class_inheritable_accessor :template_root
# The logger is used for generating information on the action run-time (including benchmarking) if available.
# Can be set to nil for no logging. Compatible with both Ruby's own Logger and Log4r loggers.
cattr_accessor :logger
# Determines which template class should be used by ActionController.
cattr_accessor :template_class
# Turn on +ignore_missing_templates+ if you want to unit test actions without making the associated templates.
cattr_accessor :ignore_missing_templates
# Holds the request object that's primarily used to get environment variables through access like
# request.env["REQUEST_URI"].
attr_accessor :request
# Holds a hash of all the GET, POST, and Url parameters passed to the action. Accessed like params["post_id"]
# to get the post_id. No type casts are made, so all values are returned as strings.
attr_accessor :params
# Holds the response object that's primarily used to set additional HTTP headers through access like
# response.headers["Cache-Control"] = "no-cache". Can also be used to access the final body HTML after a template
# has been rendered through response.body -- useful for after_filters that wants to manipulate the output,
# such as a OutputCompressionFilter.
attr_accessor :response
# Holds a hash of objects in the session. Accessed like session[:person] to get the object tied to the "person"
# key. The session will hold any type of object as values, but the key should be a string or symbol.
attr_accessor :session
# Holds a hash of header names and values. Accessed like headers["Cache-Control"] to get the value of the Cache-Control
# directive. Values should always be specified as strings.
attr_accessor :headers
# Holds the hash of variables that are passed on to the template class to be made available to the view. This hash
# is generated by taking a snapshot of all the instance variables in the current scope just before a template is rendered.
attr_accessor :assigns
# Returns the name of the action this controller is processing.
attr_accessor :action_name
class << self
# Factory for the standard create, process loop where the controller is discarded after processing.
def process(request, response) #:nodoc:
new.process(request, response)
end
# Converts the class name from something like "OneModule::TwoModule::NeatController" to "NeatController".
def controller_class_name
@controller_class_name ||= name.demodulize
end
# Converts the class name from something like "OneModule::TwoModule::NeatController" to "neat".
def controller_name
@controller_name ||= controller_class_name.sub(/Controller$/, '').underscore
end
# Converts the class name from something like "OneModule::TwoModule::NeatController" to "one_module/two_module/neat".
def controller_path
@controller_path ||= name.gsub(/Controller$/, '').underscore
end
# Return an array containing the names of public methods that have been marked hidden from the action processor.
# By default, all methods defined in ActionController::Base and included modules are hidden.
# More methods can be hidden using hide_actions.
def hidden_actions
write_inheritable_attribute(:hidden_actions, ActionController::Base.public_instance_methods) unless read_inheritable_attribute(:hidden_actions)
read_inheritable_attribute(:hidden_actions)
end
# Hide each of the given methods from being callable as actions.
def hide_action(*names)
write_inheritable_attribute(:hidden_actions, hidden_actions | names.collect { |n| n.to_s })
end
# Replace sensitive paramater data from the request log.
# Filters paramaters that have any of the arguments as a substring.
# Looks in all subhashes of the param hash for keys to filter.
# If a block is given, each key and value of the paramater hash and all
# subhashes is passed to it, the value or key
# can be replaced using String#replace or similar method.
#
# Examples:
# filter_parameter_logging
# => Does nothing, just slows the logging process down
#
# filter_parameter_logging :password
# => replaces the value to all keys matching /password/i with "[FILTERED]"
#
# filter_parameter_logging :foo, "bar"
# => replaces the value to all keys matching /foo|bar/i with "[FILTERED]"
#
# filter_parameter_logging { |k,v| v.reverse! if k =~ /secret/i }
# => reverses the value to all keys matching /secret/i
#
# filter_parameter_logging(:foo, "bar") { |k,v| v.reverse! if k =~ /secret/i }
# => reverses the value to all keys matching /secret/i, and
# replaces the value to all keys matching /foo|bar/i with "[FILTERED]"
def filter_parameter_logging(*filter_words, &block)
parameter_filter = Regexp.new(filter_words.collect{ |s| s.to_s }.join('|'), true) if filter_words.length > 0
define_method(:filter_parameters) do |unfiltered_parameters|
filtered_parameters = {}
unfiltered_parameters.each do |key, value|
if key =~ parameter_filter
filtered_parameters[key] = '[FILTERED]'
elsif value.is_a?(Hash)
filtered_parameters[key] = filter_parameters(value)
elsif block_given?
key, value = key.dup, value.dup
yield key, value
filtered_parameters[key] = value
else
filtered_parameters[key] = value
end
end
filtered_parameters
end
end
end
public
# Extracts the action_name from the request parameters and performs that action.
def process(request, response, method = :perform_action, *arguments) #:nodoc:
initialize_template_class(response)
assign_shortcuts(request, response)
initialize_current_url
assign_names
forget_variables_added_to_assigns
log_processing
send(method, *arguments)
response
ensure
process_cleanup
end
# Returns a URL that has been rewritten according to the options hash and the defined Routes.
# (For doing a complete redirect, use redirect_to).
# Â
# url_for is used to:
# Â
# All keys given to url_for are forwarded to the Route module, save for the following:
# * :anchor -- specifies the anchor name to be appended to the path. For example,
# url_for :controller => 'posts', :action => 'show', :id => 10, :anchor => 'comments'
# will produce "/posts/show/10#comments".
# * :only_path -- if true, returns the absolute URL (omitting the protocol, host name, and port)
# * :trailing_slash -- if true, adds a trailing slash, as in "/archive/2005/". Note that this
# is currently not recommended since it breaks caching.
# * :host -- overrides the default (current) host if provided
# * :protocol -- overrides the default (current) protocol if provided
#
# The URL is generated from the remaining keys in the hash. A URL contains two key parts: the and a query string.
# Routes composes a query string as the key/value pairs not included in the .
#
# The default Routes setup supports a typical Rails path of "controller/action/id" where action and id are optional, with
# action defaulting to 'index' when not given. Here are some typical url_for statements and their corresponding URLs:
# Â
# url_for :controller => 'posts', :action => 'recent' # => 'proto://host.com/posts/recent'
# url_for :controller => 'posts', :action => 'index' # => 'proto://host.com/posts'
# url_for :controller => 'posts', :action => 'show', :id => 10 # => 'proto://host.com/posts/show/10'
#
# When generating a new URL, missing values may be filled in from the current request's parameters. For example,
# url_for :action => 'some_action' will retain the current controller, as expected. This behavior extends to
# other parameters, including :controller, :id, and any other parameters that are placed into a Route's
# path.
# Â
# The URL helpers such as url_for have a limited form of memory: when generating a new URL, they can look for
# missing values in the current request's parameters. Routes attempts to guess when a value should and should not be
# taken from the defaults. There are a few simple rules on how this is performed:
#
# * If the controller name begins with a slash, no defaults are used: url_for :controller => '/home'
# * If the controller changes, the action will default to index unless provided
#
# The final rule is applied while the URL is being generated and is best illustrated by an example. Let us consider the
# route given by map.connect 'people/:last/:first/:action', :action => 'bio', :controller => 'people'.
#
# Suppose that the current URL is "people/hh/david/contacts". Let's consider a few different cases of URLs which are generated
# from this page.
#
# * url_for :action => 'bio' -- During the generation of this URL, default values will be used for the first and
# last components, and the action shall change. The generated URL will be, "people/hh/david/bio".
# * url_for :first => 'davids-little-brother' This generates the URL 'people/hh/davids-little-brother' -- note
# that this URL leaves out the assumed action of 'bio'.
#
# However, you might ask why the action from the current request, 'contacts', isn't carried over into the new URL. The
# answer has to do with the order in which the parameters appear in the generated path. In a nutshell, since the
# value that appears in the slot for :first is not equal to default value for :first we stop using
# defaults. On it's own, this rule can account for much of the typical Rails URL behavior.
# Â
# Although a convienence, defaults can occasionaly get in your way. In some cases a default persists longer than desired.
# The default may be cleared by adding :name => nil to url_for's options.
# This is often required when writing form helpers, since the defaults in play may vary greatly depending upon where the
# helper is used from. The following line will redirect to PostController's default action, regardless of the page it is
# displayed on:
#
# url_for :controller => 'posts', :action => nil
#
# If you explicitly want to create a URL that's almost the same as the current URL, you can do so using the
# :overwrite_params options. Say for your posts you have different views for showing and printing them.
# Then, in the show view, you get the URL for the print view like this
#
# url_for :overwrite_params => { :action => 'print' }
#
# This takes the current URL as is and only exchanges the action. In contrast, url_for :action => 'print'
# would have slashed-off the path components after the changed action.
def url_for(options = {}, *parameters_for_method_reference) #:doc:
case options
when String then options
when Symbol then send(options, *parameters_for_method_reference)
when Hash then @url.rewrite(rewrite_options(options))
end
end
# Converts the class name from something like "OneModule::TwoModule::NeatController" to "NeatController".
def controller_class_name
self.class.controller_class_name
end
# Converts the class name from something like "OneModule::TwoModule::NeatController" to "neat".
def controller_name
self.class.controller_name
end
def session_enabled?
request.session_options[:disabled] != false
end
protected
# Renders the content that will be returned to the browser as the response body.
#
# === Rendering an action
#
# Action rendering is the most common form and the type used automatically by Action Controller when nothing else is
# specified. By default, actions are rendered within the current layout (if one exists).
#
# # Renders the template for the action "goal" within the current controller
# render :action => "goal"
#
# # Renders the template for the action "short_goal" within the current controller,
# # but without the current active layout
# render :action => "short_goal", :layout => false
#
# # Renders the template for the action "long_goal" within the current controller,
# # but with a custom layout
# render :action => "long_goal", :layout => "spectacular"
#
# _Deprecation_ _notice_: This used to have the signatures render_action("action", status = 200),
# render_without_layout("controller/action", status = 200), and
# render_with_layout("controller/action", status = 200, layout).
#
# === Rendering partials
#
# Partial rendering is most commonly used together with Ajax calls that only update one or a few elements on a page
# without reloading. Rendering of partials from the controller makes it possible to use the same partial template in
# both the full-page rendering (by calling it from within the template) and when sub-page updates happen (from the
# controller action responding to Ajax calls). By default, the current layout is not used.
#
# # Renders the partial located at app/views/controller/_win.r(html|xml)
# render :partial => "win"
#
# # Renders the partial with a status code of 500 (internal error)
# render :partial => "broken", :status => 500
#
# # Renders the same partial but also makes a local variable available to it
# render :partial => "win", :locals => { :name => "david" }
#
# # Renders a collection of the same partial by making each element of @wins available through
# # the local variable "win" as it builds the complete response
# render :partial => "win", :collection => @wins
#
# # Renders the same collection of partials, but also renders the win_divider partial in between
# # each win partial.
# render :partial => "win", :collection => @wins, :spacer_template => "win_divider"
#
# _Deprecation_ _notice_: This used to have the signatures
# render_partial(partial_path = default_template_name, object = nil, local_assigns = {}) and
# render_partial_collection(partial_name, collection, partial_spacer_template = nil, local_assigns = {}).
#
# === Rendering a template
#
# Template rendering works just like action rendering except that it takes a path relative to the template root.
# The current layout is automatically applied.
#
# # Renders the template located in [TEMPLATE_ROOT]/weblog/show.r(html|xml) (in Rails, app/views/weblog/show.rhtml)
# render :template => "weblog/show"
#
# === Rendering a file
#
# File rendering works just like action rendering except that it takes a filesystem path. By default, the path
# is assumed to be absolute, and the current layout is not applied.
#
# # Renders the template located at the absolute filesystem path
# render :file => "/path/to/some/template.rhtml"
# render :file => "c:/path/to/some/template.rhtml"
#
# # Renders a template within the current layout, and with a 404 status code
# render :file => "/path/to/some/template.rhtml", :layout => true, :status => 404
# render :file => "c:/path/to/some/template.rhtml", :layout => true, :status => 404
#
# # Renders a template relative to the template root and chooses the proper file extension
# render :file => "some/template", :use_full_path => true
#
# _Deprecation_ _notice_: This used to have the signature render_file(path, status = 200)
#
# === Rendering text
#
# Rendering of text is usually used for tests or for rendering prepared content, such as a cache. By default, text
# rendering is not done within the active layout.
#
# # Renders the clear text "hello world" with status code 200
# render :text => "hello world!"
#
# # Renders the clear text "Explosion!" with status code 500
# render :text => "Explosion!", :status => 500
#
# # Renders the clear text "Hi there!" within the current active layout (if one exists)
# render :text => "Explosion!", :layout => true
#
# # Renders the clear text "Hi there!" within the layout
# # placed in "app/views/layouts/special.r(html|xml)"
# render :text => "Explosion!", :layout => "special"
#
# _Deprecation_ _notice_: This used to have the signature render_text("text", status = 200)
#
# === Rendering an inline template
#
# Rendering of an inline template works as a cross between text and action rendering where the source for the template
# is supplied inline, like text, but its interpreted with ERb or Builder, like action. By default, ERb is used for rendering
# and the current layout is not used.
#
# # Renders "hello, hello, hello, again"
# render :inline => "<%= 'hello, ' * 3 + 'again' %>"
#
# # Renders "
Good seeing you!
" using Builder
# render :inline => "xml.p { 'Good seeing you!' }", :type => :rxml
#
# # Renders "hello david"
# render :inline => "<%= 'hello ' + name %>", :locals => { :name => "david" }
#
# _Deprecation_ _notice_: This used to have the signature render_template(template, status = 200, type = :rhtml)
#
# === Rendering inline JavaScriptGenerator page updates
#
# In addition to rendering JavaScriptGenerator page updates with Ajax in RJS templates (see ActionView::Base for details),
# you can also pass the :update parameter to +render+, along with a block, to render page updates inline.
#
# render :update do |page|
# page.replace_html 'user_list', :partial => 'user', :collection => @users
# page.visual_effect :highlight, 'user_list'
# end
#
# === Rendering nothing
#
# Rendering nothing is often convenient in combination with Ajax calls that perform their effect client-side or
# when you just want to communicate a status code. Due to a bug in Safari, nothing actually means a single space.
#
# # Renders an empty response with status code 200
# render :nothing => true
#
# # Renders an empty response with status code 401 (access denied)
# render :nothing => true, :status => 401
def render(options = nil, deprecated_status = nil, &block) #:doc:
raise DoubleRenderError, "Can only render or redirect once per action" if performed?
# Backwards compatibility
unless options.is_a?(Hash)
if options == :update
options = {:update => true}
else
return render_file(options || default_template_name, deprecated_status, true)
end
end
if content_type = options[:content_type]
headers["Content-Type"] = content_type
end
if text = options[:text]
render_text(text, options[:status])
else
if file = options[:file]
render_file(file, options[:status], options[:use_full_path], options[:locals] || {})
elsif template = options[:template]
render_file(template, options[:status], true)
elsif inline = options[:inline]
render_template(inline, options[:status], options[:type], options[:locals] || {})
elsif action_name = options[:action]
render_action(action_name, options[:status], options[:layout])
elsif xml = options[:xml]
render_xml(xml, options[:status])
elsif partial = options[:partial]
partial = default_template_name if partial == true
if collection = options[:collection]
render_partial_collection(partial, collection, options[:spacer_template], options[:locals], options[:status])
else
render_partial(partial, ActionView::Base::ObjectWrapper.new(options[:object]), options[:locals], options[:status])
end
elsif options[:update]
add_variables_to_assigns
@template.send :evaluate_assigns
generator = ActionView::Helpers::PrototypeHelper::JavaScriptGenerator.new(@template, &block)
render_javascript(generator.to_s)
elsif options[:nothing]
# Safari doesn't pass the headers of the return if the response is zero length
render_text(" ", options[:status])
else
render_file(default_template_name, options[:status], true)
end
end
end
# Renders according to the same rules as render, but returns the result in a string instead
# of sending it as the response body to the browser.
def render_to_string(options = nil, &block) #:doc:
result = render(options, &block)
erase_render_results
forget_variables_added_to_assigns
reset_variables_added_to_assigns
result
end
def render_action(action_name, status = nil, with_layout = true) #:nodoc:
template = default_template_name(action_name.to_s)
if with_layout && !template_exempt_from_layout?(template)
render_with_layout(template, status)
else
render_without_layout(template, status)
end
end
def render_file(template_path, status = nil, use_full_path = false, locals = {}) #:nodoc:
add_variables_to_assigns
assert_existence_of_template_file(template_path) if use_full_path
logger.info("Rendering #{template_path}" + (status ? " (#{status})" : '')) if logger
render_text(@template.render_file(template_path, use_full_path, locals), status)
end
def render_template(template, status = nil, type = :rhtml, local_assigns = {}) #:nodoc:
add_variables_to_assigns
render_text(@template.render_template(type, template, nil, local_assigns), status)
end
def render_text(text = nil, status = nil) #:nodoc:
@performed_render = true
@response.headers['Status'] = (status || DEFAULT_RENDER_STATUS_CODE).to_s
@response.body = text
end
def render_javascript(javascript, status = nil) #:nodoc:
@response.headers['Content-Type'] = 'text/javascript; charset=UTF-8'
render_text(javascript, status)
end
def render_xml(xml, status = nil) #:nodoc:
@response.headers['Content-Type'] = 'application/xml'
render_text(xml, status)
end
def render_nothing(status = nil) #:nodoc:
render_text(' ', status)
end
def render_partial(partial_path = default_template_name, object = nil, local_assigns = nil, status = nil) #:nodoc:
add_variables_to_assigns
render_text(@template.render_partial(partial_path, object, local_assigns), status)
end
def render_partial_collection(partial_name, collection, partial_spacer_template = nil, local_assigns = nil, status = nil) #:nodoc:
add_variables_to_assigns
render_text(@template.render_partial_collection(partial_name, collection, partial_spacer_template, local_assigns), status)
end
def render_with_layout(template_name = default_template_name, status = nil, layout = nil) #:nodoc:
render_with_a_layout(template_name, status, layout)
end
def render_without_layout(template_name = default_template_name, status = nil) #:nodoc:
render_with_no_layout(template_name, status)
end
# Clears the rendered results, allowing for another render to be performed.
def erase_render_results #:nodoc:
@response.body = nil
@performed_render = false
end
# Clears the redirected results from the headers, resets the status to 200 and returns
# the URL that was used to redirect or nil if there was no redirected URL
# Note that +redirect_to+ will change the body of the response to indicate a redirection.
# The response body is not reset here, see +erase_render_results+
def erase_redirect_results #:nodoc:
@performed_redirect = false
response.redirected_to = nil
response.redirected_to_method_params = nil
response.headers['Status'] = DEFAULT_RENDER_STATUS_CODE
response.headers.delete('location')
end
# Erase both render and redirect results
def erase_results #:nodoc:
erase_render_results
erase_redirect_results
end
def rewrite_options(options) #:nodoc:
if defaults = default_url_options(options)
defaults.merge(options)
else
options
end
end
# Overwrite to implement a number of default options that all url_for-based methods will use. The default options should come in
# the form of a hash, just like the one you would use for url_for directly. Example:
#
# def default_url_options(options)
# { :project => @project.active? ? @project.url_name : "unknown" }
# end
#
# As you can infer from the example, this is mostly useful for situations where you want to centralize dynamic decisions about the
# urls as they stem from the business domain. Please note that any individual url_for call can always override the defaults set
# by this method.
def default_url_options(options) #:doc:
end
# Redirects the browser to the target specified in +options+. This parameter can take one of three forms:
#
# * Hash: The URL will be generated by calling url_for with the +options+.
# * String starting with protocol:// (like http://): Is passed straight through as the target for redirection.
# * String not containing a protocol: The current protocol and host is prepended to the string.
# * :back: Back to the page that issued the request. Useful for forms that are triggered from multiple places.
# Short-hand for redirect_to(request.env["HTTP_REFERER"])
#
# Examples:
# redirect_to :action => "show", :id => 5
# redirect_to "http://www.rubyonrails.org"
# redirect_to "/images/screenshot.jpg"
# redirect_to :back
#
# The redirection happens as a "302 Moved" header.
#
# When using redirect_to :back, if there is no referrer,
# RedirectBackError will be raised. You may specify some fallback
# behavior for this case by rescueing RedirectBackError.
def redirect_to(options = {}, *parameters_for_method_reference) #:doc:
case options
when %r{^\w+://.*}
raise DoubleRenderError if performed?
logger.info("Redirected to #{options}") if logger
response.redirect(options)
response.redirected_to = options
@performed_redirect = true
when String
redirect_to(request.protocol + request.host_with_port + options)
when :back
request.env["HTTP_REFERER"] ? redirect_to(request.env["HTTP_REFERER"]) : raise(RedirectBackError)
else
if parameters_for_method_reference.empty?
redirect_to(url_for(options))
response.redirected_to = options
else
redirect_to(url_for(options, *parameters_for_method_reference))
response.redirected_to, response.redirected_to_method_params = options, parameters_for_method_reference
end
end
end
# Sets a HTTP 1.1 Cache-Control header. Defaults to issuing a "private" instruction, so that
# intermediate caches shouldn't cache the response.
#
# Examples:
# expires_in 20.minutes
# expires_in 3.hours, :private => false
# expires in 3.hours, 'max-stale' => 5.hours, :private => nil, :public => true
#
# This method will overwrite an existing Cache-Control header.
# See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities.
def expires_in(seconds, options = {}) #:doc:
cache_options = { 'max-age' => seconds, 'private' => true }.symbolize_keys.merge!(options.symbolize_keys)
cache_options.delete_if { |k,v| v.nil? or v == false }
cache_control = cache_options.map{ |k,v| v == true ? k.to_s : "#{k.to_s}=#{v.to_s}"}
@response.headers["Cache-Control"] = cache_control.join(', ')
end
# Sets a HTTP 1.1 Cache-Control header of "no-cache" so no caching should occur by the browser or
# intermediate caches (like caching proxy servers).
def expires_now #:doc:
@response.headers["Cache-Control"] = "no-cache"
end
# Resets the session by clearing out all the objects stored within and initializing a new session object.
def reset_session #:doc:
@request.reset_session
@session = @request.session
@response.session = @session
end
private
def self.view_class
@view_class ||=
# create a new class based on the default template class and include helper methods
returning Class.new(ActionView::Base) do |view_class|
view_class.send(:include, master_helper_module)
end
end
def self.view_root
@view_root ||= template_root
end
def initialize_template_class(response)
raise "You must assign a template class through ActionController.template_class= before processing a request" unless @@template_class
response.template = self.class.view_class.new(self.class.view_root, {}, self)
response.redirected_to = nil
@performed_render = @performed_redirect = false
end
def assign_shortcuts(request, response)
@request, @params, @cookies = request, request.parameters, request.cookies
@response = response
@response.session = request.session
@session = @response.session
@template = @response.template
@assigns = @response.template.assigns
@headers = @response.headers
end
def initialize_current_url
@url = UrlRewriter.new(@request, @params.clone())
end
def log_processing
if logger
logger.info "\n\nProcessing #{controller_class_name}\##{action_name} (for #{request_origin}) [#{request.method.to_s.upcase}]"
logger.info " Session ID: #{@session.session_id}" if @session and @session.respond_to?(:session_id)
logger.info " Parameters: #{respond_to?(:filter_parameters) ? filter_parameters(@params).inspect : @params.inspect}"
end
end
def perform_action
if self.class.action_methods.include?(action_name) || self.class.action_methods.include?('method_missing')
send(action_name)
render unless performed?
elsif template_exists? && template_public?
render
else
raise UnknownAction, "No action responded to #{action_name}", caller
end
end
def performed?
@performed_render || @performed_redirect
end
def assign_names
@action_name = (params['action'] || 'index')
end
def action_methods
self.class.action_methods
end
def self.action_methods
@action_methods ||= Set.new(public_instance_methods - hidden_actions)
end
def add_variables_to_assigns
unless @variables_added
add_instance_variables_to_assigns
add_class_variables_to_assigns if view_controller_internals
@variables_added = true
end
end
def forget_variables_added_to_assigns
@variables_added = nil
end
def reset_variables_added_to_assigns
@template.instance_variable_set("@assigns_added", nil)
end
def add_instance_variables_to_assigns
@@protected_variables_cache ||= protected_instance_variables.inject({}) { |h, k| h[k] = true; h }
instance_variables.each do |var|
next if @@protected_variables_cache.include?(var)
@assigns[var[1..-1]] = instance_variable_get(var)
end
end
def add_class_variables_to_assigns
%w( template_root logger template_class ignore_missing_templates ).each do |cvar|
@assigns[cvar] = self.send(cvar)
end
end
def protected_instance_variables
if view_controller_internals
[ "@assigns", "@performed_redirect", "@performed_render" ]
else
[ "@assigns", "@performed_redirect", "@performed_render", "@request", "@response", "@session", "@cookies", "@template", "@request_origin", "@parent_controller" ]
end
end
def request_origin
# this *needs* to be cached!
# otherwise you'd get different results if calling it more than once
@request_origin ||= "#{@request.remote_ip} at #{Time.now.to_s(:db)}"
end
def complete_request_uri
"#{@request.protocol}#{@request.host}#{@request.request_uri}"
end
def close_session
@session.close unless @session.nil? || Hash === @session
end
def template_exists?(template_name = default_template_name)
@template.file_exists?(template_name)
end
def template_public?(template_name = default_template_name)
@template.file_public?(template_name)
end
def template_exempt_from_layout?(template_name = default_template_name)
template_name =~ /\.rjs$/ || (@template.pick_template_extension(template_name) == :rjs rescue false)
end
def assert_existence_of_template_file(template_name)
unless template_exists?(template_name) || ignore_missing_templates
full_template_path = @template.send(:full_template_path, template_name, 'rhtml')
template_type = (template_name =~ /layouts/i) ? 'layout' : 'template'
raise(MissingTemplate, "Missing #{template_type} #{full_template_path}")
end
end
def default_template_name(action_name = self.action_name)
if action_name
action_name = action_name.to_s
if action_name.include?('/') && template_path_includes_controller?(action_name)
action_name = strip_out_controller(action_name)
end
end
"#{self.class.controller_path}/#{action_name}"
end
def strip_out_controller(path)
path.split('/', 2).last
end
def template_path_includes_controller?(path)
self.class.controller_path.split('/')[-1] == path.split('/')[0]
end
def process_cleanup
close_session
end
end
end
require 'benchmark'
module ActionController #:nodoc:
# The benchmarking module times the performance of actions and reports to the logger. If the Active Record
# package has been included, a separate timing section for database calls will be added as well.
module Benchmarking #:nodoc:
def self.included(base)
base.extend(ClassMethods)
base.class_eval do
alias_method :perform_action_without_benchmark, :perform_action
alias_method :perform_action, :perform_action_with_benchmark
alias_method :render_without_benchmark, :render
alias_method :render, :render_with_benchmark
end
end
module ClassMethods
# Log and benchmark the workings of a single block and silence whatever logging that may have happened inside it
# (unless use_silence is set to false).
#
# The benchmark is only recorded if the current level of the logger matches the log_level, which makes it
# easy to include benchmarking statements in production software that will remain inexpensive because the benchmark
# will only be conducted if the log level is low enough.
def benchmark(title, log_level = Logger::DEBUG, use_silence = true)
if logger && logger.level == log_level
result = nil
seconds = Benchmark.realtime { result = use_silence ? silence { yield } : yield }
logger.add(log_level, "#{title} (#{'%.5f' % seconds})")
result
else
yield
end
end
# Silences the logger for the duration of the block.
def silence
old_logger_level, logger.level = logger.level, Logger::ERROR if logger
yield
ensure
logger.level = old_logger_level if logger
end
end
def render_with_benchmark(options = nil, deprecated_status = nil, &block)
unless logger
render_without_benchmark(options, deprecated_status, &block)
else
db_runtime = ActiveRecord::Base.connection.reset_runtime if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected?
render_output = nil
@rendering_runtime = Benchmark::measure{ render_output = render_without_benchmark(options, deprecated_status, &block) }.real
if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected?
@db_rt_before_render = db_runtime
@db_rt_after_render = ActiveRecord::Base.connection.reset_runtime
@rendering_runtime -= @db_rt_after_render
end
render_output
end
end
def perform_action_with_benchmark
unless logger
perform_action_without_benchmark
else
runtime = [Benchmark::measure{ perform_action_without_benchmark }.real, 0.0001].max
log_message = "Completed in #{sprintf("%.5f", runtime)} (#{(1 / runtime).floor} reqs/sec)"
log_message << rendering_runtime(runtime) if @rendering_runtime
log_message << active_record_runtime(runtime) if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected?
log_message << " | #{headers["Status"]}"
log_message << " [#{complete_request_uri rescue "unknown"}]"
logger.info(log_message)
end
end
private
def rendering_runtime(runtime)
" | Rendering: #{sprintf("%.5f", @rendering_runtime)} (#{sprintf("%d", (@rendering_runtime * 100) / runtime)}%)"
end
def active_record_runtime(runtime)
db_runtime = ActiveRecord::Base.connection.reset_runtime
db_runtime += @db_rt_before_render if @db_rt_before_render
db_runtime += @db_rt_after_render if @db_rt_after_render
db_percentage = (db_runtime * 100) / runtime
" | DB: #{sprintf("%.5f", db_runtime)} (#{sprintf("%d", db_percentage)}%)"
end
end
end
require 'fileutils'
module ActionController #:nodoc:
# Caching is a cheap way of speeding up slow applications by keeping the result of calculations, renderings, and database calls
# around for subsequent requests. Action Controller affords you three approaches in varying levels of granularity: Page, Action, Fragment.
#
# You can read more about each approach and the sweeping assistance by clicking the modules below.
#
# Note: To turn off all caching and sweeping, set Base.perform_caching = false.
module Caching
def self.included(base) #:nodoc:
base.send(:include, Pages, Actions, Fragments, Sweeping)
base.class_eval do
@@perform_caching = true
cattr_accessor :perform_caching
end
end
# Page caching is an approach to caching where the entire action output of is stored as a HTML file that the web server
# can serve without going through the Action Pack. This can be as much as 100 times faster than going through the process of dynamically
# generating the content. Unfortunately, this incredible speed-up is only available to stateless pages where all visitors
# are treated the same. Content management systems -- including weblogs and wikis -- have many pages that are a great fit
# for this approach, but account-based systems where people log in and manipulate their own data are often less likely candidates.
#
# Specifying which actions to cache is done through the caches class method:
#
# class WeblogController < ActionController::Base
# caches_page :show, :new
# end
#
# This will generate cache files such as weblog/show/5 and weblog/new, which match the URLs used to trigger the dynamic
# generation. This is how the web server is able pick up a cache file when it exists and otherwise let the request pass on to
# the Action Pack to generate it.
#
# Expiration of the cache is handled by deleting the cached file, which results in a lazy regeneration approach where the cache
# is not restored before another hit is made against it. The API for doing so mimics the options from url_for and friends:
#
# class WeblogController < ActionController::Base
# def update
# List.update(params[:list][:id], params[:list])
# expire_page :action => "show", :id => params[:list][:id]
# redirect_to :action => "show", :id => params[:list][:id]
# end
# end
#
# Additionally, you can expire caches using Sweepers that act on changes in the model to determine when a cache is supposed to be
# expired.
#
# == Setting the cache directory
#
# The cache directory should be the document root for the web server and is set using Base.page_cache_directory = "/document/root".
# For Rails, this directory has already been set to RAILS_ROOT + "/public".
#
# == Setting the cache extension
#
# By default, the cache extension is .html, which makes it easy for the cached files to be picked up by the web server. If you want
# something else, like .php or .shtml, just set Base.page_cache_extension.
module Pages
def self.included(base) #:nodoc:
base.extend(ClassMethods)
base.class_eval do
@@page_cache_directory = defined?(RAILS_ROOT) ? "#{RAILS_ROOT}/public" : ""
cattr_accessor :page_cache_directory
@@page_cache_extension = '.html'
cattr_accessor :page_cache_extension
end
end
module ClassMethods
# Expires the page that was cached with the +path+ as a key. Example:
# expire_page "/lists/show"
def expire_page(path)
return unless perform_caching
benchmark "Expired page: #{page_cache_file(path)}" do
File.delete(page_cache_path(path)) if File.exists?(page_cache_path(path))
end
end
# Manually cache the +content+ in the key determined by +path+. Example:
# cache_page "I'm the cached content", "/lists/show"
def cache_page(content, path)
return unless perform_caching
benchmark "Cached page: #{page_cache_file(path)}" do
FileUtils.makedirs(File.dirname(page_cache_path(path)))
File.open(page_cache_path(path), "wb+") { |f| f.write(content) }
end
end
# Caches the +actions+ using the page-caching approach that'll store the cache in a path within the page_cache_directory that
# matches the triggering url.
def caches_page(*actions)
return unless perform_caching
actions.each do |action|
class_eval "after_filter { |c| c.cache_page if c.action_name == '#{action}' }"
end
end
private
def page_cache_file(path)
name = ((path.empty? || path == "/") ? "/index" : URI.unescape(path))
name << page_cache_extension unless (name.split('/').last || name).include? '.'
return name
end
def page_cache_path(path)
page_cache_directory + page_cache_file(path)
end
end
# Expires the page that was cached with the +options+ as a key. Example:
# expire_page :controller => "lists", :action => "show"
def expire_page(options = {})
return unless perform_caching
if options[:action].is_a?(Array)
options[:action].dup.each do |action|
self.class.expire_page(url_for(options.merge({ :only_path => true, :skip_relative_url_root => true, :action => action })))
end
else
self.class.expire_page(url_for(options.merge({ :only_path => true, :skip_relative_url_root => true })))
end
end
# Manually cache the +content+ in the key determined by +options+. If no content is provided, the contents of @response.body is used
# If no options are provided, the current +options+ for this action is used. Example:
# cache_page "I'm the cached content", :controller => "lists", :action => "show"
def cache_page(content = nil, options = {})
return unless perform_caching && caching_allowed
self.class.cache_page(content || @response.body, url_for(options.merge({ :only_path => true, :skip_relative_url_root => true })))
end
private
def caching_allowed
!@request.post? && @response.headers['Status'] && @response.headers['Status'].to_i < 400
end
end
# Action caching is similar to page caching by the fact that the entire output of the response is cached, but unlike page caching,
# every request still goes through the Action Pack. The key benefit of this is that filters are run before the cache is served, which
# allows for authentication and other restrictions on whether someone is allowed to see the cache. Example:
#
# class ListsController < ApplicationController
# before_filter :authenticate, :except => :public
# caches_page :public
# caches_action :show, :feed
# end
#
# In this example, the public action doesn't require authentication, so it's possible to use the faster page caching method. But both the
# show and feed action are to be shielded behind the authenticate filter, so we need to implement those as action caches.
#
# Action caching internally uses the fragment caching and an around filter to do the job. The fragment cache is named according to both
# the current host and the path. So a page that is accessed at http://david.somewhere.com/lists/show/1 will result in a fragment named
# "david.somewhere.com/lists/show/1". This allows the cacher to differentiate between "david.somewhere.com/lists/" and
# "jamis.somewhere.com/lists/" -- which is a helpful way of assisting the subdomain-as-account-key pattern.
module Actions
def self.append_features(base) #:nodoc:
super
base.extend(ClassMethods)
base.send(:attr_accessor, :rendered_action_cache)
end
module ClassMethods #:nodoc:
def caches_action(*actions)
return unless perform_caching
around_filter(ActionCacheFilter.new(*actions))
end
end
def expire_action(options = {})
return unless perform_caching
if options[:action].is_a?(Array)
options[:action].dup.each do |action|
expire_fragment(url_for(options.merge({ :action => action })).split("://").last)
end
else
expire_fragment(url_for(options).split("://").last)
end
end
class ActionCacheFilter #:nodoc:
def initialize(*actions)
@actions = actions
end
def before(controller)
return unless @actions.include?(controller.action_name.intern)
if cache = controller.read_fragment(controller.url_for.split("://").last)
controller.rendered_action_cache = true
controller.send(:render_text, cache)
false
end
end
def after(controller)
return if !@actions.include?(controller.action_name.intern) || controller.rendered_action_cache
controller.write_fragment(controller.url_for.split("://").last, controller.response.body)
end
end
end
# Fragment caching is used for caching various blocks within templates without caching the entire action as a whole. This is useful when
# certain elements of an action change frequently or depend on complicated state while other parts rarely change or can be shared amongst multiple
# parties. The caching is doing using the cache helper available in the Action View. A template with caching might look something like:
#
# Hello <%= @name %>
# <% cache do %>
# All the topics in the system:
# <%= render_collection_of_partials "topic", Topic.find_all %>
# <% end %>
#
# This cache will bind to the name of action that called it. So you would be able to invalidate it using
# expire_fragment(:controller => "topics", :action => "list") -- if that was the controller/action used. This is not too helpful
# if you need to cache multiple fragments per action or if the action itself is cached using caches_action. So instead we should
# qualify the name of the action used with something like:
#
# <% cache(:action => "list", :action_suffix => "all_topics") do %>
#
# That would result in a name such as "/topics/list/all_topics", which wouldn't conflict with any action cache and neither with another
# fragment using a different suffix. Note that the URL doesn't have to really exist or be callable. We're just using the url_for system
# to generate unique cache names that we can refer to later for expirations. The expiration call for this example would be
# expire_fragment(:controller => "topics", :action => "list", :action_suffix => "all_topics").
#
# == Fragment stores
#
# In order to use the fragment caching, you need to designate where the caches should be stored. This is done by assigning a fragment store
# of which there are four different kinds:
#
# * FileStore: Keeps the fragments on disk in the +cache_path+, which works well for all types of environments and shares the fragments for
# all the web server processes running off the same application directory.
# * MemoryStore: Keeps the fragments in memory, which is fine for WEBrick and for FCGI (if you don't care that each FCGI process holds its
# own fragment store). It's not suitable for CGI as the process is thrown away at the end of each request. It can potentially also take
# up a lot of memory since each process keeps all the caches in memory.
# * DRbStore: Keeps the fragments in the memory of a separate, shared DRb process. This works for all environments and only keeps one cache
# around for all processes, but requires that you run and manage a separate DRb process.
# * MemCacheStore: Works like DRbStore, but uses Danga's MemCache instead.
# Requires the ruby-memcache library: gem install ruby-memcache.
#
# Configuration examples (MemoryStore is the default):
#
# ActionController::Base.fragment_cache_store = :memory_store
# ActionController::Base.fragment_cache_store = :file_store, "/path/to/cache/directory"
# ActionController::Base.fragment_cache_store = :drb_store, "druby://localhost:9192"
# ActionController::Base.fragment_cache_store = :mem_cache_store, "localhost"
# ActionController::Base.fragment_cache_store = MyOwnStore.new("parameter")
module Fragments
def self.append_features(base) #:nodoc:
super
base.class_eval do
@@fragment_cache_store = MemoryStore.new
cattr_reader :fragment_cache_store
def self.fragment_cache_store=(store_option)
store, *parameters = *([ store_option ].flatten)
@@fragment_cache_store = if store.is_a?(Symbol)
store_class_name = (store == :drb_store ? "DRbStore" : store.to_s.camelize)
store_class = ActionController::Caching::Fragments.const_get(store_class_name)
store_class.new(*parameters)
else
store
end
end
end
end
def fragment_cache_key(name)
name.is_a?(Hash) ? url_for(name).split("://").last : name
end
# Called by CacheHelper#cache
def cache_erb_fragment(block, name = {}, options = nil)
unless perform_caching then block.call; return end
buffer = eval("_erbout", block.binding)
if cache = read_fragment(name, options)
buffer.concat(cache)
else
pos = buffer.length
block.call
write_fragment(name, buffer[pos..-1], options)
end
end
def write_fragment(name, content, options = nil)
return unless perform_caching
key = fragment_cache_key(name)
self.class.benchmark "Cached fragment: #{key}" do
fragment_cache_store.write(key, content, options)
end
content
end
def read_fragment(name, options = nil)
return unless perform_caching
key = fragment_cache_key(name)
self.class.benchmark "Fragment read: #{key}" do
fragment_cache_store.read(key, options)
end
end
# Name can take one of three forms:
# * String: This would normally take the form of a path like "pages/45/notes"
# * Hash: Is treated as an implicit call to url_for, like { :controller => "pages", :action => "notes", :id => 45 }
# * Regexp: Will destroy all the matched fragments, example: %r{pages/\d*/notes} Ensure you do not specify start and finish in the regex (^$) because the actual filename matched looks like ./cache/filename/path.cache
def expire_fragment(name, options = nil)
return unless perform_caching
key = fragment_cache_key(name)
if key.is_a?(Regexp)
self.class.benchmark "Expired fragments matching: #{key.source}" do
fragment_cache_store.delete_matched(key, options)
end
else
self.class.benchmark "Expired fragment: #{key}" do
fragment_cache_store.delete(key, options)
end
end
end
# Deprecated -- just call expire_fragment with a regular expression
def expire_matched_fragments(matcher = /.*/, options = nil) #:nodoc:
expire_fragment(matcher, options)
end
class UnthreadedMemoryStore #:nodoc:
def initialize #:nodoc:
@data = {}
end
def read(name, options=nil) #:nodoc:
@data[name]
end
def write(name, value, options=nil) #:nodoc:
@data[name] = value
end
def delete(name, options=nil) #:nodoc:
@data.delete(name)
end
def delete_matched(matcher, options=nil) #:nodoc:
@data.delete_if { |k,v| k =~ matcher }
end
end
module ThreadSafety #:nodoc:
def read(name, options=nil) #:nodoc:
@mutex.synchronize { super }
end
def write(name, value, options=nil) #:nodoc:
@mutex.synchronize { super }
end
def delete(name, options=nil) #:nodoc:
@mutex.synchronize { super }
end
def delete_matched(matcher, options=nil) #:nodoc:
@mutex.synchronize { super }
end
end
class MemoryStore < UnthreadedMemoryStore #:nodoc:
def initialize #:nodoc:
super
if ActionController::Base.allow_concurrency
@mutex = Mutex.new
MemoryStore.send(:include, ThreadSafety)
end
end
end
class DRbStore < MemoryStore #:nodoc:
attr_reader :address
def initialize(address = 'druby://localhost:9192')
super()
@address = address
@data = DRbObject.new(nil, address)
end
end
class MemCacheStore < MemoryStore #:nodoc:
attr_reader :addresses
def initialize(*addresses)
super()
addresses = addresses.flatten
addresses = ["localhost"] if addresses.empty?
@addresses = addresses
@data = MemCache.new(*addresses)
end
end
class UnthreadedFileStore #:nodoc:
attr_reader :cache_path
def initialize(cache_path)
@cache_path = cache_path
end
def write(name, value, options = nil) #:nodoc:
ensure_cache_path(File.dirname(real_file_path(name)))
File.open(real_file_path(name), "wb+") { |f| f.write(value) }
rescue => e
Base.logger.error "Couldn't create cache directory: #{name} (#{e.message})" if Base.logger
end
def read(name, options = nil) #:nodoc:
File.open(real_file_path(name), 'rb') { |f| f.read } rescue nil
end
def delete(name, options) #:nodoc:
File.delete(real_file_path(name))
rescue SystemCallError => e
# If there's no cache, then there's nothing to complain about
end
def delete_matched(matcher, options) #:nodoc:
search_dir(@cache_path) do |f|
if f =~ matcher
begin
File.delete(f)
rescue Object => e
# If there's no cache, then there's nothing to complain about
end
end
end
end
private
def real_file_path(name)
'%s/%s.cache' % [@cache_path, name.gsub('?', '.').gsub(':', '.')]
end
def ensure_cache_path(path)
FileUtils.makedirs(path) unless File.exists?(path)
end
def search_dir(dir, &callback)
Dir.foreach(dir) do |d|
next if d == "." || d == ".."
name = File.join(dir, d)
if File.directory?(name)
search_dir(name, &callback)
else
callback.call name
end
end
end
end
class FileStore < UnthreadedFileStore #:nodoc:
def initialize(cache_path)
super(cache_path)
if ActionController::Base.allow_concurrency
@mutex = Mutex.new
FileStore.send(:include, ThreadSafety)
end
end
end
end
# Sweepers are the terminators of the caching world and responsible for expiring caches when model objects change.
# They do this by being half-observers, half-filters and implementing callbacks for both roles. A Sweeper example:
#
# class ListSweeper < ActionController::Caching::Sweeper
# observe List, Item
#
# def after_save(record)
# list = record.is_a?(List) ? record : record.list
# expire_page(:controller => "lists", :action => %w( show public feed ), :id => list.id)
# expire_action(:controller => "lists", :action => "all")
# list.shares.each { |share| expire_page(:controller => "lists", :action => "show", :id => share.url_key) }
# end
# end
#
# The sweeper is assigned in the controllers that wish to have its job performed using the cache_sweeper class method:
#
# class ListsController < ApplicationController
# caches_action :index, :show, :public, :feed
# cache_sweeper :list_sweeper, :only => [ :edit, :destroy, :share ]
# end
#
# In the example above, four actions are cached and three actions are responsible for expiring those caches.
module Sweeping
def self.append_features(base) #:nodoc:
super
base.extend(ClassMethods)
end
module ClassMethods #:nodoc:
def cache_sweeper(*sweepers)
return unless perform_caching
configuration = sweepers.last.is_a?(Hash) ? sweepers.pop : {}
sweepers.each do |sweeper|
observer(sweeper)
sweeper_instance = Object.const_get(Inflector.classify(sweeper)).instance
if sweeper_instance.is_a?(Sweeper)
around_filter(sweeper_instance, :only => configuration[:only])
else
after_filter(sweeper_instance, :only => configuration[:only])
end
end
end
end
end
if defined?(ActiveRecord) and defined?(ActiveRecord::Observer)
class Sweeper < ActiveRecord::Observer #:nodoc:
attr_accessor :controller
# ActiveRecord::Observer will mark this class as reloadable even though it should not be.
# However, subclasses of ActionController::Caching::Sweeper should be Reloadable
include Reloadable::Subclasses
def before(controller)
self.controller = controller
callback(:before)
end
def after(controller)
callback(:after)
# Clean up, so that the controller can be collected after this request
self.controller = nil
end
private
def callback(timing)
controller_callback_method_name = "#{timing}_#{controller.controller_name.underscore}"
action_callback_method_name = "#{controller_callback_method_name}_#{controller.action_name}"
send(controller_callback_method_name) if respond_to?(controller_callback_method_name)
send(action_callback_method_name) if respond_to?(action_callback_method_name)
end
def method_missing(method, *arguments)
return if @controller.nil?
@controller.send(method, *arguments)
end
end
end
end
end
require 'cgi'
require 'cgi/session'
require 'cgi/session/pstore'
require 'action_controller/cgi_ext/cgi_methods'
# Wrapper around the CGIMethods that have been secluded to allow testing without
# an instantiated CGI object
class CGI #:nodoc:
class << self
alias :escapeHTML_fail_on_nil :escapeHTML
def escapeHTML(string)
escapeHTML_fail_on_nil(string) unless string.nil?
end
end
# Returns a parameter hash including values from both the request (POST/GET)
# and the query string with the latter taking precedence.
def parameters
request_parameters.update(query_parameters)
end
def query_parameters
CGIMethods.parse_query_parameters(query_string)
end
def request_parameters
CGIMethods.parse_request_parameters(params, env_table)
end
def redirect(where)
header({
"Status" => "302 Moved",
"location" => "#{where}"
})
end
def session(parameters = nil)
parameters = {} if parameters.nil?
parameters['database_manager'] = CGI::Session::PStore
CGI::Session.new(self, parameters)
end
end
require 'cgi'
require 'action_controller/vendor/xml_simple'
require 'action_controller/vendor/xml_node'
# Static methods for parsing the query and request parameters that can be used in
# a CGI extension class or testing in isolation.
class CGIMethods #:nodoc:
public
# Returns a hash with the pairs from the query string. The implicit hash construction that is done in
# parse_request_params is not done here.
def CGIMethods.parse_query_parameters(query_string)
parsed_params = {}
query_string.split(/[&;]/).each { |p|
# Ignore repeated delimiters.
next if p.empty?
k, v = p.split('=',2)
v = nil if (v && v.empty?)
k = CGI.unescape(k) if k
v = CGI.unescape(v) if v
unless k.include?(?[)
parsed_params[k] = v
else
keys = split_key(k)
last_key = keys.pop
last_key = keys.pop if (use_array = last_key.empty?)
parent = keys.inject(parsed_params) {|h, k| h[k] ||= {}}
if use_array then (parent[last_key] ||= []) << v
else parent[last_key] = v
end
end
}
parsed_params
end
# Returns the request (POST/GET) parameters in a parsed form where pairs such as "customer[address][street]" /
# "Somewhere cool!" are translated into a full hash hierarchy, like
# { "customer" => { "address" => { "street" => "Somewhere cool!" } } }
def CGIMethods.parse_request_parameters(params)
parsed_params = {}
for key, value in params
value = [value] if key =~ /.*\[\]$/
unless key.include?('[')
# much faster to test for the most common case first (GET)
# and avoid the call to build_deep_hash
parsed_params[key] = get_typed_value(value[0])
else
build_deep_hash(get_typed_value(value[0]), parsed_params, get_levels(key))
end
end
parsed_params
end
def self.parse_formatted_request_parameters(mime_type, raw_post_data)
params = case strategy = ActionController::Base.param_parsers[mime_type]
when Proc
strategy.call(raw_post_data)
when :xml_simple
raw_post_data.blank? ? nil :
typecast_xml_value(XmlSimple.xml_in(raw_post_data,
'forcearray' => false,
'forcecontent' => true,
'keeproot' => true,
'contentkey' => '__content__'))
when :yaml
YAML.load(raw_post_data)
when :xml_node
node = XmlNode.from_xml(raw_post_data)
{ node.node_name => node }
end
dasherize_keys(params || {})
rescue Object => e
{ "exception" => "#{e.message} (#{e.class})", "backtrace" => e.backtrace,
"raw_post_data" => raw_post_data, "format" => mime_type }
end
def self.typecast_xml_value(value)
case value
when Hash
if value.has_key?("__content__")
content = translate_xml_entities(value["__content__"])
case value["type"]
when "integer" then content.to_i
when "boolean" then content == "true"
when "datetime" then Time.parse(content)
when "date" then Date.parse(content)
else content
end
else
value.empty? ? nil : value.inject({}) do |h,(k,v)|
h[k] = typecast_xml_value(v)
h
end
end
when Array
value.map! { |i| typecast_xml_value(i) }
case value.length
when 0 then nil
when 1 then value.first
else value
end
else
raise "can't typecast #{value.inspect}"
end
end
private
def self.translate_xml_entities(value)
value.gsub(/</, "<").
gsub(/>/, ">").
gsub(/"/, '"').
gsub(/'/, "'").
gsub(/&/, "&")
end
def self.dasherize_keys(params)
case params.class.to_s
when "Hash"
params.inject({}) do |h,(k,v)|
h[k.to_s.tr("-", "_")] = dasherize_keys(v)
h
end
when "Array"
params.map { |v| dasherize_keys(v) }
else
params
end
end
# Splits the given key into several pieces. Example keys are 'name', 'person[name]',
# 'person[name][first]', and 'people[]'. In each instance, an Array instance is returned.
# 'person[name][first]' produces ['person', 'name', 'first']; 'people[]' produces ['people', '']
def CGIMethods.split_key(key)
if /^([^\[]+)((?:\[[^\]]*\])+)$/ =~ key
keys = [$1]
keys.concat($2[1..-2].split(']['))
keys << '' if key[-2..-1] == '[]' # Have to add it since split will drop empty strings
keys
else
[key]
end
end
def CGIMethods.get_typed_value(value)
# test most frequent case first
if value.is_a?(String)
value
elsif value.respond_to?(:content_type) && ! value.content_type.blank?
# Uploaded file
unless value.respond_to?(:full_original_filename)
class << value
alias_method :full_original_filename, :original_filename
# Take the basename of the upload's original filename.
# This handles the full Windows paths given by Internet Explorer
# (and perhaps other broken user agents) without affecting
# those which give the lone filename.
# The Windows regexp is adapted from Perl's File::Basename.
def original_filename
if md = /^(?:.*[:\\\/])?(.*)/m.match(full_original_filename)
md.captures.first
else
File.basename full_original_filename
end
end
end
end
# Return the same value after overriding original_filename.
value
elsif value.respond_to?(:read)
# Value as part of a multipart request
value.read
elsif value.class == Array
value.collect { |v| CGIMethods.get_typed_value(v) }
else
# other value (neither string nor a multipart request)
value.to_s
end
end
PARAMS_HASH_RE = /^([^\[]+)(\[.*\])?(.)?.*$/
def CGIMethods.get_levels(key)
all, main, bracketed, trailing = PARAMS_HASH_RE.match(key).to_a
if main.nil?
[]
elsif trailing
[key]
elsif bracketed
[main] + bracketed.slice(1...-1).split('][')
else
[main]
end
end
def CGIMethods.build_deep_hash(value, hash, levels)
if levels.length == 0
value
elsif hash.nil?
{ levels.first => CGIMethods.build_deep_hash(value, nil, levels[1..-1]) }
else
hash.update({ levels.first => CGIMethods.build_deep_hash(value, hash[levels.first], levels[1..-1]) })
end
end
end
CGI.module_eval { remove_const "Cookie" }
class CGI #:nodoc:
# This is a cookie class that fixes the performance problems with the default one that ships with 1.8.1 and below.
# It replaces the inheritance on SimpleDelegator with DelegateClass(Array) following the suggestion from Matz on
# http://groups.google.com/groups?th=e3a4e68ba042f842&seekm=c3sioe%241qvm%241%40news.cybercity.dk#link14
class Cookie < DelegateClass(Array)
# Create a new CGI::Cookie object.
#
# The contents of the cookie can be specified as a +name+ and one
# or more +value+ arguments. Alternatively, the contents can
# be specified as a single hash argument. The possible keywords of
# this hash are as follows:
#
# name:: the name of the cookie. Required.
# value:: the cookie's value or list of values.
# path:: the path for which this cookie applies. Defaults to the
# base directory of the CGI script.
# domain:: the domain for which this cookie applies.
# expires:: the time at which this cookie expires, as a +Time+ object.
# secure:: whether this cookie is a secure cookie or not (default to
# false). Secure cookies are only transmitted to HTTPS
# servers.
#
# These keywords correspond to attributes of the cookie object.
def initialize(name = '', *value)
if name.kind_of?(String)
@name = name
@value = Array(value)
@domain = nil
@expires = nil
@secure = false
@path = nil
else
@name = name['name']
@value = Array(name['value'])
@domain = name['domain']
@expires = name['expires']
@secure = name['secure'] || false
@path = name['path']
end
unless @name
raise ArgumentError, "`name' required"
end
# simple support for IE
unless @path
%r|^(.*/)|.match(ENV['SCRIPT_NAME'])
@path = ($1 or '')
end
super(@value)
end
def __setobj__(obj)
@_dc_obj = obj
end
attr_accessor("name", "value", "path", "domain", "expires")
attr_reader("secure")
# Set whether the Cookie is a secure cookie or not.
#
# +val+ must be a boolean.
def secure=(val)
@secure = val if val == true or val == false
@secure
end
# Convert the Cookie to its string representation.
def to_s
buf = ""
buf << @name << '='
if @value.kind_of?(String)
buf << CGI::escape(@value)
else
buf << @value.collect{|v| CGI::escape(v) }.join("&")
end
if @domain
buf << '; domain=' << @domain
end
if @path
buf << '; path=' << @path
end
if @expires
buf << '; expires=' << CGI::rfc1123_date(@expires)
end
if @secure == true
buf << '; secure'
end
buf
end
# Parse a raw cookie string into a hash of cookie-name=>Cookie
# pairs.
#
# cookies = CGI::Cookie::parse("raw_cookie_string")
# # { "name1" => cookie1, "name2" => cookie2, ... }
#
def self.parse(raw_cookie)
cookies = Hash.new([])
if raw_cookie
raw_cookie.split(/; ?/).each do |pairs|
name, values = pairs.split('=',2)
next unless name and values
name = CGI::unescape(name)
values = values.split('&').collect!{|v| CGI::unescape(v) }
unless cookies.has_key?(name)
cookies[name] = new(name, *values)
end
end
end
cookies
end
end # class Cookie
end
class CGI #:nodoc:
# Add @request.env['RAW_POST_DATA'] for the vegans.
module QueryExtension
# Initialize the data from the query.
#
# Handles multipart forms (in particular, forms that involve file uploads).
# Reads query parameters in the @params field, and cookies into @cookies.
def initialize_query()
@cookies = CGI::Cookie::parse(env_table['HTTP_COOKIE'] || env_table['COOKIE'])
#fix some strange request environments
if method = env_table['REQUEST_METHOD']
method = method.to_s.downcase.intern
else
method = :get
end
if method == :post && (boundary = multipart_form_boundary)
@multipart = true
@params = read_multipart(boundary, Integer(env_table['CONTENT_LENGTH']))
else
@multipart = false
@params = CGI::parse(read_query_params(method) || "")
end
end
private
unless defined?(MULTIPART_FORM_BOUNDARY_RE)
MULTIPART_FORM_BOUNDARY_RE = %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|n #"
end
def multipart_form_boundary
MULTIPART_FORM_BOUNDARY_RE.match(env_table['CONTENT_TYPE']).to_a.pop
end
if defined? MOD_RUBY
def read_params_from_query
Apache::request.args || ''
end
else
def read_params_from_query
# fixes CGI querystring parsing for lighttpd
env_qs = env_table['QUERY_STRING']
if env_qs.blank? && !(uri = env_table['REQUEST_URI']).blank?
uri.split('?', 2)[1] || ''
else
env_qs
end
end
end
def read_params_from_post
stdinput.binmode if stdinput.respond_to?(:binmode)
content = stdinput.read(Integer(env_table['CONTENT_LENGTH'])) || ''
# fix for Safari Ajax postings that always append \000
content.chop! if content[-1] == 0
content.gsub! /&_=$/, ''
env_table['RAW_POST_DATA'] = content.freeze
end
def read_query_params(method)
case method
when :get
read_params_from_query
when :post, :put
read_params_from_post
when :cmd
read_from_cmdline
else # when :head, :delete, :options
read_params_from_query
end
end
end # module QueryExtension
end
require 'action_controller/cgi_ext/cgi_ext'
require 'action_controller/cgi_ext/cookie_performance_fix'
require 'action_controller/cgi_ext/raw_post_data_fix'
module ActionController #:nodoc:
class Base
# Process a request extracted from an CGI object and return a response. Pass false as session_options to disable
# sessions (large performance increase if sessions are not needed). The session_options are the same as for CGI::Session:
#
# * :database_manager - standard options are CGI::Session::FileStore, CGI::Session::MemoryStore, and CGI::Session::PStore
# (default). Additionally, there is CGI::Session::DRbStore and CGI::Session::ActiveRecordStore. Read more about these in
# lib/action_controller/session.
# * :session_key - the parameter name used for the session id. Defaults to '_session_id'.
# * :session_id - the session id to use. If not provided, then it is retrieved from the +session_key+ parameter
# of the request, or automatically generated for a new session.
# * :new_session - if true, force creation of a new session. If not set, a new session is only created if none currently
# exists. If false, a new session is never created, and if none currently exists and the +session_id+ option is not set,
# an ArgumentError is raised.
# * :session_expires - the time the current session expires, as a +Time+ object. If not set, the session will continue
# indefinitely.
# * :session_domain - the hostname domain for which this session is valid. If not set, defaults to the hostname of the
# server.
# * :session_secure - if +true+, this session will only work over HTTPS.
# * :session_path - the path for which this session applies. Defaults to the directory of the CGI script.
def self.process_cgi(cgi = CGI.new, session_options = {})
new.process_cgi(cgi, session_options)
end
def process_cgi(cgi, session_options = {}) #:nodoc:
process(CgiRequest.new(cgi, session_options), CgiResponse.new(cgi)).out
end
end
class CgiRequest < AbstractRequest #:nodoc:
attr_accessor :cgi, :session_options
DEFAULT_SESSION_OPTIONS = {
:database_manager => CGI::Session::PStore,
:prefix => "ruby_sess.",
:session_path => "/"
} unless const_defined?(:DEFAULT_SESSION_OPTIONS)
def initialize(cgi, session_options = {})
@cgi = cgi
@session_options = session_options
@env = @cgi.send(:env_table)
super()
end
def query_string
if (qs = @cgi.query_string) && !qs.empty?
qs
elsif uri = @env['REQUEST_URI']
parts = uri.split('?')
parts.shift
parts.join('?')
else
@env['QUERY_STRING'] || ''
end
end
def query_parameters
(qs = self.query_string).empty? ? {} : CGIMethods.parse_query_parameters(qs)
end
def request_parameters
@request_parameters ||=
if ActionController::Base.param_parsers.has_key?(content_type)
CGIMethods.parse_formatted_request_parameters(content_type, @env['RAW_POST_DATA'])
else
CGIMethods.parse_request_parameters(@cgi.params)
end
end
def cookies
@cgi.cookies.freeze
end
def host_with_port
if forwarded = env["HTTP_X_FORWARDED_HOST"]
forwarded.split(/,\s?/).last
elsif http_host = env['HTTP_HOST']
http_host
elsif server_name = env['SERVER_NAME']
server_name
else
"#{env['SERVER_ADDR']}:#{env['SERVER_PORT']}"
end
end
def host
host_with_port[/^[^:]+/]
end
def port
if host_with_port =~ /:(\d+)$/
$1.to_i
else
standard_port
end
end
def session
unless @session
if @session_options == false
@session = Hash.new
else
stale_session_check! do
if session_options_with_string_keys['new_session'] == true
@session = new_session
else
@session = CGI::Session.new(@cgi, session_options_with_string_keys)
end
@session['__valid_session']
end
end
end
@session
end
def reset_session
@session.delete if CGI::Session === @session
@session = new_session
end
def method_missing(method_id, *arguments)
@cgi.send(method_id, *arguments) rescue super
end
private
# Delete an old session if it exists then create a new one.
def new_session
if @session_options == false
Hash.new
else
CGI::Session.new(@cgi, session_options_with_string_keys.merge("new_session" => false)).delete rescue nil
CGI::Session.new(@cgi, session_options_with_string_keys.merge("new_session" => true))
end
end
def stale_session_check!
yield
rescue ArgumentError => argument_error
if argument_error.message =~ %r{undefined class/module (\w+)}
begin
Module.const_missing($1)
rescue LoadError, NameError => const_error
raise ActionController::SessionRestoreError, < e
# lost connection to the FCGI process -- ignore the output, then
end
end
private
def convert_content_type!(headers)
if header = headers.delete("Content-Type")
headers["type"] = header
end
if header = headers.delete("Content-type")
headers["type"] = header
end
if header = headers.delete("content-type")
headers["type"] = header
end
end
end
end
module ActionController
module CodeGeneration #:nodoc:
class GenerationError < StandardError #:nodoc:
end
class Source #:nodoc:
attr_reader :lines, :indentation_level
IndentationString = ' '
def initialize
@lines, @indentation_level = [], 0
end
def line(line)
@lines << (IndentationString * @indentation_level + line)
end
alias :<< :line
def indent
@indentation_level += 1
yield
ensure
@indentation_level -= 1
end
def to_s() lines.join("\n") end
end
class CodeGenerator #:nodoc:
attr_accessor :source, :locals
def initialize(source = nil)
@locals = []
@source = source || Source.new
end
BeginKeywords = %w(if unless begin until while def).collect {|kw| kw.to_sym}
ResumeKeywords = %w(elsif else rescue).collect {|kw| kw.to_sym}
Keywords = BeginKeywords + ResumeKeywords
def method_missing(keyword, *text)
if Keywords.include? keyword
if ResumeKeywords.include? keyword
raise GenerationError, "Can only resume with #{keyword} immediately after an end" unless source.lines.last =~ /^\s*end\s*$/
source.lines.pop # Remove the 'end'
end
line "#{keyword} #{text.join ' '}"
begin source.indent { yield(self.dup) }
ensure line 'end'
end
else
super(keyword, *text)
end
end
def line(*args) self.source.line(*args) end
alias :<< :line
def indent(*args, &block) source(*args, &block) end
def to_s() source.to_s end
def share_locals_with(other)
other.locals = self.locals = (other.locals | locals)
end
FieldsToDuplicate = [:locals]
def dup
copy = self.class.new(source)
self.class::FieldsToDuplicate.each do |sym|
value = self.send(sym)
value = value.dup unless value.nil? || value.is_a?(Numeric)
copy.send("#{sym}=", value)
end
return copy
end
end
class RecognitionGenerator < CodeGenerator #:nodoc:
Attributes = [:after, :before, :current, :results, :constants, :depth, :move_ahead, :finish_statement]
attr_accessor(*Attributes)
FieldsToDuplicate = CodeGenerator::FieldsToDuplicate + Attributes
def initialize(*args)
super(*args)
@after, @before = [], []
@current = nil
@results, @constants = {}, {}
@depth = 0
@move_ahead = nil
@finish_statement = Proc.new {|hash_expr| hash_expr}
end
def if_next_matches(string, &block)
test = Routing.test_condition(next_segment(true), string)
self.if(test, &block)
end
def move_forward(places = 1)
dup = self.dup
dup.depth += 1
dup.move_ahead = places
yield dup
end
def next_segment(assign_inline = false, default = nil)
if locals.include?(segment_name)
code = segment_name
else
code = "#{segment_name} = #{path_name}[#{index_name}]"
if assign_inline
code = "(#{code})"
else
line(code)
code = segment_name
end
locals << segment_name
end
code = "(#{code} || #{default.inspect})" if default
return code.to_s
end
def segment_name() "segment#{depth}".to_sym end
def path_name() :path end
def index_name
move_ahead, @move_ahead = @move_ahead, nil
move_ahead ? "index += #{move_ahead}" : 'index'
end
def continue
dup = self.dup
dup.before << dup.current
dup.current = dup.after.shift
dup.go
end
def go
if current then current.write_recognition(self)
else self.finish
end
end
def result(key, expression, delay = false)
unless delay
line "#{key}_value = #{expression}"
expression = "#{key}_value"
end
results[key] = expression
end
def constant_result(key, object)
constants[key] = object
end
def finish(ensure_traversal_finished = true)
pairs = []
(results.keys + constants.keys).uniq.each do |key|
pairs << "#{key.to_s.inspect} => #{results[key] ? results[key] : constants[key].inspect}"
end
hash_expr = "{#{pairs.join(', ')}}"
statement = finish_statement.call(hash_expr)
if ensure_traversal_finished then self.if("! #{next_segment(true)}") {|gp| gp << statement}
else self << statement
end
end
end
class GenerationGenerator < CodeGenerator #:nodoc:
Attributes = [:after, :before, :current, :segments]
attr_accessor(*Attributes)
FieldsToDuplicate = CodeGenerator::FieldsToDuplicate + Attributes
def initialize(*args)
super(*args)
@after, @before = [], []
@current = nil
@segments = []
end
def hash_name() 'hash' end
def local_name(key) "#{key}_value" end
def hash_value(key, assign = true, default = nil)
if locals.include?(local_name(key)) then code = local_name(key)
else
code = "hash[#{key.to_sym.inspect}]"
if assign
code = "(#{local_name(key)} = #{code})"
locals << local_name(key)
end
end
code = "(#{code} || (#{default.inspect}))" if default
return code
end
def expire_for_keys(*keys)
return if keys.empty?
conds = keys.collect {|key| "expire_on[#{key.to_sym.inspect}]"}
line "not_expired, #{hash_name} = false, options if not_expired && #{conds.join(' && ')}"
end
def add_segment(*segments)
d = dup
d.segments.concat segments
yield d
end
def go
if current then current.write_generation(self)
else self.finish
end
end
def continue
d = dup
d.before << d.current
d.current = d.after.shift
d.go
end
def finish
line %("/#{segments.join('/')}")
end
def check_conditions(conditions)
tests = []
generator = nil
conditions.each do |key, condition|
tests << (generator || self).hash_value(key, true) if condition.is_a? Regexp
tests << Routing.test_condition((generator || self).hash_value(key, false), condition)
generator = self.dup unless generator
end
return tests.join(' && ')
end
end
end
end
module ActionController #:nodoc:
# Components allow you to call other actions for their rendered response while executing another action. You can either delegate
# the entire response rendering or you can mix a partial response in with your other content.
#
# class WeblogController < ActionController::Base
# # Performs a method and then lets hello_world output its render
# def delegate_action
# do_other_stuff_before_hello_world
# render_component :controller => "greeter", :action => "hello_world", :params => { :person => "david" }
# end
# end
#
# class GreeterController < ActionController::Base
# def hello_world
# render :text => "#{params[:person]} says, Hello World!"
# end
# end
#
# The same can be done in a view to do a partial rendering:
#
# Let's see a greeting:
# <%= render_component :controller => "greeter", :action => "hello_world" %>
#
# It is also possible to specify the controller as a class constant, bypassing the inflector
# code to compute the controller class at runtime:
#
# <%= render_component :controller => GreeterController, :action => "hello_world" %>
#
# == When to use components
#
# Components should be used with care. They're significantly slower than simply splitting reusable parts into partials and
# conceptually more complicated. Don't use components as a way of separating concerns inside a single application. Instead,
# reserve components to those rare cases where you truly have reusable view and controller elements that can be employed
# across many applications at once.
#
# So to repeat: Components are a special-purpose approach that can often be replaced with better use of partials and filters.
module Components
def self.included(base) #:nodoc:
base.send :include, InstanceMethods
base.extend(ClassMethods)
base.helper do
def render_component(options)
@controller.send(:render_component_as_string, options)
end
end
# If this controller was instantiated to process a component request,
# +parent_controller+ points to the instantiator of this controller.
base.send :attr_accessor, :parent_controller
base.class_eval do
alias_method :process_cleanup_without_components, :process_cleanup
alias_method :process_cleanup, :process_cleanup_with_components
alias_method :set_session_options_without_components, :set_session_options
alias_method :set_session_options, :set_session_options_with_components
alias_method :flash_without_components, :flash
alias_method :flash, :flash_with_components
alias_method :component_request?, :parent_controller
end
end
module ClassMethods
# Track parent controller to identify component requests
def process_with_components(request, response, parent_controller = nil) #:nodoc:
controller = new
controller.parent_controller = parent_controller
controller.process(request, response)
end
# Set the template root to be one directory behind the root dir of the controller. Examples:
# /code/weblog/components/admin/users_controller.rb with Admin::UsersController
# will use /code/weblog/components as template root
# and find templates in /code/weblog/components/admin/users/
#
# /code/weblog/components/admin/parties/users_controller.rb with Admin::Parties::UsersController
# will also use /code/weblog/components as template root
# and find templates in /code/weblog/components/admin/parties/users/
def uses_component_template_root
path_of_calling_controller = File.dirname(caller[0].split(/:\d+:/).first)
path_of_controller_root = path_of_calling_controller.sub(/#{controller_path.split("/")[0..-2]}$/, "") # " (for ruby-mode)
self.template_root = path_of_controller_root
end
end
module InstanceMethods
# Extracts the action_name from the request parameters and performs that action.
def process_with_components(request, response, method = :perform_action, *arguments) #:nodoc:
flash.discard if component_request?
process_without_components(request, response, method, *arguments)
end
protected
# Renders the component specified as the response for the current method
def render_component(options) #:doc:
component_logging(options) do
render_text(component_response(options, true).body, response.headers["Status"])
end
end
# Returns the component response as a string
def render_component_as_string(options) #:doc:
component_logging(options) do
response = component_response(options, false)
if redirected = response.redirected_to
render_component_as_string(redirected)
else
response.body
end
end
end
def flash_with_components(refresh = false) #:nodoc:
if @flash.nil? || refresh
@flash =
if @parent_controller
@parent_controller.flash
else
flash_without_components
end
end
@flash
end
private
def component_response(options, reuse_response)
klass = component_class(options)
request = request_for_component(klass.controller_name, options)
response = reuse_response ? @response : @response.dup
klass.process_with_components(request, response, self)
end
# determine the controller class for the component request
def component_class(options)
if controller = options[:controller]
controller.is_a?(Class) ? controller : "#{controller.camelize}Controller".constantize
else
self.class
end
end
# Create a new request object based on the current request.
# The new request inherits the session from the current request,
# bypassing any session options set for the component controller's class
def request_for_component(controller_name, options)
request = @request.dup
request.session = @request.session
request.instance_variable_set(
:@parameters,
(options[:params] || {}).with_indifferent_access.update(
"controller" => controller_name, "action" => options[:action], "id" => options[:id]
)
)
request
end
def component_logging(options)
if logger
logger.info "Start rendering component (#{options.inspect}): "
result = yield
logger.info "\n\nEnd of component rendering"
result
else
yield
end
end
def set_session_options_with_components(request)
set_session_options_without_components(request) unless component_request?
end
def process_cleanup_with_components
process_cleanup_without_components unless component_request?
end
end
end
end
module ActionController #:nodoc:
# Cookies are read and written through ActionController#cookies. The cookies being read are what were received along with the request,
# the cookies being written are what will be sent out with the response. Cookies are read by value (so you won't get the cookie object
# itself back -- just the value it holds). Examples for writing:
#
# cookies[:user_name] = "david" # => Will set a simple session cookie
# cookies[:login] = { :value => "XJ-122", :expires => Time.now + 360} # => Will set a cookie that expires in 1 hour
#
# Examples for reading:
#
# cookies[:user_name] # => "david"
# cookies.size # => 2
#
# Example for deleting:
#
# cookies.delete :user_name
#
# All the option symbols for setting cookies are:
#
# * value - the cookie's value or list of values (as an array).
# * path - the path for which this cookie applies. Defaults to the root of the application.
# * domain - the domain for which this cookie applies.
# * expires - the time at which this cookie expires, as a +Time+ object.
# * secure - whether this cookie is a secure cookie or not (default to false).
# Secure cookies are only transmitted to HTTPS servers.
module Cookies
protected
# Returns the cookie container, which operates as described above.
def cookies
CookieJar.new(self)
end
# Deprecated cookie writer method
def cookie(*options)
@response.headers["cookie"] << CGI::Cookie.new(*options)
end
end
class CookieJar < Hash #:nodoc:
def initialize(controller)
@controller, @cookies = controller, controller.instance_variable_get("@cookies")
super()
update(@cookies)
end
# Returns the value of the cookie by +name+ -- or nil if no such cookie exists. You set new cookies using either the cookie method
# or cookies[]= (for simple name/value cookies without options).
def [](name)
@cookies[name.to_s].value.first if @cookies[name.to_s] && @cookies[name.to_s].respond_to?(:value)
end
def []=(name, options)
if options.is_a?(Hash)
options = options.inject({}) { |options, pair| options[pair.first.to_s] = pair.last; options }
options["name"] = name.to_s
else
options = { "name" => name.to_s, "value" => options }
end
set_cookie(options)
end
# Removes the cookie on the client machine by setting the value to an empty string
# and setting its expiration date into the past
def delete(name)
set_cookie("name" => name.to_s, "value" => "", "expires" => Time.at(0))
end
private
def set_cookie(options) #:doc:
options["path"] = "/" unless options["path"]
cookie = CGI::Cookie.new(options)
@controller.logger.info "Cookie set: #{cookie}" unless @controller.logger.nil?
@controller.response.headers["cookie"] << cookie
end
end
end
module ActionController #:nodoc:
module Dependencies #:nodoc:
def self.append_features(base)
super
base.extend(ClassMethods)
end
# Dependencies control what classes are needed for the controller to run its course. This is an alternative to doing explicit
# +require+ statements that bring a number of benefits. It's more succinct, communicates what type of dependency we're talking about,
# can trigger special behavior (as in the case of +observer+), and enables Rails to be clever about reloading in cached environments
# like FCGI. Example:
#
# class ApplicationController < ActionController::Base
# model :account, :company, :person, :project, :category
# helper :access_control
# service :notifications, :billings
# observer :project_change_observer
# end
#
# Please note that a controller like ApplicationController will automatically attempt to require_dependency on a model of its
# singuralized name and a helper of its name. If nothing is found, no error is raised. This is especially useful for concrete
# controllers like PostController:
#
# class PostController < ApplicationController
# # model :post (already required)
# # helper :post (already required)
# end
#
# Also note, that if the models follow the pattern of just 1 class per file in the form of MyClass => my_class.rb, then these
# classes don't have to be required as Active Support will auto-require them.
module ClassMethods #:nodoc:
# Specifies a variable number of models that this controller depends on. Models are normally Active Record classes or a similar
# backend for modelling entity classes.
def model(*models)
require_dependencies(:model, models)
depend_on(:model, models)
end
# Specifies a variable number of services that this controller depends on. Services are normally singletons or factories, like
# Action Mailer service or a Payment Gateway service.
def service(*services)
require_dependencies(:service, services)
depend_on(:service, services)
end
# Specifies a variable number of observers that are to govern when this controller is handling actions. The observers will
# automatically have .instance called on them to make them active on assignment.
def observer(*observers)
require_dependencies(:observer, observers)
depend_on(:observer, observers)
instantiate_observers(observers)
end
# Returns an array of symbols that specify the dependencies on a given layer. For the example at the top, calling
# ApplicationController.dependencies_on(:model) would return [:account, :company, :person, :project, :category]
def dependencies_on(layer)
read_inheritable_attribute("#{layer}_dependencies")
end
def depend_on(layer, dependencies) #:nodoc:
write_inheritable_array("#{layer}_dependencies", dependencies)
end
private
def instantiate_observers(observers)
observers.flatten.each { |observer| Object.const_get(Inflector.classify(observer.to_s)).instance }
end
def require_dependencies(layer, dependencies)
dependencies.flatten.each do |dependency|
begin
require_dependency(dependency.to_s)
rescue LoadError => e
raise LoadError.new("Missing #{layer} #{dependency}.rb").copy_blame!(e)
rescue Object => exception
exception.blame_file! "=> #{layer} #{dependency}.rb"
raise
end
end
end
end
end
end
require 'test/unit'
require 'test/unit/assertions'
require 'rexml/document'
module Test #:nodoc:
module Unit #:nodoc:
module Assertions
def assert_success(message=nil) #:nodoc:
assert_response(:success, message)
end
def assert_redirect(message=nil) #:nodoc:
assert_response(:redirect, message)
end
def assert_rendered_file(expected=nil, message=nil) #:nodoc:
assert_template(expected, message)
end
# ensure that the session has an object with the specified name
def assert_session_has(key=nil, message=nil) #:nodoc:
msg = build_message(message, "> is not in the session >", key, @response.session)
assert_block(msg) { @response.has_session_object?(key) }
end
# ensure that the session has no object with the specified name
def assert_session_has_no(key=nil, message=nil) #:nodoc:
msg = build_message(message, "> is in the session >", key, @response.session)
assert_block(msg) { !@response.has_session_object?(key) }
end
def assert_session_equal(expected = nil, key = nil, message = nil) #:nodoc:
msg = build_message(message, "> expected in session['?'] but was >", expected, key, @response.session[key])
assert_block(msg) { expected == @response.session[key] }
end
# -- cookie assertions ---------------------------------------------------
def assert_no_cookie(key = nil, message = nil) #:nodoc:
actual = @response.cookies[key]
msg = build_message(message, "> not expected in cookies['?']", actual, key)
assert_block(msg) { actual.nil? or actual.empty? }
end
def assert_cookie_equal(expected = nil, key = nil, message = nil) #:nodoc:
actual = @response.cookies[key]
actual = actual.first if actual
msg = build_message(message, "> expected in cookies['?'] but was >", expected, key, actual)
assert_block(msg) { expected == actual }
end
# -- flash assertions ---------------------------------------------------
# ensure that the flash has an object with the specified name
def assert_flash_has(key=nil, message=nil) #:nodoc:
msg = build_message(message, "> is not in the flash >", key, @response.flash)
assert_block(msg) { @response.has_flash_object?(key) }
end
# ensure that the flash has no object with the specified name
def assert_flash_has_no(key=nil, message=nil) #:nodoc:
msg = build_message(message, "> is in the flash >", key, @response.flash)
assert_block(msg) { !@response.has_flash_object?(key) }
end
# ensure the flash exists
def assert_flash_exists(message=nil) #:nodoc:
msg = build_message(message, "the flash does not exist >", @response.session['flash'] )
assert_block(msg) { @response.has_flash? }
end
# ensure the flash does not exist
def assert_flash_not_exists(message=nil) #:nodoc:
msg = build_message(message, "the flash exists >", @response.flash)
assert_block(msg) { !@response.has_flash? }
end
# ensure the flash is empty but existent
def assert_flash_empty(message=nil) #:nodoc:
msg = build_message(message, "the flash is not empty >", @response.flash)
assert_block(msg) { !@response.has_flash_with_contents? }
end
# ensure the flash is not empty
def assert_flash_not_empty(message=nil) #:nodoc:
msg = build_message(message, "the flash is empty")
assert_block(msg) { @response.has_flash_with_contents? }
end
def assert_flash_equal(expected = nil, key = nil, message = nil) #:nodoc:
msg = build_message(message, "> expected in flash['?'] but was >", expected, key, @response.flash[key])
assert_block(msg) { expected == @response.flash[key] }
end
# ensure our redirection url is an exact match
def assert_redirect_url(url=nil, message=nil) #:nodoc:
assert_redirect(message)
msg = build_message(message, "> is not the redirected location >", url, @response.redirect_url)
assert_block(msg) { @response.redirect_url == url }
end
# ensure our redirection url matches a pattern
def assert_redirect_url_match(pattern=nil, message=nil) #:nodoc:
assert_redirect(message)
msg = build_message(message, "> was not found in the location: >", pattern, @response.redirect_url)
assert_block(msg) { @response.redirect_url_match?(pattern) }
end
# -- template assertions ------------------------------------------------
# ensure that a template object with the given name exists
def assert_template_has(key=nil, message=nil) #:nodoc:
msg = build_message(message, "> is not a template object", key )
assert_block(msg) { @response.has_template_object?(key) }
end
# ensure that a template object with the given name does not exist
def assert_template_has_no(key=nil,message=nil) #:nodoc:
msg = build_message(message, "> is a template object >", key, @response.template_objects[key])
assert_block(msg) { !@response.has_template_object?(key) }
end
# ensures that the object assigned to the template on +key+ is equal to +expected+ object.
def assert_template_equal(expected = nil, key = nil, message = nil) #:nodoc:
msg = build_message(message, "> expected in assigns['?'] but was >", expected, key, @response.template.assigns[key.to_s])
assert_block(msg) { expected == @response.template.assigns[key.to_s] }
end
alias_method :assert_assigned_equal, :assert_template_equal
# Asserts that the template returns the +expected+ string or array based on the XPath +expression+.
# This will only work if the template rendered a valid XML document.
def assert_template_xpath_match(expression=nil, expected=nil, message=nil) #:nodoc:
xml, matches = REXML::Document.new(@response.body), []
xml.elements.each(expression) { |e| matches << e.text }
if matches.empty? then
msg = build_message(message, "> not found in document", expression)
flunk(msg)
return
elsif matches.length < 2 then
matches = matches.first
end
msg = build_message(message, "> found >, not >", expression, matches, expected)
assert_block(msg) { matches == expected }
end
# Assert the template object with the given name is an Active Record descendant and is valid.
def assert_valid_record(key = nil, message = nil) #:nodoc:
record = find_record_in_template(key)
msg = build_message(message, "Active Record is invalid >)", record.errors.full_messages)
assert_block(msg) { record.valid? }
end
# Assert the template object with the given name is an Active Record descendant and is invalid.
def assert_invalid_record(key = nil, message = nil) #:nodoc:
record = find_record_in_template(key)
msg = build_message(message, "Active Record is valid)")
assert_block(msg) { !record.valid? }
end
# Assert the template object with the given name is an Active Record descendant and the specified column(s) are valid.
def assert_valid_column_on_record(key = nil, columns = "", message = nil) #:nodoc:
record = find_record_in_template(key)
record.send(:validate)
cols = glue_columns(columns)
cols.delete_if { |col| !record.errors.invalid?(col) }
msg = build_message(message, "Active Record has invalid columns >)", cols.join(",") )
assert_block(msg) { cols.empty? }
end
# Assert the template object with the given name is an Active Record descendant and the specified column(s) are invalid.
def assert_invalid_column_on_record(key = nil, columns = "", message = nil) #:nodoc:
record = find_record_in_template(key)
record.send(:validate)
cols = glue_columns(columns)
cols.delete_if { |col| record.errors.invalid?(col) }
msg = build_message(message, "Active Record has valid columns >)", cols.join(",") )
assert_block(msg) { cols.empty? }
end
private
def glue_columns(columns)
cols = []
cols << columns if columns.class == String
cols += columns if columns.class == Array
cols
end
def find_record_in_template(key = nil)
assert_template_has(key)
record = @response.template_objects[key]
assert_not_nil(record)
assert_kind_of ActiveRecord::Base, record
return record
end
end
end
endmodule ActionController
class Base
protected
# Deprecated in favor of calling redirect_to directly with the path.
def redirect_to_path(path) #:nodoc:
redirect_to(path)
end
# Deprecated in favor of calling redirect_to directly with the url. If the resource has moved permanently, it's possible to pass
# true as the second parameter and the browser will get "301 Moved Permanently" instead of "302 Found". This can also be done through
# just setting the headers["Status"] to "301 Moved Permanently" before using the redirect_to.
def redirect_to_url(url, permanently = false) #:nodoc:
headers["Status"] = "301 Moved Permanently" if permanently
redirect_to(url)
end
end
end
module ActionController
class AbstractRequest
# Determine whether the body of a HTTP call is URL-encoded (default)
# or matches one of the registered param_parsers.
#
# For backward compatibility, the post format is extracted from the
# X-Post-Data-Format HTTP header if present.
def post_format
case content_type.to_s
when 'application/xml'
:xml
when 'application/x-yaml'
:yaml
else
:url_encoded
end
end
# Is this a POST request formatted as XML or YAML?
def formatted_post?
post? && (post_format == :yaml || post_format == :xml)
end
# Is this a POST request formatted as XML?
def xml_post?
post? && post_format == :xml
end
# Is this a POST request formatted as YAML?
def yaml_post?
post? && post_format == :yaml
end
end
end
module ActionController #:nodoc:
module Filters #:nodoc:
def self.included(base)
base.extend(ClassMethods)
base.send(:include, ActionController::Filters::InstanceMethods)
end
# Filters enable controllers to run shared pre and post processing code for its actions. These filters can be used to do
# authentication, caching, or auditing before the intended action is performed. Or to do localization or output
# compression after the action has been performed.
#
# Filters have access to the request, response, and all the instance variables set by other filters in the chain
# or by the action (in the case of after filters). Additionally, it's possible for a pre-processing before_filter
# to halt the processing before the intended action is processed by returning false or performing a redirect or render.
# This is especially useful for filters like authentication where you're not interested in allowing the action to be
# performed if the proper credentials are not in order.
#
# == Filter inheritance
#
# Controller inheritance hierarchies share filters downwards, but subclasses can also add new filters without
# affecting the superclass. For example:
#
# class BankController < ActionController::Base
# before_filter :audit
#
# private
# def audit
# # record the action and parameters in an audit log
# end
# end
#
# class VaultController < BankController
# before_filter :verify_credentials
#
# private
# def verify_credentials
# # make sure the user is allowed into the vault
# end
# end
#
# Now any actions performed on the BankController will have the audit method called before. On the VaultController,
# first the audit method is called, then the verify_credentials method. If the audit method returns false, then
# verify_credentials and the intended action are never called.
#
# == Filter types
#
# A filter can take one of three forms: method reference (symbol), external class, or inline method (proc). The first
# is the most common and works by referencing a protected or private method somewhere in the inheritance hierarchy of
# the controller by use of a symbol. In the bank example above, both BankController and VaultController use this form.
#
# Using an external class makes for more easily reused generic filters, such as output compression. External filter classes
# are implemented by having a static +filter+ method on any class and then passing this class to the filter method. Example:
#
# class OutputCompressionFilter
# def self.filter(controller)
# controller.response.body = compress(controller.response.body)
# end
# end
#
# class NewspaperController < ActionController::Base
# after_filter OutputCompressionFilter
# end
#
# The filter method is passed the controller instance and is hence granted access to all aspects of the controller and can
# manipulate them as it sees fit.
#
# The inline method (using a proc) can be used to quickly do something small that doesn't require a lot of explanation.
# Or just as a quick test. It works like this:
#
# class WeblogController < ActionController::Base
# before_filter { |controller| false if controller.params["stop_action"] }
# end
#
# As you can see, the block expects to be passed the controller after it has assigned the request to the internal variables.
# This means that the block has access to both the request and response objects complete with convenience methods for params,
# session, template, and assigns. Note: The inline method doesn't strictly have to be a block; any object that responds to call
# and returns 1 or -1 on arity will do (such as a Proc or an Method object).
#
# == Filter chain ordering
#
# Using before_filter and after_filter appends the specified filters to the existing chain. That's usually
# just fine, but some times you care more about the order in which the filters are executed. When that's the case, you
# can use prepend_before_filter and prepend_after_filter. Filters added by these methods will be put at the
# beginning of their respective chain and executed before the rest. For example:
#
# class ShoppingController
# before_filter :verify_open_shop
#
# class CheckoutController
# prepend_before_filter :ensure_items_in_cart, :ensure_items_in_stock
#
# The filter chain for the CheckoutController is now :ensure_items_in_cart, :ensure_items_in_stock,
# :verify_open_shop. So if either of the ensure filters return false, we'll never get around to see if the shop
# is open or not.
#
# You may pass multiple filter arguments of each type as well as a filter block.
# If a block is given, it is treated as the last argument.
#
# == Around filters
#
# In addition to the individual before and after filters, it's also possible to specify that a single object should handle
# both the before and after call. That's especially useful when you need to keep state active between the before and after,
# such as the example of a benchmark filter below:
#
# class WeblogController < ActionController::Base
# around_filter BenchmarkingFilter.new
#
# # Before this action is performed, BenchmarkingFilter#before(controller) is executed
# def index
# end
# # After this action has been performed, BenchmarkingFilter#after(controller) is executed
# end
#
# class BenchmarkingFilter
# def initialize
# @runtime
# end
#
# def before
# start_timer
# end
#
# def after
# stop_timer
# report_result
# end
# end
#
# == Filter chain skipping
#
# Some times its convenient to specify a filter chain in a superclass that'll hold true for the majority of the
# subclasses, but not necessarily all of them. The subclasses that behave in exception can then specify which filters
# they would like to be relieved of. Examples
#
# class ApplicationController < ActionController::Base
# before_filter :authenticate
# end
#
# class WeblogController < ApplicationController
# # will run the :authenticate filter
# end
#
# class SignupController < ApplicationController
# # will not run the :authenticate filter
# skip_before_filter :authenticate
# end
#
# == Filter conditions
#
# Filters can be limited to run for only specific actions. This can be expressed either by listing the actions to
# exclude or the actions to include when executing the filter. Available conditions are +:only+ or +:except+, both
# of which accept an arbitrary number of method references. For example:
#
# class Journal < ActionController::Base
# # only require authentication if the current action is edit or delete
# before_filter :authorize, :only => [ :edit, :delete ]
#
# private
# def authorize
# # redirect to login unless authenticated
# end
# end
#
# When setting conditions on inline method (proc) filters the condition must come first and be placed in parentheses.
#
# class UserPreferences < ActionController::Base
# before_filter(:except => :new) { # some proc ... }
# # ...
# end
#
module ClassMethods
# The passed filters will be appended to the array of filters that's run _before_ actions
# on this controller are performed.
def append_before_filter(*filters, &block)
conditions = extract_conditions!(filters)
filters << block if block_given?
add_action_conditions(filters, conditions)
append_filter_to_chain('before', filters)
end
# The passed filters will be prepended to the array of filters that's run _before_ actions
# on this controller are performed.
def prepend_before_filter(*filters, &block)
conditions = extract_conditions!(filters)
filters << block if block_given?
add_action_conditions(filters, conditions)
prepend_filter_to_chain('before', filters)
end
# Short-hand for append_before_filter since that's the most common of the two.
alias :before_filter :append_before_filter
# The passed filters will be appended to the array of filters that's run _after_ actions
# on this controller are performed.
def append_after_filter(*filters, &block)
conditions = extract_conditions!(filters)
filters << block if block_given?
add_action_conditions(filters, conditions)
append_filter_to_chain('after', filters)
end
# The passed filters will be prepended to the array of filters that's run _after_ actions
# on this controller are performed.
def prepend_after_filter(*filters, &block)
conditions = extract_conditions!(filters)
filters << block if block_given?
add_action_conditions(filters, conditions)
prepend_filter_to_chain("after", filters)
end
# Short-hand for append_after_filter since that's the most common of the two.
alias :after_filter :append_after_filter
# The passed filters will have their +before+ method appended to the array of filters that's run both before actions
# on this controller are performed and have their +after+ method prepended to the after actions. The filter objects must all
# respond to both +before+ and +after+. So if you do append_around_filter A.new, B.new, the callstack will look like:
#
# B#before
# A#before
# A#after
# B#after
def append_around_filter(*filters)
conditions = extract_conditions!(filters)
for filter in filters.flatten
ensure_filter_responds_to_before_and_after(filter)
append_before_filter(conditions || {}) { |c| filter.before(c) }
prepend_after_filter(conditions || {}) { |c| filter.after(c) }
end
end
# The passed filters will have their +before+ method prepended to the array of filters that's run both before actions
# on this controller are performed and have their +after+ method appended to the after actions. The filter objects must all
# respond to both +before+ and +after+. So if you do prepend_around_filter A.new, B.new, the callstack will look like:
#
# A#before
# B#before
# B#after
# A#after
def prepend_around_filter(*filters)
for filter in filters.flatten
ensure_filter_responds_to_before_and_after(filter)
prepend_before_filter { |c| filter.before(c) }
append_after_filter { |c| filter.after(c) }
end
end
# Short-hand for append_around_filter since that's the most common of the two.
alias :around_filter :append_around_filter
# Removes the specified filters from the +before+ filter chain. Note that this only works for skipping method-reference
# filters, not procs. This is especially useful for managing the chain in inheritance hierarchies where only one out
# of many sub-controllers need a different hierarchy.
#
# You can control the actions to skip the filter for with the :only and :except options,
# just like when you apply the filters.
def skip_before_filter(*filters)
if conditions = extract_conditions!(filters)
remove_contradicting_conditions!(filters, conditions)
conditions[:only], conditions[:except] = conditions[:except], conditions[:only]
add_action_conditions(filters, conditions)
else
for filter in filters.flatten
write_inheritable_attribute("before_filters", read_inheritable_attribute("before_filters") - [ filter ])
end
end
end
# Removes the specified filters from the +after+ filter chain. Note that this only works for skipping method-reference
# filters, not procs. This is especially useful for managing the chain in inheritance hierarchies where only one out
# of many sub-controllers need a different hierarchy.
#
# You can control the actions to skip the filter for with the :only and :except options,
# just like when you apply the filters.
def skip_after_filter(*filters)
if conditions = extract_conditions!(filters)
remove_contradicting_conditions!(filters, conditions)
conditions[:only], conditions[:except] = conditions[:except], conditions[:only]
add_action_conditions(filters, conditions)
else
for filter in filters.flatten
write_inheritable_attribute("after_filters", read_inheritable_attribute("after_filters") - [ filter ])
end
end
end
# Returns all the before filters for this class and all its ancestors.
def before_filters #:nodoc:
@before_filters ||= read_inheritable_attribute("before_filters") || []
end
# Returns all the after filters for this class and all its ancestors.
def after_filters #:nodoc:
@after_filters ||= read_inheritable_attribute("after_filters") || []
end
# Returns a mapping between filters and the actions that may run them.
def included_actions #:nodoc:
@included_actions ||= read_inheritable_attribute("included_actions") || {}
end
# Returns a mapping between filters and actions that may not run them.
def excluded_actions #:nodoc:
@excluded_actions ||= read_inheritable_attribute("excluded_actions") || {}
end
private
def append_filter_to_chain(condition, filters)
write_inheritable_array("#{condition}_filters", filters)
end
def prepend_filter_to_chain(condition, filters)
old_filters = read_inheritable_attribute("#{condition}_filters") || []
write_inheritable_attribute("#{condition}_filters", filters + old_filters)
end
def ensure_filter_responds_to_before_and_after(filter)
unless filter.respond_to?(:before) && filter.respond_to?(:after)
raise ActionControllerError, "Filter object must respond to both before and after"
end
end
def extract_conditions!(filters)
return nil unless filters.last.is_a? Hash
filters.pop
end
def add_action_conditions(filters, conditions)
return unless conditions
included, excluded = conditions[:only], conditions[:except]
write_inheritable_hash('included_actions', condition_hash(filters, included)) && return if included
write_inheritable_hash('excluded_actions', condition_hash(filters, excluded)) if excluded
end
def condition_hash(filters, *actions)
filters.inject({}) {|hash, filter| hash.merge(filter => actions.flatten.map {|action| action.to_s})}
end
def remove_contradicting_conditions!(filters, conditions)
return unless conditions[:only]
filters.each do |filter|
next unless included_actions_for_filter = (read_inheritable_attribute('included_actions') || {})[filter]
[*conditions[:only]].each do |conditional_action|
conditional_action = conditional_action.to_s
included_actions_for_filter.delete(conditional_action) if included_actions_for_filter.include?(conditional_action)
end
end
end
end
module InstanceMethods # :nodoc:
def self.included(base)
base.class_eval do
alias_method :perform_action_without_filters, :perform_action
alias_method :perform_action, :perform_action_with_filters
alias_method :process_without_filters, :process
alias_method :process, :process_with_filters
alias_method :process_cleanup_without_filters, :process_cleanup
alias_method :process_cleanup, :process_cleanup_with_filters
end
end
def perform_action_with_filters
before_action_result = before_action
unless before_action_result == false || performed?
perform_action_without_filters
after_action
end
@before_filter_chain_aborted = (before_action_result == false)
end
def process_with_filters(request, response, method = :perform_action, *arguments) #:nodoc:
@before_filter_chain_aborted = false
process_without_filters(request, response, method, *arguments)
end
# Calls all the defined before-filter filters, which are added by using "before_filter :method".
# If any of the filters return false, no more filters will be executed and the action is aborted.
def before_action #:doc:
call_filters(self.class.before_filters)
end
# Calls all the defined after-filter filters, which are added by using "after_filter :method".
# If any of the filters return false, no more filters will be executed.
def after_action #:doc:
call_filters(self.class.after_filters)
end
private
def call_filters(filters)
filters.each do |filter|
next if action_exempted?(filter)
filter_result = case
when filter.is_a?(Symbol)
self.send(filter)
when filter_block?(filter)
filter.call(self)
when filter_class?(filter)
filter.filter(self)
else
raise(
ActionControllerError,
'Filters need to be either a symbol, proc/method, or class implementing a static filter method'
)
end
if filter_result == false
logger.info "Filter chain halted as [#{filter}] returned false" if logger
return false
end
end
end
def filter_block?(filter)
filter.respond_to?('call') && (filter.arity == 1 || filter.arity == -1)
end
def filter_class?(filter)
filter.respond_to?('filter')
end
def action_exempted?(filter)
case
when ia = self.class.included_actions[filter]
!ia.include?(action_name)
when ea = self.class.excluded_actions[filter]
ea.include?(action_name)
end
end
def process_cleanup_with_filters
if @before_filter_chain_aborted
close_session
else
process_cleanup_without_filters
end
end
end
end
end
module ActionController #:nodoc:
# The flash provides a way to pass temporary objects between actions. Anything you place in the flash will be exposed
# to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create action
# that sets flash[:notice] = "Successfully created" before redirecting to a display action that can then expose
# the flash to its template. Actually, that exposure is automatically done. Example:
#
# class WeblogController < ActionController::Base
# def create
# # save post
# flash[:notice] = "Successfully created post"
# redirect_to :action => "display", :params => { :id => post.id }
# end
#
# def display
# # doesn't need to assign the flash notice to the template, that's done automatically
# end
# end
#
# display.rhtml
# <% if @flash[:notice] %>
<%= @flash[:notice] %>
<% end %>
#
# This example just places a string in the flash, but you can put any object in there. And of course, you can put as many
# as you like at a time too. Just remember: They'll be gone by the time the next action has been performed.
#
# See docs on the FlashHash class for more details about the flash.
module Flash
def self.included(base)
base.send :include, InstanceMethods
base.class_eval do
alias_method :assign_shortcuts_without_flash, :assign_shortcuts
alias_method :assign_shortcuts, :assign_shortcuts_with_flash
alias_method :process_cleanup_without_flash, :process_cleanup
alias_method :process_cleanup, :process_cleanup_with_flash
end
end
class FlashNow #:nodoc:
def initialize(flash)
@flash = flash
end
def []=(k, v)
@flash[k] = v
@flash.discard(k)
v
end
def [](k)
@flash[k]
end
end
class FlashHash < Hash
def initialize #:nodoc:
super
@used = {}
end
def []=(k, v) #:nodoc:
keep(k)
super
end
def update(h) #:nodoc:
h.keys.each{ |k| discard(k) }
super
end
alias :merge! :update
def replace(h) #:nodoc:
@used = {}
super
end
# Sets a flash that will not be available to the next action, only to the current.
#
# flash.now[:message] = "Hello current action"
#
# This method enables you to use the flash as a central messaging system in your app.
# When you need to pass an object to the next action, you use the standard flash assign ([]=).
# When you need to pass an object to the current action, you use now, and your object will
# vanish when the current action is done.
#
# Entries set via now are accessed the same way as standard entries: flash['my-key'].
def now
FlashNow.new self
end
# Keeps either the entire current flash or a specific flash entry available for the next action:
#
# flash.keep # keeps the entire flash
# flash.keep(:notice) # keeps only the "notice" entry, the rest of the flash is discarded
def keep(k=nil)
use(k, false)
end
# Marks the entire flash or a single flash entry to be discarded by the end of the current action
#
# flash.keep # keep entire flash available for the next action
# flash.discard(:warning) # discard the "warning" entry (it'll still be available for the current action)
def discard(k=nil)
use(k)
end
# Mark for removal entries that were kept, and delete unkept ones.
#
# This method is called automatically by filters, so you generally don't need to care about it.
def sweep #:nodoc:
keys.each do |k|
unless @used[k]
use(k)
else
delete(k)
@used.delete(k)
end
end
(@used.keys - keys).each{|k| @used.delete k } # clean up after keys that could have been left over by calling reject! or shift on the flash
end
private
# Used internally by the keep and discard methods
# use() # marks the entire flash as used
# use('msg') # marks the "msg" entry as used
# use(nil, false) # marks the entire flash as unused (keeps it around for one more action)
# use('msg', false) # marks the "msg" entry as unused (keeps it around for one more action)
def use(k=nil, v=true)
unless k.nil?
@used[k] = v
else
keys.each{|key| use key, v }
end
end
end
module InstanceMethods #:nodoc:
def assign_shortcuts_with_flash(request, response) #:nodoc:
assign_shortcuts_without_flash(request, response)
flash(:refresh)
end
def process_cleanup_with_flash
flash.sweep if @session
process_cleanup_without_flash
end
protected
# Access the contents of the flash. Use flash["notice"] to read a notice you put there or
# flash["notice"] = "hello" to put a new one.
# Note that if sessions are disabled only flash.now will work.
def flash(refresh = false) #:doc:
if @flash.nil? || refresh
@flash =
if @session.is_a?(Hash)
# @session is a Hash, if sessions are disabled
# we don't put the flash in the session in this case
FlashHash.new
else
# otherwise, @session is a CGI::Session or a TestSession
# so make sure it gets retrieved from/saved to session storage after request processing
@session["flash"] ||= FlashHash.new
end
end
@flash
end
# deprecated. use flash.keep instead
def keep_flash #:doc:
warn 'keep_flash is deprecated; use flash.keep instead.'
flash.keep
end
end
end
endmodule ActionController #:nodoc:
module Helpers #:nodoc:
def self.append_features(base)
super
# Initialize the base module to aggregate its helpers.
base.class_inheritable_accessor :master_helper_module
base.master_helper_module = Module.new
# Extend base with class methods to declare helpers.
base.extend(ClassMethods)
base.class_eval do
# Wrap inherited to create a new master helper module for subclasses.
class << self
alias_method :inherited_without_helper, :inherited
alias_method :inherited, :inherited_with_helper
end
end
end
# The template helpers serve to relieve the templates from including the same inline code again and again. It's a
# set of standardized methods for working with forms (FormHelper), dates (DateHelper), texts (TextHelper), and
# Active Records (ActiveRecordHelper) that's available to all templates by default.
#
# It's also really easy to make your own helpers and it's much encouraged to keep the template files free
# from complicated logic. It's even encouraged to bundle common compositions of methods from other helpers
# (often the common helpers) as they're used by the specific application.
#
# module MyHelper
# def hello_world() "hello world" end
# end
#
# MyHelper can now be included in a controller, like this:
#
# class MyController < ActionController::Base
# helper :my_helper
# end
#
# ...and, same as above, used in any template rendered from MyController, like this:
#
# Let's hear what the helper has to say: <%= hello_world %>
module ClassMethods
# Makes all the (instance) methods in the helper module available to templates rendered through this controller.
# See ActionView::Helpers (link:classes/ActionView/Helpers.html) for more about making your own helper modules
# available to the templates.
def add_template_helper(helper_module) #:nodoc:
master_helper_module.send(:include, helper_module)
end
# Declare a helper:
# helper :foo
# requires 'foo_helper' and includes FooHelper in the template class.
# helper FooHelper
# includes FooHelper in the template class.
# helper { def foo() "#{bar} is the very best" end }
# evaluates the block in the template class, adding method #foo.
# helper(:three, BlindHelper) { def mice() 'mice' end }
# does all three.
def helper(*args, &block)
args.flatten.each do |arg|
case arg
when Module
add_template_helper(arg)
when String, Symbol
file_name = arg.to_s.underscore + '_helper'
class_name = file_name.camelize
begin
require_dependency(file_name)
rescue LoadError => load_error
requiree = / -- (.*?)(\.rb)?$/.match(load_error).to_a[1]
msg = (requiree == file_name) ? "Missing helper file helpers/#{file_name}.rb" : "Can't load file: #{requiree}"
raise LoadError.new(msg).copy_blame!(load_error)
end
add_template_helper(class_name.constantize)
else
raise ArgumentError, 'helper expects String, Symbol, or Module argument'
end
end
# Evaluate block in template class if given.
master_helper_module.module_eval(&block) if block_given?
end
# Declare a controller method as a helper. For example,
# helper_method :link_to
# def link_to(name, options) ... end
# makes the link_to controller method available in the view.
def helper_method(*methods)
methods.flatten.each do |method|
master_helper_module.module_eval <<-end_eval
def #{method}(*args, &block)
controller.send(%(#{method}), *args, &block)
end
end_eval
end
end
# Declare a controller attribute as a helper. For example,
# helper_attr :name
# attr_accessor :name
# makes the name and name= controller methods available in the view.
# The is a convenience wrapper for helper_method.
def helper_attr(*attrs)
attrs.flatten.each { |attr| helper_method(attr, "#{attr}=") }
end
private
def default_helper_module!
module_name = name.sub(/Controller$|$/, 'Helper')
module_path = module_name.split('::').map { |m| m.underscore }.join('/')
require_dependency module_path
helper module_name.constantize
rescue LoadError
logger.debug("#{name}: missing default helper path #{module_path}") if logger
rescue NameError
logger.debug("#{name}: missing default helper module #{module_name}") if logger
end
def inherited_with_helper(child)
inherited_without_helper(child)
begin
child.master_helper_module = Module.new
child.master_helper_module.send :include, master_helper_module
child.send :default_helper_module!
rescue MissingSourceFile => e
raise unless e.is_missing?("helpers/#{child.controller_path}_helper")
end
end
end
end
end
require 'dispatcher'
require 'stringio'
require 'uri'
module ActionController
module Integration #:nodoc:
# An integration Session instance represents a set of requests and responses
# performed sequentially by some virtual user. Becase you can instantiate
# multiple sessions and run them side-by-side, you can also mimic (to some
# limited extent) multiple simultaneous users interacting with your system.
#
# Typically, you will instantiate a new session using IntegrationTest#open_session,
# rather than instantiating Integration::Session directly.
class Session
include Test::Unit::Assertions
include ActionController::TestProcess
# The integer HTTP status code of the last request.
attr_reader :status
# The status message that accompanied the status code of the last request.
attr_reader :status_message
# The URI of the last request.
attr_reader :path
# The hostname used in the last request.
attr_accessor :host
# The remote_addr used in the last request.
attr_accessor :remote_addr
# The Accept header to send.
attr_accessor :accept
# A map of the cookies returned by the last response, and which will be
# sent with the next request.
attr_reader :cookies
# A map of the headers returned by the last response.
attr_reader :headers
# A reference to the controller instance used by the last request.
attr_reader :controller
# A reference to the request instance used by the last request.
attr_reader :request
# A reference to the response instance used by the last request.
attr_reader :response
# Create an initialize a new Session instance.
def initialize
reset!
end
# Resets the instance. This can be used to reset the state information
# in an existing session instance, so it can be used from a clean-slate
# condition.
#
# session.reset!
def reset!
@status = @path = @headers = nil
@result = @status_message = nil
@https = false
@cookies = {}
@controller = @request = @response = nil
self.host = "www.example.com"
self.remote_addr = "127.0.0.1"
self.accept = "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5"
unless @named_routes_configured
# install the named routes in this session instance.
klass = class< "XMLHttpRequest")
post(path, parameters, headers)
end
# Returns the URL for the given options, according to the rules specified
# in the application's routes.
def url_for(options)
controller ? controller.url_for(options) : generic_url_rewriter.rewrite(options)
end
private
class MockCGI < CGI #:nodoc:
attr_accessor :stdinput, :stdoutput, :env_table
def initialize(env, input=nil)
self.env_table = env
self.stdinput = StringIO.new(input || "")
self.stdoutput = StringIO.new
super()
end
end
# Tailors the session based on the given URI, setting the HTTPS value
# and the hostname.
def interpret_uri(path)
location = URI.parse(path)
https! URI::HTTPS === location if location.scheme
host! location.host if location.host
location.query ? "#{location.path}?#{location.query}" : location.path
end
# Performs the actual request.
def process(method, path, parameters=nil, headers=nil)
data = requestify(parameters)
path = interpret_uri(path) if path =~ %r{://}
path = "/#{path}" unless path[0] == ?/
@path = path
env = {}
if method == :get
env["QUERY_STRING"] = data
data = nil
end
env.update(
"REQUEST_METHOD" => method.to_s.upcase,
"REQUEST_URI" => path,
"HTTP_HOST" => host,
"REMOTE_ADDR" => remote_addr,
"SERVER_PORT" => (https? ? "443" : "80"),
"CONTENT_TYPE" => "application/x-www-form-urlencoded",
"CONTENT_LENGTH" => data ? data.length.to_s : nil,
"HTTP_COOKIE" => encode_cookies,
"HTTPS" => https? ? "on" : "off",
"HTTP_ACCEPT" => accept
)
(headers || {}).each do |key, value|
key = key.to_s.upcase.gsub(/-/, "_")
key = "HTTP_#{key}" unless env.has_key?(key) || env =~ /^X|HTTP/
env[key] = value
end
unless ActionController::Base.respond_to?(:clear_last_instantiation!)
ActionController::Base.send(:include, ControllerCapture)
end
ActionController::Base.clear_last_instantiation!
cgi = MockCGI.new(env, data)
Dispatcher.dispatch(cgi, ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS, cgi.stdoutput)
@result = cgi.stdoutput.string
@controller = ActionController::Base.last_instantiation
@request = @controller.request
@response = @controller.response
# Decorate the response with the standard behavior of the TestResponse
# so that things like assert_response can be used in integration
# tests.
@response.extend(TestResponseBehavior)
parse_result
return status
end
# Parses the result of the response and extracts the various values,
# like cookies, status, headers, etc.
def parse_result
headers, result_body = @result.split(/\r\n\r\n/, 2)
@headers = Hash.new { |h,k| h[k] = [] }
headers.each_line do |line|
key, value = line.strip.split(/:\s*/, 2)
@headers[key.downcase] << value
end
(@headers['set-cookie'] || [] ).each do |string|
name, value = string.match(/^(.*?)=(.*?);/)[1,2]
@cookies[name] = value
end
@status, @status_message = @headers["status"].first.split(/ /)
@status = @status.to_i
end
# Encode the cookies hash in a format suitable for passing to a
# request.
def encode_cookies
cookies.inject("") do |string, (name, value)|
string << "#{name}=#{value}; "
end
end
# Get a temporarly URL writer object
def generic_url_rewriter
cgi = MockCGI.new('REQUEST_METHOD' => "GET",
'QUERY_STRING' => "",
"REQUEST_URI" => "/",
"HTTP_HOST" => host,
"SERVER_PORT" => https? ? "443" : "80",
"HTTPS" => https? ? "on" : "off")
ActionController::UrlRewriter.new(ActionController::CgiRequest.new(cgi), {})
end
def name_with_prefix(prefix, name)
prefix ? "#{prefix}[#{name}]" : name.to_s
end
# Convert the given parameters to a request string. The parameters may
# be a string, +nil+, or a Hash.
def requestify(parameters, prefix=nil)
if Hash === parameters
return nil if parameters.empty?
parameters.map { |k,v| requestify(v, name_with_prefix(prefix, k)) }.join("&")
elsif Array === parameters
parameters.map { |v| requestify(v, name_with_prefix(prefix, "")) }.join("&")
elsif prefix.nil?
parameters
else
"#{CGI.escape(prefix)}=#{CGI.escape(parameters.to_s)}"
end
end
end
# A module used to extend ActionController::Base, so that integration tests
# can capture the controller used to satisfy a request.
module ControllerCapture #:nodoc:
def self.included(base)
base.extend(ClassMethods)
base.class_eval do
class < people(:jamis).username,
# :password => people(:jamis).password
# follow_redirect!
# assert_equal 200, status
# assert_equal "/home", path
# end
# end
#
# However, you can also have multiple session instances open per test, and
# even extend those instances with assertions and methods to create a very
# powerful testing DSL that is specific for your application. You can even
# reference any named routes you happen to have defined!
#
# require "#{File.dirname(__FILE__)}/test_helper"
#
# class AdvancedTest < ActionController::IntegrationTest
# fixtures :people, :rooms
#
# def test_login_and_speak
# jamis, david = login(:jamis), login(:david)
# room = rooms(:office)
#
# jamis.enter(room)
# jamis.speak(room, "anybody home?")
#
# david.enter(room)
# david.speak(room, "hello!")
# end
#
# private
#
# module CustomAssertions
# def enter(room)
# # reference a named route, for maximum internal consistency!
# get(room_url(:id => room.id))
# assert(...)
# ...
# end
#
# def speak(room, message)
# xml_http_request "/say/#{room.id}", :message => message
# assert(...)
# ...
# end
# end
#
# def login(who)
# open_session do |sess|
# sess.extend(CustomAssertions)
# who = people(who)
# sess.post "/login", :username => who.username,
# :password => who.password
# assert(...)
# end
# end
# end
class IntegrationTest < Test::Unit::TestCase
# Work around a bug in test/unit caused by the default test being named
# as a symbol (:default_test), which causes regex test filters
# (like "ruby test.rb -n /foo/") to fail because =~ doesn't work on
# symbols.
def initialize(name) #:nodoc:
super(name.to_s)
end
# Work around test/unit's requirement that every subclass of TestCase have
# at least one test method. Note that this implementation extends to all
# subclasses, as well, so subclasses of IntegrationTest may also exist
# without any test methods.
def run(*args) #:nodoc:
return if @method_name == "default_test"
super
end
# Because of how use_instantiated_fixtures and use_transactional_fixtures
# are defined, we need to treat them as special cases. Otherwise, users
# would potentially have to set their values for both Test::Unit::TestCase
# ActionController::IntegrationTest, since by the time the value is set on
# TestCase, IntegrationTest has already been defined and cannot inherit
# changes to those variables. So, we make those two attributes copy-on-write.
class << self
def use_transactional_fixtures=(flag) #:nodoc:
@_use_transactional_fixtures = true
@use_transactional_fixtures = flag
end
def use_instantiated_fixtures=(flag) #:nodoc:
@_use_instantiated_fixtures = true
@use_instantiated_fixtures = flag
end
def use_transactional_fixtures #:nodoc:
@_use_transactional_fixtures ?
@use_transactional_fixtures :
superclass.use_transactional_fixtures
end
def use_instantiated_fixtures #:nodoc:
@_use_instantiated_fixtures ?
@use_instantiated_fixtures :
superclass.use_instantiated_fixtures
end
end
# Reset the current session. This is useful for testing multiple sessions
# in a single test case.
def reset!
@integration_session = open_session
end
%w(get post cookies assigns xml_http_request).each do |method|
define_method(method) do |*args|
reset! unless @integration_session
returning @integration_session.send(method, *args) do
copy_session_variables!
end
end
end
# Open a new session instance. If a block is given, the new session is
# yielded to the block before being returned.
#
# session = open_session do |sess|
# sess.extend(CustomAssertions)
# end
#
# By default, a single session is automatically created for you, but you
# can use this method to open multiple sessions that ought to be tested
# simultaneously.
def open_session
session = Integration::Session.new
# delegate the fixture accessors back to the test instance
extras = Module.new { attr_accessor :delegate, :test_result }
self.class.fixture_table_names.each do |table_name|
name = table_name.tr(".", "_")
next unless respond_to?(name)
extras.send(:define_method, name) { |*args| delegate.send(name, *args) }
end
# delegate add_assertion to the test case
extras.send(:define_method, :add_assertion) { test_result.add_assertion }
session.extend(extras)
session.delegate = self
session.test_result = @_result
yield session if block_given?
session
end
# Copy the instance variables from the current session instance into the
# test instance.
def copy_session_variables! #:nodoc:
return unless @integration_session
%w(controller response request).each do |var|
instance_variable_set("@#{var}", @integration_session.send(var))
end
end
# Delegate unhandled messages to the current session instance.
def method_missing(sym, *args, &block)
reset! unless @integration_session
returning @integration_session.send(sym, *args, &block) do
copy_session_variables!
end
end
end
end
module ActionController #:nodoc:
module Layout #:nodoc:
def self.included(base)
base.extend(ClassMethods)
base.class_eval do
alias_method :render_with_no_layout, :render
alias_method :render, :render_with_a_layout
class << self
alias_method :inherited_without_layout, :inherited
alias_method :inherited, :inherited_with_layout
end
end
end
# Layouts reverse the common pattern of including shared headers and footers in many templates to isolate changes in
# repeated setups. The inclusion pattern has pages that look like this:
#
# <%= render "shared/header" %>
# Hello World
# <%= render "shared/footer" %>
#
# This approach is a decent way of keeping common structures isolated from the changing content, but it's verbose
# and if you ever want to change the structure of these two includes, you'll have to change all the templates.
#
# With layouts, you can flip it around and have the common structure know where to insert changing content. This means
# that the header and footer are only mentioned in one place, like this:
#
#
# <%= yield %>
#
#
# And then you have content pages that look like this:
#
# hello world
#
# Not a word about common structures. At rendering time, the content page is computed and then inserted in the layout,
# like this:
#
#
# hello world
#
#
# == Accessing shared variables
#
# Layouts have access to variables specified in the content pages and vice versa. This allows you to have layouts with
# references that won't materialize before rendering time:
#
#
<%= @page_title %>
# <%= yield %>
#
# ...and content pages that fulfill these references _at_ rendering time:
#
# <% @page_title = "Welcome" %>
# Off-world colonies offers you a chance to start a new life
#
# The result after rendering is:
#
#
Welcome
# Off-world colonies offers you a chance to start a new life
#
# == Automatic layout assignment
#
# If there is a template in app/views/layouts/ with the same name as the current controller then it will be automatically
# set as that controller's layout unless explicitly told otherwise. Say you have a WeblogController, for example. If a template named
# app/views/layouts/weblog.rhtml or app/views/layouts/weblog.rxml exists then it will be automatically set as
# the layout for your WeblogController. You can create a layout with the name application.rhtml or application.rxml
# and this will be set as the default controller if there is no layout with the same name as the current controller and there is
# no layout explicitly assigned with the +layout+ method. Nested controllers use the same folder structure for automatic layout.
# assignment. So an Admin::WeblogController will look for a template named app/views/layouts/admin/weblog.rhtml.
# Setting a layout explicitly will always override the automatic behaviour for the controller where the layout is set.
# Explicitly setting the layout in a parent class, though, will not override the child class's layout assignement if the child
# class has a layout with the same name.
#
# == Inheritance for layouts
#
# Layouts are shared downwards in the inheritance hierarchy, but not upwards. Examples:
#
# class BankController < ActionController::Base
# layout "bank_standard"
#
# class InformationController < BankController
#
# class VaultController < BankController
# layout :access_level_layout
#
# class EmployeeController < BankController
# layout nil
#
# The InformationController uses "bank_standard" inherited from the BankController, the VaultController overwrites
# and picks the layout dynamically, and the EmployeeController doesn't want to use a layout at all.
#
# == Types of layouts
#
# Layouts are basically just regular templates, but the name of this template needs not be specified statically. Sometimes
# you want to alternate layouts depending on runtime information, such as whether someone is logged in or not. This can
# be done either by specifying a method reference as a symbol or using an inline method (as a proc).
#
# The method reference is the preferred approach to variable layouts and is used like this:
#
# class WeblogController < ActionController::Base
# layout :writers_and_readers
#
# def index
# # fetching posts
# end
#
# private
# def writers_and_readers
# logged_in? ? "writer_layout" : "reader_layout"
# end
#
# Now when a new request for the index action is processed, the layout will vary depending on whether the person accessing
# is logged in or not.
#
# If you want to use an inline method, such as a proc, do something like this:
#
# class WeblogController < ActionController::Base
# layout proc{ |controller| controller.logged_in? ? "writer_layout" : "reader_layout" }
#
# Of course, the most common way of specifying a layout is still just as a plain template name:
#
# class WeblogController < ActionController::Base
# layout "weblog_standard"
#
# If no directory is specified for the template name, the template will by default by looked for in +app/views/layouts/+.
#
# == Conditional layouts
#
# If you have a layout that by default is applied to all the actions of a controller, you still have the option of rendering
# a given action or set of actions without a layout, or restricting a layout to only a single action or a set of actions. The
# :only and :except options can be passed to the layout call. For example:
#
# class WeblogController < ActionController::Base
# layout "weblog_standard", :except => :rss
#
# # ...
#
# end
#
# This will assign "weblog_standard" as the WeblogController's layout except for the +rss+ action, which will not wrap a layout
# around the rendered view.
#
# Both the :only and :except condition can accept an arbitrary number of method references, so
# #:except => [ :rss, :text_only ] is valid, as is :except => :rss.
#
# == Using a different layout in the action render call
#
# If most of your actions use the same layout, it makes perfect sense to define a controller-wide layout as described above.
# Some times you'll have exceptions, though, where one action wants to use a different layout than the rest of the controller.
# This is possible using the render method. It's just a bit more manual work as you'll have to supply fully
# qualified template and layout names as this example shows:
#
# class WeblogController < ActionController::Base
# def help
# render :action => "help/index", :layout => "help"
# end
# end
#
# As you can see, you pass the template as the first parameter, the status code as the second ("200" is OK), and the layout
# as the third.
#
# NOTE: The old notation for rendering the view from a layout was to expose the magic @content_for_layout instance
# variable. The preferred notation now is to use yield, as documented above.
module ClassMethods
# If a layout is specified, all rendered actions will have their result rendered
# when the layoutyield's. This layout can itself depend on instance variables assigned during action
# performance and have access to them as any normal template would.
def layout(template_name, conditions = {})
add_layout_conditions(conditions)
write_inheritable_attribute "layout", template_name
end
def layout_conditions #:nodoc:
@layout_conditions ||= read_inheritable_attribute("layout_conditions")
end
def default_layout #:nodoc:
@default_layout ||= read_inheritable_attribute("layout")
end
private
def inherited_with_layout(child)
inherited_without_layout(child)
child.send :include, Reloadable
layout_match = child.name.underscore.sub(/_controller$/, '').sub(/^controllers\//, '')
child.layout(layout_match) unless layout_list.grep(%r{layouts/#{layout_match}\.[a-z][0-9a-z]*$}).empty?
end
def layout_list
Dir.glob("#{template_root}/layouts/**/*")
end
def add_layout_conditions(conditions)
write_inheritable_hash "layout_conditions", normalize_conditions(conditions)
end
def normalize_conditions(conditions)
conditions.inject({}) {|hash, (key, value)| hash.merge(key => [value].flatten.map {|action| action.to_s})}
end
def layout_directory_exists_cache
@@layout_directory_exists_cache ||= Hash.new do |h, dirname|
h[dirname] = File.directory? dirname
end
end
end
# Returns the name of the active layout. If the layout was specified as a method reference (through a symbol), this method
# is called and the return value is used. Likewise if the layout was specified as an inline method (through a proc or method
# object). If the layout was defined without a directory, layouts is assumed. So layout "weblog/standard" will return
# weblog/standard, but layout "standard" will return layouts/standard.
def active_layout(passed_layout = nil)
layout = passed_layout || self.class.default_layout
active_layout = case layout
when String then layout
when Symbol then send(layout)
when Proc then layout.call(self)
end
# Explicitly passed layout names with slashes are looked up relative to the template root,
# but auto-discovered layouts derived from a nested controller will contain a slash, though be relative
# to the 'layouts' directory so we have to check the file system to infer which case the layout name came from.
if active_layout
if active_layout.include?('/') && ! layout_directory?(active_layout)
active_layout
else
"layouts/#{active_layout}"
end
end
end
def render_with_a_layout(options = nil, deprecated_status = nil, deprecated_layout = nil, &block) #:nodoc:
template_with_options = options.is_a?(Hash)
if apply_layout?(template_with_options, options) && (layout = pick_layout(template_with_options, options, deprecated_layout))
options = options.merge :layout => false if template_with_options
logger.info("Rendering #{options} within #{layout}") if logger
if template_with_options
content_for_layout = render_with_no_layout(options, &block)
deprecated_status = options[:status] || deprecated_status
else
content_for_layout = render_with_no_layout(options, deprecated_status, &block)
end
erase_render_results
add_variables_to_assigns
@template.instance_variable_set("@content_for_layout", content_for_layout)
render_text(@template.render_file(layout, true), deprecated_status)
else
render_with_no_layout(options, deprecated_status, &block)
end
end
private
def apply_layout?(template_with_options, options)
return false if options == :update
template_with_options ? candidate_for_layout?(options) : !template_exempt_from_layout?
end
def candidate_for_layout?(options)
(options.has_key?(:layout) && options[:layout] != false) ||
options.values_at(:text, :xml, :file, :inline, :partial, :nothing).compact.empty? &&
!template_exempt_from_layout?(default_template_name(options[:action] || options[:template]))
end
def pick_layout(template_with_options, options, deprecated_layout)
if deprecated_layout
deprecated_layout
elsif template_with_options
case layout = options[:layout]
when FalseClass
nil
when NilClass, TrueClass
active_layout if action_has_layout?
else
active_layout(layout)
end
else
active_layout if action_has_layout?
end
end
def action_has_layout?
if conditions = self.class.layout_conditions
case
when only = conditions[:only]
only.include?(action_name)
when except = conditions[:except]
!except.include?(action_name)
else
true
end
else
true
end
end
# Does a layout directory for this class exist?
# we cache this info in a class level hash
def layout_directory?(layout_name)
template_path = File.join(self.class.view_root, 'layouts', layout_name)
dirname = File.dirname(template_path)
self.class.send(:layout_directory_exists_cache)[dirname]
end
end
end
module ActionController
# Macros are class-level calls that add pre-defined actions to the controller based on the parameters passed in.
# Currently, they're used to bridge the JavaScript macros, like autocompletion and in-place editing, with the controller
# backing.
module Macros
module AutoComplete #:nodoc:
def self.append_features(base) #:nodoc:
super
base.extend(ClassMethods)
end
# Example:
#
# # Controller
# class BlogController < ApplicationController
# auto_complete_for :post, :title
# end
#
# # View
# <%= text_field_with_auto_complete :post, title %>
#
# By default, auto_complete_for limits the results to 10 entries,
# and sorts by the given field.
#
# auto_complete_for takes a third parameter, an options hash to
# the find method used to search for the records:
#
# auto_complete_for :post, :title, :limit => 15, :order => 'created_at DESC'
#
# For help on defining text input fields with autocompletion,
# see ActionView::Helpers::JavaScriptHelper.
#
# For more examples, see script.aculo.us:
# * http://script.aculo.us/demos/ajax/autocompleter
# * http://script.aculo.us/demos/ajax/autocompleter_customized
module ClassMethods
def auto_complete_for(object, method, options = {})
define_method("auto_complete_for_#{object}_#{method}") do
find_options = {
:conditions => [ "LOWER(#{method}) LIKE ?", '%' + params[object][method].downcase + '%' ],
:order => "#{method} ASC",
:limit => 10 }.merge!(options)
@items = object.to_s.camelize.constantize.find(:all, find_options)
render :inline => "<%= auto_complete_result @items, '#{method}' %>"
end
end
end
end
end
endmodule ActionController
module Macros
module InPlaceEditing #:nodoc:
def self.append_features(base) #:nodoc:
super
base.extend(ClassMethods)
end
# Example:
#
# # Controller
# class BlogController < ApplicationController
# in_place_edit_for :post, :title
# end
#
# # View
# <%= in_place_editor_field :post, 'title' %>
#
# For help on defining an in place editor in the browser,
# see ActionView::Helpers::JavaScriptHelper.
module ClassMethods
def in_place_edit_for(object, attribute, options = {})
define_method("set_#{object}_#{attribute}") do
@item = object.to_s.camelize.constantize.find(params[:id])
@item.update_attribute(attribute, params[:value])
render :text => @item.send(attribute)
end
end
end
end
end
end
module ActionController #:nodoc:
module MimeResponds #:nodoc:
def self.included(base)
base.send(:include, ActionController::MimeResponds::InstanceMethods)
end
module InstanceMethods
# Without web-service support, an action which collects the data for displaying a list of people
# might look something like this:
#
# def list
# @people = Person.find(:all)
# end
#
# Here's the same action, with web-service support baked in:
#
# def list
# @people = Person.find(:all)
#
# respond_to do |wants|
# wants.html
# wants.xml { render :xml => @people.to_xml }
# end
# end
#
# What that says is, "if the client wants HTML in response to this action, just respond as we
# would have before, but if the client wants XML, return them the list of people in XML format."
# (Rails determines the desired response format from the HTTP Accept header submitted by the client.)
#
# Supposing you have an action that adds a new person, optionally creating their company
# (by name) if it does not already exist, without web-services, it might look like this:
#
# def add
# @company = Company.find_or_create_by_name(params[:company][:name])
# @person = @company.people.create(params[:person])
#
# redirect_to(person_list_url)
# end
#
# Here's the same action, with web-service support baked in:
#
# def add
# company = params[:person].delete(:company)
# @company = Company.find_or_create_by_name(company[:name])
# @person = @company.people.create(params[:person])
#
# respond_to do |wants|
# wants.html { redirect_to(person_list_url) }
# wants.js
# wants.xml { render :xml => @person.to_xml(:include => @company) }
# end
# end
#
# If the client wants HTML, we just redirect them back to the person list. If they want Javascript
# (wants.js), then it is an RJS request and we render the RJS template associated with this action.
# Lastly, if the client wants XML, we render the created person as XML, but with a twist: we also
# include the person’s company in the rendered XML, so you get something like this:
#
#
# ...
# ...
#
# ...
# ...
# ...
#
#
#
# Note, however, the extra bit at the top of that action:
#
# company = params[:person].delete(:company)
# @company = Company.find_or_create_by_name(company[:name])
#
# This is because the incoming XML document (if a web-service request is in process) can only contain a
# single root-node. So, we have to rearrange things so that the request looks like this (url-encoded):
#
# person[name]=...&person[company][name]=...&...
#
# And, like this (xml-encoded):
#
#
# ...
#
# ...
#
#
#
# In other words, we make the request so that it operates on a single entity—a person. Then, in the action,
# we extract the company data from the request, find or create the company, and then create the new person
# with the remaining data.
#
# Note that you can define your own XML parameter parser which would allow you to describe multiple entities
# in a single request (i.e., by wrapping them all in a single root note), but if you just go with the flow
# and accept Rails' defaults, life will be much easier.
#
# If you need to use a MIME type which isn't supported by default, you can register your own handlers in
# environment.rb as follows.
#
# Mime::Type.register "image/jpg", :jpg
#
def respond_to(*types, &block)
raise ArgumentError, "respond_to takes either types or a block, never bot" unless types.any? ^ block
block ||= lambda { |responder| types.each { |type| responder.send(type) } }
responder = Responder.new(block.binding)
block.call(responder)
responder.respond
end
end
class Responder #:nodoc:
DEFAULT_BLOCKS = {
:html => 'Proc.new { render }',
:js => 'Proc.new { render :action => "#{action_name}.rjs" }',
:xml => 'Proc.new { render :action => "#{action_name}.rxml" }'
}
def initialize(block_binding)
@block_binding = block_binding
@mime_type_priority = eval("request.accepts", block_binding)
@order = []
@responses = {}
end
def custom(mime_type, &block)
mime_type = mime_type.is_a?(Mime::Type) ? mime_type : Mime::Type.lookup(mime_type.to_s)
@order << mime_type
if block_given?
@responses[mime_type] = block
else
@responses[mime_type] = eval(DEFAULT_BLOCKS[mime_type.to_sym], @block_binding)
end
end
for mime_type in %w( all html js xml rss atom yaml )
eval <<-EOT
def #{mime_type}(&block)
custom(Mime::#{mime_type.upcase}, &block)
end
EOT
end
def any(*args, &block)
args.each { |type| send(type, &block) }
end
def respond
for priority in @mime_type_priority
if priority == Mime::ALL
@responses[@order.first].call
return
else
if priority === @order
@responses[priority].call
return # mime type match found, be happy and return
end
end
end
if @order.include?(Mime::ALL)
@responses[Mime::ALL].call
else
eval 'render(:nothing => true, :status => "406 Not Acceptable")', @block_binding
end
end
end
end
end
module Mime
class Type #:nodoc:
# A simple helper class used in parsing the accept header
class AcceptItem #:nodoc:
attr_accessor :order, :name, :q
def initialize(order, name, q=nil)
@order = order
@name = name.strip
q ||= 0.0 if @name == "*/*" # default "*/*" to end of list
@q = ((q || 1.0).to_f * 100).to_i
end
def to_s
@name
end
def <=>(item)
result = item.q <=> q
result = order <=> item.order if result == 0
result
end
def ==(item)
name == (item.respond_to?(:name) ? item.name : item)
end
end
class << self
def lookup(string)
LOOKUP[string]
end
def parse(accept_header)
# keep track of creation order to keep the subsequent sort stable
index = 0
list = accept_header.split(/,/).
map! { |i| AcceptItem.new(index += 1, *i.split(/;\s*q=/)) }.sort!
# Take care of the broken text/xml entry by renaming or deleting it
text_xml = list.index("text/xml")
app_xml = list.index("application/xml")
if text_xml && app_xml
# set the q value to the max of the two
list[app_xml].q = [list[text_xml].q, list[app_xml].q].max
# make sure app_xml is ahead of text_xml in the list
if app_xml > text_xml
list[app_xml], list[text_xml] = list[text_xml], list[app_xml]
app_xml, text_xml = text_xml, app_xml
end
# delete text_xml from the list
list.delete_at(text_xml)
elsif text_xml
list[text_xml].name = "application/xml"
end
# Look for more specific xml-based types and sort them ahead of app/xml
if app_xml
idx = app_xml
app_xml_type = list[app_xml]
while(idx < list.length)
type = list[idx]
break if type.q < app_xml_type.q
if type.name =~ /\+xml$/
list[app_xml], list[idx] = list[idx], list[app_xml]
app_xml = idx
end
idx += 1
end
end
list.map! { |i| Mime::Type.lookup(i.name) }.uniq!
list
end
end
def initialize(string, symbol = nil, synonyms = [])
@symbol, @synonyms = symbol, synonyms
@string = string
end
def to_s
@string
end
def to_str
to_s
end
def to_sym
@symbol || @string.to_sym
end
def ===(list)
if list.is_a?(Array)
(@synonyms + [ self ]).any? { |synonym| list.include?(synonym) }
else
super
end
end
def ==(mime_type)
(@synonyms + [ self ]).any? { |synonym| synonym.to_s == mime_type.to_s } if mime_type
end
end
ALL = Type.new "*/*", :all
HTML = Type.new "text/html", :html, %w( application/xhtml+xml )
JS = Type.new "text/javascript", :js, %w( application/javascript application/x-javascript )
XML = Type.new "application/xml", :xml, %w( text/xml application/x-xml )
RSS = Type.new "application/rss+xml", :rss
ATOM = Type.new "application/atom+xml", :atom
YAML = Type.new "application/x-yaml", :yaml, %w( text/yaml )
LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) }
LOOKUP["*/*"] = ALL
LOOKUP["text/html"] = HTML
LOOKUP["application/xhtml+xml"] = HTML
LOOKUP["application/xml"] = XML
LOOKUP["text/xml"] = XML
LOOKUP["application/x-xml"] = XML
LOOKUP["text/javascript"] = JS
LOOKUP["application/javascript"] = JS
LOOKUP["application/x-javascript"] = JS
LOOKUP["text/yaml"] = YAML
LOOKUP["application/x-yaml"] = YAML
LOOKUP["application/rss+xml"] = RSS
LOOKUP["application/atom+xml"] = ATOM
endmodule ActionController
# === Action Pack pagination for Active Record collections
#
# The Pagination module aids in the process of paging large collections of
# Active Record objects. It offers macro-style automatic fetching of your
# model for multiple views, or explicit fetching for single actions. And if
# the magic isn't flexible enough for your needs, you can create your own
# paginators with a minimal amount of code.
#
# The Pagination module can handle as much or as little as you wish. In the
# controller, have it automatically query your model for pagination; or,
# if you prefer, create Paginator objects yourself.
#
# Pagination is included automatically for all controllers.
#
# For help rendering pagination links, see
# ActionView::Helpers::PaginationHelper.
#
# ==== Automatic pagination for every action in a controller
#
# class PersonController < ApplicationController
# model :person
#
# paginate :people, :order => 'last_name, first_name',
# :per_page => 20
#
# # ...
# end
#
# Each action in this controller now has access to a @people
# instance variable, which is an ordered collection of model objects for the
# current page (at most 20, sorted by last name and first name), and a
# @person_pages Paginator instance. The current page is determined
# by the params[:page] variable.
#
# ==== Pagination for a single action
#
# def list
# @person_pages, @people =
# paginate :people, :order => 'last_name, first_name'
# end
#
# Like the previous example, but explicitly creates @person_pages
# and @people for a single action, and uses the default of 10 items
# per page.
#
# ==== Custom/"classic" pagination
#
# def list
# @person_pages = Paginator.new self, Person.count, 10, params[:page]
# @people = Person.find :all, :order => 'last_name, first_name',
# :limit => @person_pages.items_per_page,
# :offset => @person_pages.current.offset
# end
#
# Explicitly creates the paginator from the previous example and uses
# Paginator#to_sql to retrieve @people from the model.
#
module Pagination
unless const_defined?(:OPTIONS)
# A hash holding options for controllers using macro-style pagination
OPTIONS = Hash.new
# The default options for pagination
DEFAULT_OPTIONS = {
:class_name => nil,
:singular_name => nil,
:per_page => 10,
:conditions => nil,
:order_by => nil,
:order => nil,
:join => nil,
:joins => nil,
:count => nil,
:include => nil,
:select => nil,
:parameter => 'page'
}
end
def self.included(base) #:nodoc:
super
base.extend(ClassMethods)
end
def self.validate_options!(collection_id, options, in_action) #:nodoc:
options.merge!(DEFAULT_OPTIONS) {|key, old, new| old}
valid_options = DEFAULT_OPTIONS.keys
valid_options << :actions unless in_action
unknown_option_keys = options.keys - valid_options
raise ActionController::ActionControllerError,
"Unknown options: #{unknown_option_keys.join(', ')}" unless
unknown_option_keys.empty?
options[:singular_name] ||= Inflector.singularize(collection_id.to_s)
options[:class_name] ||= Inflector.camelize(options[:singular_name])
end
# Returns a paginator and a collection of Active Record model instances
# for the paginator's current page. This is designed to be used in a
# single action; to automatically paginate multiple actions, consider
# ClassMethods#paginate.
#
# +options+ are:
# :singular_name:: the singular name to use, if it can't be inferred by
# singularizing the collection name
# :class_name:: the class name to use, if it can't be inferred by
# camelizing the singular name
# :per_page:: the maximum number of items to include in a
# single page. Defaults to 10
# :conditions:: optional conditions passed to Model.find(:all, *params) and
# Model.count
# :order:: optional order parameter passed to Model.find(:all, *params)
# :order_by:: (deprecated, used :order) optional order parameter passed to Model.find(:all, *params)
# :joins:: optional joins parameter passed to Model.find(:all, *params)
# and Model.count
# :join:: (deprecated, used :joins or :include) optional join parameter passed to Model.find(:all, *params)
# and Model.count
# :include:: optional eager loading parameter passed to Model.find(:all, *params)
# and Model.count
# :select:: :select parameter passed to Model.find(:all, *params)
#
# :count:: parameter passed as :select option to Model.count(*params)
#
def paginate(collection_id, options={})
Pagination.validate_options!(collection_id, options, true)
paginator_and_collection_for(collection_id, options)
end
# These methods become class methods on any controller
module ClassMethods
# Creates a +before_filter+ which automatically paginates an Active
# Record model for all actions in a controller (or certain actions if
# specified with the :actions option).
#
# +options+ are the same as PaginationHelper#paginate, with the addition
# of:
# :actions:: an array of actions for which the pagination is
# active. Defaults to +nil+ (i.e., every action)
def paginate(collection_id, options={})
Pagination.validate_options!(collection_id, options, false)
module_eval do
before_filter :create_paginators_and_retrieve_collections
OPTIONS[self] ||= Hash.new
OPTIONS[self][collection_id] = options
end
end
end
def create_paginators_and_retrieve_collections #:nodoc:
Pagination::OPTIONS[self.class].each do |collection_id, options|
next unless options[:actions].include? action_name if
options[:actions]
paginator, collection =
paginator_and_collection_for(collection_id, options)
paginator_name = "@#{options[:singular_name]}_pages"
self.instance_variable_set(paginator_name, paginator)
collection_name = "@#{collection_id.to_s}"
self.instance_variable_set(collection_name, collection)
end
end
# Returns the total number of items in the collection to be paginated for
# the +model+ and given +conditions+. Override this method to implement a
# custom counter.
def count_collection_for_pagination(model, options)
model.count(:conditions => options[:conditions],
:joins => options[:join] || options[:joins],
:include => options[:include],
:select => options[:count])
end
# Returns a collection of items for the given +model+ and +options[conditions]+,
# ordered by +options[order]+, for the current page in the given +paginator+.
# Override this method to implement a custom finder.
def find_collection_for_pagination(model, options, paginator)
model.find(:all, :conditions => options[:conditions],
:order => options[:order_by] || options[:order],
:joins => options[:join] || options[:joins], :include => options[:include],
:select => options[:select], :limit => options[:per_page],
:offset => paginator.current.offset)
end
protected :create_paginators_and_retrieve_collections,
:count_collection_for_pagination,
:find_collection_for_pagination
def paginator_and_collection_for(collection_id, options) #:nodoc:
klass = options[:class_name].constantize
page = @params[options[:parameter]]
count = count_collection_for_pagination(klass, options)
paginator = Paginator.new(self, count, options[:per_page], page)
collection = find_collection_for_pagination(klass, options, paginator)
return paginator, collection
end
private :paginator_and_collection_for
# A class representing a paginator for an Active Record collection.
class Paginator
include Enumerable
# Creates a new Paginator on the given +controller+ for a set of items
# of size +item_count+ and having +items_per_page+ items per page.
# Raises ArgumentError if items_per_page is out of bounds (i.e., less
# than or equal to zero). The page CGI parameter for links defaults to
# "page" and can be overridden with +page_parameter+.
def initialize(controller, item_count, items_per_page, current_page=1)
raise ArgumentError, 'must have at least one item per page' if
items_per_page <= 0
@controller = controller
@item_count = item_count || 0
@items_per_page = items_per_page
@pages = {}
self.current_page = current_page
end
attr_reader :controller, :item_count, :items_per_page
# Sets the current page number of this paginator. If +page+ is a Page
# object, its +number+ attribute is used as the value; if the page does
# not belong to this Paginator, an ArgumentError is raised.
def current_page=(page)
if page.is_a? Page
raise ArgumentError, 'Page/Paginator mismatch' unless
page.paginator == self
end
page = page.to_i
@current_page_number = has_page_number?(page) ? page : 1
end
# Returns a Page object representing this paginator's current page.
def current_page
@current_page ||= self[@current_page_number]
end
alias current :current_page
# Returns a new Page representing the first page in this paginator.
def first_page
@first_page ||= self[1]
end
alias first :first_page
# Returns a new Page representing the last page in this paginator.
def last_page
@last_page ||= self[page_count]
end
alias last :last_page
# Returns the number of pages in this paginator.
def page_count
@page_count ||= @item_count.zero? ? 1 :
(q,r=@item_count.divmod(@items_per_page); r==0? q : q+1)
end
alias length :page_count
# Returns true if this paginator contains the page of index +number+.
def has_page_number?(number)
number >= 1 and number <= page_count
end
# Returns a new Page representing the page with the given index
# +number+.
def [](number)
@pages[number] ||= Page.new(self, number)
end
# Successively yields all the paginator's pages to the given block.
def each(&block)
page_count.times do |n|
yield self[n+1]
end
end
# A class representing a single page in a paginator.
class Page
include Comparable
# Creates a new Page for the given +paginator+ with the index
# +number+. If +number+ is not in the range of valid page numbers or
# is not a number at all, it defaults to 1.
def initialize(paginator, number)
@paginator = paginator
@number = number.to_i
@number = 1 unless @paginator.has_page_number? @number
end
attr_reader :paginator, :number
alias to_i :number
# Compares two Page objects and returns true when they represent the
# same page (i.e., their paginators are the same and they have the
# same page number).
def ==(page)
return false if page.nil?
@paginator == page.paginator and
@number == page.number
end
# Compares two Page objects and returns -1 if the left-hand page comes
# before the right-hand page, 0 if the pages are equal, and 1 if the
# left-hand page comes after the right-hand page. Raises ArgumentError
# if the pages do not belong to the same Paginator object.
def <=>(page)
raise ArgumentError unless @paginator == page.paginator
@number <=> page.number
end
# Returns the item offset for the first item in this page.
def offset
@paginator.items_per_page * (@number - 1)
end
# Returns the number of the first item displayed.
def first_item
offset + 1
end
# Returns the number of the last item displayed.
def last_item
[@paginator.items_per_page * @number, @paginator.item_count].min
end
# Returns true if this page is the first page in the paginator.
def first?
self == @paginator.first
end
# Returns true if this page is the last page in the paginator.
def last?
self == @paginator.last
end
# Returns a new Page object representing the page just before this
# page, or nil if this is the first page.
def previous
if first? then nil else @paginator[@number - 1] end
end
# Returns a new Page object representing the page just after this
# page, or nil if this is the last page.
def next
if last? then nil else @paginator[@number + 1] end
end
# Returns a new Window object for this page with the specified
# +padding+.
def window(padding=2)
Window.new(self, padding)
end
# Returns the limit/offset array for this page.
def to_sql
[@paginator.items_per_page, offset]
end
def to_param #:nodoc:
@number.to_s
end
end
# A class for representing ranges around a given page.
class Window
# Creates a new Window object for the given +page+ with the specified
# +padding+.
def initialize(page, padding=2)
@paginator = page.paginator
@page = page
self.padding = padding
end
attr_reader :paginator, :page
# Sets the window's padding (the number of pages on either side of the
# window page).
def padding=(padding)
@padding = padding < 0 ? 0 : padding
# Find the beginning and end pages of the window
@first = @paginator.has_page_number?(@page.number - @padding) ?
@paginator[@page.number - @padding] : @paginator.first
@last = @paginator.has_page_number?(@page.number + @padding) ?
@paginator[@page.number + @padding] : @paginator.last
end
attr_reader :padding, :first, :last
# Returns an array of Page objects in the current window.
def pages
(@first.number..@last.number).to_a.collect! {|n| @paginator[n]}
end
alias to_a :pages
end
end
end
end
module ActionController
# Subclassing AbstractRequest makes these methods available to the request objects used in production and testing,
# CgiRequest and TestRequest
class AbstractRequest
cattr_accessor :relative_url_root
# Returns the hash of environment variables for this request,
# such as { 'RAILS_ENV' => 'production' }.
attr_reader :env
# Returns both GET and POST parameters in a single hash.
def parameters
@parameters ||= request_parameters.update(query_parameters).update(path_parameters).with_indifferent_access
end
# Returns the HTTP request method as a lowercase symbol (:get, for example)
def method
@request_method ||= @env['REQUEST_METHOD'].downcase.to_sym
end
# Is this a GET request? Equivalent to request.method == :get
def get?
method == :get
end
# Is this a POST request? Equivalent to request.method == :post
def post?
method == :post
end
# Is this a PUT request? Equivalent to request.method == :put
def put?
method == :put
end
# Is this a DELETE request? Equivalent to request.method == :delete
def delete?
method == :delete
end
# Is this a HEAD request? Equivalent to request.method == :head
def head?
method == :head
end
# Determine whether the body of a HTTP call is URL-encoded (default)
# or matches one of the registered param_parsers.
#
# For backward compatibility, the post format is extracted from the
# X-Post-Data-Format HTTP header if present.
def content_type
@content_type ||=
begin
content_type = @env['CONTENT_TYPE'].to_s.downcase
if x_post_format = @env['HTTP_X_POST_DATA_FORMAT']
case x_post_format.to_s.downcase
when 'yaml'
content_type = 'application/x-yaml'
when 'xml'
content_type = 'application/xml'
end
end
Mime::Type.lookup(content_type)
end
end
# Returns the accepted MIME type for the request
def accepts
@accepts ||=
if @env['HTTP_ACCEPT'].to_s.strip.empty?
[ content_type, Mime::ALL ]
else
Mime::Type.parse(@env['HTTP_ACCEPT'])
end
end
# Returns true if the request's "X-Requested-With" header contains
# "XMLHttpRequest". (The Prototype Javascript library sends this header with
# every Ajax request.)
def xml_http_request?
not /XMLHttpRequest/i.match(@env['HTTP_X_REQUESTED_WITH']).nil?
end
alias xhr? :xml_http_request?
# Determine originating IP address. REMOTE_ADDR is the standard
# but will fail if the user is behind a proxy. HTTP_CLIENT_IP and/or
# HTTP_X_FORWARDED_FOR are set by proxies so check for these before
# falling back to REMOTE_ADDR. HTTP_X_FORWARDED_FOR may be a comma-
# delimited list in the case of multiple chained proxies; the first is
# the originating IP.
def remote_ip
return @env['HTTP_CLIENT_IP'] if @env.include? 'HTTP_CLIENT_IP'
if @env.include? 'HTTP_X_FORWARDED_FOR' then
remote_ips = @env['HTTP_X_FORWARDED_FOR'].split(',').reject do |ip|
ip =~ /^unknown$|^(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\./i
end
return remote_ips.first.strip unless remote_ips.empty?
end
@env['REMOTE_ADDR']
end
# Returns the domain part of a host, such as rubyonrails.org in "www.rubyonrails.org". You can specify
# a different tld_length, such as 2 to catch rubyonrails.co.uk in "www.rubyonrails.co.uk".
def domain(tld_length = 1)
return nil if !/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/.match(host).nil? or host.nil?
host.split('.').last(1 + tld_length).join('.')
end
# Returns all the subdomains as an array, so ["dev", "www"] would be returned for "dev.www.rubyonrails.org".
# You can specify a different tld_length, such as 2 to catch ["www"] instead of ["www", "rubyonrails"]
# in "www.rubyonrails.co.uk".
def subdomains(tld_length = 1)
return [] unless host
parts = host.split('.')
parts[0..-(tld_length+2)]
end
# Receive the raw post data.
# This is useful for services such as REST, XMLRPC and SOAP
# which communicate over HTTP POST but don't use the traditional parameter format.
def raw_post
@env['RAW_POST_DATA']
end
# Returns the request URI correctly, taking into account the idiosyncracies
# of the various servers.
def request_uri
if uri = @env['REQUEST_URI']
(%r{^\w+\://[^/]+(/.*|$)$} =~ uri) ? $1 : uri # Remove domain, which webrick puts into the request_uri.
else # REQUEST_URI is blank under IIS - get this from PATH_INFO and SCRIPT_NAME
script_filename = @env['SCRIPT_NAME'].to_s.match(%r{[^/]+$})
uri = @env['PATH_INFO']
uri = uri.sub(/#{script_filename}\//, '') unless script_filename.nil?
unless (env_qs = @env['QUERY_STRING']).nil? || env_qs.empty?
uri << '?' << env_qs
end
uri
end
end
# Return 'https://' if this is an SSL request and 'http://' otherwise.
def protocol
ssl? ? 'https://' : 'http://'
end
# Is this an SSL request?
def ssl?
@env['HTTPS'] == 'on' || @env['HTTP_X_FORWARDED_PROTO'] == 'https'
end
# Returns the interpreted path to requested resource after all the installation directory of this application was taken into account
def path
path = (uri = request_uri) ? uri.split('?').first : ''
# Cut off the path to the installation directory if given
root = relative_url_root
path[0, root.length] = '' if root
path || ''
end
# Returns the path minus the web server relative installation directory.
# This can be set with the environment variable RAILS_RELATIVE_URL_ROOT.
# It can be automatically extracted for Apache setups. If the server is not
# Apache, this method returns an empty string.
def relative_url_root
@@relative_url_root ||= case
when @env["RAILS_RELATIVE_URL_ROOT"]
@env["RAILS_RELATIVE_URL_ROOT"]
when server_software == 'apache'
@env["SCRIPT_NAME"].to_s.sub(/\/dispatch\.(fcgi|rb|cgi)$/, '')
else
''
end
end
# Returns the port number of this request as an integer.
def port
@port_as_int ||= @env['SERVER_PORT'].to_i
end
# Returns the standard port number for this request's protocol
def standard_port
case protocol
when 'https://' then 443
else 80
end
end
# Returns a port suffix like ":8080" if the port number of this request
# is not the default HTTP port 80 or HTTPS port 443.
def port_string
(port == standard_port) ? '' : ":#{port}"
end
# Returns a host:port string for this request, such as example.com or
# example.com:8080.
def host_with_port
host + port_string
end
def path_parameters=(parameters) #:nodoc:
@path_parameters = parameters
@symbolized_path_parameters = @parameters = nil
end
# The same as path_parameters with explicitly symbolized keys
def symbolized_path_parameters
@symbolized_path_parameters ||= path_parameters.symbolize_keys
end
# Returns a hash with the parameters used to form the path of the request
#
# Example:
#
# {:action => 'my_action', :controller => 'my_controller'}
def path_parameters
@path_parameters ||= {}
end
# Returns the lowercase name of the HTTP server software.
def server_software
(@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ @env['SERVER_SOFTWARE']) ? $1.downcase : nil
end
#--
# Must be implemented in the concrete request
#++
def query_parameters #:nodoc:
end
def request_parameters #:nodoc:
end
# Returns the host for this request, such as example.com.
def host
end
def cookies #:nodoc:
end
def session #:nodoc:
end
def session=(session) #:nodoc:
@session = session
end
def reset_session #:nodoc:
end
end
end
module ActionController #:nodoc:
# Actions that fail to perform as expected throw exceptions. These exceptions can either be rescued for the public view
# (with a nice user-friendly explanation) or for the developers view (with tons of debugging information). The developers view
# is already implemented by the Action Controller, but the public view should be tailored to your specific application. So too
# could the decision on whether something is a public or a developer request.
#
# You can tailor the rescuing behavior and appearance by overwriting the following two stub methods.
module Rescue
def self.append_features(base) #:nodoc:
super
base.extend(ClassMethods)
base.class_eval do
alias_method :perform_action_without_rescue, :perform_action
alias_method :perform_action, :perform_action_with_rescue
end
end
module ClassMethods #:nodoc:
def process_with_exception(request, response, exception)
new.process(request, response, :rescue_action, exception)
end
end
protected
# Exception handler called when the performance of an action raises an exception.
def rescue_action(exception)
log_error(exception) if logger
erase_results if performed?
if consider_all_requests_local || local_request?
rescue_action_locally(exception)
else
rescue_action_in_public(exception)
end
end
# Overwrite to implement custom logging of errors. By default logs as fatal.
def log_error(exception) #:doc:
if ActionView::TemplateError === exception
logger.fatal(exception.to_s)
else
logger.fatal(
"\n\n#{exception.class} (#{exception.message}):\n " +
clean_backtrace(exception).join("\n ") +
"\n\n"
)
end
end
# Overwrite to implement public exception handling (for requests answering false to local_request?).
def rescue_action_in_public(exception) #:doc:
case exception
when RoutingError, UnknownAction then
render_text(IO.read(File.join(RAILS_ROOT, 'public', '404.html')), "404 Not Found")
else render_text "
Application error (Rails)
"
end
end
# Overwrite to expand the meaning of a local request in order to show local rescues on other occurrences than
# the remote IP being 127.0.0.1. For example, this could include the IP of the developer machine when debugging
# remotely.
def local_request? #:doc:
[@request.remote_addr, @request.remote_ip] == ["127.0.0.1"] * 2
end
# Renders a detailed diagnostics screen on action exceptions.
def rescue_action_locally(exception)
add_variables_to_assigns
@template.instance_variable_set("@exception", exception)
@template.instance_variable_set("@rescues_path", File.dirname(__FILE__) + "/templates/rescues/")
@template.send(:assign_variables_from_controller)
@template.instance_variable_set("@contents", @template.render_file(template_path_for_local_rescue(exception), false))
@headers["Content-Type"] = "text/html"
render_file(rescues_path("layout"), response_code_for_rescue(exception))
end
private
def perform_action_with_rescue #:nodoc:
begin
perform_action_without_rescue
rescue Object => exception
if defined?(Breakpoint) && @params["BP-RETRY"]
msg = exception.backtrace.first
if md = /^(.+?):(\d+)(?::in `(.+)')?$/.match(msg) then
origin_file, origin_line = md[1], md[2].to_i
set_trace_func(lambda do |type, file, line, method, context, klass|
if file == origin_file and line == origin_line then
set_trace_func(nil)
@params["BP-RETRY"] = false
callstack = caller
callstack.slice!(0) if callstack.first["rescue.rb"]
file, line, method = *callstack.first.match(/^(.+?):(\d+)(?::in `(.*?)')?/).captures
message = "Exception at #{file}:#{line}#{" in `#{method}'" if method}." # `´ ( for ruby-mode)
Breakpoint.handle_breakpoint(context, message, file, line)
end
end)
retry
end
end
rescue_action(exception)
end
end
def rescues_path(template_name)
File.dirname(__FILE__) + "/templates/rescues/#{template_name}.rhtml"
end
def template_path_for_local_rescue(exception)
rescues_path(
case exception
when MissingTemplate then "missing_template"
when RoutingError then "routing_error"
when UnknownAction then "unknown_action"
when ActionView::TemplateError then "template_error"
else "diagnostics"
end
)
end
def response_code_for_rescue(exception)
case exception
when UnknownAction, RoutingError then "404 Page Not Found"
else "500 Internal Error"
end
end
def clean_backtrace(exception)
exception.backtrace.collect { |line| Object.const_defined?(:RAILS_ROOT) ? line.gsub(RAILS_ROOT, "") : line }
end
end
end
module ActionController
class AbstractResponse #:nodoc:
DEFAULT_HEADERS = { "Cache-Control" => "no-cache" }
attr_accessor :body, :headers, :session, :cookies, :assigns, :template, :redirected_to, :redirected_to_method_params
def initialize
@body, @headers, @session, @assigns = "", DEFAULT_HEADERS.merge("cookie" => []), [], []
end
def redirect(to_url, permanently = false)
@headers["Status"] = "302 Found" unless @headers["Status"] == "301 Moved Permanently"
@headers["location"] = to_url
@body = "You are being redirected."
end
end
endmodule ActionController
module Routing #:nodoc:
class << self
def expiry_hash(options, recall)
k = v = nil
expire_on = {}
options.each {|k, v| expire_on[k] = ((rcv = recall[k]) && (rcv != v))}
expire_on
end
def extract_parameter_value(parameter) #:nodoc:
CGI.escape((parameter.respond_to?(:to_param) ? parameter.to_param : parameter).to_s)
end
def controller_relative_to(controller, previous)
if controller.nil? then previous
elsif controller[0] == ?/ then controller[1..-1]
elsif %r{^(.*)/} =~ previous then "#{$1}/#{controller}"
else controller
end
end
def treat_hash(hash, keys_to_delete = [])
k = v = nil
hash.each do |k, v|
if v then hash[k] = (v.respond_to? :to_param) ? v.to_param.to_s : v.to_s
else
hash.delete k
keys_to_delete << k
end
end
hash
end
def test_condition(expression, condition)
case condition
when String then "(#{expression} == #{condition.inspect})"
when Regexp then
condition = Regexp.new("^#{condition.source}$") unless /^\^.*\$$/ =~ condition.source
"(#{condition.inspect} =~ #{expression})"
when Array then
conds = condition.collect do |condition|
cond = test_condition(expression, condition)
(cond[0, 1] == '(' && cond[-1, 1] == ')') ? cond : "(#{cond})"
end
"(#{conds.join(' || ')})"
when true then expression
when nil then "! #{expression}"
else
raise ArgumentError, "Valid criteria are strings, regular expressions, true, or nil"
end
end
end
class Component #:nodoc:
def dynamic?() false end
def optional?() false end
def key() nil end
def self.new(string, *args)
return super(string, *args) unless self == Component
case string
when ':controller' then ControllerComponent.new(:controller, *args)
when /^:(\w+)$/ then DynamicComponent.new($1, *args)
when /^\*(\w+)$/ then PathComponent.new($1, *args)
else StaticComponent.new(string, *args)
end
end
end
class StaticComponent < Component #:nodoc:
attr_reader :value
def initialize(value)
@value = value
end
def write_recognition(g)
g.if_next_matches(value) do |gp|
gp.move_forward {|gpp| gpp.continue}
end
end
def write_generation(g)
g.add_segment(value) {|gp| gp.continue }
end
end
class DynamicComponent < Component #:nodoc:
attr_reader :key, :default
attr_accessor :condition
def dynamic?() true end
def optional?() @optional end
def default=(default)
@optional = true
@default = default
end
def initialize(key, options = {})
@key = key.to_sym
@optional = false
default, @condition = options[:default], options[:condition]
self.default = default if options.key?(:default)
end
def default_check(g)
presence = "#{g.hash_value(key, !! default)}"
if default
"!(#{presence} && #{g.hash_value(key, false)} != #{default.to_s.inspect})"
else
"! #{presence}"
end
end
def write_generation(g)
wrote_dropout = write_dropout_generation(g)
write_continue_generation(g, wrote_dropout)
end
def write_dropout_generation(g)
return false unless optional? && g.after.all? {|c| c.optional?}
check = [default_check(g)]
gp = g.dup # Use another generator to write the conditions after the first &&
# We do this to ensure that the generator will not assume x_value is set. It will
# not be set if it follows a false condition -- for example, false && (x = 2)
check += gp.after.map {|c| c.default_check gp}
gp.if(check.join(' && ')) { gp.finish } # If this condition is met, we stop here
true
end
def write_continue_generation(g, use_else)
test = Routing.test_condition(g.hash_value(key, true, default), condition || true)
check = (use_else && condition.nil? && default) ? [:else] : [use_else ? :elsif : :if, test]
g.send(*check) do |gp|
gp.expire_for_keys(key) unless gp.after.empty?
add_segments_to(gp) {|gpp| gpp.continue}
end
end
def add_segments_to(g)
g.add_segment(%(\#{CGI.escape(#{g.hash_value(key, true, default)})})) {|gp| yield gp}
end
def recognition_check(g)
test_type = [true, nil].include?(condition) ? :presence : :constraint
prefix = condition.is_a?(Regexp) ? "#{g.next_segment(true)} && " : ''
check = prefix + Routing.test_condition(g.next_segment(true), condition || true)
g.if(check) {|gp| yield gp, test_type}
end
def write_recognition(g)
test_type = nil
recognition_check(g) do |gp, test_type|
assign_result(gp) {|gpp| gpp.continue}
end
if optional? && g.after.all? {|c| c.optional?}
call = (test_type == :presence) ? [:else] : [:elsif, "! #{g.next_segment(true)}"]
g.send(*call) do |gp|
assign_default(gp)
gp.after.each {|c| c.assign_default(gp)}
gp.finish(false)
end
end
end
def assign_result(g, with_default = false)
g.result key, "CGI.unescape(#{g.next_segment(true, with_default ? default : nil)})"
g.move_forward {|gp| yield gp}
end
def assign_default(g)
g.constant_result key, default unless default.nil?
end
end
class ControllerComponent < DynamicComponent #:nodoc:
def key() :controller end
def add_segments_to(g)
g.add_segment(%(\#{#{g.hash_value(key, true, default)}})) {|gp| yield gp}
end
def recognition_check(g)
g << "controller_result = ::ActionController::Routing::ControllerComponent.traverse_to_controller(#{g.path_name}, #{g.index_name})"
g.if('controller_result') do |gp|
gp << 'controller_value, segments_to_controller = controller_result'
if condition
gp << "controller_path = #{gp.path_name}[#{gp.index_name},segments_to_controller].join('/')"
gp.if(Routing.test_condition("controller_path", condition)) do |gpp|
gpp.move_forward('segments_to_controller') {|gppp| yield gppp, :constraint}
end
else
gp.move_forward('segments_to_controller') {|gpp| yield gpp, :constraint}
end
end
end
def assign_result(g)
g.result key, 'controller_value'
yield g
end
def assign_default(g)
ControllerComponent.assign_controller(g, default)
end
class << self
def assign_controller(g, controller)
expr = "::#{controller.split('/').collect {|c| c.camelize}.join('::')}Controller"
g.result :controller, expr, true
end
def traverse_to_controller(segments, start_at = 0)
mod = ::Object
length = segments.length
index = start_at
mod_name = controller_name = segment = nil
while index < length
return nil unless /\A[A-Za-z][A-Za-z\d_]*\Z/ =~ (segment = segments[index])
index += 1
mod_name = segment.camelize
controller_name = "#{mod_name}Controller"
path_suffix = File.join(segments[start_at..(index - 1)])
next_mod = nil
# If the controller is already present, or if we load it, return it.
if mod.const_defined?(controller_name) || attempt_load(mod, controller_name, path_suffix + "_controller") == :defined
controller = mod.const_get(controller_name)
return nil unless controller.is_a?(Class) && controller.ancestors.include?(ActionController::Base) # it's not really a controller?
return [controller, (index - start_at)]
end
# No controller? Look for the module
if mod.const_defined? mod_name
next_mod = mod.send(:const_get, mod_name)
next_mod = nil unless next_mod.is_a?(Module)
else
# Try to load a file that defines the module we want.
case attempt_load(mod, mod_name, path_suffix)
when :defined then next_mod = mod.const_get mod_name
when :dir then # We didn't find a file, but there's a dir.
next_mod = Module.new # So create a module for the directory
mod.send :const_set, mod_name, next_mod
else
return nil
end
end
mod = next_mod
return nil unless mod && mod.is_a?(Module)
end
nil
end
protected
def safe_load_paths #:nodoc:
if defined?(RAILS_ROOT)
$LOAD_PATH.select do |base|
base = File.expand_path(base)
extended_root = File.expand_path(RAILS_ROOT)
# Exclude all paths that are not nested within app, lib, or components.
base.match(/\A#{Regexp.escape(extended_root)}\/*(app|lib|components)\/[a-z]/) || base =~ %r{rails-[\d.]+/builtin}
end
else
$LOAD_PATH
end
end
def attempt_load(mod, const_name, path)
has_dir = false
safe_load_paths.each do |load_path|
full_path = File.join(load_path, path)
file_path = full_path + '.rb'
if File.file?(file_path) # Found a .rb file? Load it up
require_dependency(file_path)
return :defined if mod.const_defined? const_name
else
has_dir ||= File.directory?(full_path)
end
end
return (has_dir ? :dir : nil)
end
end
end
class PathComponent < DynamicComponent #:nodoc:
def optional?() true end
def default() [] end
def condition() nil end
def default=(value)
raise RoutingError, "All path components have an implicit default of []" unless value == []
end
def write_generation(g)
raise RoutingError, 'Path components must occur last' unless g.after.empty?
g.if("#{g.hash_value(key, true)} && ! #{g.hash_value(key, true)}.empty?") do
g << "#{g.hash_value(key, true)} = #{g.hash_value(key, true)}.join('/') unless #{g.hash_value(key, true)}.is_a?(String)"
g.add_segment("\#{CGI.escape_skipping_slashes(#{g.hash_value(key, true)})}") {|gp| gp.finish }
end
g.else { g.finish }
end
def write_recognition(g)
raise RoutingError, "Path components must occur last" unless g.after.empty?
start = g.index_name
start = "(#{start})" unless /^\w+$/ =~ start
value_expr = "#{g.path_name}[#{start}..-1] || []"
g.result key, "ActionController::Routing::PathComponent::Result.new_escaped(#{value_expr})"
g.finish(false)
end
class Result < ::Array #:nodoc:
def to_s() join '/' end
def self.new_escaped(strings)
new strings.collect {|str| CGI.unescape str}
end
end
end
class Route #:nodoc:
attr_accessor :components, :known
attr_reader :path, :options, :keys, :defaults
def initialize(path, options = {})
@path, @options = path, options
initialize_components path
defaults, conditions = initialize_hashes options.dup
@defaults = defaults.dup
configure_components(defaults, conditions)
add_default_requirements
initialize_keys
end
def inspect
"<#{self.class} #{path.inspect}, #{options.inspect[1..-1]}>"
end
def write_generation(generator = CodeGeneration::GenerationGenerator.new)
generator.before, generator.current, generator.after = [], components.first, (components[1..-1] || [])
if known.empty? then generator.go
else
# Alter the conditions to allow :action => 'index' to also catch :action => nil
altered_known = known.collect do |k, v|
if k == :action && v== 'index' then [k, [nil, 'index']]
else [k, v]
end
end
generator.if(generator.check_conditions(altered_known)) {|gp| gp.go }
end
generator
end
def write_recognition(generator = CodeGeneration::RecognitionGenerator.new)
g = generator.dup
g.share_locals_with generator
g.before, g.current, g.after = [], components.first, (components[1..-1] || [])
known.each do |key, value|
if key == :controller then ControllerComponent.assign_controller(g, value)
else g.constant_result(key, value)
end
end
g.go
generator
end
def initialize_keys
@keys = (components.collect {|c| c.key} + known.keys).compact
@keys.freeze
end
def extra_keys(options)
options.keys - @keys
end
def matches_controller?(controller)
if known[:controller] then known[:controller] == controller
else
c = components.find {|c| c.key == :controller}
return false unless c
return c.condition.nil? || eval(Routing.test_condition('controller', c.condition))
end
end
protected
def initialize_components(path)
path = path.split('/') if path.is_a? String
path.shift if path.first.blank?
self.components = path.collect {|str| Component.new str}
end
def initialize_hashes(options)
path_keys = components.collect {|c| c.key }.compact
self.known = {}
defaults = options.delete(:defaults) || {}
conditions = options.delete(:require) || {}
conditions.update(options.delete(:requirements) || {})
options.each do |k, v|
if path_keys.include?(k) then (v.is_a?(Regexp) ? conditions : defaults)[k] = v
else known[k] = v
end
end
[defaults, conditions]
end
def configure_components(defaults, conditions)
components.each do |component|
if defaults.key?(component.key) then component.default = defaults[component.key]
elsif component.key == :action then component.default = 'index'
elsif component.key == :id then component.default = nil
end
component.condition = conditions[component.key] if conditions.key?(component.key)
end
end
def add_default_requirements
component_keys = components.collect {|c| c.key}
known[:action] ||= 'index' unless component_keys.include? :action
end
end
class RouteSet #:nodoc:
attr_reader :routes, :categories, :controller_to_selector
def initialize
@routes = []
@generation_methods = Hash.new(:generate_default_path)
end
def generate(options, request_or_recall_hash = {})
recall = request_or_recall_hash.is_a?(Hash) ? request_or_recall_hash : request_or_recall_hash.symbolized_path_parameters
use_recall = true
controller = options[:controller]
options[:action] ||= 'index' if controller
recall_controller = recall[:controller]
if (recall_controller && recall_controller.include?(?/)) || (controller && controller.include?(?/))
recall = {} if controller && controller[0] == ?/
options[:controller] = Routing.controller_relative_to(controller, recall_controller)
end
options = recall.dup if options.empty? # XXX move to url_rewriter?
keys_to_delete = []
Routing.treat_hash(options, keys_to_delete)
merged = recall.merge(options)
keys_to_delete.each {|key| merged.delete key}
expire_on = Routing.expiry_hash(options, recall)
generate_path(merged, options, expire_on)
end
def generate_path(merged, options, expire_on)
send @generation_methods[merged[:controller]], merged, options, expire_on
end
def generate_default_path(*args)
write_generation
generate_default_path(*args)
end
def write_generation
method_sources = []
@generation_methods = Hash.new(:generate_default_path)
categorize_routes.each do |controller, routes|
next unless routes.length < @routes.length
ivar = controller.gsub('/', '__')
method_name = "generate_path_for_#{ivar}".to_sym
instance_variable_set "@#{ivar}", routes
code = generation_code_for(ivar, method_name).to_s
method_sources << code
filename = "generated_code/routing/generation_for_controller_#{controller}.rb"
eval(code, nil, filename)
@generation_methods[controller.to_s] = method_name
@generation_methods[controller.to_sym] = method_name
end
code = generation_code_for('routes', 'generate_default_path').to_s
eval(code, nil, 'generated_code/routing/generation.rb')
return (method_sources << code)
end
def recognize(request)
string_path = request.path
string_path.chomp! if string_path[0] == ?/
path = string_path.split '/'
path.shift
hash = recognize_path(path)
return recognition_failed(request) unless hash && hash['controller']
controller = hash['controller']
hash['controller'] = controller.controller_path
request.path_parameters = hash
controller.new
end
alias :recognize! :recognize
def recognition_failed(request)
raise ActionController::RoutingError, "Recognition failed for #{request.path.inspect}"
end
def write_recognition
g = generator = CodeGeneration::RecognitionGenerator.new
g.finish_statement = Proc.new {|hash_expr| "return #{hash_expr}"}
g.def "self.recognize_path(path)" do
each do |route|
g << 'index = 0'
route.write_recognition(g)
end
end
eval g.to_s, nil, 'generated/routing/recognition.rb'
return g.to_s
end
def generation_code_for(ivar = 'routes', method_name = nil)
routes = instance_variable_get('@' + ivar)
key_ivar = "@keys_for_#{ivar}"
instance_variable_set(key_ivar, routes.collect {|route| route.keys})
g = generator = CodeGeneration::GenerationGenerator.new
g.def "self.#{method_name}(merged, options, expire_on)" do
g << 'unused_count = options.length + 1'
g << "unused_keys = keys = options.keys"
g << 'path = nil'
routes.each_with_index do |route, index|
g << "new_unused_keys = keys - #{key_ivar}[#{index}]"
g << 'new_path = ('
g.source.indent do
if index.zero?
g << "new_unused_count = new_unused_keys.length"
g << "hash = merged; not_expired = true"
route.write_generation(g.dup)
else
g.if "(new_unused_count = new_unused_keys.length) < unused_count" do |gp|
gp << "hash = merged; not_expired = true"
route.write_generation(gp)
end
end
end
g.source.lines.last << ' )' # Add the closing brace to the end line
g.if 'new_path' do
g << 'return new_path, [] if new_unused_count.zero?'
g << 'path = new_path; unused_keys = new_unused_keys; unused_count = new_unused_count'
end
end
g << "raise RoutingError, \"No url can be generated for the hash \#{options.inspect}\" unless path"
g << "return path, unused_keys"
end
return g
end
def categorize_routes
@categorized_routes = by_controller = Hash.new(self)
known_controllers.each do |name|
set = by_controller[name] = []
each do |route|
set << route if route.matches_controller? name
end
end
@categorized_routes
end
def known_controllers
@routes.inject([]) do |known, route|
if (controller = route.known[:controller])
if controller.is_a?(Regexp)
known << controller.source.scan(%r{[\w\d/]+}).select {|word| controller =~ word}
else known << controller
end
end
known
end.uniq
end
def reload
NamedRoutes.clear
if defined?(RAILS_ROOT) then load(File.join(RAILS_ROOT, 'config', 'routes.rb'))
else connect(':controller/:action/:id', :action => 'index', :id => nil)
end
NamedRoutes.install
end
def connect(*args)
new_route = Route.new(*args)
@routes << new_route
return new_route
end
def draw
old_routes = @routes
@routes = []
begin yield self
rescue
@routes = old_routes
raise
end
write_generation
write_recognition
end
def empty?() @routes.empty? end
def each(&block) @routes.each(&block) end
# Defines a new named route with the provided name and arguments.
# This method need only be used when you wish to use a name that a RouteSet instance
# method exists for, such as categories.
#
# For example, map.categories '/categories', :controller => 'categories' will not work
# due to RouteSet#categories.
def named_route(name, path, hash = {})
route = connect(path, hash)
NamedRoutes.name_route(route, name)
route
end
def method_missing(name, *args)
(1..2).include?(args.length) ? named_route(name, *args) : super(name, *args)
end
def extra_keys(options, recall = {})
generate(options.dup, recall).last
end
end
module NamedRoutes #:nodoc:
Helpers = []
class << self
def clear() Helpers.clear end
def hash_access_name(name)
"hash_for_#{name}_url"
end
def url_helper_name(name)
"#{name}_url"
end
def known_hash_for_route(route)
hash = route.known.symbolize_keys
route.defaults.each do |key, value|
hash[key.to_sym] ||= value if value
end
hash[:controller] = "/#{hash[:controller]}"
hash
end
def define_hash_access_method(route, name)
hash = known_hash_for_route(route)
define_method(hash_access_name(name)) do |*args|
args.first ? hash.merge(args.first) : hash
end
end
def name_route(route, name)
define_hash_access_method(route, name)
module_eval(%{def #{url_helper_name name}(options = {})
url_for(#{hash_access_name(name)}.merge(options))
end}, "generated/routing/named_routes/#{name}.rb")
protected url_helper_name(name), hash_access_name(name)
Helpers << url_helper_name(name).to_sym
Helpers << hash_access_name(name).to_sym
Helpers.uniq!
end
def install(cls = ActionController::Base)
cls.send :include, self
if cls.respond_to? :helper_method
Helpers.each do |helper_name|
cls.send :helper_method, helper_name
end
end
end
end
end
Routes = RouteSet.new
end
end
module ActionController
module Scaffolding # :nodoc:
def self.append_features(base)
super
base.extend(ClassMethods)
end
# Scaffolding is a way to quickly put an Active Record class online by providing a series of standardized actions
# for listing, showing, creating, updating, and destroying objects of the class. These standardized actions come
# with both controller logic and default templates that through introspection already know which fields to display
# and which input types to use. Example:
#
# class WeblogController < ActionController::Base
# scaffold :entry
# end
#
# This tiny piece of code will add all of the following methods to the controller:
#
# class WeblogController < ActionController::Base
# verify :method => :post, :only => [ :destroy, :create, :update ],
# :redirect_to => { :action => :list }
#
# def index
# list
# end
#
# def list
# @entries = Entry.find_all
# render_scaffold "list"
# end
#
# def show
# @entry = Entry.find(params[:id])
# render_scaffold
# end
#
# def destroy
# Entry.find(params[:id]).destroy
# redirect_to :action => "list"
# end
#
# def new
# @entry = Entry.new
# render_scaffold
# end
#
# def create
# @entry = Entry.new(params[:entry])
# if @entry.save
# flash[:notice] = "Entry was successfully created"
# redirect_to :action => "list"
# else
# render_scaffold('new')
# end
# end
#
# def edit
# @entry = Entry.find(params[:id])
# render_scaffold
# end
#
# def update
# @entry = Entry.find(params[:id])
# @entry.attributes = params[:entry]
#
# if @entry.save
# flash[:notice] = "Entry was successfully updated"
# redirect_to :action => "show", :id => @entry
# else
# render_scaffold('edit')
# end
# end
# end
#
# The render_scaffold method will first check to see if you've made your own template (like "weblog/show.rhtml" for
# the show action) and if not, then render the generic template for that action. This gives you the possibility of using the
# scaffold while you're building your specific application. Start out with a totally generic setup, then replace one template
# and one action at a time while relying on the rest of the scaffolded templates and actions.
module ClassMethods
# Adds a swath of generic CRUD actions to the controller. The +model_id+ is automatically converted into a class name unless
# one is specifically provide through options[:class_name]. So scaffold :post would use Post as the class
# and @post/@posts for the instance variables.
#
# It's possible to use more than one scaffold in a single controller by specifying options[:suffix] = true. This will
# make scaffold :post, :suffix => true use method names like list_post, show_post, and create_post
# instead of just list, show, and post. If suffix is used, then no index method is added.
def scaffold(model_id, options = {})
options.assert_valid_keys(:class_name, :suffix)
singular_name = model_id.to_s
class_name = options[:class_name] || singular_name.camelize
plural_name = singular_name.pluralize
suffix = options[:suffix] ? "_#{singular_name}" : ""
unless options[:suffix]
module_eval <<-"end_eval", __FILE__, __LINE__
def index
list
end
end_eval
end
module_eval <<-"end_eval", __FILE__, __LINE__
verify :method => :post, :only => [ :destroy#{suffix}, :create#{suffix}, :update#{suffix} ],
:redirect_to => { :action => :list#{suffix} }
def list#{suffix}
@#{singular_name}_pages, @#{plural_name} = paginate :#{plural_name}, :per_page => 10
render#{suffix}_scaffold "list#{suffix}"
end
def show#{suffix}
@#{singular_name} = #{class_name}.find(params[:id])
render#{suffix}_scaffold
end
def destroy#{suffix}
#{class_name}.find(params[:id]).destroy
redirect_to :action => "list#{suffix}"
end
def new#{suffix}
@#{singular_name} = #{class_name}.new
render#{suffix}_scaffold
end
def create#{suffix}
@#{singular_name} = #{class_name}.new(params[:#{singular_name}])
if @#{singular_name}.save
flash[:notice] = "#{class_name} was successfully created"
redirect_to :action => "list#{suffix}"
else
render#{suffix}_scaffold('new')
end
end
def edit#{suffix}
@#{singular_name} = #{class_name}.find(params[:id])
render#{suffix}_scaffold
end
def update#{suffix}
@#{singular_name} = #{class_name}.find(params[:id])
@#{singular_name}.attributes = params[:#{singular_name}]
if @#{singular_name}.save
flash[:notice] = "#{class_name} was successfully updated"
redirect_to :action => "show#{suffix}", :id => @#{singular_name}
else
render#{suffix}_scaffold('edit')
end
end
private
def render#{suffix}_scaffold(action=nil)
action ||= caller_method_name(caller)
# logger.info ("testing template:" + "\#{self.class.controller_path}/\#{action}") if logger
if template_exists?("\#{self.class.controller_path}/\#{action}")
render_action(action)
else
@scaffold_class = #{class_name}
@scaffold_singular_name, @scaffold_plural_name = "#{singular_name}", "#{plural_name}"
@scaffold_suffix = "#{suffix}"
add_instance_variables_to_assigns
@template.instance_variable_set("@content_for_layout", @template.render_file(scaffold_path(action.sub(/#{suffix}$/, "")), false))
if !active_layout.nil?
render_file(active_layout, nil, true)
else
render_file(scaffold_path("layout"))
end
end
end
def scaffold_path(template_name)
File.dirname(__FILE__) + "/templates/scaffolds/" + template_name + ".rhtml"
end
def caller_method_name(caller)
caller.first.scan(/`(.*)'/).first.first # ' ruby-mode
end
end_eval
end
end
end
end
require 'cgi'
require 'cgi/session'
require 'digest/md5'
require 'base64'
class CGI
class Session
# Return this session's underlying Session instance. Useful for the DB-backed session stores.
def model
@dbman.model if @dbman
end
# A session store backed by an Active Record class. A default class is
# provided, but any object duck-typing to an Active Record +Session+ class
# with text +session_id+ and +data+ attributes is sufficient.
#
# The default assumes a +sessions+ tables with columns:
# +id+ (numeric primary key),
# +session_id+ (text, or longtext if your session data exceeds 65K), and
# +data+ (text or longtext; careful if your session data exceeds 65KB).
# The +session_id+ column should always be indexed for speedy lookups.
# Session data is marshaled to the +data+ column in Base64 format.
# If the data you write is larger than the column's size limit,
# ActionController::SessionOverflowError will be raised.
#
# You may configure the table name, primary key, and data column.
# For example, at the end of config/environment.rb:
# CGI::Session::ActiveRecordStore::Session.table_name = 'legacy_session_table'
# CGI::Session::ActiveRecordStore::Session.primary_key = 'session_id'
# CGI::Session::ActiveRecordStore::Session.data_column_name = 'legacy_session_data'
# Note that setting the primary key to the session_id frees you from
# having a separate id column if you don't want it. However, you must
# set session.model.id = session.session_id by hand! A before_filter
# on ApplicationController is a good place.
#
# Since the default class is a simple Active Record, you get timestamps
# for free if you add +created_at+ and +updated_at+ datetime columns to
# the +sessions+ table, making periodic session expiration a snap.
#
# You may provide your own session class implementation, whether a
# feature-packed Active Record or a bare-metal high-performance SQL
# store, by setting
# +CGI::Session::ActiveRecordStore.session_class = MySessionClass+
# You must implement these methods:
# self.find_by_session_id(session_id)
# initialize(hash_of_session_id_and_data)
# attr_reader :session_id
# attr_accessor :data
# save
# destroy
#
# The example SqlBypass class is a generic SQL session store. You may
# use it as a basis for high-performance database-specific stores.
class ActiveRecordStore
# The default Active Record class.
class Session < ActiveRecord::Base
# Customizable data column name. Defaults to 'data'.
cattr_accessor :data_column_name
self.data_column_name = 'data'
before_save :marshal_data!
before_save :raise_on_session_data_overflow!
class << self
# Don't try to reload ARStore::Session in dev mode.
def reloadable? #:nodoc:
false
end
def data_column_size_limit
@data_column_size_limit ||= columns_hash[@@data_column_name].limit
end
# Hook to set up sessid compatibility.
def find_by_session_id(session_id)
setup_sessid_compatibility!
find_by_session_id(session_id)
end
def marshal(data) Base64.encode64(Marshal.dump(data)) if data end
def unmarshal(data) Marshal.load(Base64.decode64(data)) if data end
def create_table!
connection.execute <<-end_sql
CREATE TABLE #{table_name} (
id INTEGER PRIMARY KEY,
#{connection.quote_column_name('session_id')} TEXT UNIQUE,
#{connection.quote_column_name(@@data_column_name)} TEXT(255)
)
end_sql
end
def drop_table!
connection.execute "DROP TABLE #{table_name}"
end
private
# Compatibility with tables using sessid instead of session_id.
def setup_sessid_compatibility!
# Reset column info since it may be stale.
reset_column_information
if columns_hash['sessid']
def self.find_by_session_id(*args)
find_by_sessid(*args)
end
define_method(:session_id) { sessid }
define_method(:session_id=) { |session_id| self.sessid = session_id }
else
def self.find_by_session_id(session_id)
find :first, :conditions => ["session_id #{attribute_condition(session_id)}", session_id]
end
end
end
end
# Lazy-unmarshal session state.
def data
@data ||= self.class.unmarshal(read_attribute(@@data_column_name)) || {}
end
# Has the session been loaded yet?
def loaded?
!! @data
end
private
attr_writer :data
def marshal_data!
return false if !loaded?
write_attribute(@@data_column_name, self.class.marshal(self.data))
end
# Ensures that the data about to be stored in the database is not
# larger than the data storage column. Raises
# ActionController::SessionOverflowError.
def raise_on_session_data_overflow!
return false if !loaded?
limit = self.class.data_column_size_limit
if loaded? and limit and read_attribute(@@data_column_name).size > limit
raise ActionController::SessionOverflowError
end
end
end
# A barebones session store which duck-types with the default session
# store but bypasses Active Record and issues SQL directly. This is
# an example session model class meant as a basis for your own classes.
#
# The database connection, table name, and session id and data columns
# are configurable class attributes. Marshaling and unmarshaling
# are implemented as class methods that you may override. By default,
# marshaling data is +Base64.encode64(Marshal.dump(data))+ and
# unmarshaling data is +Marshal.load(Base64.decode64(data))+.
#
# This marshaling behavior is intended to store the widest range of
# binary session data in a +text+ column. For higher performance,
# store in a +blob+ column instead and forgo the Base64 encoding.
class SqlBypass
# Use the ActiveRecord::Base.connection by default.
cattr_accessor :connection
# The table name defaults to 'sessions'.
cattr_accessor :table_name
@@table_name = 'sessions'
# The session id field defaults to 'session_id'.
cattr_accessor :session_id_column
@@session_id_column = 'session_id'
# The data field defaults to 'data'.
cattr_accessor :data_column
@@data_column = 'data'
class << self
def connection
@@connection ||= ActiveRecord::Base.connection
end
# Look up a session by id and unmarshal its data if found.
def find_by_session_id(session_id)
if record = @@connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{@@connection.quote(session_id)}")
new(:session_id => session_id, :marshaled_data => record['data'])
end
end
def marshal(data) Base64.encode64(Marshal.dump(data)) if data end
def unmarshal(data) Marshal.load(Base64.decode64(data)) if data end
def create_table!
@@connection.execute <<-end_sql
CREATE TABLE #{table_name} (
id INTEGER PRIMARY KEY,
#{@@connection.quote_column_name(session_id_column)} TEXT UNIQUE,
#{@@connection.quote_column_name(data_column)} TEXT
)
end_sql
end
def drop_table!
@@connection.execute "DROP TABLE #{table_name}"
end
end
attr_reader :session_id
attr_writer :data
# Look for normal and marshaled data, self.find_by_session_id's way of
# telling us to postpone unmarshaling until the data is requested.
# We need to handle a normal data attribute in case of a new record.
def initialize(attributes)
@session_id, @data, @marshaled_data = attributes[:session_id], attributes[:data], attributes[:marshaled_data]
@new_record = @marshaled_data.nil?
end
def new_record?
@new_record
end
# Lazy-unmarshal session state.
def data
unless @data
if @marshaled_data
@data, @marshaled_data = self.class.unmarshal(@marshaled_data) || {}, nil
else
@data = {}
end
end
@data
end
def loaded?
!! @data
end
def save
return false if !loaded?
marshaled_data = self.class.marshal(data)
if @new_record
@new_record = false
@@connection.update <<-end_sql, 'Create session'
INSERT INTO #{@@table_name} (
#{@@connection.quote_column_name(@@session_id_column)},
#{@@connection.quote_column_name(@@data_column)} )
VALUES (
#{@@connection.quote(session_id)},
#{@@connection.quote(marshaled_data)} )
end_sql
else
@@connection.update <<-end_sql, 'Update session'
UPDATE #{@@table_name}
SET #{@@connection.quote_column_name(@@data_column)}=#{@@connection.quote(marshaled_data)}
WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)}
end_sql
end
end
def destroy
unless @new_record
@@connection.delete <<-end_sql, 'Destroy session'
DELETE FROM #{@@table_name}
WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)}
end_sql
end
end
end
# The class used for session storage. Defaults to
# CGI::Session::ActiveRecordStore::Session.
cattr_accessor :session_class
self.session_class = Session
# Find or instantiate a session given a CGI::Session.
def initialize(session, option = nil)
session_id = session.session_id
unless @session = ActiveRecord::Base.silence { @@session_class.find_by_session_id(session_id) }
unless session.new_session
raise CGI::Session::NoSession, 'uninitialized session'
end
@session = @@session_class.new(:session_id => session_id, :data => {})
# session saving can be lazy again, because of improved component implementation
# therefore next line gets commented out:
# @session.save
end
end
# Access the underlying session model.
def model
@session
end
# Restore session state. The session model handles unmarshaling.
def restore
if @session
@session.data
end
end
# Save session store.
def update
if @session
ActiveRecord::Base.silence { @session.save }
end
end
# Save and close the session store.
def close
if @session
update
@session = nil
end
end
# Delete and close the session store.
def delete
if @session
ActiveRecord::Base.silence { @session.destroy }
@session = nil
end
end
protected
def logger
ActionController::Base.logger rescue nil
end
end
end
end
#!/usr/local/bin/ruby -w
# This is a really simple session storage daemon, basically just a hash,
# which is enabled for DRb access.
require 'drb'
session_hash = Hash.new
session_hash.instance_eval { @mutex = Mutex.new }
class <:only and
# :except clauses to restrict the subset, otherwise options
# apply to all actions on this controller.
#
# The session options are inheritable, as well, so if you specify them in
# a parent controller, they apply to controllers that extend the parent.
#
# Usage:
#
# # turn off session management for all actions.
# session :off
#
# # turn off session management for all actions _except_ foo and bar.
# session :off, :except => %w(foo bar)
#
# # turn off session management for only the foo and bar actions.
# session :off, :only => %w(foo bar)
#
# # the session will only work over HTTPS, but only for the foo action
# session :only => :foo, :session_secure => true
#
# # the session will only be disabled for 'foo', and only if it is
# # requested as a web service
# session :off, :only => :foo,
# :if => Proc.new { |req| req.parameters[:ws] }
#
# All session options described for ActionController::Base.process_cgi
# are valid arguments.
def session(*args)
options = Hash === args.last ? args.pop : {}
options[:disabled] = true if !args.empty?
options[:only] = [*options[:only]].map { |o| o.to_s } if options[:only]
options[:except] = [*options[:except]].map { |o| o.to_s } if options[:except]
if options[:only] && options[:except]
raise ArgumentError, "only one of either :only or :except are allowed"
end
write_inheritable_array("session_options", [options])
end
def cached_session_options #:nodoc:
@session_options ||= read_inheritable_attribute("session_options") || []
end
def session_options_for(request, action) #:nodoc:
if (session_options = cached_session_options).empty?
{}
else
options = {}
action = action.to_s
session_options.each do |opts|
next if opts[:if] && !opts[:if].call(request)
if opts[:only] && opts[:only].include?(action)
options.merge!(opts)
elsif opts[:except] && !opts[:except].include?(action)
options.merge!(opts)
elsif !opts[:only] && !opts[:except]
options.merge!(opts)
end
end
if options.empty? then options
else
options.delete :only
options.delete :except
options.delete :if
options[:disabled] ? false : options
end
end
end
end
def process_with_session_management_support(request, response, method = :perform_action, *arguments) #:nodoc:
set_session_options(request)
process_without_session_management_support(request, response, method, *arguments)
end
private
def set_session_options(request)
request.session_options = self.class.session_options_for(request, request.parameters["action"] || "index")
end
def process_cleanup_with_session_management_support
process_cleanup_without_session_management_support
clear_persistent_model_associations
end
# Clear cached associations in session data so they don't overflow
# the database field. Only applies to ActiveRecordStore since there
# is not a standard way to iterate over session data.
def clear_persistent_model_associations #:doc:
if defined?(@session) && @session.instance_variables.include?('@data')
session_data = @session.instance_variable_get('@data')
if session_data && session_data.respond_to?(:each_value)
session_data.each_value do |obj|
obj.clear_association_cache if obj.respond_to?(:clear_association_cache)
end
end
end
end
end
end
module ActionController #:nodoc:
# Methods for sending files and streams to the browser instead of rendering.
module Streaming
DEFAULT_SEND_FILE_OPTIONS = {
:type => 'application/octet-stream'.freeze,
:disposition => 'attachment'.freeze,
:stream => true,
:buffer_size => 4096
}.freeze
protected
# Sends the file by streaming it 4096 bytes at a time. This way the
# whole file doesn't need to be read into memory at once. This makes
# it feasible to send even large files.
#
# Be careful to sanitize the path parameter if it coming from a web
# page. send_file(params[:path]) allows a malicious user to
# download any file on your server.
#
# Options:
# * :filename - suggests a filename for the browser to use.
# Defaults to File.basename(path).
# * :type - specifies an HTTP content type.
# Defaults to 'application/octet-stream'.
# * :disposition - specifies whether the file will be shown inline or downloaded.
# Valid values are 'inline' and 'attachment' (default).
# * :stream - whether to send the file to the user agent as it is read (true)
# or to read the entire file before sending (false). Defaults to true.
# * :buffer_size - specifies size (in bytes) of the buffer used to stream the file.
# Defaults to 4096.
# * :status - specifies the status code to send with the response. Defaults to '200 OK'.
#
# The default Content-Type and Content-Disposition headers are
# set to download arbitrary binary files in as many browsers as
# possible. IE versions 4, 5, 5.5, and 6 are all known to have
# a variety of quirks (especially when downloading over SSL).
#
# Simple download:
# send_file '/path/to.zip'
#
# Show a JPEG in the browser:
# send_file '/path/to.jpeg', :type => 'image/jpeg', :disposition => 'inline'
#
# Show a 404 page in the browser:
# send_file '/path/to/404.html, :type => 'text/html; charset=utf-8', :status => 404
#
# Read about the other Content-* HTTP headers if you'd like to
# provide the user with more information (such as Content-Description).
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11
#
# Also be aware that the document may be cached by proxies and browsers.
# The Pragma and Cache-Control headers declare how the file may be cached
# by intermediaries. They default to require clients to validate with
# the server before releasing cached responses. See
# http://www.mnot.net/cache_docs/ for an overview of web caching and
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
# for the Cache-Control header spec.
def send_file(path, options = {}) #:doc:
raise MissingFile, "Cannot read file #{path}" unless File.file?(path) and File.readable?(path)
options[:length] ||= File.size(path)
options[:filename] ||= File.basename(path)
send_file_headers! options
@performed_render = false
if options[:stream]
render :status => options[:status], :text => Proc.new { |response, output|
logger.info "Streaming file #{path}" unless logger.nil?
len = options[:buffer_size] || 4096
File.open(path, 'rb') do |file|
if output.respond_to?(:syswrite)
begin
while true
output.syswrite(file.sysread(len))
end
rescue EOFError
end
else
while buf = file.read(len)
output.write(buf)
end
end
end
}
else
logger.info "Sending file #{path}" unless logger.nil?
File.open(path, 'rb') { |file| render :status => options[:status], :text => file.read }
end
end
# Send binary data to the user as a file download. May set content type, apparent file name,
# and specify whether to show data inline or download as an attachment.
#
# Options:
# * :filename - Suggests a filename for the browser to use.
# * :type - specifies an HTTP content type.
# Defaults to 'application/octet-stream'.
# * :disposition - specifies whether the file will be shown inline or downloaded.
# * :status - specifies the status code to send with the response. Defaults to '200 OK'.
# Valid values are 'inline' and 'attachment' (default).
#
# Generic data download:
# send_data buffer
#
# Download a dynamically-generated tarball:
# send_data generate_tgz('dir'), :filename => 'dir.tgz'
#
# Display an image Active Record in the browser:
# send_data image.data, :type => image.content_type, :disposition => 'inline'
#
# See +send_file+ for more information on HTTP Content-* headers and caching.
def send_data(data, options = {}) #:doc:
logger.info "Sending data #{options[:filename]}" unless logger.nil?
send_file_headers! options.merge(:length => data.size)
@performed_render = false
render :status => options[:status], :text => data
end
private
def send_file_headers!(options)
options.update(DEFAULT_SEND_FILE_OPTIONS.merge(options))
[:length, :type, :disposition].each do |arg|
raise ArgumentError, ":#{arg} option required" if options[arg].nil?
end
disposition = options[:disposition].dup || 'attachment'
disposition <<= %(; filename="#{options[:filename]}") if options[:filename]
@headers.update(
'Content-Length' => options[:length],
'Content-Type' => options[:type].strip, # fixes a problem with extra '\r' with some browsers
'Content-Disposition' => disposition,
'Content-Transfer-Encoding' => 'binary'
)
# Fix a problem with IE 6.0 on opening downloaded files:
# If Cache-Control: no-cache is set (which Rails does by default),
# IE removes the file it just downloaded from its cache immediately
# after it displays the "open/save" dialog, which means that if you
# hit "open" the file isn't there anymore when the application that
# is called for handling the download is run, so let's workaround that
@headers['Cache-Control'] = 'private' if @headers['Cache-Control'] == 'no-cache'
end
end
end
require File.dirname(__FILE__) + '/assertions'
require File.dirname(__FILE__) + '/deprecated_assertions'
module ActionController #:nodoc:
class Base
# Process a test request called with a +TestRequest+ object.
def self.process_test(request)
new.process_test(request)
end
def process_test(request) #:nodoc:
process(request, TestResponse.new)
end
def process_with_test(*args)
returning process_without_test(*args) do
add_variables_to_assigns
end
end
alias_method :process_without_test, :process
alias_method :process, :process_with_test
end
class TestRequest < AbstractRequest #:nodoc:
attr_accessor :cookies, :session_options
attr_accessor :query_parameters, :request_parameters, :path, :session, :env
attr_accessor :host
def initialize(query_parameters = nil, request_parameters = nil, session = nil)
@query_parameters = query_parameters || {}
@request_parameters = request_parameters || {}
@session = session || TestSession.new
initialize_containers
initialize_default_values
super()
end
def reset_session
@session = {}
end
def raw_post
if raw_post = env['RAW_POST_DATA']
raw_post
else
params = self.request_parameters.dup
%w(controller action only_path).each do |k|
params.delete(k)
params.delete(k.to_sym)
end
params.map { |k,v| [ CGI.escape(k.to_s), CGI.escape(v.to_s) ].join('=') }.sort.join('&')
end
end
def port=(number)
@env["SERVER_PORT"] = number.to_i
@port_as_int = nil
end
def action=(action_name)
@query_parameters.update({ "action" => action_name })
@parameters = nil
end
# Used to check AbstractRequest's request_uri functionality.
# Disables the use of @path and @request_uri so superclass can handle those.
def set_REQUEST_URI(value)
@env["REQUEST_URI"] = value
@request_uri = nil
@path = nil
end
def request_uri=(uri)
@request_uri = uri
@path = uri.split("?").first
end
def remote_addr=(addr)
@env['REMOTE_ADDR'] = addr
end
def remote_addr
@env['REMOTE_ADDR']
end
def request_uri
@request_uri || super()
end
def path
@path || super()
end
def assign_parameters(controller_path, action, parameters)
parameters = parameters.symbolize_keys.merge(:controller => controller_path, :action => action)
extra_keys = ActionController::Routing::Routes.extra_keys(parameters)
non_path_parameters = get? ? query_parameters : request_parameters
parameters.each do |key, value|
if value.is_a? Fixnum
value = value.to_s
elsif value.is_a? Array
value = ActionController::Routing::PathComponent::Result.new(value)
end
if extra_keys.include?(key.to_sym)
non_path_parameters[key] = value
else
path_parameters[key.to_s] = value
end
end
end
def recycle!
self.request_parameters = {}
self.query_parameters = {}
self.path_parameters = {}
@request_method, @accepts, @content_type = nil, nil, nil
end
private
def initialize_containers
@env, @cookies = {}, {}
end
def initialize_default_values
@host = "test.host"
@request_uri = "/"
self.remote_addr = "0.0.0.0"
@env["SERVER_PORT"] = 80
@env['REQUEST_METHOD'] = "GET"
end
end
# A refactoring of TestResponse to allow the same behavior to be applied
# to the "real" CgiResponse class in integration tests.
module TestResponseBehavior #:nodoc:
# the response code of the request
def response_code
headers['Status'][0,3].to_i rescue 0
end
# returns a String to ensure compatibility with Net::HTTPResponse
def code
headers['Status'].to_s.split(' ')[0]
end
def message
headers['Status'].to_s.split(' ',2)[1]
end
# was the response successful?
def success?
response_code == 200
end
# was the URL not found?
def missing?
response_code == 404
end
# were we redirected?
def redirect?
(300..399).include?(response_code)
end
# was there a server-side error?
def error?
(500..599).include?(response_code)
end
alias_method :server_error?, :error?
# returns the redirection location or nil
def redirect_url
redirect? ? headers['location'] : nil
end
# does the redirect location match this regexp pattern?
def redirect_url_match?( pattern )
return false if redirect_url.nil?
p = Regexp.new(pattern) if pattern.class == String
p = pattern if pattern.class == Regexp
return false if p.nil?
p.match(redirect_url) != nil
end
# returns the template path of the file which was used to
# render this response (or nil)
def rendered_file(with_controller=false)
unless template.first_render.nil?
unless with_controller
template.first_render
else
template.first_render.split('/').last || template.first_render
end
end
end
# was this template rendered by a file?
def rendered_with_file?
!rendered_file.nil?
end
# a shortcut to the flash (or an empty hash if no flash.. hey! that rhymes!)
def flash
session['flash'] || {}
end
# do we have a flash?
def has_flash?
!session['flash'].empty?
end
# do we have a flash that has contents?
def has_flash_with_contents?
!flash.empty?
end
# does the specified flash object exist?
def has_flash_object?(name=nil)
!flash[name].nil?
end
# does the specified object exist in the session?
def has_session_object?(name=nil)
!session[name].nil?
end
# a shortcut to the template.assigns
def template_objects
template.assigns || {}
end
# does the specified template object exist?
def has_template_object?(name=nil)
!template_objects[name].nil?
end
# Returns the response cookies, converted to a Hash of (name => CGI::Cookie) pairs
# Example:
#
# assert_equal ['AuthorOfNewPage'], r.cookies['author'].value
def cookies
headers['cookie'].inject({}) { |hash, cookie| hash[cookie.name] = cookie; hash }
end
# Returns binary content (downloadable file), converted to a String
def binary_content
raise "Response body is not a Proc: #{body.inspect}" unless body.kind_of?(Proc)
require 'stringio'
sio = StringIO.new
begin
$stdout = sio
body.call
ensure
$stdout = STDOUT
end
sio.rewind
sio.read
end
end
class TestResponse < AbstractResponse #:nodoc:
include TestResponseBehavior
end
class TestSession #:nodoc:
def initialize(attributes = {})
@attributes = attributes
end
def [](key)
@attributes[key]
end
def []=(key, value)
@attributes[key] = value
end
def session_id
""
end
def update() end
def close() end
def delete() @attributes = {} end
end
# Essentially generates a modified Tempfile object similar to the object
# you'd get from the standard library CGI module in a multipart
# request. This means you can use an ActionController::TestUploadedFile
# object in the params of a test request in order to simulate
# a file upload.
#
# Usage example, within a functional test:
# post :change_avatar, :avatar => ActionController::TestUploadedFile.new(Test::Unit::TestCase.fixture_path + '/files/spongebob.png', 'image/png')
class TestUploadedFile
# The filename, *not* including the path, of the "uploaded" file
attr_reader :original_filename
# The content type of the "uploaded" file
attr_reader :content_type
def initialize(path, content_type = 'text/plain')
raise "file does not exist" unless File.exist?(path)
@content_type = content_type
@original_filename = path.sub(/^.*#{File::SEPARATOR}([^#{File::SEPARATOR}]+)$/) { $1 }
@tempfile = Tempfile.new(@original_filename)
FileUtils.copy_file(path, @tempfile.path)
end
def path #:nodoc:
@tempfile.path
end
alias local_path path
def method_missing(method_name, *args, &block) #:nodoc:
@tempfile.send(method_name, *args, &block)
end
end
module TestProcess
def self.included(base)
# execute the request simulating a specific http method and set/volley the response
%w( get post put delete head ).each do |method|
base.class_eval <<-EOV, __FILE__, __LINE__
def #{method}(action, parameters = nil, session = nil, flash = nil)
@request.env['REQUEST_METHOD'] = "#{method.upcase}" if @request
process(action, parameters, session, flash)
end
EOV
end
end
# execute the request and set/volley the response
def process(action, parameters = nil, session = nil, flash = nil)
# Sanity check for required instance variables so we can give an
# understandable error message.
%w(controller request response).each do |iv_name|
raise "@#{iv_name} is nil: make sure you set it in your test's setup method." if instance_variable_get("@#{iv_name}").nil?
end
@request.recycle!
@html_document = nil
@request.env['REQUEST_METHOD'] ||= "GET"
@request.action = action.to_s
parameters ||= {}
@request.assign_parameters(@controller.class.controller_path, action.to_s, parameters)
@request.session = ActionController::TestSession.new(session) unless session.nil?
@request.session["flash"] = ActionController::Flash::FlashHash.new.update(flash) if flash
build_request_uri(action, parameters)
@controller.process(@request, @response)
end
def xml_http_request(request_method, action, parameters = nil, session = nil, flash = nil)
@request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
@request.env['HTTP_ACCEPT'] = 'text/javascript, text/html, application/xml, text/xml, */*'
returning self.send(request_method, action, parameters, session, flash) do
@request.env.delete 'HTTP_X_REQUESTED_WITH'
@request.env.delete 'HTTP_ACCEPT'
end
end
alias xhr :xml_http_request
def follow_redirect
if @response.redirected_to[:controller]
raise "Can't follow redirects outside of current controller (#{@response.redirected_to[:controller]})"
end
get(@response.redirected_to.delete(:action), @response.redirected_to.stringify_keys)
end
def assigns(key = nil)
if key.nil?
@response.template.assigns
else
@response.template.assigns[key.to_s]
end
end
def session
@response.session
end
def flash
@response.flash
end
def cookies
@response.cookies
end
def redirect_to_url
@response.redirect_url
end
def build_request_uri(action, parameters)
unless @request.env['REQUEST_URI']
options = @controller.send(:rewrite_options, parameters)
options.update(:only_path => true, :action => action)
url = ActionController::UrlRewriter.new(@request, parameters)
@request.set_REQUEST_URI(url.rewrite(options))
end
end
def html_document
@html_document ||= HTML::Document.new(@response.body)
end
def find_tag(conditions)
html_document.find(conditions)
end
def find_all_tag(conditions)
html_document.find_all(conditions)
end
def method_missing(selector, *args)
return @controller.send(selector, *args) if ActionController::Routing::NamedRoutes::Helpers.include?(selector)
return super
end
# Shortcut for ActionController::TestUploadedFile.new(Test::Unit::TestCase.fixture_path + path, type). Example:
# post :change_avatar, :avatar => fixture_file_upload('/files/spongebob.png', 'image/png')
def fixture_file_upload(path, mime_type = nil)
ActionController::TestUploadedFile.new(
Test::Unit::TestCase.respond_to?(:fixture_path) ? Test::Unit::TestCase.fixture_path + path : path,
mime_type
)
end
# A helper to make it easier to test different route configurations.
# This method temporarily replaces ActionController::Routing::Routes
# with a new RouteSet instance.
#
# The new instance is yielded to the passed block. Typically the block
# will create some routes using map.draw { map.connect ... }:
#
# with_routing do |set|
# set.draw { set.connect ':controller/:id/:action' }
# assert_equal(
# ['/content/10/show', {}],
# set.generate(:controller => 'content', :id => 10, :action => 'show')
# )
# end
#
def with_routing
real_routes = ActionController::Routing::Routes
ActionController::Routing.send :remove_const, :Routes
temporary_routes = ActionController::Routing::RouteSet.new
ActionController::Routing.send :const_set, :Routes, temporary_routes
yield temporary_routes
ensure
if ActionController::Routing.const_defined? :Routes
ActionController::Routing.send(:remove_const, :Routes)
end
ActionController::Routing.const_set(:Routes, real_routes) if real_routes
end
end
end
module Test
module Unit
class TestCase #:nodoc:
include ActionController::TestProcess
end
end
end
module ActionController
# Rewrites URLs for Base.redirect_to and Base.url_for in the controller.
class UrlRewriter #:nodoc:
RESERVED_OPTIONS = [:anchor, :params, :only_path, :host, :protocol, :trailing_slash, :skip_relative_url_root]
def initialize(request, parameters)
@request, @parameters = request, parameters
end
def rewrite(options = {})
rewrite_url(rewrite_path(options), options)
end
def to_str
"#{@request.protocol}, #{@request.host_with_port}, #{@request.path}, #{@parameters[:controller]}, #{@parameters[:action]}, #{@request.parameters.inspect}"
end
alias_method :to_s, :to_str
private
def rewrite_url(path, options)
rewritten_url = ""
unless options[:only_path]
rewritten_url << (options[:protocol] || @request.protocol)
rewritten_url << (options[:host] || @request.host_with_port)
end
rewritten_url << @request.relative_url_root.to_s unless options[:skip_relative_url_root]
rewritten_url << path
rewritten_url << '/' if options[:trailing_slash]
rewritten_url << "##{options[:anchor]}" if options[:anchor]
rewritten_url
end
def rewrite_path(options)
options = options.symbolize_keys
options.update(options[:params].symbolize_keys) if options[:params]
if (overwrite = options.delete(:overwrite_params))
options.update(@parameters.symbolize_keys)
options.update(overwrite)
end
RESERVED_OPTIONS.each {|k| options.delete k}
path, extra_keys = Routing::Routes.generate(options.dup, @request) # Warning: Routes will mutate and violate the options hash
path << build_query_string(options, extra_keys) unless extra_keys.empty?
path
end
# Returns a query string with escaped keys and values from the passed hash. If the passed hash contains an "id" it'll
# be added as a path element instead of a regular parameter pair.
def build_query_string(hash, only_keys = nil)
elements = []
query_string = ""
only_keys ||= hash.keys
only_keys.each do |key|
value = hash[key]
key = CGI.escape key.to_s
if value.class == Array
key << '[]'
else
value = [ value ]
end
value.each { |val| elements << "#{key}=#{Routing.extract_parameter_value(val)}" }
end
query_string << ("?" + elements.join("&")) unless elements.empty?
query_string
end
end
end
require File.dirname(__FILE__) + '/tokenizer'
require File.dirname(__FILE__) + '/node'
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)
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
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 = < 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 |k,v2|
case k
when :count, :greater_than, :less_than
# keys are valid, and require no further processing
when :only
v[k] = Conditions.new(v2)
else
raise "illegal key #{k.inspect} => #{v2.inspect}"
end
end
else
raise "illegal key #{k.inspect} => #{v.inspect}"
end
end
update hash
end
private
def keys_to_strings(hash)
hash.keys.inject({}) do |h,k|
h[k.to_s] = hash[k]
h
end
end
def keys_to_symbols(hash)
hash.keys.inject({}) do |h,k|
raise "illegal key #{k.inspect}" unless k.respond_to?(:to_sym)
h[k.to_sym] = hash[k]
h
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
s = ""
@children.each { |child| s << child.to_s }
s
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 </)
return CDATA.new(parent, line, pos, scanner.pre_match)
end
closing = ( scanner.scan(/\//) ? :close : nil )
return Text.new(parent, line, pos, content) unless name = scanner.scan(/[\w:]+/)
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
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 :content 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.index(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
""
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+, :close, or :self
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.gsub(/'/,"\\\\'")}'" 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):
#
# * :tag: the node name must match the corresponding value
# * :attributes: a hash. The node's values must match the
# corresponding values in the hash.
# * :parent: a hash. The node's parent must match the
# corresponding hash.
# * :child: a hash. At least one of the node's immediate children
# must meet the criteria described by the hash.
# * :ancestor: a hash. At least one of the node's ancestors must
# meet the criteria described by the hash.
# * :descendant: a hash. At least one of the node's descendants
# must meet the criteria described by the hash.
# * :sibling: a hash. At least one of the node's siblings must
# meet the criteria described by the hash.
# * :after: a hash. The node must be after any sibling meeting
# the criteria described by the hash, and at least one sibling must match.
# * :before: a hash. The node must be before any sibling meeting
# the criteria described by the hash, and at least one sibling must match.
# * :children: a hash, for counting children of a node. Accepts the
# keys:
# ** :count: either a number or a range which must equal (or
# include) the number of children that match.
# ** :less_than: the number of matching children must be less than
# this number.
# ** :greater_than: the number of matching children must be
# greater than this number.
# ** :only: 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.match(/./) or
(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
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)
@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(/\]\]>/)
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
text << @scanner.getch # skip the escaped character
end
end
text
end
end
end
module HTML #:nodoc:
module Version #:nodoc:
MAJOR = 0
MINOR = 5
TINY = 3
STRING = [ MAJOR, MINOR, TINY ].join(".")
end
end
require 'rexml/document'
# SimpleXML like xml parser. Written by leon breet from the ruby on rails Mailing list
class XmlNode #:nodoc:
attr :node
def initialize(node, options = {})
@node = node
@children = {}
@raise_errors = options[:raise_errors]
end
def self.from_xml(xml_or_io)
document = REXML::Document.new(xml_or_io)
if document.root
XmlNode.new(document.root)
else
XmlNode.new(document)
end
end
def node_encoding
@node.encoding
end
def node_name
@node.name
end
def node_value
@node.text
end
def node_value=(value)
@node.text = value
end
def xpath(expr)
matches = nil
REXML::XPath.each(@node, expr) do |element|
matches ||= XmlNodeList.new
matches << (@children[element] ||= XmlNode.new(element))
end
matches
end
def method_missing(name, *args)
name = name.to_s
nodes = nil
@node.each_element(name) do |element|
nodes ||= XmlNodeList.new
nodes << (@children[element] ||= XmlNode.new(element))
end
nodes
end
def <<(node)
if node.is_a? REXML::Node
child = node
elsif node.respond_to? :node
child = node.node
end
@node.add_element child
@children[child] ||= XmlNode.new(child)
end
def [](name)
@node.attributes[name.to_s]
end
def []=(name, value)
@node.attributes[name.to_s] = value
end
def to_s
@node.to_s
end
def to_i
to_s.to_i
end
end
class XmlNodeList < Array #:nodoc:
def [](i)
i.is_a?(String) ? super(0)[i] : super(i)
end
def []=(i, value)
i.is_a?(String) ? self[0][i] = value : super(i, value)
end
def method_missing(name, *args)
name = name.to_s
self[0].__send__(name, *args)
end
end# = XmlSimple
#
# Author:: Maik Schmidt
# Copyright:: Copyright (c) 2003 Maik Schmidt
# License:: Distributes under the same terms as Ruby.
#
require 'rexml/document'
# Easy API to maintain XML (especially configuration files).
class XmlSimple #:nodoc:
include REXML
@@VERSION = '1.0.2'
# A simple cache for XML documents that were already transformed
# by xml_in.
class Cache #:nodoc:
# Creates and initializes a new Cache object.
def initialize
@mem_share_cache = {}
@mem_copy_cache = {}
end
# Saves a data structure into a file.
#
# data::
# Data structure to be saved.
# filename::
# Name of the file belonging to the data structure.
def save_storable(data, filename)
cache_file = get_cache_filename(filename)
File.open(cache_file, "w+") { |f| Marshal.dump(data, f) }
end
# Restores a data structure from a file. If restoring the data
# structure failed for any reason, nil will be returned.
#
# filename::
# Name of the file belonging to the data structure.
def restore_storable(filename)
cache_file = get_cache_filename(filename)
return nil unless File::exist?(cache_file)
return nil unless File::mtime(cache_file).to_i > File::mtime(filename).to_i
data = nil
File.open(cache_file) { |f| data = Marshal.load(f) }
data
end
# Saves a data structure in a shared memory cache.
#
# data::
# Data structure to be saved.
# filename::
# Name of the file belonging to the data structure.
def save_mem_share(data, filename)
@mem_share_cache[filename] = [Time::now.to_i, data]
end
# Restores a data structure from a shared memory cache. You
# should consider these elements as "read only". If restoring
# the data structure failed for any reason, nil will be
# returned.
#
# filename::
# Name of the file belonging to the data structure.
def restore_mem_share(filename)
get_from_memory_cache(filename, @mem_share_cache)
end
# Copies a data structure to a memory cache.
#
# data::
# Data structure to be copied.
# filename::
# Name of the file belonging to the data structure.
def save_mem_copy(data, filename)
@mem_share_cache[filename] = [Time::now.to_i, Marshal.dump(data)]
end
# Restores a data structure from a memory cache. If restoring
# the data structure failed for any reason, nil will be
# returned.
#
# filename::
# Name of the file belonging to the data structure.
def restore_mem_copy(filename)
data = get_from_memory_cache(filename, @mem_share_cache)
data = Marshal.load(data) unless data.nil?
data
end
private
# Returns the "cache filename" belonging to a filename, i.e.
# the extension '.xml' in the original filename will be replaced
# by '.stor'. If filename does not have this extension, '.stor'
# will be appended.
#
# filename::
# Filename to get "cache filename" for.
def get_cache_filename(filename)
filename.sub(/(\.xml)?$/, '.stor')
end
# Returns a cache entry from a memory cache belonging to a
# certain filename. If no entry could be found for any reason,
# nil will be returned.
#
# filename::
# Name of the file the cache entry belongs to.
# cache::
# Memory cache to get entry from.
def get_from_memory_cache(filename, cache)
return nil unless cache[filename]
return nil unless cache[filename][0] > File::mtime(filename).to_i
return cache[filename][1]
end
end
# Create a "global" cache.
@@cache = Cache.new
# Creates and intializes a new XmlSimple object.
#
# defaults::
# Default values for options.
def initialize(defaults = nil)
unless defaults.nil? || defaults.instance_of?(Hash)
raise ArgumentError, "Options have to be a Hash."
end
@default_options = normalize_option_names(defaults, KNOWN_OPTIONS['in'] & KNOWN_OPTIONS['out'])
@options = Hash.new
@_var_values = nil
end
# Converts an XML document in the same way as the Perl module XML::Simple.
#
# string::
# XML source. Could be one of the following:
#
# - nil: Tries to load and parse '.xml'.
# - filename: Tries to load and parse filename.
# - IO object: Reads from object until EOF is detected and parses result.
# - XML string: Parses string.
#
# options::
# Options to be used.
def xml_in(string = nil, options = nil)
handle_options('in', options)
# If no XML string or filename was supplied look for scriptname.xml.
if string.nil?
string = File::basename($0)
string.sub!(/\.[^.]+$/, '')
string += '.xml'
directory = File::dirname($0)
@options['searchpath'].unshift(directory) unless directory.nil?
end
if string.instance_of?(String)
if string =~ /<.*?>/m
@doc = parse(string)
elsif string == '-'
@doc = parse($stdin.readlines.to_s)
else
filename = find_xml_file(string, @options['searchpath'])
if @options.has_key?('cache')
@options['cache'].each { |scheme|
case(scheme)
when 'storable'
content = @@cache.restore_storable(filename)
when 'mem_share'
content = @@cache.restore_mem_share(filename)
when 'mem_copy'
content = @@cache.restore_mem_copy(filename)
else
raise ArgumentError, "Unsupported caching scheme: <#{scheme}>."
end
return content if content
}
end
@doc = load_xml_file(filename)
end
elsif string.kind_of?(IO)
@doc = parse(string.readlines.to_s)
else
raise ArgumentError, "Could not parse object of type: <#{string.type}>."
end
result = collapse(@doc.root)
result = @options['keeproot'] ? merge({}, @doc.root.name, result) : result
put_into_cache(result, filename)
result
end
# This is the functional version of the instance method xml_in.
def XmlSimple.xml_in(string = nil, options = nil)
xml_simple = XmlSimple.new
xml_simple.xml_in(string, options)
end
# Converts a data structure into an XML document.
#
# ref::
# Reference to data structure to be converted into XML.
# options::
# Options to be used.
def xml_out(ref, options = nil)
handle_options('out', options)
if ref.instance_of?(Array)
ref = { @options['anonymoustag'] => ref }
end
if @options['keeproot']
keys = ref.keys
if keys.size == 1
ref = ref[keys[0]]
@options['rootname'] = keys[0]
end
elsif @options['rootname'] == ''
if ref.instance_of?(Hash)
refsave = ref
ref = {}
refsave.each { |key, value|
if !scalar(value)
ref[key] = value
else
ref[key] = [ value.to_s ]
end
}
end
end
@ancestors = []
xml = value_to_xml(ref, @options['rootname'], '')
@ancestors = nil
if @options['xmldeclaration']
xml = @options['xmldeclaration'] + "\n" + xml
end
if @options.has_key?('outputfile')
if @options['outputfile'].kind_of?(IO)
return @options['outputfile'].write(xml)
else
File.open(@options['outputfile'], "w") { |file| file.write(xml) }
end
end
xml
end
# This is the functional version of the instance method xml_out.
def XmlSimple.xml_out(hash, options = nil)
xml_simple = XmlSimple.new
xml_simple.xml_out(hash, options)
end
private
# Declare options that are valid for xml_in and xml_out.
KNOWN_OPTIONS = {
'in' => %w(
keyattr keeproot forcecontent contentkey noattr
searchpath forcearray suppressempty anonymoustag
cache grouptags normalisespace normalizespace
variables varattr
),
'out' => %w(
keyattr keeproot contentkey noattr rootname
xmldeclaration outputfile noescape suppressempty
anonymoustag indent grouptags noindent
)
}
# Define some reasonable defaults.
DEF_KEY_ATTRIBUTES = []
DEF_ROOT_NAME = 'opt'
DEF_CONTENT_KEY = 'content'
DEF_XML_DECLARATION = ""
DEF_ANONYMOUS_TAG = 'anon'
DEF_FORCE_ARRAY = true
DEF_INDENTATION = ' '
# Normalizes option names in a hash, i.e., turns all
# characters to lower case and removes all underscores.
# Additionally, this method checks, if an unknown option
# was used and raises an according exception.
#
# options::
# Hash to be normalized.
# known_options::
# List of known options.
def normalize_option_names(options, known_options)
return nil if options.nil?
result = Hash.new
options.each { |key, value|
lkey = key.downcase
lkey.gsub!(/_/, '')
if !known_options.member?(lkey)
raise ArgumentError, "Unrecognised option: #{lkey}."
end
result[lkey] = value
}
result
end
# Merges a set of options with the default options.
#
# direction::
# 'in': If options should be handled for xml_in.
# 'out': If options should be handled for xml_out.
# options::
# Options to be merged with the default options.
def handle_options(direction, options)
@options = options || Hash.new
raise ArgumentError, "Options must be a Hash!" unless @options.instance_of?(Hash)
unless KNOWN_OPTIONS.has_key?(direction)
raise ArgumentError, "Unknown direction: <#{direction}>."
end
known_options = KNOWN_OPTIONS[direction]
@options = normalize_option_names(@options, known_options)
unless @default_options.nil?
known_options.each { |option|
unless @options.has_key?(option)
if @default_options.has_key?(option)
@options[option] = @default_options[option]
end
end
}
end
unless @options.has_key?('noattr')
@options['noattr'] = false
end
if @options.has_key?('rootname')
@options['rootname'] = '' if @options['rootname'].nil?
else
@options['rootname'] = DEF_ROOT_NAME
end
if @options.has_key?('xmldeclaration') && @options['xmldeclaration'] == true
@options['xmldeclaration'] = DEF_XML_DECLARATION
end
if @options.has_key?('contentkey')
if @options['contentkey'] =~ /^-(.*)$/
@options['contentkey'] = $1
@options['collapseagain'] = true
end
else
@options['contentkey'] = DEF_CONTENT_KEY
end
unless @options.has_key?('normalisespace')
@options['normalisespace'] = @options['normalizespace']
end
@options['normalisespace'] = 0 if @options['normalisespace'].nil?
if @options.has_key?('searchpath')
unless @options['searchpath'].instance_of?(Array)
@options['searchpath'] = [ @options['searchpath'] ]
end
else
@options['searchpath'] = []
end
if @options.has_key?('cache') && scalar(@options['cache'])
@options['cache'] = [ @options['cache'] ]
end
@options['anonymoustag'] = DEF_ANONYMOUS_TAG unless @options.has_key?('anonymoustag')
if !@options.has_key?('indent') || @options['indent'].nil?
@options['indent'] = DEF_INDENTATION
end
@options['indent'] = '' if @options.has_key?('noindent')
# Special cleanup for 'keyattr' which could be an array or
# a hash or left to default to array.
if @options.has_key?('keyattr')
if !scalar(@options['keyattr'])
# Convert keyattr => { elem => '+attr' }
# to keyattr => { elem => ['attr', '+'] }
if @options['keyattr'].instance_of?(Hash)
@options['keyattr'].each { |key, value|
if value =~ /^([-+])?(.*)$/
@options['keyattr'][key] = [$2, $1 ? $1 : '']
end
}
elsif !@options['keyattr'].instance_of?(Array)
raise ArgumentError, "'keyattr' must be String, Hash, or Array!"
end
else
@options['keyattr'] = [ @options['keyattr'] ]
end
else
@options['keyattr'] = DEF_KEY_ATTRIBUTES
end
if @options.has_key?('forcearray')
if @options['forcearray'].instance_of?(Regexp)
@options['forcearray'] = [ @options['forcearray'] ]
end
if @options['forcearray'].instance_of?(Array)
force_list = @options['forcearray']
unless force_list.empty?
@options['forcearray'] = {}
force_list.each { |tag|
if tag.instance_of?(Regexp)
unless @options['forcearray']['_regex'].instance_of?(Array)
@options['forcearray']['_regex'] = []
end
@options['forcearray']['_regex'] << tag
else
@options['forcearray'][tag] = true
end
}
else
@options['forcearray'] = false
end
else
@options['forcearray'] = @options['forcearray'] ? true : false
end
else
@options['forcearray'] = DEF_FORCE_ARRAY
end
if @options.has_key?('grouptags') && !@options['grouptags'].instance_of?(Hash)
raise ArgumentError, "Illegal value for 'GroupTags' option - expected a Hash."
end
if @options.has_key?('variables') && !@options['variables'].instance_of?(Hash)
raise ArgumentError, "Illegal value for 'Variables' option - expected a Hash."
end
if @options.has_key?('variables')
@_var_values = @options['variables']
elsif @options.has_key?('varattr')
@_var_values = {}
end
end
# Actually converts an XML document element into a data structure.
#
# element::
# The document element to be collapsed.
def collapse(element)
result = @options['noattr'] ? {} : get_attributes(element)
if @options['normalisespace'] == 2
result.each { |k, v| result[k] = normalise_space(v) }
end
if element.has_elements?
element.each_element { |child|
value = collapse(child)
if empty(value) && (element.attributes.empty? || @options['noattr'])
next if @options.has_key?('suppressempty') && @options['suppressempty'] == true
end
result = merge(result, child.name, value)
}
if has_mixed_content?(element)
# normalisespace?
content = element.texts.map { |x| x.to_s }
content = content[0] if content.size == 1
result[@options['contentkey']] = content
end
elsif element.has_text? # i.e. it has only text.
return collapse_text_node(result, element)
end
# Turn Arrays into Hashes if key fields present.
count = fold_arrays(result)
# Disintermediate grouped tags.
if @options.has_key?('grouptags')
result.each { |key, value|
next unless (value.instance_of?(Hash) && (value.size == 1))
child_key, child_value = value.to_a[0]
if @options['grouptags'][key] == child_key
result[key] = child_value
end
}
end
# Fold Hases containing a single anonymous Array up into just the Array.
if count == 1
anonymoustag = @options['anonymoustag']
if result.has_key?(anonymoustag) && result[anonymoustag].instance_of?(Array)
return result[anonymoustag]
end
end
if result.empty? && @options.has_key?('suppressempty')
return @options['suppressempty'] == '' ? '' : nil
end
result
end
# Collapses a text node and merges it with an existing Hash, if
# possible.
# Thanks to Curtis Schofield for reporting a subtle bug.
#
# hash::
# Hash to merge text node value with, if possible.
# element::
# Text node to be collapsed.
def collapse_text_node(hash, element)
value = node_to_text(element)
if empty(value) && !element.has_attributes?
return {}
end
if element.has_attributes? && !@options['noattr']
return merge(hash, @options['contentkey'], value)
else
if @options['forcecontent']
return merge(hash, @options['contentkey'], value)
else
return value
end
end
end
# Folds all arrays in a Hash.
#
# hash::
# Hash to be folded.
def fold_arrays(hash)
fold_amount = 0
keyattr = @options['keyattr']
if (keyattr.instance_of?(Array) || keyattr.instance_of?(Hash))
hash.each { |key, value|
if value.instance_of?(Array)
if keyattr.instance_of?(Array)
hash[key] = fold_array(value)
else
hash[key] = fold_array_by_name(key, value)
end
fold_amount += 1
end
}
end
fold_amount
end
# Folds an Array to a Hash, if possible. Folding happens
# according to the content of keyattr, which has to be
# an array.
#
# array::
# Array to be folded.
def fold_array(array)
hash = Hash.new
array.each { |x|
return array unless x.instance_of?(Hash)
key_matched = false
@options['keyattr'].each { |key|
if x.has_key?(key)
key_matched = true
value = x[key]
return array if value.instance_of?(Hash) || value.instance_of?(Array)
value = normalise_space(value) if @options['normalisespace'] == 1
x.delete(key)
hash[value] = x
break
end
}
return array unless key_matched
}
hash = collapse_content(hash) if @options['collapseagain']
hash
end
# Folds an Array to a Hash, if possible. Folding happens
# according to the content of keyattr, which has to be
# a Hash.
#
# name::
# Name of the attribute to be folded upon.
# array::
# Array to be folded.
def fold_array_by_name(name, array)
return array unless @options['keyattr'].has_key?(name)
key, flag = @options['keyattr'][name]
hash = Hash.new
array.each { |x|
if x.instance_of?(Hash) && x.has_key?(key)
value = x[key]
return array if value.instance_of?(Hash) || value.instance_of?(Array)
value = normalise_space(value) if @options['normalisespace'] == 1
hash[value] = x
hash[value]["-#{key}"] = hash[value][key] if flag == '-'
hash[value].delete(key) unless flag == '+'
else
$stderr.puts("Warning: <#{name}> element has no '#{key}' attribute.")
return array
end
}
hash = collapse_content(hash) if @options['collapseagain']
hash
end
# Tries to collapse a Hash even more ;-)
#
# hash::
# Hash to be collapsed again.
def collapse_content(hash)
content_key = @options['contentkey']
hash.each_value { |value|
return hash unless value.instance_of?(Hash) && value.size == 1 && value.has_key?(content_key)
hash.each_key { |key| hash[key] = hash[key][content_key] }
}
hash
end
# Adds a new key/value pair to an existing Hash. If the key to be added
# does already exist and the existing value associated with key is not
# an Array, it will be converted into an Array. Then the new value is
# appended to that Array.
#
# hash::
# Hash to add key/value pair to.
# key::
# Key to be added.
# value::
# Value to be associated with key.
def merge(hash, key, value)
if value.instance_of?(String)
value = normalise_space(value) if @options['normalisespace'] == 2
# do variable substitutions
unless @_var_values.nil? || @_var_values.empty?
value.gsub!(/\$\{(\w+)\}/) { |x| get_var($1) }
end
# look for variable definitions
if @options.has_key?('varattr')
varattr = @options['varattr']
if hash.has_key?(varattr)
set_var(hash[varattr], value)
end
end
end
if hash.has_key?(key)
if hash[key].instance_of?(Array)
hash[key] << value
else
hash[key] = [ hash[key], value ]
end
elsif value.instance_of?(Array) # Handle anonymous arrays.
hash[key] = [ value ]
else
if force_array?(key)
hash[key] = [ value ]
else
hash[key] = value
end
end
hash
end
# Checks, if the 'forcearray' option has to be used for
# a certain key.
def force_array?(key)
return false if key == @options['contentkey']
return true if @options['forcearray'] == true
forcearray = @options['forcearray']
if forcearray.instance_of?(Hash)
return true if forcearray.has_key?(key)
return false unless forcearray.has_key?('_regex')
forcearray['_regex'].each { |x| return true if key =~ x }
end
return false
end
# Converts the attributes array of a document node into a Hash.
# Returns an empty Hash, if node has no attributes.
#
# node::
# Document node to extract attributes from.
def get_attributes(node)
attributes = {}
node.attributes.each { |n,v| attributes[n] = v }
attributes
end
# Determines, if a document element has mixed content.
#
# element::
# Document element to be checked.
def has_mixed_content?(element)
if element.has_text? && element.has_elements?
return true if element.texts.join('') !~ /^\s*$/s
end
false
end
# Called when a variable definition is encountered in the XML.
# A variable definition looks like
# value
# where attrname matches the varattr setting.
def set_var(name, value)
@_var_values[name] = value
end
# Called during variable substitution to get the value for the
# named variable.
def get_var(name)
if @_var_values.has_key?(name)
return @_var_values[name]
else
return "${#{name}}"
end
end
# Recurses through a data structure building up and returning an
# XML representation of that structure as a string.
#
# ref::
# Reference to the data structure to be encoded.
# name::
# The XML tag name to be used for this item.
# indent::
# A string of spaces for use as the current indent level.
def value_to_xml(ref, name, indent)
named = !name.nil? && name != ''
nl = @options.has_key?('noindent') ? '' : "\n"
if !scalar(ref)
if @ancestors.member?(ref)
raise ArgumentError, "Circular data structures not supported!"
end
@ancestors << ref
else
if named
return [indent, '<', name, '>', @options['noescape'] ? ref.to_s : escape_value(ref.to_s), '', name, '>', nl].join('')
else
return ref.to_s + nl
end
end
# Unfold hash to array if possible.
if ref.instance_of?(Hash) && !ref.empty? && !@options['keyattr'].empty? && indent != ''
ref = hash_to_array(name, ref)
end
result = []
if ref.instance_of?(Hash)
# Reintermediate grouped values if applicable.
if @options.has_key?('grouptags')
ref.each { |key, value|
if @options['grouptags'].has_key?(key)
ref[key] = { @options['grouptags'][key] => value }
end
}
end
nested = []
text_content = nil
if named
result << indent << '<' << name
end
if !ref.empty?
ref.each { |key, value|
next if !key.nil? && key[0, 1] == '-'
if value.nil?
unless @options.has_key?('suppressempty') && @options['suppressempty'].nil?
raise ArgumentError, "Use of uninitialized value!"
end
value = {}
end
if !scalar(value) || @options['noattr']
nested << value_to_xml(value, key, indent + @options['indent'])
else
value = value.to_s
value = escape_value(value) unless @options['noescape']
if key == @options['contentkey']
text_content = value
else
result << ' ' << key << '="' << value << '"'
end
end
}
else
text_content = ''
end
if !nested.empty? || !text_content.nil?
if named
result << '>'
if !text_content.nil?
result << text_content
nested[0].sub!(/^\s+/, '') if !nested.empty?
else
result << nl
end
if !nested.empty?
result << nested << indent
end
result << '' << name << '>' << nl
else
result << nested
end
else
result << ' />' << nl
end
elsif ref.instance_of?(Array)
ref.each { |value|
if scalar(value)
result << indent << '<' << name << '>'
result << (@options['noescape'] ? value.to_s : escape_value(value.to_s))
result << '' << name << '>' << nl
elsif value.instance_of?(Hash)
result << value_to_xml(value, name, indent)
else
result << indent << '<' << name << '>' << nl
result << value_to_xml(value, @options['anonymoustag'], indent + @options['indent'])
result << indent << '' << name << '>' << nl
end
}
else
# Probably, this is obsolete.
raise ArgumentError, "Can't encode a value of type: #{ref.type}."
end
@ancestors.pop if !scalar(ref)
result.join('')
end
# Checks, if a certain value is a "scalar" value. Whatever
# that will be in Ruby ... ;-)
#
# value::
# Value to be checked.
def scalar(value)
return false if value.instance_of?(Hash) || value.instance_of?(Array)
return true
end
# Attempts to unfold a hash of hashes into an array of hashes. Returns
# a reference to th array on success or the original hash, if unfolding
# is not possible.
#
# parent::
#
# hashref::
# Reference to the hash to be unfolded.
def hash_to_array(parent, hashref)
arrayref = []
hashref.each { |key, value|
return hashref unless value.instance_of?(Hash)
if @options['keyattr'].instance_of?(Hash)
return hashref unless @options['keyattr'].has_key?(parent)
arrayref << { @options['keyattr'][parent][0] => key }.update(value)
else
arrayref << { @options['keyattr'][0] => key }.update(value)
end
}
arrayref
end
# Replaces XML markup characters by their external entities.
#
# data::
# The string to be escaped.
def escape_value(data)
return data if data.nil? || data == ''
result = data.dup
result.gsub!('&', '&')
result.gsub!('<', '<')
result.gsub!('>', '>')
result.gsub!('"', '"')
result.gsub!("'", ''')
result
end
# Removes leading and trailing whitespace and sequences of
# whitespaces from a string.
#
# text::
# String to be normalised.
def normalise_space(text)
text.sub!(/^\s+/, '')
text.sub!(/\s+$/, '')
text.gsub!(/\s\s+/, ' ')
text
end
# Checks, if an object is nil, an empty String or an empty Hash.
# Thanks to Norbert Gawor for a bugfix.
#
# value::
# Value to be checked for emptyness.
def empty(value)
case value
when Hash
return value.empty?
when String
return value !~ /\S/m
else
return value.nil?
end
end
# Converts a document node into a String.
# If the node could not be converted into a String
# for any reason, default will be returned.
#
# node::
# Document node to be converted.
# default::
# Value to be returned, if node could not be converted.
def node_to_text(node, default = nil)
if node.instance_of?(Element)
return node.texts.join('')
elsif node.instance_of?(Attribute)
return node.value.nil? ? default : node.value.strip
elsif node.instance_of?(Text)
return node.to_s.strip
else
return default
end
end
# Parses an XML string and returns the according document.
#
# xml_string::
# XML string to be parsed.
#
# The following exception may be raised:
#
# REXML::ParseException::
# If the specified file is not wellformed.
def parse(xml_string)
Document.new(xml_string)
end
# Searches in a list of paths for a certain file. Returns
# the full path to the file, if it could be found. Otherwise,
# an exception will be raised.
#
# filename::
# Name of the file to search for.
# searchpath::
# List of paths to search in.
def find_xml_file(file, searchpath)
filename = File::basename(file)
if filename != file
return file if File::file?(file)
else
searchpath.each { |path|
full_path = File::join(path, filename)
return full_path if File::file?(full_path)
}
end
if searchpath.empty?
return file if File::file?(file)
raise ArgumentError, "File does not exist: #{file}."
end
raise ArgumentError, "Could not find <#{filename}> in <#{searchpath.join(':')}>"
end
# Loads and parses an XML configuration file.
#
# filename::
# Name of the configuration file to be loaded.
#
# The following exceptions may be raised:
#
# Errno::ENOENT::
# If the specified file does not exist.
# REXML::ParseException::
# If the specified file is not wellformed.
def load_xml_file(filename)
parse(File.readlines(filename).to_s)
end
# Caches the data belonging to a certain file.
#
# data::
# Data to be cached.
# filename::
# Name of file the data was read from.
def put_into_cache(data, filename)
if @options.has_key?('cache')
@options['cache'].each { |scheme|
case(scheme)
when 'storable'
@@cache.save_storable(data, filename)
when 'mem_share'
@@cache.save_mem_share(data, filename)
when 'mem_copy'
@@cache.save_mem_copy(data, filename)
else
raise ArgumentError, "Unsupported caching scheme: <#{scheme}>."
end
}
end
end
end
# vim:sw=2
module ActionController #:nodoc:
module Verification #:nodoc:
def self.append_features(base) #:nodoc:
super
base.extend(ClassMethods)
end
# This module provides a class-level method for specifying that certain
# actions are guarded against being called without certain prerequisites
# being met. This is essentially a special kind of before_filter.
#
# An action may be guarded against being invoked without certain request
# parameters being set, or without certain session values existing.
#
# When a verification is violated, values may be inserted into the flash, and
# a specified redirection is triggered.
#
# Usage:
#
# class GlobalController < ActionController::Base
# # prevent the #update_settings action from being invoked unless
# # the 'admin_privileges' request parameter exists.
# verify :params => "admin_privileges", :only => :update_post,
# :redirect_to => { :action => "settings" }
#
# # disallow a post from being updated if there was no information
# # submitted with the post, and if there is no active post in the
# # session, and if there is no "note" key in the flash.
# verify :params => "post", :session => "post", "flash" => "note",
# :only => :update_post,
# :add_flash => { "alert" => "Failed to create your message" },
# :redirect_to => :category_url
#
module ClassMethods
# Verify the given actions so that if certain prerequisites are not met,
# the user is redirected to a different action. The +options+ parameter
# is a hash consisting of the following key/value pairs:
#
# * :params: a single key or an array of keys that must
# be in the params hash in order for the action(s) to be safely
# called.
# * :session: a single key or an array of keys that must
# be in the @session in order for the action(s) to be safely called.
# * :flash: a single key or an array of keys that must
# be in the flash in order for the action(s) to be safely called.
# * :method: a single key or an array of keys--any one of which
# must match the current request method in order for the action(s) to
# be safely called. (The key should be a symbol: :get or
# :post, for example.)
# * :xhr: true/false option to ensure that the request is coming
# from an Ajax call or not.
# * :add_flash: a hash of name/value pairs that should be merged
# into the session's flash if the prerequisites cannot be satisfied.
# * :redirect_to: the redirection parameters to be used when
# redirecting if the prerequisites cannot be satisfied.
# * :render: the render parameters to be used when
# the prerequisites cannot be satisfied.
# * :only: only apply this verification to the actions specified
# in the associated array (may also be a single value).
# * :except: do not apply this verification to the actions
# specified in the associated array (may also be a single value).
def verify(options={})
filter_opts = { :only => options[:only], :except => options[:except] }
before_filter(filter_opts) do |c|
c.send :verify_action, options
end
end
end
def verify_action(options) #:nodoc:
prereqs_invalid =
[*options[:params] ].find { |v| @params[v].nil? } ||
[*options[:session]].find { |v| @session[v].nil? } ||
[*options[:flash] ].find { |v| flash[v].nil? }
if !prereqs_invalid && options[:method]
prereqs_invalid ||=
[*options[:method]].all? { |v| @request.method != v.to_sym }
end
prereqs_invalid ||= (request.xhr? != options[:xhr]) unless options[:xhr].nil?
if prereqs_invalid
flash.update(options[:add_flash]) if options[:add_flash]
unless performed?
render(options[:render]) if options[:render]
redirect_to(options[:redirect_to]) if options[:redirect_to]
end
return false
end
true
end
private :verify_action
end
end
#--
# Copyright (c) 2004 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.
#++
$:.unshift(File.dirname(__FILE__)) unless
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
unless defined?(ActiveSupport)
begin
$:.unshift(File.dirname(__FILE__) + "/../../activesupport/lib")
require 'active_support'
rescue LoadError
require 'rubygems'
require_gem 'activesupport'
end
end
require 'action_controller/base'
require 'action_controller/deprecated_redirects'
require 'action_controller/request'
require 'action_controller/deprecated_request_methods'
require 'action_controller/rescue'
require 'action_controller/benchmarking'
require 'action_controller/flash'
require 'action_controller/filters'
require 'action_controller/layout'
require 'action_controller/dependencies'
require 'action_controller/mime_responds'
require 'action_controller/pagination'
require 'action_controller/scaffolding'
require 'action_controller/helpers'
require 'action_controller/cookies'
require 'action_controller/cgi_process'
require 'action_controller/caching'
require 'action_controller/verification'
require 'action_controller/streaming'
require 'action_controller/session_management'
require 'action_controller/components'
require 'action_controller/macros/auto_complete'
require 'action_controller/macros/in_place_editing'
require 'action_view'
ActionController::Base.template_class = ActionView::Base
ActionController::Base.class_eval do
include ActionController::Flash
include ActionController::Filters
include ActionController::Layout
include ActionController::Benchmarking
include ActionController::Rescue
include ActionController::Dependencies
include ActionController::MimeResponds
include ActionController::Pagination
include ActionController::Scaffolding
include ActionController::Helpers
include ActionController::Cookies
include ActionController::Caching
include ActionController::Verification
include ActionController::Streaming
include ActionController::SessionManagement
include ActionController::Components
include ActionController::Macros::AutoComplete
include ActionController::Macros::InPlaceEditing
end
module ActionPack #:nodoc:
module VERSION #:nodoc:
MAJOR = 1
MINOR = 12
TINY = 5
STRING = [MAJOR, MINOR, TINY].join('.')
end
end
#--
# Copyright (c) 2004 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 'action_pack/version'
require 'erb'
module ActionView #:nodoc:
class ActionViewError < StandardError #:nodoc:
end
# Action View templates can be written in three ways. If the template file has a +.rhtml+ extension then it uses a mixture of ERb
# (included in Ruby) and HTML. If the template file has a +.rxml+ extension then Jim Weirich's Builder::XmlMarkup library is used.
# If the template file has a +.rjs+ extension then it will use ActionView::Helpers::PrototypeHelper::JavaScriptGenerator.
#
# = 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:
#
# Names of all the people
# <% for person in @people %>
# Name: <%= person.name %>
# <% 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:
#
# Hi, Mr. <% puts "Frodo" %>
#
# If you absolutely must write from within a function, you can use the TextHelper#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 @page_title variable and use it for outputting a title tag:
#
# <%= @page_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 shared/header with:
#
# Headline: <%= headline %>
# First name: <%= person.first_name %>
#
# == 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.
#
# == 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 +.rxml+ extension.
#
# Here are some basic examples:
#
# xml.em("emphasized") # => emphasized
# xml.em { xml.b("emp & bold") } # => emph & bold
# xml.a("A Link", "href"=>"http://onestepback.org") # => A Link
# xml.target("name"=>"compile", "option"=>"fast") # =>
# # 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 {
# xml.h1(@person.name)
# xml.p(@person.bio)
# }
#
# would produce something like:
#
#
#
David Heinemeier Hansson
#
A product of Danish Design during the Winter of '79...
#
#
# 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"
#
# for item in @recent_items
# 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.
#
# == JavaScriptGenerator
#
# JavaScriptGenerator templates end in +.rjs+. Unlike conventional templates which are used to
# render the results of an action, these templates generate instructions on how to modify an already rendered page. This makes it easy to
# modify multiple elements on your page in one declarative Ajax response. Actions with these templates are called in the background with Ajax
# and make updates to the page where the request originated from.
#
# An instance of the JavaScriptGenerator object named +page+ is automatically made available to your template, which is implicitly wrapped in an ActionView::Helpers::PrototypeHelper#update_page block.
#
# When an .rjs action is called with +link_to_remote+, the generated JavaScript is automatically evaluated. Example:
#
# link_to_remote :url => {:action => 'delete'}
#
# The subsequently rendered +delete.rjs+ might look like:
#
# page.replace_html 'sidebar', :partial => 'sidebar'
# page.remove "person-#{@person.id}"
# page.visual_effect :highlight, 'user-list'
#
# This refreshes the sidebar, removes a person element and highlights the user list.
#
# See the ActionView::Helpers::PrototypeHelper::JavaScriptGenerator documentation for more details.
class Base
include ERB::Util
attr_reader :first_render
attr_accessor :base_path, :assigns, :template_extension
attr_accessor :controller
attr_reader :logger, :params, :request, :response, :session, :headers, :flash
# Specify trim mode for the ERB compiler. Defaults to '-'.
# See ERB documentation for suitable values.
@@erb_trim_mode = '-'
cattr_accessor :erb_trim_mode
# Specify whether file modification times should be checked to see if a template needs recompilation
@@cache_template_loading = false
cattr_accessor :cache_template_loading
# Specify whether file extension lookup should be cached.
# Should be +false+ for development environments. Defaults to +true+.
@@cache_template_extensions = true
cattr_accessor :cache_template_extensions
# Specify whether local_assigns should be able to use string keys.
# Defaults to +true+. String keys are deprecated and will be removed
# shortly.
@@local_assigns_support_string_keys = true
cattr_accessor :local_assigns_support_string_keys
# Specify whether RJS responses should be wrapped in a try/catch block
# that alert()s the caught exception (and then re-raises it).
@@debug_rjs = false
cattr_accessor :debug_rjs
@@template_handlers = HashWithIndifferentAccess.new
module CompiledTemplates #:nodoc:
# holds compiled template code
end
include CompiledTemplates
# maps inline templates to their method names
@@method_names = {}
# map method names to their compile time
@@compile_time = {}
# map method names to the names passed in local assigns so far
@@template_args = {}
# count the number of inline templates
@@inline_template_count = 0
# maps template paths without extension to their file extension returned by pick_template_extension.
# if for a given path, path.ext1 and path.ext2 exist on the file system, the order of extensions
# used by pick_template_extension determines whether ext1 or ext2 will be stored
@@cached_template_extension = {}
class ObjectWrapper < Struct.new(:value) #:nodoc:
end
def self.load_helpers(helper_dir)#:nodoc:
Dir.foreach(helper_dir) do |helper_file|
next unless helper_file =~ /^([a-z][a-z_]*_helper).rb$/
require File.join(helper_dir, $1)
helper_module_name = $1.camelize
class_eval("include ActionView::Helpers::#{helper_module_name}") if Helpers.const_defined?(helper_module_name)
end
end
# Register a class that knows how to handle template files with the given
# extension. This can be used to implement new template types.
# The constructor for the class must take the ActiveView::Base instance
# as a parameter, and the class must implement a #render method that
# takes the contents of the template to render as well as the Hash of
# local assigns available to the template. The #render method ought to
# return the rendered template as a string.
def self.register_template_handler(extension, klass)
@@template_handlers[extension] = klass
end
def initialize(base_path = nil, assigns_for_first_render = {}, controller = nil)#:nodoc:
@base_path, @assigns = base_path, assigns_for_first_render
@assigns_added = nil
@controller = controller
@logger = controller && controller.logger
end
# Renders the template present at template_path. If use_full_path is set to true,
# it's relative to the template_root, otherwise it's absolute. The hash in local_assigns
# is made available as local variables.
def render_file(template_path, use_full_path = true, local_assigns = {}) #:nodoc:
@first_render ||= template_path
if use_full_path
template_path_without_extension, template_extension = path_and_extension(template_path)
if template_extension
template_file_name = full_template_path(template_path_without_extension, template_extension)
else
template_extension = pick_template_extension(template_path).to_s
template_file_name = full_template_path(template_path, template_extension)
end
else
template_file_name = template_path
template_extension = template_path.split('.').last
end
template_source = nil # Don't read the source until we know that it is required
begin
render_template(template_extension, template_source, template_file_name, local_assigns)
rescue Exception => e
if TemplateError === e
e.sub_template_of(template_file_name)
raise e
else
raise TemplateError.new(@base_path, template_file_name, @assigns, template_source, e)
end
end
end
# Renders the template present at template_path (relative to the template_root).
# The hash in local_assigns is made available as local variables.
def render(options = {}, old_local_assigns = {}, &block) #:nodoc:
if options.is_a?(String)
render_file(options, true, old_local_assigns)
elsif options == :update
update_page(&block)
elsif options.is_a?(Hash)
options[:locals] ||= {}
options[:use_full_path] = options[:use_full_path].nil? ? true : options[:use_full_path]
if options[:file]
render_file(options[:file], options[:use_full_path], options[:locals])
elsif options[:partial] && options[:collection]
render_partial_collection(options[:partial], options[:collection], options[:spacer_template], options[:locals])
elsif options[:partial]
render_partial(options[:partial], ActionView::Base::ObjectWrapper.new(options[:object]), options[:locals])
elsif options[:inline]
render_template(options[:type] || :rhtml, options[:inline], nil, options[:locals] || {})
end
end
end
# Renders the +template+ which is given as a string as either rhtml or rxml depending on template_extension.
# The hash in local_assigns is made available as local variables.
def render_template(template_extension, template, file_path = nil, local_assigns = {}) #:nodoc:
if handler = @@template_handlers[template_extension]
template ||= read_template_file(file_path, template_extension) # Make sure that a lazyily-read template is loaded.
delegate_render(handler, template, local_assigns)
else
compile_and_render_template(template_extension, template, file_path, local_assigns)
end
end
# Render the provided template with the given local assigns. If the template has not been rendered with the provided
# local assigns yet, or if the template has been updated on disk, then the template will be compiled to a method.
#
# Either, but not both, of template and file_path may be nil. If file_path is given, the template
# will only be read if it has to be compiled.
#
def compile_and_render_template(extension, template = nil, file_path = nil, local_assigns = {}) #:nodoc:
# compile the given template, if necessary
if compile_template?(template, file_path, local_assigns)
template ||= read_template_file(file_path, extension)
compile_template(extension, template, file_path, local_assigns)
end
# Get the method name for this template and run it
method_name = @@method_names[file_path || template]
evaluate_assigns
local_assigns = local_assigns.symbolize_keys if @@local_assigns_support_string_keys
send(method_name, local_assigns) do |*name|
instance_variable_get "@content_for_#{name.first || 'layout'}"
end
end
def pick_template_extension(template_path)#:nodoc:
if @@cache_template_extensions
@@cached_template_extension[template_path] ||= find_template_extension_for(template_path)
else
find_template_extension_for(template_path)
end
end
def delegate_template_exists?(template_path)#:nodoc:
@@template_handlers.find { |k,| template_exists?(template_path, k) }
end
def erb_template_exists?(template_path)#:nodoc:
template_exists?(template_path, :rhtml)
end
def builder_template_exists?(template_path)#:nodoc:
template_exists?(template_path, :rxml)
end
def javascript_template_exists?(template_path)#:nodoc:
template_exists?(template_path, :rjs)
end
def file_exists?(template_path)#:nodoc:
template_file_name, template_file_extension = path_and_extension(template_path)
if template_file_extension
template_exists?(template_file_name, template_file_extension)
else
cached_template_extension(template_path) ||
%w(erb builder javascript delegate).any? do |template_type|
send("#{template_type}_template_exists?", template_path)
end
end
end
# Returns true is the file may be rendered implicitly.
def file_public?(template_path)#:nodoc:
template_path.split('/').last[0,1] != '_'
end
private
def full_template_path(template_path, extension)
"#{@base_path}/#{template_path}.#{extension}"
end
def template_exists?(template_path, extension)
file_path = full_template_path(template_path, extension)
@@method_names.has_key?(file_path) || FileTest.exists?(file_path)
end
def path_and_extension(template_path)
template_path_without_extension = template_path.sub(/\.(\w+)$/, '')
[ template_path_without_extension, $1 ]
end
def cached_template_extension(template_path)
@@cache_template_extensions && @@cached_template_extension[template_path]
end
def find_template_extension_for(template_path)
if match = delegate_template_exists?(template_path)
match.first.to_sym
elsif erb_template_exists?(template_path): :rhtml
elsif builder_template_exists?(template_path): :rxml
elsif javascript_template_exists?(template_path): :rjs
else
raise ActionViewError, "No rhtml, rxml, rjs or delegate template found for #{template_path}"
end
end
# This method reads a template file.
def read_template_file(template_path, extension)
File.read(template_path)
end
def evaluate_assigns
unless @assigns_added
assign_variables_from_controller
@assigns_added = true
end
end
def delegate_render(handler, template, local_assigns)
handler.new(self).render(template, local_assigns)
end
def assign_variables_from_controller
@assigns.each { |key, value| instance_variable_set("@#{key}", value) }
end
# Return true if the given template was compiled for a superset of the keys in local_assigns
def supports_local_assigns?(render_symbol, local_assigns)
local_assigns.empty? ||
((args = @@template_args[render_symbol]) && local_assigns.all? { |k,_| args.has_key?(k) })
end
# Check whether compilation is necessary.
# Compile if the inline template or file has not been compiled yet.
# Or if local_assigns has a new key, which isn't supported by the compiled code yet.
# Or if the file has changed on disk and checking file mods hasn't been disabled.
def compile_template?(template, file_name, local_assigns)
method_key = file_name || template
render_symbol = @@method_names[method_key]
if @@compile_time[render_symbol] && supports_local_assigns?(render_symbol, local_assigns)
if file_name && !@@cache_template_loading
@@compile_time[render_symbol] < File.mtime(file_name) || (File.symlink?(file_name) ?
@@compile_time[render_symbol] < File.lstat(file_name).mtime : false)
end
else
true
end
end
# Create source code for given template
def create_template_source(extension, template, render_symbol, locals)
if template_requires_setup?(extension)
body = case extension.to_sym
when :rxml
"xml = Builder::XmlMarkup.new(:indent => 2)\n" +
"@controller.headers['Content-Type'] ||= 'application/xml'\n" +
template
when :rjs
"@controller.headers['Content-Type'] ||= 'text/javascript'\n" +
"update_page do |page|\n#{template}\nend"
end
else
body = ERB.new(template, nil, @@erb_trim_mode).src
end
@@template_args[render_symbol] ||= {}
locals_keys = @@template_args[render_symbol].keys | locals
@@template_args[render_symbol] = locals_keys.inject({}) { |h, k| h[k] = true; h }
locals_code = ""
locals_keys.each do |key|
locals_code << "#{key} = local_assigns[:#{key}] if local_assigns.has_key?(:#{key})\n"
end
"def #{render_symbol}(local_assigns)\n#{locals_code}#{body}\nend"
end
def template_requires_setup?(extension)
templates_requiring_setup.include? extension.to_s
end
def templates_requiring_setup
%w(rxml rjs)
end
def assign_method_name(extension, template, file_name)
method_name = '_run_'
method_name << "#{extension}_" if extension
if file_name
file_path = File.expand_path(file_name)
base_path = File.expand_path(@base_path)
i = file_path.index(base_path)
l = base_path.length
method_name_file_part = i ? file_path[i+l+1,file_path.length-l-1] : file_path.clone
method_name_file_part.sub!(/\.r(html|xml|js)$/,'')
method_name_file_part.tr!('/:-', '_')
method_name_file_part.gsub!(/[^a-zA-Z0-9_]/){|s| s[0].to_s}
method_name += method_name_file_part
else
@@inline_template_count += 1
method_name << @@inline_template_count.to_s
end
@@method_names[file_name || template] = method_name.intern
end
def compile_template(extension, template, file_name, local_assigns)
method_key = file_name || template
render_symbol = @@method_names[method_key] || assign_method_name(extension, template, file_name)
render_source = create_template_source(extension, template, render_symbol, local_assigns.keys)
line_offset = @@template_args[render_symbol].size
if extension
case extension.to_sym
when :rxml, :rjs
line_offset += 2
end
end
begin
unless file_name.blank?
CompiledTemplates.module_eval(render_source, file_name, -line_offset)
else
CompiledTemplates.module_eval(render_source, 'compiled-template', -line_offset)
end
rescue Object => e
if logger
logger.debug "ERROR: compiling #{render_symbol} RAISED #{e}"
logger.debug "Function body: #{render_source}"
logger.debug "Backtrace: #{e.backtrace.join("\n")}"
end
raise TemplateError.new(@base_path, method_key, @assigns, template, e)
end
@@compile_time[render_symbol] = Time.now
# logger.debug "Compiled template #{method_key}\n ==> #{render_symbol}" if logger
end
end
end
require 'action_view/template_error'
module ActionView
# CompiledTemplates modules hold methods that have been compiled.
# Templates are compiled into these methods so that they do not need to be
# re-read and re-parsed each request.
#
# Each template may be compiled into one or more methods. Each method accepts a given
# set of parameters which is used to implement local assigns passing.
#
# To use a compiled template module, create a new instance and include it into the class
# in which you want the template to be rendered.
class CompiledTemplates < Module #:nodoc:
attr_reader :method_names
def initialize
@method_names = Hash.new do |hash, key|
hash[key] = "__compiled_method_#{(hash.length + 1)}"
end
@mtimes = {}
end
# Return the full key for the given identifier and argument names
def full_key(identifier, arg_names)
[identifier, arg_names]
end
# Return the selector for this method or nil if it has not been compiled
def selector(identifier, arg_names)
key = full_key(identifier, arg_names)
method_names.key?(key) ? method_names[key] : nil
end
alias :compiled? :selector
# Return the time at which the method for the given identifier and argument names was compiled.
def mtime(identifier, arg_names)
@mtimes[full_key(identifier, arg_names)]
end
# Compile the provided source code for the given argument names and with the given initial line number.
# The identifier should be unique to this source.
#
# The file_name, if provided will appear in backtraces. If not provded, the file_name defaults
# to the identifier.
#
# This method will return the selector for the compiled version of this method.
def compile_source(identifier, arg_names, source, initial_line_number = 0, file_name = nil)
file_name ||= identifier
name = method_names[full_key(identifier, arg_names)]
arg_desc = arg_names.empty? ? '' : "(#{arg_names * ', '})"
fake_file_name = "#{file_name}#{arg_desc}" # Include the arguments for this version (for now)
method_def = wrap_source(name, arg_names, source)
begin
module_eval(method_def, fake_file_name, initial_line_number)
@mtimes[full_key(identifier, arg_names)] = Time.now
rescue Object => e
e.blame_file! identifier
raise
end
name
end
# Wrap the provided source in a def ... end block.
def wrap_source(name, arg_names, source)
"def #{name}(#{arg_names * ', '})\n#{source}\nend"
end
end
end
require 'cgi'
require File.dirname(__FILE__) + '/form_helper'
module ActionView
class Base
@@field_error_proc = Proc.new{ |html_tag, instance| "
#{html_tag}
" }
cattr_accessor :field_error_proc
end
module Helpers
# The Active Record Helper makes it easier to create forms for records kept in instance variables. The most far-reaching is the form
# method that creates a complete form for all the basic content types of the record (not associations or aggregations, though). This
# is a great of making the record quickly available for editing, but likely to prove lackluster for a complicated real-world form.
# In that case, it's better to use the input method and the specialized form methods in link:classes/ActionView/Helpers/FormHelper.html
module ActiveRecordHelper
# Returns a default input tag for the type of object returned by the method. Example
# (title is a VARCHAR column and holds "Hello World"):
# input("post", "title") =>
#
def input(record_name, method, options = {})
InstanceTag.new(record_name, method, self).to_tag(options)
end
# Returns an entire form with input tags and everything for a specified Active Record object. Example
# (post is a new record that has a title using VARCHAR and a body using TEXT):
# form("post") =>
#
#
# It's possible to specialize the form builder by using a different action name and by supplying another
# block renderer. Example (entry is a new record that has a message attribute using VARCHAR):
#
# form("entry", :action => "sign", :input_block =>
# Proc.new { |record, column| "#{column.human_name}: #{input(record, column.name)} " }) =>
#
#
#
# It's also possible to add additional content to the form by giving it a block, such as:
#
# form("entry", :action => "sign") do |form|
# form << content_tag("b", "Department")
# form << collection_select("department", "id", @departments, "id", "name")
# end
def form(record_name, options = {})
record = instance_variable_get("@#{record_name}")
options = options.symbolize_keys
options[:action] ||= record.new_record? ? "create" : "update"
action = url_for(:action => options[:action], :id => record)
submit_value = options[:submit_value] || options[:action].gsub(/[^\w]/, '').capitalize
contents = ''
contents << hidden_field(record_name, :id) unless record.new_record?
contents << all_input_tags(record, record_name, options)
yield contents if block_given?
contents << submit_tag(submit_value)
content_tag('form', contents, :action => action, :method => 'post', :enctype => options[:multipart] ? 'multipart/form-data': nil)
end
# Returns a string containing the error message attached to the +method+ on the +object+, if one exists.
# This error message is wrapped in a DIV tag, which can be specialized to include both a +prepend_text+ and +append_text+
# to properly introduce the error and a +css_class+ to style it accordingly. Examples (post has an error message
# "can't be empty" on the title attribute):
#
# <%= error_message_on "post", "title" %> =>
#
def error_message_on(object, method, prepend_text = "", append_text = "", css_class = "formError")
if errors = instance_variable_get("@#{object}").errors.on(method)
content_tag("div", "#{prepend_text}#{errors.is_a?(Array) ? errors.first : errors}#{append_text}", :class => css_class)
end
end
# Returns a string with a div containing all the error messages for the object located as an instance variable by the name
# of object_name. This div can be tailored by the following options:
#
# * header_tag - Used for the header of the error div (default: h2)
# * id - The id of the error div (default: errorExplanation)
# * class - The class of the error div (default: errorExplanation)
#
# NOTE: This is a pre-packaged presentation of the errors with embedded strings and a certain HTML structure. If what
# you need is significantly different from the default presentation, it makes plenty of sense to access the object.errors
# instance yourself and set it up. View the source of this method to see how easy it is.
def error_messages_for(object_name, options = {})
options = options.symbolize_keys
object = instance_variable_get("@#{object_name}")
if object && !object.errors.empty?
content_tag("div",
content_tag(
options[:header_tag] || "h2",
"#{pluralize(object.errors.count, "error")} prohibited this #{object_name.to_s.gsub("_", " ")} from being saved"
) +
content_tag("p", "There were problems with the following fields:") +
content_tag("ul", object.errors.full_messages.collect { |msg| content_tag("li", msg) }),
"id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation"
)
else
""
end
end
private
def all_input_tags(record, record_name, options)
input_block = options[:input_block] || default_input_block
record.class.content_columns.collect{ |column| input_block.call(record_name, column) }.join("\n")
end
def default_input_block
Proc.new { |record, column| %(
#{input(record, column.name)}
) }
end
end
class InstanceTag #:nodoc:
def to_tag(options = {})
case column_type
when :string
field_type = @method_name.include?("password") ? "password" : "text"
to_input_field_tag(field_type, options)
when :text
to_text_area_tag(options)
when :integer, :float
to_input_field_tag("text", options)
when :date
to_date_select_tag(options)
when :datetime, :timestamp
to_datetime_select_tag(options)
when :boolean
to_boolean_select_tag(options)
end
end
alias_method :tag_without_error_wrapping, :tag
def tag(name, options)
if object.respond_to?("errors") && object.errors.respond_to?("on")
error_wrapping(tag_without_error_wrapping(name, options), object.errors.on(@method_name))
else
tag_without_error_wrapping(name, options)
end
end
alias_method :content_tag_without_error_wrapping, :content_tag
def content_tag(name, value, options)
if object.respond_to?("errors") && object.errors.respond_to?("on")
error_wrapping(content_tag_without_error_wrapping(name, value, options), object.errors.on(@method_name))
else
content_tag_without_error_wrapping(name, value, options)
end
end
alias_method :to_date_select_tag_without_error_wrapping, :to_date_select_tag
def to_date_select_tag(options = {})
if object.respond_to?("errors") && object.errors.respond_to?("on")
error_wrapping(to_date_select_tag_without_error_wrapping(options), object.errors.on(@method_name))
else
to_date_select_tag_without_error_wrapping(options)
end
end
alias_method :to_datetime_select_tag_without_error_wrapping, :to_datetime_select_tag
def to_datetime_select_tag(options = {})
if object.respond_to?("errors") && object.errors.respond_to?("on")
error_wrapping(to_datetime_select_tag_without_error_wrapping(options), object.errors.on(@method_name))
else
to_datetime_select_tag_without_error_wrapping(options)
end
end
def error_wrapping(html_tag, has_error)
has_error ? Base.field_error_proc.call(html_tag, self) : html_tag
end
def error_message
object.errors.on(@method_name)
end
def column_type
object.send("column_for_attribute", @method_name).type
end
end
end
end
require 'cgi'
require File.dirname(__FILE__) + '/url_helper'
require File.dirname(__FILE__) + '/tag_helper'
module ActionView
module Helpers
# Provides methods for linking a HTML page together with other assets, such as javascripts, stylesheets, and feeds.
module AssetTagHelper
# Returns a link tag that browsers and news readers can use to auto-detect a RSS or ATOM feed for this page. The +type+ can
# either be :rss (default) or :atom and the +options+ follow the url_for style of declaring a link target.
#
# Examples:
# auto_discovery_link_tag # =>
#
# auto_discovery_link_tag(:atom) # =>
#
# auto_discovery_link_tag(:rss, {:action => "feed"}) # =>
#
# auto_discovery_link_tag(:rss, {:action => "feed"}, {:title => "My RSS"}) # =>
#
def auto_discovery_link_tag(type = :rss, url_options = {}, tag_options = {})
tag(
"link",
"rel" => tag_options[:rel] || "alternate",
"type" => tag_options[:type] || "application/#{type}+xml",
"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 path to a javascript asset. Example:
#
# javascript_path "xmlhr" # => /javascripts/xmlhr.js
def javascript_path(source)
compute_public_path(source, 'javascripts', 'js')
end
JAVASCRIPT_DEFAULT_SOURCES = ['prototype', 'effects', 'dragdrop', 'controls'] unless const_defined?(:JAVASCRIPT_DEFAULT_SOURCES)
@@javascript_default_sources = JAVASCRIPT_DEFAULT_SOURCES.dup
# Returns a script include tag per source given as argument. Examples:
#
# javascript_include_tag "xmlhr" # =>
#
#
# javascript_include_tag "common.javascript", "/elsewhere/cools" # =>
#
#
#
# javascript_include_tag :defaults # =>
#
#
# ...
# *see below
#
# If there's an application.js file in your public/javascripts directory,
# javascript_include_tag :defaults will automatically include it. This file
# facilitates the inclusion of small snippets of JavaScript code, along the lines of
# controllers/application.rb and helpers/application_helper.rb.
def javascript_include_tag(*sources)
options = sources.last.is_a?(Hash) ? sources.pop.stringify_keys : { }
if sources.include?(:defaults)
sources = sources[0..(sources.index(:defaults))] +
@@javascript_default_sources.dup +
sources[(sources.index(:defaults) + 1)..sources.length]
sources.delete(:defaults)
sources << "application" if defined?(RAILS_ROOT) && File.exists?("#{RAILS_ROOT}/public/javascripts/application.js")
end
sources.collect { |source|
source = javascript_path(source)
content_tag("script", "", { "type" => "text/javascript", "src" => source }.merge(options))
}.join("\n")
end
# Register one or more additional JavaScript files to be included when
#
# javascript_include_tag :defaults
#
# is called. This method is intended to be called only from plugin initialization
# to register extra .js files the plugin installed in public/javascripts.
def self.register_javascript_include_default(*sources)
@@javascript_default_sources.concat(sources)
end
def self.reset_javascript_include_default #:nodoc:
@@javascript_default_sources = JAVASCRIPT_DEFAULT_SOURCES.dup
end
# Returns path to a stylesheet asset. Example:
#
# stylesheet_path "style" # => /stylesheets/style.css
def stylesheet_path(source)
compute_public_path(source, 'stylesheets', 'css')
end
# Returns a css link tag per source given as argument. Examples:
#
# stylesheet_link_tag "style" # =>
#
#
# stylesheet_link_tag "style", :media => "all" # =>
#
#
# stylesheet_link_tag "random.styles", "/css/stylish" # =>
#
#
def stylesheet_link_tag(*sources)
options = sources.last.is_a?(Hash) ? sources.pop.stringify_keys : { }
sources.collect { |source|
source = stylesheet_path(source)
tag("link", { "rel" => "Stylesheet", "type" => "text/css", "media" => "screen", "href" => source }.merge(options))
}.join("\n")
end
# Returns path to an image asset. Example:
#
# The +src+ can be supplied as a...
# * full path, like "/my_images/image.gif"
# * file name, like "rss.gif", that gets expanded to "/images/rss.gif"
# * file name without extension, like "logo", that gets expanded to "/images/logo.png"
def image_path(source)
compute_public_path(source, 'images', 'png')
end
# Returns an image tag converting the +options+ into html options on the tag, but with these special cases:
#
# * :alt - If no alt text is given, the file name part of the +src+ is used (capitalized and without the extension)
# * :size - Supplied as "XxY", so "30x45" becomes width="30" and height="45"
#
# The +src+ can be supplied as a...
# * full path, like "/my_images/image.gif"
# * file name, like "rss.gif", that gets expanded to "/images/rss.gif"
# * file name without extension, like "logo", that gets expanded to "/images/logo.png"
def image_tag(source, options = {})
options.symbolize_keys!
options[:src] = image_path(source)
options[:alt] ||= File.basename(options[:src], '.*').split('.').first.capitalize
if options[:size]
options[:width], options[:height] = options[:size].split("x")
options.delete :size
end
tag("img", options)
end
private
def compute_public_path(source, dir, ext)
source = "/#{dir}/#{source}" unless source.first == "/" || source.include?(":")
source << ".#{ext}" unless source.split("/").last.include?(".")
source << '?' + rails_asset_id(source) if defined?(RAILS_ROOT) && %r{^[-a-z]+://} !~ source
source = "#{@controller.request.relative_url_root}#{source}" unless %r{^[-a-z]+://} =~ source
source = ActionController::Base.asset_host + source unless source.include?(":")
source
end
def rails_asset_id(source)
ENV["RAILS_ASSET_ID"] ||
File.mtime("#{RAILS_ROOT}/public/#{source}").to_i.to_s rescue ""
end
end
end
end
require 'benchmark'
module ActionView
module Helpers
module BenchmarkHelper
# Measures the execution time of a block in a template and reports the result to the log. Example:
#
# <% benchmark "Notes section" do %>
# <%= expensive_notes_operation %>
# <% end %>
#
# Will add something like "Notes section (0.34523)" to the log.
#
# You may give an optional logger level as the second argument
# (:debug, :info, :warn, :error). The default is :info.
def benchmark(message = "Benchmarking", level = :info)
if @logger
real = Benchmark.realtime { yield }
@logger.send level, "#{message} (#{'%.5f' % real})"
end
end
end
end
end
module ActionView
module Helpers
# See ActionController::Caching::Fragments for usage instructions.
module CacheHelper
def cache(name = {}, &block)
@controller.cache_erb_fragment(block, name)
end
end
end
end
module ActionView
module Helpers
# Capture lets you extract parts of code which
# can be used in other points of the template or even layout file.
#
# == Capturing a block into an instance variable
#
# <% @script = capture do %>
# [some html...]
# <% end %>
#
# == Add javascript to header using content_for
#
# content_for("name") is a wrapper for capture which will
# make the fragment available by name to a yielding layout or template.
#
# layout.rhtml:
#
#
#
# layout with js
#
#
#
# <%= yield %>
#
#
#
# view.rhtml
#
# This page shows an alert box!
#
# <% content_for("script") do %>
# alert('hello world')
# <% end %>
#
# Normal view text
module CaptureHelper
# Capture allows you to extract a part of the template into an
# instance variable. You can use this instance variable anywhere
# in your templates and even in your layout.
#
# Example of capture being used in a .rhtml page:
#
# <% @greeting = capture do %>
# Welcome To my shiny new web page!
# <% end %>
#
# Example of capture being used in a .rxml page:
#
# @greeting = capture do
# 'Welcome To my shiny new web page!'
# end
def capture(*args, &block)
# execute the block
begin
buffer = eval("_erbout", block.binding)
rescue
buffer = nil
end
if buffer.nil?
capture_block(*args, &block)
else
capture_erb_with_buffer(buffer, *args, &block)
end
end
# Calling content_for stores the block of markup for later use.
# Subsequently, you can make calls to it by name with yield
# in another template or in the layout.
#
# Example:
#
# <% content_for("header") do %>
# alert('hello world')
# <% end %>
#
# You can use yield :header anywhere in your templates.
#
# <%= yield :header %>
#
# NOTE: Beware that content_for is ignored in caches. So you shouldn't use it
# for elements that are going to be fragment cached.
#
# The deprecated way of accessing a content_for block was to use a instance variable
# named @@content_for_#{name_of_the_content_block}@. So <%= content_for('footer') %>
# would be avaiable as <%= @content_for_footer %>. The preferred notation now is
# <%= yield :footer %>.
def content_for(name, &block)
eval "@content_for_#{name} = (@content_for_#{name} || '') + capture(&block)"
end
private
def capture_block(*args, &block)
block.call(*args)
end
def capture_erb(*args, &block)
buffer = eval("_erbout", block.binding)
capture_erb_with_buffer(buffer, *args, &block)
end
def capture_erb_with_buffer(buffer, *args, &block)
pos = buffer.length
block.call(*args)
# extract the block
data = buffer[pos..-1]
# replace it in the original with empty string
buffer[pos..-1] = ''
data
end
def erb_content_for(name, &block)
eval "@content_for_#{name} = (@content_for_#{name} || '') + capture_erb(&block)"
end
def block_content_for(name, &block)
eval "@content_for_#{name} = (@content_for_#{name} || '') + capture_block(&block)"
end
end
end
end
require "date"
module ActionView
module Helpers
# The Date Helper primarily creates select/option tags for different kinds of dates and date elements. All of the select-type methods
# share a number of common options that are as follows:
#
# * :prefix - 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 select_month method.
# * :include_blank - set to true if it should be possible to set an empty date.
# * :discard_type - set to true if you want to discard the type part of the select name. If set to true, the select_month
# method would use simply "date" (which can be overwritten using :prefix) instead of "date[month]".
module DateHelper
DEFAULT_PREFIX = 'date' unless const_defined?('DEFAULT_PREFIX')
# Reports the approximate distance in time between two Time objects or integers.
# For example, if the distance is 47 minutes, it'll return
# "about 1 hour". See the source for the complete wording list.
#
# Integers are interpreted as seconds. So,
# distance_of_time_in_words(50) returns "less than a minute".
#
# Set include_seconds to true if you want more detailed approximations if distance < 1 minute
def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false)
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)
distance_in_minutes = (((to_time - from_time).abs)/60).round
distance_in_seconds = ((to_time - from_time).abs).round
case distance_in_minutes
when 0..1
return (distance_in_minutes==0) ? 'less than a minute' : '1 minute' unless include_seconds
case distance_in_seconds
when 0..5 then 'less than 5 seconds'
when 6..10 then 'less than 10 seconds'
when 11..20 then 'less than 20 seconds'
when 21..40 then 'half a minute'
when 41..59 then 'less than a minute'
else '1 minute'
end
when 2..45 then "#{distance_in_minutes} minutes"
when 46..90 then 'about 1 hour'
when 90..1440 then "about #{(distance_in_minutes.to_f / 60.0).round} hours"
when 1441..2880 then '1 day'
else "#{(distance_in_minutes / 1440).round} days"
end
end
# Like distance_of_time_in_words, but where to_time is fixed to Time.now.
def time_ago_in_words(from_time, include_seconds = false)
distance_of_time_in_words(from_time, Time.now, include_seconds)
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+). It's possible to tailor the selects through the +options+ hash,
# which accepts all the keys that each of the individual select builders do (like :use_month_numbers for select_month) as well as a range of
# discard options. The discard options are :discard_year, :discard_month and :discard_day. Set to true, they'll
# drop the respective select. Discarding the month select will also automatically discard the day select. It's also possible to explicitly
# set the order of the tags using the :order option with an array of symbols :year, :month and :day in
# the desired order. Symbols may be omitted and the respective select is not included.
#
# Passing :disabled => true as part of the +options+ will make elements inaccessible for change.
#
# NOTE: Discarded selects will default to 1. So if no month select is available, January will be assumed.
#
# Examples:
#
# date_select("post", "written_on")
# date_select("post", "written_on", :start_year => 1995)
# date_select("post", "written_on", :start_year => 1995, :use_month_numbers => true,
# :discard_day => true, :include_blank => true)
# date_select("post", "written_on", :order => [:day, :month, :year])
# date_select("user", "birthday", :order => [:month, :day])
#
# The selects are prepared for multi-parameter assignment to an Active Record object.
def date_select(object_name, method, options = {})
InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_date_select_tag(options)
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+). Examples:
#
# datetime_select("post", "written_on")
# datetime_select("post", "written_on", :start_year => 1995)
#
# The selects are prepared for multi-parameter assignment to an Active Record object.
def datetime_select(object_name, method, options = {})
InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_datetime_select_tag(options)
end
# Returns a set of html select-tags (one for year, month, and day) pre-selected with the +date+.
def select_date(date = Date.today, options = {})
select_year(date, options) + select_month(date, options) + select_day(date, options)
end
# Returns a set of html select-tags (one for year, month, day, hour, and minute) pre-selected with the +datetime+.
def select_datetime(datetime = Time.now, options = {})
select_year(datetime, options) + select_month(datetime, options) + select_day(datetime, options) +
select_hour(datetime, options) + select_minute(datetime, options)
end
# Returns a set of html select-tags (one for hour and minute)
def select_time(datetime = Time.now, options = {})
h = select_hour(datetime, options) + select_minute(datetime, options) + (options[:include_seconds] ? select_second(datetime, options) : '')
end
# Returns a select tag with options for each of the seconds 0 through 59 with the current second selected.
# The second can also be substituted for a second number.
# Override the field name using the :field_name option, 'second' by default.
def select_second(datetime, options = {})
second_options = []
0.upto(59) do |second|
second_options << ((datetime && (datetime.kind_of?(Fixnum) ? datetime : datetime.sec) == second) ?
%(\n) :
%(\n)
)
end
select_html(options[:field_name] || 'second', second_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled])
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 minute_step from 0 through 59 with the 00 minute selected
# The minute can also be substituted for a minute number.
# Override the field name using the :field_name option, 'minute' by default.
def select_minute(datetime, options = {})
minute_options = []
0.step(59, options[:minute_step] || 1) do |minute|
minute_options << ((datetime && (datetime.kind_of?(Fixnum) ? datetime : datetime.min) == minute) ?
%(\n) :
%(\n)
)
end
select_html(options[:field_name] || 'minute', minute_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled])
end
# Returns a select tag with options for each of the hours 0 through 23 with the current hour selected.
# The hour can also be substituted for a hour number.
# Override the field name using the :field_name option, 'hour' by default.
def select_hour(datetime, options = {})
hour_options = []
0.upto(23) do |hour|
hour_options << ((datetime && (datetime.kind_of?(Fixnum) ? datetime : datetime.hour) == hour) ?
%(\n) :
%(\n)
)
end
select_html(options[:field_name] || 'hour', hour_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled])
end
# Returns a select tag with options for each of the days 1 through 31 with the current day selected.
# The date can also be substituted for a hour number.
# Override the field name using the :field_name option, 'day' by default.
def select_day(date, options = {})
day_options = []
1.upto(31) do |day|
day_options << ((date && (date.kind_of?(Fixnum) ? date : date.day) == day) ?
%(\n) :
%(\n)
)
end
select_html(options[:field_name] || 'day', day_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled])
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 :use_month_numbers key in +options+ to true for this to happen. If you want both numbers and names,
# set the :add_month_numbers key in +options+ to true. Examples:
#
# select_month(Date.today) # Will use keys like "January", "March"
# select_month(Date.today, :use_month_numbers => true) # Will use keys like "1", "3"
# select_month(Date.today, :add_month_numbers => true) # Will use keys like "1 - January", "3 - March"
#
# Override the field name using the :field_name option, 'month' by default.
#
# If you would prefer to show month names as abbreviations, set the
# :use_short_month key in +options+ to true.
def select_month(date, options = {})
month_options = []
month_names = options[:use_short_month] ? Date::ABBR_MONTHNAMES : Date::MONTHNAMES
1.upto(12) do |month_number|
month_name = if options[:use_month_numbers]
month_number
elsif options[:add_month_numbers]
month_number.to_s + ' - ' + month_names[month_number]
else
month_names[month_number]
end
month_options << ((date && (date.kind_of?(Fixnum) ? date : date.month) == month_number) ?
%(\n) :
%(\n)
)
end
select_html(options[:field_name] || 'month', month_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled])
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 :start_year and :end_year keys in the +options+. Both ascending and descending year
# lists are supported by making :start_year less than or greater than :end_year. The date can also be
# substituted for a year given as a number. Example:
#
# select_year(Date.today, :start_year => 1992, :end_year => 2007) # ascending year values
# select_year(Date.today, :start_year => 2005, :end_year => 1900) # descending year values
#
# Override the field name using the :field_name option, 'year' by default.
def select_year(date, options = {})
year_options = []
y = date ? (date.kind_of?(Fixnum) ? (y = (date == 0) ? Date.today.year : date) : date.year) : Date.today.year
start_year, end_year = (options[:start_year] || y-5), (options[:end_year] || y+5)
step_val = start_year < end_year ? 1 : -1
start_year.step(end_year, step_val) do |year|
year_options << ((date && (date.kind_of?(Fixnum) ? date : date.year) == year) ?
%(\n) :
%(\n)
)
end
select_html(options[:field_name] || 'year', year_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled])
end
private
def select_html(type, options, prefix = nil, include_blank = false, discard_type = false, disabled = false)
select_html = %(\n"
end
def leading_zero_on_single_digits(number)
number > 9 ? number : "0#{number}"
end
end
class InstanceTag #:nodoc:
include DateHelper
def to_date_select_tag(options = {})
defaults = { :discard_type => true }
options = defaults.merge(options)
options_with_prefix = Proc.new { |position| options.merge(:prefix => "#{@object_name}[#{@method_name}(#{position}i)]") }
date = options[:include_blank] ? (value || 0) : (value || Date.today)
date_select = ''
options[:order] = [:month, :year, :day] if options[:month_before_year] # For backwards compatibility
options[:order] ||= [:year, :month, :day]
position = {:year => 1, :month => 2, :day => 3}
discard = {}
discard[:year] = true if options[:discard_year]
discard[:month] = true if options[:discard_month]
discard[:day] = true if options[:discard_day] or options[:discard_month]
options[:order].each do |param|
date_select << self.send("select_#{param}", date, options_with_prefix.call(position[param])) unless discard[param]
end
date_select
end
def to_datetime_select_tag(options = {})
defaults = { :discard_type => true }
options = defaults.merge(options)
options_with_prefix = Proc.new { |position| options.merge(:prefix => "#{@object_name}[#{@method_name}(#{position}i)]") }
datetime = options[:include_blank] ? (value || nil) : (value || Time.now)
datetime_select = select_year(datetime, options_with_prefix.call(1))
datetime_select << select_month(datetime, options_with_prefix.call(2)) unless options[:discard_month]
datetime_select << select_day(datetime, options_with_prefix.call(3)) unless options[:discard_day] || options[:discard_month]
datetime_select << ' — ' + select_hour(datetime, options_with_prefix.call(4)) unless options[:discard_hour]
datetime_select << ' : ' + select_minute(datetime, options_with_prefix.call(5)) unless options[:discard_minute] || options[:discard_hour]
datetime_select
end
end
class FormBuilder
def date_select(method, options = {})
@template.date_select(@object_name, method, options.merge(:object => @object))
end
def datetime_select(method, options = {})
@template.datetime_select(@object_name, method, options.merge(:object => @object))
end
end
end
end
module ActionView
module Helpers
# Provides a set of methods for making it easier to locate problems.
module DebugHelper
# Returns a
-tag set with the +object+ dumped by YAML. Very readable way to inspect an object.
def debug(object)
begin
Marshal::dump(object)
"
#{h(object.to_yaml).gsub(" ", " ")}
"
rescue Object => e
# Object couldn't be dumped, perhaps because of singleton methods -- this is the fallback
"#{h(object.inspect)}"
end
end
end
end
endrequire 'cgi'
require File.dirname(__FILE__) + '/date_helper'
require File.dirname(__FILE__) + '/tag_helper'
module ActionView
module Helpers
# Provides a set of methods for working with forms and especially forms related to objects assigned to the template.
# The following is an example of a complete form for a person object that works for both creates and updates built
# with all the form helpers. The @person object was assigned by an action on the controller:
#
#
# ...is compiled to:
#
#
#
# If the object name contains square brackets the id for the object will be inserted. Example:
#
# <%= text_field "person[]", "name" %>
#
# ...becomes:
#
#
#
# If the helper is being used to generate a repetitive sequence of similar form elements, for example in a partial
# used by render_collection_of_partials, the "index" option may come in handy. Example:
#
# <%= text_field "person", "name", "index" => 1 %>
#
# becomes
#
#
#
# There's also methods for helping to build form tags in link:classes/ActionView/Helpers/FormOptionsHelper.html,
# link:classes/ActionView/Helpers/DateHelper.html, and link:classes/ActionView/Helpers/ActiveRecordHelper.html
module FormHelper
# Creates a form and a scope around a specific model object, which is then used as a base for questioning about
# values for the fields. Examples:
#
# <% form_for :person, @person, :url => { :action => "update" } do |f| %>
# First name: <%= f.text_field :first_name %>
# Last name : <%= f.text_field :last_name %>
# Biography : <%= f.text_area :biography %>
# Admin? : <%= f.check_box :admin %>
# <% end %>
#
# Worth noting is that the form_for tag is called in a ERb evaluation block, not a ERb output block. So that's <% %>,
# not <%= %>. Also worth noting is that the form_for yields a form_builder object, in this example as f, which emulates
# the API for the stand-alone FormHelper methods, but without the object name. So instead of text_field :person, :name,
# you get away with f.text_field :name.
#
# That in itself is a modest increase in comfort. The big news is that form_for allows us to more easily escape the instance
# variable convention, so while the stand-alone approach would require text_field :person, :name, :object => person
# to work with local variables instead of instance ones, the form_for calls remain the same. You simply declare once with
# :person, person and all subsequent field calls save :person and :object => person.
#
# 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. Example:
#
# <% form_for :person, @person, :url => { :action => "update" } 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]", @person.company.admin? %>
# <% end %>
#
# Note: This also works for the methods in FormOptionHelper and DateHelper that are designed to work with an object as base.
# Like collection_select and datetime_select.
#
# Html attributes for the form tag can be given as :html => {...}. Example:
#
# <% form_for :person, @person, :html => {:id => 'person_form'} do |f| %>
# ...
# <% end %>
#
# You can also build forms using a customized FormBuilder class. Subclass FormBuilder and override or define some more helpers,
# then use your custom builder like so:
#
# <% form_for :person, @person, :url => { :action => "update" }, :builder => LabellingFormBuilder do |f| %>
# <%= f.text_field :first_name %>
# <%= f.text_field :last_name %>
# <%= text_area :person, :biography %>
# <%= check_box_tag "person[admin]", @person.company.admin? %>
# <% end %>
#
# In many cases you will want to wrap the above in another helper, such as:
#
# def labelled_form_for(name, object, options, &proc)
# form_for(name, object, options.merge(:builder => LabellingFormBuiler), &proc)
# end
#
def form_for(object_name, *args, &proc)
raise ArgumentError, "Missing block" unless block_given?
options = args.last.is_a?(Hash) ? args.pop : {}
concat(form_tag(options.delete(:url) || {}, options.delete(:html) || {}), proc.binding)
fields_for(object_name, *(args << options), &proc)
concat('', proc.binding)
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. Example:
#
# <% form_for :person, @person, :url => { :action => "update" } 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 %>
# <% end %>
#
# Note: This also works for the methods in FormOptionHelper and DateHelper that are designed to work with an object as base.
# Like collection_select and datetime_select.
def fields_for(object_name, *args, &proc)
raise ArgumentError, "Missing block" unless block_given?
options = args.last.is_a?(Hash) ? args.pop : {}
object = args.first
yield((options[:builder] || FormBuilder).new(object_name, object, self, options, proc))
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+.
#
# Examples (call, result):
# text_field("post", "title", "size" => 20)
#
def text_field(object_name, method, options = {})
InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("text", options)
end
# Works just like text_field, but returns an input tag of the "password" type instead.
def password_field(object_name, method, options = {})
InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("password", options)
end
# Works just like text_field, but returns an input tag of the "hidden" type instead.
def hidden_field(object_name, method, options = {})
InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("hidden", options)
end
# Works just like text_field, but returns an input tag of the "file" type instead, which won't have a default value.
def file_field(object_name, method, options = {})
InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("file", options)
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+.
#
# Example (call, result):
# text_area("post", "body", "cols" => 20, "rows" => 40)
#
def text_area(object_name, method, options = {})
InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_text_area_tag(options)
end
# Returns a checkbox tag tailored for accessing a specified attribute (identified by +method+) on an object
# assigned to the template (identified by +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. Usually unchecked checkboxes don't post anything.
# We work around this problem by adding a hidden value with the same name as the checkbox.
#
# Example (call, result). Imagine that @post.validated? returns 1:
# check_box("post", "validated")
#
#
#
# Example (call, result). Imagine that @puppy.gooddog returns no:
# check_box("puppy", "gooddog", {}, "yes", "no")
#
#
def check_box(object_name, method, options = {}, checked_value = "1", unchecked_value = "0")
InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_check_box_tag(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. Additional options on the input tag can be passed as a
# hash with +options+.
# Example (call, result). Imagine that @post.category returns "rails":
# radio_button("post", "category", "rails")
# radio_button("post", "category", "java")
#
#
#
def radio_button(object_name, method, tag_value, options = {})
InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_radio_button_tag(tag_value, options)
end
end
class InstanceTag #:nodoc:
include Helpers::TagHelper
attr_reader :method_name, :object_name
DEFAULT_FIELD_OPTIONS = { "size" => 30 }.freeze unless const_defined?(:DEFAULT_FIELD_OPTIONS)
DEFAULT_RADIO_OPTIONS = { }.freeze unless const_defined?(:DEFAULT_RADIO_OPTIONS)
DEFAULT_TEXT_AREA_OPTIONS = { "cols" => 40, "rows" => 20 }.freeze unless const_defined?(:DEFAULT_TEXT_AREA_OPTIONS)
DEFAULT_DATE_OPTIONS = { :discard_type => true }.freeze unless const_defined?(:DEFAULT_DATE_OPTIONS)
def initialize(object_name, method_name, template_object, local_binding = nil, object = nil)
@object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup
@template_object, @local_binding = template_object, local_binding
@object = object
if @object_name.sub!(/\[\]$/,"")
@auto_index = @template_object.instance_variable_get("@#{Regexp.last_match.pre_match}").id_before_type_cast
end
end
def to_input_field_tag(field_type, options = {})
options = options.stringify_keys
options["size"] ||= options["maxlength"] || DEFAULT_FIELD_OPTIONS["size"]
options = DEFAULT_FIELD_OPTIONS.merge(options)
if field_type == "hidden"
options.delete("size")
end
options["type"] = field_type
options["value"] ||= value_before_type_cast unless field_type == "file"
add_default_name_and_id(options)
tag("input", options)
end
def to_radio_button_tag(tag_value, options = {})
options = DEFAULT_RADIO_OPTIONS.merge(options.stringify_keys)
options["type"] = "radio"
options["value"] = tag_value
options["checked"] = "checked" if value.to_s == tag_value.to_s
pretty_tag_value = tag_value.to_s.gsub(/\s/, "_").gsub(/\W/, "").downcase
options["id"] = @auto_index ?
"#{@object_name}_#{@auto_index}_#{@method_name}_#{pretty_tag_value}" :
"#{@object_name}_#{@method_name}_#{pretty_tag_value}"
add_default_name_and_id(options)
tag("input", options)
end
def to_text_area_tag(options = {})
options = DEFAULT_TEXT_AREA_OPTIONS.merge(options.stringify_keys)
add_default_name_and_id(options)
content_tag("textarea", html_escape(options.delete('value') || value_before_type_cast), options)
end
def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0")
options = options.stringify_keys
options["type"] = "checkbox"
options["value"] = checked_value
checked = case value
when TrueClass, FalseClass
value
when NilClass
false
when Integer
value != 0
when String
value == checked_value
else
value.to_i != 0
end
if checked || options["checked"] == "checked"
options["checked"] = "checked"
else
options.delete("checked")
end
add_default_name_and_id(options)
tag("input", options) << tag("input", "name" => options["name"], "type" => "hidden", "value" => unchecked_value)
end
def to_date_tag()
defaults = DEFAULT_DATE_OPTIONS.dup
date = value || Date.today
options = Proc.new { |position| defaults.merge(:prefix => "#{@object_name}[#{@method_name}(#{position}i)]") }
html_day_select(date, options.call(3)) +
html_month_select(date, options.call(2)) +
html_year_select(date, options.call(1))
end
def to_boolean_select_tag(options = {})
options = options.stringify_keys
add_default_name_and_id(options)
tag_text = ""
end
def to_content_tag(tag_name, options = {})
content_tag(tag_name, value, options)
end
def object
@object || @template_object.instance_variable_get("@#{@object_name}")
end
def value
unless object.nil?
object.send(@method_name)
end
end
def value_before_type_cast
unless object.nil?
object.respond_to?(@method_name + "_before_type_cast") ?
object.send(@method_name + "_before_type_cast") :
object.send(@method_name)
end
end
private
def add_default_name_and_id(options)
if options.has_key?("index")
options["name"] ||= tag_name_with_index(options["index"])
options["id"] ||= tag_id_with_index(options["index"])
options.delete("index")
elsif @auto_index
options["name"] ||= tag_name_with_index(@auto_index)
options["id"] ||= tag_id_with_index(@auto_index)
else
options["name"] ||= tag_name
options["id"] ||= tag_id
end
end
def tag_name
"#{@object_name}[#{@method_name}]"
end
def tag_name_with_index(index)
"#{@object_name}[#{index}][#{@method_name}]"
end
def tag_id
"#{@object_name}_#{@method_name}"
end
def tag_id_with_index(index)
"#{@object_name}_#{index}_#{@method_name}"
end
end
class FormBuilder #:nodoc:
# The methods which wrap a form helper call.
class_inheritable_accessor :field_helpers
self.field_helpers = (FormHelper.instance_methods - ['form_for'])
attr_accessor :object_name, :object
def initialize(object_name, object, template, options, proc)
@object_name, @object, @template, @options, @proc = object_name, object, template, options, proc
end
(field_helpers - %w(check_box radio_button)).each do |selector|
src = <<-end_src
def #{selector}(method, options = {})
@template.send(#{selector.inspect}, @object_name, method, options.merge(:object => @object))
end
end_src
class_eval src, __FILE__, __LINE__
end
def check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
@template.check_box(@object_name, method, options.merge(:object => @object), checked_value, unchecked_value)
end
def radio_button(method, tag_value, options = {})
@template.radio_button(@object_name, method, tag_value, options.merge(:object => @object))
end
end
end
end
require 'cgi'
require 'erb'
require File.dirname(__FILE__) + '/form_helper'
module ActionView
module Helpers
# Provides a number of methods for turning different kinds of containers into a set of option tags.
# == Options
# The collection_select, country_select, select,
# and time_zone_select methods take an options parameter,
# a hash.
#
# * :include_blank - set to true 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. For example,
#
# select("post", "category", Post::CATEGORIES, {:include_blank => true})
#
# could become:
#
#
#
# * :prompt - 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.
#
# Another common case is a select tag for an belongs_to-associated object. For example,
#
# select("post", "person_id", Person.find_all.collect {|p| [ p.name, p.id ] })
#
# could become:
#
#
module FormOptionsHelper
include ERB::Util
# 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.
# See options_for_select for the required format of the choices parameter.
#
# Example with @post.person_id => 1:
# select("post", "person_id", Person.find_all.collect {|p| [ p.name, p.id ] }, { :include_blank => true })
#
# could become:
#
#
#
# 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, post.person_id is the selected option. Specify :selected => value to use a different selection
# or :selected => nil to leave all options unselected.
def select(object, method, choices, options = {}, html_options = {})
InstanceTag.new(object, method, self, nil, options.delete(:object)).to_select_tag(choices, options, html_options)
end
# Return select and option tags for the given object and method using options_from_collection_for_select to generate the list of option tags.
def collection_select(object, method, collection, value_method, text_method, options = {}, html_options = {})
InstanceTag.new(object, method, self, nil, options.delete(:object)).to_collection_select_tag(collection, value_method, text_method, options, html_options)
end
# Return select and option tags for the given object and method, using country_options_for_select to generate the list of option tags.
def country_select(object, method, priority_countries = nil, options = {}, html_options = {})
InstanceTag.new(object, method, self, nil, options.delete(:object)).to_country_select_tag(priority_countries, options, html_options)
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 :include_blank option documented above,
# this method also supports a :model option, which defaults
# to TimeZone. This may be used by users to specify a different time
# zone model object. (See #time_zone_options_for_select for more
# information.)
def time_zone_select(object, method, priority_zones = nil, options = {}, html_options = {})
InstanceTag.new(object, method, self, nil, options.delete(:object)).to_time_zone_select_tag(priority_zones, options, html_options)
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.
#
# Examples (call, result):
# options_for_select([["Dollar", "$"], ["Kroner", "DKK"]])
# \n
#
# options_for_select([ "VISA", "MasterCard" ], "MasterCard")
# \n
#
# options_for_select({ "Basic" => "$20", "Plus" => "$40" }, "$40")
# \n
#
# options_for_select([ "VISA", "MasterCard", "Discover" ], ["VISA", "Discover"])
# \n\n
#
# 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)
container = container.to_a if Hash === container
options_for_select = container.inject([]) do |options, element|
if !element.is_a?(String) and element.respond_to?(:first) and element.respond_to?(:last)
is_selected = ( (selected.respond_to?(:include?) ? selected.include?(element.last) : element.last == selected) )
is_selected = ( (selected.respond_to?(:include?) && !selected.is_a?(String) ? selected.include?(element.last) : element.last == selected) )
if is_selected
options << ""
else
options << ""
end
else
is_selected = ( (selected.respond_to?(:include?) ? selected.include?(element) : element == selected) )
is_selected = ( (selected.respond_to?(:include?) && !selected.is_a?(String) ? selected.include?(element) : element == selected) )
options << ((is_selected) ? "" : "")
end
end
options_for_select.join("\n")
end
# Returns a string of option tags that have been compiled by iterating over the +collection+ and assigning the
# the result of a call to the +value_method+ as the option value and the +text_method+ as the option text.
# If +selected_value+ is specified, the element returning a match on +value_method+ will get the selected option tag.
#
# Example (call, result). Imagine a loop iterating over each +person+ in @project.people to generate an input tag:
# options_from_collection_for_select(@project.people, "id", "name")
#
#
# NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag.
def options_from_collection_for_select(collection, value_method, text_method, selected_value = nil)
options_for_select(
collection.inject([]) { |options, object| options << [ object.send(text_method), object.send(value_method) ] },
selected_value
)
end
# Returns a string of option tags, like options_from_collection_for_select, but surrounds them with
),
form("post")
)
class << @post
def new_record?() false end
def to_param() id end
def id() 1 end
end
assert_dom_equal(
%(),
form("post")
)
end
def test_form_with_date
def Post.content_columns() [ Column.new(:date, "written_on", "Written on") ] end
assert_dom_equal(
%(),
form("post")
)
end
def test_form_with_datetime
def Post.content_columns() [ Column.new(:datetime, "written_on", "Written on") ] end
@post.written_on = Time.gm(2004, 6, 15, 16, 30)
assert_dom_equal(
%(),
form("post")
)
end
def test_error_for_block
assert_dom_equal %(
1 error prohibited this post from being saved
There were problems with the following fields:
Author name can't be empty
), error_messages_for("post")
assert_equal %(
1 error prohibited this post from being saved
There were problems with the following fields:
Author name can't be empty
), error_messages_for("post", :class => "errorDeathByClass", :id => "errorDeathById", :header_tag => "h1")
end
def test_error_messages_for_handles_nil
assert_equal "", error_messages_for("notthere")
end
def test_form_with_string_multipart
assert_dom_equal(
%(),
form("post", :multipart => true)
)
end
end
require File.dirname(__FILE__) + '/../abstract_unit'
class AssetTagHelperTest < Test::Unit::TestCase
include ActionView::Helpers::TagHelper
include ActionView::Helpers::UrlHelper
include ActionView::Helpers::AssetTagHelper
def setup
@controller = Class.new do
attr_accessor :request
def url_for(options, *parameters_for_method_reference)
"http://www.example.com"
end
end.new
@request = Class.new do
def relative_url_root
""
end
end.new
@controller.request = @request
ActionView::Helpers::AssetTagHelper::reset_javascript_include_default
end
def teardown
Object.send(:remove_const, :RAILS_ROOT) if defined?(RAILS_ROOT)
ENV["RAILS_ASSET_ID"] = nil
end
AutoDiscoveryToTag = {
%(auto_discovery_link_tag) => %(),
%(auto_discovery_link_tag(:atom)) => %(),
%(auto_discovery_link_tag(:rss, :action => "feed")) => %(),
%(auto_discovery_link_tag(:rss, "http://localhost/feed")) => %(),
%(auto_discovery_link_tag(:rss, {:action => "feed"}, {:title => "My RSS"})) => %(),
%(auto_discovery_link_tag(:rss, {}, {:title => "My RSS"})) => %(),
%(auto_discovery_link_tag(nil, {}, {:type => "text/html"})) => %(),
%(auto_discovery_link_tag(nil, {}, {:title => "No stream.. really", :type => "text/html"})) => %(),
%(auto_discovery_link_tag(:rss, {}, {:title => "My RSS", :type => "text/html"})) => %(),
%(auto_discovery_link_tag(:atom, {}, {:rel => "Not so alternate"})) => %(),
}
JavascriptPathToTag = {
%(javascript_path("xmlhr")) => %(/javascripts/xmlhr.js),
%(javascript_path("super/xmlhr")) => %(/javascripts/super/xmlhr.js)
}
JavascriptIncludeToTag = {
%(javascript_include_tag("xmlhr")) => %(),
%(javascript_include_tag("xmlhr", :lang => "vbscript")) => %(),
%(javascript_include_tag("common.javascript", "/elsewhere/cools")) => %(\n),
%(javascript_include_tag(:defaults)) => %(\n\n\n),
%(javascript_include_tag(:defaults, "test")) => %(\n\n\n\n),
%(javascript_include_tag("test", :defaults)) => %(\n\n\n\n)
}
StylePathToTag = {
%(stylesheet_path("style")) => %(/stylesheets/style.css),
%(stylesheet_path('dir/file')) => %(/stylesheets/dir/file.css),
%(stylesheet_path('/dir/file')) => %(/dir/file.css)
}
StyleLinkToTag = {
%(stylesheet_link_tag("style")) => %(),
%(stylesheet_link_tag("/dir/file")) => %(),
%(stylesheet_link_tag("dir/file")) => %(),
%(stylesheet_link_tag("style", :media => "all")) => %(),
%(stylesheet_link_tag("random.styles", "/css/stylish")) => %(\n)
}
ImagePathToTag = {
%(image_path("xml")) => %(/images/xml.png),
}
ImageLinkToTag = {
%(image_tag("xml")) => %(),
%(image_tag("rss", :alt => "rss syndication")) => %(),
%(image_tag("gold", :size => "45x70")) => %(),
%(image_tag("symbolize", "size" => "45x70")) => %(),
%(image_tag("http://www.rubyonrails.com/images/rails")) => %()
}
def test_auto_discovery
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_javascript_include
JavascriptIncludeToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end
def test_register_javascript_include_default
ActionView::Helpers::AssetTagHelper::register_javascript_include_default 'slider'
assert_dom_equal %(\n\n\n\n), javascript_include_tag(:defaults)
ActionView::Helpers::AssetTagHelper::register_javascript_include_default 'lib1', '/elsewhere/blub/lib2'
assert_dom_equal %(\n\n\n\n\n\n), javascript_include_tag(:defaults)
end
def test_style_path
StylePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end
def test_style_link
StyleLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end
def test_image_path
ImagePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end
def test_image_tag
ImageLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end
def test_timebased_asset_id
Object.send(:const_set, :RAILS_ROOT, File.dirname(__FILE__) + "/../fixtures/")
expected_time = File.stat(File.expand_path(File.dirname(__FILE__) + "/../fixtures/public/images/rails.png")).mtime.to_i.to_s
assert_equal %(), image_tag("rails.png")
end
def test_skipping_asset_id_on_complete_url
Object.send(:const_set, :RAILS_ROOT, File.dirname(__FILE__) + "/../fixtures/")
assert_equal %(), image_tag("http://www.example.com/rails.png")
end
def test_preset_asset_id
Object.send(:const_set, :RAILS_ROOT, File.dirname(__FILE__) + "/../fixtures/")
ENV["RAILS_ASSET_ID"] = "4500"
assert_equal %(), image_tag("rails.png")
end
end
class AssetTagHelperNonVhostTest < Test::Unit::TestCase
include ActionView::Helpers::TagHelper
include ActionView::Helpers::UrlHelper
include ActionView::Helpers::AssetTagHelper
def setup
@controller = Class.new do
attr_accessor :request
def url_for(options, *parameters_for_method_reference)
"http://www.example.com/calloboration/hieraki"
end
end.new
@request = Class.new do
def relative_url_root
"/calloboration/hieraki"
end
end.new
@controller.request = @request
ActionView::Helpers::AssetTagHelper::reset_javascript_include_default
end
AutoDiscoveryToTag = {
%(auto_discovery_link_tag(:rss, :action => "feed")) => %(),
%(auto_discovery_link_tag(:atom)) => %(),
%(auto_discovery_link_tag) => %(),
}
JavascriptPathToTag = {
%(javascript_path("xmlhr")) => %(/calloboration/hieraki/javascripts/xmlhr.js),
}
JavascriptIncludeToTag = {
%(javascript_include_tag("xmlhr")) => %(),
%(javascript_include_tag("common.javascript", "/elsewhere/cools")) => %(\n),
%(javascript_include_tag(:defaults)) => %(\n\n\n)
}
StylePathToTag = {
%(stylesheet_path("style")) => %(/calloboration/hieraki/stylesheets/style.css),
}
StyleLinkToTag = {
%(stylesheet_link_tag("style")) => %(),
%(stylesheet_link_tag("random.styles", "/css/stylish")) => %(\n)
}
ImagePathToTag = {
%(image_path("xml")) => %(/calloboration/hieraki/images/xml.png),
}
ImageLinkToTag = {
%(image_tag("xml")) => %(),
%(image_tag("rss", :alt => "rss syndication")) => %(),
%(image_tag("gold", :size => "45x70")) => %(),
%(image_tag("http://www.example.com/images/icon.gif")) => %(),
%(image_tag("symbolize", "size" => "45x70")) => %()
}
def test_auto_discovery
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_javascript_include
JavascriptIncludeToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end
def test_register_javascript_include_default
ActionView::Helpers::AssetTagHelper::register_javascript_include_default 'slider'
assert_dom_equal %(\n\n\n\n), javascript_include_tag(:defaults)
ActionView::Helpers::AssetTagHelper::register_javascript_include_default 'lib1', '/elsewhere/blub/lib2'
assert_dom_equal %(\n\n\n\n\n\n), javascript_include_tag(:defaults)
end
def test_style_path
StylePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end
def test_style_link
StyleLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end
def test_image_path
ImagePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end
def test_image_tag
ImageLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
# Assigning a default alt tag should not cause an exception to be raised
assert_nothing_raised { image_tag('') }
end
def test_stylesheet_with_asset_host_already_encoded
ActionController::Base.asset_host = "http://foo.example.com"
result = stylesheet_link_tag("http://bar.example.com/stylesheets/style.css")
assert_dom_equal(
%(),
result)
ensure
ActionController::Base.asset_host = ""
end
end
require 'test/unit'
require File.dirname(__FILE__) + '/../../lib/action_view/helpers/benchmark_helper'
class BenchmarkHelperTest < Test::Unit::TestCase
include ActionView::Helpers::BenchmarkHelper
class MockLogger
attr_reader :logged
def initialize
@logged = []
end
def method_missing(method, *args)
@logged << [method, args]
end
end
def setup
@logger = MockLogger.new
end
def test_without_logger_or_block
@logger = nil
assert_nothing_raised { benchmark }
end
def test_without_block
assert_raise(LocalJumpError) { benchmark }
assert @logger.logged.empty?
end
def test_without_logger
@logger = nil
i_was_run = false
benchmark { i_was_run = true }
assert !i_was_run
end
def test_defaults
i_was_run = false
benchmark { i_was_run = true }
assert i_was_run
assert 1, @logger.logged.size
assert_last_logged
end
def test_with_message
i_was_run = false
benchmark('test_run') { i_was_run = true }
assert i_was_run
assert 1, @logger.logged.size
assert_last_logged 'test_run'
end
def test_with_message_and_level
i_was_run = false
benchmark('debug_run', :debug) { i_was_run = true }
assert i_was_run
assert 1, @logger.logged.size
assert_last_logged 'debug_run', :debug
end
private
def assert_last_logged(message = 'Benchmarking', level = :info)
last = @logger.logged.last
assert 2, last.size
assert_equal level, last.first
assert 1, last[1].size
assert last[1][0] =~ /^#{message} \(.*\)$/
end
end
require 'test/unit'
require File.dirname(__FILE__) + '/../../lib/action_view/helpers/date_helper'
require File.dirname(__FILE__) + "/../abstract_unit"
class CompiledTemplateTests < Test::Unit::TestCase
def setup
@ct = ActionView::CompiledTemplates.new
@v = Class.new
@v.send :include, @ct
@a = './test_compile_template_a.rhtml'
@b = './test_compile_template_b.rhtml'
@s = './test_compile_template_link.rhtml'
end
def teardown
[@a, @b, @s].each do |f|
`rm #{f}` if File.exist?(f) || File.symlink?(f)
end
end
attr_reader :ct, :v
def test_name_allocation
hi_world = ct.method_names['hi world']
hi_sexy = ct.method_names['hi sexy']
wish_upon_a_star = ct.method_names['I love seeing decent error messages']
assert_equal hi_world, ct.method_names['hi world']
assert_equal hi_sexy, ct.method_names['hi sexy']
assert_equal wish_upon_a_star, ct.method_names['I love seeing decent error messages']
assert_equal 3, [hi_world, hi_sexy, wish_upon_a_star].uniq.length
end
def test_wrap_source
assert_equal(
"def aliased_assignment(value)\nself.value = value\nend",
@ct.wrap_source(:aliased_assignment, [:value], 'self.value = value')
)
assert_equal(
"def simple()\nnil\nend",
@ct.wrap_source(:simple, [], 'nil')
)
end
def test_compile_source_single_method
selector = ct.compile_source('doubling method', [:a], 'a + a')
assert_equal 2, @v.new.send(selector, 1)
assert_equal 4, @v.new.send(selector, 2)
assert_equal -4, @v.new.send(selector, -2)
assert_equal 0, @v.new.send(selector, 0)
selector
end
def test_compile_source_two_method
sel1 = test_compile_source_single_method # compile the method in the other test
sel2 = ct.compile_source('doubling method', [:a, :b], 'a + b + a + b')
assert_not_equal sel1, sel2
assert_equal 2, @v.new.send(sel1, 1)
assert_equal 4, @v.new.send(sel1, 2)
assert_equal 6, @v.new.send(sel2, 1, 2)
assert_equal 32, @v.new.send(sel2, 15, 1)
end
def test_mtime
t1 = Time.now
test_compile_source_single_method
assert (t1..Time.now).include?(ct.mtime('doubling method', [:a]))
end
def test_compile_time
`echo '#{@a}' > #{@a}; echo '#{@b}' > #{@b}; ln -s #{@a} #{@s}`
v = ActionView::Base.new
v.base_path = '.'
v.cache_template_loading = false;
sleep 1
t = Time.now
v.compile_and_render_template(:rhtml, '', @a)
v.compile_and_render_template(:rhtml, '', @b)
v.compile_and_render_template(:rhtml, '', @s)
a_n = v.method_names[@a]
b_n = v.method_names[@b]
s_n = v.method_names[@s]
# all of the files have changed since last compile
assert v.compile_time[a_n] > t
assert v.compile_time[b_n] > t
assert v.compile_time[s_n] > t
sleep 1
t = Time.now
v.compile_and_render_template(:rhtml, '', @a)
v.compile_and_render_template(:rhtml, '', @b)
v.compile_and_render_template(:rhtml, '', @s)
# none of the files have changed since last compile
assert v.compile_time[a_n] < t
assert v.compile_time[b_n] < t
assert v.compile_time[s_n] < t
`rm #{@s}; ln -s #{@b} #{@s}`
v.compile_and_render_template(:rhtml, '', @a)
v.compile_and_render_template(:rhtml, '', @b)
v.compile_and_render_template(:rhtml, '', @s)
# the symlink has changed since last compile
assert v.compile_time[a_n] < t
assert v.compile_time[b_n] < t
assert v.compile_time[s_n] > t
sleep 1
`touch #{@b}`
t = Time.now
v.compile_and_render_template(:rhtml, '', @a)
v.compile_and_render_template(:rhtml, '', @b)
v.compile_and_render_template(:rhtml, '', @s)
# the file at the end of the symlink has changed since last compile
# both the symlink and the file at the end of it should be recompiled
assert v.compile_time[a_n] < t
assert v.compile_time[b_n] > t
assert v.compile_time[s_n] > t
end
end
module ActionView
class Base
def compile_time
@@compile_time
end
def method_names
@@method_names
end
end
end
require 'test/unit'
require File.dirname(__FILE__) + "/../abstract_unit"
class DateHelperTest < Test::Unit::TestCase
include ActionView::Helpers::DateHelper
include ActionView::Helpers::FormHelper
silence_warnings do
Post = Struct.new("Post", :written_on, :updated_at)
end
def test_distance_in_words
from = Time.mktime(2004, 3, 6, 21, 41, 18)
assert_equal "less than a minute", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 41, 25))
assert_equal "5 minutes", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 46, 25))
assert_equal "about 1 hour", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 22, 47, 25))
assert_equal "about 3 hours", distance_of_time_in_words(from, Time.mktime(2004, 3, 7, 0, 41))
assert_equal "about 4 hours", distance_of_time_in_words(from, Time.mktime(2004, 3, 7, 1, 20))
assert_equal "2 days", distance_of_time_in_words(from, Time.mktime(2004, 3, 9, 15, 40))
# include seconds
assert_equal "less than a minute", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 41, 19), false)
assert_equal "less than 5 seconds", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 41, 19), true)
assert_equal "less than 10 seconds", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 41, 28), true)
assert_equal "less than 20 seconds", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 41, 38), true)
assert_equal "half a minute", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 41, 48), true)
assert_equal "less than a minute", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 42, 17), true)
assert_equal "1 minute", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 42, 18), true)
assert_equal "1 minute", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 42, 28), true)
assert_equal "2 minutes", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 42, 48), true)
# test to < from
assert_equal "about 4 hours", distance_of_time_in_words(Time.mktime(2004, 3, 7, 1, 20), from)
assert_equal "less than 20 seconds", distance_of_time_in_words(Time.mktime(2004, 3, 6, 21, 41, 38), from, true)
# test with integers
assert_equal "less than a minute", distance_of_time_in_words(50)
assert_equal "about 1 hour", distance_of_time_in_words(60*60)
# more cumbersome test with integers
assert_equal "less than a minute", distance_of_time_in_words(0, 50)
assert_equal "about 1 hour", distance_of_time_in_words(60*60, 0)
end
def test_distance_in_words_date
start_date = Date.new 1975, 1, 31
end_date = Date.new 1977, 4, 17
assert_not_equal("13 minutes",
distance_of_time_in_words(start_date, end_date))
end
def test_select_day
expected = %(\n"
assert_equal expected, select_day(Time.mktime(2003, 8, 16))
assert_equal expected, select_day(16)
end
def test_select_day_with_blank
expected = %(\n"
assert_equal expected, select_day(Time.mktime(2003, 8, 16), :include_blank => true)
assert_equal expected, select_day(16, :include_blank => true)
end
def test_select_day_nil_with_blank
expected = %(\n"
assert_equal expected, select_day(nil, :include_blank => true)
end
def test_select_month
expected = %(\n"
assert_equal expected, select_month(Time.mktime(2003, 8, 16))
assert_equal expected, select_month(8)
end
def test_select_month_with_disabled
expected = %(\n"
assert_equal expected, select_month(Time.mktime(2003, 8, 16), :disabled => true)
assert_equal expected, select_month(8, :disabled => true)
end
def test_select_month_with_field_name_override
expected = %(\n"
assert_equal expected, select_month(Time.mktime(2003, 8, 16), :field_name => 'mois')
assert_equal expected, select_month(8, :field_name => 'mois')
end
def test_select_month_with_blank
expected = %(\n"
assert_equal expected, select_month(Time.mktime(2003, 8, 16), :include_blank => true)
assert_equal expected, select_month(8, :include_blank => true)
end
def test_select_month_nil_with_blank
expected = %(\n"
assert_equal expected, select_month(nil, :include_blank => true)
end
def test_select_month_with_numbers
expected = %(\n"
assert_equal expected, select_month(Time.mktime(2003, 8, 16), :use_month_numbers => true)
assert_equal expected, select_month(8, :use_month_numbers => true)
end
def test_select_month_with_numbers_and_names
expected = %(\n"
assert_equal expected, select_month(Time.mktime(2003, 8, 16), :add_month_numbers => true)
assert_equal expected, select_month(8, :add_month_numbers => true)
end
def test_select_month_with_numbers_and_names_with_abbv
expected = %(\n"
assert_equal expected, select_month(Time.mktime(2003, 8, 16), :add_month_numbers => true, :use_short_month => true)
assert_equal expected, select_month(8, :add_month_numbers => true, :use_short_month => true)
end
def test_select_month_with_abbv
expected = %(\n"
assert_equal expected, select_month(Time.mktime(2003, 8, 16), :use_short_month => true)
assert_equal expected, select_month(8, :use_short_month => true)
end
def test_select_year
expected = %(\n"
assert_equal expected, select_year(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005)
assert_equal expected, select_year(2003, :start_year => 2003, :end_year => 2005)
end
def test_select_year_with_disabled
expected = %(\n"
assert_equal expected, select_year(Time.mktime(2003, 8, 16), :disabled => true, :start_year => 2003, :end_year => 2005)
assert_equal expected, select_year(2003, :disabled => true, :start_year => 2003, :end_year => 2005)
end
def test_select_year_with_field_name_override
expected = %(\n"
assert_equal expected, select_year(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :field_name => 'annee')
assert_equal expected, select_year(2003, :start_year => 2003, :end_year => 2005, :field_name => 'annee')
end
def test_select_year_with_type_discarding
expected = %(\n"
assert_equal expected, select_year(
Time.mktime(2003, 8, 16), :prefix => "date_year", :discard_type => true, :start_year => 2003, :end_year => 2005)
assert_equal expected, select_year(
2003, :prefix => "date_year", :discard_type => true, :start_year => 2003, :end_year => 2005)
end
def test_select_year_descending
expected = %(\n"
assert_equal expected, select_year(Time.mktime(2005, 8, 16), :start_year => 2005, :end_year => 2003)
assert_equal expected, select_year(2005, :start_year => 2005, :end_year => 2003)
end
def test_select_hour
expected = %(\n"
assert_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18))
end
def test_select_hour_with_disabled
expected = %(\n"
assert_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :disabled => true)
end
def test_select_hour_with_field_name_override
expected = %(\n"
assert_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :field_name => 'heure')
end
def test_select_hour_with_blank
expected = %(\n"
assert_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :include_blank => true)
end
def test_select_hour_nil_with_blank
expected = %(\n"
assert_equal expected, select_hour(nil, :include_blank => true)
end
def test_select_minute
expected = %(\n"
assert_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18))
end
def test_select_minute_with_disabled
expected = %(\n"
assert_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), :disabled => true)
end
def test_select_minute_with_field_name_override
expected = %(\n"
assert_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), :field_name => 'minuto')
end
def test_select_minute_with_blank
expected = %(\n"
assert_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 = %(\n"
assert_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 = %(\n"
assert_equal expected, select_minute(nil, :include_blank => true)
end
def test_select_minute_nil_with_blank_and_step
expected = %(\n"
assert_equal expected, select_minute(nil, { :include_blank => true , :minute_step => 15 })
end
def test_select_second
expected = %(\n"
assert_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18))
end
def test_select_second_with_disabled
expected = %(\n"
assert_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), :disabled => true)
end
def test_select_second_with_field_name_override
expected = %(\n"
assert_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), :field_name => 'segundo')
end
def test_select_second_with_blank
expected = %(\n"
assert_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), :include_blank => true)
end
def test_select_second_nil_with_blank
expected = %(\n"
assert_equal expected, select_second(nil, :include_blank => true)
end
def test_select_date
expected = %(\n"
expected << %(\n"
expected << %(\n"
assert_equal expected, select_date(
Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :prefix => "date[first]"
)
end
def test_select_date_with_disabled
expected = %(\n"
expected << %(\n"
expected << %(\n"
assert_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 = %(\n"
expected << %(\n"
expected << %(\n"
assert_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 = %(\n"
expected << %(\n"
expected << %(\n"
assert_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 = %(\n"
expected << %(\n"
expected << %(\n"
assert_equal expected, select_date(
Time.mktime(Date.today.year, 8, 16), :prefix => "date[first]"
)
end
def test_select_time_with_seconds
expected = %(\n"
expected << %(\n"
expected << %(\n"
assert_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :include_seconds => true)
end
def test_select_time_without_seconds
expected = %(\n"
expected << %(\n"
assert_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18))
assert_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :include_seconds => false)
end
def test_date_select_with_zero_value
expected = %(\n"
expected << %(\n"
expected << %(\n"
assert_equal expected, select_date(0, :start_year => 2003, :end_year => 2005, :prefix => "date[first]")
end
def test_date_select_within_fields_for
@post = Post.new
@post.written_on = Date.new(2004, 6, 15)
_erbout = ''
fields_for :post, @post do |f|
_erbout.concat f.date_select(:written_on)
end
expected = "\n" +
"\n" +
"\n"
assert_dom_equal(expected, _erbout)
end
def test_datetime_select_within_fields_for
@post = Post.new
@post.updated_at = Time.local(2004, 6, 15, 16, 35)
_erbout = ''
fields_for :post, @post do |f|
_erbout.concat f.datetime_select(:updated_at)
end
expected = "\n\n\n — \n : \n"
assert_dom_equal(expected, _erbout)
end
def test_date_select_with_zero_value_and_no_start_year
expected = %(\n"
expected << %(\n"
expected << %(\n"
assert_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 = %(\n"
expected << %(\n"
expected << %(\n"
assert_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 = %(\n"
expected << %(\n"
expected << %(\n"
assert_equal expected, select_date(0, :prefix => "date[first]")
end
def test_date_select_with_nil_value_and_no_start_and_end_year
expected = %(\n"
expected << %(\n"
expected << %(\n"
assert_equal expected, select_date(nil, :prefix => "date[first]")
end
def test_datetime_select_with_nil_value_and_no_start_and_end_year
expected = %(\n"
expected << %(\n"
expected << %(\n"
expected << %(\n"
expected << %(\n"
assert_equal expected, select_datetime(nil, :prefix => "date[first]")
end
end
require File.dirname(__FILE__) + '/../abstract_unit'
class FormHelperTest < Test::Unit::TestCase
include ActionView::Helpers::FormHelper
include ActionView::Helpers::FormTagHelper
include ActionView::Helpers::UrlHelper
include ActionView::Helpers::TagHelper
include ActionView::Helpers::TextHelper
silence_warnings do
Post = Struct.new("Post", :title, :author_name, :body, :secret, :written_on, :cost)
Post.class_eval do
alias_method :title_before_type_cast, :title unless respond_to?(:title_before_type_cast)
alias_method :body_before_type_cast, :body unless respond_to?(:body_before_type_cast)
alias_method :author_name_before_type_cast, :author_name unless respond_to?(:author_name_before_type_cast)
end
end
def setup
@post = Post.new
def @post.errors() Class.new{ def on(field) field == "author_name" end }.new end
def @post.id; 123; end
def @post.id_before_type_cast; 123; end
@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)
@controller = Class.new do
attr_reader :url_for_options
def url_for(options, *parameters_for_method_reference)
@url_for_options = options
"http://www.example.com"
end
end
@controller = @controller.new
end
def test_text_field
assert_dom_equal(
'', text_field("post", "title")
)
assert_dom_equal(
'', password_field("post", "title")
)
assert_dom_equal(
'', password_field("person", "name")
)
end
def test_text_field_with_escapes
@post.title = "Hello World"
assert_dom_equal(
'', text_field("post", "title")
)
end
def test_text_field_with_options
expected = ''
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 = ''
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_doesnt_change_param_values
object_name = 'post[]'
expected = ''
assert_equal expected, text_field(object_name, "title")
assert_equal object_name, "post[]"
end
def test_check_box
assert_dom_equal(
'',
check_box("post", "secret")
)
@post.secret = 0
assert_dom_equal(
'',
check_box("post", "secret")
)
assert_dom_equal(
'',
check_box("post", "secret" ,{"checked"=>"checked"})
)
@post.secret = true
assert_dom_equal(
'',
check_box("post", "secret")
)
end
def test_check_box_with_explicit_checked_and_unchecked_values
@post.secret = "on"
assert_dom_equal(
'',
check_box("post", "secret", {}, "on", "off")
)
end
def test_radio_button
assert_dom_equal('',
radio_button("post", "title", "Hello World")
)
assert_dom_equal('',
radio_button("post", "title", "Goodbye World")
)
end
def test_radio_button_is_checked_with_integers
assert_dom_equal('',
radio_button("post", "secret", "1")
)
end
def test_text_area
assert_dom_equal(
'',
text_area("post", "body")
)
end
def test_text_area_with_escapes
@post.body = "Back to the hill and over it again!"
assert_dom_equal(
'',
text_area("post", "body")
)
end
def test_text_area_with_alternate_value
assert_dom_equal(
'',
text_area("post", "body", :value => 'Testing alternate values.')
)
end
def test_date_selects
assert_dom_equal(
'',
text_area("post", "body")
)
end
def test_explicit_name
assert_dom_equal(
'', text_field("post", "title", "name" => "dont guess")
)
assert_dom_equal(
'',
text_area("post", "body", "name" => "really!")
)
assert_dom_equal(
'',
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(
'', text_field("post", "title", "id" => "dont guess")
)
assert_dom_equal(
'',
text_area("post", "body", "id" => "really!")
)
assert_dom_equal(
'',
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_auto_index
pid = @post.id
assert_dom_equal(
"", text_field("post[]","title")
)
assert_dom_equal(
"",
text_area("post[]", "body")
)
assert_dom_equal(
"",
check_box("post[]", "secret")
)
assert_dom_equal(
"",
radio_button("post[]", "title", "Hello World")
)
assert_dom_equal("",
radio_button("post[]", "title", "Goodbye World")
)
end
def test_form_for
_erbout = ''
form_for(:post, @post, :html => { :id => 'create-post' }) do |f|
_erbout.concat f.text_field(:title)
_erbout.concat f.text_area(:body)
_erbout.concat f.check_box(:secret)
end
expected =
""
assert_dom_equal expected, _erbout
end
def test_form_for_without_object
_erbout = ''
form_for(:post, :html => { :id => 'create-post' }) do |f|
_erbout.concat f.text_field(:title)
_erbout.concat f.text_area(:body)
_erbout.concat f.check_box(:secret)
end
expected =
""
assert_dom_equal expected, _erbout
end
def test_fields_for
_erbout = ''
fields_for(:post, @post) do |f|
_erbout.concat f.text_field(:title)
_erbout.concat f.text_area(:body)
_erbout.concat f.check_box(:secret)
end
expected =
"" +
"" +
"" +
""
assert_dom_equal expected, _erbout
end
def test_fields_for_without_object
_erbout = ''
fields_for(:post) do |f|
_erbout.concat f.text_field(:title)
_erbout.concat f.text_area(:body)
_erbout.concat f.check_box(:secret)
end
expected =
"" +
"" +
"" +
""
assert_dom_equal expected, _erbout
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
_erbout = ''
form_for(:post, @post, :html => { :id => 'create-post' }) do |post_form|
_erbout.concat post_form.text_field(:title)
_erbout.concat post_form.text_area(:body)
fields_for(:parent_post, @post) do |parent_fields|
_erbout.concat parent_fields.check_box(:secret)
end
end
expected =
""
assert_dom_equal expected, _erbout
end
class LabelledFormBuilder < ActionView::Helpers::FormBuilder
(field_helpers - %w(hidden_field)).each do |selector|
src = <<-END_SRC
def #{selector}(field, *args, &proc)
" " + super + " "
end
END_SRC
class_eval src, __FILE__, __LINE__
end
end
def test_form_for_with_labelled_builder
_erbout = ''
form_for(:post, @post, :builder => LabelledFormBuilder) do |f|
_erbout.concat f.text_field(:title)
_erbout.concat f.text_area(:body)
_erbout.concat f.check_box(:secret)
end
expected =
""
assert_dom_equal expected, _erbout
end
# Perhaps this test should be moved to prototype helper tests.
def test_remote_form_for_with_labelled_builder
self.extend ActionView::Helpers::PrototypeHelper
_erbout = ''
remote_form_for(:post, @post, :builder => LabelledFormBuilder) do |f|
_erbout.concat f.text_field(:title)
_erbout.concat f.text_area(:body)
_erbout.concat f.check_box(:secret)
end
expected =
%("
assert_dom_equal expected, _erbout
end
def test_fields_for_with_labelled_builder
_erbout = ''
fields_for(:post, @post, :builder => LabelledFormBuilder) do |f|
_erbout.concat f.text_field(:title)
_erbout.concat f.text_area(:body)
_erbout.concat f.check_box(:secret)
end
expected =
" " +
" " +
" " +
" "
assert_dom_equal expected, _erbout
end
def test_form_for_with_html_options_adds_options_to_form_tag
_erbout = ''
form_for(:post, @post, :html => {:id => 'some_form', :class => 'some_class'}) do |f| end
expected = ""
assert_dom_equal expected, _erbout
end
def test_form_for_with_string_url_option
_erbout = ''
form_for(:post, @post, :url => 'http://www.otherdomain.com') do |f| end
assert_equal 'http://www.otherdomain.com', @controller.url_for_options
end
def test_form_for_with_hash_url_option
_erbout = ''
form_for(:post, @post, :url => {:controller => 'controller', :action => 'action'}) do |f| end
assert_equal 'controller', @controller.url_for_options[:controller]
assert_equal 'action', @controller.url_for_options[:action]
end
def test_remote_form_for_with_html_options_adds_options_to_form_tag
self.extend ActionView::Helpers::PrototypeHelper
_erbout = ''
remote_form_for(:post, @post, :html => {:id => 'some_form', :class => 'some_class'}) do |f| end
expected = ""
assert_dom_equal expected, _erbout
end
end
require File.dirname(__FILE__) + '/../abstract_unit'
class MockTimeZone
attr_reader :name
def initialize( name )
@name = name
end
def self.all
[ "A", "B", "C", "D", "E" ].map { |s| new s }
end
def ==( z )
z && @name == z.name
end
def to_s
@name
end
end
ActionView::Helpers::FormOptionsHelper::TimeZone = MockTimeZone
class FormOptionsHelperTest < Test::Unit::TestCase
include ActionView::Helpers::FormHelper
include ActionView::Helpers::FormOptionsHelper
silence_warnings do
Post = Struct.new('Post', :title, :author_name, :body, :secret, :written_on, :category, :origin)
Continent = Struct.new('Continent', :continent_name, :countries)
Country = Struct.new('Country', :country_id, :country_name)
Firm = Struct.new('Firm', :time_zone)
end
def test_collection_options
@posts = [
Post.new(" went home", "", "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!")
]
assert_dom_equal(
"\n\n",
options_from_collection_for_select(@posts, "author_name", "title")
)
end
def test_collection_options_with_preselected_value
@posts = [
Post.new(" went home", "", "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!")
]
assert_dom_equal(
"\n\n",
options_from_collection_for_select(@posts, "author_name", "title", "Babe")
)
end
def test_collection_options_with_preselected_value_array
@posts = [
Post.new(" went home", "", "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!")
]
assert_dom_equal(
"\n\n",
options_from_collection_for_select(@posts, "author_name", "title", [ "Babe", "Cabe" ])
)
end
def test_array_options_for_select
assert_dom_equal(
"\n\n",
options_for_select([ "", "USA", "Sweden" ])
)
end
def test_array_options_for_select_with_selection
assert_dom_equal(
"\n\n",
options_for_select([ "Denmark", "", "Sweden" ], "")
)
end
def test_array_options_for_select_with_selection_array
assert_dom_equal(
"\n\n",
options_for_select([ "Denmark", "", "Sweden" ], [ "", "Sweden" ])
)
end
def test_array_options_for_string_include_in_other_string_bug_fix
assert_dom_equal(
"\n",
options_for_select([ "ruby", "rubyonrails" ], "rubyonrails")
)
assert_dom_equal(
"\n",
options_for_select([ "ruby", "rubyonrails" ], "ruby")
)
end
def test_hash_options_for_select
assert_dom_equal(
"\n",
options_for_select({ "$" => "Dollar", "" => "" })
)
assert_dom_equal(
"\n",
options_for_select({ "$" => "Dollar", "" => "" }, "Dollar")
)
assert_dom_equal(
"\n",
options_for_select({ "$" => "Dollar", "" => "" }, [ "Dollar", "" ])
)
end
def test_ducktyped_options_for_select
quack = Struct.new(:first, :last)
assert_dom_equal(
"\n",
options_for_select([quack.new("", ""), quack.new("$", "Dollar")])
)
assert_dom_equal(
"\n",
options_for_select([quack.new("", ""), quack.new("$", "Dollar")], "Dollar")
)
assert_dom_equal(
"\n",
options_for_select([quack.new("", ""), quack.new("$", "Dollar")], ["Dollar", ""])
)
end
def test_html_option_groups_from_collection
@continents = [
Continent.new("", [Country.new("", ""), Country.new("so", "Somalia")] ),
Continent.new("Europe", [Country.new("dk", "Denmark"), Country.new("ie", "Ireland")] )
]
assert_dom_equal(
"
\n
\n
",
option_groups_from_collection_for_select(@continents, "countries", "continent_name", "country_id", "country_name", "dk")
)
end
def test_time_zone_options_no_parms
opts = time_zone_options_for_select
assert_dom_equal "\n" +
"\n" +
"\n" +
"\n" +
"",
opts
end
def test_time_zone_options_with_selected
opts = time_zone_options_for_select( "D" )
assert_dom_equal "\n" +
"\n" +
"\n" +
"\n" +
"",
opts
end
def test_time_zone_options_with_unknown_selected
opts = time_zone_options_for_select( "K" )
assert_dom_equal "\n" +
"\n" +
"\n" +
"\n" +
"",
opts
end
def test_time_zone_options_with_priority_zones
zones = [ TimeZone.new( "B" ), TimeZone.new( "E" ) ]
opts = time_zone_options_for_select( nil, zones )
assert_dom_equal "\n" +
"" +
"\n" +
"\n" +
"\n" +
"",
opts
end
def test_time_zone_options_with_selected_priority_zones
zones = [ TimeZone.new( "B" ), TimeZone.new( "E" ) ]
opts = time_zone_options_for_select( "E", zones )
assert_dom_equal "\n" +
"" +
"\n" +
"\n" +
"\n" +
"",
opts
end
def test_time_zone_options_with_unselected_priority_zones
zones = [ TimeZone.new( "B" ), TimeZone.new( "E" ) ]
opts = time_zone_options_for_select( "C", zones )
assert_dom_equal "\n" +
"" +
"\n" +
"\n" +
"\n" +
"",
opts
end
def test_select
@post = Post.new
@post.category = ""
assert_dom_equal(
"",
select("post", "category", %w( abe hest))
)
end
def test_select_under_fields_for
@post = Post.new
@post.category = ""
_erbout = ''
fields_for :post, @post do |f|
_erbout.concat f.select(:category, %w( abe hest))
end
assert_dom_equal(
"",
_erbout
)
end
def test_select_with_blank
@post = Post.new
@post.category = ""
assert_dom_equal(
"",
select("post", "category", %w( abe hest), :include_blank => true)
)
end
def test_select_with_default_prompt
@post = Post.new
@post.category = ""
assert_dom_equal(
"",
select("post", "category", %w( abe hest), :prompt => true)
)
end
def test_select_no_prompt_when_select_has_value
@post = Post.new
@post.category = ""
assert_dom_equal(
"",
select("post", "category", %w( abe hest), :prompt => true)
)
end
def test_select_with_given_prompt
@post = Post.new
@post.category = ""
assert_dom_equal(
"",
select("post", "category", %w( abe hest), :prompt => 'The prompt')
)
end
def test_select_with_prompt_and_blank
@post = Post.new
@post.category = ""
assert_dom_equal(
"",
select("post", "category", %w( abe hest), :prompt => true, :include_blank => true)
)
end
def test_select_with_selected_value
@post = Post.new
@post.category = ""
assert_dom_equal(
"",
select("post", "category", %w( abe hest ), :selected => 'abe')
)
end
def test_select_with_selected_nil
@post = Post.new
@post.category = ""
assert_dom_equal(
"",
select("post", "category", %w( abe hest ), :selected => nil)
)
end
def test_collection_select
@posts = [
Post.new(" went home", "", "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!")
]
@post = Post.new
@post.author_name = "Babe"
assert_dom_equal(
"",
collection_select("post", "author_name", @posts, "author_name", "author_name")
)
end
def test_collection_select_under_fields_for
@posts = [
Post.new(" went home", "", "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!")
]
@post = Post.new
@post.author_name = "Babe"
_erbout = ''
fields_for :post, @post do |f|
_erbout.concat f.collection_select(:author_name, @posts, :author_name, :author_name)
end
assert_dom_equal(
"",
_erbout
)
end
def test_collection_select_with_blank_and_style
@posts = [
Post.new(" went home", "", "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!")
]
@post = Post.new
@post.author_name = "Babe"
assert_dom_equal(
"",
collection_select("post", "author_name", @posts, "author_name", "author_name", { :include_blank => true }, "style" => "width: 200px")
)
end
def test_country_select
@post = Post.new
@post.origin = "Denmark"
assert_dom_equal(
"",
country_select("post", "origin")
)
end
def test_time_zone_select
@firm = Firm.new("D")
html = time_zone_select( "firm", "time_zone" )
assert_dom_equal "",
html
end
def test_time_zone_select_under_fields_for
@firm = Firm.new("D")
_erbout = ''
fields_for :firm, @firm do |f|
_erbout.concat f.time_zone_select(:time_zone)
end
assert_dom_equal(
"",
_erbout
)
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 "",
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 "",
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 "",
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_priority_zones
@firm = Firm.new("D")
zones = [ TimeZone.new("A"), TimeZone.new("D") ]
html = time_zone_select("firm", "time_zone", zones )
assert_dom_equal "",
html
end
end
require File.dirname(__FILE__) + '/../abstract_unit'
class FormTagHelperTest < Test::Unit::TestCase
include ActionView::Helpers::UrlHelper
include ActionView::Helpers::TagHelper
include ActionView::Helpers::FormTagHelper
def setup
@controller = Class.new do
def url_for(options, *parameters_for_method_reference)
"http://www.example.com"
end
end
@controller = @controller.new
end
def test_check_box_tag
actual = check_box_tag "admin"
expected = %()
assert_dom_equal expected, actual
end
def test_form_tag
actual = form_tag
expected = %(