TeX and CSS tweaks.

Sync with latest Instiki Trunk
(Updates Rails to 1.2.2)
This commit is contained in:
Jacques Distler 2007-02-09 02:04:31 -06:00
parent 0ac586ee25
commit c358389f25
443 changed files with 24218 additions and 9823 deletions

View file

@ -23,7 +23,7 @@ class Web < ActiveRecord::Base
'FROM revisions r ' + 'FROM revisions r ' +
'JOIN pages p ON p.id = r.page_id ' + 'JOIN pages p ON p.id = r.page_id ' +
'WHERE p.web_id = ' + self.id.to_s + 'WHERE p.web_id = ' + self.id.to_s +
'ORDER by 1 ' ' ORDER by 1 '
).collect { |row| row['author'] } ).collect { |row| row['author'] }
end end

View file

@ -5,7 +5,7 @@
<ul id="feedsList"> <ul id="feedsList">
<li><%= link_to 'HTML', :web => @web.address, :action => 'export_html' %></li> <li><%= link_to 'HTML', :web => @web.address, :action => 'export_html' %></li>
<li><%= link_to "Markup (#{@web.markup.to_s.capitalize})", :web => @web.address, :action => 'export_markup' %></li> <li><%= link_to "Markup (#{@web.markup.to_s.capitalize})", :web => @web.address, :action => 'export_markup' %></li>
<% if OPTIONS[:pdflatex] && @web.markup == :textile %> <% if OPTIONS[:pdflatex] && @web.markup == :textile or @web.markup == :markdownMML %>
<li><%= link_to 'TeX', :web => @web.address, :action => 'export_tex' %></li> <li><%= link_to 'TeX', :web => @web.address, :action => 'export_tex' %></li>
<li><%= link_to 'PDF', :web => @web.address, :action => 'export_pdf' %></li> <li><%= link_to 'PDF', :web => @web.address, :action => 'export_pdf' %></li>
<% end %> <% end %>

View file

@ -2,7 +2,6 @@
\usepackage{amsmath} \usepackage{amsmath}
\usepackage{amsfonts} \usepackage{amsfonts}
\usepackage[OT1]{fontenc} %rigtige danske bogstaver...
\usepackage{graphicx} \usepackage{graphicx}
\usepackage{ucs} \usepackage{ucs}
\usepackage[utf8x]{inputenc} \usepackage[utf8x]{inputenc}
@ -12,8 +11,6 @@
\begin{document} \begin{document}
\sloppy
%------------------------------------------------------------------- %-------------------------------------------------------------------
\section*{<%= @page.name %>} \section*{<%= @page.name %>}

View file

@ -1,19 +1,11 @@
\documentclass[12pt,titlepage]{article} \documentclass[12pt,titlepage]{article}
\usepackage{fancyhdr} \usepackage{amsmath}
\pagestyle{fancy} \usepackage{amsfonts}
\fancyhead[LE,RO]{}
\fancyhead[LO,RE]{\nouppercase{\bfseries \leftmark}}
\fancyfoot[C]{\thepage}
\usepackage[danish]{babel} %danske tekster
\usepackage{a4}
\usepackage{graphicx} \usepackage{graphicx}
\usepackage{ucs} \usepackage{ucs}
\usepackage[utf8]{inputenc} \usepackage[utf8x]{inputenc}
\input epsf \usepackage{hyperref}
%------------------------------------------------------------------- %-------------------------------------------------------------------
@ -26,8 +18,6 @@
\tableofcontents \tableofcontents
\pagebreak \pagebreak
\sloppy
%------------------------------------------------------------------- %-------------------------------------------------------------------
<%= @tex_content %> <%= @tex_content %>

View file

@ -7,7 +7,7 @@ Rails::Initializer.run do |config|
# Use the database for sessions instead of the file system # Use the database for sessions instead of the file system
# (create the session table with 'rake create_sessions_table') # (create the session table with 'rake create_sessions_table')
config.action_controller.session_store = :active_record_store #config.action_controller.session_store = :active_record_store
# Enable page/fragment caching by setting a file-based store # Enable page/fragment caching by setting a file-based store
# (remember to create the caching directory and make it readable to the application) # (remember to create the caching directory and make it readable to the application)

View file

@ -24,7 +24,7 @@ a:visited {
color:#666; color:#666;
} }
h1,h2,h3 { h1,h2,h3,h4,h5,h6 {
color:#333; color:#333;
font-family:georgia, verdana, sans-serif; font-family:georgia, verdana, sans-serif;
} }
@ -34,13 +34,21 @@ font-size:200%;
} }
h2 { h2 {
font-size:130%; font-size:173%;
} }
h3 { h3 {
font-size:144%;
}
h4 {
font-size:120%; font-size:120%;
} }
h5,h6 {
font-size:100%
}
h1#pageName { h1#pageName {
line-height:1em; line-height:1em;
margin:0.2em 0 0; margin:0.2em 0 0;

View file

@ -246,9 +246,56 @@ module SQLite3
end end
def table_info( table, &block ) # :yields: row def table_info( table, &block ) # :yields: row
get_query_pragma "table_info", table, &block columns, *rows = execute2("PRAGMA table_info(#{table})")
needs_tweak_default = version_compare(driver.libversion, "3.3.7") > 0
result = [] unless block_given?
rows.each do |row|
new_row = {}
columns.each_with_index do |name, index|
new_row[name] = row[index]
end
tweak_default(new_row) if needs_tweak_default
if block_given?
yield new_row
else
result << new_row
end
end
result
end end
private
# Compares two version strings
def version_compare(v1, v2)
v1 = v1.split(".").map { |i| i.to_i }
v2 = v2.split(".").map { |i| i.to_i }
parts = [v1.length, v2.length].max
v1.push 0 while v1.length < parts
v2.push 0 while v2.length < parts
v1.zip(v2).each do |a,b|
return -1 if a < b
return 1 if a > b
end
return 0
end
# Since SQLite 3.3.8, the table_info pragma has returned the default
# value of the row as a quoted SQL value. This method essentially
# unquotes those values.
def tweak_default(hash)
case hash["dflt_value"]
when /^null$/i then
hash["dflt_value"] = nil
when /^'(.*)'$/
hash["dflt_value"] = $1.gsub(/''/, "'")
end
end
end end
end end

View file

@ -36,9 +36,10 @@ module SQLite3
MAJOR = 1 MAJOR = 1
MINOR = 2 MINOR = 2
TINY = 0 TINY = 1
STRING = [ MAJOR, MINOR, TINY ].join( "." ) STRING = [ MAJOR, MINOR, TINY ].join( "." )
#:beta-tag:
end end

View file

@ -1,3 +1,38 @@
*1.13.2* (February 5th, 2007)
* Deprecate server_settings renaming it to smtp_settings, add sendmail_settings to allow you to override the arguments to and location of the sendmail executable. [Koz]
*1.3.1* (January 16th, 2007)
* Depend on Action Pack 1.13.1
*1.3.0* (January 16th, 2007)
* Make mime version default to 1.0. closes #2323 [ror@andreas-s.net]
* Make sure quoted-printable text is decoded correctly when only portions of the text are encoded. closes #3154. [jon@siliconcircus.com]
* Make sure DOS newlines in quoted-printable text are normalized to unix newlines before unquoting. closes #4166 and #4452. [Jamis Buck]
* Fixed that iconv decoding should catch InvalidEncoding #3153 [jon@siliconcircus.com]
* Tighten rescue clauses. #5985 [james@grayproductions.net]
* Automatically included ActionController::UrlWriter, such that URL generation can happen within ActionMailer controllers. [DHH]
* Replace Reloadable with Reloadable::Deprecated. [Nicholas Seckar]
* Resolve action naming collision. #5520 [ssinghi@kreeti.com]
* ActionMailer::Base documentation rewrite. Closes #4991 [Kevin Clark, Marcel Molina Jr.]
* Replace alias method chaining with Module#alias_method_chain. [Marcel Molina Jr.]
* Replace Ruby's deprecated append_features in favor of included. [Marcel Molina Jr.]
*1.2.5* (August 10th, 2006) *1.2.5* (August 10th, 2006)
* Depend on Action Pack 1.12.5 * Depend on Action Pack 1.12.5
@ -22,12 +57,12 @@
* Depend on Action Pack 1.12.2 * Depend on Action Pack 1.12.2
*1.2.1* (April 6th, 2005) *1.2.1* (April 6th, 2006)
* Be part of Rails 1.1.1 * Be part of Rails 1.1.1
*1.2.0* (March 27th, 2005) *1.2.0* (March 27th, 2006)
* Nil charset caused subject line to be improperly quoted in implicitly multipart messages #2662 [ehalvorsen+rails@runbox.com] * Nil charset caused subject line to be improperly quoted in implicitly multipart messages #2662 [ehalvorsen+rails@runbox.com]

View file

@ -1,4 +1,4 @@
Copyright (c) 2004 David Heinemeier Hansson Copyright (c) 2004-2006 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the a copy of this software and associated documentation files (the

View file

@ -1,7 +1,7 @@
= Action Mailer -- Easy email delivery and testing = Action Mailer -- Easy email delivery and testing
Action Mailer is a framework for designing email-service layers. These layers Action Mailer is a framework for designing email-service layers. These layers
are used to consolidate code for sending out forgotten passwords, welcoming are used to consolidate code for sending out forgotten passwords, welcome
wishes on signup, invoices for billing, and any other use case that requires wishes on signup, invoices for billing, and any other use case that requires
a written notification to either a person or another system. a written notification to either a person or another system.
@ -136,13 +136,10 @@ Action Mailer is released under the MIT license.
== Support == Support
The Action Mailer homepage is http://actionmailer.rubyonrails.org. You can find The Action Mailer homepage is http://www.rubyonrails.org. You can find
the Action Mailer RubyForge page at http://rubyforge.org/projects/actionmailer. the Action Mailer RubyForge page at http://rubyforge.org/projects/actionmailer.
And as Jim from Rake says: And as Jim from Rake says:
Feel free to submit commits or feature requests. If you send a patch, Feel free to submit commits or feature requests. If you send a patch,
remember to update the corresponding unit tests. If fact, I prefer remember to update the corresponding unit tests. If fact, I prefer
new feature to be submitted in the form of new unit tests. new feature to be submitted in the form of new unit tests.
For other information, feel free to ask on the ruby-talk mailing list (which
is mirrored to comp.lang.ruby) or contact mailto:david@loudthinking.com.

View file

@ -54,7 +54,7 @@ spec = Gem::Specification.new do |s|
s.rubyforge_project = "actionmailer" s.rubyforge_project = "actionmailer"
s.homepage = "http://www.rubyonrails.org" s.homepage = "http://www.rubyonrails.org"
s.add_dependency('actionpack', '= 1.12.5' + PKG_BUILD) s.add_dependency('actionpack', '= 1.13.2' + PKG_BUILD)
s.has_rdoc = true s.has_rdoc = true
s.requirements << 'none' s.requirements << 'none'

View file

@ -1,5 +1,5 @@
#-- #--
# Copyright (c) 2004 David Heinemeier Hansson # Copyright (c) 2004-2006 David Heinemeier Hansson
# #
# Permission is hereby granted, free of charge, to any person obtaining # Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the # a copy of this software and associated documentation files (the
@ -21,14 +21,13 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++ #++
begin unless defined?(ActionController)
require 'action_controller'
rescue LoadError
begin begin
require File.dirname(__FILE__) + '/../../actionpack/lib/action_controller' $:.unshift "#{File.dirname(__FILE__)}/../../actionpack/lib"
require 'action_controller'
rescue LoadError rescue LoadError
require 'rubygems' require 'rubygems'
require_gem 'actionpack', '>= 1.9.1' gem 'actionpack', '>= 1.12.5'
end end
end end

View file

@ -1,7 +1,6 @@
module ActionMailer module ActionMailer
module AdvAttrAccessor #:nodoc: module AdvAttrAccessor #:nodoc:
def self.append_features(base) def self.included(base)
super
base.extend(ClassMethods) base.extend(ClassMethods)
end end

View file

@ -7,7 +7,9 @@ require 'tmail/net'
module ActionMailer #:nodoc: module ActionMailer #:nodoc:
# ActionMailer allows you to send email from your application using a mailer model and views. # ActionMailer allows you to send email from your application using a mailer model and views.
# #
#
# = Mailer Models # = Mailer Models
#
# To use ActionMailer, you need to create a mailer model. # To use ActionMailer, you need to create a mailer model.
# #
# $ script/generate mailer Notifier # $ script/generate mailer Notifier
@ -23,7 +25,7 @@ module ActionMailer #:nodoc:
# recipients recipient.email_address_with_name # recipients recipient.email_address_with_name
# from "system@example.com" # from "system@example.com"
# subject "New account information" # subject "New account information"
# body "account" => recipient # body :account => recipient
# end # end
# end # end
# #
@ -45,7 +47,9 @@ module ActionMailer #:nodoc:
# in an instance variable <tt>@account</tt> with the value of <tt>recipient</tt> being accessible in the # in an instance variable <tt>@account</tt> with the value of <tt>recipient</tt> being accessible in the
# view. # view.
# #
# = Mailer Views #
# = Mailer views
#
# Like ActionController, each mailer class has a corresponding view directory # Like ActionController, each mailer class has a corresponding view directory
# in which each method of the class looks for a template with its name. # in which each method of the class looks for a template with its name.
# To define a template to be used with a mailing, create an <tt>.rhtml</tt> file with the same name as the method # To define a template to be used with a mailing, create an <tt>.rhtml</tt> file with the same name as the method
@ -59,7 +63,30 @@ module ActionMailer #:nodoc:
# Hi <%= @account.name %>, # Hi <%= @account.name %>,
# Thanks for joining our service! Please check back often. # Thanks for joining our service! Please check back often.
# #
# = Sending Mail # You can even use Action Pack helpers in these views. For example:
#
# You got a new note!
# <%= truncate(note.body, 25) %>
#
#
# = Generating URLs for mailer views
#
# If your view includes URLs from the application, you need to use url_for in the mailing method instead of the view.
# Unlike controllers from Action Pack, the mailer instance doesn't have any context about the incoming request. That's
# why you need to jump this little hoop and supply all the details needed for the URL. Example:
#
# def signup_notification(recipient)
# recipients recipient.email_address_with_name
# from "system@example.com"
# subject "New account information"
# body :account => recipient,
# :home_page => url_for(:host => "example.com", :controller => "welcome", :action => "greeting")
# end
#
# You can now access @home_page in the template and get http://example.com/welcome/greeting.
#
# = Sending mail
#
# Once a mailer action and template are defined, you can deliver your message or create it and save it # Once a mailer action and template are defined, you can deliver your message or create it and save it
# for delivery later: # for delivery later:
# #
@ -73,7 +100,9 @@ module ActionMailer #:nodoc:
# like to deliver. The <tt>signup_notification</tt> method defined above is # like to deliver. The <tt>signup_notification</tt> method defined above is
# delivered by invoking <tt>Notifier.deliver_signup_notification</tt>. # delivered by invoking <tt>Notifier.deliver_signup_notification</tt>.
# #
# = HTML Email #
# = HTML email
#
# To send mail as HTML, make sure your view (the <tt>.rhtml</tt> file) generates HTML and # To send mail as HTML, make sure your view (the <tt>.rhtml</tt> file) generates HTML and
# set the content type to html. # set the content type to html.
# #
@ -87,7 +116,9 @@ module ActionMailer #:nodoc:
# end # end
# end # end
# #
# = Multipart Email #
# = Multipart email
#
# You can explicitly specify multipart messages: # You can explicitly specify multipart messages:
# #
# class ApplicationMailer < ActionMailer::Base # class ApplicationMailer < ActionMailer::Base
@ -120,7 +151,9 @@ module ActionMailer #:nodoc:
# with the corresponding content type. The same body hash is passed to # with the corresponding content type. The same body hash is passed to
# each template. # each template.
# #
#
# = Attachments # = Attachments
#
# Attachments can be added by using the +attachment+ method. # Attachments can be added by using the +attachment+ method.
# #
# Example: # Example:
@ -141,6 +174,7 @@ module ActionMailer #:nodoc:
# end # end
# end # end
# #
#
# = Configuration options # = Configuration options
# #
# These options are specified on the class level, like <tt>ActionMailer::Base.template_root = "/my/templates"</tt> # These options are specified on the class level, like <tt>ActionMailer::Base.template_root = "/my/templates"</tt>
@ -150,7 +184,7 @@ module ActionMailer #:nodoc:
# * <tt>logger</tt> - the logger is used for generating information on the mailing run if available. # * <tt>logger</tt> - the logger is used for generating information on the mailing run if available.
# Can be set to nil for no logging. Compatible with both Ruby's own Logger and Log4r loggers. # Can be set to nil for no logging. Compatible with both Ruby's own Logger and Log4r loggers.
# #
# * <tt>server_settings</tt> - Allows detailed configuration of the server: # * <tt>smtp_settings</tt> - Allows detailed configuration for :smtp delivery method:
# * <tt>:address</tt> Allows you to use a remote mail server. Just change it from its default "localhost" setting. # * <tt>:address</tt> Allows you to use a remote mail server. Just change it from its default "localhost" setting.
# * <tt>:port</tt> On the off chance that your mail server doesn't run on port 25, you can change it. # * <tt>:port</tt> On the off chance that your mail server doesn't run on port 25, you can change it.
# * <tt>:domain</tt> If you need to specify a HELO domain, you can do it here. # * <tt>:domain</tt> If you need to specify a HELO domain, you can do it here.
@ -159,10 +193,12 @@ module ActionMailer #:nodoc:
# * <tt>:authentication</tt> If your mail server requires authentication, you need to specify the authentication type here. # * <tt>:authentication</tt> If your mail server requires authentication, you need to specify the authentication type here.
# This is a symbol and one of :plain, :login, :cram_md5 # This is a symbol and one of :plain, :login, :cram_md5
# #
# * <tt>sendmail_settings</tt> - Allows you to override options for the :sendmail delivery method
# * <tt>:location</tt> The location of the sendmail executable, defaults to "/usr/sbin/sendmail"
# * <tt>:arguments</tt> The command line arguments
# * <tt>raise_delivery_errors</tt> - whether or not errors should be raised if the email fails to be delivered. # * <tt>raise_delivery_errors</tt> - whether or not errors should be raised if the email fails to be delivered.
# #
# * <tt>delivery_method</tt> - Defines a delivery method. Possible values are :smtp (default), :sendmail, and :test. # * <tt>delivery_method</tt> - Defines a delivery method. Possible values are :smtp (default), :sendmail, and :test.
# Sendmail is assumed to be present at "/usr/sbin/sendmail".
# #
# * <tt>perform_deliveries</tt> - Determines whether deliver_* methods are actually carried out. By default they are, # * <tt>perform_deliveries</tt> - Determines whether deliver_* methods are actually carried out. By default they are,
# but this can be turned off to help functional testing. # but this can be turned off to help functional testing.
@ -174,9 +210,8 @@ module ActionMailer #:nodoc:
# pick a different charset from inside a method with <tt>@charset</tt>. # pick a different charset from inside a method with <tt>@charset</tt>.
# * <tt>default_content_type</tt> - The default content type used for the main part of the message. Defaults to "text/plain". You # * <tt>default_content_type</tt> - The default content type used for the main part of the message. Defaults to "text/plain". You
# can also pick a different content type from inside a method with <tt>@content_type</tt>. # can also pick a different content type from inside a method with <tt>@content_type</tt>.
# * <tt>default_mime_version</tt> - The default mime version used for the message. Defaults to nil. You # * <tt>default_mime_version</tt> - The default mime version used for the message. Defaults to "1.0". You
# can also pick a different value from inside a method with <tt>@mime_version</tt>. When multipart messages are in # can also pick a different value from inside a method with <tt>@mime_version</tt>.
# use, <tt>@mime_version</tt> will be set to "1.0" if it is not set inside a method.
# * <tt>default_implicit_parts_order</tt> - When a message is built implicitly (i.e. multiple parts are assembled from templates # * <tt>default_implicit_parts_order</tt> - When a message is built implicitly (i.e. multiple parts are assembled from templates
# which specify the content type in their filenames) this variable controls how the parts are ordered. Defaults to # which specify the content type in their filenames) this variable controls how the parts are ordered. Defaults to
# ["text/html", "text/enriched", "text/plain"]. Items that appear first in the array have higher priority in the mail client # ["text/html", "text/enriched", "text/plain"]. Items that appear first in the array have higher priority in the mail client
@ -184,17 +219,18 @@ module ActionMailer #:nodoc:
# <tt>@implicit_parts_order</tt>. # <tt>@implicit_parts_order</tt>.
class Base class Base
include AdvAttrAccessor, PartContainer include AdvAttrAccessor, PartContainer
include ActionController::UrlWriter
# Action Mailer subclasses should be reloaded by the dispatcher in Rails # Action Mailer subclasses should be reloaded by the dispatcher in Rails
# when Dependencies.mechanism = :load. # when Dependencies.mechanism = :load.
include Reloadable::Subclasses include Reloadable::Deprecated
private_class_method :new #:nodoc: private_class_method :new #:nodoc:
class_inheritable_accessor :template_root class_inheritable_accessor :template_root
cattr_accessor :logger cattr_accessor :logger
@@server_settings = { @@smtp_settings = {
:address => "localhost", :address => "localhost",
:port => 25, :port => 25,
:domain => 'localhost.localdomain', :domain => 'localhost.localdomain',
@ -202,7 +238,13 @@ module ActionMailer #:nodoc:
:password => nil, :password => nil,
:authentication => nil :authentication => nil
} }
cattr_accessor :server_settings cattr_accessor :smtp_settings
@@sendmail_settings = {
:location => '/usr/sbin/sendmail',
:arguments => '-i -t'
}
cattr_accessor :sendmail_settings
@@raise_delivery_errors = true @@raise_delivery_errors = true
cattr_accessor :raise_delivery_errors cattr_accessor :raise_delivery_errors
@ -222,7 +264,7 @@ module ActionMailer #:nodoc:
@@default_content_type = "text/plain" @@default_content_type = "text/plain"
cattr_accessor :default_content_type cattr_accessor :default_content_type
@@default_mime_version = nil @@default_mime_version = "1.0"
cattr_accessor :default_mime_version cattr_accessor :default_mime_version
@@default_implicit_parts_order = [ "text/html", "text/enriched", "text/plain" ] @@default_implicit_parts_order = [ "text/html", "text/enriched", "text/plain" ]
@ -321,6 +363,18 @@ module ActionMailer #:nodoc:
def deliver(mail) def deliver(mail)
new.deliver!(mail) new.deliver!(mail)
end end
# Server Settings is the old name for <tt>smtp_settings</tt>
def server_settings
smtp_settings
end
deprecate :server_settings=>"It's now named smtp_settings"
def server_settings=(settings)
ActiveSupport::Deprecation.warn("server_settings has been renamed smtp_settings, this warning will be removed with rails 2.0", caller)
self.smtp_settings=settings
end
end end
# Instantiate a new mailer object. If +method_name+ is not +nil+, the mailer # Instantiate a new mailer object. If +method_name+ is not +nil+, the mailer
@ -348,7 +402,7 @@ module ActionMailer #:nodoc:
templates.each do |path| templates.each do |path|
# TODO: don't hardcode rhtml|rxml # TODO: don't hardcode rhtml|rxml
basename = File.basename(path) basename = File.basename(path)
next unless md = /^([^\.]+)\.([^\.]+\.[^\+]+)\.(rhtml|rxml)$/.match(basename) next unless md = /^([^\.]+)\.([^\.]+\.[^\.]+)\.(rhtml|rxml)$/.match(basename)
template_name = basename template_name = basename
content_type = md.captures[1].gsub('.', '/') content_type = md.captures[1].gsub('.', '/')
@parts << Part.new(:content_type => content_type, @parts << Part.new(:content_type => content_type,
@ -395,7 +449,7 @@ module ActionMailer #:nodoc:
begin begin
send("perform_delivery_#{delivery_method}", mail) if perform_deliveries send("perform_delivery_#{delivery_method}", mail) if perform_deliveries
rescue Object => e rescue Exception => e # Net::SMTP errors or sendmail pipe errors
raise e if raise_delivery_errors raise e if raise_delivery_errors
end end
@ -508,14 +562,14 @@ module ActionMailer #:nodoc:
destinations = mail.destinations destinations = mail.destinations
mail.ready_to_send mail.ready_to_send
Net::SMTP.start(server_settings[:address], server_settings[:port], server_settings[:domain], Net::SMTP.start(smtp_settings[:address], smtp_settings[:port], smtp_settings[:domain],
server_settings[:user_name], server_settings[:password], server_settings[:authentication]) do |smtp| smtp_settings[:user_name], smtp_settings[:password], smtp_settings[:authentication]) do |smtp|
smtp.sendmail(mail.encoded, mail.from, destinations) smtp.sendmail(mail.encoded, mail.from, destinations)
end end
end end
def perform_delivery_sendmail(mail) def perform_delivery_sendmail(mail)
IO.popen("/usr/sbin/sendmail -i -t","w+") do |sm| IO.popen("#{sendmail_settings[:location]} #{sendmail_settings[:arguments]}","w+") do |sm|
sm.print(mail.encoded.gsub(/\r/, '')) sm.print(mail.encoded.gsub(/\r/, ''))
sm.flush sm.flush
end end

View file

@ -1,8 +1,6 @@
module ActionMailer module ActionMailer
module Helpers #:nodoc: module Helpers #:nodoc:
def self.append_features(base) #:nodoc: def self.included(base) #:nodoc:
super
# Initialize the base module to aggregate its helpers. # Initialize the base module to aggregate its helpers.
base.class_inheritable_accessor :master_helper_module base.class_inheritable_accessor :master_helper_module
base.master_helper_module = Module.new base.master_helper_module = Module.new
@ -13,14 +11,12 @@ module ActionMailer
base.class_eval do base.class_eval do
# Wrap inherited to create a new master helper module for subclasses. # Wrap inherited to create a new master helper module for subclasses.
class << self class << self
alias_method :inherited_without_helper, :inherited alias_method_chain :inherited, :helper
alias_method :inherited, :inherited_with_helper
end end
# Wrap initialize_template_class to extend new template class # Wrap initialize_template_class to extend new template class
# instances with the master helper module. # instances with the master helper module.
alias_method :initialize_template_class_without_helper, :initialize_template_class alias_method_chain :initialize_template_class, :helper
alias_method :initialize_template_class, :initialize_template_class_with_helper
end end
end end

View file

@ -49,25 +49,31 @@ module TMail
class << self class << self
def unquote_and_convert_to(text, to_charset, from_charset = "iso-8859-1", preserve_underscores=false) def unquote_and_convert_to(text, to_charset, from_charset = "iso-8859-1", preserve_underscores=false)
return "" if text.nil? return "" if text.nil?
if text =~ /^=\?(.*?)\?(.)\?(.*)\?=$/ text.gsub(/(.*?)(?:(?:=\?(.*?)\?(.)\?(.*?)\?=)|$)/) do
from_charset = $1 before = $1
quoting_method = $2 from_charset = $2
text = $3 quoting_method = $3
case quoting_method.upcase text = $4
when "Q" then
unquote_quoted_printable_and_convert_to(text, to_charset, from_charset, preserve_underscores) before = convert_to(before, to_charset, from_charset) if before.length > 0
when "B" then before + case quoting_method
unquote_base64_and_convert_to(text, to_charset, from_charset) when "q", "Q" then
else unquote_quoted_printable_and_convert_to(text, to_charset, from_charset, preserve_underscores)
raise "unknown quoting method #{quoting_method.inspect}" when "b", "B" then
end unquote_base64_and_convert_to(text, to_charset, from_charset)
else when nil then
convert_to(text, to_charset, from_charset) # will be nil at the end of the string, due to the nature of
# the regex used.
""
else
raise "unknown quoting method #{quoting_method.inspect}"
end
end end
end end
def unquote_quoted_printable_and_convert_to(text, to, from, preserve_underscores=false) def unquote_quoted_printable_and_convert_to(text, to, from, preserve_underscores=false)
text = text.gsub(/_/, " ") unless preserve_underscores text = text.gsub(/_/, " ") unless preserve_underscores
text = text.gsub(/\r\n|\r/, "\n") # normalize newlines
convert_to(text.unpack("M*").first, to, from) convert_to(text.unpack("M*").first, to, from)
end end
@ -80,7 +86,7 @@ module TMail
def convert_to(text, to, from) def convert_to(text, to, from)
return text unless to && from return text unless to && from
text ? Iconv.iconv(to, from, text).first : "" text ? Iconv.iconv(to, from, text).first : ""
rescue Iconv::IllegalSequence, Errno::EINVAL rescue Iconv::IllegalSequence, Iconv::InvalidEncoding, Errno::EINVAL
# the 'from' parameter specifies a charset other than what the text # the 'from' parameter specifies a charset other than what the text
# actually is...not much we can do in this case but just return the # actually is...not much we can do in this case but just return the
# unconverted text. # unconverted text.

View file

@ -1,8 +1,8 @@
module ActionMailer module ActionMailer
module VERSION #:nodoc: module VERSION #:nodoc:
MAJOR = 1 MAJOR = 1
MINOR = 2 MINOR = 3
TINY = 5 TINY = 2
STRING = [MAJOR, MINOR, TINY].join('.') STRING = [MAJOR, MINOR, TINY].join('.')
end end

View file

@ -1 +0,0 @@
Have a lovely picture, from me. Enjoy!

View file

@ -1,8 +1,4 @@
$:.unshift(File.dirname(__FILE__) + "/../lib/") require "#{File.dirname(__FILE__)}/abstract_unit"
$:.unshift File.dirname(__FILE__) + "/fixtures/helpers"
require 'test/unit'
require 'action_mailer'
module MailerHelper module MailerHelper
def person_name def person_name
@ -56,8 +52,6 @@ class HelperMailer < ActionMailer::Base
helper_method :name_of_the_mailer_class helper_method :name_of_the_mailer_class
end end
HelperMailer.template_root = File.dirname(__FILE__) + "/fixtures"
class MailerHelperTest < Test::Unit::TestCase class MailerHelperTest < Test::Unit::TestCase
def new_mail( charset="utf-8" ) def new_mail( charset="utf-8" )
mail = TMail::Mail.new mail = TMail::Mail.new

View file

@ -1,7 +1,4 @@
$:.unshift(File.dirname(__FILE__) + "/../lib/") require "#{File.dirname(__FILE__)}/abstract_unit"
require 'test/unit'
require 'action_mailer'
class RenderMailer < ActionMailer::Base class RenderMailer < ActionMailer::Base
def inline_template(recipient) def inline_template(recipient)
@ -24,7 +21,21 @@ class RenderMailer < ActionMailer::Base
end end
end end
RenderMailer.template_root = File.dirname(__FILE__) + "/fixtures" class FirstMailer < ActionMailer::Base
def share(recipient)
recipients recipient
subject "using helpers"
from "tester@example.com"
end
end
class SecondMailer < ActionMailer::Base
def share(recipient)
recipients recipient
subject "using helpers"
from "tester@example.com"
end
end
class RenderHelperTest < Test::Unit::TestCase class RenderHelperTest < Test::Unit::TestCase
def setup def setup
@ -46,3 +57,23 @@ class RenderHelperTest < Test::Unit::TestCase
end end
end end
class FirstSecondHelperTest < Test::Unit::TestCase
def setup
ActionMailer::Base.delivery_method = :test
ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.deliveries = []
@recipient = 'test@localhost'
end
def test_ordering
mail = FirstMailer.create_share(@recipient)
assert_equal "first mail", mail.body.strip
mail = SecondMailer.create_share(@recipient)
assert_equal "second mail", mail.body.strip
mail = FirstMailer.create_share(@recipient)
assert_equal "first mail", mail.body.strip
mail = SecondMailer.create_share(@recipient)
assert_equal "second mail", mail.body.strip
end
end

View file

@ -1,27 +1,4 @@
$:.unshift(File.dirname(__FILE__) + "/../lib/") require "#{File.dirname(__FILE__)}/abstract_unit"
require 'test/unit'
require 'action_mailer'
class MockSMTP
def self.deliveries
@@deliveries
end
def initialize
@@deliveries = []
end
def sendmail(mail, from, to)
@@deliveries << [mail, from, to]
end
end
class Net::SMTP
def self.start(*args)
yield MockSMTP.new
end
end
class FunkyPathMailer < ActionMailer::Base class FunkyPathMailer < ActionMailer::Base
self.template_root = "#{File.dirname(__FILE__)}/fixtures/path.with.dots" self.template_root = "#{File.dirname(__FILE__)}/fixtures/path.with.dots"
@ -33,14 +10,9 @@ class FunkyPathMailer < ActionMailer::Base
attachment :content_type => "image/jpeg", attachment :content_type => "image/jpeg",
:body => "not really a jpeg, we're only testing, after all" :body => "not really a jpeg, we're only testing, after all"
end end
def template_path
"#{File.dirname(__FILE__)}/fixtures/path.with.dots"
end
end end
class TestMailer < ActionMailer::Base class TestMailer < ActionMailer::Base
def signed_up(recipient) def signed_up(recipient)
@recipients = recipient @recipients = recipient
@subject = "[Signed up] Welcome #{recipient}" @subject = "[Signed up] Welcome #{recipient}"
@ -222,7 +194,7 @@ class TestMailer < ActionMailer::Base
subject "nested multipart" subject "nested multipart"
from "test@example.com" from "test@example.com"
content_type "multipart/mixed" content_type "multipart/mixed"
part :content_type => "multipart/alternative", :content_disposition => "inline" do |p| part :content_type => "multipart/alternative", :content_disposition => "inline", :headers => { "foo" => "bar" } do |p|
p.part :content_type => "text/plain", :body => "test text\nline #2" p.part :content_type => "text/plain", :body => "test text\nline #2"
p.part :content_type => "text/html", :body => "<b>test</b> HTML<br/>\nline #2" p.part :content_type => "text/html", :body => "<b>test</b> HTML<br/>\nline #2"
end end
@ -273,8 +245,6 @@ class TestMailer < ActionMailer::Base
end end
end end
TestMailer.template_root = File.dirname(__FILE__) + "/fixtures"
class ActionMailerTest < Test::Unit::TestCase class ActionMailerTest < Test::Unit::TestCase
include ActionMailer::Quoting include ActionMailer::Quoting
@ -284,6 +254,7 @@ class ActionMailerTest < Test::Unit::TestCase
def new_mail( charset="utf-8" ) def new_mail( charset="utf-8" )
mail = TMail::Mail.new mail = TMail::Mail.new
mail.mime_version = "1.0"
if charset if charset
mail.set_content_type "text", "plain", { "charset" => charset } mail.set_content_type "text", "plain", { "charset" => charset }
end end
@ -306,6 +277,7 @@ class ActionMailerTest < Test::Unit::TestCase
assert_equal "multipart/mixed", created.content_type assert_equal "multipart/mixed", created.content_type
assert_equal "multipart/alternative", created.parts.first.content_type assert_equal "multipart/alternative", created.parts.first.content_type
assert_equal "bar", created.parts.first.header['foo'].to_s
assert_equal "text/plain", created.parts.first.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 "text/html", created.parts.first.parts[1].content_type
assert_equal "application/octet-stream", created.parts[1].content_type assert_equal "application/octet-stream", created.parts[1].content_type
@ -324,7 +296,6 @@ class ActionMailerTest < Test::Unit::TestCase
expected.body = "Hello there, \n\nMr. #{@recipient}" expected.body = "Hello there, \n\nMr. #{@recipient}"
expected.from = "system@loudthinking.com" expected.from = "system@loudthinking.com"
expected.date = Time.local(2004, 12, 12) expected.date = Time.local(2004, 12, 12)
expected.mime_version = nil
created = nil created = nil
assert_nothing_raised { created = TestMailer.create_signed_up(@recipient) } assert_nothing_raised { created = TestMailer.create_signed_up(@recipient) }
@ -816,6 +787,19 @@ EOF
assert_match %r{format=flowed}, mail['content-type'].to_s assert_match %r{format=flowed}, mail['content-type'].to_s
assert_match %r{charset=utf-8}, mail['content-type'].to_s assert_match %r{charset=utf-8}, mail['content-type'].to_s
end end
def test_deprecated_server_settings
old_smtp_settings = ActionMailer::Base.smtp_settings
assert_deprecated do
ActionMailer::Base.server_settings
end
assert_deprecated do
ActionMailer::Base.server_settings={}
assert_equal Hash.new, ActionMailer::Base.smtp_settings
end
ensure
ActionMailer::Base.smtp_settings=old_smtp_settings
end
end end
class InheritableTemplateRootTest < Test::Unit::TestCase class InheritableTemplateRootTest < Test::Unit::TestCase

View file

@ -1,7 +1,4 @@
$:.unshift(File.dirname(__FILE__) + "/../lib/") require "#{File.dirname(__FILE__)}/abstract_unit"
$:.unshift(File.dirname(__FILE__) + "/../lib/action_mailer/vendor")
require 'test/unit'
require 'tmail' require 'tmail'
require 'tempfile' require 'tempfile'
@ -22,6 +19,18 @@ class QuotingTest < Test::Unit::TestCase
assert_equal unquoted, original assert_equal unquoted, original
end end
# test an email that has been created using \r\n newlines, instead of
# \n newlines.
def test_email_quoted_with_0d0a
mail = TMail::Mail.parse(IO.read("#{File.dirname(__FILE__)}/fixtures/raw_email_quoted_with_0d0a"))
assert_match %r{Elapsed time}, mail.body
end
def test_email_with_partially_quoted_subject
mail = TMail::Mail.parse(IO.read("#{File.dirname(__FILE__)}/fixtures/raw_email_with_partially_quoted_subject"))
assert_equal "Re: Test: \"\346\274\242\345\255\227\" mid \"\346\274\242\345\255\227\" tail", mail.subject
end
private private
# This whole thing *could* be much simpler, but I don't think Tempfile, # This whole thing *could* be much simpler, but I don't think Tempfile,
@ -40,7 +49,7 @@ class QuotingTest < Test::Unit::TestCase
end end
system("ruby #{test_name} > #{res_name}") or raise "could not run test in sandbox" system("ruby #{test_name} > #{res_name}") or raise "could not run test in sandbox"
File.read(res_name) File.read(res_name).chomp
ensure ensure
File.delete(test_name) rescue nil File.delete(test_name) rescue nil
File.delete(res_name) rescue nil File.delete(res_name) rescue nil

View file

@ -1,8 +1,4 @@
$:.unshift(File.dirname(__FILE__) + "/../lib/") require "#{File.dirname(__FILE__)}/abstract_unit"
$:.unshift File.dirname(__FILE__) + "/fixtures/helpers"
require 'test/unit'
require 'action_mailer'
class TMailMailTest < Test::Unit::TestCase class TMailMailTest < Test::Unit::TestCase
def test_body def test_body

View file

@ -1,3 +1,557 @@
*1.13.2* (February 5th, 2007)
* Add much-needed html-scanner tests. Fixed CDATA parsing bug. [Rick]
* improve error message for Routing for named routes. [Rob Sanheim]
* Added enhanced docs to routing assertions. [Rob Sanheim]
* fix form_for example in ActionController::Resources documentation. [gnarg]
* Add singleton resources from trunk [Rick Olson]
* TestSession supports indifferent access so session['foo'] == session[:foo] in your tests. #7372 [julik, jean.helou]
* select :multiple => true suffixes the attribute name with [] unless already suffixed. #6977 [nik.kakelin, ben, julik]
* Improve routes documentation. #7095 [zackchandler]
* Resource member routes require :id, eliminating the ambiguous overlap with collection routes. #7229 [dkubb]
* Fixed NumberHelper#number_with_delimiter to use "." always for splitting the original number, not the delimiter parameter #7389 [ceefour]
* Autolinking recognizes trailing and embedded . , : ; #7354 [Jarkko Laine]
* Make TextHelper::auto_link recognize URLs with colons in path correctly, fixes #7268. [imajes]
* Improved auto_link to match more valid urls correctly [Tobias Luetke]
*1.13.1* (January 18th, 2007)
* Fixed content-type bug in Prototype [sam]
*1.13.0* (January 16th, 2007)
* Modernize cookie testing code, and increase coverage (Heckle++) #7101 [Kevin Clark]
* Heckling ActionController::Resources::Resource revealed that set_prefixes didn't break when :name_prefix was munged. #7081 [Kevin Clark]
* Update to Prototype 1.5.0. [Sam Stephenson]
* Allow exempt_from_layout :rhtml. #6742, #7026 [dcmanges, Squeegy]
* Fix parsing of array[] CGI parameters so extra empty values aren't included. #6252 [Nicholas Seckar, aiwilliams, brentrowland]
* link_to_unless_current works with full URLs as well as paths. #6891 [Jarkko Laine, manfred, idrifter]
* Fix HTML::Node to output double quotes instead of single quotes. Closes #6845 [mitreandy]
* Fix no method error with error_messages_on. Closes #6935 [nik.wakelin Koz]
* Slight doc tweak to the ActionView::Helpers::PrototypeHelper#replace docs. Closes #6922 [Steven Bristol]
* Slight doc tweak to #prepend_filter. Closes #6493 [Jeremy Voorhis]
* Add more extensive documentation to the AssetTagHelper. Closes #6452 [Bob Silva]
* Clean up multiple calls to #stringify_keys in TagHelper, add better documentation and testing for TagHelper. Closes #6394 [Bob Silva]
* [DOCS] fix reference to ActionController::Macros::AutoComplete for #text_field_with_auto_complete. Closes #2578 [Jan Prill]
* Make sure html_document is reset between integration test requests. [ctm]
* Set session to an empty hash if :new_session => false and no session cookie or param is present. CGI::Session was raising an unrescued ArgumentError. [Josh Susser]
* Fix assert_redirected_to bug where redirecting from a nested to to a top-level controller incorrectly added the current controller's nesting. Closes #6128. [Rick Olson]
* Ensure render :json => ... skips the layout. #6808 [Josh Peek]
* Silence log_error deprecation warnings from inspecting deprecated instance variables. [Nate Wiger]
* Only cache GET requests with a 200 OK response. #6514, #6743 [RSL, anamba]
* Correctly report which filter halted the chain. #6699 [Martin Emde]
* respond_to recognizes JSON. render :json => @person.to_json automatically sets the content type and takes a :callback option to specify a client-side function to call using the rendered JSON as an argument. #4185 [Scott Raymond, eventualbuddha]
# application/json response with body 'Element.show({:name: "David"})'
respond_to do |format|
format.json { render :json => { :name => "David" }.to_json, :callback => 'Element.show' }
end
* Makes :discard_year work without breaking multi-attribute parsing in AR. #1260, #3800 [sean@ardismg.com, jmartin@desertflood.com, stephen@touset.org, Bob Silva]
* Adds html id attribute to date helper elements. #1050, #1382 [mortonda@dgrmm.net, David North, Bob Silva]
* Add :index and @auto_index capability to model driven date/time selects. #847, #2655 [moriq, Doug Fales, Bob Silva]
* Add :order to datetime_select, select_datetime, and select_date. #1427 [Timothee Peignier, patrick@lenz.sh, Bob Silva]
* Added time_select to work with time values in models. Update scaffolding. #2489, #2833 [Justin Palmer, Andre Caum, Bob Silva]
* Added :include_seconds to select_datetime, datetime_select and time_select. #2998 [csn, Bob Silva]
* All date/datetime selects can now accept an array of month names with :use_month_names. Allows for localization. #363 [tomasj, Bob Silva]
* Adds :time_separator to select_time and :date_separator to select_datetime. Preserves BC. #3811 [Bob Silva]
* @response.redirect_url works with 201 Created responses: just return headers['Location'] rather than checking the response status. [Jeremy Kemper]
* Fixed that HEAD should return the proper Content-Length header (that is, actually use @body.size, not just 0) [DHH]
* Added GET-masquarading for HEAD, so request.method will return :get even for HEADs. This will help anyone relying on case request.method to automatically work with HEAD and map.resources will also allow HEADs to all GET actions. Rails automatically throws away the response content in a reply to HEAD, so you don't even need to worry about that. If you, for whatever reason, still need to distinguish between GET and HEAD in some edge case, you can use Request#head? and even Request.headers["REQUEST_METHOD"] for get the "real" answer. Closes #6694 [DHH]
*1.13.0 RC1* (r5619, November 22nd, 2006)
* Update Routing to complain when :controller is not specified by a route. Closes #6669. [Nicholas Seckar]
* Ensure render_to_string cleans up after itself when an exception is raised. #6658 [rsanheim]
* Update to Prototype and script.aculo.us [5579]. [Sam Stephenson, Thomas Fuchs]
* simple_format helper doesn't choke on nil. #6644 [jerry426]
* Reuse named route helper module between Routing reloads. Use remove_method to delete named route methods after each load. Since the module is never collected, this fixes a significant memory leak. [Nicholas Seckar]
* Deprecate standalone components. [Jeremy Kemper]
* Always clear model associations from session. #4795 [sd@notso.net, andylien@gmail.com]
* Remove JavaScriptLiteral in favor of ActiveSupport::JSON::Variable. [Sam Stephenson]
* Sync ActionController::StatusCodes::STATUS_CODES with http://www.iana.org/assignments/http-status-codes. #6586 [dkubb]
* Multipart form values may have a content type without being treated as uploaded files if they do not provide a filename. #6401 [Andreas Schwarz, Jeremy Kemper]
* assert_response supports symbolic status codes. #6569 [Kevin Clark]
assert_response :ok
assert_response :not_found
assert_response :forbidden
* Cache parsed query parameters. #6559 [Stefan Kaes]
* Deprecate JavaScriptHelper#update_element_function, which is superseeded by RJS [Thomas Fuchs]
* Fix invalid test fixture exposed by stricter Ruby 1.8.5 multipart parsing. #6524 [Bob Silva]
* Set ActionView::Base.default_form_builder once rather than passing the :builder option to every form or overriding the form helper methods. [Jeremy Kemper]
* Deprecate expire_matched_fragments. Use expire_fragment instead. #6535 [Bob Silva]
* Deprecate start_form_tag and end_form_tag. Use form_tag / '</form>' from now on. [Rick]
* Added block-usage to PrototypeHelper#form_remote_tag, document block-usage of FormTagHelper#form_tag [Rick]
* Add a 0 margin/padding div around the hidden _method input tag that form_tag outputs. [Rick]
* Added block-usage to TagHelper#content_tag [DHH]. Example:
<% content_tag :div, :class => "strong" %>
Hello world!
<% end %>
Will output:
<div class="strong">Hello world!</div>
* Deprecated UrlHelper#link_to_image and UrlHelper#link_to :post => true #6409 [BobSilva]
* Upgraded NumberHelper with number_to_phone support international formats to comply with ITU E.123 by supporting area codes with less than 3 digits, added precision argument to number_to_human_size (defaults to 1) #6421 [BobSilva]
* Fixed that setting RAILS_ASSET_ID to "" should not add a trailing slash after assets #6454 [BobSilva/chrismear]
* Force *_url named routes to show the host in ActionView [Rick]
<%= url_for ... %> # no host
<%= foo_path %> # no host
<%= foo_url %> # host!
* Add support for converting blocks into function arguments to JavaScriptGenerator#call and JavaScriptProxy#call. [Sam Stephenson]
* Add JavaScriptGenerator#literal for wrapping a string in an object whose #to_json is the string itself. [Sam Stephenson]
* Add <%= escape_once html %> to escape html while leaving any currently escaped entities alone. Fix button_to double-escaping issue. [Rick]
* Fix double-escaped entities, such as &amp;amp;, &amp;#123;, etc. [Rick]
* Fix routing to correctly determine when generation fails. Closes #6300. [psross].
* Fix broken assert_generates when extra keys are being checked. [Jamis Buck]
* Replace KCODE checks with String#chars for truncate. Closes #6385 [Manfred Stienstra]
* Make page caching respect the format of the resource that is being requested even if the current route is the default route so that, e.g. posts.rss is not transformed by url_for to '/' and subsequently cached as '/index.html' when it should be cached as '/posts.rss'. [Marcel Molina Jr.]
* Use String#chars in TextHelper::excerpt. Closes #6386 [Manfred Stienstra]
* Fix relative URL root matching problems. [Mark Imbriaco]
* Fix filter skipping in controller subclasses. #5949, #6297, #6299 [Martin Emde]
* render_text may optionally append to the response body. render_javascript appends by default. This allows you to chain multiple render :update calls by setting @performed_render = false between them (awaiting a better public API). [Jeremy Kemper]
* Rename test assertion to prevent shadowing. Closes #6306. [psross]
* Fixed that NumberHelper#number_to_delimiter should respect precision of higher than two digits #6231 [phallstrom]
* Fixed that FormHelper#radio_button didn't respect an :id being passed in #6266 [evansj]
* Added an html_options hash parameter to javascript_tag() and update_page_tag() helpers #6311 [tzaharia]. Example:
update_page_tag :defer => 'true' { |page| ... }
Gives:
<script defer="true" type="text/javascript">...</script>
Which is needed for dealing with the IE6 DOM when it's not yet fully loaded.
* Fixed that rescue template path shouldn't be hardcoded, then it's easier to hook in your own #6295 [mnaberez]
* Fixed escaping of backslashes in JavaScriptHelper#escape_javascript #6302 [sven@c3d2.de]
* Fixed that some 500 rescues would cause 500's themselves because the response had not yet been generated #6329 [cmselmer]
* respond_to :html doesn't assume .rhtml. #6281 [Hampton Catlin]
* Fixed some deprecation warnings in ActionPack [Rick Olson]
* assert_select_rjs decodes escaped unicode chars since the Javascript generators encode them. #6240 [japgolly]
* Deprecation: @cookies, @headers, @request, @response will be removed after 1.2. Use the corresponding method instead. [Jeremy Kemper]
* Make the :status parameter expand to the default message for that status code if it is an integer. Also support symbol statuses. [Jamis Buck]. Examples:
head :status => 404 # expands to "404 Not Found"
head :status => :not_found # expands to "404 Not Found"
head :status => :created # expands to "201 Created"
* Add head(options = {}) for responses that have no body. [Jamis Buck]. Examples:
head :status => 404 # return an empty response with a 404 status
head :location => person_path(@person), :status => 201
* Fix bug that kept any before_filter except the first one from being able to halt the before_filter chain. [Rick Olson]
* strip_links is case-insensitive. #6285 [tagoh, Bob Silva]
* Clear the cache of possible controllers whenever Routes are reloaded. [Nicholas Seckar]
* Filters overhaul including meantime filter support using around filters + blocks. #5949 [Martin Emde, Roman Le Negrate, Stefan Kaes, Jeremy Kemper]
* Update CGI process to allow sessions to contain namespaced models. Closes #4638. [dfelstead@site5.com]
* Fix routing to respect user provided requirements and defaults when assigning default routing options (such as :action => 'index'). Closes #5950. [Nicholas Seckar]
* Rescue Errno::ECONNRESET to handle an unexpectedly closed socket connection. Improves SCGI reliability. #3368, #6226 [sdsykes, fhanshaw@vesaria.com]
* Added that respond_to blocks will automatically set the content type to be the same as is requested [DHH]. Examples:
respond_to do |format|
format.html { render :text => "I'm being sent as text/html" }
format.rss { render :text => "I'm being sent as application/rss+xml" }
format.atom { render :text => "I'm being sent as application/xml", :content_type => Mime::XML }
end
* Added utf-8 as the default charset for all renders. You can change this default using ActionController::Base.default_charset=(encoding) [DHH]
* Added proper getters and setters for content type and charset [DHH]. Example of what we used to do:
response.headers["Content-Type"] = "application/atom+xml; charset=utf-8"
...now:
response.content_type = Mime::ATOM
response.charset = "utf-8"
* Declare file extensions exempt from layouts. #6219 [brandon]
Example: ActionController::Base.exempt_from_layout 'rpdf'
* Add chained replace/update support for assert_select_rjs [Rick Olson]
Given RJS like...
page['test1'].replace "<div id=\"1\">foo</div>"
page['test2'].replace_html "<div id=\"2\">foo</div>"
Test it with...
assert_select_rjs :chained_replace
assert_select_rjs :chained_replace, "test1"
assert_select_rjs :chained_replace_html
assert_select_rjs :chained_replace_html, "test2"
* Load helpers in alphabetical order for consistency. Resolve cyclic javascript_helper dependency. #6132, #6178 [choonkeat@gmail.com]
* Skip params with empty names, such as the &=Save query string from <input type="submit"/>. #2569 [manfred, raphinou@yahoo.com]
* Fix assert_tag so that :content => "foo" does not match substrings, but only exact strings. Use :content => /foo/ to match substrings. #2799 [Eric Hodel]
* Update JavaScriptGenerator#show/hide/toggle/remove to new Prototype syntax for multiple ids, #6068 [petermichaux@gmail.com]
* Update UrlWriter to support :only_path. [Nicholas Seckar, Dave Thomas]
* Fixed JavaScriptHelper#link_to_function and JavaScriptHelper#button_to_function to have the script argument be optional [DHH]. So what used to require a nil, like this:
link_to("Hider", nil, :class => "hider_link") { |p| p[:something].hide }
...can be written like this:
link_to("Hider", :class => "hider_link") { |p| p[:something].hide }
* Added access to nested attributes in RJS #4548 [richcollins@gmail.com]. Examples:
page['foo']['style'] # => $('foo').style;
page['foo']['style']['color'] # => $('blank_slate').style.color;
page['foo']['style']['color'] = 'red' # => $('blank_slate').style.color = 'red';
page['foo']['style'].color = 'red' # => $('blank_slate').style.color = 'red';
* Fixed that AssetTagHelper#image_tag and others using compute_public_path should not modify the incoming source argument (closes #5102) [eule@space.ch]
* Deprecated the auto-appending of .png to AssetTagHelper#image_tag calls that doesn't have an extension [DHH]
* Fixed FormOptionsHelper#select to respect :selected value #5813
* Fixed TextHelper#simple_format to deal with multiple single returns within a single paragraph #5835 [moriq@moriq.com]
* Fixed TextHelper#pluralize to handle 1 as a string #5909 [rails@bencurtis.com]
* Improved resolution of DateHelper#distance_of_time_in_words for better precision #5994 [Bob Silva]
* Changed that uncaught exceptions raised any where in the application will cause RAILS_ROOT/public/500.html to be read and shown instead of just the static "Application error (Rails)" [DHH]
* Added deprecation language for pagination which will become a plugin by Rails 2.0 [DHH]
* Added deprecation language for in_place_editor and auto_complete_field that both pieces will become plugins by Rails 2.0 [DHH]
* Deprecated all of ActionController::Dependencies. All dependency loading is now handled from Active Support [DHH]
* Added assert_select* for CSS selector-based testing (deprecates assert_tag) #5936 [assaf.arkin@gmail.com]
* radio_button_tag generates unique id attributes. #3353 [Bob Silva, somekool@gmail.com]
* strip_tags passes through blank args such as nil or "". #2229, #6702 [duncan@whomwah.com, dharana]
* Cleanup assert_tag :children counting. #2181 [jamie@bravenet.com]
* button_to accepts :method so you can PUT and DELETE with it. #6005 [Dan Webb]
* Update sanitize text helper to strip plaintext tags, and <img src="javascript:bang">. [Rick Olson]
* Add routing tests to assert that RoutingError is raised when conditions aren't met. Closes #6016 [Nathan Witmer]
* Make auto_link parse a greater subset of valid url formats. [Jamis Buck]
* Integration tests: headers beginning with X aren't excluded from the HTTP_ prefix, so X-Requested-With becomes HTTP_X_REQUESTED_WITH as expected. [Mike Clark]
* Switch to using FormEncodedPairParser for parsing request parameters. [Nicholas Seckar, DHH]
* respond_to .html now always renders #{action_name}.rhtml so that registered custom template handlers do not override it in priority. Custom mime types require a block and throw proper error now. [Tobias Luetke]
* Deprecation: test deprecated instance vars in partials. [Jeremy Kemper]
* Add UrlWriter to allow writing urls from Mailers and scripts. [Nicholas Seckar]
* Relax Routing's anchor pattern warning; it was preventing use of [^/] inside restrictions. [Nicholas Seckar]
* Add controller_paths variable to Routing. [Nicholas Seckar]
* Fix assert_redirected_to issue with named routes for module controllers. [Rick Olson]
* Tweak RoutingError message to show option diffs, not just missing named route significant keys. [Rick Olson]
* Invoke method_missing directly on hidden actions. Closes #3030. [Nicholas Seckar]
* Add RoutingError exception when RouteSet fails to generate a path from a Named Route. [Rick Olson]
* Replace Reloadable with Reloadable::Deprecated. [Nicholas Seckar]
* Deprecation: check whether instance variables have been monkeyed with before assigning them to deprecation proxies. Raises a RuntimeError if so. [Jeremy Kemper]
* Add support for the param_name parameter to the auto_complete_field helper. #5026 [david.a.williams@gmail.com]
* Deprecation! @params, @session, @flash will be removed after 1.2. Use the corresponding instance methods instead. You'll get printed warnings during tests and logged warnings in dev mode when you access either instance variable directly. [Jeremy Kemper]
* Make Routing noisy when an anchor regexp is assigned to a segment. #5674 [francois.beausoleil@gmail.com]
* Added months and years to the resolution of DateHelper#distance_of_time_in_words, such that "60 days ago" becomes "2 months ago" #5611 [pjhyett@gmail.com]
* Make controller_path available as an instance method. #5724 [jmckible@gmail.com]
* Update query parser to support adjacent hashes. [Nicholas Seckar]
* Make action caching aware of different formats for the same action so that, e.g. foo.xml is cached separately from foo.html. Implicitly set content type when reading in cached content with mime revealing extensions so the entire onous isn't on the webserver. [Marcel Molina Jr.]
* Restrict Request Method hacking with ?_method to POST requests. [Rick Olson]
* Fixed the new_#{resource}_url route and added named route tests for Simply Restful. [Rick Olson]
* Added map.resources from the Simply Restful plugin [DHH]. Examples (the API has changed to use plurals!):
map.resources :messages
map.resources :messages, :comments
map.resources :messages, :new => { :preview => :post }
* Fixed that integration simulation of XHRs should set Accept header as well [Edward Frederick]
* TestRequest#reset_session should restore a TestSession, not a hash [Koz]
* Don't search a load-path of '.' for controller files [Jamis Buck]
* Update integration.rb to require test_process explicitly instead of via Dependencies. [Nicholas Seckar]
* Fixed that you can still access the flash after the flash has been reset in reset_session. Closes #5584 [lmarlow@yahoo.com]
* Allow form_for and fields_for to work with indexed form inputs. [Jeremy Kemper, Matt Lyon]
<% form_for 'post[]', @post do |f| -%>
<% end -%>
* Remove leak in development mode by replacing define_method with module_eval. [Nicholas Seckar]
* Provide support for decimal columns to form helpers. Closes #5672. [dave@pragprog.com]
* Pass :id => nil or :class => nil to error_messages_for to supress that html attribute. #3586 [olivier_ansaldi@yahoo.com, sebastien@goetzilla.info]
* Reset @html_document between requests so assert_tag works. #4810 [jarkko@jlaine.net, easleydp@gmail.com]
* Integration tests behave well with render_component. #4632 [edward.frederick@revolution.com, dev.rubyonrails@maxdunn.com]
* Added exception handling of missing layouts #5373 [chris@ozmm.org]
* Fixed that real files and symlinks should be treated the same when compiling templates #5438 [zachary@panandscan.com]
* Fixed that the flash should be reset when reset_session is called #5584 [shugo@ruby-lang.org]
* Added special case for "1 Byte" in NumberHelper#number_to_human_size #5593 [murpyh@rubychan.de]
* Fixed proper form-encoded parameter parsing for requests with "Content-Type: application/x-www-form-urlencoded; charset=utf-8" (note the presence of a charset directive) [DHH]
* Add route_name_path method to generate only the path for a named routes. For example, map.person will add person_path. [Nicholas Seckar]
* Avoid naming collision among compiled view methods. [Jeremy Kemper]
* Fix CGI extensions when they expect string but get nil in Windows. Closes #5276 [mislav@nippur.irb.hr]
* Determine the correct template_root for deeply nested components. #2841 [s.brink@web.de]
* Fix that routes with *path segments in the recall can generate URLs. [Rick]
* Fix strip_links so that it doesn't hang on multiline <acronym> tags [Jamis Buck]
* Remove problematic control chars in rescue template. #5316 [Stefan Kaes]
* Make sure passed routing options are not mutated by routing code. #5314 [Blair Zajac]
* Make sure changing the controller from foo/bar to bing/bang does not change relative to foo. [Jamis Buck]
* Escape the path before routing recognition. #3671
* Make sure :id and friends are unescaped properly. #5275 [me@julik.nl]
* Rewind readable CGI params so others may reread them (such as CGI::Session when passing the session id in a multipart form). #210 [mklame@atxeu.com, matthew@walker.wattle.id.au]
* Added Mime::TEXT (text/plain) and Mime::ICS (text/calendar) as new default types [DHH]
* Added Mime::Type.register(string, symbol, synonyms = []) for adding new custom mime types [DHH]. Example: Mime::Type.register("image/gif", :gif)
* Added support for Mime objects in render :content_type option [DHH]. Example: render :text => some_atom, :content_type => Mime::ATOM
* Add :status option to send_data and send_file. Defaults to '200 OK'. #5243 [Manfred Stienstra <m.stienstra@fngtps.com>]
* Routing rewrite. Simpler, faster, easier to understand. The published API for config/routes.rb is unchanged, but nearly everything else is different, so expect breakage in plugins and libs that try to fiddle with routes. [Nicholas Seckar, Jamis Buck]
map.connect '/foo/:id', :controller => '...', :action => '...'
map.connect '/foo/:id.:format', :controller => '...', :action => '...'
map.connect '/foo/:id', ..., :conditions => { :method => :get }
* Cope with missing content type and length headers. Parse parameters from multipart and urlencoded request bodies only. [Jeremy Kemper]
* Accept multipart PUT parameters. #5235 [guy.naor@famundo.com]
* Added interrogation of params[:format] to determine Accept type. If :format is specified and matches a declared extension, like "rss" or "xml", that mime type will be put in front of the accept handler. This means you can link to the same action from different extensions and use that fact to determine output [DHH]. Example:
class WeblogController < ActionController::Base
def index
@posts = Post.find :all
respond_to do |format|
format.html
format.xml { render :xml => @posts.to_xml }
format.rss { render :action => "feed.rxml" }
end
end
end
# returns HTML when requested by a browser, since the browser
# has the HTML mimetype at the top of its priority list
Accept: text/html
GET /weblog
# returns the XML
Accept: application/xml
GET /weblog
# returns the HTML
Accept: application/xml
GET /weblog.html
# returns the XML
Accept: text/html
GET /weblog.xml
All this relies on the fact that you have a route that includes .:format.
* Expanded :method option in FormTagHelper#form_tag, FormHelper#form_for, PrototypeHelper#remote_form_for, PrototypeHelper#remote_form_tag, and PrototypeHelper#link_to_remote to allow for verbs other than GET and POST by automatically creating a hidden form field named _method, which will simulate the other verbs over post [DHH]
* Added :method option to UrlHelper#link_to, which allows for using other verbs than GET for the link. This replaces the :post option, which is now deprecated. Example: link_to "Destroy", person_url(:id => person), :method => :delete [DHH]
* follow_redirect doesn't complain about being redirected to the same controller. #5153 [dymo@mk.ukrtelecom.ua]
* Add layout attribute to response object with the name of the layout that was rendered, or nil if none rendered. [Kevin Clark kevin.clark@gmail.com]
* Fix NoMethodError when parsing params like &&. [Adam Greenfield]
* form.text_area handles the :size option just like the original text_area (:size => '60x10' becomes cols="60" rows="10"). [Jeremy Kemper]
* Excise ingrown code from FormOptionsHelper#options_for_select. #5008 [anonymous]
* Small fix in routing to allow dynamic routes (broken after [4242]) [Rick]
map.connect '*path', :controller => 'files', :action => 'show'
* Use #flush between switching from #write to #syswrite. Closes #4907. [Blair Zajac <blair@orcaware.com>]
* Allow error_messages_for to report errors for multiple objects, as well as support for customizing the name of the object in the error summary header. Closes #4186. [andrew@redlinesoftware.com, Marcel Molina Jr.]
error_messages_for :account, :user, :subscription, :object_name => :account
* Fix assert_redirected_to tests according to real-world usage. Also, don't fail if you add an extra :controller option: [Rick]
redirect_to :action => 'new'
assert_redirected_to :controller => 'monkeys', :action => 'new'
* Diff compared routing options. Allow #assert_recognizes to take a second arg as a hash to specify optional request method [Rick]
assert_recognizes({:controller => 'users', :action => 'index'}, 'users')
assert_recognizes({:controller => 'users', :action => 'create'}, {:path => 'users', :method => :post})
* Diff compared options with #assert_redirected_to [Rick]
* Add support in routes for semicolon delimited "subpaths", like /books/:id;:action [Jamis Buck]
* Change link_to_function and button_to_function to (optionally) take an update_page block instead of a JavaScript string. Closes #4804. [zraii@comcast.net, Sam Stephenson]
* Modify routing so that you can say :require => { :method => :post } for a route, and the route will never be selected unless the request method is POST. Only works for route recognition, not for route generation. [Jamis Buck]
* Added :add_headers option to verify which merges a hash of name/value pairs into the response's headers hash if the prerequisites cannot be satisfied. [Sam Stephenson]
ex. verify :only => :speak, :method => :post,
:render => { :status => 405, :text => "Must be post" },
:add_headers => { "Allow" => "POST" }
*1.12.5* (August 10th, 2006) *1.12.5* (August 10th, 2006)
* Updated security fix * Updated security fix
@ -5,24 +559,6 @@
*1.12.4* (August 8th, 2006) *1.12.4* (August 8th, 2006)
* Documentation fix: integration test scripts don't require integration_test. #4914 [Frederick Ros <sl33p3r@free.fr>]
* ActionController::Base Summary documentation rewrite. #4900 [kevin.clark@gmail.com]
* Fix text_helper.rb documentation rendering. #4725 [Frederick Ros]
* Fixes bad rendering of JavaScriptMacrosHelper rdoc. #4910 [Frederick Ros]
* Enhance documentation for setting headers in integration tests. Skip auto HTTP prepending when its already there. #4079 [Rick Olson]
* Documentation for AbstractRequest. #4895 [kevin.clark@gmail.com]
* Remove all remaining references to @params in the documentation. [Marcel Molina Jr.]
* Add documentation for redirect_to :back's RedirectBackError exception. [Marcel Molina Jr.]
* Update layout and content_for documentation to use yield rather than magic @content_for instance variables. [Marcel Molina Jr.]
* Cache CgiRequest#request_parameters so that multiple calls don't re-parse multipart data. [Rick] * Cache CgiRequest#request_parameters so that multiple calls don't re-parse multipart data. [Rick]
* Fixed that remote_form_for can leave out the object parameter and default to the instance variable of the object_name, just like form_for [DHH] * Fixed that remote_form_for can leave out the object parameter and default to the instance variable of the object_name, just like form_for [DHH]
@ -1928,7 +2464,7 @@ Default YAML web services were retired, ActionController::Base.param_parsers car
Before: Before:
module WeblogHelper module WeblogHelper
def self.append_features(controller) #:nodoc: def self.included(controller) #:nodoc:
controller.ancestors.include?(ActionController::Base) ? controller.add_template_helper(self) : super controller.ancestors.include?(ActionController::Base) ? controller.add_template_helper(self) : super
end end
end end
@ -2286,9 +2822,9 @@ Default YAML web services were retired, ActionController::Base.param_parsers car
* Added pluralize method to the TextHelper that makes it easy to get strings like "1 message", "3 messages" * Added pluralize method to the TextHelper that makes it easy to get strings like "1 message", "3 messages"
* Added proper escaping for the rescues [Andreas Schwartz] * Added proper escaping for the rescues [Andreas Schwarz]
* Added proper escaping for the option and collection tags [Andreas Schwartz] * Added proper escaping for the option and collection tags [Andreas Schwarz]
* Fixed NaN errors on benchmarking [Jim Weirich] * Fixed NaN errors on benchmarking [Jim Weirich]

View file

@ -1,4 +1,4 @@
Copyright (c) 2004 David Heinemeier Hansson Copyright (c) 2004-2006 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the a copy of this software and associated documentation files (the

View file

@ -34,7 +34,7 @@ A short rundown of the major features:
and can therefore share helper methods. and can therefore share helper methods.
BlogController < ActionController::Base BlogController < ActionController::Base
def display def show
@customer = find_customer @customer = find_customer
end end
@ -100,7 +100,7 @@ A short rundown of the major features:
after_filter { |c| c.response.body = GZip::compress(c.response.body) } after_filter { |c| c.response.body = GZip::compress(c.response.body) }
after_filter LocalizeFilter after_filter LocalizeFilter
def list def index
# Before this action is run, the user will be authenticated, the cache # Before this action is run, the user will be authenticated, the cache
# will be examined to see if a valid copy of the results already # will be examined to see if a valid copy of the results already
# exists, and the action will be logged for auditing. # exists, and the action will be logged for auditing.
@ -139,7 +139,7 @@ A short rundown of the major features:
end end
Layout file (called weblog_layout): Layout file (called weblog_layout):
<html><body><%= @content_for_layout %></body></html> <html><body><%= yield %></body></html>
Template for hello_world action: Template for hello_world action:
<h1>Hello world</h1> <h1>Hello world</h1>
@ -155,7 +155,7 @@ A short rundown of the major features:
map.connect 'clients/:client_name/:project_name/:controller/:action' map.connect 'clients/:client_name/:project_name/:controller/:action'
Accessing /clients/37signals/basecamp/project/dash calls ProjectController#dash with Accessing /clients/37signals/basecamp/project/dash calls ProjectController#dash with
{ "client_name" => "37signals", "project_name" => "basecamp" } in @params["params"] { "client_name" => "37signals", "project_name" => "basecamp" } in params[:params]
From that URL, you can rewrite the redirect in a number of ways: From that URL, you can rewrite the redirect in a number of ways:
@ -296,9 +296,8 @@ A short rundown of the major features:
{Learn more}[link:classes/ActionController/Rescue.html] {Learn more}[link:classes/ActionController/Rescue.html]
* Scaffolding for Action Record model objects * Scaffolding for Active Record model objects
require 'account' # must be an Active Record class
class AccountController < ActionController::Base class AccountController < ActionController::Base
scaffold :account scaffold :account
end end
@ -306,7 +305,7 @@ A short rundown of the major features:
The AccountController now has the full CRUD range of actions and default The AccountController now has the full CRUD range of actions and default
templates: list, show, destroy, new, create, edit, update templates: list, show, destroy, new, create, edit, update
{Learn more}link:classes/ActionController/Scaffolding/ClassMethods.html {Learn more}[link:classes/ActionController/Scaffolding/ClassMethods.html]
* Form building for Active Record model objects * Form building for Active Record model objects
@ -338,10 +337,10 @@ A short rundown of the major features:
<input type="submit" value="Create"> <input type="submit" value="Create">
</form> </form>
This form generates a @params["post"] array that can be used directly in a save action: This form generates a params[:post] array that can be used directly in a save action:
class WeblogController < ActionController::Base class WeblogController < ActionController::Base
def save def create
post = Post.create(params[:post]) post = Post.create(params[:post])
redirect_to :action => "display", :id => post.id redirect_to :action => "display", :id => post.id
end end
@ -350,10 +349,10 @@ A short rundown of the major features:
{Learn more}[link:classes/ActionView/Helpers/ActiveRecordHelper.html] {Learn more}[link:classes/ActionView/Helpers/ActiveRecordHelper.html]
* Runs on top of WEBrick, CGI, FCGI, and mod_ruby * Runs on top of WEBrick, Mongrel, CGI, FCGI, and mod_ruby
== Simple example == Simple example (from outside of Rails)
This example will implement a simple weblog system using inline templates and This example will implement a simple weblog system using inline templates and
an Active Record model. So let's build that WeblogController with just a few an Active Record model. So let's build that WeblogController with just a few
@ -366,11 +365,11 @@ methods:
layout "weblog/layout" layout "weblog/layout"
def index def index
@posts = Post.find_all @posts = Post.find(:all)
end end
def display def display
@post = Post.find(:params[:id]) @post = Post.find(params[:id])
end end
def new def new
@ -394,7 +393,7 @@ And the templates look like this:
weblog/layout.rhtml: weblog/layout.rhtml:
<html><body> <html><body>
<%= @content_for_layout %> <%= yield %>
</body></html> </body></html>
weblog/index.rhtml: weblog/index.rhtml:
@ -431,6 +430,8 @@ template casing from content.
Please note that you might need to change the "shebang" line to Please note that you might need to change the "shebang" line to
#!/usr/local/env ruby, if your Ruby is not placed in /usr/local/bin/ruby #!/usr/local/env ruby, if your Ruby is not placed in /usr/local/bin/ruby
Also note that these examples are all for demonstrating using Action Pack on
its own. Not for when it's used inside of Rails.
== Download == Download
@ -440,7 +441,7 @@ The latest version of Action Pack can be found at
Documentation can be found at Documentation can be found at
* http://ap.rubyonrails.com * http://api.rubyonrails.com
== Installation == Installation
@ -459,13 +460,10 @@ Action Pack is released under the MIT license.
== Support == Support
The Action Pack homepage is http://www.rubyonrails.com. You can find The Action Pack homepage is http://www.rubyonrails.org. You can find
the Action Pack RubyForge page at http://rubyforge.org/projects/actionpack. the Action Pack RubyForge page at http://rubyforge.org/projects/actionpack.
And as Jim from Rake says: And as Jim from Rake says:
Feel free to submit commits or feature requests. If you send a patch, Feel free to submit commits or feature requests. If you send a patch,
remember to update the corresponding unit tests. If fact, I prefer remember to update the corresponding unit tests. If fact, I prefer
new feature to be submitted in the form of new unit tests. new feature to be submitted in the form of new unit tests.
For other information, feel free to ask on the ruby-talk mailing list (which
is mirrored to comp.lang.ruby) or contact mailto:david@loudthinking.com.

View file

@ -22,11 +22,14 @@ task :default => [ :test ]
# Run the unit tests # Run the unit tests
Rake::TestTask.new { |t| desc "Run all unit tests"
task :test => [:test_action_pack, :test_active_record_integration]
Rake::TestTask.new(:test_action_pack) { |t|
t.libs << "test" t.libs << "test"
# make sure we include the controller tests (c*) first as on some systems # make sure we include the controller tests (c*) first as on some systems
# this will not happen automatically and the tests (as a whole) will error # this will not happen automatically and the tests (as a whole) will error
t.test_files=Dir.glob( "test/c*/*_test.rb" ) + Dir.glob( "test/[ft]*/*_test.rb" ) t.test_files=Dir.glob( "test/c*/**/*_test.rb" ) + Dir.glob( "test/[ft]*/*_test.rb" )
# t.pattern = 'test/*/*_test.rb' # t.pattern = 'test/*/*_test.rb'
t.verbose = true t.verbose = true
} }
@ -72,12 +75,12 @@ spec = Gem::Specification.new do |s|
s.has_rdoc = true s.has_rdoc = true
s.requirements << 'none' s.requirements << 'none'
s.add_dependency('activesupport', '= 1.3.1' + PKG_BUILD) s.add_dependency('activesupport', '= 1.4.1' + PKG_BUILD)
s.require_path = 'lib' s.require_path = 'lib'
s.autorequire = 'action_controller' s.autorequire = 'action_controller'
s.files = [ "Rakefile", "install.rb", "filler.txt", "README", "RUNNING_UNIT_TESTS", "CHANGELOG", "MIT-LICENSE", "examples/.htaccess" ] s.files = [ "Rakefile", "install.rb", "README", "RUNNING_UNIT_TESTS", "CHANGELOG", "MIT-LICENSE", "examples/.htaccess" ]
dist_dirs.each do |dir| dist_dirs.each do |dir|
s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) } s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
end end

View file

@ -28,11 +28,11 @@ class AddressBookController < ActionController::Base
end end
def person def person
@person = @address_book.find_person(@params["id"]) @person = @address_book.find_person(params[:id])
end end
def create_person def create_person
@address_book.create_person(@params["person"]) @address_book.create_person(params[:person])
redirect_to :action => "index" redirect_to :action => "index"
end end

View file

@ -14,7 +14,7 @@ class BlogController < ActionController::Base
render_template <<-"EOF" render_template <<-"EOF"
<html><body> <html><body>
<%= @flash["alert"] %> <%= flash["alert"] %>
<h1>Posts</h1> <h1>Posts</h1>
<% @posts.each do |post| %> <% @posts.each do |post| %>
<p><b><%= post.title %></b><br /><%= post.body %></p> <p><b><%= post.title %></b><br /><%= post.body %></p>
@ -32,7 +32,7 @@ class BlogController < ActionController::Base
end end
def create def create
@session["posts"].unshift(Post.new(@params["post"]["title"], @params["post"]["body"])) @session["posts"].unshift(Post.new(params[:post][:title], params[:post][:body]))
flash["alert"] = "New post added!" flash["alert"] = "New post added!"
redirect_to :action => "index" redirect_to :action => "index"
end end

View file

@ -25,19 +25,19 @@ class DebateController < ActionController::Base
end end
def topic def topic
@topic = @debate.find_topic(@params["id"]) @topic = @debate.find_topic(params[:id])
end end
# def new_topic() end <-- This is not needed as the template doesn't require any assigns # def new_topic() end <-- This is not needed as the template doesn't require any assigns
def create_topic def create_topic
@debate.create_topic(@params["topic"]) @debate.create_topic(params[:topic])
redirect_to :action => "index" redirect_to :action => "index"
end end
def create_reply def create_reply
@debate.create_reply(@params["reply"]) @debate.create_reply(params[:reply])
redirect_to :action => "topic", :path_params => { "id" => @params["reply"]["topic_id"] } redirect_to :action => "topic", :path_params => { "id" => params[:reply][:topic_id] }
end end
private private

View file

@ -1,5 +1,5 @@
#-- #--
# Copyright (c) 2004 David Heinemeier Hansson # Copyright (c) 2004-2006 David Heinemeier Hansson
# #
# Permission is hereby granted, free of charge, to any person obtaining # Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the # a copy of this software and associated documentation files (the
@ -30,7 +30,7 @@ unless defined?(ActiveSupport)
require 'active_support' require 'active_support'
rescue LoadError rescue LoadError
require 'rubygems' require 'rubygems'
require_gem 'activesupport' gem 'activesupport'
end end
end end
@ -43,7 +43,7 @@ require 'action_controller/benchmarking'
require 'action_controller/flash' require 'action_controller/flash'
require 'action_controller/filters' require 'action_controller/filters'
require 'action_controller/layout' require 'action_controller/layout'
require 'action_controller/dependencies' require 'action_controller/deprecated_dependencies'
require 'action_controller/mime_responds' require 'action_controller/mime_responds'
require 'action_controller/pagination' require 'action_controller/pagination'
require 'action_controller/scaffolding' require 'action_controller/scaffolding'

View file

@ -1,320 +1,82 @@
require 'test/unit' require 'test/unit'
require 'test/unit/assertions' require 'test/unit/assertions'
require 'rexml/document'
require File.dirname(__FILE__) + "/vendor/html-scanner/html/document"
module Test #:nodoc: module ActionController #:nodoc:
module Unit #:nodoc: # In addition to these specific assertions, you also have easy access to various collections that the regular test/unit assertions
# 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:
# can be used against. These collections are: #
# # * assigns: Instance variables assigned in the action that are available for the view.
# * assigns: Instance variables assigned in the action that are available for the view. # * session: Objects being saved in the session.
# * session: Objects being saved in the session. # * flash: The flash objects currently in the session.
# * flash: The flash objects currently in the session. # * cookies: Cookies being sent to the user on this request.
# * cookies: Cookies being sent to the user on this request. #
# # These collections can be used just like any other hash:
# 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_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_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
# 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
# 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.
# 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.
# 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.
# 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
# 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.
# action call which can then be asserted against. #
# # == Manipulating the request collections
# == 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
# 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
# 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:
# and cookies, though. For sessions, you just do: #
# # @request.session[:key] = "value"
# @request.session[:key] = "value" #
# # For cookies, you need to manually create the cookie, like this:
# For cookies, you need to manually create the cookie, like this: #
# # @request.cookies["key"] = CGI::Cookie.new("key", "value")
# @request.cookies["key"] = CGI::Cookie.new("key", "value") #
# # == Testing named routes
# == 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.
# If you're using named routes, they can be easily tested using the original named routes methods straight in the test case. # Example:
# Example: #
# # assert_redirected_to page_url(:title => 'foo')
# assert_redirected_to page_url(:title => 'foo') module Assertions
module Assertions def self.included(klass)
# Asserts that the response is one of the following types: klass.class_eval do
# include ActionController::Assertions::ResponseAssertions
# * <tt>:success</tt>: Status code was 200 include ActionController::Assertions::SelectorAssertions
# * <tt>:redirect</tt>: Status code was in the 300-399 range include ActionController::Assertions::RoutingAssertions
# * <tt>:missing</tt>: Status code was 404 include ActionController::Assertions::TagAssertions
# * <tt>:error</tt>: Status code was in the 500-599 range include ActionController::Assertions::DomAssertions
# include ActionController::Assertions::ModelAssertions
# You can also pass an explicit status code number as the type, like assert_response(501) include ActionController::Assertions::DeprecatedAssertions
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 end
end
# Assert that the redirection options passed in match those of the redirect called in the latest action. This match can be partial, def clean_backtrace(&block)
# such that assert_redirected_to(:controller => "weblog") will also match the redirection of yield
# redirect_to(:controller => "weblog", :action => "show") and so on. rescue Test::Unit::AssertionFailedError => e
def assert_redirected_to(options = {}, message=nil) path = File.expand_path(__FILE__)
clean_backtrace do raise Test::Unit::AssertionFailedError, e.message, e.backtrace.reject { |line| File.expand_path(line) =~ /#{path}/ }
assert_response(:redirect, message) end
end
if options.is_a?(String) end
msg = build_message(message, "expected a redirect to <?>, found one to <?>", options, @response.redirect_url)
url_regexp = %r{^(\w+://.*?(/|$|\?))(.*)$} require File.dirname(__FILE__) + '/assertions/response_assertions'
eurl, epath, url, path = [options, @response.redirect_url].collect do |url| require File.dirname(__FILE__) + '/assertions/selector_assertions'
u, p = (url_regexp =~ url) ? [$1, $3] : [nil, url] require File.dirname(__FILE__) + '/assertions/tag_assertions'
[u, (p[0..0] == '/') ? p : '/' + p] require File.dirname(__FILE__) + '/assertions/dom_assertions'
end.flatten require File.dirname(__FILE__) + '/assertions/routing_assertions'
require File.dirname(__FILE__) + '/assertions/model_assertions'
assert_equal(eurl, url, msg) if eurl && url require File.dirname(__FILE__) + '/assertions/deprecated_assertions'
assert_equal(epath, path, msg) if epath && path
else module Test #:nodoc:
@response_diff = options.diff(@response.redirected_to) if options.is_a?(Hash) && @response.redirected_to.is_a?(Hash) module Unit #:nodoc:
msg = build_message(message, "response is not a redirection to all of the options supplied (redirection is <?>)#{', difference: <?>' if @response_diff}", class TestCase #:nodoc:
@response.redirected_to || @response.redirect_url, @response_diff) include ActionController::Assertions
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):
#
# * <tt>:tag</tt>: the node type must match the corresponding value
# * <tt>:attributes</tt>: a hash. The node's attributes must match the
# corresponding values in the hash.
# * <tt>:parent</tt>: a hash. The node's parent must match the
# corresponding hash.
# * <tt>:child</tt>: a hash. At least one of the node's immediate children
# must meet the criteria described by the hash.
# * <tt>:ancestor</tt>: a hash. At least one of the node's ancestors must
# meet the criteria described by the hash.
# * <tt>:descendant</tt>: a hash. At least one of the node's descendants
# must meet the criteria described by the hash.
# * <tt>:sibling</tt>: a hash. At least one of the node's siblings must
# meet the criteria described by the hash.
# * <tt>:after</tt>: a hash. The node must be after any sibling meeting
# the criteria described by the hash, and at least one sibling must match.
# * <tt>:before</tt>: a hash. The node must be before any sibling meeting
# the criteria described by the hash, and at least one sibling must match.
# * <tt>:children</tt>: a hash, for counting children of a node. Accepts
# the keys:
# * <tt>:count</tt>: either a number or a range which must equal (or
# include) the number of children that match.
# * <tt>:less_than</tt>: the number of matching children must be less
# than this number.
# * <tt>:greater_than</tt>: the number of matching children must be
# greater than this number.
# * <tt>:only</tt>: another hash consisting of the keys to use
# to match on the children, and only matching children will be
# counted.
# * <tt>:content</tt>: 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/ }
#
# <strong>Please note</strong: #assert_tag and #assert_no_tag only work
# with well-formed XHTML. They recognize a few tags as implicitly self-closing
# (like br and hr and such) but will not work correctly with tags
# that allow optional closing tags (p, li, td). <em>You must explicitly
# close all of your tags to use these assertions.</em>
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 end
end end

View file

@ -2,8 +2,9 @@ require 'action_controller/mime_type'
require 'action_controller/request' require 'action_controller/request'
require 'action_controller/response' require 'action_controller/response'
require 'action_controller/routing' require 'action_controller/routing'
require 'action_controller/code_generation' require 'action_controller/resources'
require 'action_controller/url_rewriter' require 'action_controller/url_rewriter'
require 'action_controller/status_codes'
require 'drb' require 'drb'
require 'set' require 'set'
@ -27,6 +28,8 @@ module ActionController #:nodoc:
end end
class MissingFile < ActionControllerError #:nodoc: class MissingFile < ActionControllerError #:nodoc:
end end
class RenderError < ActionControllerError #:nodoc:
end
class SessionOverflowError < ActionControllerError #:nodoc: 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.' 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.'
@ -42,7 +45,7 @@ module ActionController #:nodoc:
end end
end end
class RedirectBackError < ActionControllerError #:nodoc: 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"].' 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) def initialize(message = nil)
super(message || DEFAULT_MESSAGE) super(message || DEFAULT_MESSAGE)
@ -206,7 +209,8 @@ module ActionController #:nodoc:
class Base class Base
DEFAULT_RENDER_STATUS_CODE = "200 OK" DEFAULT_RENDER_STATUS_CODE = "200 OK"
include Reloadable::Subclasses include Reloadable::Deprecated
include StatusCodes
# Determines whether the view has access to controller internals @request, @response, @session, and @template. # Determines whether the view has access to controller internals @request, @response, @session, and @template.
# By default, it does. # By default, it does.
@ -242,7 +246,7 @@ module ActionController #:nodoc:
cattr_accessor :allow_concurrency cattr_accessor :allow_concurrency
# Modern REST web services often need to submit complex data to the web application. # 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 # The param_parsers hash lets you register handlers which will process the http body and add parameters to the
# <tt>params</tt> hash. These handlers are invoked for post and put requests. # <tt>params</tt> 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 # By default application/xml is enabled. A XmlSimple class with the same param name as the root will be instanciated
@ -270,6 +274,10 @@ module ActionController #:nodoc:
@@param_parsers = { Mime::XML => :xml_simple } @@param_parsers = { Mime::XML => :xml_simple }
cattr_accessor :param_parsers cattr_accessor :param_parsers
# Controls the default charset for all renders.
@@default_charset = "utf-8"
cattr_accessor :default_charset
# Template root determines the base from which template references will be made. So a call to render("test/template") # 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". # will be converted to "#{template_root}/test/template.rhtml".
class_inheritable_accessor :template_root class_inheritable_accessor :template_root
@ -286,25 +294,25 @@ module ActionController #:nodoc:
# Holds the request object that's primarily used to get environment variables through access like # Holds the request object that's primarily used to get environment variables through access like
# <tt>request.env["REQUEST_URI"]</tt>. # <tt>request.env["REQUEST_URI"]</tt>.
attr_accessor :request attr_internal :request
# Holds a hash of all the GET, POST, and Url parameters passed to the action. Accessed like <tt>params["post_id"]</tt> # Holds a hash of all the GET, POST, and Url parameters passed to the action. Accessed like <tt>params["post_id"]</tt>
# to get the post_id. No type casts are made, so all values are returned as strings. # to get the post_id. No type casts are made, so all values are returned as strings.
attr_accessor :params attr_internal :params
# Holds the response object that's primarily used to set additional HTTP headers through access like # Holds the response object that's primarily used to set additional HTTP headers through access like
# <tt>response.headers["Cache-Control"] = "no-cache"</tt>. Can also be used to access the final body HTML after a template # <tt>response.headers["Cache-Control"] = "no-cache"</tt>. Can also be used to access the final body HTML after a template
# has been rendered through response.body -- useful for <tt>after_filter</tt>s that wants to manipulate the output, # has been rendered through response.body -- useful for <tt>after_filter</tt>s that wants to manipulate the output,
# such as a OutputCompressionFilter. # such as a OutputCompressionFilter.
attr_accessor :response attr_internal :response
# Holds a hash of objects in the session. Accessed like <tt>session[:person]</tt> to get the object tied to the "person" # Holds a hash of objects in the session. Accessed like <tt>session[:person]</tt> 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. # key. The session will hold any type of object as values, but the key should be a string or symbol.
attr_accessor :session attr_internal :session
# Holds a hash of header names and values. Accessed like <tt>headers["Cache-Control"]</tt> to get the value of the Cache-Control # Holds a hash of header names and values. Accessed like <tt>headers["Cache-Control"]</tt> to get the value of the Cache-Control
# directive. Values should always be specified as strings. # directive. Values should always be specified as strings.
attr_accessor :headers attr_internal :headers
# Holds the hash of variables that are passed on to the template class to be made available to the view. This hash # 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. # is generated by taking a snapshot of all the instance variables in the current scope just before a template is rendered.
@ -313,6 +321,9 @@ module ActionController #:nodoc:
# Returns the name of the action this controller is processing. # Returns the name of the action this controller is processing.
attr_accessor :action_name attr_accessor :action_name
# Templates that are exempt from layouts
@@exempt_from_layout = Set.new([/\.rjs$/])
class << self class << self
# Factory for the standard create, process loop where the controller is discarded after processing. # Factory for the standard create, process loop where the controller is discarded after processing.
def process(request, response) #:nodoc: def process(request, response) #:nodoc:
@ -393,6 +404,17 @@ module ActionController #:nodoc:
filtered_parameters filtered_parameters
end end
end end
# Don't render layouts for templates with the given extensions.
def exempt_from_layout(*extensions)
@@exempt_from_layout.merge extensions.collect { |extension|
if extension.is_a?(Regexp)
extension
else
/\.#{Regexp.escape(extension.to_s)}$/
end
}
end
end end
public public
@ -407,6 +429,7 @@ module ActionController #:nodoc:
log_processing log_processing
send(method, *arguments) send(method, *arguments)
assign_default_content_type_and_charset
response response
ensure ensure
process_cleanup process_cleanup
@ -421,7 +444,7 @@ module ActionController #:nodoc:
# * <tt>:anchor</tt> -- specifies the anchor name to be appended to the path. For example, # * <tt>:anchor</tt> -- specifies the anchor name to be appended to the path. For example,
# <tt>url_for :controller => 'posts', :action => 'show', :id => 10, :anchor => 'comments'</tt> # <tt>url_for :controller => 'posts', :action => 'show', :id => 10, :anchor => 'comments'</tt>
# will produce "/posts/show/10#comments". # will produce "/posts/show/10#comments".
# * <tt>:only_path</tt> -- if true, returns the absolute URL (omitting the protocol, host name, and port) # * <tt>:only_path</tt> -- if true, returns the relative URL (omitting the protocol, host name, and port) (<tt>false</tt> by default)
# * <tt>:trailing_slash</tt> -- if true, adds a trailing slash, as in "/archive/2005/". Note that this # * <tt>:trailing_slash</tt> -- if true, adds a trailing slash, as in "/archive/2005/". Note that this
# is currently not recommended since it breaks caching. # is currently not recommended since it breaks caching.
# * <tt>:host</tt> -- overrides the default (current) host if provided # * <tt>:host</tt> -- overrides the default (current) host if provided
@ -483,9 +506,20 @@ module ActionController #:nodoc:
# would have slashed-off the path components after the changed action. # would have slashed-off the path components after the changed action.
def url_for(options = {}, *parameters_for_method_reference) #:doc: def url_for(options = {}, *parameters_for_method_reference) #:doc:
case options case options
when String then options when String
when Symbol then send(options, *parameters_for_method_reference) options
when Hash then @url.rewrite(rewrite_options(options))
when Symbol
ActiveSupport::Deprecation.warn(
"You called url_for(:#{options}), which is a deprecated API call. Instead you should use the named " +
"route directly, like #{options}(). Using symbols and parameters with url_for will be removed from Rails 2.0.",
caller
)
send(options, *parameters_for_method_reference)
when Hash
@url.rewrite(rewrite_options(options))
end end
end end
@ -499,6 +533,11 @@ module ActionController #:nodoc:
self.class.controller_name self.class.controller_name
end end
# Converts the class name from something like "OneModule::TwoModule::NeatController" to "one_module/two_module/neat".
def controller_path
self.class.controller_path
end
def session_enabled? def session_enabled?
request.session_options[:disabled] != false request.session_options[:disabled] != false
end end
@ -528,27 +567,34 @@ module ActionController #:nodoc:
# #
# === Rendering partials # === Rendering partials
# #
# Partial rendering is most commonly used together with Ajax calls that only update one or a few elements on a page # Partial rendering in a controller 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 # 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 # 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. # 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) # # Renders the same partial with a local variable.
# render :partial => "win" # render :partial => "person", :locals => { :name => "david" }
# #
# # Renders the partial with a status code of 500 (internal error) # # Renders a collection of the same partial by making each element
# # of @winners available through the local variable "person" as it
# # builds the complete response.
# render :partial => "person", :collection => @winners
#
# # Renders the same collection of partials, but also renders the
# # person_divider partial between each person partial.
# render :partial => "person", :collection => @winners, :spacer_template => "person_divider"
#
# # Renders a collection of partials located in a view subfolder
# # outside of our current controller. In this example we will be
# # rendering app/views/shared/_note.r(html|xml) Inside the partial
# # each element of @new_notes is available as the local var "note".
# render :partial => "shared/note", :collection => @new_notes
#
# # Renders the partial with a status code of 500 (internal error).
# render :partial => "broken", :status => 500 # render :partial => "broken", :status => 500
# #
# # Renders the same partial but also makes a local variable available to it # Note that the partial filename must also be a valid Ruby variable name,
# render :partial => "win", :locals => { :name => "david" } # so e.g. 2005 and register-user are invalid.
#
# # 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 # _Deprecation_ _notice_: This used to have the signatures
# <tt>render_partial(partial_path = default_template_name, object = nil, local_assigns = {})</tt> and # <tt>render_partial(partial_path = default_template_name, object = nil, local_assigns = {})</tt> and
@ -598,8 +644,29 @@ module ActionController #:nodoc:
# # placed in "app/views/layouts/special.r(html|xml)" # # placed in "app/views/layouts/special.r(html|xml)"
# render :text => "Explosion!", :layout => "special" # render :text => "Explosion!", :layout => "special"
# #
# The :text option can also accept a Proc object, which can be used to manually control the page generation. This should
# generally be avoided, as it violates the separation between code and content, and because almost everything that can be
# done with this method can also be done more cleanly using one of the other rendering methods, most notably templates.
#
# # Renders "Hello from code!"
# render :text => proc { |response, output| output.write("Hello from code!") }
#
# _Deprecation_ _notice_: This used to have the signature <tt>render_text("text", status = 200)</tt> # _Deprecation_ _notice_: This used to have the signature <tt>render_text("text", status = 200)</tt>
# #
# === Rendering JSON
#
# Rendering JSON sets the content type to text/x-json and optionally wraps the JSON in a callback. It is expected
# that the response will be eval'd for use as a data structure.
#
# # Renders '{name: "David"}'
# render :json => {:name => "David"}.to_json
#
# Sometimes the result isn't handled directly by a script (such as when the request comes from a SCRIPT tag),
# so the callback option is provided for these cases.
#
# # Renders 'show({name: "David"})'
# render :json => {:name => "David"}.to_json, :callback => 'show'
#
# === Rendering an inline template # === Rendering an inline template
# #
# Rendering of an inline template works as a cross between text and action rendering where the source for the template # Rendering of an inline template works as a cross between text and action rendering where the source for the template
@ -640,17 +707,27 @@ module ActionController #:nodoc:
def render(options = nil, deprecated_status = nil, &block) #:doc: def render(options = nil, deprecated_status = nil, &block) #:doc:
raise DoubleRenderError, "Can only render or redirect once per action" if performed? raise DoubleRenderError, "Can only render or redirect once per action" if performed?
# Backwards compatibility if options.nil?
unless options.is_a?(Hash) return render_file(default_template_name, deprecated_status, true)
if options == :update else
options = {:update => true} # Backwards compatibility
else unless options.is_a?(Hash)
return render_file(options || default_template_name, deprecated_status, true) if options == :update
options = { :update => true }
else
ActiveSupport::Deprecation.warn(
"You called render('#{options}'), which is a deprecated API call. Instead you use " +
"render :file => #{options}. Calling render with just a string will be removed from Rails 2.0.",
caller
)
return render_file(options, deprecated_status, true)
end
end end
end end
if content_type = options[:content_type] if content_type = options[:content_type]
headers["Content-Type"] = content_type response.content_type = content_type.to_s
end end
if text = options[:text] if text = options[:text]
@ -667,11 +744,16 @@ module ActionController #:nodoc:
render_template(inline, options[:status], options[:type], options[:locals] || {}) render_template(inline, options[:status], options[:type], options[:locals] || {})
elsif action_name = options[:action] elsif action_name = options[:action]
render_action(action_name, options[:status], options[:layout]) ActiveSupport::Deprecation.silence do
render_action(action_name, options[:status], options[:layout])
end
elsif xml = options[:xml] elsif xml = options[:xml]
render_xml(xml, options[:status]) render_xml(xml, options[:status])
elsif json = options[:json]
render_json(json, options[:callback], options[:status])
elsif partial = options[:partial] elsif partial = options[:partial]
partial = default_template_name if partial == true partial = default_template_name if partial == true
if collection = options[:collection] if collection = options[:collection]
@ -701,21 +783,19 @@ module ActionController #:nodoc:
# Renders according to the same rules as <tt>render</tt>, but returns the result in a string instead # Renders according to the same rules as <tt>render</tt>, but returns the result in a string instead
# of sending it as the response body to the browser. # of sending it as the response body to the browser.
def render_to_string(options = nil, &block) #:doc: def render_to_string(options = nil, &block) #:doc:
result = render(options, &block) ActiveSupport::Deprecation.silence { render(options, &block) }
ensure
erase_render_results erase_render_results
forget_variables_added_to_assigns forget_variables_added_to_assigns
reset_variables_added_to_assigns reset_variables_added_to_assigns
result
end end
def render_action(action_name, status = nil, with_layout = true) #:nodoc: def render_action(action_name, status = nil, with_layout = true) #:nodoc:
template = default_template_name(action_name.to_s) template = default_template_name(action_name.to_s)
if with_layout && !template_exempt_from_layout?(template) if with_layout && !template_exempt_from_layout?(template)
render_with_layout(template, status) render_with_layout(:file => template, :status => status, :use_full_path => true, :layout => true)
else else
render_without_layout(template, status) render_without_layout(:file => template, :status => status, :use_full_path => true)
end end
end end
@ -731,22 +811,36 @@ module ActionController #:nodoc:
render_text(@template.render_template(type, template, nil, local_assigns), status) render_text(@template.render_template(type, template, nil, local_assigns), status)
end end
def render_text(text = nil, status = nil) #:nodoc: def render_text(text = nil, status = nil, append_response = false) #:nodoc:
@performed_render = true @performed_render = true
@response.headers['Status'] = (status || DEFAULT_RENDER_STATUS_CODE).to_s
@response.body = text response.headers['Status'] = interpret_status(status || DEFAULT_RENDER_STATUS_CODE)
if append_response
response.body ||= ''
response.body << text
else
response.body = text
end
end end
def render_javascript(javascript, status = nil) #:nodoc: def render_javascript(javascript, status = nil, append_response = true) #:nodoc:
@response.headers['Content-Type'] = 'text/javascript; charset=UTF-8' response.content_type = Mime::JS
render_text(javascript, status) render_text(javascript, status, append_response)
end end
def render_xml(xml, status = nil) #:nodoc: def render_xml(xml, status = nil) #:nodoc:
@response.headers['Content-Type'] = 'application/xml' response.content_type = Mime::XML
render_text(xml, status) render_text(xml, status)
end end
def render_json(json, callback = nil, status = nil) #:nodoc:
json = "#{callback}(#{json})" unless callback.blank?
response.content_type = Mime::JSON
render_text(json, status)
end
def render_nothing(status = nil) #:nodoc: def render_nothing(status = nil) #:nodoc:
render_text(' ', status) render_text(' ', status)
end end
@ -770,9 +864,48 @@ module ActionController #:nodoc:
end end
# Return a response that has no content (merely headers). The options
# argument is interpreted to be a hash of header names and values.
# This allows you to easily return a response that consists only of
# significant headers:
#
# head :created, :location => person_path(@person)
#
# It can also be used to return exceptional conditions:
#
# return head(:method_not_allowed) unless request.post?
# return head(:bad_request) unless valid_request?
# render
def head(*args)
if args.length > 2
raise ArgumentError, "too many arguments to head"
elsif args.empty?
raise ArgumentError, "too few arguments to head"
elsif args.length == 2
status = args.shift
options = args.shift
elsif args.first.is_a?(Hash)
options = args.first
else
status = args.first
options = {}
end
raise ArgumentError, "head requires an options hash" if !options.is_a?(Hash)
status = interpret_status(status || options.delete(:status) || :ok)
options.each do |key, value|
headers[key.to_s.dasherize.split(/-/).map { |v| v.capitalize }.join("-")] = value.to_s
end
render :nothing => true, :status => status
end
# Clears the rendered results, allowing for another render to be performed. # Clears the rendered results, allowing for another render to be performed.
def erase_render_results #:nodoc: def erase_render_results #:nodoc:
@response.body = nil response.body = nil
@performed_render = false @performed_render = false
end end
@ -785,7 +918,7 @@ module ActionController #:nodoc:
response.redirected_to = nil response.redirected_to = nil
response.redirected_to_method_params = nil response.redirected_to_method_params = nil
response.headers['Status'] = DEFAULT_RENDER_STATUS_CODE response.headers['Status'] = DEFAULT_RENDER_STATUS_CODE
response.headers.delete('location') response.headers.delete('Location')
end end
# Erase both render and redirect results # Erase both render and redirect results
@ -854,6 +987,7 @@ module ActionController #:nodoc:
redirect_to(url_for(options)) redirect_to(url_for(options))
response.redirected_to = options response.redirected_to = options
else else
# TOOD: Deprecate me!
redirect_to(url_for(options, *parameters_for_method_reference)) redirect_to(url_for(options, *parameters_for_method_reference))
response.redirected_to, response.redirected_to_method_params = options, parameters_for_method_reference response.redirected_to, response.redirected_to_method_params = options, parameters_for_method_reference
end end
@ -874,20 +1008,20 @@ module ActionController #:nodoc:
cache_options = { 'max-age' => seconds, 'private' => true }.symbolize_keys.merge!(options.symbolize_keys) 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_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}"} 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(', ') response.headers["Cache-Control"] = cache_control.join(', ')
end end
# Sets a HTTP 1.1 Cache-Control header of "no-cache" so no caching should occur by the browser or # 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). # intermediate caches (like caching proxy servers).
def expires_now #:doc: def expires_now #:doc:
@response.headers["Cache-Control"] = "no-cache" response.headers["Cache-Control"] = "no-cache"
end end
# Resets the session by clearing out all the objects stored within and initializing a new session object. # Resets the session by clearing out all the objects stored within and initializing a new session object.
def reset_session #:doc: def reset_session #:doc:
@request.reset_session request.reset_session
@session = @request.session @_session = request.session
@response.session = @session response.session = @_session
end end
private private
@ -912,34 +1046,57 @@ module ActionController #:nodoc:
end end
def assign_shortcuts(request, response) def assign_shortcuts(request, response)
@request, @params, @cookies = request, request.parameters, request.cookies @_request, @_params, @_cookies = request, request.parameters, request.cookies
@response = response @_response = response
@response.session = request.session @_response.session = request.session
@session = @response.session @_session = @_response.session
@template = @response.template @template = @_response.template
@assigns = @response.template.assigns @assigns = @_response.template.assigns
@headers = @response.headers @_headers = @_response.headers
assign_deprecated_shortcuts(request, response)
end
# TODO: assigns cookies headers params request response template
DEPRECATED_INSTANCE_VARIABLES = %w(cookies flash headers params request response session)
# Gone after 1.2.
def assign_deprecated_shortcuts(request, response)
DEPRECATED_INSTANCE_VARIABLES.each do |method|
var = "@#{method}"
if instance_variables.include?(var)
value = instance_variable_get(var)
unless ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy === value
raise "Deprecating #{var}, but it's already set to #{value.inspect}! Use the #{method}= writer method instead of setting #{var} directly."
end
end
instance_variable_set var, ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(self, method)
end
end end
def initialize_current_url def initialize_current_url
@url = UrlRewriter.new(@request, @params.clone()) @url = UrlRewriter.new(request, params.clone)
end end
def log_processing def log_processing
if logger if logger
logger.info "\n\nProcessing #{controller_class_name}\##{action_name} (for #{request_origin}) [#{request.method.to_s.upcase}]" 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 " 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}" logger.info " Parameters: #{respond_to?(:filter_parameters) ? filter_parameters(params).inspect : params.inspect}"
end end
end end
def perform_action def perform_action
if self.class.action_methods.include?(action_name) || self.class.action_methods.include?('method_missing') if self.class.action_methods.include?(action_name)
send(action_name) send(action_name)
render unless performed? render unless performed?
elsif respond_to? :method_missing
send(:method_missing, action_name)
render unless performed?
elsif template_exists? && template_public? elsif template_exists? && template_public?
render render
else else
@ -955,6 +1112,15 @@ module ActionController #:nodoc:
@action_name = (params['action'] || 'index') @action_name = (params['action'] || 'index')
end end
def assign_default_content_type_and_charset
response.content_type ||= Mime::HTML
response.charset ||= self.class.default_charset unless sending_file?
end
def sending_file?
response.headers["Content-Transfer-Encoding"] == "binary"
end
def action_methods def action_methods
self.class.action_methods self.class.action_methods
end end
@ -980,7 +1146,7 @@ module ActionController #:nodoc:
end end
def add_instance_variables_to_assigns def add_instance_variables_to_assigns
@@protected_variables_cache ||= protected_instance_variables.inject({}) { |h, k| h[k] = true; h } @@protected_variables_cache ||= Set.new(protected_instance_variables)
instance_variables.each do |var| instance_variables.each do |var|
next if @@protected_variables_cache.include?(var) next if @@protected_variables_cache.include?(var)
@assigns[var[1..-1]] = instance_variable_get(var) @assigns[var[1..-1]] = instance_variable_get(var)
@ -988,31 +1154,34 @@ module ActionController #:nodoc:
end end
def add_class_variables_to_assigns def add_class_variables_to_assigns
%w( template_root logger template_class ignore_missing_templates ).each do |cvar| %w(template_root logger template_class ignore_missing_templates).each do |cvar|
@assigns[cvar] = self.send(cvar) @assigns[cvar] = self.send(cvar)
end end
end end
def protected_instance_variables def protected_instance_variables
if view_controller_internals if view_controller_internals
[ "@assigns", "@performed_redirect", "@performed_render" ] %w(@assigns @performed_redirect @performed_render)
else else
[ "@assigns", "@performed_redirect", "@performed_render", "@request", "@response", "@session", "@cookies", "@template", "@request_origin", "@parent_controller" ] %w(@assigns @performed_redirect @performed_render
@_request @request @_response @response @_params @params
@_session @session @_cookies @cookies
@template @request_origin @parent_controller)
end end
end end
def request_origin def request_origin
# this *needs* to be cached! # this *needs* to be cached!
# otherwise you'd get different results if calling it more than once # otherwise you'd get different results if calling it more than once
@request_origin ||= "#{@request.remote_ip} at #{Time.now.to_s(:db)}" @request_origin ||= "#{request.remote_ip} at #{Time.now.to_s(:db)}"
end end
def complete_request_uri def complete_request_uri
"#{@request.protocol}#{@request.host}#{@request.request_uri}" "#{request.protocol}#{request.host}#{request.request_uri}"
end end
def close_session def close_session
@session.close unless @session.nil? || Hash === @session @_session.close if @_session && @_session.respond_to?(:close)
end end
def template_exists?(template_name = default_template_name) def template_exists?(template_name = default_template_name)
@ -1024,7 +1193,9 @@ module ActionController #:nodoc:
end end
def template_exempt_from_layout?(template_name = default_template_name) def template_exempt_from_layout?(template_name = default_template_name)
template_name =~ /\.rjs$/ || (@template.pick_template_extension(template_name) == :rjs rescue false) extension = @template.pick_template_extension(template_name) rescue nil
name_with_extension = !template_name.include?('.') && extension ? "#{template_name}.#{extension}" : template_name
extension == :rjs || @@exempt_from_layout.any? { |ext| name_with_extension =~ ext }
end end
def assert_existence_of_template_file(template_name) def assert_existence_of_template_file(template_name)

View file

@ -8,11 +8,8 @@ module ActionController #:nodoc:
base.extend(ClassMethods) base.extend(ClassMethods)
base.class_eval do base.class_eval do
alias_method :perform_action_without_benchmark, :perform_action alias_method_chain :perform_action, :benchmark
alias_method :perform_action, :perform_action_with_benchmark alias_method_chain :render, :benchmark
alias_method :render_without_benchmark, :render
alias_method :render, :render_with_benchmark
end end
end end
@ -68,7 +65,7 @@ module ActionController #:nodoc:
else else
runtime = [Benchmark::measure{ perform_action_without_benchmark }.real, 0.0001].max 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 = "Completed in #{sprintf("%.5f", runtime)} (#{(1 / runtime).floor} reqs/sec)"
log_message << rendering_runtime(runtime) if @rendering_runtime log_message << rendering_runtime(runtime) if defined?(@rendering_runtime)
log_message << active_record_runtime(runtime) if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected? log_message << active_record_runtime(runtime) if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected?
log_message << " | #{headers["Status"]}" log_message << " | #{headers["Status"]}"
log_message << " [#{complete_request_uri rescue "unknown"}]" log_message << " [#{complete_request_uri rescue "unknown"}]"

View file

@ -1,4 +1,5 @@
require 'fileutils' require 'fileutils'
require 'uri'
module ActionController #:nodoc: module ActionController #:nodoc:
# Caching is a cheap way of speeding up slow applications by keeping the result of calculations, renderings, and database calls # Caching is a cheap way of speeding up slow applications by keeping the result of calculations, renderings, and database calls
@ -117,24 +118,24 @@ module ActionController #:nodoc:
return unless perform_caching return unless perform_caching
if options[:action].is_a?(Array) if options[:action].is_a?(Array)
options[:action].dup.each do |action| options[:action].dup.each do |action|
self.class.expire_page(url_for(options.merge({ :only_path => true, :skip_relative_url_root => true, :action => action }))) self.class.expire_page(url_for(options.merge(:only_path => true, :skip_relative_url_root => true, :action => action)))
end end
else else
self.class.expire_page(url_for(options.merge({ :only_path => true, :skip_relative_url_root => true }))) self.class.expire_page(url_for(options.merge(:only_path => true, :skip_relative_url_root => true)))
end end
end end
# Manually cache the +content+ in the key determined by +options+. If no content is provided, the contents of @response.body is used # 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: # 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" # cache_page "I'm the cached content", :controller => "lists", :action => "show"
def cache_page(content = nil, options = {}) def cache_page(content = nil, options = {})
return unless perform_caching && caching_allowed 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 }))) self.class.cache_page(content || response.body, url_for(options.merge(:only_path => true, :skip_relative_url_root => true, :format => params[:format])))
end end
private private
def caching_allowed def caching_allowed
!@request.post? && @response.headers['Status'] && @response.headers['Status'].to_i < 400 request.get? && response.headers['Status'].to_i == 200
end end
end end
@ -155,9 +156,12 @@ module ActionController #:nodoc:
# 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 # 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 # "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. # "jamis.somewhere.com/lists/" -- which is a helpful way of assisting the subdomain-as-account-key pattern.
#
# Different representations of the same resource, e.g. <tt>http://david.somewhere.com/lists</tt> and <tt>http://david.somewhere.com/lists.xml</tt>
# are treated like separate requests and so are cached separately. Keep in mind when expiring an action cache that <tt>:action => 'lists'</tt> is not the same
# as <tt>:action => 'list', :format => :xml</tt>.
module Actions module Actions
def self.append_features(base) #:nodoc: def self.included(base) #:nodoc:
super
base.extend(ClassMethods) base.extend(ClassMethods)
base.send(:attr_accessor, :rendered_action_cache) base.send(:attr_accessor, :rendered_action_cache)
end end
@ -173,22 +177,24 @@ module ActionController #:nodoc:
return unless perform_caching return unless perform_caching
if options[:action].is_a?(Array) if options[:action].is_a?(Array)
options[:action].dup.each do |action| options[:action].dup.each do |action|
expire_fragment(url_for(options.merge({ :action => action })).split("://").last) expire_fragment(ActionCachePath.path_for(self, options.merge({ :action => action })))
end end
else else
expire_fragment(url_for(options).split("://").last) expire_fragment(ActionCachePath.path_for(self, options))
end end
end end
class ActionCacheFilter #:nodoc: class ActionCacheFilter #:nodoc:
def initialize(*actions) def initialize(*actions, &block)
@actions = actions @actions = actions
end end
def before(controller) def before(controller)
return unless @actions.include?(controller.action_name.intern) return unless @actions.include?(controller.action_name.intern)
if cache = controller.read_fragment(controller.url_for.split("://").last) action_cache_path = ActionCachePath.new(controller)
if cache = controller.read_fragment(action_cache_path.path)
controller.rendered_action_cache = true controller.rendered_action_cache = true
set_content_type!(action_cache_path)
controller.send(:render_text, cache) controller.send(:render_text, cache)
false false
end end
@ -196,8 +202,60 @@ module ActionController #:nodoc:
def after(controller) def after(controller)
return if !@actions.include?(controller.action_name.intern) || controller.rendered_action_cache return if !@actions.include?(controller.action_name.intern) || controller.rendered_action_cache
controller.write_fragment(controller.url_for.split("://").last, controller.response.body) controller.write_fragment(ActionCachePath.path_for(controller), controller.response.body)
end end
private
def set_content_type!(action_cache_path)
if extention = action_cache_path.extension
content_type = Mime::EXTENSION_LOOKUP[extention]
action_cache_path.controller.response.content_type = content_type.to_s
end
end
end
class ActionCachePath
attr_reader :controller, :options
class << self
def path_for(*args, &block)
new(*args).path
end
end
def initialize(controller, options = {})
@controller = controller
@options = options
end
def path
return @path if @path
@path = controller.url_for(options).split('://').last
normalize!
add_extension!
URI.unescape(@path)
end
def extension
@extension ||= extract_extension(controller.request.path)
end
private
def normalize!
@path << 'index' if @path.last == '/'
end
def add_extension!
@path << ".#{extension}" if extension
end
def extract_extension(file_path)
# Don't want just what comes after the last '.' to accomodate multi part extensions
# such as tar.gz.
file_path[/^[^.]+\.(.+)$/, 1]
end
end end
end end
@ -208,7 +266,7 @@ module ActionController #:nodoc:
# <b>Hello <%= @name %></b> # <b>Hello <%= @name %></b>
# <% cache do %> # <% cache do %>
# All the topics in the system: # All the topics in the system:
# <%= render_collection_of_partials "topic", Topic.find_all %> # <%= render :partial => "topic", :collection => Topic.find(:all) %>
# <% end %> # <% end %>
# #
# This cache will bind to the name of action that called it. So you would be able to invalidate it using # This cache will bind to the name of action that called it. So you would be able to invalidate it using
@ -246,8 +304,7 @@ module ActionController #:nodoc:
# ActionController::Base.fragment_cache_store = :mem_cache_store, "localhost" # ActionController::Base.fragment_cache_store = :mem_cache_store, "localhost"
# ActionController::Base.fragment_cache_store = MyOwnStore.new("parameter") # ActionController::Base.fragment_cache_store = MyOwnStore.new("parameter")
module Fragments module Fragments
def self.append_features(base) #:nodoc: def self.included(base) #:nodoc:
super
base.class_eval do base.class_eval do
@@fragment_cache_store = MemoryStore.new @@fragment_cache_store = MemoryStore.new
cattr_reader :fragment_cache_store cattr_reader :fragment_cache_store
@ -306,7 +363,12 @@ module ActionController #:nodoc:
# Name can take one of three forms: # Name can take one of three forms:
# * String: This would normally take the form of a path like "pages/45/notes" # * 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 } # * 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 # * 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
# Regexp expiration is not supported on caches which can't iterate over
# all keys, such as memcached.
def expire_fragment(name, options = nil) def expire_fragment(name, options = nil)
return unless perform_caching return unless perform_caching
@ -327,6 +389,7 @@ module ActionController #:nodoc:
def expire_matched_fragments(matcher = /.*/, options = nil) #:nodoc: def expire_matched_fragments(matcher = /.*/, options = nil) #:nodoc:
expire_fragment(matcher, options) expire_fragment(matcher, options)
end end
deprecate :expire_matched_fragments => :expire_fragment
class UnthreadedMemoryStore #:nodoc: class UnthreadedMemoryStore #:nodoc:
@ -430,7 +493,7 @@ module ActionController #:nodoc:
if f =~ matcher if f =~ matcher
begin begin
File.delete(f) File.delete(f)
rescue Object => e rescue SystemCallError => e
# If there's no cache, then there's nothing to complain about # If there's no cache, then there's nothing to complain about
end end
end end
@ -493,8 +556,7 @@ module ActionController #:nodoc:
# #
# In the example above, four actions are cached and three actions are responsible for expiring those caches. # In the example above, four actions are cached and three actions are responsible for expiring those caches.
module Sweeping module Sweeping
def self.append_features(base) #:nodoc: def self.included(base) #:nodoc:
super
base.extend(ClassMethods) base.extend(ClassMethods)
end end
@ -503,8 +565,7 @@ module ActionController #:nodoc:
return unless perform_caching return unless perform_caching
configuration = sweepers.last.is_a?(Hash) ? sweepers.pop : {} configuration = sweepers.last.is_a?(Hash) ? sweepers.pop : {}
sweepers.each do |sweeper| sweepers.each do |sweeper|
observer(sweeper) ActiveRecord::Base.observers << sweeper if defined?(ActiveRecord) and defined?(ActiveRecord::Base)
sweeper_instance = Object.const_get(Inflector.classify(sweeper)).instance sweeper_instance = Object.const_get(Inflector.classify(sweeper)).instance
if sweeper_instance.is_a?(Sweeper) if sweeper_instance.is_a?(Sweeper)
@ -523,7 +584,7 @@ module ActionController #:nodoc:
# ActiveRecord::Observer will mark this class as reloadable even though it should not be. # ActiveRecord::Observer will mark this class as reloadable even though it should not be.
# However, subclasses of ActionController::Caching::Sweeper should be Reloadable # However, subclasses of ActionController::Caching::Sweeper should be Reloadable
include Reloadable::Subclasses include Reloadable::Deprecated
def before(controller) def before(controller)
self.controller = controller self.controller = controller

View file

@ -28,13 +28,6 @@ class CGI #:nodoc:
CGIMethods.parse_request_parameters(params, env_table) CGIMethods.parse_request_parameters(params, env_table)
end end
def redirect(where)
header({
"Status" => "302 Moved",
"location" => "#{where}"
})
end
def session(parameters = nil) def session(parameters = nil)
parameters = {} if parameters.nil? parameters = {} if parameters.nil?
parameters['database_manager'] = CGI::Session::PStore parameters['database_manager'] = CGI::Session::PStore

View file

@ -1,217 +1,211 @@
require 'cgi' require 'cgi'
require 'action_controller/vendor/xml_simple'
require 'action_controller/vendor/xml_node' require 'action_controller/vendor/xml_node'
require 'strscan'
# Static methods for parsing the query and request parameters that can be used in # Static methods for parsing the query and request parameters that can be used in
# a CGI extension class or testing in isolation. # a CGI extension class or testing in isolation.
class CGIMethods #:nodoc: class CGIMethods #:nodoc:
public class << self
# Returns a hash with the pairs from the query string. The implicit hash construction that is done in # DEPRECATED: Use parse_form_encoded_parameters
# parse_request_params is not done here. def parse_query_parameters(query_string)
def CGIMethods.parse_query_parameters(query_string) pairs = query_string.split('&').collect do |chunk|
parsed_params = {} next if chunk.empty?
key, value = chunk.split('=', 2)
next if key.empty?
value = (value.nil? || value.empty?) ? nil : CGI.unescape(value)
[ CGI.unescape(key), value ]
end.compact
query_string.split(/[&;]/).each { |p| FormEncodedPairParser.new(pairs).result
# 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 end
# Returns the request (POST/GET) parameters in a parsed form where pairs such as "customer[address][street]" / # DEPRECATED: Use parse_form_encoded_parameters
# "Somewhere cool!" are translated into a full hash hierarchy, like def parse_request_parameters(params)
# { "customer" => { "address" => { "street" => "Somewhere cool!" } } } parser = FormEncodedPairParser.new
def CGIMethods.parse_request_parameters(params)
parsed_params = {}
for key, value in params params = params.dup
value = [value] if key =~ /.*\[\]$/ until params.empty?
unless key.include?('[') for key, value in params
# much faster to test for the most common case first (GET) if key.blank?
# and avoid the call to build_deep_hash params.delete key
parsed_params[key] = get_typed_value(value[0]) elsif !key.include?('[')
else # much faster to test for the most common case first (GET)
build_deep_hash(get_typed_value(value[0]), parsed_params, get_levels(key)) # and avoid the call to build_deep_hash
parser.result[key] = get_typed_value(value[0])
params.delete key
elsif value.is_a?(Array)
parser.parse(key, get_typed_value(value.shift))
params.delete key if value.empty?
else
raise TypeError, "Expected array, found #{value.inspect}"
end
end end
end end
parsed_params parser.result
end end
def self.parse_formatted_request_parameters(mime_type, raw_post_data) def parse_formatted_request_parameters(mime_type, raw_post_data)
params = case strategy = ActionController::Base.param_parsers[mime_type] case strategy = ActionController::Base.param_parsers[mime_type]
when Proc when Proc
strategy.call(raw_post_data) strategy.call(raw_post_data)
when :xml_simple when :xml_simple
raw_post_data.blank? ? nil : raw_post_data.blank? ? {} : Hash.from_xml(raw_post_data)
typecast_xml_value(XmlSimple.xml_in(raw_post_data,
'forcearray' => false,
'forcecontent' => true,
'keeproot' => true,
'contentkey' => '__content__'))
when :yaml when :yaml
YAML.load(raw_post_data) YAML.load(raw_post_data)
when :xml_node when :xml_node
node = XmlNode.from_xml(raw_post_data) node = XmlNode.from_xml(raw_post_data)
{ node.node_name => node } { node.node_name => node }
end end
rescue Exception => e # YAML, XML or Ruby code block errors
dasherize_keys(params || {})
rescue Object => e
{ "exception" => "#{e.message} (#{e.class})", "backtrace" => e.backtrace, { "exception" => "#{e.message} (#{e.class})", "backtrace" => e.backtrace,
"raw_post_data" => raw_post_data, "format" => mime_type } "raw_post_data" => raw_post_data, "format" => mime_type }
end end
def self.typecast_xml_value(value) private
case value def get_typed_value(value)
when Hash case value
if value.has_key?("__content__") when String
content = translate_xml_entities(value["__content__"]) value
case value["type"] when NilClass
when "integer" then content.to_i ''
when "boolean" then content == "true" when Array
when "datetime" then Time.parse(content) value.map { |v| get_typed_value(v) }
when "date" then Date.parse(content) else
else content # Uploaded file provides content type and filename.
end if value.respond_to?(:content_type) &&
else !value.content_type.blank? &&
value.empty? ? nil : value.inject({}) do |h,(k,v)| !value.original_filename.blank?
h[k] = typecast_xml_value(v) unless value.respond_to?(:full_original_filename)
h class << value
end alias_method :full_original_filename, :original_filename
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 # Take the basename of the upload's original filename.
# This handles the full Windows paths given by Internet Explorer
def self.translate_xml_entities(value) # (and perhaps other broken user agents) without affecting
value.gsub(/&lt;/, "<"). # those which give the lone filename.
gsub(/&gt;/, ">"). # The Windows regexp is adapted from Perl's File::Basename.
gsub(/&quot;/, '"'). def original_filename
gsub(/&apos;/, "'"). if md = /^(?:.*[:\\\/])?(.*)/m.match(full_original_filename)
gsub(/&amp;/, "&") md.captures.first
end else
File.basename full_original_filename
def self.dasherize_keys(params) end
case params.class.to_s end
when "Hash" end
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
# Return the same value after overriding original_filename.
value
# Multipart values may have content type, but no filename.
elsif value.respond_to?(:read)
result = value.read
value.rewind
result
# Unknown value, neither string nor multipart.
else
raise "Unknown form value: #{value.inspect}"
end end
end
end
end
class FormEncodedPairParser < StringScanner #:nodoc:
attr_reader :top, :parent, :result
def initialize(pairs = [])
super('')
@result = {}
pairs.each { |key, value| parse(key, value) }
end
KEY_REGEXP = %r{([^\[\]=&]+)}
BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]+)\]}
# Parse the query string
def parse(key, value)
self.string = key
@top, @parent = result, nil
# First scan the bare key
key = scan(KEY_REGEXP) or return
key = post_key_check(key)
# Then scan as many nestings as present
until eos?
r = scan(BRACKETED_KEY_REGEXP) or return
key = self[1]
key = post_key_check(key)
end
bind(key, value)
end
private
# After we see a key, we must look ahead to determine our next action. Cases:
#
# [] follows the key. Then the value must be an array.
# = follows the key. (A value comes next)
# & or the end of string follows the key. Then the key is a flag.
# otherwise, a hash follows the key.
def post_key_check(key)
if scan(/\[\]/) # a[b][] indicates that b is an array
container(key, Array)
nil
elsif check(/\[[^\]]/) # a[b] indicates that a is a hash
container(key, Hash)
nil
else # End of key? We do nothing.
key
end
end
# Add a container to the stack.
#
def container(key, klass)
type_conflict! klass, top[key] if top.is_a?(Hash) && top.key?(key) && ! top[key].is_a?(klass)
value = bind(key, klass.new)
type_conflict! klass, value unless value.is_a?(klass)
push(value)
end
# Push a value onto the 'stack', which is actually only the top 2 items.
def push(value)
@parent, @top = @top, value
end
# Bind a key (which may be nil for items in an array) to the provided value.
def bind(key, value)
if top.is_a? Array
if key
if top[-1].is_a?(Hash) && ! top[-1].key?(key)
top[-1][key] = value
else
top << {key => value}.with_indifferent_access
push top.last
end
else
top << value
end end
elsif top.is_a? Hash
key = CGI.unescape(key)
parent << (@top = {}) if top.key?(key) && parent.is_a?(Array)
return top[key] ||= value
else
raise ArgumentError, "Don't know what to do: top is #{top.inspect}"
end end
# Return the same value after overriding original_filename. return value
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
end
PARAMS_HASH_RE = /^([^\[]+)(\[.*\])?(.)?.*$/ def type_conflict!(klass, value)
def CGIMethods.get_levels(key) raise TypeError,
all, main, bracketed, trailing = PARAMS_HASH_RE.match(key).to_a "Conflicting types for parameter containers. " +
if main.nil? "Expected an instance of #{klass}, but found an instance of #{value.class}. " +
[] "This can be caused by passing Array and Hash based paramters qs[]=value&qs[key]=value. "
elsif trailing
[key]
elsif bracketed
[main] + bracketed.slice(1...-1).split('][')
else
[main]
end 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
end end

View file

@ -1,27 +1,48 @@
class CGI #:nodoc: class CGI #:nodoc:
# Add @request.env['RAW_POST_DATA'] for the vegans.
module QueryExtension module QueryExtension
# Initialize the data from the query. # Initialize the data from the query.
# #
# Handles multipart forms (in particular, forms that involve file uploads). # Handles multipart forms (in particular, forms that involve file uploads).
# Reads query parameters in the @params field, and cookies into @cookies. # Reads query parameters in the @params field, and cookies into @cookies.
def initialize_query() def initialize_query
@cookies = CGI::Cookie::parse(env_table['HTTP_COOKIE'] || env_table['COOKIE']) @cookies = CGI::Cookie::parse(env_table['HTTP_COOKIE'] || env_table['COOKIE'])
#fix some strange request environments # Fix some strange request environments.
if method = env_table['REQUEST_METHOD'] if method = env_table['REQUEST_METHOD']
method = method.to_s.downcase.intern method = method.to_s.downcase.intern
else else
method = :get method = :get
end end
if method == :post && (boundary = multipart_form_boundary) # POST assumes missing Content-Type is application/x-www-form-urlencoded.
@multipart = true content_type = env_table['CONTENT_TYPE']
@params = read_multipart(boundary, Integer(env_table['CONTENT_LENGTH'])) if content_type.blank? && method == :post
else content_type = 'application/x-www-form-urlencoded'
@multipart = false
@params = CGI::parse(read_query_params(method) || "")
end end
# Force content length to zero if missing.
content_length = env_table['CONTENT_LENGTH'].to_i
# Set multipart to false by default.
@multipart = false
# POST and PUT may have params in entity body. If content type is
# missing for POST, assume urlencoded. If content type is missing
# for PUT, don't assume anything and don't parse the parameters:
# it's likely binary data.
#
# The other HTTP methods have their params in the query string.
if method == :post || method == :put
if boundary = extract_multipart_form_boundary(content_type)
@multipart = true
@params = read_multipart(boundary, content_length)
elsif content_type.blank? || content_type !~ %r{application/x-www-form-urlencoded}i
read_params(method, content_length)
@params = {}
end
end
@params ||= CGI.parse(read_params(method, content_length))
end end
private private
@ -29,16 +50,16 @@ class CGI #:nodoc:
MULTIPART_FORM_BOUNDARY_RE = %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|n #" MULTIPART_FORM_BOUNDARY_RE = %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|n #"
end end
def multipart_form_boundary def extract_multipart_form_boundary(content_type)
MULTIPART_FORM_BOUNDARY_RE.match(env_table['CONTENT_TYPE']).to_a.pop MULTIPART_FORM_BOUNDARY_RE.match(content_type).to_a.pop
end end
if defined? MOD_RUBY if defined? MOD_RUBY
def read_params_from_query def read_query
Apache::request.args || '' Apache::request.args || ''
end end
else else
def read_params_from_query def read_query
# fixes CGI querystring parsing for lighttpd # fixes CGI querystring parsing for lighttpd
env_qs = env_table['QUERY_STRING'] env_qs = env_table['QUERY_STRING']
if env_qs.blank? && !(uri = env_table['REQUEST_URI']).blank? if env_qs.blank? && !(uri = env_table['REQUEST_URI']).blank?
@ -49,25 +70,25 @@ class CGI #:nodoc:
end end
end end
def read_params_from_post def read_body(content_length)
stdinput.binmode if stdinput.respond_to?(:binmode) stdinput.binmode if stdinput.respond_to?(:binmode)
content = stdinput.read(Integer(env_table['CONTENT_LENGTH'])) || '' content = stdinput.read(content_length) || ''
# fix for Safari Ajax postings that always append \000 # Fix for Safari Ajax postings that always append \000
content.chop! if content[-1] == 0 content.chop! if content[-1] == 0
content.gsub! /&_=$/, '' content.gsub!(/&_=$/, '')
env_table['RAW_POST_DATA'] = content.freeze env_table['RAW_POST_DATA'] = content.freeze
end end
def read_query_params(method) def read_params(method, content_length)
case method case method
when :get when :get
read_params_from_query read_query
when :post, :put when :post, :put
read_params_from_post read_body(content_length)
when :cmd when :cmd
read_from_cmdline read_from_cmdline
else # when :head, :delete, :options else # :head, :delete, :options, :trace, :connect
read_params_from_query read_query
end end
end end
end # module QueryExtension end # module QueryExtension

View file

@ -60,7 +60,8 @@ module ActionController #:nodoc:
end end
def query_parameters def query_parameters
(qs = self.query_string).empty? ? {} : CGIMethods.parse_query_parameters(qs) @query_parameters ||=
(qs = self.query_string).empty? ? {} : CGIMethods.parse_query_parameters(qs)
end end
def request_parameters def request_parameters
@ -101,15 +102,26 @@ module ActionController #:nodoc:
end end
def session def session
unless @session unless defined?(@session)
if @session_options == false if @session_options == false
@session = Hash.new @session = Hash.new
else else
stale_session_check! do stale_session_check! do
if session_options_with_string_keys['new_session'] == true case value = session_options_with_string_keys['new_session']
@session = new_session when true
else @session = new_session
@session = CGI::Session.new(@cgi, session_options_with_string_keys) when false
begin
@session = CGI::Session.new(@cgi, session_options_with_string_keys)
# CGI::Session raises ArgumentError if 'new_session' == false
# and no session cookie or query param is present.
rescue ArgumentError
@session = Hash.new
end
when nil
@session = CGI::Session.new(@cgi, session_options_with_string_keys)
else
raise ArgumentError, "Invalid new_session option: #{value}"
end end
@session['__valid_session'] @session['__valid_session']
end end
@ -119,7 +131,7 @@ module ActionController #:nodoc:
end end
def reset_session def reset_session
@session.delete if CGI::Session === @session @session.delete if defined?(@session) && @session.is_a?(CGI::Session)
@session = new_session @session = new_session
end end
@ -141,11 +153,11 @@ module ActionController #:nodoc:
def stale_session_check! def stale_session_check!
yield yield
rescue ArgumentError => argument_error rescue ArgumentError => argument_error
if argument_error.message =~ %r{undefined class/module (\w+)} if argument_error.message =~ %r{undefined class/module ([\w:]+)}
begin begin
Module.const_missing($1) Module.const_missing($1)
rescue LoadError, NameError => const_error rescue LoadError, NameError => const_error
raise ActionController::SessionRestoreError, <<end_msg raise ActionController::SessionRestoreError, <<-end_msg
Session contains objects whose class definition isn\'t available. Session contains objects whose class definition isn\'t available.
Remember to require the classes for all objects kept in the session. Remember to require the classes for all objects kept in the session.
(Original exception: #{const_error.message} [#{const_error.class}]) (Original exception: #{const_error.message} [#{const_error.class}])
@ -159,7 +171,7 @@ end_msg
end end
def session_options_with_string_keys def session_options_with_string_keys
@session_options_with_string_keys ||= DEFAULT_SESSION_OPTIONS.merge(@session_options).inject({}) { |options, (k,v)| options[k.to_s] = v; options } @session_options_with_string_keys ||= DEFAULT_SESSION_OPTIONS.merge(@session_options).stringify_keys
end end
end end
@ -170,7 +182,9 @@ end_msg
end end
def out(output = $stdout) def out(output = $stdout)
convert_content_type!(@headers) convert_content_type!
set_content_length!
output.binmode if output.respond_to?(:binmode) output.binmode if output.respond_to?(:binmode)
output.sync = false if output.respond_to?(:sync=) output.sync = false if output.respond_to?(:sync=)
@ -180,28 +194,37 @@ end_msg
if @cgi.send(:env_table)['REQUEST_METHOD'] == 'HEAD' if @cgi.send(:env_table)['REQUEST_METHOD'] == 'HEAD'
return return
elsif @body.respond_to?(:call) elsif @body.respond_to?(:call)
# Flush the output now in case the @body Proc uses
# #syswrite.
output.flush if output.respond_to?(:flush)
@body.call(self, output) @body.call(self, output)
else else
output.write(@body) output.write(@body)
end end
output.flush if output.respond_to?(:flush) output.flush if output.respond_to?(:flush)
rescue Errno::EPIPE => e rescue Errno::EPIPE, Errno::ECONNRESET
# lost connection to the FCGI process -- ignore the output, then # lost connection to parent process, ignore output
end end
end end
private private
def convert_content_type!(headers) def convert_content_type!
if header = headers.delete("Content-Type") if content_type = @headers.delete("Content-Type")
headers["type"] = header @headers["type"] = content_type
end end
if header = headers.delete("Content-type") if content_type = @headers.delete("Content-type")
headers["type"] = header @headers["type"] = content_type
end end
if header = headers.delete("content-type") if content_type = @headers.delete("content-type")
headers["type"] = header @headers["type"] = content_type
end end
end end
# Don't set the Content-Length for block-based bodies as that would mean reading it all into memory. Not nice
# for, say, a 2GB streaming file.
def set_content_length!
@headers["Content-Length"] = @body.size unless @body.respond_to?(:call)
end
end end
end end

View file

@ -50,14 +50,9 @@ module ActionController #:nodoc:
base.send :attr_accessor, :parent_controller base.send :attr_accessor, :parent_controller
base.class_eval do base.class_eval do
alias_method :process_cleanup_without_components, :process_cleanup alias_method_chain :process_cleanup, :components
alias_method :process_cleanup, :process_cleanup_with_components alias_method_chain :set_session_options, :components
alias_method_chain :flash, :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 alias_method :component_request?, :parent_controller
end end
@ -80,11 +75,13 @@ module ActionController #:nodoc:
# will also use /code/weblog/components as template root # will also use /code/weblog/components as template root
# and find templates in /code/weblog/components/admin/parties/users/ # and find templates in /code/weblog/components/admin/parties/users/
def uses_component_template_root def uses_component_template_root
path_of_calling_controller = File.dirname(caller[0].split(/:\d+:/).first) path_of_calling_controller = File.dirname(caller[1].split(/:\d+:/, 2).first)
path_of_controller_root = path_of_calling_controller.sub(/#{controller_path.split("/")[0..-2]}$/, "") # " (for ruby-mode) path_of_controller_root = path_of_calling_controller.sub(/#{Regexp.escape(File.dirname(controller_path))}$/, "")
self.template_root = path_of_controller_root self.template_root = path_of_controller_root
end end
deprecate :uses_component_template_root => 'Components are deprecated and will be removed in Rails 2.0.'
end end
module InstanceMethods module InstanceMethods
@ -116,25 +113,24 @@ module ActionController #:nodoc:
end end
def flash_with_components(refresh = false) #:nodoc: def flash_with_components(refresh = false) #:nodoc:
if @flash.nil? || refresh if !defined?(@_flash) || refresh
@flash = @_flash =
if @parent_controller if defined?(@parent_controller)
@parent_controller.flash @parent_controller.flash
else else
flash_without_components flash_without_components
end end
end end
@_flash
@flash
end end
private private
def component_response(options, reuse_response) def component_response(options, reuse_response)
klass = component_class(options) klass = component_class(options)
request = request_for_component(klass.controller_name, options) request = request_for_component(klass.controller_name, options)
response = reuse_response ? @response : @response.dup new_response = reuse_response ? response : response.dup
klass.process_with_components(request, response, self) klass.process_with_components(request, new_response, self)
end end
# determine the controller class for the component request # determine the controller class for the component request
@ -150,17 +146,17 @@ module ActionController #:nodoc:
# The new request inherits the session from the current request, # The new request inherits the session from the current request,
# bypassing any session options set for the component controller's class # bypassing any session options set for the component controller's class
def request_for_component(controller_name, options) def request_for_component(controller_name, options)
request = @request.dup new_request = request.dup
request.session = @request.session new_request.session = request.session
request.instance_variable_set( new_request.instance_variable_set(
:@parameters, :@parameters,
(options[:params] || {}).with_indifferent_access.update( (options[:params] || {}).with_indifferent_access.update(
"controller" => controller_name, "action" => options[:action], "id" => options[:id] "controller" => controller_name, "action" => options[:action], "id" => options[:id]
) )
) )
request new_request
end end
def component_logging(options) def component_logging(options)

View file

@ -4,7 +4,8 @@ module ActionController #:nodoc:
# itself back -- just the value it holds). Examples for writing: # itself back -- just the value it holds). Examples for writing:
# #
# cookies[:user_name] = "david" # => Will set a simple session cookie # 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 # cookies[:login] = { :value => "XJ-122", :expires => 1.hour.from_now }
# # => Will set a cookie that expires in 1 hour
# #
# Examples for reading: # Examples for reading:
# #
@ -32,13 +33,13 @@ module ActionController #:nodoc:
# Deprecated cookie writer method # Deprecated cookie writer method
def cookie(*options) def cookie(*options)
@response.headers["cookie"] << CGI::Cookie.new(*options) response.headers['cookie'] << CGI::Cookie.new(*options)
end end
end end
class CookieJar < Hash #:nodoc: class CookieJar < Hash #:nodoc:
def initialize(controller) def initialize(controller)
@controller, @cookies = controller, controller.instance_variable_get("@cookies") @controller, @cookies = controller, controller.request.cookies
super() super()
update(@cookies) update(@cookies)
end end

View file

@ -7,17 +7,12 @@ module ActionController #:nodoc:
# Filters enable controllers to run shared pre and post processing code for its actions. These filters can be used to do # 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 # authentication, caching, or auditing before the intended action is performed. Or to do localization or output
# compression after the action has been performed. # 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).
# 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 <tt>before_filter</tt>
# 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 # == Filter inheritance
# #
# Controller inheritance hierarchies share filters downwards, but subclasses can also add new filters without # Controller inheritance hierarchies share filters downwards, but subclasses can also add or skip filters without
# affecting the superclass. For example: # affecting the superclass. For example:
# #
# class BankController < ActionController::Base # class BankController < ActionController::Base
@ -76,6 +71,9 @@ module ActionController #:nodoc:
# session, template, and assigns. Note: The inline method doesn't strictly have to be a block; any object that responds to call # 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). # and returns 1 or -1 on arity will do (such as a Proc or an Method object).
# #
# Please note that around_filters function a little differently than the normal before and after filters with regard to filter
# types. Please see the section dedicated to around_filters below.
#
# == Filter chain ordering # == Filter chain ordering
# #
# Using <tt>before_filter</tt> and <tt>after_filter</tt> appends the specified filters to the existing chain. That's usually # Using <tt>before_filter</tt> and <tt>after_filter</tt> appends the specified filters to the existing chain. That's usually
@ -83,10 +81,10 @@ module ActionController #:nodoc:
# can use <tt>prepend_before_filter</tt> and <tt>prepend_after_filter</tt>. Filters added by these methods will be put at the # can use <tt>prepend_before_filter</tt> and <tt>prepend_after_filter</tt>. Filters added by these methods will be put at the
# beginning of their respective chain and executed before the rest. For example: # beginning of their respective chain and executed before the rest. For example:
# #
# class ShoppingController # class ShoppingController < ActionController::Base
# before_filter :verify_open_shop # before_filter :verify_open_shop
# #
# class CheckoutController # class CheckoutController < ShoppingController
# prepend_before_filter :ensure_items_in_cart, :ensure_items_in_stock # prepend_before_filter :ensure_items_in_cart, :ensure_items_in_stock
# #
# The filter chain for the CheckoutController is now <tt>:ensure_items_in_cart, :ensure_items_in_stock,</tt> # The filter chain for the CheckoutController is now <tt>:ensure_items_in_cart, :ensure_items_in_stock,</tt>
@ -98,153 +96,221 @@ module ActionController #:nodoc:
# #
# == Around filters # == Around filters
# #
# In addition to the individual before and after filters, it's also possible to specify that a single object should handle # Around filters wrap an action, executing code both before and after.
# both the before and after call. That's especially useful when you need to keep state active between the before and after, # They may be declared as method references, blocks, or objects responding
# such as the example of a benchmark filter below: # to #filter or to both #before and #after.
# #
# class WeblogController < ActionController::Base # To use a method as an around_filter, pass a symbol naming the Ruby method.
# around_filter BenchmarkingFilter.new # Yield (or block.call) within the method to run the action.
# #
# # Before this action is performed, BenchmarkingFilter#before(controller) is executed # around_filter :catch_exceptions
# def index #
# private
# def catch_exceptions
# yield
# rescue => exception
# logger.debug "Caught exception! #{exception}"
# raise
# end # end
# # After this action has been performed, BenchmarkingFilter#after(controller) is executed #
# To use a block as an around_filter, pass a block taking as args both
# the controller and the action block. You can't call yield directly from
# an around_filter block; explicitly call the action block instead:
#
# around_filter do |controller, action|
# logger.debug "before #{controller.action_name}"
# action.call
# logger.debug "after #{controller.action_name}"
# end # end
# #
# To use a filter object with around_filter, pass an object responding
# to :filter or both :before and :after. With a filter method, yield to
# the block as above:
#
# around_filter BenchmarkingFilter
#
# class BenchmarkingFilter # class BenchmarkingFilter
# def initialize # def self.filter(controller, &block)
# @runtime # Benchmark.measure(&block)
# end
#
# def before
# start_timer
# end
#
# def after
# stop_timer
# report_result
# end # end
# end # end
# #
# With before and after methods:
#
# around_filter Authorizer.new
#
# class Authorizer
# # This will run before the action. Returning false aborts the action.
# def before(controller)
# if user.authorized?
# return true
# else
# redirect_to login_url
# return false
# end
# end
#
# # This will run after the action if and only if before returned true.
# def after(controller)
# end
# end
#
# If the filter has before and after methods, the before method will be
# called before the action. If before returns false, the filter chain is
# halted and after will not be run. See Filter Chain Halting below for
# an example.
#
# == Filter chain skipping # == Filter chain skipping
# #
# Some times its convenient to specify a filter chain in a superclass that'll hold true for the majority of the # Declaring a filter on a base class conveniently applies to its subclasses,
# subclasses, but not necessarily all of them. The subclasses that behave in exception can then specify which filters # but sometimes a subclass should skip some of its superclass' filters:
# they would like to be relieved of. Examples
# #
# class ApplicationController < ActionController::Base # class ApplicationController < ActionController::Base
# before_filter :authenticate # before_filter :authenticate
# around_filter :catch_exceptions
# end # end
# #
# class WeblogController < ApplicationController # class WeblogController < ApplicationController
# # will run the :authenticate filter # # Will run the :authenticate and :catch_exceptions filters.
# end # end
# #
# class SignupController < ApplicationController # class SignupController < ApplicationController
# # will not run the :authenticate filter # # Skip :authenticate, run :catch_exceptions.
# skip_before_filter :authenticate # skip_before_filter :authenticate
# end # end
# #
# class ProjectsController < ApplicationController
# # Skip :catch_exceptions, run :authenticate.
# skip_filter :catch_exceptions
# end
#
# class ClientsController < ApplicationController
# # Skip :catch_exceptions and :authenticate unless action is index.
# skip_filter :catch_exceptions, :authenticate, :except => :index
# end
#
# == Filter conditions # == Filter conditions
# #
# Filters can be limited to run for only specific actions. This can be expressed either by listing the actions to # Filters may be limited to specific actions by declaring the actions to
# exclude or the actions to include when executing the filter. Available conditions are +:only+ or +:except+, both # include or exclude. Both options accept single actions (:only => :index)
# of which accept an arbitrary number of method references. For example: # or arrays of actions (:except => [:foo, :bar]).
# #
# class Journal < ActionController::Base # class Journal < ActionController::Base
# # only require authentication if the current action is edit or delete # # Require authentication for edit and delete.
# before_filter :authorize, :only => [ :edit, :delete ] # before_filter :authorize, :only => [:edit, :delete]
#
# # Passing options to a filter with a block.
# around_filter(:except => :index) do |controller, action_block|
# results = Profiler.run(&action_block)
# controller.response.sub! "</body>", "#{results}</body>"
# end
# #
# private # private
# def authorize # def authorize
# # redirect to login unless authenticated # # Redirect to login unless authenticated.
# end # end
# end # end
# #
# When setting conditions on inline method (proc) filters the condition must come first and be placed in parentheses. # == Filter Chain Halting
# #
# class UserPreferences < ActionController::Base # <tt>before_filter</tt> and <tt>around_filter</tt> may halt the request
# before_filter(:except => :new) { # some proc ... } # before controller action is run. This is useful, for example, to deny
# # ... # access to unauthenticated users or to redirect from http to https.
# end # Simply return false from the filter or call render or redirect.
# #
# Around filters halt the request unless the action block is called.
# Given these filters
# after_filter :after
# around_filter :around
# before_filter :before
#
# The filter chain will look like:
#
# ...
# . \
# . #around (code before yield)
# . . \
# . . #before (actual filter code is run)
# . . . \
# . . . execute controller action
# . . . /
# . . ...
# . . /
# . #around (code after yield)
# . /
# #after (actual filter code is run)
#
# If #around returns before yielding, only #after will be run. The #before
# filter and controller action will not be run. If #before returns false,
# the second half of #around and all of #after will still run but the
# action will not.
module ClassMethods module ClassMethods
# The passed <tt>filters</tt> will be appended to the array of filters that's run _before_ actions # The passed <tt>filters</tt> will be appended to the filter_chain and
# on this controller are performed. # will execute before the action on this controller is performed.
def append_before_filter(*filters, &block) def append_before_filter(*filters, &block)
conditions = extract_conditions!(filters) append_filter_to_chain(filters, :before, &block)
filters << block if block_given?
add_action_conditions(filters, conditions)
append_filter_to_chain('before', filters)
end end
# The passed <tt>filters</tt> will be prepended to the array of filters that's run _before_ actions # The passed <tt>filters</tt> will be prepended to the filter_chain and
# on this controller are performed. # will execute before the action on this controller is performed.
def prepend_before_filter(*filters, &block) def prepend_before_filter(*filters, &block)
conditions = extract_conditions!(filters) prepend_filter_to_chain(filters, :before, &block)
filters << block if block_given?
add_action_conditions(filters, conditions)
prepend_filter_to_chain('before', filters)
end end
# Short-hand for append_before_filter since that's the most common of the two. # Shorthand for append_before_filter since it's the most common.
alias :before_filter :append_before_filter alias :before_filter :append_before_filter
# The passed <tt>filters</tt> will be appended to the array of filters that's run _after_ actions # The passed <tt>filters</tt> will be appended to the array of filters
# on this controller are performed. # that run _after_ actions on this controller are performed.
def append_after_filter(*filters, &block) def append_after_filter(*filters, &block)
conditions = extract_conditions!(filters) prepend_filter_to_chain(filters, :after, &block)
filters << block if block_given?
add_action_conditions(filters, conditions)
append_filter_to_chain('after', filters)
end end
# The passed <tt>filters</tt> will be prepended to the array of filters that's run _after_ actions # The passed <tt>filters</tt> will be prepended to the array of filters
# on this controller are performed. # that run _after_ actions on this controller are performed.
def prepend_after_filter(*filters, &block) def prepend_after_filter(*filters, &block)
conditions = extract_conditions!(filters) append_filter_to_chain(filters, :after, &block)
filters << block if block_given?
add_action_conditions(filters, conditions)
prepend_filter_to_chain("after", filters)
end end
# Short-hand for append_after_filter since that's the most common of the two. # Shorthand for append_after_filter since it's the most common.
alias :after_filter :append_after_filter alias :after_filter :append_after_filter
# The passed <tt>filters</tt> 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 # If you append_around_filter A.new, B.new, the filter chain looks like
# respond to both +before+ and +after+. So if you do append_around_filter A.new, B.new, the callstack will look like:
# #
# B#before # B#before
# A#before # A#before
# # run the action
# A#after # A#after
# B#after # B#after
def append_around_filter(*filters) #
conditions = extract_conditions!(filters) # With around filters which yield to the action block, #before and #after
for filter in filters.flatten # are the code before and after the yield.
ensure_filter_responds_to_before_and_after(filter) def append_around_filter(*filters, &block)
append_before_filter(conditions || {}) { |c| filter.before(c) } filters, conditions = extract_conditions(filters, &block)
prepend_after_filter(conditions || {}) { |c| filter.after(c) } filters.map { |f| proxy_before_and_after_filter(f) }.each do |filter|
append_filter_to_chain([filter, conditions])
end end
end end
# The passed <tt>filters</tt> will have their +before+ method prepended to the array of filters that's run both before actions # If you prepend_around_filter A.new, B.new, the filter chain looks like:
# 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 # A#before
# B#before # B#before
# # run the action
# B#after # B#after
# A#after # A#after
def prepend_around_filter(*filters) #
for filter in filters.flatten # With around filters which yield to the action block, #before and #after
ensure_filter_responds_to_before_and_after(filter) # are the code before and after the yield.
prepend_before_filter { |c| filter.before(c) } def prepend_around_filter(*filters, &block)
append_after_filter { |c| filter.after(c) } filters, conditions = extract_conditions(filters, &block)
filters.map { |f| proxy_before_and_after_filter(f) }.each do |filter|
prepend_filter_to_chain([filter, conditions])
end end
end end
# Short-hand for append_around_filter since that's the most common of the two. # Shorthand for append_around_filter since it's the most common.
alias :around_filter :append_around_filter alias :around_filter :append_around_filter
# Removes the specified filters from the +before+ filter chain. Note that this only works for skipping method-reference # Removes the specified filters from the +before+ filter chain. Note that this only works for skipping method-reference
@ -254,15 +320,7 @@ module ActionController #:nodoc:
# You can control the actions to skip the filter for with the <tt>:only</tt> and <tt>:except</tt> options, # You can control the actions to skip the filter for with the <tt>:only</tt> and <tt>:except</tt> options,
# just like when you apply the filters. # just like when you apply the filters.
def skip_before_filter(*filters) def skip_before_filter(*filters)
if conditions = extract_conditions!(filters) skip_filter_in_chain(*filters, &:before?)
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 end
# Removes the specified filters from the +after+ filter chain. Note that this only works for skipping method-reference # Removes the specified filters from the +after+ filter chain. Note that this only works for skipping method-reference
@ -272,76 +330,277 @@ module ActionController #:nodoc:
# You can control the actions to skip the filter for with the <tt>:only</tt> and <tt>:except</tt> options, # You can control the actions to skip the filter for with the <tt>:only</tt> and <tt>:except</tt> options,
# just like when you apply the filters. # just like when you apply the filters.
def skip_after_filter(*filters) def skip_after_filter(*filters)
if conditions = extract_conditions!(filters) skip_filter_in_chain(*filters, &:after?)
remove_contradicting_conditions!(filters, conditions) end
conditions[:only], conditions[:except] = conditions[:except], conditions[:only]
add_action_conditions(filters, conditions) # Removes the specified filters from the filter chain. This only works for method reference (symbol)
else # filters, not procs. This method is different from skip_after_filter and skip_before_filter in that
for filter in filters.flatten # it will match any before, after or yielding around filter.
write_inheritable_attribute("after_filters", read_inheritable_attribute("after_filters") - [ filter ]) #
end # You can control the actions to skip the filter for with the <tt>:only</tt> and <tt>:except</tt> options,
end # just like when you apply the filters.
def skip_filter(*filters)
skip_filter_in_chain(*filters)
end
# Returns an array of Filter objects for this controller.
def filter_chain
read_inheritable_attribute("filter_chain") || []
end end
# Returns all the before filters for this class and all its ancestors. # Returns all the before filters for this class and all its ancestors.
# This method returns the actual filter that was assigned in the controller to maintain existing functionality.
def before_filters #:nodoc: def before_filters #:nodoc:
@before_filters ||= read_inheritable_attribute("before_filters") || [] filter_chain.select(&:before?).map(&:filter)
end end
# Returns all the after filters for this class and all its ancestors. # Returns all the after filters for this class and all its ancestors.
# This method returns the actual filter that was assigned in the controller to maintain existing functionality.
def after_filters #:nodoc: def after_filters #:nodoc:
@after_filters ||= read_inheritable_attribute("after_filters") || [] filter_chain.select(&:after?).map(&:filter)
end end
# Returns a mapping between filters and the actions that may run them. # Returns a mapping between filters and the actions that may run them.
def included_actions #:nodoc: def included_actions #:nodoc:
@included_actions ||= read_inheritable_attribute("included_actions") || {} read_inheritable_attribute("included_actions") || {}
end end
# Returns a mapping between filters and actions that may not run them. # Returns a mapping between filters and actions that may not run them.
def excluded_actions #:nodoc: def excluded_actions #:nodoc:
@excluded_actions ||= read_inheritable_attribute("excluded_actions") || {} read_inheritable_attribute("excluded_actions") || {}
end end
private # Find a filter in the filter_chain where the filter method matches the _filter_ param
def append_filter_to_chain(condition, filters) # and (optionally) the passed block evaluates to true (mostly used for testing before?
write_inheritable_array("#{condition}_filters", filters) # and after? on the filter). Useful for symbol filters.
#
# The object of type Filter is passed to the block when yielded, not the filter itself.
def find_filter(filter, &block) #:nodoc:
filter_chain.select { |f| f.filter == filter && (!block_given? || yield(f)) }.first
end
# Returns true if the filter is excluded from the given action
def filter_excluded_from_action?(filter,action) #:nodoc:
if (ia = included_actions[filter]) && !ia.empty?
!ia.include?(action)
else
(excluded_actions[filter] || []).include?(action)
end
end
# Filter class is an abstract base class for all filters. Handles all of the included/excluded actions but
# contains no logic for calling the actual filters.
class Filter #:nodoc:
attr_reader :filter, :included_actions, :excluded_actions
def initialize(filter)
@filter = filter
end end
def prepend_filter_to_chain(condition, filters) def before?
old_filters = read_inheritable_attribute("#{condition}_filters") || [] false
write_inheritable_attribute("#{condition}_filters", filters + old_filters)
end end
def ensure_filter_responds_to_before_and_after(filter) def after?
unless filter.respond_to?(:before) && filter.respond_to?(:after) false
raise ActionControllerError, "Filter object must respond to both before and after" end
def around?
true
end
def call(controller, &block)
raise(ActionControllerError, 'No filter type: Nothing to do here.')
end
end
# Abstract base class for filter proxies. FilterProxy objects are meant to mimic the behaviour of the old
# before_filter and after_filter by moving the logic into the filter itself.
class FilterProxy < Filter #:nodoc:
def filter
@filter.filter
end
def around?
false
end
end
class BeforeFilterProxy < FilterProxy #:nodoc:
def before?
true
end
def call(controller, &block)
if false == @filter.call(controller) # must only stop if equal to false. only filters returning false are halted.
controller.halt_filter_chain(@filter, :returned_false)
else
yield
end
end
end
class AfterFilterProxy < FilterProxy #:nodoc:
def after?
true
end
def call(controller, &block)
yield
@filter.call(controller)
end
end
class SymbolFilter < Filter #:nodoc:
def call(controller, &block)
controller.send(@filter, &block)
end
end
class ProcFilter < Filter #:nodoc:
def call(controller)
@filter.call(controller)
rescue LocalJumpError # a yield from a proc... no no bad dog.
raise(ActionControllerError, 'Cannot yield from a Proc type filter. The Proc must take two arguments and execute #call on the second argument.')
end
end
class ProcWithCallFilter < Filter #:nodoc:
def call(controller, &block)
@filter.call(controller, block)
rescue LocalJumpError # a yield from a proc... no no bad dog.
raise(ActionControllerError, 'Cannot yield from a Proc type filter. The Proc must take two arguments and execute #call on the second argument.')
end
end
class MethodFilter < Filter #:nodoc:
def call(controller, &block)
@filter.call(controller, &block)
end
end
class ClassFilter < Filter #:nodoc:
def call(controller, &block)
@filter.filter(controller, &block)
end
end
protected
def append_filter_to_chain(filters, position = :around, &block)
write_inheritable_array('filter_chain', create_filters(filters, position, &block) )
end
def prepend_filter_to_chain(filters, position = :around, &block)
write_inheritable_attribute('filter_chain', create_filters(filters, position, &block) + filter_chain)
end
def create_filters(filters, position, &block) #:nodoc:
filters, conditions = extract_conditions(filters, &block)
filters.map! { |filter| find_or_create_filter(filter,position) }
update_conditions(filters, conditions)
filters
end
def find_or_create_filter(filter,position)
if found_filter = find_filter(filter) { |f| f.send("#{position}?") }
found_filter
else
f = class_for_filter(filter).new(filter)
# apply proxy to filter if necessary
case position
when :before
BeforeFilterProxy.new(f)
when :after
AfterFilterProxy.new(f)
else
f
end
end end
end end
def extract_conditions!(filters) # The determination of the filter type was once done at run time.
return nil unless filters.last.is_a? Hash # This method is here to extract as much logic from the filter run time as possible
filters.pop def class_for_filter(filter) #:nodoc:
case
when filter.is_a?(Symbol)
SymbolFilter
when filter.respond_to?(:call)
if filter.is_a?(Method)
MethodFilter
elsif filter.arity == 1
ProcFilter
else
ProcWithCallFilter
end
when filter.respond_to?(:filter)
ClassFilter
else
raise(ActionControllerError, 'A filters must be a Symbol, Proc, Method, or object responding to filter.')
end
end end
def add_action_conditions(filters, conditions) def extract_conditions(*filters, &block) #:nodoc:
return unless conditions filters.flatten!
included, excluded = conditions[:only], conditions[:except] conditions = filters.last.is_a?(Hash) ? filters.pop : {}
write_inheritable_hash('included_actions', condition_hash(filters, included)) && return if included filters << block if block_given?
write_inheritable_hash('excluded_actions', condition_hash(filters, excluded)) if excluded return filters, conditions
end
def update_conditions(filters, conditions)
return if conditions.empty?
if conditions[:only]
write_inheritable_hash('included_actions', condition_hash(filters, conditions[:only]))
else
write_inheritable_hash('excluded_actions', condition_hash(filters, conditions[:except])) if conditions[:except]
end
end end
def condition_hash(filters, *actions) def condition_hash(filters, *actions)
filters.inject({}) {|hash, filter| hash.merge(filter => actions.flatten.map {|action| action.to_s})} actions = actions.flatten.map(&:to_s)
filters.inject({}) { |h,f| h.update( f => (actions.blank? ? nil : actions)) }
end end
def remove_contradicting_conditions!(filters, conditions) def skip_filter_in_chain(*filters, &test) #:nodoc:
return unless conditions[:only] filters, conditions = extract_conditions(filters)
filters.each do |filter| filters.map! { |f| block_given? ? find_filter(f, &test) : find_filter(f) }
next unless included_actions_for_filter = (read_inheritable_attribute('included_actions') || {})[filter] filters.compact!
[*conditions[:only]].each do |conditional_action|
conditional_action = conditional_action.to_s if conditions.empty?
included_actions_for_filter.delete(conditional_action) if included_actions_for_filter.include?(conditional_action) delete_filters_in_chain(filters)
else
remove_actions_from_included_actions!(filters,conditions[:only] || [])
conditions[:only], conditions[:except] = conditions[:except], conditions[:only]
update_conditions(filters,conditions)
end
end
def remove_actions_from_included_actions!(filters,*actions)
actions = actions.flatten.map(&:to_s)
updated_hash = filters.inject(included_actions) do |hash,filter|
ia = (hash[filter] || []) - actions
ia.blank? ? hash.delete(filter) : hash[filter] = ia
hash
end
write_inheritable_attribute('included_actions', updated_hash)
end
def delete_filters_in_chain(filters) #:nodoc:
write_inheritable_attribute('filter_chain', filter_chain.reject { |f| filters.include?(f) })
end
def filter_responds_to_before_and_after(filter) #:nodoc:
filter.respond_to?(:before) && filter.respond_to?(:after)
end
def proxy_before_and_after_filter(filter) #:nodoc:
return filter unless filter_responds_to_before_and_after(filter)
Proc.new do |controller, action|
unless filter.before(controller) == false
begin
action.call
ensure
filter.after(controller)
end
end end
end end
end end
@ -350,26 +609,14 @@ module ActionController #:nodoc:
module InstanceMethods # :nodoc: module InstanceMethods # :nodoc:
def self.included(base) def self.included(base)
base.class_eval do base.class_eval do
alias_method :perform_action_without_filters, :perform_action alias_method_chain :perform_action, :filters
alias_method :perform_action, :perform_action_with_filters alias_method_chain :process, :filters
alias_method_chain :process_cleanup, :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
end end
def perform_action_with_filters def perform_action_with_filters
before_action_result = before_action call_filter(self.class.filter_chain, 0)
unless before_action_result == false || performed?
perform_action_without_filters
after_action
end
@before_filter_chain_aborted = (before_action_result == false)
end end
def process_with_filters(request, response, method = :perform_action, *arguments) #:nodoc: def process_with_filters(request, response, method = :perform_action, *arguments) #:nodoc:
@ -377,61 +624,37 @@ module ActionController #:nodoc:
process_without_filters(request, response, method, *arguments) process_without_filters(request, response, method, *arguments)
end end
# Calls all the defined before-filter filters, which are added by using "before_filter :method". def filter_chain
# If any of the filters return false, no more filters will be executed and the action is aborted. self.class.filter_chain
def before_action #:doc:
call_filters(self.class.before_filters)
end end
# Calls all the defined after-filter filters, which are added by using "after_filter :method". def call_filter(chain, index)
# If any of the filters return false, no more filters will be executed. return (performed? || perform_action_without_filters) if index >= chain.size
def after_action #:doc: filter = chain[index]
call_filters(self.class.after_filters) return call_filter(chain, index.next) if self.class.filter_excluded_from_action?(filter,action_name)
halted = false
filter.call(self) do
halted = call_filter(chain, index.next)
end
halt_filter_chain(filter.filter, :no_yield) if halted == false unless @before_filter_chain_aborted
halted
end
def halt_filter_chain(filter, reason)
if logger
case reason
when :no_yield
logger.info "Filter chain halted as [#{filter.inspect}] did not yield."
when :returned_false
logger.info "Filter chain halted as [#{filter.inspect}] returned false."
end
end
@before_filter_chain_aborted = true
return false
end end
private 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 def process_cleanup_with_filters
if @before_filter_chain_aborted if @before_filter_chain_aborted
close_session close_session

View file

@ -17,7 +17,7 @@ module ActionController #:nodoc:
# end # end
# #
# display.rhtml # display.rhtml
# <% if @flash[:notice] %><div class="notice"><%= @flash[:notice] %></div><% end %> # <% if flash[:notice] %><div class="notice"><%= flash[:notice] %></div><% 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 # 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. # as you like at a time too. Just remember: They'll be gone by the time the next action has been performed.
@ -28,11 +28,9 @@ module ActionController #:nodoc:
base.send :include, InstanceMethods base.send :include, InstanceMethods
base.class_eval do base.class_eval do
alias_method :assign_shortcuts_without_flash, :assign_shortcuts alias_method_chain :assign_shortcuts, :flash
alias_method :assign_shortcuts, :assign_shortcuts_with_flash alias_method_chain :process_cleanup, :flash
alias_method_chain :reset_session, :flash
alias_method :process_cleanup_without_flash, :process_cleanup
alias_method :process_cleanup, :process_cleanup_with_flash
end end
end end
@ -94,7 +92,7 @@ module ActionController #:nodoc:
# #
# flash.keep # keeps the entire flash # flash.keep # keeps the entire flash
# flash.keep(:notice) # keeps only the "notice" entry, the rest of the flash is discarded # flash.keep(:notice) # keeps only the "notice" entry, the rest of the flash is discarded
def keep(k=nil) def keep(k = nil)
use(k, false) use(k, false)
end end
@ -102,7 +100,7 @@ module ActionController #:nodoc:
# #
# flash.keep # keep entire flash available for the next 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) # flash.discard(:warning) # discard the "warning" entry (it'll still be available for the current action)
def discard(k=nil) def discard(k = nil)
use(k) use(k)
end end
@ -118,6 +116,7 @@ module ActionController #:nodoc:
@used.delete(k) @used.delete(k)
end end
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 (@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 end
@ -143,34 +142,39 @@ module ActionController #:nodoc:
end end
def process_cleanup_with_flash def process_cleanup_with_flash
flash.sweep if @session flash.sweep if @_session
process_cleanup_without_flash process_cleanup_without_flash
end end
def reset_session_with_flash
reset_session_without_flash
remove_instance_variable(:@_flash)
flash(:refresh)
end
protected protected
# Access the contents of the flash. Use <tt>flash["notice"]</tt> to read a notice you put there or # Access the contents of the flash. Use <tt>flash["notice"]</tt> to read a notice you put there or
# <tt>flash["notice"] = "hello"</tt> to put a new one. # <tt>flash["notice"] = "hello"</tt> to put a new one.
# Note that if sessions are disabled only flash.now will work. # Note that if sessions are disabled only flash.now will work.
def flash(refresh = false) #:doc: def flash(refresh = false) #:doc:
if @flash.nil? || refresh if !defined?(@_flash) || refresh
@flash = @_flash =
if @session.is_a?(Hash) if session.is_a?(Hash)
# @session is a Hash, if sessions are disabled # don't put flash in session if disabled
# we don't put the flash in the session in this case
FlashHash.new FlashHash.new
else else
# otherwise, @session is a CGI::Session or a TestSession # otherwise, session is a CGI::Session or a TestSession
# so make sure it gets retrieved from/saved to session storage after request processing # so make sure it gets retrieved from/saved to session storage after request processing
@session["flash"] ||= FlashHash.new session["flash"] ||= FlashHash.new
end end
end end
@flash @_flash
end end
# deprecated. use <tt>flash.keep</tt> instead # deprecated. use <tt>flash.keep</tt> instead
def keep_flash #:doc: def keep_flash #:doc:
warn 'keep_flash is deprecated; use flash.keep instead.' ActiveSupport::Deprecation.warn 'keep_flash is deprecated; use flash.keep instead.', caller
flash.keep flash.keep
end end
end end

View file

@ -1,8 +1,6 @@
module ActionController #:nodoc: module ActionController #:nodoc:
module Helpers #:nodoc: module Helpers #:nodoc:
def self.append_features(base) def self.included(base)
super
# Initialize the base module to aggregate its helpers. # Initialize the base module to aggregate its helpers.
base.class_inheritable_accessor :master_helper_module base.class_inheritable_accessor :master_helper_module
base.master_helper_module = Module.new base.master_helper_module = Module.new
@ -13,8 +11,7 @@ module ActionController #:nodoc:
base.class_eval do base.class_eval do
# Wrap inherited to create a new master helper module for subclasses. # Wrap inherited to create a new master helper module for subclasses.
class << self class << self
alias_method :inherited_without_helper, :inherited alias_method_chain :inherited, :helper
alias_method :inherited, :inherited_with_helper
end end
end end
end end

View file

@ -1,6 +1,7 @@
require 'dispatcher' require 'dispatcher'
require 'stringio' require 'stringio'
require 'uri' require 'uri'
require 'action_controller/test_process'
module ActionController module ActionController
module Integration #:nodoc: module Integration #:nodoc:
@ -13,6 +14,7 @@ module ActionController
# rather than instantiating Integration::Session directly. # rather than instantiating Integration::Session directly.
class Session class Session
include Test::Unit::Assertions include Test::Unit::Assertions
include ActionController::Assertions
include ActionController::TestProcess include ActionController::TestProcess
# The integer HTTP status code of the last request. # The integer HTTP status code of the last request.
@ -73,11 +75,11 @@ module ActionController
unless @named_routes_configured unless @named_routes_configured
# install the named routes in this session instance. # install the named routes in this session instance.
klass = class<<self; self; end klass = class<<self; self; end
Routing::NamedRoutes.install(klass) Routing::Routes.named_routes.install(klass)
# the helpers are made protected by default--we make them public for # the helpers are made protected by default--we make them public for
# easier access during testing and troubleshooting. # easier access during testing and troubleshooting.
klass.send(:public, *Routing::NamedRoutes::Helpers) klass.send(:public, *Routing::Routes.named_routes.helpers)
@named_routes_configured = true @named_routes_configured = true
end end
end end
@ -111,7 +113,7 @@ module ActionController
# performed on the location header. # performed on the location header.
def follow_redirect! def follow_redirect!
raise "not a redirect! #{@status} #{@status_message}" unless redirect? raise "not a redirect! #{@status} #{@status_message}" unless redirect?
get(interpret_uri(headers["location"].first)) get(interpret_uri(headers['location'].first))
status status
end end
@ -143,19 +145,33 @@ module ActionController
# (application/x-www-form-urlencoded or multipart/form-data). The headers # (application/x-www-form-urlencoded or multipart/form-data). The headers
# should be a hash. The keys will automatically be upcased, with the # should be a hash. The keys will automatically be upcased, with the
# prefix 'HTTP_' added if needed. # prefix 'HTTP_' added if needed.
#
# You can also perform POST, PUT, DELETE, and HEAD requests with #post,
# #put, #delete, and #head.
def get(path, parameters=nil, headers=nil) def get(path, parameters=nil, headers=nil)
process :get, path, parameters, headers process :get, path, parameters, headers
end end
# Performs a POST request with the given parameters. The parameters may # Performs a POST request with the given parameters. See get() for more details.
# be +nil+, a Hash, or a string that is appropriately encoded
# (application/x-www-form-urlencoded or multipart/form-data). The headers
# should be a hash. The keys will automatically be upcased, with the
# prefix 'HTTP_' added if needed.
def post(path, parameters=nil, headers=nil) def post(path, parameters=nil, headers=nil)
process :post, path, parameters, headers process :post, path, parameters, headers
end end
# Performs a PUT request with the given parameters. See get() for more details.
def put(path, parameters=nil, headers=nil)
process :put, path, parameters, headers
end
# Performs a DELETE request with the given parameters. See get() for more details.
def delete(path, parameters=nil, headers=nil)
process :delete, path, parameters, headers
end
# Performs a HEAD request with the given parameters. See get() for more details.
def head(path, parameters=nil, headers=nil)
process :head, path, parameters, headers
end
# Performs an XMLHttpRequest request with the given parameters, mimicing # Performs an XMLHttpRequest request with the given parameters, mimicing
# the request environment created by the Prototype library. The parameters # the request environment created by the Prototype library. The parameters
# may be +nil+, a Hash, or a string that is appropriately encoded # may be +nil+, a Hash, or a string that is appropriately encoded
@ -163,7 +179,11 @@ module ActionController
# should be a hash. The keys will automatically be upcased, with the # should be a hash. The keys will automatically be upcased, with the
# prefix 'HTTP_' added if needed. # prefix 'HTTP_' added if needed.
def xml_http_request(path, parameters=nil, headers=nil) def xml_http_request(path, parameters=nil, headers=nil)
headers = (headers || {}).merge("X-Requested-With" => "XMLHttpRequest") headers = (headers || {}).merge(
"X-Requested-With" => "XMLHttpRequest",
"Accept" => "text/javascript, text/html, application/xml, text/xml, */*"
)
post(path, parameters, headers) post(path, parameters, headers)
end end
@ -174,7 +194,6 @@ module ActionController
end end
private private
class MockCGI < CGI #:nodoc: class MockCGI < CGI #:nodoc:
attr_accessor :stdinput, :stdoutput, :env_table attr_accessor :stdinput, :stdoutput, :env_table
@ -224,7 +243,7 @@ module ActionController
(headers || {}).each do |key, value| (headers || {}).each do |key, value|
key = key.to_s.upcase.gsub(/-/, "_") key = key.to_s.upcase.gsub(/-/, "_")
key = "HTTP_#{key}" unless env.has_key?(key) || env =~ /^X|HTTP/ key = "HTTP_#{key}" unless env.has_key?(key) || key =~ /^HTTP_/
env[key] = value env[key] = value
end end
@ -247,6 +266,8 @@ module ActionController
# tests. # tests.
@response.extend(TestResponseBehavior) @response.extend(TestResponseBehavior)
@html_document = nil
parse_result parse_result
return status return status
end end
@ -317,9 +338,8 @@ module ActionController
def self.included(base) def self.included(base)
base.extend(ClassMethods) base.extend(ClassMethods)
base.class_eval do base.class_eval do
class <<self class << self
alias_method :new_without_capture, :new alias_method_chain :new, :capture
alias_method :new, :new_with_capture
end end
end end
end end
@ -332,7 +352,9 @@ module ActionController
end end
def new_with_capture(*args) def new_with_capture(*args)
self.last_instantiation ||= new_without_capture(*args) controller = new_without_capture(*args)
self.last_instantiation ||= controller
controller
end end
end end
end end
@ -471,6 +493,8 @@ module ActionController
%w(get post cookies assigns xml_http_request).each do |method| %w(get post cookies assigns xml_http_request).each do |method|
define_method(method) do |*args| define_method(method) do |*args|
reset! unless @integration_session reset! unless @integration_session
# reset the html_document variable, but only for new get/post calls
@html_document = nil unless %w(cookies assigns).include?(method)
returning @integration_session.send(method, *args) do returning @integration_session.send(method, *args) do
copy_session_variables! copy_session_variables!
end end

View file

@ -3,12 +3,13 @@ module ActionController #:nodoc:
def self.included(base) def self.included(base)
base.extend(ClassMethods) base.extend(ClassMethods)
base.class_eval do base.class_eval do
# NOTE: Can't use alias_method_chain here because +render_without_layout+ is already
# defined as a publicly exposed method
alias_method :render_with_no_layout, :render alias_method :render_with_no_layout, :render
alias_method :render, :render_with_a_layout alias_method :render, :render_with_a_layout
class << self class << self
alias_method :inherited_without_layout, :inherited alias_method_chain :inherited, :layout
alias_method :inherited, :inherited_with_layout
end end
end end
end end
@ -26,9 +27,9 @@ module ActionController #:nodoc:
# With layouts, you can flip it around and have the common structure know where to insert changing content. This means # 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: # that the header and footer are only mentioned in one place, like this:
# #
# <!-- The header part of this layout --> # // The header part of this layout
# <%= yield %> # <%= yield %>
# <!-- The footer part of this layout --> # // The footer part of this layout -->
# #
# And then you have content pages that look like this: # And then you have content pages that look like this:
# #
@ -37,9 +38,9 @@ module ActionController #:nodoc:
# Not a word about common structures. At rendering time, the content page is computed and then inserted in the layout, # Not a word about common structures. At rendering time, the content page is computed and then inserted in the layout,
# like this: # like this:
# #
# <!-- The header part of this layout --> # // The header part of this layout
# hello world # hello world
# <!-- The footer part of this layout --> # // The footer part of this layout -->
# #
# == Accessing shared variables # == Accessing shared variables
# #
@ -182,7 +183,6 @@ module ActionController #:nodoc:
private private
def inherited_with_layout(child) def inherited_with_layout(child)
inherited_without_layout(child) inherited_without_layout(child)
child.send :include, Reloadable
layout_match = child.name.underscore.sub(/_controller$/, '').sub(/^controllers\//, '') 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? child.layout(layout_match) unless layout_list.grep(%r{layouts/#{layout_match}\.[a-z][0-9a-z]*$}).empty?
end end
@ -235,6 +235,8 @@ module ActionController #:nodoc:
template_with_options = options.is_a?(Hash) template_with_options = options.is_a?(Hash)
if apply_layout?(template_with_options, options) && (layout = pick_layout(template_with_options, options, deprecated_layout)) if apply_layout?(template_with_options, options) && (layout = pick_layout(template_with_options, options, deprecated_layout))
assert_existence_of_template_file(layout)
options = options.merge :layout => false if template_with_options options = options.merge :layout => false if template_with_options
logger.info("Rendering #{options} within #{layout}") if logger logger.info("Rendering #{options} within #{layout}") if logger
@ -248,6 +250,7 @@ module ActionController #:nodoc:
erase_render_results erase_render_results
add_variables_to_assigns add_variables_to_assigns
@template.instance_variable_set("@content_for_layout", content_for_layout) @template.instance_variable_set("@content_for_layout", content_for_layout)
response.layout = layout
render_text(@template.render_file(layout, true), deprecated_status) render_text(@template.render_file(layout, true), deprecated_status)
else else
render_with_no_layout(options, deprecated_status, &block) render_with_no_layout(options, deprecated_status, &block)
@ -263,7 +266,7 @@ module ActionController #:nodoc:
def candidate_for_layout?(options) def candidate_for_layout?(options)
(options.has_key?(:layout) && options[:layout] != false) || (options.has_key?(:layout) && options[:layout] != false) ||
options.values_at(:text, :xml, :file, :inline, :partial, :nothing).compact.empty? && options.values_at(:text, :xml, :json, :file, :inline, :partial, :nothing).compact.empty? &&
!template_exempt_from_layout?(default_template_name(options[:action] || options[:template])) !template_exempt_from_layout?(default_template_name(options[:action] || options[:template]))
end end

View file

@ -4,11 +4,12 @@ module ActionController
# backing. # backing.
module Macros module Macros
module AutoComplete #:nodoc: module AutoComplete #:nodoc:
def self.append_features(base) #:nodoc: def self.included(base) #:nodoc:
super
base.extend(ClassMethods) base.extend(ClassMethods)
end end
# DEPRECATION WARNING: This method will become a separate plugin when Rails 2.0 ships.
#
# Example: # Example:
# #
# # Controller # # Controller

View file

@ -1,11 +1,12 @@
module ActionController module ActionController
module Macros module Macros
module InPlaceEditing #:nodoc: module InPlaceEditing #:nodoc:
def self.append_features(base) #:nodoc: def self.included(base) #:nodoc:
super
base.extend(ClassMethods) base.extend(ClassMethods)
end end
# DEPRECATION WARNING: This method will become a separate plugin when Rails 2.0 ships.
#
# Example: # Example:
# #
# # Controller # # Controller

View file

@ -8,18 +8,18 @@ module ActionController #:nodoc:
# Without web-service support, an action which collects the data for displaying a list of people # Without web-service support, an action which collects the data for displaying a list of people
# might look something like this: # might look something like this:
# #
# def list # def index
# @people = Person.find(:all) # @people = Person.find(:all)
# end # end
# #
# Here's the same action, with web-service support baked in: # Here's the same action, with web-service support baked in:
# #
# def list # def index
# @people = Person.find(:all) # @people = Person.find(:all)
# #
# respond_to do |wants| # respond_to do |format|
# wants.html # format.html
# wants.xml { render :xml => @people.to_xml } # format.xml { render :xml => @people.to_xml }
# end # end
# end # end
# #
@ -30,7 +30,7 @@ module ActionController #:nodoc:
# Supposing you have an action that adds a new person, optionally creating their company # 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: # (by name) if it does not already exist, without web-services, it might look like this:
# #
# def add # def create
# @company = Company.find_or_create_by_name(params[:company][:name]) # @company = Company.find_or_create_by_name(params[:company][:name])
# @person = @company.people.create(params[:person]) # @person = @company.people.create(params[:person])
# #
@ -39,15 +39,15 @@ module ActionController #:nodoc:
# #
# Here's the same action, with web-service support baked in: # Here's the same action, with web-service support baked in:
# #
# def add # def create
# company = params[:person].delete(:company) # company = params[:person].delete(:company)
# @company = Company.find_or_create_by_name(company[:name]) # @company = Company.find_or_create_by_name(company[:name])
# @person = @company.people.create(params[:person]) # @person = @company.people.create(params[:person])
# #
# respond_to do |wants| # respond_to do |format|
# wants.html { redirect_to(person_list_url) } # format.html { redirect_to(person_list_url) }
# wants.js # format.js
# wants.xml { render :xml => @person.to_xml(:include => @company) } # format.xml { render :xml => @person.to_xml(:include => @company) }
# end # end
# end # end
# #
@ -97,9 +97,8 @@ module ActionController #:nodoc:
# environment.rb as follows. # environment.rb as follows.
# #
# Mime::Type.register "image/jpg", :jpg # Mime::Type.register "image/jpg", :jpg
#
def respond_to(*types, &block) def respond_to(*types, &block)
raise ArgumentError, "respond_to takes either types or a block, never bot" unless types.any? ^ block raise ArgumentError, "respond_to takes either types or a block, never both" unless types.any? ^ block
block ||= lambda { |responder| types.each { |type| responder.send(type) } } block ||= lambda { |responder| types.each { |type| responder.send(type) } }
responder = Responder.new(block.binding) responder = Responder.new(block.binding)
block.call(responder) block.call(responder)
@ -108,15 +107,19 @@ module ActionController #:nodoc:
end end
class Responder #:nodoc: class Responder #:nodoc:
DEFAULT_BLOCKS = { DEFAULT_BLOCKS = [:html, :js, :xml].inject({}) do |blocks, ext|
:html => 'Proc.new { render }', template_extension = (ext == :html ? '' : ".r#{ext}")
:js => 'Proc.new { render :action => "#{action_name}.rjs" }', blocks.update ext => %(Proc.new { render :action => "\#{action_name}#{template_extension}", :content_type => Mime::#{ext.to_s.upcase} })
:xml => 'Proc.new { render :action => "#{action_name}.rxml" }' end
}
def initialize(block_binding) def initialize(block_binding)
@block_binding = block_binding @block_binding = block_binding
@mime_type_priority = eval("request.accepts", block_binding) @mime_type_priority = eval(
"(params[:format] && Mime::EXTENSION_LOOKUP[params[:format]]) ? " +
"[ Mime::EXTENSION_LOOKUP[params[:format]] ] : request.accepts",
block_binding
)
@order = [] @order = []
@responses = {} @responses = {}
end end
@ -127,24 +130,33 @@ module ActionController #:nodoc:
@order << mime_type @order << mime_type
if block_given? if block_given?
@responses[mime_type] = block @responses[mime_type] = Proc.new do
else eval "response.content_type = '#{mime_type.to_s}'", @block_binding
@responses[mime_type] = eval(DEFAULT_BLOCKS[mime_type.to_sym], @block_binding) block.call
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 end
EOT else
if source = DEFAULT_BLOCKS[mime_type.to_sym]
@responses[mime_type] = eval(source, @block_binding)
else
raise ActionController::RenderError, "Expected a block but none was given for custom mime handler #{mime_type}"
end
end
end end
def any(*args, &block) def any(*args, &block)
args.each { |type| send(type, &block) } args.each { |type| send(type, &block) }
end end
def method_missing(symbol, &block)
mime_constant = symbol.to_s.upcase
if Mime::SET.include?(Mime.const_get(mime_constant))
custom(Mime.const_get(mime_constant), &block)
else
super
end
end
def respond def respond
for priority in @mime_type_priority for priority in @mime_type_priority
if priority == Mime::ALL if priority == Mime::ALL

View file

@ -1,5 +1,18 @@
module Mime module Mime
class Type #:nodoc: # Encapsulates the notion of a mime type. Can be used at render time, for example, with:
#
# class PostsController < ActionController::Base
# def show
# @post = Post.find(params[:id])
#
# respond_to do |format|
# format.html
# format.ics { render :text => post.to_ics, :mime_type => Mime::Type["text/calendar"] }
# format.xml { render :xml => @people.to_xml }
# end
# end
# end
class Type
# A simple helper class used in parsing the accept header # A simple helper class used in parsing the accept header
class AcceptItem #:nodoc: class AcceptItem #:nodoc:
attr_accessor :order, :name, :q attr_accessor :order, :name, :q
@ -31,14 +44,20 @@ module Mime
LOOKUP[string] LOOKUP[string]
end end
def register(string, symbol, synonyms = [])
Mime.send :const_set, symbol.to_s.upcase, Type.new(string, symbol, synonyms)
SET << Mime.send(:const_get, symbol.to_s.upcase)
LOOKUP[string] = EXTENSION_LOOKUP[symbol.to_s] = SET.last
end
def parse(accept_header) def parse(accept_header)
# keep track of creation order to keep the subsequent sort stable # keep track of creation order to keep the subsequent sort stable
index = 0 index = 0
list = accept_header.split(/,/). list = accept_header.split(/,/).map! do |i|
map! { |i| AcceptItem.new(index += 1, *i.split(/;\s*q=/)) }.sort! AcceptItem.new(index += 1, *i.split(/;\s*q=/))
end.sort!
# Take care of the broken text/xml entry by renaming or deleting it # Take care of the broken text/xml entry by renaming or deleting it
text_xml = list.index("text/xml") text_xml = list.index("text/xml")
app_xml = list.index("application/xml") app_xml = list.index("application/xml")
@ -112,31 +131,70 @@ module Mime
end end
ALL = Type.new "*/*", :all ALL = Type.new "*/*", :all
TEXT = Type.new "text/plain", :text
HTML = Type.new "text/html", :html, %w( application/xhtml+xml ) HTML = Type.new "text/html", :html, %w( application/xhtml+xml )
JS = Type.new "text/javascript", :js, %w( application/javascript application/x-javascript ) JS = Type.new "text/javascript", :js, %w( application/javascript application/x-javascript )
ICS = Type.new "text/calendar", :ics
CSV = Type.new "text/csv", :csv
XML = Type.new "application/xml", :xml, %w( text/xml application/x-xml ) XML = Type.new "application/xml", :xml, %w( text/xml application/x-xml )
RSS = Type.new "application/rss+xml", :rss RSS = Type.new "application/rss+xml", :rss
ATOM = Type.new "application/atom+xml", :atom ATOM = Type.new "application/atom+xml", :atom
YAML = Type.new "application/x-yaml", :yaml, %w( text/yaml ) YAML = Type.new "application/x-yaml", :yaml, %w( text/yaml )
JSON = Type.new "application/json", :json, %w( text/x-json )
LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) } SET = [ ALL, TEXT, HTML, JS, ICS, XML, RSS, ATOM, YAML, JSON ]
LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k == "" }
LOOKUP["*/*"] = ALL LOOKUP["*/*"] = ALL
LOOKUP["text/plain"] = TEXT
LOOKUP["text/html"] = HTML LOOKUP["text/html"] = HTML
LOOKUP["application/xhtml+xml"] = HTML LOOKUP["application/xhtml+xml"] = HTML
LOOKUP["application/xml"] = XML
LOOKUP["text/xml"] = XML
LOOKUP["application/x-xml"] = XML
LOOKUP["text/javascript"] = JS LOOKUP["text/javascript"] = JS
LOOKUP["application/javascript"] = JS LOOKUP["application/javascript"] = JS
LOOKUP["application/x-javascript"] = JS LOOKUP["application/x-javascript"] = JS
LOOKUP["text/calendar"] = ICS
LOOKUP["text/csv"] = CSV
LOOKUP["application/xml"] = XML
LOOKUP["text/xml"] = XML
LOOKUP["application/x-xml"] = XML
LOOKUP["text/yaml"] = YAML LOOKUP["text/yaml"] = YAML
LOOKUP["application/x-yaml"] = YAML LOOKUP["application/x-yaml"] = YAML
LOOKUP["application/rss+xml"] = RSS LOOKUP["application/rss+xml"] = RSS
LOOKUP["application/atom+xml"] = ATOM LOOKUP["application/atom+xml"] = ATOM
LOOKUP["application/json"] = JSON
LOOKUP["text/x-json"] = JSON
EXTENSION_LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k == "" }
EXTENSION_LOOKUP["html"] = HTML
EXTENSION_LOOKUP["xhtml"] = HTML
EXTENSION_LOOKUP["txt"] = TEXT
EXTENSION_LOOKUP["xml"] = XML
EXTENSION_LOOKUP["js"] = JS
EXTENSION_LOOKUP["ics"] = ICS
EXTENSION_LOOKUP["csv"] = CSV
EXTENSION_LOOKUP["yml"] = YAML
EXTENSION_LOOKUP["yaml"] = YAML
EXTENSION_LOOKUP["rss"] = RSS
EXTENSION_LOOKUP["atom"] = ATOM
EXTENSION_LOOKUP["json"] = JSON
end end

View file

@ -1,6 +1,8 @@
module ActionController module ActionController
# === Action Pack pagination for Active Record collections # === Action Pack pagination for Active Record collections
# #
# DEPRECATION WARNING: Pagination will be separated into its own plugin with Rails 2.0.
#
# The Pagination module aids in the process of paging large collections of # The Pagination module aids in the process of paging large collections of
# Active Record objects. It offers macro-style automatic fetching of your # Active Record objects. It offers macro-style automatic fetching of your
# model for multiple views, or explicit fetching for single actions. And if # model for multiple views, or explicit fetching for single actions. And if
@ -104,8 +106,7 @@ module ActionController
# ClassMethods#paginate. # ClassMethods#paginate.
# #
# +options+ are: # +options+ are:
# <tt>:singular_name</tt>:: the singular name to use, if it can't be inferred by # <tt>:singular_name</tt>:: the singular name to use, if it can't be inferred by singularizing the collection name
# singularizing the collection name
# <tt>:class_name</tt>:: the class name to use, if it can't be inferred by # <tt>:class_name</tt>:: the class name to use, if it can't be inferred by
# camelizing the singular name # camelizing the singular name
# <tt>:per_page</tt>:: the maximum number of items to include in a # <tt>:per_page</tt>:: the maximum number of items to include in a
@ -192,7 +193,7 @@ module ActionController
def paginator_and_collection_for(collection_id, options) #:nodoc: def paginator_and_collection_for(collection_id, options) #:nodoc:
klass = options[:class_name].constantize klass = options[:class_name].constantize
page = @params[options[:parameter]] page = params[options[:parameter]]
count = count_collection_for_pagination(klass, options) count = count_collection_for_pagination(klass, options)
paginator = Paginator.new(self, count, options[:per_page], page) paginator = Paginator.new(self, count, options[:per_page], page)
collection = find_collection_for_pagination(klass, options, paginator) collection = find_collection_for_pagination(klass, options, paginator)

View file

@ -13,12 +13,18 @@ module ActionController
@parameters ||= request_parameters.update(query_parameters).update(path_parameters).with_indifferent_access @parameters ||= request_parameters.update(query_parameters).update(path_parameters).with_indifferent_access
end end
# Returns the HTTP request method as a lowercase symbol (:get, for example) # Returns the HTTP request method as a lowercase symbol (:get, for example). Note, HEAD is returned as :get
# since the two are supposedly to be functionaly equivilent for all purposes except that HEAD won't return a response
# body (which Rails also takes care of elsewhere).
def method def method
@request_method ||= @env['REQUEST_METHOD'].downcase.to_sym @request_method ||= (!parameters[:_method].blank? && @env['REQUEST_METHOD'] == 'POST') ?
parameters[:_method].to_s.downcase.to_sym :
@env['REQUEST_METHOD'].downcase.to_sym
@request_method == :head ? :get : @request_method
end end
# Is this a GET request? Equivalent to request.method == :get # Is this a GET (or HEAD) request? Equivalent to request.method == :get
def get? def get?
method == :get method == :get
end end
@ -38,9 +44,10 @@ module ActionController
method == :delete method == :delete
end end
# Is this a HEAD request? Equivalent to request.method == :head # Is this a HEAD request? HEAD is mapped as :get for request.method, so here we ask the
# REQUEST_METHOD header directly. Thus, for head, both get? and head? will return true.
def head? def head?
method == :head @env['REQUEST_METHOD'].downcase.to_sym == :head
end end
# Determine whether the body of a HTTP call is URL-encoded (default) # Determine whether the body of a HTTP call is URL-encoded (default)
@ -128,19 +135,21 @@ module ActionController
@env['RAW_POST_DATA'] @env['RAW_POST_DATA']
end end
# Returns the request URI correctly, taking into account the idiosyncracies # Return the request URI, accounting for server idiosyncracies.
# of the various servers. # WEBrick includes the full URL. IIS leaves REQUEST_URI blank.
def request_uri def request_uri
if uri = @env['REQUEST_URI'] if uri = @env['REQUEST_URI']
(%r{^\w+\://[^/]+(/.*|$)$} =~ uri) ? $1 : uri # Remove domain, which webrick puts into the request_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 (%r{^\w+\://[^/]+(/.*|$)$} =~ uri) ? $1 : uri
else
# Construct IIS missing REQUEST_URI from SCRIPT_NAME and PATH_INFO.
script_filename = @env['SCRIPT_NAME'].to_s.match(%r{[^/]+$}) script_filename = @env['SCRIPT_NAME'].to_s.match(%r{[^/]+$})
uri = @env['PATH_INFO'] uri = @env['PATH_INFO']
uri = uri.sub(/#{script_filename}\//, '') unless script_filename.nil? uri = uri.sub(/#{script_filename}\//, '') unless script_filename.nil?
unless (env_qs = @env['QUERY_STRING']).nil? || env_qs.empty? unless (env_qs = @env['QUERY_STRING']).nil? || env_qs.empty?
uri << '?' << env_qs uri << '?' << env_qs
end end
uri @env['REQUEST_URI'] = uri
end end
end end
@ -159,8 +168,7 @@ module ActionController
path = (uri = request_uri) ? uri.split('?').first : '' path = (uri = request_uri) ? uri.split('?').first : ''
# Cut off the path to the installation directory if given # Cut off the path to the installation directory if given
root = relative_url_root path.sub!(%r/^#{relative_url_root}/, '')
path[0, root.length] = '' if root
path || '' path || ''
end end

View file

@ -6,12 +6,10 @@ module ActionController #:nodoc:
# #
# You can tailor the rescuing behavior and appearance by overwriting the following two stub methods. # You can tailor the rescuing behavior and appearance by overwriting the following two stub methods.
module Rescue module Rescue
def self.append_features(base) #:nodoc: def self.included(base) #:nodoc:
super
base.extend(ClassMethods) base.extend(ClassMethods)
base.class_eval do base.class_eval do
alias_method :perform_action_without_rescue, :perform_action alias_method_chain :perform_action, :rescue
alias_method :perform_action, :perform_action_with_rescue
end end
end end
@ -36,23 +34,26 @@ module ActionController #:nodoc:
# Overwrite to implement custom logging of errors. By default logs as fatal. # Overwrite to implement custom logging of errors. By default logs as fatal.
def log_error(exception) #:doc: def log_error(exception) #:doc:
if ActionView::TemplateError === exception ActiveSupport::Deprecation.silence do
logger.fatal(exception.to_s) if ActionView::TemplateError === exception
else logger.fatal(exception.to_s)
logger.fatal( else
"\n\n#{exception.class} (#{exception.message}):\n " + logger.fatal(
clean_backtrace(exception).join("\n ") + "\n\n#{exception.class} (#{exception.message}):\n " +
"\n\n" clean_backtrace(exception).join("\n ") +
) "\n\n"
)
end
end end
end end
# Overwrite to implement public exception handling (for requests answering false to <tt>local_request?</tt>). # Overwrite to implement public exception handling (for requests answering false to <tt>local_request?</tt>).
def rescue_action_in_public(exception) #:doc: def rescue_action_in_public(exception) #:doc:
case exception case exception
when RoutingError, UnknownAction then when RoutingError, UnknownAction
render_text(IO.read(File.join(RAILS_ROOT, 'public', '404.html')), "404 Not Found") render_text(IO.read(File.join(RAILS_ROOT, 'public', '404.html')), "404 Not Found")
else render_text "<html><body><h1>Application error (Rails)</h1></body></html>" else
render_text(IO.read(File.join(RAILS_ROOT, 'public', '500.html')), "500 Internal Error")
end end
end end
@ -60,19 +61,19 @@ module ActionController #:nodoc:
# the remote IP being 127.0.0.1. For example, this could include the IP of the developer machine when debugging # the remote IP being 127.0.0.1. For example, this could include the IP of the developer machine when debugging
# remotely. # remotely.
def local_request? #:doc: def local_request? #:doc:
[@request.remote_addr, @request.remote_ip] == ["127.0.0.1"] * 2 [request.remote_addr, request.remote_ip] == ["127.0.0.1"] * 2
end end
# Renders a detailed diagnostics screen on action exceptions. # Renders a detailed diagnostics screen on action exceptions.
def rescue_action_locally(exception) def rescue_action_locally(exception)
add_variables_to_assigns add_variables_to_assigns
@template.instance_variable_set("@exception", exception) @template.instance_variable_set("@exception", exception)
@template.instance_variable_set("@rescues_path", File.dirname(__FILE__) + "/templates/rescues/") @template.instance_variable_set("@rescues_path", File.dirname(rescues_path("stub")))
@template.send(:assign_variables_from_controller) @template.send(:assign_variables_from_controller)
@template.instance_variable_set("@contents", @template.render_file(template_path_for_local_rescue(exception), false)) @template.instance_variable_set("@contents", @template.render_file(template_path_for_local_rescue(exception), false))
@headers["Content-Type"] = "text/html" response.content_type = Mime::HTML
render_file(rescues_path("layout"), response_code_for_rescue(exception)) render_file(rescues_path("layout"), response_code_for_rescue(exception))
end end
@ -80,8 +81,8 @@ module ActionController #:nodoc:
def perform_action_with_rescue #:nodoc: def perform_action_with_rescue #:nodoc:
begin begin
perform_action_without_rescue perform_action_without_rescue
rescue Object => exception rescue Exception => exception # errors from action performed
if defined?(Breakpoint) && @params["BP-RETRY"] if defined?(Breakpoint) && params["BP-RETRY"]
msg = exception.backtrace.first msg = exception.backtrace.first
if md = /^(.+?):(\d+)(?::in `(.+)')?$/.match(msg) then if md = /^(.+?):(\d+)(?::in `(.+)')?$/.match(msg) then
origin_file, origin_line = md[1], md[2].to_i origin_file, origin_line = md[1], md[2].to_i
@ -89,7 +90,7 @@ module ActionController #:nodoc:
set_trace_func(lambda do |type, file, line, method, context, klass| set_trace_func(lambda do |type, file, line, method, context, klass|
if file == origin_file and line == origin_line then if file == origin_file and line == origin_line then
set_trace_func(nil) set_trace_func(nil)
@params["BP-RETRY"] = false params["BP-RETRY"] = false
callstack = caller callstack = caller
callstack.slice!(0) if callstack.first["rescue.rb"] callstack.slice!(0) if callstack.first["rescue.rb"]
@ -127,8 +128,10 @@ module ActionController #:nodoc:
def response_code_for_rescue(exception) def response_code_for_rescue(exception)
case exception case exception
when UnknownAction, RoutingError then "404 Page Not Found" when UnknownAction, RoutingError
else "500 Internal Error" "404 Page Not Found"
else
"500 Internal Error"
end end
end end

View file

@ -1,15 +1,33 @@
module ActionController module ActionController
class AbstractResponse #:nodoc: class AbstractResponse #:nodoc:
DEFAULT_HEADERS = { "Cache-Control" => "no-cache" } DEFAULT_HEADERS = { "Cache-Control" => "no-cache" }
attr_accessor :body, :headers, :session, :cookies, :assigns, :template, :redirected_to, :redirected_to_method_params attr_accessor :body, :headers, :session, :cookies, :assigns, :template, :redirected_to, :redirected_to_method_params, :layout
def initialize def initialize
@body, @headers, @session, @assigns = "", DEFAULT_HEADERS.merge("cookie" => []), [], [] @body, @headers, @session, @assigns = "", DEFAULT_HEADERS.merge("cookie" => []), [], []
end end
def content_type=(mime_type)
@headers["Content-Type"] = charset ? "#{mime_type}; charset=#{charset}" : mime_type
end
def content_type
content_type = String(@headers["Content-Type"]).split(";")[0]
content_type.blank? ? nil : content_type
end
def charset=(encoding)
@headers["Content-Type"] = "#{content_type || "text/html"}; charset=#{encoding}"
end
def charset
charset = String(@headers["Content-Type"]).split(";")[1]
charset.blank? ? nil : charset.strip.split("=")[1]
end
def redirect(to_url, permanently = false) def redirect(to_url, permanently = false)
@headers["Status"] = "302 Found" unless @headers["Status"] == "301 Moved Permanently" @headers["Status"] = "302 Found" unless @headers["Status"] == "301 Moved Permanently"
@headers["location"] = to_url @headers["Location"] = to_url
@body = "<html><body>You are being <a href=\"#{to_url}\">redirected</a>.</body></html>" @body = "<html><body>You are being <a href=\"#{to_url}\">redirected</a>.</body></html>"
end end

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,6 @@
module ActionController module ActionController
module Scaffolding # :nodoc: module Scaffolding # :nodoc:
def self.append_features(base) def self.included(base)
super
base.extend(ClassMethods) base.extend(ClassMethods)
end end
@ -25,7 +24,7 @@ module ActionController
# end # end
# #
# def list # def list
# @entries = Entry.find_all # @entries = Entry.find(:all)
# render_scaffold "list" # render_scaffold "list"
# end # end
# #
@ -159,7 +158,7 @@ module ActionController
# logger.info ("testing template:" + "\#{self.class.controller_path}/\#{action}") if logger # logger.info ("testing template:" + "\#{self.class.controller_path}/\#{action}") if logger
if template_exists?("\#{self.class.controller_path}/\#{action}") if template_exists?("\#{self.class.controller_path}/\#{action}")
render_action(action) render :action => action
else else
@scaffold_class = #{class_name} @scaffold_class = #{class_name}
@scaffold_singular_name, @scaffold_plural_name = "#{singular_name}", "#{plural_name}" @scaffold_singular_name, @scaffold_plural_name = "#{singular_name}", "#{plural_name}"
@ -169,9 +168,9 @@ module ActionController
@template.instance_variable_set("@content_for_layout", @template.render_file(scaffold_path(action.sub(/#{suffix}$/, "")), false)) @template.instance_variable_set("@content_for_layout", @template.render_file(scaffold_path(action.sub(/#{suffix}$/, "")), false))
if !active_layout.nil? if !active_layout.nil?
render_file(active_layout, nil, true) render :file => active_layout, :use_full_path => true
else else
render_file(scaffold_path("layout")) render :file => scaffold_path('layout')
end end
end end
end end

View file

@ -5,6 +5,8 @@ require 'base64'
class CGI class CGI
class Session class Session
attr_reader :data
# Return this session's underlying Session instance. Useful for the DB-backed session stores. # Return this session's underlying Session instance. Useful for the DB-backed session stores.
def model def model
@dbman.model if @dbman @dbman.model if @dbman

View file

@ -26,6 +26,10 @@ class CGI #:nodoc:all
def delete def delete
@@session_data.delete(@session_id) @@session_data.delete(@session_id)
end end
def data
@@session_data[@session_id]
end
end end
end end
end end

View file

@ -93,6 +93,10 @@ begin
end end
@session_data = {} @session_data = {}
end end
def data
@session_data
end
end end
end end
end end

View file

@ -9,11 +9,8 @@ module ActionController #:nodoc:
def self.included(base) def self.included(base)
base.extend(ClassMethods) base.extend(ClassMethods)
base.send :alias_method, :process_without_session_management_support, :process base.send :alias_method_chain, :process, :session_management_support
base.send :alias_method, :process, :process_with_session_management_support base.send :alias_method_chain, :process_cleanup, :session_management_support
base.send :alias_method, :process_cleanup_without_session_management_support, :process_cleanup
base.send :alias_method, :process_cleanup, :process_cleanup_with_session_management_support
end end
module ClassMethods module ClassMethods
@ -123,16 +120,16 @@ module ActionController #:nodoc:
end end
def process_cleanup_with_session_management_support def process_cleanup_with_session_management_support
process_cleanup_without_session_management_support
clear_persistent_model_associations clear_persistent_model_associations
process_cleanup_without_session_management_support
end end
# Clear cached associations in session data so they don't overflow # Clear cached associations in session data so they don't overflow
# the database field. Only applies to ActiveRecordStore since there # the database field. Only applies to ActiveRecordStore since there
# is not a standard way to iterate over session data. # is not a standard way to iterate over session data.
def clear_persistent_model_associations #:doc: def clear_persistent_model_associations #:doc:
if defined?(@session) && @session.instance_variables.include?('@data') if defined?(@_session) && @_session.respond_to?(:data)
session_data = @session.instance_variable_get('@data') session_data = @_session.data
if session_data && session_data.respond_to?(:each_value) if session_data && session_data.respond_to?(:each_value)
session_data.each_value do |obj| session_data.each_value do |obj|

View file

@ -69,17 +69,8 @@ module ActionController #:nodoc:
logger.info "Streaming file #{path}" unless logger.nil? logger.info "Streaming file #{path}" unless logger.nil?
len = options[:buffer_size] || 4096 len = options[:buffer_size] || 4096
File.open(path, 'rb') do |file| File.open(path, 'rb') do |file|
if output.respond_to?(:syswrite) while buf = file.read(len)
begin output.write(buf)
while true
output.syswrite(file.sysread(len))
end
rescue EOFError
end
else
while buf = file.read(len)
output.write(buf)
end
end end
end end
} }
@ -97,8 +88,8 @@ module ActionController #:nodoc:
# * <tt>:type</tt> - specifies an HTTP content type. # * <tt>:type</tt> - specifies an HTTP content type.
# Defaults to 'application/octet-stream'. # Defaults to 'application/octet-stream'.
# * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded. # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
# * <tt>:status</tt> - specifies the status code to send with the response. Defaults to '200 OK'.
# Valid values are 'inline' and 'attachment' (default). # Valid values are 'inline' and 'attachment' (default).
# * <tt>:status</tt> - specifies the status code to send with the response. Defaults to '200 OK'.
# #
# Generic data download: # Generic data download:
# send_data buffer # send_data buffer
@ -128,7 +119,7 @@ module ActionController #:nodoc:
disposition <<= %(; filename="#{options[:filename]}") if options[:filename] disposition <<= %(; filename="#{options[:filename]}") if options[:filename]
@headers.update( headers.update(
'Content-Length' => options[:length], 'Content-Length' => options[:length],
'Content-Type' => options[:type].strip, # fixes a problem with extra '\r' with some browsers 'Content-Type' => options[:type].strip, # fixes a problem with extra '\r' with some browsers
'Content-Disposition' => disposition, 'Content-Disposition' => disposition,
@ -141,7 +132,7 @@ module ActionController #:nodoc:
# after it displays the "open/save" dialog, which means that if you # after it displays the "open/save" dialog, which means that if you
# hit "open" the file isn't there anymore when the application that # 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 # is called for handling the download is run, so let's workaround that
@headers['Cache-Control'] = 'private' if @headers['Cache-Control'] == 'no-cache' headers['Cache-Control'] = 'private' if headers['Cache-Control'] == 'no-cache'
end end
end end
end end

View file

@ -8,10 +8,10 @@
<% if false %> <% if false %>
<br /><br /> <br /><br />
<% begin %> <% begin %>
<%= form_tag(@request.request_uri, "method" => @request.method) %> <%= form_tag(request.request_uri, "method" => request.method) %>
<input type="hidden" name="BP-RETRY" value="1" /> <input type="hidden" name="BP-RETRY" value="1" />
<% for key, values in @params %> <% for key, values in params %>
<% next if key == "BP-RETRY" %> <% next if key == "BP-RETRY" %>
<% for value in Array(values) %> <% for value in Array(values) %>
<input type="hidden" name="<%= key %>" value="<%= value %>" /> <input type="hidden" name="<%= key %>" value="<%= value %>" />
@ -26,7 +26,7 @@
<% end %> <% end %>
<% <%
request_parameters_without_action = @request.parameters.clone request_parameters_without_action = request.parameters.clone
request_parameters_without_action.delete("action") request_parameters_without_action.delete("action")
request_parameters_without_action.delete("controller") request_parameters_without_action.delete("controller")
@ -37,8 +37,8 @@
<p><b>Parameters</b>: <%=h request_dump == "{}" ? "None" : request_dump %></p> <p><b>Parameters</b>: <%=h request_dump == "{}" ? "None" : request_dump %></p>
<p><a href="#" onclick="document.getElementById('session_dump').style.display='block'; return false;">Show session dump</a></p> <p><a href="#" onclick="document.getElementById('session_dump').style.display='block'; return false;">Show session dump</a></p>
<div id="session_dump" style="display:none"><%= debug(@request.session.instance_variable_get("@data")) %></div> <div id="session_dump" style="display:none"><%= debug(request.session.instance_variable_get("@data")) %></div>
<h2 style="margin-top: 30px">Response</h2> <h2 style="margin-top: 30px">Response</h2>
<b>Headers</b>: <%=h @response.headers.inspect.gsub(/,/, ",\n") %><br/> <b>Headers</b>: <%=h response ? response.headers.inspect.gsub(/,/, ",\n") : "None" %><br/>

View file

@ -1,7 +1,7 @@
<h1> <h1>
<%=h @exception.class.to_s %> <%=h @exception.class.to_s %>
<% if @request.parameters['controller'] %> <% if request.parameters['controller'] %>
in <%=h @request.parameters['controller'].humanize %>Controller<% if @request.parameters['action'] %>#<%=h @request.parameters['action'] %><% end %> in <%=h request.parameters['controller'].humanize %>Controller<% if request.parameters['action'] %>#<%=h request.parameters['action'] %><% end %>
<% end %> <% end %>
</h1> </h1>
<pre><%=h @exception.clean_message %></pre> <pre><%=h @exception.clean_message %></pre>

View file

@ -1,10 +1,10 @@
<h1>Routing Error</h1> <h1>Routing Error</h1>
<p><pre><%=h @exception.message %></pre></p> <p><pre><%=h @exception.message %></pre></p>
<% unless @exception.failures.empty? %><p> <% unless @exception.failures.empty? %><p>
<h2>Failure reasons:</h2> <h2>Failure reasons:</h2>
<ol> <ol>
<% @exception.failures.each do |route, reason| %> <% @exception.failures.each do |route, reason| %>
<li><code><%=h route.inspect.gsub('\\', '') %></code> failed because <%=h reason.downcase %></li> <li><code><%=h route.inspect.gsub('\\', '') %></code> failed because <%=h reason.downcase %></li>
<% end %> <% end %>
</ol> </ol>
</p><% end %> </p><% end %>

View file

@ -1,6 +1,6 @@
<h1> <h1>
<%=h @exception.original_exception.class.to_s %> in <%=h @exception.original_exception.class.to_s %> in
<%=h @request.parameters["controller"].capitalize if @request.parameters["controller"]%>#<%=h @request.parameters["action"] %> <%=h request.parameters["controller"].capitalize if request.parameters["controller"]%>#<%=h request.parameters["action"] %>
</h1> </h1>
<p> <p>

View file

@ -14,7 +14,7 @@
<% end %> <% end %>
<td><%= link_to "Show", :action => "show#{@scaffold_suffix}", :id => entry %></td> <td><%= link_to "Show", :action => "show#{@scaffold_suffix}", :id => entry %></td>
<td><%= link_to "Edit", :action => "edit#{@scaffold_suffix}", :id => entry %></td> <td><%= link_to "Edit", :action => "edit#{@scaffold_suffix}", :id => entry %></td>
<td><%= link_to "Destroy", {:action => "destroy#{@scaffold_suffix}", :id => entry}, { :confirm => "Are you sure?", :post => true} %></td> <td><%= link_to "Destroy", {:action => "destroy#{@scaffold_suffix}", :id => entry}, { :confirm => "Are you sure?", :method => :post } %></td>
</tr> </tr>
<% end %> <% end %>
</table> </table>

View file

@ -1,5 +1,4 @@
require File.dirname(__FILE__) + '/assertions' require File.dirname(__FILE__) + '/assertions'
require File.dirname(__FILE__) + '/deprecated_assertions'
module ActionController #:nodoc: module ActionController #:nodoc:
class Base class Base
@ -18,8 +17,7 @@ module ActionController #:nodoc:
end end
end end
alias_method :process_without_test, :process alias_method_chain :process, :test
alias_method :process, :process_with_test
end end
class TestRequest < AbstractRequest #:nodoc: class TestRequest < AbstractRequest #:nodoc:
@ -39,7 +37,7 @@ module ActionController #:nodoc:
end end
def reset_session def reset_session
@session = {} @session = TestSession.new
end end
def raw_post def raw_post
@ -79,6 +77,10 @@ module ActionController #:nodoc:
@path = uri.split("?").first @path = uri.split("?").first
end end
def accept=(mime_types)
@env["HTTP_ACCEPT"] = Array(mime_types).collect { |mime_types| mime_types.to_s }.join(",")
end
def remote_addr=(addr) def remote_addr=(addr)
@env['REMOTE_ADDR'] = addr @env['REMOTE_ADDR'] = addr
end end
@ -103,7 +105,7 @@ module ActionController #:nodoc:
if value.is_a? Fixnum if value.is_a? Fixnum
value = value.to_s value = value.to_s
elsif value.is_a? Array elsif value.is_a? Array
value = ActionController::Routing::PathComponent::Result.new(value) value = ActionController::Routing::PathSegment::Result.new(value)
end end
if extra_keys.include?(key.to_sym) if extra_keys.include?(key.to_sym)
@ -112,6 +114,7 @@ module ActionController #:nodoc:
path_parameters[key.to_s] = value path_parameters[key.to_s] = value
end end
end end
@parameters = nil # reset TestRequest#parameters to use the new path_parameters
end end
def recycle! def recycle!
@ -176,7 +179,7 @@ module ActionController #:nodoc:
# returns the redirection location or nil # returns the redirection location or nil
def redirect_url def redirect_url
redirect? ? headers['location'] : nil headers['Location']
end end
# does the redirect location match this regexp pattern? # does the redirect location match this regexp pattern?
@ -272,25 +275,38 @@ module ActionController #:nodoc:
end end
class TestSession #:nodoc: class TestSession #:nodoc:
def initialize(attributes = {}) attr_accessor :session_id
def initialize(attributes = nil)
@session_id = ''
@attributes = attributes @attributes = attributes
@saved_attributes = nil
end
def data
@attributes ||= @saved_attributes || {}
end end
def [](key) def [](key)
@attributes[key] data[key.to_s]
end end
def []=(key, value) def []=(key, value)
@attributes[key] = value data[key.to_s] = value
end end
def session_id def update
"" @saved_attributes = @attributes
end end
def update() end def delete
def close() end @attributes = nil
def delete() @attributes = {} end end
def close
update
delete
end
end end
# Essentially generates a modified Tempfile object similar to the object # Essentially generates a modified Tempfile object similar to the object
@ -301,6 +317,7 @@ module ActionController #:nodoc:
# #
# Usage example, within a functional test: # Usage example, within a functional test:
# post :change_avatar, :avatar => ActionController::TestUploadedFile.new(Test::Unit::TestCase.fixture_path + '/files/spongebob.png', 'image/png') # post :change_avatar, :avatar => ActionController::TestUploadedFile.new(Test::Unit::TestCase.fixture_path + '/files/spongebob.png', 'image/png')
require 'tempfile'
class TestUploadedFile class TestUploadedFile
# The filename, *not* including the path, of the "uploaded" file # The filename, *not* including the path, of the "uploaded" file
attr_reader :original_filename attr_reader :original_filename
@ -309,7 +326,7 @@ module ActionController #:nodoc:
attr_reader :content_type attr_reader :content_type
def initialize(path, content_type = 'text/plain') def initialize(path, content_type = 'text/plain')
raise "file does not exist" unless File.exist?(path) raise "#{path} file does not exist" unless File.exist?(path)
@content_type = content_type @content_type = content_type
@original_filename = path.sub(/^.*#{File::SEPARATOR}([^#{File::SEPARATOR}]+)$/) { $1 } @original_filename = path.sub(/^.*#{File::SEPARATOR}([^#{File::SEPARATOR}]+)$/) { $1 }
@tempfile = Tempfile.new(@original_filename) @tempfile = Tempfile.new(@original_filename)
@ -333,7 +350,7 @@ module ActionController #:nodoc:
%w( get post put delete head ).each do |method| %w( get post put delete head ).each do |method|
base.class_eval <<-EOV, __FILE__, __LINE__ base.class_eval <<-EOV, __FILE__, __LINE__
def #{method}(action, parameters = nil, session = nil, flash = nil) def #{method}(action, parameters = nil, session = nil, flash = nil)
@request.env['REQUEST_METHOD'] = "#{method.upcase}" if @request @request.env['REQUEST_METHOD'] = "#{method.upcase}" if defined?(@request)
process(action, parameters, session, flash) process(action, parameters, session, flash)
end end
EOV EOV
@ -344,8 +361,10 @@ module ActionController #:nodoc:
def process(action, parameters = nil, session = nil, flash = nil) def process(action, parameters = nil, session = nil, flash = nil)
# Sanity check for required instance variables so we can give an # Sanity check for required instance variables so we can give an
# understandable error message. # understandable error message.
%w(controller request response).each do |iv_name| %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? if !instance_variables.include?(iv_name) || instance_variable_get(iv_name).nil?
raise "#{iv_name} is nil: make sure you set it in your test's setup method."
end
end end
@request.recycle! @request.recycle!
@ -374,8 +393,9 @@ module ActionController #:nodoc:
alias xhr :xml_http_request alias xhr :xml_http_request
def follow_redirect def follow_redirect
if @response.redirected_to[:controller] redirected_controller = @response.redirected_to[:controller]
raise "Can't follow redirects outside of current controller (#{@response.redirected_to[:controller]})" if redirected_controller && redirected_controller != @controller.controller_name
raise "Can't follow redirects outside of current controller (from #{@controller.controller_name} to #{redirected_controller})"
end end
get(@response.redirected_to.delete(:action), @response.redirected_to.stringify_keys) get(@response.redirected_to.delete(:action), @response.redirected_to.stringify_keys)
@ -428,7 +448,7 @@ module ActionController #:nodoc:
end end
def method_missing(selector, *args) def method_missing(selector, *args)
return @controller.send(selector, *args) if ActionController::Routing::NamedRoutes::Helpers.include?(selector) return @controller.send(selector, *args) if ActionController::Routing::Routes.named_routes.helpers.include?(selector)
return super return super
end end
@ -448,13 +468,15 @@ module ActionController #:nodoc:
# The new instance is yielded to the passed block. Typically the block # The new instance is yielded to the passed block. Typically the block
# will create some routes using map.draw { map.connect ... }: # will create some routes using map.draw { map.connect ... }:
# #
# with_routing do |set| # with_routing do |set|
# set.draw { set.connect ':controller/:id/:action' } # set.draw do |map|
# assert_equal( # map.connect ':controller/:action/:id'
# ['/content/10/show', {}], # assert_equal(
# set.generate(:controller => 'content', :id => 10, :action => 'show') # ['/content/10/show', {}],
# ) # map.generate(:controller => 'content', :id => 10, :action => 'show')
# end # end
# end
# end
# #
def with_routing def with_routing
real_routes = ActionController::Routing::Routes real_routes = ActionController::Routing::Routes

View file

@ -1,6 +1,64 @@
module ActionController module ActionController
# Rewrites URLs for Base.redirect_to and Base.url_for in the controller.
# Write URLs from arbitrary places in your codebase, such as your mailers.
#
# Example:
#
# class MyMailer
# include ActionController::UrlWriter
# default_url_options[:host] = 'www.basecamphq.com'
#
# def signup_url(token)
# url_for(:controller => 'signup', action => 'index', :token => token)
# end
# end
#
# In addition to providing +url_for+, named routes are also accessible after
# including UrlWriter.
#
module UrlWriter
# The default options for urls written by this writer. Typically a :host pair
# is provided.
mattr_accessor :default_url_options
self.default_url_options = {}
def self.included(base) #:nodoc:
ActionController::Routing::Routes.named_routes.install base
base.mattr_accessor :default_url_options
base.default_url_options ||= default_url_options
end
# Generate a url with the provided options. The following special options may
# effect the constructed url:
#
# * :host Specifies the host the link should be targetted at. This option
# must be provided either explicitly, or via default_url_options.
# * :protocol The protocol to connect to. Defaults to 'http'
# * :port Optionally specify the port to connect to.
#
def url_for(options)
options = self.class.default_url_options.merge(options)
url = ''
unless options.delete :only_path
url << (options.delete(:protocol) || 'http')
url << '://'
raise "Missing host to link to! Please provide :host parameter or set default_url_options[:host]" unless options[:host]
url << options.delete(:host)
url << ":#{options.delete(:port)}" if options.key?(:port)
else
# Delete the unused options to prevent their appearance in the query string
[:protocol, :host, :port].each { |k| options.delete k }
end
url << Routing::Routes.generate(options, {})
return url
end
end
# Rewrites URLs for Base.redirect_to and Base.url_for in the controller.
class UrlRewriter #:nodoc: class UrlRewriter #:nodoc:
RESERVED_OPTIONS = [:anchor, :params, :only_path, :host, :protocol, :trailing_slash, :skip_relative_url_root] RESERVED_OPTIONS = [:anchor, :params, :only_path, :host, :protocol, :trailing_slash, :skip_relative_url_root]
def initialize(request, parameters) def initialize(request, parameters)
@ -41,34 +99,10 @@ module ActionController
options.update(overwrite) options.update(overwrite)
end end
RESERVED_OPTIONS.each {|k| options.delete k} 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? # Generates the query string, too
Routing::Routes.generate(options, @request.symbolized_path_parameters)
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 end
end end

View file

@ -1,5 +1,6 @@
require File.dirname(__FILE__) + '/tokenizer' require File.dirname(__FILE__) + '/tokenizer'
require File.dirname(__FILE__) + '/node' require File.dirname(__FILE__) + '/node'
require File.dirname(__FILE__) + '/selector'
module HTML #:nodoc: module HTML #:nodoc:

View file

@ -92,7 +92,6 @@ module HTML #:nodoc:
# returns non +nil+. Returns the result of the #find call that succeeded. # returns non +nil+. Returns the result of the #find call that succeeded.
def find(conditions) def find(conditions)
conditions = validate_conditions(conditions) conditions = validate_conditions(conditions)
@children.each do |child| @children.each do |child|
node = child.find(conditions) node = child.find(conditions)
return node if node return node if node
@ -152,11 +151,11 @@ module HTML #:nodoc:
if scanner.skip(/!\[CDATA\[/) if scanner.skip(/!\[CDATA\[/)
scanner.scan_until(/\]\]>/) scanner.scan_until(/\]\]>/)
return CDATA.new(parent, line, pos, scanner.pre_match) return CDATA.new(parent, line, pos, scanner.pre_match.gsub(/<!\[CDATA\[/, ''))
end end
closing = ( scanner.scan(/\//) ? :close : nil ) closing = ( scanner.scan(/\//) ? :close : nil )
return Text.new(parent, line, pos, content) unless name = scanner.scan(/[\w:]+/) return Text.new(parent, line, pos, content) unless name = scanner.scan(/[\w:-]+/)
name.downcase! name.downcase!
unless closing unless closing
@ -239,7 +238,7 @@ module HTML #:nodoc:
def match(conditions) def match(conditions)
case conditions case conditions
when String when String
@content.index(conditions) @content == conditions
when Regexp when Regexp
@content =~ conditions @content =~ conditions
when Hash when Hash
@ -316,7 +315,7 @@ module HTML #:nodoc:
s = "<#{@name}" s = "<#{@name}"
@attributes.each do |k,v| @attributes.each do |k,v|
s << " #{k}" s << " #{k}"
s << "='#{v.gsub(/'/,"\\\\'")}'" if String === v s << "=\"#{v}\"" if String === v
end end
s << " /" if @closing == :self s << " /" if @closing == :self
s << ">" s << ">"
@ -410,7 +409,6 @@ module HTML #:nodoc:
# :child => /hello world/ } # :child => /hello world/ }
def match(conditions) def match(conditions)
conditions = validate_conditions(conditions) conditions = validate_conditions(conditions)
# check content of child nodes # check content of child nodes
if conditions[:content] if conditions[:content]
if children.empty? if children.empty?
@ -455,7 +453,6 @@ module HTML #:nodoc:
# count children # count children
if opts = conditions[:children] if opts = conditions[:children]
matches = children.select do |c| matches = children.select do |c|
c.match(/./) or
(c.kind_of?(HTML::Tag) and (c.closing == :self or ! c.childless?)) (c.kind_of?(HTML::Tag) and (c.closing == :self or ! c.childless?))
end end

View file

@ -1,7 +1,6 @@
module ActionController #:nodoc: module ActionController #:nodoc:
module Verification #:nodoc: module Verification #:nodoc:
def self.append_features(base) #:nodoc: def self.included(base) #:nodoc:
super
base.extend(ClassMethods) base.extend(ClassMethods)
end end
@ -18,19 +17,26 @@ module ActionController #:nodoc:
# Usage: # Usage:
# #
# class GlobalController < ActionController::Base # class GlobalController < ActionController::Base
# # prevent the #update_settings action from being invoked unless # # Prevent the #update_settings action from being invoked unless
# # the 'admin_privileges' request parameter exists. # # the 'admin_privileges' request parameter exists. The
# # settings action will be redirected to in current controller
# # if verification fails.
# verify :params => "admin_privileges", :only => :update_post, # verify :params => "admin_privileges", :only => :update_post,
# :redirect_to => { :action => "settings" } # :redirect_to => { :action => "settings" }
# #
# # disallow a post from being updated if there was no information # # Disallow a post from being updated if there was no information
# # submitted with the post, and if there is no active post in the # # submitted with the post, and if there is no active post in the
# # session, and if there is no "note" key in the flash. # # session, and if there is no "note" key in the flash. The route
# # named category_url will be redirected to if verification fails.
#
# verify :params => "post", :session => "post", "flash" => "note", # verify :params => "post", :session => "post", "flash" => "note",
# :only => :update_post, # :only => :update_post,
# :add_flash => { "alert" => "Failed to create your message" }, # :add_flash => { "alert" => "Failed to create your message" },
# :redirect_to => :category_url # :redirect_to => :category_url
# #
# Note that these prerequisites are not business rules. They do not examine
# the content of the session or the parameters. That level of validation should
# be encapsulated by your domain model or helper methods in the controller.
module ClassMethods module ClassMethods
# Verify the given actions so that if certain prerequisites are not met, # Verify the given actions so that if certain prerequisites are not met,
# the user is redirected to a different action. The +options+ parameter # the user is redirected to a different action. The +options+ parameter
@ -40,7 +46,7 @@ module ActionController #:nodoc:
# be in the <tt>params</tt> hash in order for the action(s) to be safely # be in the <tt>params</tt> hash in order for the action(s) to be safely
# called. # called.
# * <tt>:session</tt>: a single key or an array of keys that must # * <tt>:session</tt>: a single key or an array of keys that must
# be in the @session in order for the action(s) to be safely called. # be in the <tt>session</tt> in order for the action(s) to be safely called.
# * <tt>:flash</tt>: a single key or an array of keys that must # * <tt>:flash</tt>: a single key or an array of keys that must
# be in the flash in order for the action(s) to be safely called. # be in the flash in order for the action(s) to be safely called.
# * <tt>:method</tt>: a single key or an array of keys--any one of which # * <tt>:method</tt>: a single key or an array of keys--any one of which
@ -51,8 +57,12 @@ module ActionController #:nodoc:
# from an Ajax call or not. # from an Ajax call or not.
# * <tt>:add_flash</tt>: a hash of name/value pairs that should be merged # * <tt>:add_flash</tt>: a hash of name/value pairs that should be merged
# into the session's flash if the prerequisites cannot be satisfied. # into the session's flash if the prerequisites cannot be satisfied.
# * <tt>:add_headers</tt>: a hash of name/value pairs that should be
# merged into the response's headers hash if the prerequisites cannot
# be satisfied.
# * <tt>:redirect_to</tt>: the redirection parameters to be used when # * <tt>:redirect_to</tt>: the redirection parameters to be used when
# redirecting if the prerequisites cannot be satisfied. # redirecting if the prerequisites cannot be satisfied. You can
# redirect either to named route or to the action in some controller.
# * <tt>:render</tt>: the render parameters to be used when # * <tt>:render</tt>: the render parameters to be used when
# the prerequisites cannot be satisfied. # the prerequisites cannot be satisfied.
# * <tt>:only</tt>: only apply this verification to the actions specified # * <tt>:only</tt>: only apply this verification to the actions specified
@ -69,19 +79,20 @@ module ActionController #:nodoc:
def verify_action(options) #:nodoc: def verify_action(options) #:nodoc:
prereqs_invalid = prereqs_invalid =
[*options[:params] ].find { |v| @params[v].nil? } || [*options[:params] ].find { |v| params[v].nil? } ||
[*options[:session]].find { |v| @session[v].nil? } || [*options[:session]].find { |v| session[v].nil? } ||
[*options[:flash] ].find { |v| flash[v].nil? } [*options[:flash] ].find { |v| flash[v].nil? }
if !prereqs_invalid && options[:method] if !prereqs_invalid && options[:method]
prereqs_invalid ||= prereqs_invalid ||=
[*options[:method]].all? { |v| @request.method != v.to_sym } [*options[:method]].all? { |v| request.method != v.to_sym }
end end
prereqs_invalid ||= (request.xhr? != options[:xhr]) unless options[:xhr].nil? prereqs_invalid ||= (request.xhr? != options[:xhr]) unless options[:xhr].nil?
if prereqs_invalid if prereqs_invalid
flash.update(options[:add_flash]) if options[:add_flash] flash.update(options[:add_flash]) if options[:add_flash]
response.headers.update(options[:add_headers]) if options[:add_headers]
unless performed? unless performed?
render(options[:render]) if options[:render] render(options[:render]) if options[:render]
redirect_to(options[:redirect_to]) if options[:redirect_to] redirect_to(options[:redirect_to]) if options[:redirect_to]

View file

@ -1,5 +1,5 @@
#-- #--
# Copyright (c) 2004 David Heinemeier Hansson # Copyright (c) 2004-2006 David Heinemeier Hansson
# #
# Permission is hereby granted, free of charge, to any person obtaining # Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the # a copy of this software and associated documentation files (the

View file

@ -1,8 +1,8 @@
module ActionPack #:nodoc: module ActionPack #:nodoc:
module VERSION #:nodoc: module VERSION #:nodoc:
MAJOR = 1 MAJOR = 1
MINOR = 12 MINOR = 13
TINY = 5 TINY = 2
STRING = [MAJOR, MINOR, TINY].join('.') STRING = [MAJOR, MINOR, TINY].join('.')
end end

View file

@ -1,5 +1,5 @@
#-- #--
# Copyright (c) 2004 David Heinemeier Hansson # Copyright (c) 2004-2006 David Heinemeier Hansson
# #
# Permission is hereby granted, free of charge, to any person obtaining # Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the # a copy of this software and associated documentation files (the

View file

@ -1,7 +1,6 @@
require 'erb' require 'erb'
module ActionView #:nodoc: module ActionView #:nodoc:
class ActionViewError < StandardError #:nodoc: class ActionViewError < StandardError #:nodoc:
end end
@ -54,13 +53,22 @@ module ActionView #:nodoc:
# #
# You can pass local variables to sub templates by using a hash with the variable names as keys and the objects as values: # 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 } %> # <%= render "shared/header", { :headline => "Welcome", :person => person } %>
# #
# These can now be accessed in shared/header with: # These can now be accessed in shared/header with:
# #
# Headline: <%= headline %> # Headline: <%= headline %>
# First name: <%= person.first_name %> # First name: <%= person.first_name %>
# #
# If you need to find out whether a certain local variable has been assigned a value in a particular render call,
# you need to use the following pattern:
#
# <% if local_assigns.has_key? :headline %>
# Headline: <%= headline %>
# <% end %>
#
# Testing using <tt>defined? headline</tt> will not work. This is an implementation restriction.
#
# == Template caching # == Template caching
# #
# By default, Rails will compile each template to a method in order to render it. When you alter a template, Rails will # By default, Rails will compile each template to a method in order to render it. When you alter a template, Rails will
@ -148,7 +156,8 @@ module ActionView #:nodoc:
attr_accessor :base_path, :assigns, :template_extension attr_accessor :base_path, :assigns, :template_extension
attr_accessor :controller attr_accessor :controller
attr_reader :logger, :params, :request, :response, :session, :headers, :flash attr_reader :logger, :response, :headers
attr_internal *ActionController::Base::DEPRECATED_INSTANCE_VARIABLES
# Specify trim mode for the ERB compiler. Defaults to '-'. # Specify trim mode for the ERB compiler. Defaults to '-'.
# See ERB documentation for suitable values. # See ERB documentation for suitable values.
@ -199,7 +208,7 @@ module ActionView #:nodoc:
end end
def self.load_helpers(helper_dir)#:nodoc: def self.load_helpers(helper_dir)#:nodoc:
Dir.foreach(helper_dir) do |helper_file| Dir.entries(helper_dir).sort.each do |helper_file|
next unless helper_file =~ /^([a-z][a-z_]*_helper).rb$/ next unless helper_file =~ /^([a-z][a-z_]*_helper).rb$/
require File.join(helper_dir, $1) require File.join(helper_dir, $1)
helper_module_name = $1.camelize helper_module_name = $1.camelize
@ -301,6 +310,9 @@ module ActionView #:nodoc:
# will only be read if it has to be compiled. # will only be read if it has to be compiled.
# #
def compile_and_render_template(extension, template = nil, file_path = nil, local_assigns = {}) #:nodoc: def compile_and_render_template(extension, template = nil, file_path = nil, local_assigns = {}) #:nodoc:
# convert string keys to symbols if requested
local_assigns = local_assigns.symbolize_keys if @@local_assigns_support_string_keys
# compile the given template, if necessary # compile the given template, if necessary
if compile_template?(template, file_path, local_assigns) if compile_template?(template, file_path, local_assigns)
template ||= read_template_file(file_path, extension) template ||= read_template_file(file_path, extension)
@ -311,8 +323,6 @@ module ActionView #:nodoc:
method_name = @@method_names[file_path || template] method_name = @@method_names[file_path || template]
evaluate_assigns evaluate_assigns
local_assigns = local_assigns.symbolize_keys if @@local_assigns_support_string_keys
send(method_name, local_assigns) do |*name| send(method_name, local_assigns) do |*name|
instance_variable_get "@content_for_#{name.first || 'layout'}" instance_variable_get "@content_for_#{name.first || 'layout'}"
end end
@ -386,7 +396,7 @@ module ActionView #:nodoc:
elsif builder_template_exists?(template_path): :rxml elsif builder_template_exists?(template_path): :rxml
elsif javascript_template_exists?(template_path): :rjs elsif javascript_template_exists?(template_path): :rjs
else else
raise ActionViewError, "No rhtml, rxml, rjs or delegate template found for #{template_path}" raise ActionViewError, "No rhtml, rxml, rjs or delegate template found for #{template_path} in #{@base_path}"
end end
end end
@ -427,8 +437,8 @@ module ActionView #:nodoc:
if @@compile_time[render_symbol] && supports_local_assigns?(render_symbol, local_assigns) if @@compile_time[render_symbol] && supports_local_assigns?(render_symbol, local_assigns)
if file_name && !@@cache_template_loading if file_name && !@@cache_template_loading
@@compile_time[render_symbol] < File.mtime(file_name) || (File.symlink?(file_name) ? @@compile_time[render_symbol] < File.mtime(file_name) ||
@@compile_time[render_symbol] < File.lstat(file_name).mtime : false) (File.symlink?(file_name) && (@@compile_time[render_symbol] < File.lstat(file_name).mtime))
end end
else else
true true
@ -440,11 +450,11 @@ module ActionView #:nodoc:
if template_requires_setup?(extension) if template_requires_setup?(extension)
body = case extension.to_sym body = case extension.to_sym
when :rxml when :rxml
"controller.response.content_type ||= 'application/xml'\n" +
"xml = Builder::XmlMarkup.new(:indent => 2)\n" + "xml = Builder::XmlMarkup.new(:indent => 2)\n" +
"@controller.headers['Content-Type'] ||= 'application/xml'\n" +
template template
when :rjs when :rjs
"@controller.headers['Content-Type'] ||= 'text/javascript'\n" + "controller.response.content_type ||= 'text/javascript'\n" +
"update_page do |page|\n#{template}\nend" "update_page do |page|\n#{template}\nend"
end end
else else
@ -457,7 +467,7 @@ module ActionView #:nodoc:
locals_code = "" locals_code = ""
locals_keys.each do |key| locals_keys.each do |key|
locals_code << "#{key} = local_assigns[:#{key}] if local_assigns.has_key?(:#{key})\n" locals_code << "#{key} = local_assigns[:#{key}]\n"
end end
"def #{render_symbol}(local_assigns)\n#{locals_code}#{body}\nend" "def #{render_symbol}(local_assigns)\n#{locals_code}#{body}\nend"
@ -472,34 +482,27 @@ module ActionView #:nodoc:
end end
def assign_method_name(extension, template, file_name) def assign_method_name(extension, template, file_name)
method_name = '_run_' method_key = file_name || template
method_name << "#{extension}_" if extension @@method_names[method_key] ||= compiled_method_name(extension, template, file_name)
end
def compiled_method_name(extension, template, file_name)
['_run', extension, compiled_method_name_file_path_segment(file_name)].compact.join('_').to_sym
end
def compiled_method_name_file_path_segment(file_name)
if file_name if file_name
file_path = File.expand_path(file_name) s = File.expand_path(file_name)
base_path = File.expand_path(@base_path) s.sub!(/^#{Regexp.escape(File.expand_path(RAILS_ROOT))}/, '') if defined?(RAILS_ROOT)
s.gsub!(/([^a-zA-Z0-9_])/) { $1[0].to_s }
i = file_path.index(base_path) s
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 else
@@inline_template_count += 1 (@@inline_template_count += 1).to_s
method_name << @@inline_template_count.to_s
end end
@@method_names[file_name || template] = method_name.intern
end end
def compile_template(extension, template, file_name, local_assigns) def compile_template(extension, template, file_name, local_assigns)
method_key = file_name || template render_symbol = assign_method_name(extension, template, file_name)
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) render_source = create_template_source(extension, template, render_symbol, local_assigns.keys)
line_offset = @@template_args[render_symbol].size line_offset = @@template_args[render_symbol].size
@ -516,18 +519,18 @@ module ActionView #:nodoc:
else else
CompiledTemplates.module_eval(render_source, 'compiled-template', -line_offset) CompiledTemplates.module_eval(render_source, 'compiled-template', -line_offset)
end end
rescue Object => e rescue Exception => e # errors from template code
if logger if logger
logger.debug "ERROR: compiling #{render_symbol} RAISED #{e}" logger.debug "ERROR: compiling #{render_symbol} RAISED #{e}"
logger.debug "Function body: #{render_source}" logger.debug "Function body: #{render_source}"
logger.debug "Backtrace: #{e.backtrace.join("\n")}" logger.debug "Backtrace: #{e.backtrace.join("\n")}"
end end
raise TemplateError.new(@base_path, method_key, @assigns, template, e) raise TemplateError.new(@base_path, file_name || template, @assigns, template, e)
end end
@@compile_time[render_symbol] = Time.now @@compile_time[render_symbol] = Time.now
# logger.debug "Compiled template #{method_key}\n ==> #{render_symbol}" if logger # logger.debug "Compiled template #{file_name || template}\n ==> #{render_symbol}" if logger
end end
end end
end end

View file

@ -55,7 +55,7 @@ module ActionView
begin begin
module_eval(method_def, fake_file_name, initial_line_number) module_eval(method_def, fake_file_name, initial_line_number)
@mtimes[full_key(identifier, arg_names)] = Time.now @mtimes[full_key(identifier, arg_names)] = Time.now
rescue Object => e rescue Exception => e # errors from compiled source
e.blame_file! identifier e.blame_file! identifier
raise raise
end end

View file

@ -85,36 +85,62 @@ module ActionView
# <%= error_message_on "post", "title", "Title simply ", " (or it won't work)", "inputError" %> => # <%= error_message_on "post", "title", "Title simply ", " (or it won't work)", "inputError" %> =>
# <div class="inputError">Title simply can't be empty (or it won't work)</div> # <div class="inputError">Title simply can't be empty (or it won't work)</div>
def error_message_on(object, method, prepend_text = "", append_text = "", css_class = "formError") def error_message_on(object, method, prepend_text = "", append_text = "", css_class = "formError")
if errors = instance_variable_get("@#{object}").errors.on(method) if (obj = instance_variable_get("@#{object}")) && (errors = obj.errors.on(method))
content_tag("div", "#{prepend_text}#{errors.is_a?(Array) ? errors.first : errors}#{append_text}", :class => css_class) content_tag("div", "#{prepend_text}#{errors.is_a?(Array) ? errors.first : errors}#{append_text}", :class => css_class)
else
''
end end
end end
# Returns a string with a div containing all the error messages for the object located as an instance variable by the name # Returns a string with a div containing all of the error messages for the objects located as instance variables by the names
# of <tt>object_name</tt>. This div can be tailored by the following options: # given. If more than one object is specified, the errors for the objects are displayed in the order that the object names are
# provided.
#
# This div can be tailored by the following options:
# #
# * <tt>header_tag</tt> - Used for the header of the error div (default: h2) # * <tt>header_tag</tt> - Used for the header of the error div (default: h2)
# * <tt>id</tt> - The id of the error div (default: errorExplanation) # * <tt>id</tt> - The id of the error div (default: errorExplanation)
# * <tt>class</tt> - The class of the error div (default: errorExplanation) # * <tt>class</tt> - The class of the error div (default: errorExplanation)
# * <tt>object_name</tt> - The object name to use in the header, or
# any text that you prefer. If <tt>object_name</tt> is not set, the name of
# the first object will be used.
#
# Specifying one object:
#
# error_messages_for 'user'
#
# Specifying more than one object (and using the name 'user' in the
# header as the <tt>object_name</tt> instead of 'user_common'):
#
# error_messages_for 'user_common', 'user', :object_name => 'user'
# #
# NOTE: This is a pre-packaged presentation of the errors with embedded strings and a certain HTML structure. If what # 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 # 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. # 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 = {}) def error_messages_for(*params)
options = options.symbolize_keys options = params.last.is_a?(Hash) ? params.pop.symbolize_keys : {}
object = instance_variable_get("@#{object_name}") objects = params.collect {|object_name| instance_variable_get("@#{object_name}") }.compact
if object && !object.errors.empty? count = objects.inject(0) {|sum, object| sum + object.errors.count }
content_tag("div", unless count.zero?
content_tag( html = {}
options[:header_tag] || "h2", [:id, :class].each do |key|
"#{pluralize(object.errors.count, "error")} prohibited this #{object_name.to_s.gsub("_", " ")} from being saved" if options.include?(key)
) + value = options[key]
content_tag("p", "There were problems with the following fields:") + html[key] = value unless value.blank?
content_tag("ul", object.errors.full_messages.collect { |msg| content_tag("li", msg) }), else
"id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation" html[key] = 'errorExplanation'
end
end
header_message = "#{pluralize(count, 'error')} prohibited this #{(options[:object_name] || params.first).to_s.gsub('_', ' ')} from being saved"
error_messages = objects.map {|object| object.errors.full_messages.map {|msg| content_tag(:li, msg) } }
content_tag(:div,
content_tag(options[:header_tag] || :h2, header_message) <<
content_tag(:p, 'There were problems with the following fields:') <<
content_tag(:ul, error_messages),
html
) )
else else
"" ''
end end
end end
@ -137,12 +163,14 @@ module ActionView
to_input_field_tag(field_type, options) to_input_field_tag(field_type, options)
when :text when :text
to_text_area_tag(options) to_text_area_tag(options)
when :integer, :float when :integer, :float, :decimal
to_input_field_tag("text", options) to_input_field_tag("text", options)
when :date when :date
to_date_select_tag(options) to_date_select_tag(options)
when :datetime, :timestamp when :datetime, :timestamp
to_datetime_select_tag(options) to_datetime_select_tag(options)
when :time
to_time_select_tag(options)
when :boolean when :boolean
to_boolean_select_tag(options) to_boolean_select_tag(options)
end end
@ -184,6 +212,15 @@ module ActionView
end end
end end
alias_method :to_time_select_tag_without_error_wrapping, :to_time_select_tag
def to_time_select_tag(options = {})
if object.respond_to?("errors") && object.errors.respond_to?("on")
error_wrapping(to_time_select_tag_without_error_wrapping(options), object.errors.on(@method_name))
else
to_time_select_tag_without_error_wrapping(options)
end
end
def error_wrapping(html_tag, has_error) def error_wrapping(html_tag, has_error)
has_error ? Base.field_error_proc.call(html_tag, self) : html_tag has_error ? Base.field_error_proc.call(html_tag, self) : html_tag
end end

View file

@ -3,20 +3,36 @@ require File.dirname(__FILE__) + '/url_helper'
require File.dirname(__FILE__) + '/tag_helper' require File.dirname(__FILE__) + '/tag_helper'
module ActionView module ActionView
module Helpers module Helpers #:nodoc:
# Provides methods for linking a HTML page together with other assets, such as javascripts, stylesheets, and feeds. # Provides methods for linking an HTML page together with other assets such
# as images, javascripts, stylesheets, and feeds. You can direct Rails to
# link to assets from a dedicated assets server by setting ActionController::Base.asset_host
# in your environment.rb. These methods do not verify the assets exist before
# linking to them.
#
# ActionController::Base.asset_host = "http://assets.example.com"
# image_tag("rails.png")
# => <img src="http://assets.example.com/images/rails.png" alt="Rails" />
# stylesheet_include_tag("application")
# => <link href="http://assets.example.com/stylesheets/application.css" media="screen" rel="Stylesheet" type="text/css" />
module AssetTagHelper 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 # Returns a link tag that browsers and news readers can use to auto-detect
# either be <tt>:rss</tt> (default) or <tt>:atom</tt> and the +options+ follow the url_for style of declaring a link target. # an RSS or ATOM feed. The +type+ can either be <tt>:rss</tt> (default) or
# <tt>:atom</tt>. Control the link options in url_for format using the
# +url_options+. You can modify the LINK tag itself in +tag_options+.
# #
# Examples: # Tag Options:
# auto_discovery_link_tag # => # * <tt>:rel</tt> - Specify the relation of this link, defaults to "alternate"
# * <tt>:type</tt> - Override the auto-generated mime type
# * <tt>:title</tt> - Specify the title of the link, defaults to the +type+
#
# auto_discovery_link_tag # =>
# <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.curenthost.com/controller/action" /> # <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.curenthost.com/controller/action" />
# auto_discovery_link_tag(:atom) # => # auto_discovery_link_tag(:atom) # =>
# <link rel="alternate" type="application/atom+xml" title="ATOM" href="http://www.curenthost.com/controller/action" /> # <link rel="alternate" type="application/atom+xml" title="ATOM" href="http://www.curenthost.com/controller/action" />
# auto_discovery_link_tag(:rss, {:action => "feed"}) # => # auto_discovery_link_tag(:rss, {:action => "feed"}) # =>
# <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.curenthost.com/controller/feed" /> # <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.curenthost.com/controller/feed" />
# auto_discovery_link_tag(:rss, {:action => "feed"}, {:title => "My RSS"}) # => # auto_discovery_link_tag(:rss, {:action => "feed"}, {:title => "My RSS"}) # =>
# <link rel="alternate" type="application/rss+xml" title="My RSS" href="http://www.curenthost.com/controller/feed" /> # <link rel="alternate" type="application/rss+xml" title="My RSS" href="http://www.curenthost.com/controller/feed" />
def auto_discovery_link_tag(type = :rss, url_options = {}, tag_options = {}) def auto_discovery_link_tag(type = :rss, url_options = {}, tag_options = {})
tag( tag(
@ -28,9 +44,14 @@ module ActionView
) )
end end
# Returns path to a javascript asset. Example: # Computes the path to a javascript asset in the public javascripts directory.
# If the +source+ filename has no extension, .js will be appended.
# Full paths from the document root will be passed through.
# Used internally by javascript_include_tag to build the script path.
# #
# javascript_path "xmlhr" # => /javascripts/xmlhr.js # javascript_path "xmlhr" # => /javascripts/xmlhr.js
# javascript_path "dir/xmlhr.js" # => /javascripts/dir/xmlhr.js
# javascript_path "/dir/xmlhr" # => /dir/xmlhr.js
def javascript_path(source) def javascript_path(source)
compute_public_path(source, 'javascripts', 'js') compute_public_path(source, 'javascripts', 'js')
end end
@ -38,7 +59,15 @@ module ActionView
JAVASCRIPT_DEFAULT_SOURCES = ['prototype', 'effects', 'dragdrop', 'controls'] unless const_defined?(:JAVASCRIPT_DEFAULT_SOURCES) JAVASCRIPT_DEFAULT_SOURCES = ['prototype', 'effects', 'dragdrop', 'controls'] unless const_defined?(:JAVASCRIPT_DEFAULT_SOURCES)
@@javascript_default_sources = JAVASCRIPT_DEFAULT_SOURCES.dup @@javascript_default_sources = JAVASCRIPT_DEFAULT_SOURCES.dup
# Returns a script include tag per source given as argument. Examples: # Returns an html script tag for each of the +sources+ provided. You
# can pass in the filename (.js extension is optional) of javascript files
# that exist in your public/javascripts directory for inclusion into the
# current page or you can pass the full path relative to your document
# root. To include the Prototype and Scriptaculous javascript libraries in
# your application, pass <tt>:defaults</tt> as the source. When using
# :defaults, if an <tt>application.js</tt> file exists in your public
# javascripts directory, it will be included as well. You can modify the
# html attributes of the script tag by passing a hash as the last argument.
# #
# javascript_include_tag "xmlhr" # => # javascript_include_tag "xmlhr" # =>
# <script type="text/javascript" src="/javascripts/xmlhr.js"></script> # <script type="text/javascript" src="/javascripts/xmlhr.js"></script>
@ -52,11 +81,6 @@ module ActionView
# <script type="text/javascript" src="/javascripts/effects.js"></script> # <script type="text/javascript" src="/javascripts/effects.js"></script>
# ... # ...
# <script type="text/javascript" src="/javascripts/application.js"></script> *see below # <script type="text/javascript" src="/javascripts/application.js"></script> *see below
#
# If there's an <tt>application.js</tt> file in your <tt>public/javascripts</tt> directory,
# <tt>javascript_include_tag :defaults</tt> will automatically include it. This file
# facilitates the inclusion of small snippets of JavaScript code, along the lines of
# <tt>controllers/application.rb</tt> and <tt>helpers/application_helper.rb</tt>.
def javascript_include_tag(*sources) def javascript_include_tag(*sources)
options = sources.last.is_a?(Hash) ? sources.pop.stringify_keys : { } options = sources.last.is_a?(Hash) ? sources.pop.stringify_keys : { }
@ -69,18 +93,16 @@ module ActionView
sources << "application" if defined?(RAILS_ROOT) && File.exists?("#{RAILS_ROOT}/public/javascripts/application.js") sources << "application" if defined?(RAILS_ROOT) && File.exists?("#{RAILS_ROOT}/public/javascripts/application.js")
end end
sources.collect { |source| sources.collect do |source|
source = javascript_path(source) source = javascript_path(source)
content_tag("script", "", { "type" => "text/javascript", "src" => source }.merge(options)) content_tag("script", "", { "type" => "text/javascript", "src" => source }.merge(options))
}.join("\n") end.join("\n")
end end
# Register one or more additional JavaScript files to be included when # Register one or more additional JavaScript files to be included when
# # <tt>javascript_include_tag :defaults</tt> is called. This method is
# javascript_include_tag :defaults # only intended to be called from plugin initialization to register additional
# # .js files that the plugin installed in <tt>public/javascripts</tt>.
# is called. This method is intended to be called only from plugin initialization
# to register extra .js files the plugin installed in <tt>public/javascripts</tt>.
def self.register_javascript_include_default(*sources) def self.register_javascript_include_default(*sources)
@@javascript_default_sources.concat(sources) @@javascript_default_sources.concat(sources)
end end
@ -89,14 +111,21 @@ module ActionView
@@javascript_default_sources = JAVASCRIPT_DEFAULT_SOURCES.dup @@javascript_default_sources = JAVASCRIPT_DEFAULT_SOURCES.dup
end end
# Returns path to a stylesheet asset. Example: # Computes the path to a stylesheet asset in the public stylesheets directory.
# If the +source+ filename has no extension, .css will be appended.
# Full paths from the document root will be passed through.
# Used internally by stylesheet_link_tag to build the stylesheet path.
# #
# stylesheet_path "style" # => /stylesheets/style.css # stylesheet_path "style" # => /stylesheets/style.css
# stylesheet_path "dir/style.css" # => /stylesheets/dir/style.css
# stylesheet_path "/dir/style.css" # => /dir/style.css
def stylesheet_path(source) def stylesheet_path(source)
compute_public_path(source, 'stylesheets', 'css') compute_public_path(source, 'stylesheets', 'css')
end end
# Returns a css link tag per source given as argument. Examples: # Returns a stylesheet link tag for the sources specified as arguments. If
# you don't specify an extension, .css will be appended automatically.
# You can modify the link attributes by passing a hash as the last argument.
# #
# stylesheet_link_tag "style" # => # stylesheet_link_tag "style" # =>
# <link href="/stylesheets/style.css" media="screen" rel="Stylesheet" type="text/css" /> # <link href="/stylesheets/style.css" media="screen" rel="Stylesheet" type="text/css" />
@ -109,31 +138,50 @@ module ActionView
# <link href="/css/stylish.css" media="screen" rel="Stylesheet" type="text/css" /> # <link href="/css/stylish.css" media="screen" rel="Stylesheet" type="text/css" />
def stylesheet_link_tag(*sources) def stylesheet_link_tag(*sources)
options = sources.last.is_a?(Hash) ? sources.pop.stringify_keys : { } options = sources.last.is_a?(Hash) ? sources.pop.stringify_keys : { }
sources.collect { |source| sources.collect do |source|
source = stylesheet_path(source) source = stylesheet_path(source)
tag("link", { "rel" => "Stylesheet", "type" => "text/css", "media" => "screen", "href" => source }.merge(options)) tag("link", { "rel" => "Stylesheet", "type" => "text/css", "media" => "screen", "href" => source }.merge(options))
}.join("\n") end.join("\n")
end end
# Returns path to an image asset. Example: # Computes the path to an image asset in the public images directory.
# Full paths from the document root will be passed through.
# Used internally by image_tag to build the image path. Passing
# a filename without an extension is deprecated.
# #
# The +src+ can be supplied as a... # image_path("edit.png") # => /images/edit.png
# * full path, like "/my_images/image.gif" # image_path("icons/edit.png") # => /images/icons/edit.png
# * file name, like "rss.gif", that gets expanded to "/images/rss.gif" # image_path("/icons/edit.png") # => /icons/edit.png
# * file name without extension, like "logo", that gets expanded to "/images/logo.png"
def image_path(source) def image_path(source)
unless (source.split("/").last || source).include?(".") || source.blank?
ActiveSupport::Deprecation.warn(
"You've called image_path with a source that doesn't include an extension. " +
"In Rails 2.0, that will not result in .png automatically being appended. " +
"So you should call image_path('#{source}.png') instead", caller
)
end
compute_public_path(source, 'images', 'png') compute_public_path(source, 'images', 'png')
end end
# Returns an image tag converting the +options+ into html options on the tag, but with these special cases: # Returns an html image tag for the +source+. The +source+ can be a full
# path or a file that exists in your public images directory. Note that
# specifying a filename without the extension is now deprecated in Rails.
# You can add html attributes using the +options+. The +options+ supports
# two additional keys for convienence and conformance:
# #
# * <tt>:alt</tt> - If no alt text is given, the file name part of the +src+ is used (capitalized and without the extension) # * <tt>:alt</tt> - If no alt text is given, the file name part of the
# * <tt>:size</tt> - Supplied as "XxY", so "30x45" becomes width="30" and height="45" # +source+ is used (capitalized and without the extension)
# * <tt>:size</tt> - Supplied as "{Width}x{Height}", so "30x45" becomes
# width="30" and height="45". <tt>:size</tt> will be ignored if the
# value is not in the correct format.
# #
# The +src+ can be supplied as a... # image_tag("icon.png") # =>
# * full path, like "/my_images/image.gif" # <img src="/images/icon.png" alt="Icon" />
# * file name, like "rss.gif", that gets expanded to "/images/rss.gif" # image_tag("icon.png", :size => "16x10", :alt => "Edit Entry") # =>
# * file name without extension, like "logo", that gets expanded to "/images/logo.png" # <img src="/images/icon.png" width="16" height="10" alt="Edit Entry" />
# image_tag("/icons/icon.gif", :size => "16x16") # =>
# <img src="/icons/icon.gif" width="16" height="16" alt="Icon" />
def image_tag(source, options = {}) def image_tag(source, options = {})
options.symbolize_keys! options.symbolize_keys!
@ -141,8 +189,8 @@ module ActionView
options[:alt] ||= File.basename(options[:src], '.*').split('.').first.capitalize options[:alt] ||= File.basename(options[:src], '.*').split('.').first.capitalize
if options[:size] if options[:size]
options[:width], options[:height] = options[:size].split("x") options[:width], options[:height] = options[:size].split("x") if options[:size] =~ %r{^\d+x\d+$}
options.delete :size options.delete(:size)
end end
tag("img", options) tag("img", options)
@ -150,11 +198,14 @@ module ActionView
private private
def compute_public_path(source, dir, ext) def compute_public_path(source, dir, ext)
source = "/#{dir}/#{source}" unless source.first == "/" || source.include?(":") source = source.dup
source << ".#{ext}" unless source.split("/").last.include?(".") source << ".#{ext}" if File.extname(source).blank?
source << '?' + rails_asset_id(source) if defined?(RAILS_ROOT) && %r{^[-a-z]+://} !~ source unless source =~ %r{^[-a-z]+://}
source = "#{@controller.request.relative_url_root}#{source}" unless %r{^[-a-z]+://} =~ source source = "/#{dir}/#{source}" unless source[0] == ?/
source = ActionController::Base.asset_host + source unless source.include?(":") asset_id = rails_asset_id(source)
source << '?' + asset_id if defined?(RAILS_ROOT) && !asset_id.blank?
source = "#{ActionController::Base.asset_host}#{@controller.request.relative_url_root}#{source}"
end
source source
end end

View file

@ -89,7 +89,7 @@ module ActionView
# named @@content_for_#{name_of_the_content_block}@. So <tt><%= content_for('footer') %></tt> # named @@content_for_#{name_of_the_content_block}@. So <tt><%= content_for('footer') %></tt>
# would be avaiable as <tt><%= @content_for_footer %></tt>. The preferred notation now is # would be avaiable as <tt><%= @content_for_footer %></tt>. The preferred notation now is
# <tt><%= yield :footer %></tt>. # <tt><%= yield :footer %></tt>.
def content_for(name, &block) def content_for(name, content = nil, &block)
eval "@content_for_#{name} = (@content_for_#{name} || '') + capture(&block)" eval "@content_for_#{name} = (@content_for_#{name} || '') + capture(&block)"
end end

View file

@ -13,14 +13,38 @@ module ActionView
module DateHelper module DateHelper
DEFAULT_PREFIX = 'date' unless const_defined?('DEFAULT_PREFIX') DEFAULT_PREFIX = 'date' unless const_defined?('DEFAULT_PREFIX')
# Reports the approximate distance in time between two Time objects or integers. # Reports the approximate distance in time between two Time or Date objects or integers as seconds.
# For example, if the distance is 47 minutes, it'll return # Set <tt>include_seconds</tt> to true if you want more detailed approximations when distance < 1 min, 29 secs
# "about 1 hour". See the source for the complete wording list. # Distances are reported base on the following table:
# #
# Integers are interpreted as seconds. So, # 0 <-> 29 secs # => less than a minute
# <tt>distance_of_time_in_words(50)</tt> returns "less than a minute". # 30 secs <-> 1 min, 29 secs # => 1 minute
# 1 min, 30 secs <-> 44 mins, 29 secs # => [2..44] minutes
# 44 mins, 30 secs <-> 89 mins, 29 secs # => about 1 hour
# 89 mins, 29 secs <-> 23 hrs, 59 mins, 29 secs # => about [2..24] hours
# 23 hrs, 59 mins, 29 secs <-> 47 hrs, 59 mins, 29 secs # => 1 day
# 47 hrs, 59 mins, 29 secs <-> 29 days, 23 hrs, 59 mins, 29 secs # => [2..29] days
# 29 days, 23 hrs, 59 mins, 30 secs <-> 59 days, 23 hrs, 59 mins, 29 secs # => about 1 month
# 59 days, 23 hrs, 59 mins, 30 secs <-> 1 yr minus 31 secs # => [2..12] months
# 1 yr minus 30 secs <-> 2 yrs minus 31 secs # => about 1 year
# 2 yrs minus 30 secs <-> max time or date # => over [2..X] years
# #
# Set <tt>include_seconds</tt> to true if you want more detailed approximations if distance < 1 minute # With include_seconds = true and the difference < 1 minute 29 seconds
# 0-4 secs # => less than 5 seconds
# 5-9 secs # => less than 10 seconds
# 10-19 secs # => less than 20 seconds
# 20-39 secs # => half a minute
# 40-59 secs # => less than a minute
# 60-89 secs # => 1 minute
#
# Examples:
#
# from_time = Time.now
# distance_of_time_in_words(from_time, from_time + 50.minutes) # => about 1 hour
# distance_of_time_in_words(from_time, from_time + 15.seconds) # => less than a minute
# distance_of_time_in_words(from_time, from_time + 15.seconds, true) # => less than 20 seconds
#
# Note: Rails calculates one year as 365.25 days.
def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false) 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) 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) to_time = to_time.to_time if to_time.respond_to?(:to_time)
@ -29,21 +53,25 @@ module ActionView
case distance_in_minutes case distance_in_minutes
when 0..1 when 0..1
return (distance_in_minutes==0) ? 'less than a minute' : '1 minute' unless include_seconds return (distance_in_minutes == 0) ? 'less than a minute' : '1 minute' unless include_seconds
case distance_in_seconds case distance_in_seconds
when 0..5 then 'less than 5 seconds' when 0..4 then 'less than 5 seconds'
when 6..10 then 'less than 10 seconds' when 5..9 then 'less than 10 seconds'
when 11..20 then 'less than 20 seconds' when 10..19 then 'less than 20 seconds'
when 21..40 then 'half a minute' when 20..39 then 'half a minute'
when 41..59 then 'less than a minute' when 40..59 then 'less than a minute'
else '1 minute' else '1 minute'
end end
when 2..45 then "#{distance_in_minutes} minutes" when 2..44 then "#{distance_in_minutes} minutes"
when 46..90 then 'about 1 hour' when 45..89 then 'about 1 hour'
when 90..1440 then "about #{(distance_in_minutes.to_f / 60.0).round} hours" when 90..1439 then "about #{(distance_in_minutes.to_f / 60.0).round} hours"
when 1441..2880 then '1 day' when 1440..2879 then '1 day'
else "#{(distance_in_minutes / 1440).round} days" when 2880..43199 then "#{(distance_in_minutes / 1440).round} days"
when 43200..86399 then 'about 1 month'
when 86400..525959 then "#{(distance_in_minutes / 43200).round} months"
when 525960..1051919 then 'about 1 year'
else "over #{(distance_in_minutes / 525960).round} years"
end end
end end
@ -80,6 +108,19 @@ module ActionView
InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_date_select_tag(options) InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_date_select_tag(options)
end end
# Returns a set of select tags (one for hour, minute and optionally second) pre-selected for accessing a specified
# time-based attribute (identified by +method+) on an object assigned to the template (identified by +object+).
# You can include the seconds with <tt>:include_seconds</tt>.
# Examples:
#
# time_select("post", "sunrise")
# time_select("post", "start_time", :include_seconds => true)
#
# The selects are prepared for multi-parameter assignment to an Active Record object.
def time_select(object_name, method, options = {})
InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_time_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 # 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: # attribute (identified by +method+) on an object assigned to the template (identified by +object+). Examples:
# #
@ -91,36 +132,55 @@ module ActionView
InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_datetime_select_tag(options) InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_datetime_select_tag(options)
end 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+. # 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 = {}) # It's also possible to explicitly set the order of the tags using the <tt>:order</tt> option with an array of
select_year(datetime, options) + select_month(datetime, options) + select_day(datetime, options) + # symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. If you do not supply a Symbol, it
select_hour(datetime, options) + select_minute(datetime, options) # will be appened onto the <tt>:order</tt> passed in. You can also add <tt>:date_separator</tt> and <tt>:time_separator</tt>
# keys to the +options+ to control visual display of the elements.
def select_datetime(datetime = Time.now, options = {})
separator = options[:datetime_separator] || ''
select_date(datetime, options) + separator + select_time(datetime, options)
end
# Returns a set of html select-tags (one for year, month, and day) pre-selected with the +date+.
# It's possible to explicitly set the order of the tags using the <tt>:order</tt> option with an array of
# symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. If you do not supply a Symbol, it
# will be appened onto the <tt>:order</tt> passed in.
def select_date(date = Date.today, options = {})
options[:order] ||= []
[:year, :month, :day].each { |o| options[:order].push(o) unless options[:order].include?(o) }
select_date = ''
options[:order].each do |o|
select_date << self.send("select_#{o}", date, options)
end
select_date
end end
# Returns a set of html select-tags (one for hour and minute) # Returns a set of html select-tags (one for hour and minute)
# You can set <tt>:add_separator</tt> key to format the output.
def select_time(datetime = Time.now, options = {}) def select_time(datetime = Time.now, options = {})
h = select_hour(datetime, options) + select_minute(datetime, options) + (options[:include_seconds] ? select_second(datetime, options) : '') separator = options[:time_separator] || ''
select_hour(datetime, options) + separator + select_minute(datetime, options) + (options[:include_seconds] ? separator + select_second(datetime, options) : '')
end end
# Returns a select tag with options for each of the seconds 0 through 59 with the current second selected. # Returns a select tag with options for each of the seconds 0 through 59 with the current second selected.
# The <tt>second</tt> can also be substituted for a second number. # The <tt>second</tt> can also be substituted for a second number.
# Override the field name using the <tt>:field_name</tt> option, 'second' by default. # Override the field name using the <tt>:field_name</tt> option, 'second' by default.
def select_second(datetime, options = {}) def select_second(datetime, options = {})
second_options = [] val = datetime ? (datetime.kind_of?(Fixnum) ? datetime : datetime.sec) : ''
if options[:use_hidden]
0.upto(59) do |second| options[:include_seconds] ? hidden_html(options[:field_name] || 'second', val, options) : ''
second_options << ((datetime && (datetime.kind_of?(Fixnum) ? datetime : datetime.sec) == second) ? else
%(<option value="#{leading_zero_on_single_digits(second)}" selected="selected">#{leading_zero_on_single_digits(second)}</option>\n) : second_options = []
%(<option value="#{leading_zero_on_single_digits(second)}">#{leading_zero_on_single_digits(second)}</option>\n) 0.upto(59) do |second|
) second_options << ((val == second) ?
%(<option value="#{leading_zero_on_single_digits(second)}" selected="selected">#{leading_zero_on_single_digits(second)}</option>\n) :
%(<option value="#{leading_zero_on_single_digits(second)}">#{leading_zero_on_single_digits(second)}</option>\n)
)
end
select_html(options[:field_name] || 'second', second_options, options)
end end
select_html(options[:field_name] || 'second', second_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled])
end end
# Returns a select tag with options for each of the minutes 0 through 59 with the current minute selected. # Returns a select tag with options for each of the minutes 0 through 59 with the current minute selected.
@ -128,84 +188,100 @@ module ActionView
# The <tt>minute</tt> can also be substituted for a minute number. # The <tt>minute</tt> can also be substituted for a minute number.
# Override the field name using the <tt>:field_name</tt> option, 'minute' by default. # Override the field name using the <tt>:field_name</tt> option, 'minute' by default.
def select_minute(datetime, options = {}) def select_minute(datetime, options = {})
minute_options = [] val = datetime ? (datetime.kind_of?(Fixnum) ? datetime : datetime.min) : ''
if options[:use_hidden]
0.step(59, options[:minute_step] || 1) do |minute| hidden_html(options[:field_name] || 'minute', val, options)
minute_options << ((datetime && (datetime.kind_of?(Fixnum) ? datetime : datetime.min) == minute) ? else
%(<option value="#{leading_zero_on_single_digits(minute)}" selected="selected">#{leading_zero_on_single_digits(minute)}</option>\n) : minute_options = []
%(<option value="#{leading_zero_on_single_digits(minute)}">#{leading_zero_on_single_digits(minute)}</option>\n) 0.step(59, options[:minute_step] || 1) do |minute|
) minute_options << ((val == minute) ?
end %(<option value="#{leading_zero_on_single_digits(minute)}" selected="selected">#{leading_zero_on_single_digits(minute)}</option>\n) :
%(<option value="#{leading_zero_on_single_digits(minute)}">#{leading_zero_on_single_digits(minute)}</option>\n)
select_html(options[:field_name] || 'minute', minute_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled]) )
end
select_html(options[:field_name] || 'minute', minute_options, options)
end
end end
# Returns a select tag with options for each of the hours 0 through 23 with the current hour selected. # Returns a select tag with options for each of the hours 0 through 23 with the current hour selected.
# The <tt>hour</tt> can also be substituted for a hour number. # The <tt>hour</tt> can also be substituted for a hour number.
# Override the field name using the <tt>:field_name</tt> option, 'hour' by default. # Override the field name using the <tt>:field_name</tt> option, 'hour' by default.
def select_hour(datetime, options = {}) def select_hour(datetime, options = {})
hour_options = [] val = datetime ? (datetime.kind_of?(Fixnum) ? datetime : datetime.hour) : ''
if options[:use_hidden]
0.upto(23) do |hour| hidden_html(options[:field_name] || 'hour', val, options)
hour_options << ((datetime && (datetime.kind_of?(Fixnum) ? datetime : datetime.hour) == hour) ? else
%(<option value="#{leading_zero_on_single_digits(hour)}" selected="selected">#{leading_zero_on_single_digits(hour)}</option>\n) : hour_options = []
%(<option value="#{leading_zero_on_single_digits(hour)}">#{leading_zero_on_single_digits(hour)}</option>\n) 0.upto(23) do |hour|
) hour_options << ((val == hour) ?
%(<option value="#{leading_zero_on_single_digits(hour)}" selected="selected">#{leading_zero_on_single_digits(hour)}</option>\n) :
%(<option value="#{leading_zero_on_single_digits(hour)}">#{leading_zero_on_single_digits(hour)}</option>\n)
)
end
select_html(options[:field_name] || 'hour', hour_options, options)
end end
select_html(options[:field_name] || 'hour', hour_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled])
end end
# Returns a select tag with options for each of the days 1 through 31 with the current day selected. # Returns a select tag with options for each of the days 1 through 31 with the current day selected.
# The <tt>date</tt> can also be substituted for a hour number. # The <tt>date</tt> can also be substituted for a hour number.
# Override the field name using the <tt>:field_name</tt> option, 'day' by default. # Override the field name using the <tt>:field_name</tt> option, 'day' by default.
def select_day(date, options = {}) def select_day(date, options = {})
day_options = [] val = date ? (date.kind_of?(Fixnum) ? date : date.day) : ''
if options[:use_hidden]
1.upto(31) do |day| hidden_html(options[:field_name] || 'day', val, options)
day_options << ((date && (date.kind_of?(Fixnum) ? date : date.day) == day) ? else
%(<option value="#{day}" selected="selected">#{day}</option>\n) : day_options = []
%(<option value="#{day}">#{day}</option>\n) 1.upto(31) do |day|
) day_options << ((val == day) ?
%(<option value="#{day}" selected="selected">#{day}</option>\n) :
%(<option value="#{day}">#{day}</option>\n)
)
end
select_html(options[:field_name] || 'day', day_options, options)
end end
select_html(options[:field_name] || 'day', day_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled])
end end
# Returns a select tag with options for each of the months January through December with the current month selected. # 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 # 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 -- # (what's submitted to the server). It's also possible to use month numbers for the presentation instead of names --
# set the <tt>:use_month_numbers</tt> key in +options+ to true for this to happen. If you want both numbers and names, # set the <tt>:use_month_numbers</tt> key in +options+ to true for this to happen. If you want both numbers and names,
# set the <tt>:add_month_numbers</tt> key in +options+ to true. Examples: # set the <tt>:add_month_numbers</tt> key in +options+ to true. If you would prefer to show month names as abbreviations,
# set the <tt>:use_short_month</tt> key in +options+ to true. If you want to use your own month names, set the
# <tt>:use_month_names</tt> key in +options+ to an array of 12 month names.
#
# Examples:
# #
# select_month(Date.today) # Will use keys like "January", "March" # 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, :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" # select_month(Date.today, :add_month_numbers => true) # Will use keys like "1 - January", "3 - March"
# select_month(Date.today, :use_short_month => true) # Will use keys like "Jan", "Mar"
# select_month(Date.today, :use_month_names => %w(Januar Februar Marts ...)) # Will use keys like "Januar", "Marts"
# #
# Override the field name using the <tt>:field_name</tt> option, 'month' by default. # Override the field name using the <tt>:field_name</tt> option, 'month' by default.
#
# If you would prefer to show month names as abbreviations, set the
# <tt>:use_short_month</tt> key in +options+ to true.
def select_month(date, options = {}) def select_month(date, options = {})
month_options = [] val = date ? (date.kind_of?(Fixnum) ? date : date.month) : ''
month_names = options[:use_short_month] ? Date::ABBR_MONTHNAMES : Date::MONTHNAMES if options[:use_hidden]
hidden_html(options[:field_name] || 'month', val, options)
else
month_options = []
month_names = options[:use_month_names] || (options[:use_short_month] ? Date::ABBR_MONTHNAMES : Date::MONTHNAMES)
month_names.unshift(nil) if month_names.size < 13
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
1.upto(12) do |month_number| month_options << ((val == month_number) ?
month_name = if options[:use_month_numbers] %(<option value="#{month_number}" selected="selected">#{month_name}</option>\n) :
month_number %(<option value="#{month_number}">#{month_name}</option>\n)
elsif options[:add_month_numbers] )
month_number.to_s + ' - ' + month_names[month_number]
else
month_names[month_number]
end end
select_html(options[:field_name] || 'month', month_options, options)
month_options << ((date && (date.kind_of?(Fixnum) ? date : date.month) == month_number) ?
%(<option value="#{month_number}" selected="selected">#{month_name}</option>\n) :
%(<option value="#{month_number}">#{month_name}</option>\n)
)
end end
select_html(options[:field_name] || 'month', month_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled])
end 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 # 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
@ -215,37 +291,51 @@ module ActionView
# #
# select_year(Date.today, :start_year => 1992, :end_year => 2007) # ascending year values # 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 # select_year(Date.today, :start_year => 2005, :end_year => 1900) # descending year values
# select_year(2006, :start_year => 2000, :end_year => 2010)
# #
# Override the field name using the <tt>:field_name</tt> option, 'year' by default. # Override the field name using the <tt>:field_name</tt> option, 'year' by default.
def select_year(date, options = {}) def select_year(date, options = {})
year_options = [] val = date ? (date.kind_of?(Fixnum) ? date : date.year) : ''
y = date ? (date.kind_of?(Fixnum) ? (y = (date == 0) ? Date.today.year : date) : date.year) : Date.today.year if options[:use_hidden]
hidden_html(options[:field_name] || 'year', val, options)
else
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) start_year, end_year = (options[:start_year] || y-5), (options[:end_year] || y+5)
step_val = start_year < end_year ? 1 : -1 step_val = start_year < end_year ? 1 : -1
start_year.step(end_year, step_val) do |year|
start_year.step(end_year, step_val) do |year| year_options << ((val == year) ?
year_options << ((date && (date.kind_of?(Fixnum) ? date : date.year) == year) ? %(<option value="#{year}" selected="selected">#{year}</option>\n) :
%(<option value="#{year}" selected="selected">#{year}</option>\n) : %(<option value="#{year}">#{year}</option>\n)
%(<option value="#{year}">#{year}</option>\n) )
) end
select_html(options[:field_name] || 'year', year_options, options)
end end
select_html(options[:field_name] || 'year', year_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled])
end end
private private
def select_html(type, options, prefix = nil, include_blank = false, discard_type = false, disabled = false)
select_html = %(<select name="#{prefix || DEFAULT_PREFIX}) def select_html(type, html_options, options)
select_html << "[#{type}]" unless discard_type name_and_id_from_options(options, type)
select_html << %(") select_html = %(<select id="#{options[:id]}" name="#{options[:name]}")
select_html << %( disabled="disabled") if disabled select_html << %( disabled="disabled") if options[:disabled]
select_html << %(>\n) select_html << %(>\n)
select_html << %(<option value=""></option>\n) if include_blank select_html << %(<option value=""></option>\n) if options[:include_blank]
select_html << options.to_s select_html << html_options.to_s
select_html << "</select>\n" select_html << "</select>\n"
end end
def hidden_html(type, value, options)
name_and_id_from_options(options, type)
hidden_html = %(<input type="hidden" id="#{options[:id]}" name="#{options[:name]}" value="#{value}" />\n)
end
def name_and_id_from_options(options, type)
options[:name] = (options[:prefix] || DEFAULT_PREFIX) + (options[:discard_type] ? '' : "[#{type}]")
options[:id] = options[:name].gsub(/([\[\(])|(\]\[)/, '_').gsub(/[\]\)]/, '')
end
def leading_zero_on_single_digits(number) def leading_zero_on_single_digits(number)
number > 9 ? number : "0#{number}" number > 9 ? number : "0#{number}"
end end
@ -255,43 +345,71 @@ module ActionView
include DateHelper include DateHelper
def to_date_select_tag(options = {}) def to_date_select_tag(options = {})
defaults = { :discard_type => true } date_or_time_select options.merge(:discard_hour => true)
options = defaults.merge(options) end
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 = '' def to_time_select_tag(options = {})
options[:order] = [:month, :year, :day] if options[:month_before_year] # For backwards compatibility date_or_time_select options.merge(:discard_year => true, :discard_month => true)
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 end
def to_datetime_select_tag(options = {}) def to_datetime_select_tag(options = {})
defaults = { :discard_type => true } date_or_time_select options
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 << ' &mdash; ' + 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
private
def date_or_time_select(options)
defaults = { :discard_type => true }
options = defaults.merge(options)
datetime = value(object)
datetime ||= Time.now unless options[:include_blank]
position = { :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6 }
order = (options[:order] ||= [:year, :month, :day])
# Discard explicit and implicit by not being included in the :order
discard = {}
discard[:year] = true if options[:discard_year] or !order.include?(:year)
discard[:month] = true if options[:discard_month] or !order.include?(:month)
discard[:day] = true if options[:discard_day] or discard[:month] or !order.include?(:day)
discard[:hour] = true if options[:discard_hour]
discard[:minute] = true if options[:discard_minute] or discard[:hour]
discard[:second] = true unless options[:include_seconds] && !discard[:minute]
# Maintain valid dates by including hidden fields for discarded elements
[:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) }
# Ensure proper ordering of :hour, :minute and :second
[:hour, :minute, :second].each { |o| order.delete(o); order.push(o) }
date_or_time_select = ''
order.reverse.each do |param|
# Send hidden fields for discarded elements once output has started
# This ensures AR can reconstruct valid dates using ParseDate
next if discard[param] && date_or_time_select.empty?
date_or_time_select.insert(0, self.send("select_#{param}", datetime, options_with_prefix(position[param], options.merge(:use_hidden => discard[param]))))
date_or_time_select.insert(0,
case param
when :hour then (discard[:year] && discard[:day] ? "" : " &mdash; ")
when :minute then " : "
when :second then options[:include_seconds] ? " : " : ""
else ""
end)
end
date_or_time_select
end
def options_with_prefix(position, options)
prefix = "#{@object_name}"
if options[:index]
prefix << "[#{options[:index]}]"
elsif @auto_index
prefix << "[#{@auto_index}]"
end
options.merge(:prefix => "#{prefix}[#{@method_name}(#{position}i)]")
end
end end
class FormBuilder class FormBuilder
@ -299,6 +417,10 @@ module ActionView
@template.date_select(@object_name, method, options.merge(:object => @object)) @template.date_select(@object_name, method, options.merge(:object => @object))
end end
def time_select(method, options = {})
@template.time_select(@object_name, method, options.merge(:object => @object))
end
def datetime_select(method, options = {}) def datetime_select(method, options = {})
@template.datetime_select(@object_name, method, options.merge(:object => @object)) @template.datetime_select(@object_name, method, options.merge(:object => @object))
end end

View file

@ -7,7 +7,7 @@ module ActionView
begin begin
Marshal::dump(object) Marshal::dump(object)
"<pre class='debug_dump'>#{h(object.to_yaml).gsub(" ", "&nbsp; ")}</pre>" "<pre class='debug_dump'>#{h(object.to_yaml).gsub(" ", "&nbsp; ")}</pre>"
rescue Object => e rescue Exception => e # errors from Marshal or YAML
# Object couldn't be dumped, perhaps because of singleton methods -- this is the fallback # Object couldn't be dumped, perhaps because of singleton methods -- this is the fallback
"<code class='debug_dump'>#{h(object.inspect)}</code>" "<code class='debug_dump'>#{h(object.inspect)}</code>"
end end

View file

@ -142,11 +142,13 @@ module ActionView
# #
# Note: This also works for the methods in FormOptionHelper and DateHelper that are designed to work with an object as base. # 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. # Like collection_select and datetime_select.
def fields_for(object_name, *args, &proc) def fields_for(object_name, *args, &block)
raise ArgumentError, "Missing block" unless block_given? raise ArgumentError, "Missing block" unless block_given?
options = args.last.is_a?(Hash) ? args.pop : {} options = args.last.is_a?(Hash) ? args.pop : {}
object = args.first object = args.first
yield((options[:builder] || FormBuilder).new(object_name, object, self, options, proc))
builder = options[:builder] || ActionView::Base.default_form_builder
yield builder.new(object_name, object, self, options, block)
end end
# Returns an input tag of the "text" type tailored for accessing a specified attribute (identified by +method+) on an object # Returns an input tag of the "text" type tailored for accessing a specified attribute (identified by +method+) on an object
@ -238,7 +240,11 @@ module ActionView
@template_object, @local_binding = template_object, local_binding @template_object, @local_binding = template_object, local_binding
@object = object @object = object
if @object_name.sub!(/\[\]$/,"") if @object_name.sub!(/\[\]$/,"")
@auto_index = @template_object.instance_variable_get("@#{Regexp.last_match.pre_match}").id_before_type_cast if object ||= @template_object.instance_variable_get("@#{Regexp.last_match.pre_match}") and object.respond_to?(:id_before_type_cast)
@auto_index = object.id_before_type_cast
else
raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to id_before_type_cast: #{object.inspect}"
end
end end
end end
@ -250,7 +256,7 @@ module ActionView
options.delete("size") options.delete("size")
end end
options["type"] = field_type options["type"] = field_type
options["value"] ||= value_before_type_cast unless field_type == "file" options["value"] ||= value_before_type_cast(object) unless field_type == "file"
add_default_name_and_id(options) add_default_name_and_id(options)
tag("input", options) tag("input", options)
end end
@ -259,9 +265,15 @@ module ActionView
options = DEFAULT_RADIO_OPTIONS.merge(options.stringify_keys) options = DEFAULT_RADIO_OPTIONS.merge(options.stringify_keys)
options["type"] = "radio" options["type"] = "radio"
options["value"] = tag_value options["value"] = tag_value
options["checked"] = "checked" if value.to_s == tag_value.to_s if options.has_key?("checked")
cv = options.delete "checked"
checked = cv == true || cv == "checked"
else
checked = self.class.radio_button_checked?(value(object), tag_value)
end
options["checked"] = "checked" if checked
pretty_tag_value = tag_value.to_s.gsub(/\s/, "_").gsub(/\W/, "").downcase pretty_tag_value = tag_value.to_s.gsub(/\s/, "_").gsub(/\W/, "").downcase
options["id"] = @auto_index ? options["id"] ||= defined?(@auto_index) ?
"#{@object_name}_#{@auto_index}_#{@method_name}_#{pretty_tag_value}" : "#{@object_name}_#{@auto_index}_#{@method_name}_#{pretty_tag_value}" :
"#{@object_name}_#{@method_name}_#{pretty_tag_value}" "#{@object_name}_#{@method_name}_#{pretty_tag_value}"
add_default_name_and_id(options) add_default_name_and_id(options)
@ -271,14 +283,82 @@ module ActionView
def to_text_area_tag(options = {}) def to_text_area_tag(options = {})
options = DEFAULT_TEXT_AREA_OPTIONS.merge(options.stringify_keys) options = DEFAULT_TEXT_AREA_OPTIONS.merge(options.stringify_keys)
add_default_name_and_id(options) add_default_name_and_id(options)
content_tag("textarea", html_escape(options.delete('value') || value_before_type_cast), options)
if size = options.delete("size")
options["cols"], options["rows"] = size.split("x")
end
content_tag("textarea", html_escape(options.delete('value') || value_before_type_cast(object)), options)
end end
def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0") def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0")
options = options.stringify_keys options = options.stringify_keys
options["type"] = "checkbox" options["type"] = "checkbox"
options["value"] = checked_value options["value"] = checked_value
checked = case value if options.has_key?("checked")
cv = options.delete "checked"
checked = cv == true || cv == "checked"
else
checked = self.class.check_box_checked?(value(object), checked_value)
end
options["checked"] = "checked" if checked
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(object) || 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)
value = value(object)
tag_text = "<select"
tag_text << tag_options(options)
tag_text << "><option value=\"false\""
tag_text << " selected" if value == false
tag_text << ">False</option><option value=\"true\""
tag_text << " selected" if value
tag_text << ">True</option></select>"
end
def to_content_tag(tag_name, options = {})
content_tag(tag_name, value(object), options)
end
def object
@object || @template_object.instance_variable_get("@#{@object_name}")
end
def value(object)
self.class.value(object, @method_name)
end
def value_before_type_cast(object)
self.class.value_before_type_cast(object, @method_name)
end
class << self
def value(object, method_name)
object.send method_name unless object.nil?
end
def value_before_type_cast(object, method_name)
unless object.nil?
object.respond_to?(method_name + "_before_type_cast") ?
object.send(method_name + "_before_type_cast") :
object.send(method_name)
end
end
def check_box_checked?(value, checked_value)
case value
when TrueClass, FalseClass when TrueClass, FalseClass
value value
when NilClass when NilClass
@ -290,55 +370,10 @@ module ActionView
else else
value.to_i != 0 value.to_i != 0
end end
if checked || options["checked"] == "checked"
options["checked"] = "checked"
else
options.delete("checked")
end 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() def radio_button_checked?(value, checked_value)
defaults = DEFAULT_DATE_OPTIONS.dup value.to_s == checked_value.to_s
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 = "<select"
tag_text << tag_options(options)
tag_text << "><option value=\"false\""
tag_text << " selected" if value == false
tag_text << ">False</option><option value=\"true\""
tag_text << " selected" if value
tag_text << ">True</option></select>"
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
end end
@ -348,11 +383,11 @@ module ActionView
options["name"] ||= tag_name_with_index(options["index"]) options["name"] ||= tag_name_with_index(options["index"])
options["id"] ||= tag_id_with_index(options["index"]) options["id"] ||= tag_id_with_index(options["index"])
options.delete("index") options.delete("index")
elsif @auto_index elsif defined?(@auto_index)
options["name"] ||= tag_name_with_index(@auto_index) options["name"] ||= tag_name_with_index(@auto_index)
options["id"] ||= tag_id_with_index(@auto_index) options["id"] ||= tag_id_with_index(@auto_index)
else else
options["name"] ||= tag_name options["name"] ||= tag_name + (options.has_key?('multiple') ? '[]' : '')
options["id"] ||= tag_id options["id"] ||= tag_id
end end
end end
@ -379,7 +414,7 @@ module ActionView
class_inheritable_accessor :field_helpers class_inheritable_accessor :field_helpers
self.field_helpers = (FormHelper.instance_methods - ['form_for']) self.field_helpers = (FormHelper.instance_methods - ['form_for'])
attr_accessor :object_name, :object attr_accessor :object_name, :object, :options
def initialize(object_name, object, template, options, proc) def initialize(object_name, object, template, options, proc)
@object_name, @object, @template, @options, @proc = object_name, object, template, options, proc @object_name, @object, @template, @options, @proc = object_name, object, template, options, proc
@ -403,4 +438,9 @@ module ActionView
end end
end end
end end
class Base
cattr_accessor :default_form_builder
self.default_form_builder = ::ActionView::Helpers::FormBuilder
end
end end

View file

@ -26,7 +26,7 @@ module ActionView
# #
# Another common case is a select tag for an <tt>belongs_to</tt>-associated object. For example, # Another common case is a select tag for an <tt>belongs_to</tt>-associated object. For example,
# #
# select("post", "person_id", Person.find_all.collect {|p| [ p.name, p.id ] }) # select("post", "person_id", Person.find(:all).collect {|p| [ p.name, p.id ] })
# #
# could become: # could become:
# #
@ -43,7 +43,7 @@ module ActionView
# See options_for_select for the required format of the choices parameter. # See options_for_select for the required format of the choices parameter.
# #
# Example with @post.person_id => 1: # Example with @post.person_id => 1:
# select("post", "person_id", Person.find_all.collect {|p| [ p.name, p.id ] }, { :include_blank => true }) # select("post", "person_id", Person.find(:all).collect {|p| [ p.name, p.id ] }, { :include_blank => true })
# #
# could become: # could become:
# #
@ -113,7 +113,6 @@ module ActionView
options_for_select = container.inject([]) do |options, element| options_for_select = container.inject([]) do |options, element|
if !element.is_a?(String) and element.respond_to?(:first) and element.respond_to?(:last) 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) ) is_selected = ( (selected.respond_to?(:include?) && !selected.is_a?(String) ? selected.include?(element.last) : element.last == selected) )
if is_selected if is_selected
options << "<option value=\"#{html_escape(element.last.to_s)}\" selected=\"selected\">#{html_escape(element.first.to_s)}</option>" options << "<option value=\"#{html_escape(element.last.to_s)}\" selected=\"selected\">#{html_escape(element.first.to_s)}</option>"
@ -121,7 +120,6 @@ module ActionView
options << "<option value=\"#{html_escape(element.last.to_s)}\">#{html_escape(element.first.to_s)}</option>" options << "<option value=\"#{html_escape(element.last.to_s)}\">#{html_escape(element.first.to_s)}</option>"
end end
else 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) ) is_selected = ( (selected.respond_to?(:include?) && !selected.is_a?(String) ? selected.include?(element) : element == selected) )
options << ((is_selected) ? "<option value=\"#{html_escape(element.to_s)}\" selected=\"selected\">#{html_escape(element.to_s)}</option>" : "<option value=\"#{html_escape(element.to_s)}\">#{html_escape(element.to_s)}</option>") options << ((is_selected) ? "<option value=\"#{html_escape(element.to_s)}\" selected=\"selected\">#{html_escape(element.to_s)}</option>" : "<option value=\"#{html_escape(element.to_s)}\">#{html_escape(element.to_s)}</option>")
end end
@ -299,13 +297,15 @@ module ActionView
def to_select_tag(choices, options, html_options) def to_select_tag(choices, options, html_options)
html_options = html_options.stringify_keys html_options = html_options.stringify_keys
add_default_name_and_id(html_options) add_default_name_and_id(html_options)
value = value(object)
selected_value = options.has_key?(:selected) ? options[:selected] : value selected_value = options.has_key?(:selected) ? options[:selected] : value
content_tag("select", add_options(options_for_select(choices, selected_value), options, value), html_options) content_tag("select", add_options(options_for_select(choices, selected_value), options, selected_value), html_options)
end end
def to_collection_select_tag(collection, value_method, text_method, options, html_options) def to_collection_select_tag(collection, value_method, text_method, options, html_options)
html_options = html_options.stringify_keys html_options = html_options.stringify_keys
add_default_name_and_id(html_options) add_default_name_and_id(html_options)
value = value(object)
content_tag( content_tag(
"select", add_options(options_from_collection_for_select(collection, value_method, text_method, value), options, value), html_options "select", add_options(options_from_collection_for_select(collection, value_method, text_method, value), options, value), html_options
) )
@ -314,12 +314,14 @@ module ActionView
def to_country_select_tag(priority_countries, options, html_options) def to_country_select_tag(priority_countries, options, html_options)
html_options = html_options.stringify_keys html_options = html_options.stringify_keys
add_default_name_and_id(html_options) add_default_name_and_id(html_options)
value = value(object)
content_tag("select", add_options(country_options_for_select(value, priority_countries), options, value), html_options) content_tag("select", add_options(country_options_for_select(value, priority_countries), options, value), html_options)
end end
def to_time_zone_select_tag(priority_zones, options, html_options) def to_time_zone_select_tag(priority_zones, options, html_options)
html_options = html_options.stringify_keys html_options = html_options.stringify_keys
add_default_name_and_id(html_options) add_default_name_and_id(html_options)
value = value(object)
content_tag("select", content_tag("select",
add_options( add_options(
time_zone_options_for_select(value, priority_zones, options[:model] || TimeZone), time_zone_options_for_select(value, priority_zones, options[:model] || TimeZone),

View file

@ -12,14 +12,49 @@ module ActionView
# Starts a form tag that points the action to an url configured with <tt>url_for_options</tt> just like # Starts a form tag that points the action to an url configured with <tt>url_for_options</tt> just like
# ActionController::Base#url_for. The method for the form defaults to POST. # ActionController::Base#url_for. The method for the form defaults to POST.
# #
# Examples:
# * <tt>form_tag('/posts') => <form action="/posts" method="post"></tt>
# * <tt>form_tag('/posts/1', :method => :put) => <form action="/posts/1" method="put"></tt>
# * <tt>form_tag('/upload', :multipart => true) => <form action="/upload" method="post" enctype="multipart/form-data"></tt>
#
# ERb example:
# <% form_tag '/posts' do -%>
# <div><%= submit_tag 'Save' %></div>
# <% end -%>
#
# Will output:
# <form action="/posts" method="post"><div><input type="submit" name="submit" value="Save" /></div></form>
#
# Options: # Options:
# * <tt>:multipart</tt> - If set to true, the enctype is set to "multipart/form-data". # * <tt>:multipart</tt> - If set to true, the enctype is set to "multipart/form-data".
# * <tt>:method</tt> - The method to use when submitting the form, usually either "get" or "post". # * <tt>:method</tt> - The method to use when submitting the form, usually either "get" or "post".
def form_tag(url_for_options = {}, options = {}, *parameters_for_url, &proc) # If "put", "delete", or another verb is used, a hidden input with name _method
html_options = { "method" => "post" }.merge(options.stringify_keys) # is added to simulate the verb over post.
def form_tag(url_for_options = {}, options = {}, *parameters_for_url, &block)
html_options = options.stringify_keys
html_options["enctype"] = "multipart/form-data" if html_options.delete("multipart") html_options["enctype"] = "multipart/form-data" if html_options.delete("multipart")
html_options["action"] = url_for(url_for_options, *parameters_for_url) html_options["action"] = url_for(url_for_options, *parameters_for_url)
tag :form, html_options, true
method_tag = ""
case method = html_options.delete("method").to_s
when /^get$/i # must be case-insentive, but can't use downcase as might be nil
html_options["method"] = "get"
when /^post$/i, "", nil
html_options["method"] = "post"
else
html_options["method"] = "post"
method_tag = content_tag(:div, tag(:input, :type => "hidden", :name => "_method", :value => method), :style => 'margin:0;padding:0')
end
if block_given?
content = capture(&block)
concat(tag(:form, html_options, true) + method_tag, block.binding)
concat(content, block.binding)
concat("</form>", block.binding)
else
tag(:form, html_options, true) + method_tag
end
end end
alias_method :start_form_tag, :form_tag alias_method :start_form_tag, :form_tag
@ -29,6 +64,8 @@ module ActionView
"</form>" "</form>"
end end
deprecate :end_form_tag, :start_form_tag => :form_tag
# Creates a dropdown selection box, or if the <tt>:multiple</tt> option is set to true, a multiple # Creates a dropdown selection box, or if the <tt>:multiple</tt> option is set to true, a multiple
# choice selection box. # choice selection box.
# #
@ -110,7 +147,8 @@ module ActionView
# Creates a radio button. # Creates a radio button.
def radio_button_tag(name, value, checked = false, options = {}) def radio_button_tag(name, value, checked = false, options = {})
html_options = { "type" => "radio", "name" => name, "id" => name, "value" => value }.update(options.stringify_keys) pretty_tag_value = value.to_s.gsub(/\s/, "_").gsub(/(?!-)\W/, "").downcase
html_options = { "type" => "radio", "name" => name, "id" => "#{name}_#{pretty_tag_value}", "value" => value }.update(options.stringify_keys)
html_options["checked"] = "checked" if checked html_options["checked"] = "checked" if checked
tag :input, html_options tag :input, html_options
end end

View file

@ -7,6 +7,8 @@ module ActionView
# editing relies on ActionController::Base.in_place_edit_for and the autocompletion relies on # editing relies on ActionController::Base.in_place_edit_for and the autocompletion relies on
# ActionController::Base.auto_complete_for. # ActionController::Base.auto_complete_for.
module JavaScriptMacrosHelper module JavaScriptMacrosHelper
# DEPRECATION WARNING: This method will become a separate plugin when Rails 2.0 ships.
#
# Makes an HTML element specified by the DOM ID +field_id+ become an in-place # Makes an HTML element specified by the DOM ID +field_id+ become an in-place
# editor of a property. # editor of a property.
# #
@ -27,20 +29,21 @@ module ActionView
# <tt>:url</tt>:: Specifies the url where the updated value should # <tt>:url</tt>:: Specifies the url where the updated value should
# be sent after the user presses "ok". # be sent after the user presses "ok".
# #
#
# Addtional +options+ are: # Addtional +options+ are:
# <tt>:rows</tt>:: Number of rows (more than 1 will use a TEXTAREA) # <tt>:rows</tt>:: Number of rows (more than 1 will use a TEXTAREA)
# <tt>:cols</tt>:: Number of characters the text input should span (works for both INPUT and TEXTAREA) # <tt>:cols</tt>:: Number of characters the text input should span (works for both INPUT and TEXTAREA)
# <tt>:size</tt>:: Synonym for :cols when using a single line text input. # <tt>:size</tt>:: Synonym for :cols when using a single line text input.
# <tt>:cancel_text</tt>:: The text on the cancel link. (default: "cancel") # <tt>:cancel_text</tt>:: The text on the cancel link. (default: "cancel")
# <tt>:save_text</tt>:: The text on the save link. (default: "ok") # <tt>:save_text</tt>:: The text on the save link. (default: "ok")
# <tt>:loading_text</tt>:: The text to display when submitting to the server (default: "Saving...") # <tt>:loading_text</tt>:: The text to display while the data is being loaded from the server (default: "Loading...")
# <tt>:saving_text</tt>:: The text to display when submitting to the server (default: "Saving...")
# <tt>:external_control</tt>:: The id of an external control used to enter edit mode. # <tt>:external_control</tt>:: The id of an external control used to enter edit mode.
# <tt>:load_text_url</tt>:: URL where initial value of editor (content) is retrieved. # <tt>:load_text_url</tt>:: URL where initial value of editor (content) is retrieved.
# <tt>:options</tt>:: Pass through options to the AJAX call (see prototype's Ajax.Updater) # <tt>:options</tt>:: Pass through options to the AJAX call (see prototype's Ajax.Updater)
# <tt>:with</tt>:: JavaScript snippet that should return what is to be sent # <tt>:with</tt>:: JavaScript snippet that should return what is to be sent
# in the AJAX call, +form+ is an implicit parameter # in the AJAX call, +form+ is an implicit parameter
# <tt>:script</tt>:: Instructs the in-place editor to evaluate the remote JavaScript response (default: false) # <tt>:script</tt>:: Instructs the in-place editor to evaluate the remote JavaScript response (default: false)
# <tt>:click_to_edit_text</tt>::The text shown during mouseover the editable text (default: "Click to edit")
def in_place_editor(field_id, options = {}) def in_place_editor(field_id, options = {})
function = "new Ajax.InPlaceEditor(" function = "new Ajax.InPlaceEditor("
function << "'#{field_id}', " function << "'#{field_id}', "
@ -50,6 +53,7 @@ module ActionView
js_options['cancelText'] = %('#{options[:cancel_text]}') if options[:cancel_text] js_options['cancelText'] = %('#{options[:cancel_text]}') if options[:cancel_text]
js_options['okText'] = %('#{options[:save_text]}') if options[:save_text] js_options['okText'] = %('#{options[:save_text]}') if options[:save_text]
js_options['loadingText'] = %('#{options[:loading_text]}') if options[:loading_text] js_options['loadingText'] = %('#{options[:loading_text]}') if options[:loading_text]
js_options['savingText'] = %('#{options[:saving_text]}') if options[:saving_text]
js_options['rows'] = options[:rows] if options[:rows] js_options['rows'] = options[:rows] if options[:rows]
js_options['cols'] = options[:cols] if options[:cols] js_options['cols'] = options[:cols] if options[:cols]
js_options['size'] = options[:size] if options[:size] js_options['size'] = options[:size] if options[:size]
@ -58,6 +62,7 @@ module ActionView
js_options['ajaxOptions'] = options[:options] if options[:options] js_options['ajaxOptions'] = options[:options] if options[:options]
js_options['evalScripts'] = options[:script] if options[:script] js_options['evalScripts'] = options[:script] if options[:script]
js_options['callback'] = "function(form) { return #{options[:with]} }" if options[:with] js_options['callback'] = "function(form) { return #{options[:with]} }" if options[:with]
js_options['clickToEditText'] = %('#{options[:click_to_edit_text]}') if options[:click_to_edit_text]
function << (', ' + options_for_javascript(js_options)) unless js_options.empty? function << (', ' + options_for_javascript(js_options)) unless js_options.empty?
function << ')' function << ')'
@ -65,6 +70,8 @@ module ActionView
javascript_tag(function) javascript_tag(function)
end end
# DEPRECATION WARNING: This method will become a separate plugin when Rails 2.0 ships.
#
# Renders the value of the specified object and method with in-place editing capabilities. # Renders the value of the specified object and method with in-place editing capabilities.
# #
# See the RDoc on ActionController::InPlaceEditing to learn more about this. # See the RDoc on ActionController::InPlaceEditing to learn more about this.
@ -76,14 +83,16 @@ module ActionView
in_place_editor(tag_options[:id], in_place_editor_options) in_place_editor(tag_options[:id], in_place_editor_options)
end end
# DEPRECATION WARNING: This method will become a separate plugin when Rails 2.0 ships.
#
# Adds AJAX autocomplete functionality to the text input field with the # Adds AJAX autocomplete functionality to the text input field with the
# DOM ID specified by +field_id+. # DOM ID specified by +field_id+.
# #
# This function expects that the called action returns a HTML <ul> list, # This function expects that the called action returns an HTML <ul> list,
# or nothing if no entries should be displayed for autocompletion. # or nothing if no entries should be displayed for autocompletion.
# #
# You'll probably want to turn the browser's built-in autocompletion off, # You'll probably want to turn the browser's built-in autocompletion off,
# so be sure to include a autocomplete="off" attribute with your text # so be sure to include an <tt>autocomplete="off"</tt> attribute with your text
# input field. # input field.
# #
# The autocompleter object is assigned to a Javascript variable named <tt>field_id</tt>_auto_completer. # The autocompleter object is assigned to a Javascript variable named <tt>field_id</tt>_auto_completer.
@ -91,45 +100,45 @@ module ActionView
# other means than user input (for that specific case, call the <tt>activate</tt> method on that object). # other means than user input (for that specific case, call the <tt>activate</tt> method on that object).
# #
# Required +options+ are: # Required +options+ are:
# <tt>:url</tt>:: URL to call for autocompletion results # <tt>:url</tt>:: URL to call for autocompletion results
# in url_for format. # in url_for format.
# #
# Addtional +options+ are: # Addtional +options+ are:
# <tt>:update</tt>:: Specifies the DOM ID of the element whose # <tt>:update</tt>:: Specifies the DOM ID of the element whose
# innerHTML should be updated with the autocomplete # innerHTML should be updated with the autocomplete
# entries returned by the AJAX request. # entries returned by the AJAX request.
# Defaults to field_id + '_auto_complete' # Defaults to <tt>field_id</tt> + '_auto_complete'
# <tt>:with</tt>:: A JavaScript expression specifying the # <tt>:with</tt>:: A JavaScript expression specifying the
# parameters for the XMLHttpRequest. This defaults # parameters for the XMLHttpRequest. This defaults
# to 'fieldname=value'. # to 'fieldname=value'.
# <tt>:frequency</tt>:: Determines the time to wait after the last keystroke # <tt>:frequency</tt>:: Determines the time to wait after the last keystroke
# for the AJAX request to be initiated. # for the AJAX request to be initiated.
# <tt>:indicator</tt>:: Specifies the DOM ID of an element which will be # <tt>:indicator</tt>:: Specifies the DOM ID of an element which will be
# displayed while autocomplete is running. # displayed while autocomplete is running.
# <tt>:tokens</tt>:: A string or an array of strings containing # <tt>:tokens</tt>:: A string or an array of strings containing
# separator tokens for tokenized incremental # separator tokens for tokenized incremental
# autocompletion. Example: <tt>:tokens => ','</tt> would # autocompletion. Example: <tt>:tokens => ','</tt> would
# allow multiple autocompletion entries, separated # allow multiple autocompletion entries, separated
# by commas. # by commas.
# <tt>:min_chars</tt>:: The minimum number of characters that should be # <tt>:min_chars</tt>:: The minimum number of characters that should be
# in the input field before an Ajax call is made # in the input field before an Ajax call is made
# to the server. # to the server.
# <tt>:on_hide</tt>:: A Javascript expression that is called when the # <tt>:on_hide</tt>:: A Javascript expression that is called when the
# autocompletion div is hidden. The expression # autocompletion div is hidden. The expression
# should take two variables: element and update. # should take two variables: element and update.
# Element is a DOM element for the field, update # Element is a DOM element for the field, update
# is a DOM element for the div from which the # is a DOM element for the div from which the
# innerHTML is replaced. # innerHTML is replaced.
# <tt>:on_show</tt>:: Like on_hide, only now the expression is called # <tt>:on_show</tt>:: Like on_hide, only now the expression is called
# then the div is shown. # then the div is shown.
# <tt>:after_update_element</tt>:: A Javascript expression that is called when the # <tt>:after_update_element</tt>:: A Javascript expression that is called when the
# user has selected one of the proposed values. # user has selected one of the proposed values.
# The expression should take two variables: element and value. # The expression should take two variables: element and value.
# Element is a DOM element for the field, value # Element is a DOM element for the field, value
# is the value selected by the user. # is the value selected by the user.
# <tt>:select</tt>:: Pick the class of the element from which the value for # <tt>:select</tt>:: Pick the class of the element from which the value for
# insertion should be extracted. If this is not specified, # insertion should be extracted. If this is not specified,
# the entire element is used. # the entire element is used.
def auto_complete_field(field_id, options = {}) def auto_complete_field(field_id, options = {})
function = "var #{field_id}_auto_completer = new Ajax.Autocompleter(" function = "var #{field_id}_auto_completer = new Ajax.Autocompleter("
function << "'#{field_id}', " function << "'#{field_id}', "
@ -141,6 +150,7 @@ module ActionView
js_options[:callback] = "function(element, value) { return #{options[:with]} }" if options[:with] js_options[:callback] = "function(element, value) { return #{options[:with]} }" if options[:with]
js_options[:indicator] = "'#{options[:indicator]}'" if options[:indicator] js_options[:indicator] = "'#{options[:indicator]}'" if options[:indicator]
js_options[:select] = "'#{options[:select]}'" if options[:select] js_options[:select] = "'#{options[:select]}'" if options[:select]
js_options[:paramName] = "'#{options[:param_name]}'" if options[:param_name]
js_options[:frequency] = "#{options[:frequency]}" if options[:frequency] js_options[:frequency] = "#{options[:frequency]}" if options[:frequency]
{ :after_update_element => :afterUpdateElement, { :after_update_element => :afterUpdateElement,
@ -153,6 +163,8 @@ module ActionView
javascript_tag(function) javascript_tag(function)
end end
# DEPRECATION WARNING: This method will become a separate plugin when Rails 2.0 ships.
#
# Use this method in your view to generate a return for the AJAX autocomplete requests. # Use this method in your view to generate a return for the AJAX autocomplete requests.
# #
# Example action: # Example action:
@ -161,7 +173,7 @@ module ActionView
# @items = Item.find(:all, # @items = Item.find(:all,
# :conditions => [ 'LOWER(description) LIKE ?', # :conditions => [ 'LOWER(description) LIKE ?',
# '%' + request.raw_post.downcase + '%' ]) # '%' + request.raw_post.downcase + '%' ])
# render :inline => '<%= auto_complete_result(@items, 'description') %>' # render :inline => "<%= auto_complete_result(@items, 'description') %>"
# end # end
# #
# The auto_complete_result can of course also be called from a view belonging to the # The auto_complete_result can of course also be called from a view belonging to the
@ -172,12 +184,14 @@ module ActionView
content_tag("ul", items.uniq) content_tag("ul", items.uniq)
end end
# DEPRECATION WARNING: This method will become a separate plugin when Rails 2.0 ships.
#
# Wrapper for text_field with added AJAX autocompletion functionality. # Wrapper for text_field with added AJAX autocompletion functionality.
# #
# In your controller, you'll need to define an action called # In your controller, you'll need to define an action called
# auto_complete_for_object_method to respond the AJAX calls, # auto_complete_for to respond the AJAX calls,
# #
# See the RDoc on ActionController::AutoComplete to learn more about this. # See the RDoc on ActionController::Macros::AutoComplete to learn more about this.
def text_field_with_auto_complete(object, method, tag_options = {}, completion_options = {}) def text_field_with_auto_complete(object, method, tag_options = {}, completion_options = {})
(completion_options[:skip_style] ? "" : auto_complete_stylesheet) + (completion_options[:skip_style] ? "" : auto_complete_stylesheet) +
text_field(object, method, tag_options) + text_field(object, method, tag_options) +
@ -187,7 +201,7 @@ module ActionView
private private
def auto_complete_stylesheet def auto_complete_stylesheet
content_tag("style", <<-EOT content_tag('style', <<-EOT, :type => 'text/css')
div.auto_complete { div.auto_complete {
width: 350px; width: 350px;
background: #fff; background: #fff;
@ -212,7 +226,6 @@ module ActionView
padding:0; padding:0;
} }
EOT EOT
)
end end
end end

View file

@ -1,4 +1,5 @@
require File.dirname(__FILE__) + '/tag_helper' require File.dirname(__FILE__) + '/tag_helper'
require File.dirname(__FILE__) + '/prototype_helper'
module ActionView module ActionView
module Helpers module Helpers
@ -41,14 +42,49 @@ module ActionView
JAVASCRIPT_PATH = File.join(File.dirname(__FILE__), 'javascripts') JAVASCRIPT_PATH = File.join(File.dirname(__FILE__), 'javascripts')
end end
# Returns a link that'll trigger a JavaScript +function+ using the include PrototypeHelper
# Returns a link that will trigger a JavaScript +function+ using the
# onclick handler and return false after the fact. # onclick handler and return false after the fact.
# #
# The +function+ argument can be omitted in favor of an +update_page+
# block, which evaluates to a string when the template is rendered
# (instead of making an Ajax request first).
#
# Examples: # Examples:
# link_to_function "Greeting", "alert('Hello world!')" # link_to_function "Greeting", "alert('Hello world!')"
# link_to_function(image_tag("delete"), "if confirm('Really?'){ do_delete(); }") # Produces:
def link_to_function(name, function, html_options = {}) # <a onclick="alert('Hello world!'); return false;" href="#">Greeting</a>
#
# link_to_function(image_tag("delete"), "if (confirm('Really?')) do_delete()")
# Produces:
# <a onclick="if (confirm('Really?')) do_delete(); return false;" href="#">
# <img src="/images/delete.png?" alt="Delete"/>
# </a>
#
# link_to_function("Show me more", nil, :id => "more_link") do |page|
# page[:details].visual_effect :toggle_blind
# page[:more_link].replace_html "Show me less"
# end
# Produces:
# <a href="#" id="more_link" onclick="try {
# $(&quot;details&quot;).visualEffect(&quot;toggle_blind&quot;);
# $(&quot;more_link&quot;).update(&quot;Show me less&quot;);
# }
# catch (e) {
# alert('RJS error:\n\n' + e.toString());
# alert('$(\&quot;details\&quot;).visualEffect(\&quot;toggle_blind\&quot;);
# \n$(\&quot;more_link\&quot;).update(\&quot;Show me less\&quot;);');
# throw e
# };
# return false;">Show me more</a>
#
def link_to_function(name, *args, &block)
html_options = args.last.is_a?(Hash) ? args.pop : {}
function = args[0] || ''
html_options.symbolize_keys! html_options.symbolize_keys!
function = update_page(&block) if block_given?
content_tag( content_tag(
"a", name, "a", name,
html_options.merge({ html_options.merge({
@ -58,14 +94,28 @@ module ActionView
) )
end end
# Returns a link that'll trigger a JavaScript +function+ using the # Returns a button that'll trigger a JavaScript +function+ using the
# onclick handler. # onclick handler.
# #
# The +function+ argument can be omitted in favor of an +update_page+
# block, which evaluates to a string when the template is rendered
# (instead of making an Ajax request first).
#
# Examples: # Examples:
# button_to_function "Greeting", "alert('Hello world!')" # button_to_function "Greeting", "alert('Hello world!')"
# button_to_function "Delete", "if confirm('Really?'){ do_delete(); }") # button_to_function "Delete", "if (confirm('Really?')) do_delete()"
def button_to_function(name, function, html_options = {}) # button_to_function "Details" do |page|
# page[:details].visual_effect :toggle_slide
# end
# button_to_function "Details", :class => "details_button" do |page|
# page[:details].visual_effect :toggle_slide
# end
def button_to_function(name, *args, &block)
html_options = args.last.is_a?(Hash) ? args.pop : {}
function = args[0] || ''
html_options.symbolize_keys! html_options.symbolize_keys!
function = update_page(&block) if block_given?
tag(:input, html_options.merge({ tag(:input, html_options.merge({
:type => "button", :value => name, :type => "button", :value => name,
:onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};" :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
@ -99,13 +149,24 @@ module ActionView
# Escape carrier returns and single and double quotes for JavaScript segments. # Escape carrier returns and single and double quotes for JavaScript segments.
def escape_javascript(javascript) def escape_javascript(javascript)
(javascript || '').gsub(/\r\n|\n|\r/, "\\n").gsub(/["']/) { |m| "\\#{m}" } (javascript || '').gsub('\\','\0\0').gsub(/\r\n|\n|\r/, "\\n").gsub(/["']/) { |m| "\\#{m}" }
end end
# Returns a JavaScript tag with the +content+ inside. Example: # Returns a JavaScript tag with the +content+ inside. Example:
# javascript_tag "alert('All is good')" # => <script type="text/javascript">alert('All is good')</script> # javascript_tag "alert('All is good')"
def javascript_tag(content) #
content_tag("script", javascript_cdata_section(content), :type => "text/javascript") # Returns:
#
# <script type="text/javascript">
# //<![CDATA[
# alert('All is good')
# //]]>
# </script>
#
# +html_options+ may be a hash of attributes for the <script> tag. Example:
# javascript_tag "alert('All is good')", :defer => 'true' # => <script defer="true" type="text/javascript">alert('All is good')</script>
def javascript_tag(content, html_options = {})
content_tag("script", javascript_cdata_section(content), html_options.merge(:type => "text/javascript"))
end end
def javascript_cdata_section(content) #:nodoc: def javascript_cdata_section(content) #:nodoc:

View file

@ -1,12 +1,13 @@
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) // Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan) // (c) 2005, 2006 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
// (c) 2005 Jon Tirsen (http://www.tirsen.com) // (c) 2005, 2006 Jon Tirsen (http://www.tirsen.com)
// Contributors: // Contributors:
// Richard Livsey // Richard Livsey
// Rahul Bhargava // Rahul Bhargava
// Rob Wills // Rob Wills
// //
// See scriptaculous.js for full license. // script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/
// Autocompleter.Base handles all the autocompletion functionality // Autocompleter.Base handles all the autocompletion functionality
// that's independent of the data source for autocompletion. This // that's independent of the data source for autocompletion. This
@ -33,6 +34,9 @@
// useful when one of the tokens is \n (a newline), as it // useful when one of the tokens is \n (a newline), as it
// allows smart autocompletion after linebreaks. // allows smart autocompletion after linebreaks.
if(typeof Effect == 'undefined')
throw("controls.js requires including script.aculo.us' effects.js library");
var Autocompleter = {} var Autocompleter = {}
Autocompleter.Base = function() {}; Autocompleter.Base = function() {};
Autocompleter.Base.prototype = { Autocompleter.Base.prototype = {
@ -45,7 +49,7 @@ Autocompleter.Base.prototype = {
this.index = 0; this.index = 0;
this.entryCount = 0; this.entryCount = 0;
if (this.setOptions) if(this.setOptions)
this.setOptions(options); this.setOptions(options);
else else
this.options = options || {}; this.options = options || {};
@ -55,17 +59,20 @@ Autocompleter.Base.prototype = {
this.options.frequency = this.options.frequency || 0.4; this.options.frequency = this.options.frequency || 0.4;
this.options.minChars = this.options.minChars || 1; this.options.minChars = this.options.minChars || 1;
this.options.onShow = this.options.onShow || this.options.onShow = this.options.onShow ||
function(element, update){ function(element, update){
if(!update.style.position || update.style.position=='absolute') { if(!update.style.position || update.style.position=='absolute') {
update.style.position = 'absolute'; update.style.position = 'absolute';
Position.clone(element, update, {setHeight: false, offsetTop: element.offsetHeight}); Position.clone(element, update, {
} setHeight: false,
Effect.Appear(update,{duration:0.15}); offsetTop: element.offsetHeight
}; });
}
Effect.Appear(update,{duration:0.15});
};
this.options.onHide = this.options.onHide || this.options.onHide = this.options.onHide ||
function(element, update){ new Effect.Fade(update,{duration:0.15}) }; function(element, update){ new Effect.Fade(update,{duration:0.15}) };
if (typeof(this.options.tokens) == 'string') if(typeof(this.options.tokens) == 'string')
this.options.tokens = new Array(this.options.tokens); this.options.tokens = new Array(this.options.tokens);
this.observer = null; this.observer = null;
@ -94,7 +101,7 @@ Autocompleter.Base.prototype = {
}, },
fixIEOverlapping: function() { fixIEOverlapping: function() {
Position.clone(this.update, this.iefix); Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)});
this.iefix.style.zIndex = 1; this.iefix.style.zIndex = 1;
this.update.style.zIndex = 2; this.update.style.zIndex = 2;
Element.show(this.iefix); Element.show(this.iefix);
@ -202,11 +209,13 @@ Autocompleter.Base.prototype = {
markPrevious: function() { markPrevious: function() {
if(this.index > 0) this.index-- if(this.index > 0) this.index--
else this.index = this.entryCount-1; else this.index = this.entryCount-1;
this.getEntry(this.index).scrollIntoView(true);
}, },
markNext: function() { markNext: function() {
if(this.index < this.entryCount-1) this.index++ if(this.index < this.entryCount-1) this.index++
else this.index = 0; else this.index = 0;
this.getEntry(this.index).scrollIntoView(false);
}, },
getEntry: function(index) { getEntry: function(index) {
@ -254,11 +263,11 @@ Autocompleter.Base.prototype = {
if(!this.changed && this.hasFocus) { if(!this.changed && this.hasFocus) {
this.update.innerHTML = choices; this.update.innerHTML = choices;
Element.cleanWhitespace(this.update); Element.cleanWhitespace(this.update);
Element.cleanWhitespace(this.update.firstChild); Element.cleanWhitespace(this.update.down());
if(this.update.firstChild && this.update.firstChild.childNodes) { if(this.update.firstChild && this.update.down().childNodes) {
this.entryCount = this.entryCount =
this.update.firstChild.childNodes.length; this.update.down().childNodes.length;
for (var i = 0; i < this.entryCount; i++) { for (var i = 0; i < this.entryCount; i++) {
var entry = this.getEntry(i); var entry = this.getEntry(i);
entry.autocompleteIndex = i; entry.autocompleteIndex = i;
@ -269,9 +278,14 @@ Autocompleter.Base.prototype = {
} }
this.stopIndicator(); this.stopIndicator();
this.index = 0; this.index = 0;
this.render();
if(this.entryCount==1 && this.options.autoSelect) {
this.selectEntry();
this.hide();
} else {
this.render();
}
} }
}, },
@ -459,6 +473,7 @@ Ajax.InPlaceEditor.prototype = {
this.element = $(element); this.element = $(element);
this.options = Object.extend({ this.options = Object.extend({
paramName: "value",
okButton: true, okButton: true,
okText: "ok", okText: "ok",
cancelLink: true, cancelLink: true,
@ -531,7 +546,7 @@ Ajax.InPlaceEditor.prototype = {
Element.hide(this.element); Element.hide(this.element);
this.createForm(); this.createForm();
this.element.parentNode.insertBefore(this.form, this.element); this.element.parentNode.insertBefore(this.form, this.element);
Field.scrollFreeActivate(this.editField); if (!this.options.loadTextURL) Field.scrollFreeActivate(this.editField);
// stop the event to avoid a page refresh in Safari // stop the event to avoid a page refresh in Safari
if (evt) { if (evt) {
Event.stop(evt); Event.stop(evt);
@ -590,7 +605,7 @@ Ajax.InPlaceEditor.prototype = {
var textField = document.createElement("input"); var textField = document.createElement("input");
textField.obj = this; textField.obj = this;
textField.type = "text"; textField.type = "text";
textField.name = "value"; textField.name = this.options.paramName;
textField.value = text; textField.value = text;
textField.style.backgroundColor = this.options.highlightcolor; textField.style.backgroundColor = this.options.highlightcolor;
textField.className = 'editor_field'; textField.className = 'editor_field';
@ -603,7 +618,7 @@ Ajax.InPlaceEditor.prototype = {
this.options.textarea = true; this.options.textarea = true;
var textArea = document.createElement("textarea"); var textArea = document.createElement("textarea");
textArea.obj = this; textArea.obj = this;
textArea.name = "value"; textArea.name = this.options.paramName;
textArea.value = this.convertHTMLLineBreaks(text); textArea.value = this.convertHTMLLineBreaks(text);
textArea.rows = this.options.rows; textArea.rows = this.options.rows;
textArea.cols = this.options.cols || 40; textArea.cols = this.options.cols || 40;
@ -636,6 +651,7 @@ Ajax.InPlaceEditor.prototype = {
Element.removeClassName(this.form, this.options.loadingClassName); Element.removeClassName(this.form, this.options.loadingClassName);
this.editField.disabled = false; this.editField.disabled = false;
this.editField.value = transport.responseText.stripTags(); this.editField.value = transport.responseText.stripTags();
Field.scrollFreeActivate(this.editField);
}, },
onclickCancel: function() { onclickCancel: function() {
this.onComplete(); this.onComplete();
@ -772,6 +788,8 @@ Object.extend(Ajax.InPlaceCollectionEditor.prototype, {
collection.each(function(e,i) { collection.each(function(e,i) {
optionTag = document.createElement("option"); optionTag = document.createElement("option");
optionTag.value = (e instanceof Array) ? e[0] : e; optionTag.value = (e instanceof Array) ? e[0] : e;
if((typeof this.options.value == 'undefined') &&
((e instanceof Array) ? this.element.innerHTML == e[1] : e == optionTag.value)) optionTag.selected = true;
if(this.options.value==optionTag.value) optionTag.selected = true; if(this.options.value==optionTag.value) optionTag.selected = true;
optionTag.appendChild(document.createTextNode((e instanceof Array) ? e[1] : e)); optionTag.appendChild(document.createTextNode((e instanceof Array) ? e[1] : e));
selectTag.appendChild(optionTag); selectTag.appendChild(optionTag);

View file

@ -1,9 +1,11 @@
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) // Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// (c) 2005 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz) // (c) 2005, 2006 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
// //
// See scriptaculous.js for full license. // script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/
/*--------------------------------------------------------------------------*/ if(typeof Effect == 'undefined')
throw("dragdrop.js requires including script.aculo.us' effects.js library");
var Droppables = { var Droppables = {
drops: [], drops: [],
@ -145,8 +147,16 @@ var Draggables = {
}, },
activate: function(draggable) { activate: function(draggable) {
window.focus(); // allows keypress events if window isn't currently focused, fails for Safari if(draggable.options.delay) {
this.activeDraggable = draggable; this._timeout = setTimeout(function() {
Draggables._timeout = null;
window.focus();
Draggables.activeDraggable = draggable;
}.bind(this), draggable.options.delay);
} else {
window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
this.activeDraggable = draggable;
}
}, },
deactivate: function() { deactivate: function() {
@ -160,10 +170,15 @@ var Draggables = {
// the same coordinates, prevent needless redrawing (moz bug?) // the same coordinates, prevent needless redrawing (moz bug?)
if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return; if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
this._lastPointer = pointer; this._lastPointer = pointer;
this.activeDraggable.updateDrag(event, pointer); this.activeDraggable.updateDrag(event, pointer);
}, },
endDrag: function(event) { endDrag: function(event) {
if(this._timeout) {
clearTimeout(this._timeout);
this._timeout = null;
}
if(!this.activeDraggable) return; if(!this.activeDraggable) return;
this._lastPointer = null; this._lastPointer = null;
this.activeDraggable.endDrag(event); this.activeDraggable.endDrag(event);
@ -190,6 +205,7 @@ var Draggables = {
this.observers.each( function(o) { this.observers.each( function(o) {
if(o[eventName]) o[eventName](eventName, draggable, event); if(o[eventName]) o[eventName](eventName, draggable, event);
}); });
if(draggable.options[eventName]) draggable.options[eventName](draggable, event);
}, },
_cacheObserverCallbacks: function() { _cacheObserverCallbacks: function() {
@ -204,39 +220,59 @@ var Draggables = {
/*--------------------------------------------------------------------------*/ /*--------------------------------------------------------------------------*/
var Draggable = Class.create(); var Draggable = Class.create();
Draggable._dragging = {};
Draggable.prototype = { Draggable.prototype = {
initialize: function(element) { initialize: function(element) {
var options = Object.extend({ var defaults = {
handle: false, handle: false,
starteffect: function(element) {
new Effect.Opacity(element, {duration:0.2, from:1.0, to:0.7});
},
reverteffect: function(element, top_offset, left_offset) { reverteffect: function(element, top_offset, left_offset) {
var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02; var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
element._revert = new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur}); new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur,
queue: {scope:'_draggable', position:'end'}
});
}, },
endeffect: function(element) { endeffect: function(element) {
new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0}); var toOpacity = typeof element._opacity == 'number' ? element._opacity : 1.0;
new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity,
queue: {scope:'_draggable', position:'end'},
afterFinish: function(){
Draggable._dragging[element] = false
}
});
}, },
zindex: 1000, zindex: 1000,
revert: false, revert: false,
scroll: false, scroll: false,
scrollSensitivity: 20, scrollSensitivity: 20,
scrollSpeed: 15, scrollSpeed: 15,
snap: false // false, or xy or [x,y] or function(x,y){ return [x,y] } snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] }
}, arguments[1] || {}); delay: 0
};
if(!arguments[1] || typeof arguments[1].endeffect == 'undefined')
Object.extend(defaults, {
starteffect: function(element) {
element._opacity = Element.getOpacity(element);
Draggable._dragging[element] = true;
new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7});
}
});
var options = Object.extend(defaults, arguments[1] || {});
this.element = $(element); this.element = $(element);
if(options.handle && (typeof options.handle == 'string')) { if(options.handle && (typeof options.handle == 'string'))
var h = Element.childrenWithClassName(this.element, options.handle, true); this.handle = this.element.down('.'+options.handle, 0);
if(h.length>0) this.handle = h[0];
}
if(!this.handle) this.handle = $(options.handle); if(!this.handle) this.handle = $(options.handle);
if(!this.handle) this.handle = this.element; if(!this.handle) this.handle = this.element;
if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) {
options.scroll = $(options.scroll); options.scroll = $(options.scroll);
this._isScrollChild = Element.childOf(this.element, options.scroll);
}
Element.makePositioned(this.element); // fix IE Element.makePositioned(this.element); // fix IE
@ -262,6 +298,8 @@ Draggable.prototype = {
}, },
initDrag: function(event) { initDrag: function(event) {
if(typeof Draggable._dragging[this.element] != 'undefined' &&
Draggable._dragging[this.element]) return;
if(Event.isLeftClick(event)) { if(Event.isLeftClick(event)) {
// abort on form elements, fixes a Firefox issue // abort on form elements, fixes a Firefox issue
var src = Event.element(event); var src = Event.element(event);
@ -272,11 +310,6 @@ Draggable.prototype = {
src.tagName=='BUTTON' || src.tagName=='BUTTON' ||
src.tagName=='TEXTAREA')) return; src.tagName=='TEXTAREA')) return;
if(this.element._revert) {
this.element._revert.cancel();
this.element._revert = null;
}
var pointer = [Event.pointerX(event), Event.pointerY(event)]; var pointer = [Event.pointerX(event), Event.pointerY(event)];
var pos = Position.cumulativeOffset(this.element); var pos = Position.cumulativeOffset(this.element);
this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) }); this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });
@ -312,6 +345,7 @@ Draggable.prototype = {
} }
Draggables.notify('onStart', this, event); Draggables.notify('onStart', this, event);
if(this.options.starteffect) this.options.starteffect(this.element); if(this.options.starteffect) this.options.starteffect(this.element);
}, },
@ -320,6 +354,7 @@ Draggable.prototype = {
Position.prepare(); Position.prepare();
Droppables.show(pointer, this.element); Droppables.show(pointer, this.element);
Draggables.notify('onDrag', this, event); Draggables.notify('onDrag', this, event);
this.draw(pointer); this.draw(pointer);
if(this.options.change) this.options.change(this); if(this.options.change) this.options.change(this);
@ -331,8 +366,8 @@ Draggable.prototype = {
with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; } with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
} else { } else {
p = Position.page(this.options.scroll); p = Position.page(this.options.scroll);
p[0] += this.options.scroll.scrollLeft; p[0] += this.options.scroll.scrollLeft + Position.deltaX;
p[1] += this.options.scroll.scrollTop; p[1] += this.options.scroll.scrollTop + Position.deltaY;
p.push(p[0]+this.options.scroll.offsetWidth); p.push(p[0]+this.options.scroll.offsetWidth);
p.push(p[1]+this.options.scroll.offsetHeight); p.push(p[1]+this.options.scroll.offsetHeight);
} }
@ -398,10 +433,15 @@ Draggable.prototype = {
draw: function(point) { draw: function(point) {
var pos = Position.cumulativeOffset(this.element); var pos = Position.cumulativeOffset(this.element);
if(this.options.ghosting) {
var r = Position.realOffset(this.element);
pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY;
}
var d = this.currentDelta(); var d = this.currentDelta();
pos[0] -= d[0]; pos[1] -= d[1]; pos[0] -= d[0]; pos[1] -= d[1];
if(this.options.scroll && (this.options.scroll != window)) { if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) {
pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft; pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop; pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
} }
@ -412,7 +452,7 @@ Draggable.prototype = {
if(this.options.snap) { if(this.options.snap) {
if(typeof this.options.snap == 'function') { if(typeof this.options.snap == 'function') {
p = this.options.snap(p[0],p[1]); p = this.options.snap(p[0],p[1],this);
} else { } else {
if(this.options.snap instanceof Array) { if(this.options.snap instanceof Array) {
p = p.map( function(v, i) { p = p.map( function(v, i) {
@ -428,6 +468,7 @@ Draggable.prototype = {
style.left = p[0] + "px"; style.left = p[0] + "px";
if((!this.options.constraint) || (this.options.constraint=='vertical')) if((!this.options.constraint) || (this.options.constraint=='vertical'))
style.top = p[1] + "px"; style.top = p[1] + "px";
if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
}, },
@ -440,6 +481,7 @@ Draggable.prototype = {
}, },
startScrolling: function(speed) { startScrolling: function(speed) {
if(!(speed[0] || speed[1])) return;
this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed]; this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed];
this.lastScrolled = new Date(); this.lastScrolled = new Date();
this.scrollInterval = setInterval(this.scroll.bind(this), 10); this.scrollInterval = setInterval(this.scroll.bind(this), 10);
@ -464,14 +506,16 @@ Draggable.prototype = {
Position.prepare(); Position.prepare();
Droppables.show(Draggables._lastPointer, this.element); Droppables.show(Draggables._lastPointer, this.element);
Draggables.notify('onDrag', this); Draggables.notify('onDrag', this);
Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer); if (this._isScrollChild) {
Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000; Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer);
Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000; Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000;
if (Draggables._lastScrollPointer[0] < 0) Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000;
Draggables._lastScrollPointer[0] = 0; if (Draggables._lastScrollPointer[0] < 0)
if (Draggables._lastScrollPointer[1] < 0) Draggables._lastScrollPointer[0] = 0;
Draggables._lastScrollPointer[1] = 0; if (Draggables._lastScrollPointer[1] < 0)
this.draw(Draggables._lastScrollPointer); Draggables._lastScrollPointer[1] = 0;
this.draw(Draggables._lastScrollPointer);
}
if(this.options.change) this.options.change(this); if(this.options.change) this.options.change(this);
}, },
@ -523,6 +567,8 @@ SortableObserver.prototype = {
} }
var Sortable = { var Sortable = {
SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/,
sortables: {}, sortables: {},
_findRootElement: function(element) { _findRootElement: function(element) {
@ -563,12 +609,13 @@ var Sortable = {
containment: element, // also takes array of elements (or id's); or false containment: element, // also takes array of elements (or id's); or false
handle: false, // or a CSS class handle: false, // or a CSS class
only: false, only: false,
delay: 0,
hoverclass: null, hoverclass: null,
ghosting: false, ghosting: false,
scroll: false, scroll: false,
scrollSensitivity: 20, scrollSensitivity: 20,
scrollSpeed: 15, scrollSpeed: 15,
format: /^[^_]*_(.*)$/, format: this.SERIALIZE_RULE,
onChange: Prototype.emptyFunction, onChange: Prototype.emptyFunction,
onUpdate: Prototype.emptyFunction onUpdate: Prototype.emptyFunction
}, arguments[1] || {}); }, arguments[1] || {});
@ -582,6 +629,7 @@ var Sortable = {
scroll: options.scroll, scroll: options.scroll,
scrollSpeed: options.scrollSpeed, scrollSpeed: options.scrollSpeed,
scrollSensitivity: options.scrollSensitivity, scrollSensitivity: options.scrollSensitivity,
delay: options.delay,
ghosting: options.ghosting, ghosting: options.ghosting,
constraint: options.constraint, constraint: options.constraint,
handle: options.handle }; handle: options.handle };
@ -610,7 +658,6 @@ var Sortable = {
tree: options.tree, tree: options.tree,
hoverclass: options.hoverclass, hoverclass: options.hoverclass,
onHover: Sortable.onHover onHover: Sortable.onHover
//greedy: !options.dropOnEmpty
} }
var options_for_tree = { var options_for_tree = {
@ -635,7 +682,7 @@ var Sortable = {
(this.findElements(element, options) || []).each( function(e) { (this.findElements(element, options) || []).each( function(e) {
// handles are per-draggable // handles are per-draggable
var handle = options.handle ? var handle = options.handle ?
Element.childrenWithClassName(e, options.handle)[0] : e; $(e).down('.'+options.handle,0) : e;
options.draggables.push( options.draggables.push(
new Draggable(e, Object.extend(options_for_draggable, { handle: handle }))); new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
Droppables.add(e, options_for_droppable); Droppables.add(e, options_for_droppable);
@ -706,7 +753,7 @@ var Sortable = {
if(!Element.isParent(dropon, element)) { if(!Element.isParent(dropon, element)) {
var index; var index;
var children = Sortable.findElements(dropon, {tag: droponOptions.tag}); var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only});
var child = null; var child = null;
if(children) { if(children) {
@ -733,7 +780,7 @@ var Sortable = {
}, },
unmark: function() { unmark: function() {
if(Sortable._marker) Element.hide(Sortable._marker); if(Sortable._marker) Sortable._marker.hide();
}, },
mark: function(dropon, position) { mark: function(dropon, position) {
@ -742,23 +789,21 @@ var Sortable = {
if(sortable && !sortable.ghosting) return; if(sortable && !sortable.ghosting) return;
if(!Sortable._marker) { if(!Sortable._marker) {
Sortable._marker = $('dropmarker') || document.createElement('DIV'); Sortable._marker =
Element.hide(Sortable._marker); ($('dropmarker') || Element.extend(document.createElement('DIV'))).
Element.addClassName(Sortable._marker, 'dropmarker'); hide().addClassName('dropmarker').setStyle({position:'absolute'});
Sortable._marker.style.position = 'absolute';
document.getElementsByTagName("body").item(0).appendChild(Sortable._marker); document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
} }
var offsets = Position.cumulativeOffset(dropon); var offsets = Position.cumulativeOffset(dropon);
Sortable._marker.style.left = offsets[0] + 'px'; Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'});
Sortable._marker.style.top = offsets[1] + 'px';
if(position=='after') if(position=='after')
if(sortable.overlap == 'horizontal') if(sortable.overlap == 'horizontal')
Sortable._marker.style.left = (offsets[0]+dropon.clientWidth) + 'px'; Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'});
else else
Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px'; Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'});
Element.show(Sortable._marker); Sortable._marker.show();
}, },
_tree: function(element, options, parent) { _tree: function(element, options, parent) {
@ -773,9 +818,9 @@ var Sortable = {
id: encodeURIComponent(match ? match[1] : null), id: encodeURIComponent(match ? match[1] : null),
element: element, element: element,
parent: parent, parent: parent,
children: new Array, children: [],
position: parent.children.length, position: parent.children.length,
container: Sortable._findChildrenElement(children[i], options.treeTag.toUpperCase()) container: $(children[i]).down(options.treeTag)
} }
/* Get the element containing the children and recurse over it */ /* Get the element containing the children and recurse over it */
@ -788,17 +833,6 @@ var Sortable = {
return parent; return parent;
}, },
/* Finds the first element of the given tag type within a parent element.
Used for finding the first LI[ST] within a L[IST]I[TEM].*/
_findChildrenElement: function (element, containerTag) {
if (element && element.hasChildNodes)
for (var i = 0; i < element.childNodes.length; ++i)
if (element.childNodes[i].tagName == containerTag)
return element.childNodes[i];
return null;
},
tree: function(element) { tree: function(element) {
element = $(element); element = $(element);
var sortableOptions = this.options(element); var sortableOptions = this.options(element);
@ -813,12 +847,12 @@ var Sortable = {
var root = { var root = {
id: null, id: null,
parent: null, parent: null,
children: new Array, children: [],
container: element, container: element,
position: 0 position: 0
} }
return Sortable._tree (element, options, root); return Sortable._tree(element, options, root);
}, },
/* Construct a [i] index for a particular node */ /* Construct a [i] index for a particular node */
@ -867,7 +901,7 @@ var Sortable = {
if (options.tree) { if (options.tree) {
return Sortable.tree(element, arguments[1]).children.map( function (item) { return Sortable.tree(element, arguments[1]).children.map( function (item) {
return [name + Sortable._constructIndex(item) + "=" + return [name + Sortable._constructIndex(item) + "[id]=" +
encodeURIComponent(item.id)].concat(item.children.map(arguments.callee)); encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
}).flatten().join('&'); }).flatten().join('&');
} else { } else {
@ -878,12 +912,10 @@ var Sortable = {
} }
} }
/* Returns true if child is contained within element */ // Returns true if child is contained within element
Element.isParent = function(child, element) { Element.isParent = function(child, element) {
if (!child.parentNode || child == element) return false; if (!child.parentNode || child == element) return false;
if (child.parentNode == element) return true; if (child.parentNode == element) return true;
return Element.isParent(child.parentNode, element); return Element.isParent(child.parentNode, element);
} }
@ -906,8 +938,5 @@ Element.findChildren = function(element, only, recursive, tagName) {
} }
Element.offsetSize = function (element, type) { Element.offsetSize = function (element, type) {
if (type == 'vertical' || type == 'height') return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')];
return element.offsetHeight;
else
return element.offsetWidth;
} }

View file

@ -1,10 +1,11 @@
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) // Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// Contributors: // Contributors:
// Justin Palmer (http://encytemedia.com/) // Justin Palmer (http://encytemedia.com/)
// Mark Pilgrim (http://diveintomark.org/) // Mark Pilgrim (http://diveintomark.org/)
// Martin Bialasinki // Martin Bialasinki
// //
// See scriptaculous.js for full license. // script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/
// converts rgb() and #xxx to #xxxxxx format, // converts rgb() and #xxx to #xxxxxx format,
// returns self (or first argument) if not convertable // returns self (or first argument) if not convertable
@ -41,15 +42,17 @@ Element.collectTextNodesIgnoreClass = function(element, className) {
Element.setContentZoom = function(element, percent) { Element.setContentZoom = function(element, percent) {
element = $(element); element = $(element);
Element.setStyle(element, {fontSize: (percent/100) + 'em'}); element.setStyle({fontSize: (percent/100) + 'em'});
if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
return element;
} }
Element.getOpacity = function(element){ Element.getOpacity = function(element){
element = $(element);
var opacity; var opacity;
if (opacity = Element.getStyle(element, 'opacity')) if (opacity = element.getStyle('opacity'))
return parseFloat(opacity); return parseFloat(opacity);
if (opacity = (Element.getStyle(element, 'filter') || '').match(/alpha\(opacity=(.*)\)/)) if (opacity = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
if(opacity[1]) return parseFloat(opacity[1]) / 100; if(opacity[1]) return parseFloat(opacity[1]) / 100;
return 1.0; return 1.0;
} }
@ -57,34 +60,26 @@ Element.getOpacity = function(element){
Element.setOpacity = function(element, value){ Element.setOpacity = function(element, value){
element= $(element); element= $(element);
if (value == 1){ if (value == 1){
Element.setStyle(element, { opacity: element.setStyle({ opacity:
(/Gecko/.test(navigator.userAgent) && !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ? (/Gecko/.test(navigator.userAgent) && !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ?
0.999999 : null }); 0.999999 : 1.0 });
if(/MSIE/.test(navigator.userAgent)) if(/MSIE/.test(navigator.userAgent) && !window.opera)
Element.setStyle(element, {filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')}); element.setStyle({filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')});
} else { } else {
if(value < 0.00001) value = 0; if(value < 0.00001) value = 0;
Element.setStyle(element, {opacity: value}); element.setStyle({opacity: value});
if(/MSIE/.test(navigator.userAgent)) if(/MSIE/.test(navigator.userAgent) && !window.opera)
Element.setStyle(element, element.setStyle(
{ filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'') + { filter: element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'') +
'alpha(opacity='+value*100+')' }); 'alpha(opacity='+value*100+')' });
} }
return element;
} }
Element.getInlineOpacity = function(element){ Element.getInlineOpacity = function(element){
return $(element).style.opacity || ''; return $(element).style.opacity || '';
} }
Element.childrenWithClassName = function(element, className, findFirst) {
var classNameRegExp = new RegExp("(^|\\s)" + className + "(\\s|$)");
var results = $A($(element).getElementsByTagName('*'))[findFirst ? 'detect' : 'select']( function(c) {
return (c.className && c.className.match(classNameRegExp));
});
if(!results) results = [];
return results;
}
Element.forceRerendering = function(element) { Element.forceRerendering = function(element) {
try { try {
element = $(element); element = $(element);
@ -104,9 +99,17 @@ Array.prototype.call = function() {
/*--------------------------------------------------------------------------*/ /*--------------------------------------------------------------------------*/
var Effect = { var Effect = {
_elementDoesNotExistError: {
name: 'ElementDoesNotExistError',
message: 'The specified DOM element does not exist, but is required for this effect to operate'
},
tagifyText: function(element) { tagifyText: function(element) {
if(typeof Builder == 'undefined')
throw("Effect.tagifyText requires including script.aculo.us' builder.js library");
var tagifyStyle = 'position:relative'; var tagifyStyle = 'position:relative';
if(/MSIE/.test(navigator.userAgent)) tagifyStyle += ';zoom:1'; if(/MSIE/.test(navigator.userAgent) && !window.opera) tagifyStyle += ';zoom:1';
element = $(element); element = $(element);
$A(element.childNodes).each( function(child) { $A(element.childNodes).each( function(child) {
if(child.nodeType==3) { if(child.nodeType==3) {
@ -159,33 +162,35 @@ var Effect2 = Effect; // deprecated
/* ------------- transitions ------------- */ /* ------------- transitions ------------- */
Effect.Transitions = {} Effect.Transitions = {
linear: Prototype.K,
Effect.Transitions.linear = function(pos) { sinoidal: function(pos) {
return pos; return (-Math.cos(pos*Math.PI)/2) + 0.5;
} },
Effect.Transitions.sinoidal = function(pos) { reverse: function(pos) {
return (-Math.cos(pos*Math.PI)/2) + 0.5; return 1-pos;
} },
Effect.Transitions.reverse = function(pos) { flicker: function(pos) {
return 1-pos; return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
} },
Effect.Transitions.flicker = function(pos) { wobble: function(pos) {
return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4; return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
} },
Effect.Transitions.wobble = function(pos) { pulse: function(pos, pulses) {
return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5; pulses = pulses || 5;
} return (
Effect.Transitions.pulse = function(pos) { Math.round((pos % (1/pulses)) * pulses) == 0 ?
return (Math.floor(pos*10) % 2 == 0 ? ((pos * pulses * 2) - Math.floor(pos * pulses * 2)) :
(pos*10-Math.floor(pos*10)) : 1-(pos*10-Math.floor(pos*10))); 1 - ((pos * pulses * 2) - Math.floor(pos * pulses * 2))
} );
Effect.Transitions.none = function(pos) { },
return 0; none: function(pos) {
} return 0;
Effect.Transitions.full = function(pos) { },
return 1; full: function(pos) {
} return 1;
}
};
/* ------------- core effects ------------- */ /* ------------- core effects ------------- */
@ -212,6 +217,9 @@ Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), {
e.finishOn += effect.finishOn; e.finishOn += effect.finishOn;
}); });
break; break;
case 'with-last':
timestamp = this.effects.pluck('startOn').max() || timestamp;
break;
case 'end': case 'end':
// start effect after last queued effect has finished // start effect after last queued effect has finished
timestamp = this.effects.pluck('finishOn').max() || timestamp; timestamp = this.effects.pluck('finishOn').max() || timestamp;
@ -348,12 +356,24 @@ Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), {
} }
}); });
Effect.Event = Class.create();
Object.extend(Object.extend(Effect.Event.prototype, Effect.Base.prototype), {
initialize: function() {
var options = Object.extend({
duration: 0
}, arguments[0] || {});
this.start(options);
},
update: Prototype.emptyFunction
});
Effect.Opacity = Class.create(); Effect.Opacity = Class.create();
Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), { Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), {
initialize: function(element) { initialize: function(element) {
this.element = $(element); this.element = $(element);
if(!this.element) throw(Effect._elementDoesNotExistError);
// make this work on IE on elements without 'layout' // make this work on IE on elements without 'layout'
if(/MSIE/.test(navigator.userAgent) && (!this.element.hasLayout)) if(/MSIE/.test(navigator.userAgent) && !window.opera && (!this.element.currentStyle.hasLayout))
this.element.setStyle({zoom: 1}); this.element.setStyle({zoom: 1});
var options = Object.extend({ var options = Object.extend({
from: this.element.getOpacity() || 0.0, from: this.element.getOpacity() || 0.0,
@ -370,6 +390,7 @@ Effect.Move = Class.create();
Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), { Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), {
initialize: function(element) { initialize: function(element) {
this.element = $(element); this.element = $(element);
if(!this.element) throw(Effect._elementDoesNotExistError);
var options = Object.extend({ var options = Object.extend({
x: 0, x: 0,
y: 0, y: 0,
@ -393,8 +414,8 @@ Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), {
}, },
update: function(position) { update: function(position) {
this.element.setStyle({ this.element.setStyle({
left: this.options.x * position + this.originalLeft + 'px', left: Math.round(this.options.x * position + this.originalLeft) + 'px',
top: this.options.y * position + this.originalTop + 'px' top: Math.round(this.options.y * position + this.originalTop) + 'px'
}); });
} }
}); });
@ -408,7 +429,8 @@ Effect.MoveBy = function(element, toTop, toLeft) {
Effect.Scale = Class.create(); Effect.Scale = Class.create();
Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), { Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), {
initialize: function(element, percent) { initialize: function(element, percent) {
this.element = $(element) this.element = $(element);
if(!this.element) throw(Effect._elementDoesNotExistError);
var options = Object.extend({ var options = Object.extend({
scaleX: true, scaleX: true,
scaleY: true, scaleY: true,
@ -433,7 +455,7 @@ Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), {
this.originalLeft = this.element.offsetLeft; this.originalLeft = this.element.offsetLeft;
var fontSize = this.element.getStyle('font-size') || '100%'; var fontSize = this.element.getStyle('font-size') || '100%';
['em','px','%'].each( function(fontSizeType) { ['em','px','%','pt'].each( function(fontSizeType) {
if(fontSize.indexOf(fontSizeType)>0) { if(fontSize.indexOf(fontSizeType)>0) {
this.fontSize = parseFloat(fontSize); this.fontSize = parseFloat(fontSize);
this.fontSizeType = fontSizeType; this.fontSizeType = fontSizeType;
@ -458,12 +480,12 @@ Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), {
this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale); this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
}, },
finish: function(position) { finish: function(position) {
if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle); if(this.restoreAfterFinish) this.element.setStyle(this.originalStyle);
}, },
setDimensions: function(height, width) { setDimensions: function(height, width) {
var d = {}; var d = {};
if(this.options.scaleX) d.width = width + 'px'; if(this.options.scaleX) d.width = Math.round(width) + 'px';
if(this.options.scaleY) d.height = height + 'px'; if(this.options.scaleY) d.height = Math.round(height) + 'px';
if(this.options.scaleFromCenter) { if(this.options.scaleFromCenter) {
var topd = (height - this.dims[0])/2; var topd = (height - this.dims[0])/2;
var leftd = (width - this.dims[1])/2; var leftd = (width - this.dims[1])/2;
@ -483,6 +505,7 @@ Effect.Highlight = Class.create();
Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), { Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), {
initialize: function(element) { initialize: function(element) {
this.element = $(element); this.element = $(element);
if(!this.element) throw(Effect._elementDoesNotExistError);
var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || {}); var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || {});
this.start(options); this.start(options);
}, },
@ -547,8 +570,7 @@ Effect.Fade = function(element) {
to: 0.0, to: 0.0,
afterFinishInternal: function(effect) { afterFinishInternal: function(effect) {
if(effect.options.to!=0) return; if(effect.options.to!=0) return;
effect.element.hide(); effect.element.hide().setStyle({opacity: oldOpacity});
effect.element.setStyle({opacity: oldOpacity});
}}, arguments[1] || {}); }}, arguments[1] || {});
return new Effect.Opacity(element,options); return new Effect.Opacity(element,options);
} }
@ -563,25 +585,31 @@ Effect.Appear = function(element) {
effect.element.forceRerendering(); effect.element.forceRerendering();
}, },
beforeSetup: function(effect) { beforeSetup: function(effect) {
effect.element.setOpacity(effect.options.from); effect.element.setOpacity(effect.options.from).show();
effect.element.show();
}}, arguments[1] || {}); }}, arguments[1] || {});
return new Effect.Opacity(element,options); return new Effect.Opacity(element,options);
} }
Effect.Puff = function(element) { Effect.Puff = function(element) {
element = $(element); element = $(element);
var oldStyle = { opacity: element.getInlineOpacity(), position: element.getStyle('position') }; var oldStyle = {
opacity: element.getInlineOpacity(),
position: element.getStyle('position'),
top: element.style.top,
left: element.style.left,
width: element.style.width,
height: element.style.height
};
return new Effect.Parallel( return new Effect.Parallel(
[ new Effect.Scale(element, 200, [ new Effect.Scale(element, 200,
{ sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }),
new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], new Effect.Opacity(element, { sync: true, to: 0.0 } ) ],
Object.extend({ duration: 1.0, Object.extend({ duration: 1.0,
beforeSetupInternal: function(effect) { beforeSetupInternal: function(effect) {
effect.effects[0].element.setStyle({position: 'absolute'}); }, Position.absolutize(effect.effects[0].element)
},
afterFinishInternal: function(effect) { afterFinishInternal: function(effect) {
effect.effects[0].element.hide(); effect.effects[0].element.hide().setStyle(oldStyle); }
effect.effects[0].element.setStyle(oldStyle); }
}, arguments[1] || {}) }, arguments[1] || {})
); );
} }
@ -594,8 +622,7 @@ Effect.BlindUp = function(element) {
scaleX: false, scaleX: false,
restoreAfterFinish: true, restoreAfterFinish: true,
afterFinishInternal: function(effect) { afterFinishInternal: function(effect) {
effect.element.hide(); effect.element.hide().undoClipping();
effect.element.undoClipping();
} }
}, arguments[1] || {}) }, arguments[1] || {})
); );
@ -604,28 +631,25 @@ Effect.BlindUp = function(element) {
Effect.BlindDown = function(element) { Effect.BlindDown = function(element) {
element = $(element); element = $(element);
var elementDimensions = element.getDimensions(); var elementDimensions = element.getDimensions();
return new Effect.Scale(element, 100, return new Effect.Scale(element, 100, Object.extend({
Object.extend({ scaleContent: false, scaleContent: false,
scaleX: false, scaleX: false,
scaleFrom: 0, scaleFrom: 0,
scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
restoreAfterFinish: true, restoreAfterFinish: true,
afterSetup: function(effect) { afterSetup: function(effect) {
effect.element.makeClipping(); effect.element.makeClipping().setStyle({height: '0px'}).show();
effect.element.setStyle({height: '0px'}); },
effect.element.show(); afterFinishInternal: function(effect) {
}, effect.element.undoClipping();
afterFinishInternal: function(effect) { }
effect.element.undoClipping(); }, arguments[1] || {}));
}
}, arguments[1] || {})
);
} }
Effect.SwitchOff = function(element) { Effect.SwitchOff = function(element) {
element = $(element); element = $(element);
var oldOpacity = element.getInlineOpacity(); var oldOpacity = element.getInlineOpacity();
return new Effect.Appear(element, { return new Effect.Appear(element, Object.extend({
duration: 0.4, duration: 0.4,
from: 0, from: 0,
transition: Effect.Transitions.flicker, transition: Effect.Transitions.flicker,
@ -634,18 +658,14 @@ Effect.SwitchOff = function(element) {
duration: 0.3, scaleFromCenter: true, duration: 0.3, scaleFromCenter: true,
scaleX: false, scaleContent: false, restoreAfterFinish: true, scaleX: false, scaleContent: false, restoreAfterFinish: true,
beforeSetup: function(effect) { beforeSetup: function(effect) {
effect.element.makePositioned(); effect.element.makePositioned().makeClipping();
effect.element.makeClipping();
}, },
afterFinishInternal: function(effect) { afterFinishInternal: function(effect) {
effect.element.hide(); effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity});
effect.element.undoClipping();
effect.element.undoPositioned();
effect.element.setStyle({opacity: oldOpacity});
} }
}) })
} }
}); }, arguments[1] || {}));
} }
Effect.DropOut = function(element) { Effect.DropOut = function(element) {
@ -663,9 +683,7 @@ Effect.DropOut = function(element) {
effect.effects[0].element.makePositioned(); effect.effects[0].element.makePositioned();
}, },
afterFinishInternal: function(effect) { afterFinishInternal: function(effect) {
effect.effects[0].element.hide(); effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle);
effect.effects[0].element.undoPositioned();
effect.effects[0].element.setStyle(oldStyle);
} }
}, arguments[1] || {})); }, arguments[1] || {}));
} }
@ -687,54 +705,42 @@ Effect.Shake = function(element) {
{ x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { { x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
new Effect.Move(effect.element, new Effect.Move(effect.element,
{ x: -20, y: 0, duration: 0.05, afterFinishInternal: function(effect) { { x: -20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
effect.element.undoPositioned(); effect.element.undoPositioned().setStyle(oldStyle);
effect.element.setStyle(oldStyle);
}}) }}) }}) }}) }}) }}); }}) }}) }}) }}) }}) }});
} }
Effect.SlideDown = function(element) { Effect.SlideDown = function(element) {
element = $(element); element = $(element).cleanWhitespace();
element.cleanWhitespace();
// SlideDown need to have the content of the element wrapped in a container element with fixed height! // SlideDown need to have the content of the element wrapped in a container element with fixed height!
var oldInnerBottom = $(element.firstChild).getStyle('bottom'); var oldInnerBottom = element.down().getStyle('bottom');
var elementDimensions = element.getDimensions(); var elementDimensions = element.getDimensions();
return new Effect.Scale(element, 100, Object.extend({ return new Effect.Scale(element, 100, Object.extend({
scaleContent: false, scaleContent: false,
scaleX: false, scaleX: false,
scaleFrom: 0, scaleFrom: window.opera ? 0 : 1,
scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
restoreAfterFinish: true, restoreAfterFinish: true,
afterSetup: function(effect) { afterSetup: function(effect) {
effect.element.makePositioned(); effect.element.makePositioned();
effect.element.firstChild.makePositioned(); effect.element.down().makePositioned();
if(window.opera) effect.element.setStyle({top: ''}); if(window.opera) effect.element.setStyle({top: ''});
effect.element.makeClipping(); effect.element.makeClipping().setStyle({height: '0px'}).show();
effect.element.setStyle({height: '0px'}); },
effect.element.show(); },
afterUpdateInternal: function(effect) { afterUpdateInternal: function(effect) {
effect.element.firstChild.setStyle({bottom: effect.element.down().setStyle({bottom:
(effect.dims[0] - effect.element.clientHeight) + 'px' }); (effect.dims[0] - effect.element.clientHeight) + 'px' });
}, },
afterFinishInternal: function(effect) { afterFinishInternal: function(effect) {
effect.element.undoClipping(); effect.element.undoClipping().undoPositioned();
// IE will crash if child is undoPositioned first effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); }
if(/MSIE/.test(navigator.userAgent)){
effect.element.undoPositioned();
effect.element.firstChild.undoPositioned();
}else{
effect.element.firstChild.undoPositioned();
effect.element.undoPositioned();
}
effect.element.firstChild.setStyle({bottom: oldInnerBottom}); }
}, arguments[1] || {}) }, arguments[1] || {})
); );
} }
Effect.SlideUp = function(element) { Effect.SlideUp = function(element) {
element = $(element); element = $(element).cleanWhitespace();
element.cleanWhitespace(); var oldInnerBottom = element.down().getStyle('bottom');
var oldInnerBottom = $(element.firstChild).getStyle('bottom'); return new Effect.Scale(element, window.opera ? 0 : 1,
return new Effect.Scale(element, 0,
Object.extend({ scaleContent: false, Object.extend({ scaleContent: false,
scaleX: false, scaleX: false,
scaleMode: 'box', scaleMode: 'box',
@ -742,32 +748,32 @@ Effect.SlideUp = function(element) {
restoreAfterFinish: true, restoreAfterFinish: true,
beforeStartInternal: function(effect) { beforeStartInternal: function(effect) {
effect.element.makePositioned(); effect.element.makePositioned();
effect.element.firstChild.makePositioned(); effect.element.down().makePositioned();
if(window.opera) effect.element.setStyle({top: ''}); if(window.opera) effect.element.setStyle({top: ''});
effect.element.makeClipping(); effect.element.makeClipping().show();
effect.element.show(); }, },
afterUpdateInternal: function(effect) { afterUpdateInternal: function(effect) {
effect.element.firstChild.setStyle({bottom: effect.element.down().setStyle({bottom:
(effect.dims[0] - effect.element.clientHeight) + 'px' }); }, (effect.dims[0] - effect.element.clientHeight) + 'px' });
},
afterFinishInternal: function(effect) { afterFinishInternal: function(effect) {
effect.element.hide(); effect.element.hide().undoClipping().undoPositioned().setStyle({bottom: oldInnerBottom});
effect.element.undoClipping(); effect.element.down().undoPositioned();
effect.element.firstChild.undoPositioned(); }
effect.element.undoPositioned();
effect.element.setStyle({bottom: oldInnerBottom}); }
}, arguments[1] || {}) }, arguments[1] || {})
); );
} }
// Bug in opera makes the TD containing this element expand for a instance after finish // Bug in opera makes the TD containing this element expand for a instance after finish
Effect.Squish = function(element) { Effect.Squish = function(element) {
return new Effect.Scale(element, window.opera ? 1 : 0, return new Effect.Scale(element, window.opera ? 1 : 0, {
{ restoreAfterFinish: true, restoreAfterFinish: true,
beforeSetup: function(effect) { beforeSetup: function(effect) {
effect.element.makeClipping(effect.element); }, effect.element.makeClipping();
afterFinishInternal: function(effect) { },
effect.element.hide(effect.element); afterFinishInternal: function(effect) {
effect.element.undoClipping(effect.element); } effect.element.hide().undoClipping();
}
}); });
} }
@ -823,9 +829,7 @@ Effect.Grow = function(element) {
y: initialMoveY, y: initialMoveY,
duration: 0.01, duration: 0.01,
beforeSetup: function(effect) { beforeSetup: function(effect) {
effect.element.hide(); effect.element.hide().makeClipping().makePositioned();
effect.element.makeClipping();
effect.element.makePositioned();
}, },
afterFinishInternal: function(effect) { afterFinishInternal: function(effect) {
new Effect.Parallel( new Effect.Parallel(
@ -836,13 +840,10 @@ Effect.Grow = function(element) {
sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true}) sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true})
], Object.extend({ ], Object.extend({
beforeSetup: function(effect) { beforeSetup: function(effect) {
effect.effects[0].element.setStyle({height: '0px'}); effect.effects[0].element.setStyle({height: '0px'}).show();
effect.effects[0].element.show();
}, },
afterFinishInternal: function(effect) { afterFinishInternal: function(effect) {
effect.effects[0].element.undoClipping(); effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle);
effect.effects[0].element.undoPositioned();
effect.effects[0].element.setStyle(oldStyle);
} }
}, options) }, options)
) )
@ -896,13 +897,10 @@ Effect.Shrink = function(element) {
new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }) new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition })
], Object.extend({ ], Object.extend({
beforeStartInternal: function(effect) { beforeStartInternal: function(effect) {
effect.effects[0].element.makePositioned(); effect.effects[0].element.makePositioned().makeClipping();
effect.effects[0].element.makeClipping(); }, },
afterFinishInternal: function(effect) { afterFinishInternal: function(effect) {
effect.effects[0].element.hide(); effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); }
effect.effects[0].element.undoClipping();
effect.effects[0].element.undoPositioned();
effect.effects[0].element.setStyle(oldStyle); }
}, options) }, options)
); );
} }
@ -912,10 +910,10 @@ Effect.Pulsate = function(element) {
var options = arguments[1] || {}; var options = arguments[1] || {};
var oldOpacity = element.getInlineOpacity(); var oldOpacity = element.getInlineOpacity();
var transition = options.transition || Effect.Transitions.sinoidal; var transition = options.transition || Effect.Transitions.sinoidal;
var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos)) }; var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos, options.pulses)) };
reverser.bind(transition); reverser.bind(transition);
return new Effect.Opacity(element, return new Effect.Opacity(element,
Object.extend(Object.extend({ duration: 3.0, from: 0, Object.extend(Object.extend({ duration: 2.0, from: 0,
afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); } afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); }
}, options), {transition: reverser})); }, options), {transition: reverser}));
} }
@ -927,7 +925,7 @@ Effect.Fold = function(element) {
left: element.style.left, left: element.style.left,
width: element.style.width, width: element.style.width,
height: element.style.height }; height: element.style.height };
Element.makeClipping(element); element.makeClipping();
return new Effect.Scale(element, 5, Object.extend({ return new Effect.Scale(element, 5, Object.extend({
scaleContent: false, scaleContent: false,
scaleX: false, scaleX: false,
@ -936,15 +934,147 @@ Effect.Fold = function(element) {
scaleContent: false, scaleContent: false,
scaleY: false, scaleY: false,
afterFinishInternal: function(effect) { afterFinishInternal: function(effect) {
effect.element.hide(); effect.element.hide().undoClipping().setStyle(oldStyle);
effect.element.undoClipping();
effect.element.setStyle(oldStyle);
} }); } });
}}, arguments[1] || {})); }}, arguments[1] || {}));
}; };
Effect.Morph = Class.create();
Object.extend(Object.extend(Effect.Morph.prototype, Effect.Base.prototype), {
initialize: function(element) {
this.element = $(element);
if(!this.element) throw(Effect._elementDoesNotExistError);
var options = Object.extend({
style: ''
}, arguments[1] || {});
this.start(options);
},
setup: function(){
function parseColor(color){
if(!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff';
color = color.parseColor();
return $R(0,2).map(function(i){
return parseInt( color.slice(i*2+1,i*2+3), 16 )
});
}
this.transforms = this.options.style.parseStyle().map(function(property){
var originalValue = this.element.getStyle(property[0]);
return $H({
style: property[0],
originalValue: property[1].unit=='color' ?
parseColor(originalValue) : parseFloat(originalValue || 0),
targetValue: property[1].unit=='color' ?
parseColor(property[1].value) : property[1].value,
unit: property[1].unit
});
}.bind(this)).reject(function(transform){
return (
(transform.originalValue == transform.targetValue) ||
(
transform.unit != 'color' &&
(isNaN(transform.originalValue) || isNaN(transform.targetValue))
)
)
});
},
update: function(position) {
var style = $H(), value = null;
this.transforms.each(function(transform){
value = transform.unit=='color' ?
$R(0,2).inject('#',function(m,v,i){
return m+(Math.round(transform.originalValue[i]+
(transform.targetValue[i] - transform.originalValue[i])*position)).toColorPart() }) :
transform.originalValue + Math.round(
((transform.targetValue - transform.originalValue) * position) * 1000)/1000 + transform.unit;
style[transform.style] = value;
});
this.element.setStyle(style);
}
});
Effect.Transform = Class.create();
Object.extend(Effect.Transform.prototype, {
initialize: function(tracks){
this.tracks = [];
this.options = arguments[1] || {};
this.addTracks(tracks);
},
addTracks: function(tracks){
tracks.each(function(track){
var data = $H(track).values().first();
this.tracks.push($H({
ids: $H(track).keys().first(),
effect: Effect.Morph,
options: { style: data }
}));
}.bind(this));
return this;
},
play: function(){
return new Effect.Parallel(
this.tracks.map(function(track){
var elements = [$(track.ids) || $$(track.ids)].flatten();
return elements.map(function(e){ return new track.effect(e, Object.extend({ sync:true }, track.options)) });
}).flatten(),
this.options
);
}
});
Element.CSS_PROPERTIES = ['azimuth', 'backgroundAttachment', 'backgroundColor', 'backgroundImage',
'backgroundPosition', 'backgroundRepeat', 'borderBottomColor', 'borderBottomStyle',
'borderBottomWidth', 'borderCollapse', 'borderLeftColor', 'borderLeftStyle', 'borderLeftWidth',
'borderRightColor', 'borderRightStyle', 'borderRightWidth', 'borderSpacing', 'borderTopColor',
'borderTopStyle', 'borderTopWidth', 'bottom', 'captionSide', 'clear', 'clip', 'color', 'content',
'counterIncrement', 'counterReset', 'cssFloat', 'cueAfter', 'cueBefore', 'cursor', 'direction',
'display', 'elevation', 'emptyCells', 'fontFamily', 'fontSize', 'fontSizeAdjust', 'fontStretch',
'fontStyle', 'fontVariant', 'fontWeight', 'height', 'left', 'letterSpacing', 'lineHeight',
'listStyleImage', 'listStylePosition', 'listStyleType', 'marginBottom', 'marginLeft', 'marginRight',
'marginTop', 'markerOffset', 'marks', 'maxHeight', 'maxWidth', 'minHeight', 'minWidth', 'opacity',
'orphans', 'outlineColor', 'outlineOffset', 'outlineStyle', 'outlineWidth', 'overflowX', 'overflowY',
'paddingBottom', 'paddingLeft', 'paddingRight', 'paddingTop', 'page', 'pageBreakAfter', 'pageBreakBefore',
'pageBreakInside', 'pauseAfter', 'pauseBefore', 'pitch', 'pitchRange', 'position', 'quotes',
'richness', 'right', 'size', 'speakHeader', 'speakNumeral', 'speakPunctuation', 'speechRate', 'stress',
'tableLayout', 'textAlign', 'textDecoration', 'textIndent', 'textShadow', 'textTransform', 'top',
'unicodeBidi', 'verticalAlign', 'visibility', 'voiceFamily', 'volume', 'whiteSpace', 'widows',
'width', 'wordSpacing', 'zIndex'];
Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/;
String.prototype.parseStyle = function(){
var element = Element.extend(document.createElement('div'));
element.innerHTML = '<div style="' + this + '"></div>';
var style = element.down().style, styleRules = $H();
Element.CSS_PROPERTIES.each(function(property){
if(style[property]) styleRules[property] = style[property];
});
var result = $H();
styleRules.each(function(pair){
var property = pair[0], value = pair[1], unit = null;
if(value.parseColor('#zzzzzz') != '#zzzzzz') {
value = value.parseColor();
unit = 'color';
} else if(Element.CSS_LENGTH.test(value))
var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/),
value = parseFloat(components[1]), unit = (components.length == 3) ? components[2] : null;
result[property.underscore().dasherize()] = $H({ value:value, unit:unit });
}.bind(this));
return result;
};
Element.morph = function(element, style) {
new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || {}));
return element;
};
['setOpacity','getOpacity','getInlineOpacity','forceRerendering','setContentZoom', ['setOpacity','getOpacity','getInlineOpacity','forceRerendering','setContentZoom',
'collectTextNodes','collectTextNodesIgnoreClass','childrenWithClassName'].each( 'collectTextNodes','collectTextNodesIgnoreClass','morph'].each(
function(f) { Element.Methods[f] = Element[f]; } function(f) { Element.Methods[f] = Element[f]; }
); );

File diff suppressed because it is too large Load diff

View file

@ -1,43 +1,64 @@
module ActionView module ActionView
module Helpers module Helpers #:nodoc:
# Provides methods for converting a number into a formatted string that currently represents # Provides methods for converting a numbers into formatted strings.
# one of the following forms: phone number, percentage, money, or precision level. # Methods are provided for phone numbers, currency, percentage,
# precision, positional notation, and file size.
module NumberHelper module NumberHelper
# Formats a +number+ into a US phone number. You can customize the format
# Formats a +number+ into a US phone number string. The +options+ can be a hash used to customize the format of the output. # in the +options+ hash.
# The area code can be surrounded by parentheses by setting +:area_code+ to true; default is false # * <tt>:area_code</tt> - Adds parentheses around the area code.
# The delimiter can be set using +:delimiter+; default is "-" # * <tt>:delimiter</tt> - Specifies the delimiter to use, defaults to "-".
# Examples: # * <tt>:extension</tt> - Specifies an extension to add to the end of the
# number_to_phone(1235551234) => 123-555-1234 # generated number
# number_to_phone(1235551234, {:area_code => true}) => (123) 555-1234 # * <tt>:country_code</tt> - Sets the country code for the phone number.
# number_to_phone(1235551234, {:delimiter => " "}) => 123 555 1234 #
# number_to_phone(1235551234, {:area_code => true, :extension => 555}) => (123) 555-1234 x 555 # number_to_phone(1235551234) => 123-555-1234
# number_to_phone(1235551234, :area_code => true) => (123) 555-1234
# number_to_phone(1235551234, :delimiter => " ") => 123 555 1234
# number_to_phone(1235551234, :area_code => true, :extension => 555) => (123) 555-1234 x 555
# number_to_phone(1235551234, :country_code => 1)
def number_to_phone(number, options = {}) def number_to_phone(number, options = {})
options = options.stringify_keys number = number.to_s.strip unless number.nil?
area_code = options.delete("area_code") { false } options = options.stringify_keys
delimiter = options.delete("delimiter") { "-" } area_code = options["area_code"] || nil
extension = options.delete("extension") { "" } delimiter = options["delimiter"] || "-"
extension = options["extension"].to_s.strip || nil
country_code = options["country_code"] || nil
begin begin
str = area_code == true ? number.to_s.gsub(/([0-9]{3})([0-9]{3})([0-9]{4})/,"(\\1) \\2#{delimiter}\\3") : number.to_s.gsub(/([0-9]{3})([0-9]{3})([0-9]{4})/,"\\1#{delimiter}\\2#{delimiter}\\3") str = ""
extension.to_s.strip.empty? ? str : "#{str} x #{extension.to_s.strip}" str << "+#{country_code}#{delimiter}" unless country_code.blank?
str << if area_code
number.gsub!(/([0-9]{1,3})([0-9]{3})([0-9]{4}$)/,"(\\1) \\2#{delimiter}\\3")
else
number.gsub!(/([0-9]{1,3})([0-9]{3})([0-9]{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3")
end
str << " x #{extension}" unless extension.blank?
str
rescue rescue
number number
end end
end end
# Formats a +number+ into a currency string. The +options+ hash can be used to customize the format of the output. # Formats a +number+ into a currency string. You can customize the format
# The +number+ can contain a level of precision using the +precision+ key; default is 2 # in the +options+ hash.
# The currency type can be set using the +unit+ key; default is "$" # * <tt>:precision</tt> - Sets the level of precision, defaults to 2
# The unit separator can be set using the +separator+ key; default is "." # * <tt>:unit</tt> - Sets the denomination of the currency, defaults to "$"
# The delimiter can be set using the +delimiter+ key; default is "," # * <tt>:separator</tt> - Sets the separator between the units, defaults to "."
# Examples: # * <tt>:delimiter</tt> - Sets the thousands delimiter, defaults to ","
# number_to_currency(1234567890.50) => $1,234,567,890.50 #
# number_to_currency(1234567890.506) => $1,234,567,890.51 # number_to_currency(1234567890.50) => $1,234,567,890.50
# number_to_currency(1234567890.50, {:unit => "&pound;", :separator => ",", :delimiter => ""}) => &pound;1234567890,50 # number_to_currency(1234567890.506) => $1,234,567,890.51
# number_to_currency(1234567890.506, :precision => 3) => $1,234,567,890.506
# number_to_currency(1234567890.50, :unit => "&pound;", :separator => ",", :delimiter => "")
# => &pound;1234567890,50
def number_to_currency(number, options = {}) def number_to_currency(number, options = {})
options = options.stringify_keys options = options.stringify_keys
precision, unit, separator, delimiter = options.delete("precision") { 2 }, options.delete("unit") { "$" }, options.delete("separator") { "." }, options.delete("delimiter") { "," } precision = options["precision"] || 2
separator = "" unless precision > 0 unit = options["unit"] || "$"
separator = precision > 0 ? options["separator"] || "." : ""
delimiter = options["delimiter"] || ","
begin begin
parts = number_with_precision(number, precision).split('.') parts = number_with_precision(number, precision).split('.')
unit + number_with_delimiter(parts[0], delimiter) + separator + parts[1].to_s unit + number_with_delimiter(parts[0], delimiter) + separator + parts[1].to_s
@ -46,16 +67,19 @@ module ActionView
end end
end end
# Formats a +number+ as into a percentage string. The +options+ hash can be used to customize the format of the output. # Formats a +number+ as a percentage string. You can customize the
# The +number+ can contain a level of precision using the +precision+ key; default is 3 # format in the +options+ hash.
# The unit separator can be set using the +separator+ key; default is "." # * <tt>:precision</tt> - Sets the level of precision, defaults to 3
# Examples: # * <tt>:separator</tt> - Sets the separator between the units, defaults to "."
# number_to_percentage(100) => 100.000% #
# number_to_percentage(100, {:precision => 0}) => 100% # number_to_percentage(100) => 100.000%
# number_to_percentage(302.0574, {:precision => 2}) => 302.06% # number_to_percentage(100, {:precision => 0}) => 100%
# number_to_percentage(302.0574, {:precision => 2}) => 302.06%
def number_to_percentage(number, options = {}) def number_to_percentage(number, options = {})
options = options.stringify_keys options = options.stringify_keys
precision, separator = options.delete("precision") { 3 }, options.delete("separator") { "." } precision = options["precision"] || 3
separator = options["separator"] || "."
begin begin
number = number_with_precision(number, precision) number = number_with_precision(number, precision)
parts = number.split('.') parts = number.split('.')
@ -69,41 +93,63 @@ module ActionView
end end
end end
# Formats a +number+ with a +delimiter+. # Formats a +number+ with grouped thousands using +delimiter+. You
# Example: # can customize the format using optional <em>delimiter</em> and <em>separator</em> parameters.
# number_with_delimiter(12345678) => 12,345,678 # * <tt>delimiter</tt> - Sets the thousands delimiter, defaults to ","
def number_with_delimiter(number, delimiter=",") # * <tt>separator</tt> - Sets the separator between the units, defaults to "."
number.to_s.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{delimiter}") #
# number_with_delimiter(12345678) => 12,345,678
# number_with_delimiter(12345678.05) => 12,345,678.05
# number_with_delimiter(12345678, ".") => 12.345.678
def number_with_delimiter(number, delimiter=",", separator=".")
begin
parts = number.to_s.split('.')
parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{delimiter}")
parts.join separator
rescue
number
end
end end
# Returns a formatted-for-humans file size. # Formats a +number+ with the specified level of +precision+. The default
# level of precision is 3.
# #
# Examples: # number_with_precision(111.2345) => 111.235
# human_size(123) => 123 Bytes # number_with_precision(111.2345, 2) => 111.24
# human_size(1234) => 1.2 KB def number_with_precision(number, precision=3)
# human_size(12345) => 12.1 KB "%01.#{precision}f" % number
# human_size(1234567) => 1.2 MB rescue
# human_size(1234567890) => 1.1 GB number
def number_to_human_size(size) end
# Formats the bytes in +size+ into a more understandable representation.
# Useful for reporting file sizes to users. This method returns nil if
# +size+ cannot be converted into a number. You can change the default
# precision of 1 in +precision+.
#
# number_to_human_size(123) => 123 Bytes
# number_to_human_size(1234) => 1.2 KB
# number_to_human_size(12345) => 12.1 KB
# number_to_human_size(1234567) => 1.2 MB
# number_to_human_size(1234567890) => 1.1 GB
# number_to_human_size(1234567890123) => 1.1 TB
# number_to_human_size(1234567, 2) => 1.18 MB
def number_to_human_size(size, precision=1)
size = Kernel.Float(size)
case case
when size < 1.kilobyte: '%d Bytes' % size when size == 1 : "1 Byte"
when size < 1.megabyte: '%.1f KB' % (size / 1.0.kilobyte) when size < 1.kilobyte: "%d Bytes" % size
when size < 1.gigabyte: '%.1f MB' % (size / 1.0.megabyte) when size < 1.megabyte: "%.#{precision}f KB" % (size / 1.0.kilobyte)
when size < 1.terabyte: '%.1f GB' % (size / 1.0.gigabyte) when size < 1.gigabyte: "%.#{precision}f MB" % (size / 1.0.megabyte)
else '%.1f TB' % (size / 1.0.terabyte) when size < 1.terabyte: "%.#{precision}f GB" % (size / 1.0.gigabyte)
else "%.#{precision}f TB" % (size / 1.0.terabyte)
end.sub('.0', '') end.sub('.0', '')
rescue rescue
nil nil
end end
alias_method :human_size, :number_to_human_size # deprecated alias alias_method :human_size, :number_to_human_size # deprecated alias
deprecate :human_size => :number_to_human_size
# Formats a +number+ with a level of +precision+.
# Example:
# number_with_precision(111.2345) => 111.235
def number_with_precision(number, precision=3)
sprintf("%01.#{precision}f", number)
end
end end
end end
end end

View file

@ -1,4 +1,3 @@
require File.dirname(__FILE__) + '/javascript_helper'
require 'set' require 'set'
module ActionView module ActionView
@ -39,7 +38,7 @@ module ActionView
# XMLHttpRequest. The result of that request can then be inserted into a # XMLHttpRequest. The result of that request can then be inserted into a
# DOM object whose id can be specified with <tt>options[:update]</tt>. # DOM object whose id can be specified with <tt>options[:update]</tt>.
# Usually, the result would be a partial prepared by the controller with # Usually, the result would be a partial prepared by the controller with
# either render_partial or render_partial_collection. # render :partial.
# #
# Examples: # Examples:
# link_to_remote "Delete this post", :update => "posts", # link_to_remote "Delete this post", :update => "posts",
@ -60,6 +59,12 @@ module ActionView
# influence how the target DOM element is updated. It must be one of # influence how the target DOM element is updated. It must be one of
# <tt>:before</tt>, <tt>:top</tt>, <tt>:bottom</tt>, or <tt>:after</tt>. # <tt>:before</tt>, <tt>:top</tt>, <tt>:bottom</tt>, or <tt>:after</tt>.
# #
# The method used is by default POST. You can also specify GET or you
# can simulate PUT or DELETE over POST. All specified with <tt>options[:method]</tt>
#
# Example:
# link_to_remote "Destroy", :url => person_url(:id => person), :method => :delete
#
# By default, these remote requests are processed asynchronous during # By default, these remote requests are processed asynchronous during
# which various JavaScript callbacks can be triggered (for progress # which various JavaScript callbacks can be triggered (for progress
# indicators and the likes). All callbacks get access to the # indicators and the likes). All callbacks get access to the
@ -159,15 +164,20 @@ module ActionView
# #
# By default the fall-through action is the same as the one specified in # By default the fall-through action is the same as the one specified in
# the :url (and the default method is :post). # the :url (and the default method is :post).
def form_remote_tag(options = {}) #
# form_remote_tag also takes a block, like form_tag:
# <% form_remote_tag :url => '/posts' do -%>
# <div><%= submit_tag 'Save' %></div>
# <% end -%>
def form_remote_tag(options = {}, &block)
options[:form] = true options[:form] = true
options[:html] ||= {} options[:html] ||= {}
options[:html][:onsubmit] = "#{remote_function(options)}; return false;" options[:html][:onsubmit] =
options[:html][:action] = options[:html][:action] || url_for(options[:url]) (options[:html][:onsubmit] ? options[:html][:onsubmit] + "; " : "") +
options[:html][:method] = options[:html][:method] || "post" "#{remote_function(options)}; return false;"
tag("form", options[:html], true) form_tag(options[:html].delete(:action) || url_for(options[:url]), options[:html], &block)
end end
# Works like form_remote_tag, but uses form_for semantics. # Works like form_remote_tag, but uses form_for semantics.
@ -194,81 +204,6 @@ module ActionView
tag("input", options[:html], false) tag("input", options[:html], false)
end end
# Returns a JavaScript function (or expression) that'll update a DOM
# element according to the options passed.
#
# * <tt>:content</tt>: The content to use for updating. Can be left out
# if using block, see example.
# * <tt>:action</tt>: Valid options are :update (assumed by default),
# :empty, :remove
# * <tt>:position</tt> If the :action is :update, you can optionally
# specify one of the following positions: :before, :top, :bottom,
# :after.
#
# Examples:
# <%= javascript_tag(update_element_function("products",
# :position => :bottom, :content => "<p>New product!</p>")) %>
#
# <% replacement_function = update_element_function("products") do %>
# <p>Product 1</p>
# <p>Product 2</p>
# <% end %>
# <%= javascript_tag(replacement_function) %>
#
# This method can also be used in combination with remote method call
# where the result is evaluated afterwards to cause multiple updates on
# a page. Example:
#
# # Calling view
# <%= form_remote_tag :url => { :action => "buy" },
# :complete => evaluate_remote_response %>
# all the inputs here...
#
# # Controller action
# def buy
# @product = Product.find(1)
# end
#
# # Returning view
# <%= update_element_function(
# "cart", :action => :update, :position => :bottom,
# :content => "<p>New Product: #{@product.name}</p>")) %>
# <% update_element_function("status", :binding => binding) do %>
# You've bought a new product!
# <% end %>
#
# Notice how the second call doesn't need to be in an ERb output block
# since it uses a block and passes in the binding to render directly.
# This trick will however only work in ERb (not Builder or other
# template forms).
#
# See also JavaScriptGenerator and update_page.
def update_element_function(element_id, options = {}, &block)
content = escape_javascript(options[:content] || '')
content = escape_javascript(capture(&block)) if block
javascript_function = case (options[:action] || :update)
when :update
if options[:position]
"new Insertion.#{options[:position].to_s.camelize}('#{element_id}','#{content}')"
else
"$('#{element_id}').innerHTML = '#{content}'"
end
when :empty
"$('#{element_id}').innerHTML = ''"
when :remove
"Element.remove('#{element_id}')"
else
raise ArgumentError, "Invalid action, choose one of :update, :remove, :empty"
end
javascript_function << ";\n"
options[:binding] ? concat(javascript_function, options[:binding]) : javascript_function
end
# Returns 'eval(request.responseText)' which is the JavaScript function # Returns 'eval(request.responseText)' which is the JavaScript function
# that form_remote_tag can call in :complete to evaluate a multiple # that form_remote_tag can call in :complete to evaluate a multiple
# update return document using update_element_function calls. # update return document using update_element_function calls.
@ -289,7 +224,7 @@ module ActionView
javascript_options = options_for_ajax(options) javascript_options = options_for_ajax(options)
update = '' update = ''
if options[:update] and options[:update].is_a?Hash if options[:update] && options[:update].is_a?(Hash)
update = [] update = []
update << "success:'#{options[:update][:success]}'" if options[:update][:success] update << "success:'#{options[:update][:success]}'" if options[:update][:success]
update << "failure:'#{options[:update][:failure]}'" if options[:update][:failure] update << "failure:'#{options[:update][:failure]}'" if options[:update][:failure]
@ -303,7 +238,7 @@ module ActionView
"new Ajax.Updater(#{update}, " "new Ajax.Updater(#{update}, "
url_options = options[:url] url_options = options[:url]
url_options = url_options.merge(:escape => false) if url_options.is_a? Hash url_options = url_options.merge(:escape => false) if url_options.is_a?(Hash)
function << "'#{url_for(url_options)}'" function << "'#{url_for(url_options)}'"
function << ", #{javascript_options})" function << ", #{javascript_options})"
@ -438,7 +373,7 @@ module ActionView
if ActionView::Base.debug_rjs if ActionView::Base.debug_rjs
source = javascript.dup source = javascript.dup
javascript.replace "try {\n#{source}\n} catch (e) " javascript.replace "try {\n#{source}\n} catch (e) "
javascript << "{ alert('RJS error:\\n\\n' + e.toString()); alert('#{source.gsub(/\r\n|\n|\r/, "\\n").gsub(/["']/) { |m| "\\#{m}" }}'); throw e }" javascript << "{ alert('RJS error:\\n\\n' + e.toString()); alert('#{source.gsub('\\','\0\0').gsub(/\r\n|\n|\r/, "\\n").gsub(/["']/) { |m| "\\#{m}" }}'); throw e }"
end end
end end
end end
@ -453,6 +388,12 @@ module ActionView
JavaScriptElementProxy.new(self, id) JavaScriptElementProxy.new(self, id)
end end
# Returns an object whose <tt>#to_json</tt> evaluates to +code+. Use this to pass a literal JavaScript
# expression as an argument to another JavaScriptGenerator method.
def literal(code)
ActiveSupport::JSON::Variable.new(code.to_s)
end
# Returns a collection reference by finding it through a CSS +pattern+ in the DOM. This collection can then be # Returns a collection reference by finding it through a CSS +pattern+ in the DOM. This collection can then be
# used for further method calls. Examples: # used for further method calls. Examples:
# #
@ -526,7 +467,7 @@ module ActionView
# #
# # Replace the DOM element having ID 'person-45' with the # # Replace the DOM element having ID 'person-45' with the
# # 'person' partial for the appropriate object. # # 'person' partial for the appropriate object.
# replace_html 'person-45', :partial => 'person', :object => @person # replace 'person-45', :partial => 'person', :object => @person
# #
# This allows the same partial that is used for the +insert_html+ to # This allows the same partial that is used for the +insert_html+ to
# be also used for the input to +replace+ without resorting to # be also used for the input to +replace+ without resorting to
@ -550,22 +491,22 @@ module ActionView
# Removes the DOM elements with the given +ids+ from the page. # Removes the DOM elements with the given +ids+ from the page.
def remove(*ids) def remove(*ids)
record "#{javascript_object_for(ids)}.each(Element.remove)" loop_on_multiple_args 'Element.remove', ids
end end
# Shows hidden DOM elements with the given +ids+. # Shows hidden DOM elements with the given +ids+.
def show(*ids) def show(*ids)
call 'Element.show', *ids loop_on_multiple_args 'Element.show', ids
end end
# Hides the visible DOM elements with the given +ids+. # Hides the visible DOM elements with the given +ids+.
def hide(*ids) def hide(*ids)
call 'Element.hide', *ids loop_on_multiple_args 'Element.hide', ids
end end
# Toggles the visibility of the DOM elements with the given +ids+. # Toggles the visibility of the DOM elements with the given +ids+.
def toggle(*ids) def toggle(*ids)
call 'Element.toggle', *ids loop_on_multiple_args 'Element.toggle', ids
end end
# Displays an alert dialog with the given +message+. # Displays an alert dialog with the given +message+.
@ -573,16 +514,18 @@ module ActionView
call 'alert', message call 'alert', message
end end
# Redirects the browser to the given +location+, in the same form as # Redirects the browser to the given +location+, in the same form as +url_for+.
# +url_for+.
def redirect_to(location) def redirect_to(location)
assign 'window.location.href', @context.url_for(location) assign 'window.location.href', @context.url_for(location)
end end
# Calls the JavaScript +function+, optionally with the given # Calls the JavaScript +function+, optionally with the given +arguments+.
# +arguments+. #
def call(function, *arguments) # If a block is given, the block will be passed to a new JavaScriptGenerator;
record "#{function}(#{arguments_for_call(arguments)})" # the resulting JavaScript code will then be wrapped inside <tt>function() { ... }</tt>
# and passed as the called function's final argument.
def call(function, *arguments, &block)
record "#{function}(#{arguments_for_call(arguments, block)})"
end end
# Assigns the JavaScript +variable+ the given +value+. # Assigns the JavaScript +variable+ the given +value+.
@ -633,12 +576,18 @@ module ActionView
end end
private private
def loop_on_multiple_args(method, ids)
record(ids.size>1 ?
"#{javascript_object_for(ids)}.each(#{method})" :
"#{method}(#{ids.first.to_json})")
end
def page def page
self self
end end
def record(line) def record(line)
returning line = "#{line.to_s.chomp.gsub /\;$/, ''};" do returning line = "#{line.to_s.chomp.gsub(/\;\z/, '')};" do
self << line self << line
end end
end end
@ -653,10 +602,16 @@ module ActionView
object.respond_to?(:to_json) ? object.to_json : object.inspect object.respond_to?(:to_json) ? object.to_json : object.inspect
end end
def arguments_for_call(arguments) def arguments_for_call(arguments, block = nil)
arguments << block_to_function(block) if block
arguments.map { |argument| javascript_object_for(argument) }.join ', ' arguments.map { |argument| javascript_object_for(argument) }.join ', '
end end
def block_to_function(block)
generator = self.class.new(@context, &block)
literal("function() { #{generator.to_s} }")
end
def method_missing(method, *arguments) def method_missing(method, *arguments)
JavaScriptProxy.new(self, method.to_s.camelize) JavaScriptProxy.new(self, method.to_s.camelize)
end end
@ -673,8 +628,11 @@ module ActionView
# Works like update_page but wraps the generated JavaScript in a <script> # Works like update_page but wraps the generated JavaScript in a <script>
# tag. Use this to include generated JavaScript in an ERb template. # tag. Use this to include generated JavaScript in an ERb template.
# See JavaScriptGenerator for more information. # See JavaScriptGenerator for more information.
def update_page_tag(&block) #
javascript_tag update_page(&block) # +html_options+ may be a hash of <script> attributes to be passed
# to ActionView::Helpers::JavaScriptHelper#javascript_tag.
def update_page_tag(html_options = {}, &block)
javascript_tag update_page(&block), html_options
end end
protected protected
@ -738,16 +696,16 @@ module ActionView
end end
private private
def method_missing(method, *arguments) def method_missing(method, *arguments, &block)
if method.to_s =~ /(.*)=$/ if method.to_s =~ /(.*)=$/
assign($1, arguments.first) assign($1, arguments.first)
else else
call("#{method.to_s.camelize(:lower)}", *arguments) call("#{method.to_s.camelize(:lower)}", *arguments, &block)
end end
end end
def call(function, *arguments) def call(function, *arguments, &block)
append_to_function_chain!("#{function}(#{@generator.send(:arguments_for_call, arguments)})") append_to_function_chain!("#{function}(#{@generator.send(:arguments_for_call, arguments, block)})")
self self
end end
@ -756,7 +714,7 @@ module ActionView
end end
def function_chain def function_chain
@function_chain ||= @generator.instance_variable_get("@lines") @function_chain ||= @generator.instance_variable_get(:@lines)
end end
def append_to_function_chain!(call) def append_to_function_chain!(call)
@ -771,6 +729,21 @@ module ActionView
super(generator, "$(#{id.to_json})") super(generator, "$(#{id.to_json})")
end end
# Allows access of element attributes through +attribute+. Examples:
#
# page['foo']['style'] # => $('foo').style;
# page['foo']['style']['color'] # => $('blank_slate').style.color;
# page['foo']['style']['color'] = 'red' # => $('blank_slate').style.color = 'red';
# page['foo']['style'].color = 'red' # => $('blank_slate').style.color = 'red';
def [](attribute)
append_to_function_chain!(attribute)
self
end
def []=(variable, value)
assign(variable, value)
end
def replace_html(*options_for_render) def replace_html(*options_for_render)
call 'update', @generator.send(:render, *options_for_render) call 'update', @generator.send(:render, *options_for_render)
end end
@ -779,8 +752,8 @@ module ActionView
call 'replace', @generator.send(:render, *options_for_render) call 'replace', @generator.send(:render, *options_for_render)
end end
def reload def reload(options_for_replace = {})
replace :partial => @id.to_s replace(options_for_replace.merge({ :partial => @id.to_s }))
end end
end end
@ -811,8 +784,8 @@ module ActionView
end end
class JavaScriptCollectionProxy < JavaScriptProxy #:nodoc: class JavaScriptCollectionProxy < JavaScriptProxy #:nodoc:
ENUMERABLE_METHODS_WITH_RETURN = [:all, :any, :collect, :map, :detect, :find, :find_all, :select, :max, :min, :partition, :reject, :sort_by] ENUMERABLE_METHODS_WITH_RETURN = [:all, :any, :collect, :map, :detect, :find, :find_all, :select, :max, :min, :partition, :reject, :sort_by] unless defined? ENUMERABLE_METHODS_WITH_RETURN
ENUMERABLE_METHODS = ENUMERABLE_METHODS_WITH_RETURN + [:each] ENUMERABLE_METHODS = ENUMERABLE_METHODS_WITH_RETURN + [:each] unless defined? ENUMERABLE_METHODS
attr_reader :generator attr_reader :generator
delegate :arguments_for_call, :to => :generator delegate :arguments_for_call, :to => :generator
@ -899,3 +872,5 @@ module ActionView
end end
end end
end end
require File.dirname(__FILE__) + '/javascript_helper'

View file

@ -69,6 +69,11 @@ module ActionView
# containing the values of the ids of elements the sortable consists # containing the values of the ids of elements the sortable consists
# of, in the current order. # of, in the current order.
# #
# Important: For this to work, the sortable elements must have id
# attributes in the form "string_identifier". For example, "item_1". Only
# the identifier part of the id attribute will be serialized.
#
#
# You can change the behaviour with various options, see # You can change the behaviour with various options, see
# http://script.aculo.us for more documentation. # http://script.aculo.us for more documentation.
def sortable_element(element_id, options = {}) def sortable_element(element_id, options = {})

View file

@ -2,39 +2,87 @@ require 'cgi'
require 'erb' require 'erb'
module ActionView module ActionView
module Helpers module Helpers #:nodoc:
# This is poor man's Builder for the rare cases where you need to programmatically make tags but can't use Builder. # Use these methods to generate HTML tags programmatically when you can't use
# a Builder. By default, they output XHTML compliant tags.
module TagHelper module TagHelper
include ERB::Util include ERB::Util
# Examples: # Returns an empty HTML tag of type +name+ which by default is XHTML
# * <tt>tag("br") => <br /></tt> # compliant. Setting +open+ to true will create an open tag compatible
# * <tt>tag("input", { "type" => "text"}) => <input type="text" /></tt> # with HTML 4.0 and below. Add HTML attributes by passing an attributes
# hash to +options+. For attributes with no value like (disabled and
# readonly), give it a value of true in the +options+ hash. You can use
# symbols or strings for the attribute names.
#
# tag("br")
# # => <br />
# tag("br", nil, true)
# # => <br>
# tag("input", { :type => 'text', :disabled => true })
# # => <input type="text" disabled="disabled" />
def tag(name, options = nil, open = false) def tag(name, options = nil, open = false)
"<#{name}#{tag_options(options.stringify_keys) if options}" + (open ? ">" : " />") "<#{name}#{tag_options(options) if options}" + (open ? ">" : " />")
end end
# Examples: # Returns an HTML block tag of type +name+ surrounding the +content+. Add
# * <tt>content_tag("p", "Hello world!") => <p>Hello world!</p></tt> # HTML attributes by passing an attributes hash to +options+. For attributes
# * <tt>content_tag("div", content_tag("p", "Hello world!"), "class" => "strong") => </tt> # with no value like (disabled and readonly), give it a value of true in
# <tt><div class="strong"><p>Hello world!</p></div></tt> # the +options+ hash. You can use symbols or strings for the attribute names.
def content_tag(name, content, options = nil) #
"<#{name}#{tag_options(options.stringify_keys) if options}>#{content}</#{name}>" # content_tag(:p, "Hello world!")
# # => <p>Hello world!</p>
# content_tag(:div, content_tag(:p, "Hello world!"), :class => "strong")
# # => <div class="strong"><p>Hello world!</p></div>
# content_tag("select", options, :multiple => true)
# # => <select multiple="multiple">...options...</select>
#
# Instead of passing the content as an argument, you can also use a block
# in which case, you pass your +options+ as the second parameter.
#
# <% content_tag :div, :class => "strong" do -%>
# Hello world!
# <% end -%>
# # => <div class="strong"><p>Hello world!</p></div>
def content_tag(name, content_or_options_with_block = nil, options = nil, &block)
if block_given?
options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash)
content = capture(&block)
concat(content_tag_string(name, content, options), block.binding)
else
content = content_or_options_with_block
content_tag_string(name, content, options)
end
end end
# Returns a CDATA section for the given +content+. CDATA sections # Returns a CDATA section with the given +content+. CDATA sections
# are used to escape blocks of text containing characters which would # are used to escape blocks of text containing characters which would
# otherwise be recognized as markup. CDATA sections begin with the string # otherwise be recognized as markup. CDATA sections begin with the string
# <tt>&lt;![CDATA[</tt> and end with (and may not contain) the string # <tt><![CDATA[</tt> and end with (and may not contain) the string <tt>]]></tt>.
# <tt>]]></tt>. #
# cdata_section("<hello world>")
# # => <![CDATA[<hello world>]]>
def cdata_section(content) def cdata_section(content)
"<![CDATA[#{content}]]>" "<![CDATA[#{content}]]>"
end end
# Returns the escaped +html+ without affecting existing escaped entities.
#
# escape_once("1 > 2 &amp; 3")
# # => "1 &lt; 2 &amp; 3"
def escape_once(html)
fix_double_escape(html_escape(html.to_s))
end
private private
def content_tag_string(name, content, options)
tag_options = options ? tag_options(options) : ""
"<#{name}#{tag_options}>#{content}</#{name}>"
end
def tag_options(options) def tag_options(options)
cleaned_options = convert_booleans(options.stringify_keys.reject {|key, value| value.nil?}) cleaned_options = convert_booleans(options.stringify_keys.reject {|key, value| value.nil?})
' ' + cleaned_options.map {|key, value| %(#{key}="#{html_escape(value.to_s)}")}.sort * ' ' unless cleaned_options.empty? ' ' + cleaned_options.map {|key, value| %(#{key}="#{escape_once(value)}")}.sort * ' ' unless cleaned_options.empty?
end end
def convert_booleans(options) def convert_booleans(options)
@ -45,6 +93,11 @@ module ActionView
def boolean_attribute(options, attribute) def boolean_attribute(options, attribute)
options[attribute] ? options[attribute] = attribute : options.delete(attribute) options[attribute] ? options[attribute] = attribute : options.delete(attribute)
end end
# Fix double-escaped entities, such as &amp;amp;, &amp;#123;, etc.
def fix_double_escape(escaped)
escaped.gsub(/&amp;([a-z]+|(#\d+));/i) { "&#{$1};" }
end
end end
end end
end end

View file

@ -2,65 +2,90 @@ require File.dirname(__FILE__) + '/tag_helper'
module ActionView module ActionView
module Helpers #:nodoc: module Helpers #:nodoc:
# Provides a set of methods for working with text strings that can help unburden the level of inline Ruby code in the # The TextHelper Module provides a set of methods for filtering, formatting
# templates. In the example below we iterate over a collection of posts provided to the template and print each title # and transforming strings that can reduce the amount of inline Ruby code in
# after making sure it doesn't run longer than 20 characters: # your views. These helper methods extend ActionView making them callable
# <% for post in @posts %> # within your template files as shown in the following example which truncates
# Title: <%= truncate(post.title, 20) %> # the title of each post to 10 characters.
#
# <% @posts.each do |post| %>
# # post == 'This is my title'
# Title: <%= truncate(post.title, 10) %>
# <% end %> # <% end %>
# => Title: This is my...
module TextHelper module TextHelper
# The regular puts and print are outlawed in eRuby. It's recommended to use the <%= "hello" %> form instead of print "hello". # The preferred method of outputting text in your views is to use the
# If you absolutely must use a method-based output, you can use concat. It's used like this: <% concat "hello", binding %>. Notice that # <%= "text" %> eRuby syntax. The regular _puts_ and _print_ methods
# it doesn't have an equal sign in front. Using <%= concat "hello" %> would result in a double hello. # do not operate as expected in an eRuby code block. If you absolutely must
# output text within a code block, you can use the concat method.
#
# <% concat "hello", binding %>
# is equivalent to using:
# <%= "hello" %>
def concat(string, binding) def concat(string, binding)
eval("_erbout", binding).concat(string) eval("_erbout", binding).concat(string)
end end
# Truncates +text+ to the length of +length+ and replaces the last three characters with the +truncate_string+ # If +text+ is longer than +length+, +text+ will be truncated to the length of
# if the +text+ is longer than +length+. # +length+ and the last three characters will be replaced with the +truncate_string+.
#
# truncate("Once upon a time in a world far far away", 14)
# => Once upon a...
def truncate(text, length = 30, truncate_string = "...") def truncate(text, length = 30, truncate_string = "...")
if text.nil? then return end if text.nil? then return end
l = length - truncate_string.length l = length - truncate_string.chars.length
if $KCODE == "NONE" text.chars.length > length ? text.chars[0...l] + truncate_string : text
text.length > length ? text[0...l] + truncate_string : text
else
chars = text.split(//)
chars.length > length ? chars[0...l].join + truncate_string : text
end
end end
# Highlights the +phrase+ where it is found in the +text+ by surrounding it like # Highlights +phrase+ everywhere it is found in +text+ by inserting it into
# <strong class="highlight">I'm a highlight phrase</strong>. The highlighter can be specialized by # a +highlighter+ string. The highlighter can be specialized by passing +highlighter+
# passing +highlighter+ as single-quoted string with \1 where the phrase is supposed to be inserted. # as a single-quoted string with \1 where the phrase is to be inserted.
# N.B.: The +phrase+ is sanitized to include only letters, digits, and spaces before use. #
# highlight('You searched for: rails', 'rails')
# => You searched for: <strong class="highlight">rails</strong>
def highlight(text, phrase, highlighter = '<strong class="highlight">\1</strong>') def highlight(text, phrase, highlighter = '<strong class="highlight">\1</strong>')
if phrase.blank? then return text end if phrase.blank? then return text end
text.gsub(/(#{Regexp.escape(phrase)})/i, highlighter) unless text.nil? text.gsub(/(#{Regexp.escape(phrase)})/i, highlighter) unless text.nil?
end end
# Extracts an excerpt from the +text+ surrounding the +phrase+ with a number of characters on each side determined # Extracts an excerpt from +text+ that matches the first instance of +phrase+.
# by +radius+. If the phrase isn't found, nil is returned. Ex: # The +radius+ expands the excerpt on each side of +phrase+ by the number of characters
# excerpt("hello my world", "my", 3) => "...lo my wo..." # defined in +radius+. If the excerpt radius overflows the beginning or end of the +text+,
# then the +excerpt_string+ will be prepended/appended accordingly. If the +phrase+
# isn't found, nil is returned.
#
# excerpt('This is an example', 'an', 5)
# => "...s is an examp..."
#
# excerpt('This is an example', 'is', 5)
# => "This is an..."
def excerpt(text, phrase, radius = 100, excerpt_string = "...") def excerpt(text, phrase, radius = 100, excerpt_string = "...")
if text.nil? || phrase.nil? then return end if text.nil? || phrase.nil? then return end
phrase = Regexp.escape(phrase) phrase = Regexp.escape(phrase)
if found_pos = text =~ /(#{phrase})/i if found_pos = text.chars =~ /(#{phrase})/i
start_pos = [ found_pos - radius, 0 ].max start_pos = [ found_pos - radius, 0 ].max
end_pos = [ found_pos + phrase.length + radius, text.length ].min end_pos = [ found_pos + phrase.chars.length + radius, text.chars.length ].min
prefix = start_pos > 0 ? excerpt_string : "" prefix = start_pos > 0 ? excerpt_string : ""
postfix = end_pos < text.length ? excerpt_string : "" postfix = end_pos < text.chars.length ? excerpt_string : ""
prefix + text[start_pos..end_pos].strip + postfix prefix + text.chars[start_pos..end_pos].strip + postfix
else else
nil nil
end end
end end
# Attempts to pluralize the +singular+ word unless +count+ is 1. See source for pluralization rules. # Attempts to pluralize the +singular+ word unless +count+ is 1. If +plural+
# is supplied, it will use that when count is > 1, if the ActiveSupport Inflector
# is loaded, it will use the Inflector to determine the plural form, otherwise
# it will just add an 's' to the +singular+ word.
#
# pluralize(1, 'person') => 1 person
# pluralize(2, 'person') => 2 people
# pluralize(3, 'person', 'users') => 3 users
def pluralize(count, singular, plural = nil) def pluralize(count, singular, plural = nil)
"#{count} " + if count == 1 "#{count} " + if count == 1 || count == '1'
singular singular
elsif plural elsif plural
plural plural
@ -71,7 +96,11 @@ module ActionView
end end
end end
# Word wrap long lines to line_width. # Wraps the +text+ into lines no longer than +line_width+ width. This method
# breaks on the first whitespace character that does not exceed +line_width+.
#
# word_wrap('Once upon a time', 4)
# => Once\nupon\na\ntime
def word_wrap(text, line_width = 80) def word_wrap(text, line_width = 80)
text.gsub(/\n/, "\n\n").gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1\n").strip text.gsub(/\n/, "\n\n").gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1\n").strip
end end
@ -79,8 +108,9 @@ module ActionView
begin begin
require_library_or_gem "redcloth" unless Object.const_defined?(:RedCloth) require_library_or_gem "redcloth" unless Object.const_defined?(:RedCloth)
# Returns the text with all the Textile codes turned into HTML-tags. # Returns the text with all the Textile codes turned into HTML tags.
# <i>This method is only available if RedCloth can be required</i>. # <i>This method is only available if RedCloth[http://whytheluckystiff.net/ruby/redcloth/]
# is available</i>.
def textilize(text) def textilize(text)
if text.blank? if text.blank?
"" ""
@ -91,8 +121,10 @@ module ActionView
end end
end end
# Returns the text with all the Textile codes turned into HTML-tags, but without the regular bounding <p> tag. # Returns the text with all the Textile codes turned into HTML tags,
# <i>This method is only available if RedCloth can be required</i>. # but without the bounding <p> tag that RedCloth adds.
# <i>This method is only available if RedCloth[http://whytheluckystiff.net/ruby/redcloth/]
# is available</i>.
def textilize_without_paragraph(text) def textilize_without_paragraph(text)
textiled = textilize(text) textiled = textilize(text)
if textiled[0..2] == "<p>" then textiled = textiled[3..-1] end if textiled[0..2] == "<p>" then textiled = textiled[3..-1] end
@ -106,8 +138,9 @@ module ActionView
begin begin
require_library_or_gem "bluecloth" unless Object.const_defined?(:BlueCloth) require_library_or_gem "bluecloth" unless Object.const_defined?(:BlueCloth)
# Returns the text with all the Markdown codes turned into HTML-tags. # Returns the text with all the Markdown codes turned into HTML tags.
# <i>This method is only available if BlueCloth can be required</i>. # <i>This method is only available if BlueCloth[http://www.deveiate.org/projects/BlueCloth]
# is available</i>.
def markdown(text) def markdown(text)
text.blank? ? "" : BlueCloth.new(text).to_html text.blank? ? "" : BlueCloth.new(text).to_html
end end
@ -115,29 +148,30 @@ module ActionView
# We can't really help what's not there # We can't really help what's not there
end end
# Returns +text+ transformed into HTML using very simple formatting rules # Returns +text+ transformed into HTML using simple formatting rules.
# Surrounds paragraphs with <tt><p></tt> tags, and converts line breaks into <tt><br/></tt> # Two or more consecutive newlines(<tt>\n\n</tt>) are considered as a
# Two consecutive newlines(<tt>\n\n</tt>) are considered as a paragraph, one newline (<tt>\n</tt>) is # paragraph and wrapped in <tt><p></tt> tags. One newline (<tt>\n</tt>) is
# considered a linebreak, three or more consecutive newlines are turned into two newlines # considered as a linebreak and a <tt><br /></tt> tag is appended. This
# method does not remove the newlines from the +text+.
def simple_format(text) def simple_format(text)
text.gsub!(/(\r\n|\n|\r)/, "\n") # lets make them newlines crossplatform content_tag 'p', text.to_s.
text.gsub!(/\n\n+/, "\n\n") # zap dupes gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
text.gsub!(/\n\n/, '</p>\0<p>') # turn two newlines into paragraph gsub(/\n\n+/, "</p>\n\n<p>"). # 2+ newline -> paragraph
text.gsub!(/([^\n])(\n)([^\n])/, '\1\2<br />\3') # turn single newline into <br /> gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
content_tag("p", text)
end end
# Turns all urls and email addresses into clickable links. The +link+ parameter can limit what should be linked. # Turns all urls and email addresses into clickable links. The +link+ parameter
# Options are <tt>:all</tt> (default), <tt>:email_addresses</tt>, and <tt>:urls</tt>. # will limit what should be linked. You can add html attributes to the links using
# +href_options+. Options for +link+ are <tt>:all</tt> (default),
# <tt>:email_addresses</tt>, and <tt>:urls</tt>.
# #
# Example: # auto_link("Go to http://www.rubyonrails.org and say hello to david@loudthinking.com") =>
# auto_link("Go to http://www.rubyonrails.com and say hello to david@loudthinking.com") => # Go to <a href="http://www.rubyonrails.org">http://www.rubyonrails.org</a> and
# Go to <a href="http://www.rubyonrails.com">http://www.rubyonrails.com</a> and
# say hello to <a href="mailto:david@loudthinking.com">david@loudthinking.com</a> # say hello to <a href="mailto:david@loudthinking.com">david@loudthinking.com</a>
# #
# If a block is given, each url and email address is yielded and the # If a block is given, each url and email address is yielded and the
# result is used as the link text. Example: # result is used as the link text.
#
# auto_link(post.body, :all, :target => '_blank') do |text| # auto_link(post.body, :all, :target => '_blank') do |text|
# truncate(text, 15) # truncate(text, 15)
# end # end
@ -150,9 +184,12 @@ module ActionView
end end
end end
# Turns all links into words, like "<a href="something">else</a>" to "else". # Strips link tags from +text+ leaving just the link label.
#
# strip_links('<a href="http://www.rubyonrails.org">Ruby on Rails</a>')
# => Ruby on Rails
def strip_links(text) def strip_links(text)
text.gsub(/<a.*>(.*)<\/a>/m, '\1') text.gsub(/<a\b.*?>(.*?)<\/a>/mi, '\1')
end end
# Try to require the html-scanner library # Try to require the html-scanner library
@ -161,22 +198,26 @@ module ActionView
require 'html/node' require 'html/node'
rescue LoadError rescue LoadError
# if there isn't a copy installed, use the vendor version in # if there isn't a copy installed, use the vendor version in
# action controller # ActionController
$:.unshift File.join(File.dirname(__FILE__), "..", "..", $:.unshift File.join(File.dirname(__FILE__), "..", "..",
"action_controller", "vendor", "html-scanner") "action_controller", "vendor", "html-scanner")
require 'html/tokenizer' require 'html/tokenizer'
require 'html/node' require 'html/node'
end end
VERBOTEN_TAGS = %w(form script) unless defined?(VERBOTEN_TAGS) VERBOTEN_TAGS = %w(form script plaintext) unless defined?(VERBOTEN_TAGS)
VERBOTEN_ATTRS = /^on/i unless defined?(VERBOTEN_ATTRS) VERBOTEN_ATTRS = /^on/i unless defined?(VERBOTEN_ATTRS)
# Sanitizes the given HTML by making form and script tags into regular # Sanitizes the +html+ by converting <form> and <script> tags into regular
# text, and removing all "onxxx" attributes (so that arbitrary Javascript # text, and removing all "onxxx" attributes (so that arbitrary Javascript
# cannot be executed). Also removes href attributes that start with # cannot be executed). It also removes href= and src= attributes that start with
# "javascript:". # "javascript:". You can modify what gets sanitized by defining VERBOTEN_TAGS
# and VERBOTEN_ATTRS before this Module is loaded.
# #
# Returns the sanitized text. # sanitize('<script> do_nasty_stuff() </script>')
# => &lt;script> do_nasty_stuff() &lt;/script>
# sanitize('<a href="javascript: sucker();">Click here for $100</a>')
# => <a>Click here for $100</a>
def sanitize(html) def sanitize(html)
# only do this if absolutely necessary # only do this if absolutely necessary
if html.index("<") if html.index("<")
@ -192,8 +233,8 @@ module ActionView
else else
if node.closing != :close if node.closing != :close
node.attributes.delete_if { |attr,v| attr =~ VERBOTEN_ATTRS } node.attributes.delete_if { |attr,v| attr =~ VERBOTEN_ATTRS }
if node.attributes["href"] =~ /^javascript:/i %w(href src).each do |attr|
node.attributes.delete "href" node.attributes.delete attr if node.attributes[attr] =~ /^javascript:/i
end end
end end
node.to_s node.to_s
@ -209,11 +250,11 @@ module ActionView
html html
end end
# Strips all HTML tags from the input, including comments. This uses the html-scanner # Strips all HTML tags from the +html+, including comments. This uses the
# tokenizer and so it's HTML parsing ability is limited by that of html-scanner. # html-scanner tokenizer and so its HTML parsing ability is limited by
# # that of html-scanner.
# Returns the tag free text.
def strip_tags(html) def strip_tags(html)
return html if html.blank?
if html.index("<") if html.index("<")
text = "" text = ""
tokenizer = HTML::Tokenizer.new(html) tokenizer = HTML::Tokenizer.new(html)
@ -231,32 +272,33 @@ module ActionView
end end
end end
# Returns a Cycle object whose to_s value cycles through items of an # Creates a Cycle object whose _to_s_ method cycles through elements of an
# array every time it is called. This can be used to alternate classes # array every time it is called. This can be used for example, to alternate
# for table rows: # classes for table rows:
# #
# <%- for item in @items do -%> # <% @items.each do |item| %>
# <tr class="<%= cycle("even", "odd") %>"> # <tr class="<%= cycle("even", "odd") -%>">
# ... use item ... # <td>item</td>
# </tr> # </tr>
# <%- end -%> # <% end %>
# #
# You can use named cycles to prevent clashes in nested loops. You'll # You can use named cycles to allow nesting in loops. Passing a Hash as
# have to reset the inner cycle, manually: # the last parameter with a <tt>:name</tt> key will create a named cycle.
# You can manually reset a cycle by calling reset_cycle and passing the
# name of the cycle.
# #
# <%- for item in @items do -%> # <% @items.each do |item| %>
# <tr class="<%= cycle("even", "odd", :name => "row_class") # <tr class="<%= cycle("even", "odd", :name => "row_class")
# <td> # <td>
# <%- for value in item.values do -%> # <% item.values.each do |value| %>
# <span style="color:'<%= cycle("red", "green", "blue" # <span style="color:<%= cycle("red", "green", "blue", :name => "colors") -%>">
# :name => "colors") %>'"> # value
# item
# </span> # </span>
# <%- end -%> # <% end %>
# <%- reset_cycle("colors") -%> # <% reset_cycle("colors") %>
# </td> # </td>
# </tr> # </tr>
# <%- end -%> # <% end %>
def cycle(first_value, *values) def cycle(first_value, *values)
if (values.last.instance_of? Hash) if (values.last.instance_of? Hash)
params = values.pop params = values.pop
@ -273,12 +315,11 @@ module ActionView
return cycle.to_s return cycle.to_s
end end
# Resets a cycle so that it starts from the first element in the array # Resets a cycle so that it starts from the first element the next time
# the next time it is used. # it is called. Pass in +name+ to reset a named cycle.
def reset_cycle(name = "default") def reset_cycle(name = "default")
cycle = get_cycle(name) cycle = get_cycle(name)
return if cycle.nil? cycle.reset unless cycle.nil?
cycle.reset
end end
class Cycle #:nodoc: class Cycle #:nodoc:
@ -305,42 +346,42 @@ module ActionView
# guaranteed to be reset every time a page is rendered, so it # guaranteed to be reset every time a page is rendered, so it
# uses an instance variable of ActionView::Base. # uses an instance variable of ActionView::Base.
def get_cycle(name) def get_cycle(name)
@_cycles = Hash.new if @_cycles.nil? @_cycles = Hash.new unless defined?(@_cycles)
return @_cycles[name] return @_cycles[name]
end end
def set_cycle(name, cycle_object) def set_cycle(name, cycle_object)
@_cycles = Hash.new if @_cycles.nil? @_cycles = Hash.new unless defined?(@_cycles)
@_cycles[name] = cycle_object @_cycles[name] = cycle_object
end end
AUTO_LINK_RE = / AUTO_LINK_RE = %r{
( # leading text ( # leading text
<\w+.*?>| # leading HTML tag, or <\w+.*?>| # leading HTML tag, or
[^=!:'"\/]| # leading punctuation, or [^=!:'"/]| # leading punctuation, or
^ # beginning of line ^ # beginning of line
) )
( (
(?:http[s]?:\/\/)| # protocol spec, or (?:https?://)| # protocol spec, or
(?:www\.) # www.* (?:www\.) # www.*
) )
( (
([\w]+:?[=?&\/.-]?)* # url segment [-\w]+ # subdomain or domain
\w+[\/]? # url tail (?:\.[-\w]+)* # remaining subdomains or domain
(?:\#\w*)? # trailing anchor (?::\d+)? # port
(?:/(?:(?:[~\w\+%-]|(?:[,.;:][^\s$]))+)?)* # path
(?:\?[\w\+%&=.;-]+)? # query string
(?:\#[\w\-]*)? # trailing anchor
) )
([[:punct:]]|\s|<|$) # trailing text ([[:punct:]]|\s|<|$) # trailing text
/x unless const_defined?(:AUTO_LINK_RE) }x unless const_defined?(:AUTO_LINK_RE)
# Turns all urls into clickable links. If a block is given, each url # Turns all urls into clickable links. If a block is given, each url
# is yielded and the result is used as the link text. Example: # is yielded and the result is used as the link text.
# auto_link_urls(post.body, :all, :target => '_blank') do |text|
# truncate(text, 15)
# end
def auto_link_urls(text, href_options = {}) def auto_link_urls(text, href_options = {})
extra_options = tag_options(href_options.stringify_keys) || "" extra_options = tag_options(href_options.stringify_keys) || ""
text.gsub(AUTO_LINK_RE) do text.gsub(AUTO_LINK_RE) do
all, a, b, c, d = $&, $1, $2, $3, $5 all, a, b, c, d = $&, $1, $2, $3, $4
if a =~ /<a\s/i # don't replace URL's that are already linked if a =~ /<a\s/i # don't replace URL's that are already linked
all all
else else
@ -353,10 +394,6 @@ module ActionView
# Turns all email addresses into clickable links. If a block is given, # Turns all email addresses into clickable links. If a block is given,
# each email is yielded and the result is used as the link text. # each email is yielded and the result is used as the link text.
# Example:
# auto_link_email_addresses(post.body) do |text|
# truncate(text, 15)
# end
def auto_link_email_addresses(text) def auto_link_email_addresses(text)
text.gsub(/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do text.gsub(/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do
text = $1 text = $1

View file

@ -1,22 +1,21 @@
require File.dirname(__FILE__) + '/javascript_helper' require File.dirname(__FILE__) + '/javascript_helper'
module ActionView module ActionView
module Helpers module Helpers #:nodoc:
# Provides a set of methods for making easy links and getting urls that depend on the controller and action. This means that # Provides a set of methods for making easy links and getting urls that
# you can use the same format for links in the views that you do in the controller. The different methods are even named # depend on the controller and action. This means that you can use the
# synchronously, so link_to uses that same url as is generated by url_for, which again is the same url used for # same format for links in the views that you do in the controller.
# redirection in redirect_to.
module UrlHelper module UrlHelper
include JavaScriptHelper include JavaScriptHelper
# Returns the URL for the set of +options+ provided. This takes the same options # Returns the URL for the set of +options+ provided. This takes the
# as url_for. For a list, see the documentation for ActionController::Base#url_for. # same options as url_for in action controller. For a list, see the
# Note that it'll set :only_path => true so you'll get /controller/action instead of the # documentation for ActionController::Base#url_for. Note that it'll
# http://example.com/controller/action part (makes it harder to parse httpd log files) # set :only_path => true so you'll get the relative /controller/action
# # instead of the fully qualified http://example.com/controller/action.
# When called from a view, url_for returns an HTML escaped url. If you need an unescaped
# url, pass :escape => false to url_for.
# #
# When called from a view, url_for returns an HTML escaped url. If you
# need an unescaped url, pass :escape => false in the +options+.
def url_for(options = {}, *parameters_for_method_reference) def url_for(options = {}, *parameters_for_method_reference)
if options.kind_of? Hash if options.kind_of? Hash
options = { :only_path => true }.update(options.symbolize_keys) options = { :only_path => true }.update(options.symbolize_keys)
@ -24,30 +23,46 @@ module ActionView
else else
escape = true escape = true
end end
url = @controller.send(:url_for, options, *parameters_for_method_reference) url = @controller.send(:url_for, options, *parameters_for_method_reference)
escape ? html_escape(url) : url escape ? html_escape(url) : url
end end
# Creates a link tag of the given +name+ using an URL created by the set of +options+. See the valid options in # Creates a link tag of the given +name+ using a URL created by the set
# the documentation for ActionController::Base#url_for. It's also possible to pass a string instead of an options hash to # of +options+. See the valid options in the documentation for
# get a link tag that just points without consideration. If nil is passed as a name, the link itself will become the name. # ActionController::Base#url_for. It's also possible to pass a string instead
# of an options hash to get a link tag that uses the value of the string as the
# href for the link. If nil is passed as a name, the link itself will become
# the name.
# #
# The html_options has three special features. One for creating javascript confirm alerts where if you pass :confirm => 'Are you sure?', # The +html_options+ will accept a hash of html attributes for the link tag.
# the link will be guarded with a JS popup asking that question. If the user accepts, the link is processed, otherwise not. # It also accepts 3 modifiers that specialize the link behavior.
# #
# Another for creating a popup window, which is done by either passing :popup with true or the options of the window in # * <tt>:confirm => 'question?'</tt>: This will add a JavaScript confirm
# Javascript form. # prompt with the question specified. If the user accepts, the link is
# processed normally, otherwise no action is taken.
# * <tt>:popup => true || array of window options</tt>: This will force the
# link to open in a popup window. By passing true, a default browser window
# will be opened with the URL. You can also specify an array of options
# that are passed-thru to JavaScripts window.open method.
# * <tt>:method => symbol of HTTP verb</tt>: This modifier will dynamically
# create an HTML form and immediately submit the form for processing using
# the HTTP verb specified. Useful for having links perform a POST operation
# in dangerous actions like deleting a record (which search bots can follow
# while spidering your site). Supported verbs are :post, :delete and :put.
# Note that if the user has JavaScript disabled, the request will fall back
# to using GET. If you are relying on the POST behavior, your should check
# for it in your controllers action by using the request objects methods
# for post?, delete? or put?.
# #
# And a third for making the link do a POST request (instead of the regular GET) through a dynamically added form element that # You can mix and match the +html_options+ with the exception of
# is instantly submitted. Note that if the user has turned off Javascript, the request will fall back on the GET. So its # :popup and :method which will raise an ActionView::ActionViewError
# your responsibility to determine what the action should be once it arrives at the controller. The POST form is turned on by # exception.
# passing :post as true. Note, it's not possible to use POST requests and popup targets at the same time (an exception will be thrown).
# #
# Examples: # link_to "Visit Other Site", "http://www.rubyonrails.org/", :confirm => "Are you sure?"
# link_to "Delete this page", { :action => "destroy", :id => @page.id }, :confirm => "Are you sure?"
# link_to "Help", { :action => "help" }, :popup => true # link_to "Help", { :action => "help" }, :popup => true
# link_to "Busy loop", { :action => "busy" }, :popup => ['new_window', 'height=300,width=600'] # link_to "View Image", { :action => "view" }, :popup => ['new_window_name', 'height=300,width=600']
# link_to "Destroy account", { :action => "destroy" }, :confirm => "Are you sure?", :post => true # link_to "Delete Image", { :action => "delete", :id => @image.id }, :confirm => "Are you sure?", :method => :delete
def link_to(name, options = {}, html_options = nil, *parameters_for_method_reference) def link_to(name, options = {}, html_options = nil, *parameters_for_method_reference)
if html_options if html_options
html_options = html_options.stringify_keys html_options = html_options.stringify_keys
@ -56,76 +71,77 @@ module ActionView
else else
tag_options = nil tag_options = nil
end end
url = options.is_a?(String) ? options : self.url_for(options, *parameters_for_method_reference) url = options.is_a?(String) ? options : self.url_for(options, *parameters_for_method_reference)
"<a href=\"#{url}\"#{tag_options}>#{name || url}</a>" "<a href=\"#{url}\"#{tag_options}>#{name || url}</a>"
end end
# Generates a form containing a sole button that submits to the # Generates a form containing a single button that submits to the URL created
# URL given by _options_. Use this method instead of +link_to+ # by the set of +options+. This is the safest method to ensure links that
# for actions that do not have the safe HTTP GET semantics # cause changes to your data are not triggered by search bots or accelerators.
# implied by using a hypertext link. # If the HTML button does not work with your layout, you can also consider
# using the link_to method with the <tt>:method</tt> modifier as described in
# the link_to documentation.
# #
# The parameters are the same as for +link_to+. Any _html_options_ # The generated FORM element has a class name of <tt>button-to</tt>
# that you pass will be applied to the inner +input+ element. # to allow styling of the form itself and its children. You can control
# In particular, pass # the form submission and input element behavior using +html_options+.
# This method accepts the <tt>:method</tt> and <tt>:confirm</tt> modifiers
# described in the link_to documentation. If no <tt>:method</tt> modifier
# is given, it will default to performing a POST operation. You can also
# disable the button by passing <tt>:disabled => true</tt> in +html_options+.
# #
# :disabled => true/false # button_to "New", :action => "new"
# #
# as part of _html_options_ to control whether the button is # Generates the following HTML:
# disabled. The generated form element is given the class
# 'button-to', to which you can attach CSS styles for display
# purposes.
# #
# Example 1: # <form method="post" action="/controller/new" class="button-to">
# # <div><input value="New" type="submit" /></div>
# # inside of controller for "feeds"
# button_to "Edit", :action => 'edit', :id => 3
#
# Generates the following HTML (sans formatting):
#
# <form method="post" action="/feeds/edit/3" class="button-to">
# <div><input value="Edit" type="submit" /></div>
# </form> # </form>
# #
# Example 2: # If you are using RESTful routes, you can pass the <tt>:method</tt>
# to change the HTTP verb used to submit the form.
# #
# button_to "Destroy", { :action => 'destroy', :id => 3 }, # button_to "Delete Image", { :action => "delete", :id => @image.id },
# :confirm => "Are you sure?" # :confirm => "Are you sure?", :method => :delete
# #
# Generates the following HTML (sans formatting): # Which generates the following HTML:
# #
# <form method="post" action="/feeds/destroy/3" class="button-to"> # <form method="post" action="/images/delete/1" class="button-to">
# <div><input onclick="return confirm('Are you sure?');" # <div>
# value="Destroy" type="submit" /> # <input type="hidden" name="_method" value="delete" />
# <input onclick="return confirm('Are you sure?');"
# value="Delete" type="submit" />
# </div> # </div>
# </form> # </form>
# def button_to(name, options = {}, html_options = {})
# *NOTE*: This method generates HTML code that represents a form. html_options = html_options.stringify_keys
# Forms are "block" content, which means that you should not try to
# insert them into your HTML where only inline content is expected.
# For example, you can legally insert a form inside of a +div+ or
# +td+ element or in between +p+ elements, but not in the middle of
# a run of text, nor can you place a form within another form.
# (Bottom line: Always validate your HTML before going public.)
def button_to(name, options = {}, html_options = nil)
html_options = (html_options || {}).stringify_keys
convert_boolean_attributes!(html_options, %w( disabled )) convert_boolean_attributes!(html_options, %w( disabled ))
method_tag = ''
if (method = html_options.delete('method')) && %w{put delete}.include?(method.to_s)
method_tag = tag('input', :type => 'hidden', :name => '_method', :value => method.to_s)
end
form_method = method.to_s == 'get' ? 'get' : 'post'
if confirm = html_options.delete("confirm") if confirm = html_options.delete("confirm")
html_options["onclick"] = "return #{confirm_javascript_function(confirm)};" html_options["onclick"] = "return #{confirm_javascript_function(confirm)};"
end end
url = options.is_a?(String) ? options : url_for(options) url = options.is_a?(String) ? options : self.url_for(options)
name ||= url name ||= url
html_options.merge!("type" => "submit", "value" => name) html_options.merge!("type" => "submit", "value" => name)
"<form method=\"post\" action=\"#{h url}\" class=\"button-to\"><div>" + "<form method=\"#{form_method}\" action=\"#{escape_once url}\" class=\"button-to\"><div>" +
tag("input", html_options) + "</div></form>" method_tag + tag("input", html_options) + "</div></form>"
end end
# This tag is deprecated. Combine the link_to and AssetTagHelper::image_tag yourself instead, like: # DEPRECATED. It is reccommended to use the AssetTagHelper::image_tag within
# a link_to method to generate a linked image.
#
# link_to(image_tag("rss", :size => "30x45", :border => 0), "http://www.example.com") # link_to(image_tag("rss", :size => "30x45", :border => 0), "http://www.example.com")
def link_image_to(src, options = {}, html_options = {}, *parameters_for_method_reference) def link_image_to(src, options = {}, html_options = {}, *parameters_for_method_reference)
image_options = { "src" => src.include?("/") ? src : "/images/#{src}" } image_options = { "src" => src.include?("/") ? src : "/images/#{src}" }
@ -157,18 +173,42 @@ module ActionView
link_to(tag("img", image_options), options, html_options, *parameters_for_method_reference) link_to(tag("img", image_options), options, html_options, *parameters_for_method_reference)
end end
alias_method :link_to_image, :link_image_to # deprecated name alias_method :link_to_image, :link_image_to
deprecate :link_to_image => "use link_to(image_tag(...), url)",
:link_image_to => "use link_to(image_tag(...), url)"
# Creates a link tag of the given +name+ using an URL created by the set of +options+, unless the current # Creates a link tag of the given +name+ using a URL created by the set of
# request uri is the same as the link's, in which case only the name is returned (or the # +options+ unless the current request uri is the same as the links, in
# given block is yielded, if one exists). This is useful for creating link bars where you don't want to link # which case only the name is returned (or the given block is yielded, if
# to the page currently being viewed. # one exists). Refer to the documentation for link_to_unless for block usage.
#
# <ul id="navbar">
# <li><%= link_to_unless_current("Home", { :action => "index" }) %></li>
# <li><%= link_to_unless_current("About Us", { :action => "about" }) %></li>
# </ul>
#
# This will render the following HTML when on the about us page:
#
# <ul id="navbar">
# <li><a href="/controller/index">Home</a></li>
# <li>About Us</li>
# </ul>
def link_to_unless_current(name, options = {}, html_options = {}, *parameters_for_method_reference, &block) def link_to_unless_current(name, options = {}, html_options = {}, *parameters_for_method_reference, &block)
link_to_unless current_page?(options), name, options, html_options, *parameters_for_method_reference, &block link_to_unless current_page?(options), name, options, html_options, *parameters_for_method_reference, &block
end end
# Create a link tag of the given +name+ using an URL created by the set of +options+, unless +condition+ # Creates a link tag of the given +name+ using a URL created by the set of
# is true, in which case only the name is returned (or the given block is yielded, if one exists). # +options+ unless +condition+ is true, in which case only the name is
# returned. To specialize the default behavior, you can pass a block that
# accepts the name or the full argument list for link_to_unless (see the example).
#
# <%= link_to_unless(@current_user.nil?, "Reply", { :action => "reply" }) %>
#
# This example uses a block to modify the link if the condition isn't met.
#
# <%= link_to_unless(@current_user.nil?, "Reply", { :action => "reply" }) do |name|
# link_to(name, { :controller => "accounts", :action => "signup" })
# end %>
def link_to_unless(condition, name, options = {}, html_options = {}, *parameters_for_method_reference, &block) def link_to_unless(condition, name, options = {}, html_options = {}, *parameters_for_method_reference, &block)
if condition if condition
if block_given? if block_given?
@ -181,30 +221,56 @@ module ActionView
end end
end end
# Create a link tag of the given +name+ using an URL created by the set of +options+, if +condition+ # Creates a link tag of the given +name+ using a URL created by the set of
# is true, in which case only the name is returned (or the given block is yielded, if one exists). # +options+ if +condition+ is true, in which case only the name is
# returned. To specialize the default behavior, you can pass a block that
# accepts the name or the full argument list for link_to_unless (see the examples
# in link_to_unless).
def link_to_if(condition, name, options = {}, html_options = {}, *parameters_for_method_reference, &block) def link_to_if(condition, name, options = {}, html_options = {}, *parameters_for_method_reference, &block)
link_to_unless !condition, name, options, html_options, *parameters_for_method_reference, &block link_to_unless !condition, name, options, html_options, *parameters_for_method_reference, &block
end end
# Creates a link tag for starting an email to the specified <tt>email_address</tt>, which is also used as the name of the # Creates a mailto link tag to the specified +email_address+, which is
# link unless +name+ is specified. Additional HTML options, such as class or id, can be passed in the <tt>html_options</tt> hash. # also used as the name of the link unless +name+ is specified. Additional
# html attributes for the link can be passed in +html_options+.
#
# mail_to has several methods for hindering email harvestors and customizing
# the email itself by passing special keys to +html_options+.
#
# Special HTML Options:
#
# * <tt>:encode</tt> - This key will accept the strings "javascript" or "hex".
# Passing "javascript" will dynamically create and encode the mailto: link then
# eval it into the DOM of the page. This method will not show the link on
# the page if the user has JavaScript disabled. Passing "hex" will hex
# encode the +email_address+ before outputting the mailto: link.
# * <tt>:replace_at</tt> - When the link +name+ isn't provided, the
# +email_address+ is used for the link label. You can use this option to
# obfuscate the +email_address+ by substituting the @ sign with the string
# given as the value.
# * <tt>:replace_dot</tt> - When the link +name+ isn't provided, the
# +email_address+ is used for the link label. You can use this option to
# obfuscate the +email_address+ by substituting the . in the email with the
# string given as the value.
# * <tt>:subject</tt> - Preset the subject line of the email.
# * <tt>:body</tt> - Preset the body of the email.
# * <tt>:cc</tt> - Carbon Copy addition recipients on the email.
# * <tt>:bcc</tt> - Blind Carbon Copy additional recipients on the email.
# #
# You can also make it difficult for spiders to harvest email address by obfuscating them.
# Examples: # Examples:
# mail_to "me@domain.com" # => <a href="mailto:me@domain.com">me@domain.com</a>
# mail_to "me@domain.com", "My email", :encode => "javascript" # => # mail_to "me@domain.com", "My email", :encode => "javascript" # =>
# <script type="text/javascript" language="javascript">eval(unescape('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%22%6d%61%69%6c%74%6f%3a%6d%65%40%64%6f%6d%61%69%6e%2e%63%6f%6d%22%3e%4d%79%20%65%6d%61%69%6c%3c%2f%61%3e%27%29%3b'))</script> # <script type="text/javascript">eval(unescape('%64%6f%63...%6d%65%6e'))</script>
# #
# mail_to "me@domain.com", "My email", :encode => "hex" # => # mail_to "me@domain.com", "My email", :encode => "hex" # =>
# <a href="mailto:%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d">My email</a> # <a href="mailto:%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d">My email</a>
# #
# You can also specify the cc address, bcc address, subject, and body parts of the message header to create a complex e-mail using the # mail_to "me@domain.com", nil, :replace_at => "_at_", :replace_dot => "_dot_", :class => "email" # =>
# corresponding +cc+, +bcc+, +subject+, and +body+ <tt>html_options</tt> keys. Each of these options are URI escaped and then appended to # <a href="mailto:me@domain.com" class="email">me_at_domain_dot_com</a>
# the <tt>email_address</tt> before being output. <b>Be aware that javascript keywords will not be escaped and may break this feature #
# when encoding with javascript.</b> # mail_to "me@domain.com", "My email", :cc => "ccaddress@domain.com",
# Examples: # :subject => "This is an example email" # =>
# mail_to "me@domain.com", "My email", :cc => "ccaddress@domain.com", :bcc => "bccaddress@domain.com", :subject => "This is an example email", :body => "This is the body of the message." # => # <a href="mailto:me@domain.com?cc=ccaddress@domain.com&subject=This%20is%20an%20example%20email">My email</a>
# <a href="mailto:me@domain.com?cc="ccaddress@domain.com"&bcc="bccaddress@domain.com"&body="This%20is%20the%20body%20of%20the%20message."&subject="This%20is%20an%20example%20email">My email</a>
def mail_to(email_address, name = nil, html_options = {}) def mail_to(email_address, name = nil, html_options = {})
html_options = html_options.stringify_keys html_options = html_options.stringify_keys
encode = html_options.delete("encode") encode = html_options.delete("encode")
@ -218,17 +284,19 @@ module ActionView
extras << "subject=#{CGI.escape(subject).gsub("+", "%20")}&" unless subject.nil? extras << "subject=#{CGI.escape(subject).gsub("+", "%20")}&" unless subject.nil?
extras = "?" << extras.gsub!(/&?$/,"") unless extras.empty? extras = "?" << extras.gsub!(/&?$/,"") unless extras.empty?
email_address = email_address.to_s
email_address_obfuscated = email_address.dup email_address_obfuscated = email_address.dup
email_address_obfuscated.gsub!(/@/, html_options.delete("replace_at")) if html_options.has_key?("replace_at") email_address_obfuscated.gsub!(/@/, html_options.delete("replace_at")) if html_options.has_key?("replace_at")
email_address_obfuscated.gsub!(/\./, html_options.delete("replace_dot")) if html_options.has_key?("replace_dot") email_address_obfuscated.gsub!(/\./, html_options.delete("replace_dot")) if html_options.has_key?("replace_dot")
if encode == 'javascript' if encode == "javascript"
tmp = "document.write('#{content_tag("a", name || email_address, html_options.merge({ "href" => "mailto:"+email_address.to_s+extras }))}');" tmp = "document.write('#{content_tag("a", name || email_address, html_options.merge({ "href" => "mailto:"+email_address+extras }))}');"
for i in 0...tmp.length for i in 0...tmp.length
string << sprintf("%%%x",tmp[i]) string << sprintf("%%%x",tmp[i])
end end
"<script type=\"text/javascript\">eval(unescape('#{string}'))</script>" "<script type=\"text/javascript\">eval(unescape('#{string}'))</script>"
elsif encode == 'hex' elsif encode == "hex"
for i in 0...email_address.length for i in 0...email_address.length
if email_address[i,1] =~ /\w/ if email_address[i,1] =~ /\w/
string << sprintf("%%%x",email_address[i]) string << sprintf("%%%x",email_address[i])
@ -242,26 +310,42 @@ module ActionView
end end
end end
# Returns true if the current page uri is generated by the options passed (in url_for format). # True if the current request uri was generated by the given +options+.
def current_page?(options) def current_page?(options)
CGI.escapeHTML(url_for(options)) == @controller.request.request_uri url_string = CGI.escapeHTML(url_for(options))
request = @controller.request
if url_string =~ /^\w+:\/\//
url_string == "#{request.protocol}#{request.host_with_port}#{request.request_uri}"
else
url_string == request.request_uri
end
end end
private private
def convert_options_to_javascript!(html_options) def convert_options_to_javascript!(html_options)
confirm, popup, post = html_options.delete("confirm"), html_options.delete("popup"), html_options.delete("post") confirm, popup = html_options.delete("confirm"), html_options.delete("popup")
# post is deprecated, but if its specified and method is not, assume that method = :post
method, post = html_options.delete("method"), html_options.delete("post")
if !method && post
ActiveSupport::Deprecation.warn(
"Passing :post as a link modifier is deprecated. " +
"Use :method => \"post\" instead. :post will be removed in Rails 2.0."
)
method = :post
end
html_options["onclick"] = case html_options["onclick"] = case
when popup && post when popup && method
raise ActionView::ActionViewError, "You can't use :popup and :post in the same link" raise ActionView::ActionViewError, "You can't use :popup and :post in the same link"
when confirm && popup when confirm && popup
"if (#{confirm_javascript_function(confirm)}) { #{popup_javascript_function(popup)} };return false;" "if (#{confirm_javascript_function(confirm)}) { #{popup_javascript_function(popup)} };return false;"
when confirm && post when confirm && method
"if (#{confirm_javascript_function(confirm)}) { #{post_javascript_function} };return false;" "if (#{confirm_javascript_function(confirm)}) { #{method_javascript_function(method)} };return false;"
when confirm when confirm
"return #{confirm_javascript_function(confirm)};" "return #{confirm_javascript_function(confirm)};"
when post when method
"#{post_javascript_function}return false;" "#{method_javascript_function(method)}return false;"
when popup when popup
popup_javascript_function(popup) + 'return false;' popup_javascript_function(popup) + 'return false;'
else else
@ -277,8 +361,17 @@ module ActionView
popup.is_a?(Array) ? "window.open(this.href,'#{popup.first}','#{popup.last}');" : "window.open(this.href);" popup.is_a?(Array) ? "window.open(this.href,'#{popup.first}','#{popup.last}');" : "window.open(this.href);"
end end
def post_javascript_function def method_javascript_function(method)
"var f = document.createElement('form'); this.parentNode.appendChild(f); f.method = 'POST'; f.action = this.href; f.submit();" submit_function =
"var f = document.createElement('form'); f.style.display = 'none'; " +
"this.parentNode.appendChild(f); f.method = 'POST'; f.action = this.href;"
unless method == :post
submit_function << "var m = document.createElement('input'); m.setAttribute('type', 'hidden'); "
submit_function << "m.setAttribute('name', '_method'); m.setAttribute('value', '#{method}'); f.appendChild(m);"
end
submit_function << "f.submit();"
end end
# Processes the _html_options_ hash, converting the boolean # Processes the _html_options_ hash, converting the boolean

View file

@ -6,14 +6,20 @@ module ActionView
attr_reader :original_exception attr_reader :original_exception
def initialize(base_path, file_name, assigns, source, original_exception) def initialize(base_path, file_path, assigns, source, original_exception)
@base_path, @assigns, @source, @original_exception = @base_path, @assigns, @source, @original_exception =
base_path, assigns, source, original_exception base_path, assigns.dup, source, original_exception
@file_name = file_name @file_path = file_path
remove_deprecated_assigns!
end end
def message def message
original_exception.message ActiveSupport::Deprecation.silence { original_exception.message }
end
def clean_backtrace
original_exception.clean_backtrace
end end
def sub_template_message def sub_template_message
@ -25,62 +31,80 @@ module ActionView
end end
end end
def source_extract(indention = 0) def source_extract(indentation = 0)
source_code = IO.readlines(@file_name) return unless num = line_number
num = num.to_i
start_on_line = [ line_number - SOURCE_CODE_RADIUS - 1, 0 ].max source_code = IO.readlines(@file_path)
end_on_line = [ line_number + SOURCE_CODE_RADIUS - 1, source_code.length].min
start_on_line = [ num - SOURCE_CODE_RADIUS - 1, 0 ].max
end_on_line = [ num + SOURCE_CODE_RADIUS - 1, source_code.length].min
indent = ' ' * indentation
line_counter = start_on_line line_counter = start_on_line
extract = source_code[start_on_line..end_on_line].collect do |line|
line_counter += 1
"#{' ' * indention}#{line_counter}: " + line
end
extract.join source_code[start_on_line..end_on_line].sum do |line|
line_counter += 1
"#{indent}#{line_counter}: #{line}"
end
end end
def sub_template_of(file_name) def sub_template_of(template_path)
@sub_templates ||= [] @sub_templates ||= []
@sub_templates << file_name @sub_templates << template_path
end end
def line_number def line_number
if file_name @line_number ||=
regexp = /#{Regexp.escape File.basename(file_name)}:(\d+)/ if file_name
[@original_exception.message, @original_exception.clean_backtrace].flatten.each do |line| regexp = /#{Regexp.escape File.basename(file_name)}:(\d+)/
return $1.to_i if regexp =~ line
$1 if message =~ regexp or clean_backtrace.find { |line| line =~ regexp }
end end
end
0
end end
def file_name def file_name
stripped = strip_base_path(@file_name) stripped = strip_base_path(@file_path)
stripped[0] == ?/ ? stripped[1..-1] : stripped stripped.slice!(0,1) if stripped[0] == ?/
stripped
end end
def to_s def to_s
"\n\n#{self.class} (#{message}) on line ##{line_number} of #{file_name}:\n" + "\n\n#{self.class} (#{message}) #{source_location}:\n" +
source_extract + "\n " + "#{source_extract}\n #{clean_backtrace.join("\n ")}\n\n"
original_exception.clean_backtrace.join("\n ") +
"\n\n"
end end
def backtrace def backtrace
[ [
"On line ##{line_number} of #{file_name}\n\n#{source_extract(4)}\n " + "#{source_location.capitalize}\n\n#{source_extract(4)}\n " +
original_exception.clean_backtrace.join("\n ") clean_backtrace.join("\n ")
] ]
end end
private private
def strip_base_path(file_name) def remove_deprecated_assigns!
file_name = File.expand_path(file_name).gsub(/^#{Regexp.escape File.expand_path(RAILS_ROOT)}/, '') ActionController::Base::DEPRECATED_INSTANCE_VARIABLES.each do |ivar|
file_name.gsub(@base_path, "") @assigns.delete(ivar)
end
end
def strip_base_path(path)
File.expand_path(path).
gsub(/^#{Regexp.escape File.expand_path(RAILS_ROOT)}/, '').
gsub(@base_path, "")
end
def source_location
if line_number
"on line ##{line_number} of "
else
'in '
end + file_name
end end
end end
end end
Exception::TraceSubstitutions << [/:in\s+`_run_(html|xml).*'\s*$/, ''] if defined?(Exception::TraceSubstitutions) if defined?(Exception::TraceSubstitutions)
Exception::TraceSubstitutions << [%r{^\s*#{Regexp.escape RAILS_ROOT}}, '#{RAILS_ROOT}'] if defined?(RAILS_ROOT) Exception::TraceSubstitutions << [/:in\s+`_run_(html|xml).*'\s*$/, '']
Exception::TraceSubstitutions << [%r{^\s*#{Regexp.escape RAILS_ROOT}}, '#{RAILS_ROOT}'] if defined?(RAILS_ROOT)
end

View file

@ -6,9 +6,11 @@ require 'yaml'
require 'test/unit' require 'test/unit'
require 'action_controller' require 'action_controller'
require 'breakpoint' require 'breakpoint'
require 'action_controller/test_process' require 'action_controller/test_process'
# Show backtraces for deprecated behavior for quicker cleanup.
ActiveSupport::Deprecation.debug = true
ActionController::Base.logger = nil ActionController::Base.logger = nil
ActionController::Base.ignore_missing_templates = false ActionController::Base.ignore_missing_templates = false
ActionController::Routing::Routes.reload rescue nil ActionController::Routing::Routes.reload rescue nil

View file

@ -11,53 +11,74 @@ class ActiveRecordTestConnector
end end
# Try to grab AR # Try to grab AR
begin if defined?(ActiveRecord) && defined?(Fixtures)
PATH_TO_AR = File.dirname(__FILE__) + '/../../activerecord' $stderr.puts 'Active Record is already loaded, running tests'
require "#{PATH_TO_AR}/lib/active_record" unless Object.const_defined?(:ActiveRecord) else
require "#{PATH_TO_AR}/lib/active_record/fixtures" unless Object.const_defined?(:Fixtures) $stderr.print 'Attempting to load Active Record... '
rescue Object => e begin
$stderr.puts "\nSkipping ActiveRecord assertion tests: #{e}" PATH_TO_AR = "#{File.dirname(__FILE__)}/../../activerecord/lib"
ActiveRecordTestConnector.able_to_connect = false raise LoadError, "#{PATH_TO_AR} doesn't exist" unless File.directory?(PATH_TO_AR)
$LOAD_PATH.unshift PATH_TO_AR
require 'active_record'
require 'active_record/fixtures'
$stderr.puts 'success'
rescue LoadError => e
$stderr.print "failed. Skipping Active Record assertion tests: #{e}"
ActiveRecordTestConnector.able_to_connect = false
end
end end
$stderr.flush
# Define the rest of the connector # Define the rest of the connector
class ActiveRecordTestConnector class ActiveRecordTestConnector
def self.setup class << self
unless self.connected || !self.able_to_connect def setup
setup_connection unless self.connected || !self.able_to_connect
load_schema setup_connection
self.connected = true load_schema
end require_fixture_models
rescue Object => e self.connected = true
$stderr.puts "\nSkipping ActiveRecord assertion tests: #{e}"
#$stderr.puts " #{e.backtrace.join("\n ")}\n"
self.able_to_connect = false
end
private
def self.setup_connection
if Object.const_defined?(:ActiveRecord)
begin
ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :dbfile => ':memory:')
ActiveRecord::Base.connection
rescue Object
$stderr.puts 'SQLite 3 unavailable; falling to SQLite 2.'
ActiveRecord::Base.establish_connection(:adapter => 'sqlite', :dbfile => ':memory:')
ActiveRecord::Base.connection
end end
rescue Exception => e # errors from ActiveRecord setup
Object.send(:const_set, :QUOTED_TYPE, ActiveRecord::Base.connection.quote_column_name('type')) unless Object.const_defined?(:QUOTED_TYPE) $stderr.puts "\nSkipping ActiveRecord assertion tests: #{e}"
else #$stderr.puts " #{e.backtrace.join("\n ")}\n"
raise "Couldn't locate ActiveRecord." self.able_to_connect = false
end end
end
# Load actionpack sqlite tables private
def self.load_schema
File.read(File.dirname(__FILE__) + "/fixtures/db_definitions/sqlite.sql").split(';').each do |sql| def setup_connection
ActiveRecord::Base.connection.execute(sql) unless sql.blank? if Object.const_defined?(:ActiveRecord)
begin
connection_options = {:adapter => 'sqlite3', :dbfile => ':memory:'}
ActiveRecord::Base.establish_connection(connection_options)
ActiveRecord::Base.configurations = { 'sqlite3_ar_integration' => connection_options }
ActiveRecord::Base.connection
rescue Exception # errors from establishing a connection
$stderr.puts 'SQLite 3 unavailable; falling to SQLite 2.'
connection_options = {:adapter => 'sqlite', :dbfile => ':memory:'}
ActiveRecord::Base.establish_connection(connection_options)
ActiveRecord::Base.configurations = { 'sqlite2_ar_integration' => connection_options }
ActiveRecord::Base.connection
end
Object.send(:const_set, :QUOTED_TYPE, ActiveRecord::Base.connection.quote_column_name('type')) unless Object.const_defined?(:QUOTED_TYPE)
else
raise "Couldn't locate ActiveRecord."
end
end
# Load actionpack sqlite tables
def load_schema
File.read(File.dirname(__FILE__) + "/fixtures/db_definitions/sqlite.sql").split(';').each do |sql|
ActiveRecord::Base.connection.execute(sql) unless sql.blank?
end
end
def require_fixture_models
Dir.glob(File.dirname(__FILE__) + "/fixtures/*.rb").each {|f| require f}
end end
end end
end end
@ -65,10 +86,17 @@ end
# Test case for inheiritance # Test case for inheiritance
class ActiveRecordTestCase < Test::Unit::TestCase class ActiveRecordTestCase < Test::Unit::TestCase
# Set our fixture path # Set our fixture path
self.fixture_path = "#{File.dirname(__FILE__)}/fixtures/" if ActiveRecordTestConnector.able_to_connect
self.fixture_path = "#{File.dirname(__FILE__)}/fixtures/"
self.use_transactional_fixtures = false
end
def self.fixtures(*args)
super if ActiveRecordTestConnector.connected
end
def setup def setup
abort_tests unless ActiveRecordTestConnector.connected = true abort_tests unless ActiveRecordTestConnector.connected
end end
# Default so Test::Unit::TestCase doesn't complain # Default so Test::Unit::TestCase doesn't complain
@ -76,13 +104,13 @@ class ActiveRecordTestCase < Test::Unit::TestCase
end end
private private
# If things go wrong, we don't want to run our test cases. We'll just define them to test nothing.
# If things go wrong, we don't want to run our test cases. We'll just define them to test nothing. def abort_tests
def abort_tests $stderr.puts 'No Active Record connection, aborting tests.'
self.class.public_instance_methods.grep(/^test./).each do |method| self.class.public_instance_methods.grep(/^test./).each do |method|
self.class.class_eval { define_method(method.to_sym){} } self.class.class_eval { define_method(method.to_sym){} }
end
end end
end
end end
ActiveRecordTestConnector.setup ActiveRecordTestConnector.setup

Some files were not shown because too many files have changed in this diff Show more