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

@ -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
# a copy of this software and associated documentation files (the
@ -30,7 +30,7 @@ unless defined?(ActiveSupport)
require 'active_support'
rescue LoadError
require 'rubygems'
require_gem 'activesupport'
gem 'activesupport'
end
end
@ -43,7 +43,7 @@ require 'action_controller/benchmarking'
require 'action_controller/flash'
require 'action_controller/filters'
require 'action_controller/layout'
require 'action_controller/dependencies'
require 'action_controller/deprecated_dependencies'
require 'action_controller/mime_responds'
require 'action_controller/pagination'
require 'action_controller/scaffolding'

View file

@ -1,320 +1,82 @@
require 'test/unit'
require 'test/unit/assertions'
require 'rexml/document'
require File.dirname(__FILE__) + "/vendor/html-scanner/html/document"
module Test #:nodoc:
module Unit #:nodoc:
# In addition to these specific assertions, you also have easy access to various collections that the regular test/unit assertions
# can be used against. These collections are:
#
# * assigns: Instance variables assigned in the action that are available for the view.
# * session: Objects being saved in the session.
# * flash: The flash objects currently in the session.
# * cookies: Cookies being sent to the user on this request.
#
# These collections can be used just like any other hash:
#
# assert_not_nil assigns(:person) # makes sure that a @person instance variable was set
# assert_equal "Dave", cookies[:name] # makes sure that a cookie called :name was set as "Dave"
# assert flash.empty? # makes sure that there's nothing in the flash
#
# For historic reasons, the assigns hash uses string-based keys. So assigns[:person] won't work, but assigns["person"] will. To
# appease our yearning for symbols, though, an alternative accessor has been deviced using a method call instead of index referencing.
# So assigns(:person) will work just like assigns["person"], but again, assigns[:person] will not work.
#
# On top of the collections, you have the complete url that a given action redirected to available in redirect_to_url.
#
# For redirects within the same controller, you can even call follow_redirect and the redirect will be followed, triggering another
# action call which can then be asserted against.
#
# == Manipulating the request collections
#
# The collections described above link to the response, so you can test if what the actions were expected to do happened. But
# sometimes you also want to manipulate these collections in the incoming request. This is really only relevant for sessions
# and cookies, though. For sessions, you just do:
#
# @request.session[:key] = "value"
#
# For cookies, you need to manually create the cookie, like this:
#
# @request.cookies["key"] = CGI::Cookie.new("key", "value")
#
# == Testing named routes
#
# If you're using named routes, they can be easily tested using the original named routes methods straight in the test case.
# Example:
#
# assert_redirected_to page_url(:title => 'foo')
module Assertions
# Asserts that the response is one of the following types:
#
# * <tt>:success</tt>: Status code was 200
# * <tt>:redirect</tt>: Status code was in the 300-399 range
# * <tt>:missing</tt>: Status code was 404
# * <tt>:error</tt>: Status code was in the 500-599 range
#
# You can also pass an explicit status code number as the type, like assert_response(501)
def assert_response(type, message = nil)
clean_backtrace do
if [ :success, :missing, :redirect, :error ].include?(type) && @response.send("#{type}?")
assert_block("") { true } # to count the assertion
elsif type.is_a?(Fixnum) && @response.response_code == type
assert_block("") { true } # to count the assertion
else
assert_block(build_message(message, "Expected response to be a <?>, but was <?>", type, @response.response_code)) { false }
end
end
module ActionController #:nodoc:
# In addition to these specific assertions, you also have easy access to various collections that the regular test/unit assertions
# can be used against. These collections are:
#
# * assigns: Instance variables assigned in the action that are available for the view.
# * session: Objects being saved in the session.
# * flash: The flash objects currently in the session.
# * cookies: Cookies being sent to the user on this request.
#
# These collections can be used just like any other hash:
#
# assert_not_nil assigns(:person) # makes sure that a @person instance variable was set
# assert_equal "Dave", cookies[:name] # makes sure that a cookie called :name was set as "Dave"
# assert flash.empty? # makes sure that there's nothing in the flash
#
# For historic reasons, the assigns hash uses string-based keys. So assigns[:person] won't work, but assigns["person"] will. To
# appease our yearning for symbols, though, an alternative accessor has been deviced using a method call instead of index referencing.
# So assigns(:person) will work just like assigns["person"], but again, assigns[:person] will not work.
#
# On top of the collections, you have the complete url that a given action redirected to available in redirect_to_url.
#
# For redirects within the same controller, you can even call follow_redirect and the redirect will be followed, triggering another
# action call which can then be asserted against.
#
# == Manipulating the request collections
#
# The collections described above link to the response, so you can test if what the actions were expected to do happened. But
# sometimes you also want to manipulate these collections in the incoming request. This is really only relevant for sessions
# and cookies, though. For sessions, you just do:
#
# @request.session[:key] = "value"
#
# For cookies, you need to manually create the cookie, like this:
#
# @request.cookies["key"] = CGI::Cookie.new("key", "value")
#
# == Testing named routes
#
# If you're using named routes, they can be easily tested using the original named routes methods straight in the test case.
# Example:
#
# assert_redirected_to page_url(:title => 'foo')
module Assertions
def self.included(klass)
klass.class_eval do
include ActionController::Assertions::ResponseAssertions
include ActionController::Assertions::SelectorAssertions
include ActionController::Assertions::RoutingAssertions
include ActionController::Assertions::TagAssertions
include ActionController::Assertions::DomAssertions
include ActionController::Assertions::ModelAssertions
include ActionController::Assertions::DeprecatedAssertions
end
end
# Assert that the redirection options passed in match those of the redirect called in the latest action. This match can be partial,
# such that assert_redirected_to(:controller => "weblog") will also match the redirection of
# redirect_to(:controller => "weblog", :action => "show") and so on.
def assert_redirected_to(options = {}, message=nil)
clean_backtrace do
assert_response(:redirect, message)
if options.is_a?(String)
msg = build_message(message, "expected a redirect to <?>, found one to <?>", options, @response.redirect_url)
url_regexp = %r{^(\w+://.*?(/|$|\?))(.*)$}
eurl, epath, url, path = [options, @response.redirect_url].collect do |url|
u, p = (url_regexp =~ url) ? [$1, $3] : [nil, url]
[u, (p[0..0] == '/') ? p : '/' + p]
end.flatten
assert_equal(eurl, url, msg) if eurl && url
assert_equal(epath, path, msg) if epath && path
else
@response_diff = options.diff(@response.redirected_to) if options.is_a?(Hash) && @response.redirected_to.is_a?(Hash)
msg = build_message(message, "response is not a redirection to all of the options supplied (redirection is <?>)#{', difference: <?>' if @response_diff}",
@response.redirected_to || @response.redirect_url, @response_diff)
assert_block(msg) do
if options.is_a?(Symbol)
@response.redirected_to == options
else
options.keys.all? do |k|
if k == :controller then options[k] == ActionController::Routing.controller_relative_to(@response.redirected_to[k], @controller.class.controller_path)
else options[k] == (@response.redirected_to[k].respond_to?(:to_param) ? @response.redirected_to[k].to_param : @response.redirected_to[k] unless @response.redirected_to[k].nil?)
end
end
end
end
end
end
end
# Asserts that the request was rendered with the appropriate template file.
def assert_template(expected = nil, message=nil)
clean_backtrace do
rendered = expected ? @response.rendered_file(!expected.include?('/')) : @response.rendered_file
msg = build_message(message, "expecting <?> but rendering with <?>", expected, rendered)
assert_block(msg) do
if expected.nil?
!@response.rendered_with_file?
else
expected == rendered
end
end
end
end
# Asserts that the routing of the given path was handled correctly and that the parsed options match.
def assert_recognizes(expected_options, path, extras={}, message=nil)
clean_backtrace do
path = "/#{path}" unless path[0..0] == '/'
# Load routes.rb if it hasn't been loaded.
ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty?
# Assume given controller
request = ActionController::TestRequest.new({}, {}, nil)
request.path = path
ActionController::Routing::Routes.recognize!(request)
expected_options = expected_options.clone
extras.each_key { |key| expected_options.delete key } unless extras.nil?
expected_options.stringify_keys!
msg = build_message(message, "The recognized options <?> did not match <?>",
request.path_parameters, expected_options)
assert_block(msg) { request.path_parameters == expected_options }
end
end
# Asserts that the provided options can be used to generate the provided path.
def assert_generates(expected_path, options, defaults={}, extras = {}, message=nil)
clean_backtrace do
expected_path = "/#{expected_path}" unless expected_path[0] == ?/
# Load routes.rb if it hasn't been loaded.
ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty?
generated_path, extra_keys = ActionController::Routing::Routes.generate(options, extras)
found_extras = options.reject {|k, v| ! extra_keys.include? k}
msg = build_message(message, "found extras <?>, not <?>", found_extras, extras)
assert_block(msg) { found_extras == extras }
msg = build_message(message, "The generated path <?> did not match <?>", generated_path,
expected_path)
assert_block(msg) { expected_path == generated_path }
end
end
# Asserts that path and options match both ways; in other words, the URL generated from
# options is the same as path, and also that the options recognized from path are the same as options
def assert_routing(path, options, defaults={}, extras={}, message=nil)
assert_recognizes(options, path, extras, message)
controller, default_controller = options[:controller], defaults[:controller]
if controller && controller.include?(?/) && default_controller && default_controller.include?(?/)
options[:controller] = "/#{controller}"
end
assert_generates(path, options, defaults, extras, message)
end
# Asserts that there is a tag/node/element in the body of the response
# that meets all of the given conditions. The +conditions+ parameter must
# be a hash of any of the following keys (all are optional):
#
# * <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
def clean_backtrace(&block)
yield
rescue Test::Unit::AssertionFailedError => e
path = File.expand_path(__FILE__)
raise Test::Unit::AssertionFailedError, e.message, e.backtrace.reject { |line| File.expand_path(line) =~ /#{path}/ }
end
end
end
require File.dirname(__FILE__) + '/assertions/response_assertions'
require File.dirname(__FILE__) + '/assertions/selector_assertions'
require File.dirname(__FILE__) + '/assertions/tag_assertions'
require File.dirname(__FILE__) + '/assertions/dom_assertions'
require File.dirname(__FILE__) + '/assertions/routing_assertions'
require File.dirname(__FILE__) + '/assertions/model_assertions'
require File.dirname(__FILE__) + '/assertions/deprecated_assertions'
module Test #:nodoc:
module Unit #:nodoc:
class TestCase #:nodoc:
include ActionController::Assertions
end
end
end

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,4 +1,5 @@
require 'fileutils'
require 'uri'
module ActionController #:nodoc:
# 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
if options[:action].is_a?(Array)
options[:action].dup.each do |action|
self.class.expire_page(url_for(options.merge({ :only_path => true, :skip_relative_url_root => true, :action => action })))
self.class.expire_page(url_for(options.merge(:only_path => true, :skip_relative_url_root => true, :action => action)))
end
else
self.class.expire_page(url_for(options.merge({ :only_path => true, :skip_relative_url_root => true })))
self.class.expire_page(url_for(options.merge(:only_path => true, :skip_relative_url_root => true)))
end
end
# Manually cache the +content+ in the key determined by +options+. If no content is provided, the contents of @response.body is used
# Manually cache the +content+ in the key determined by +options+. If no content is provided, the contents of response.body is used
# If no options are provided, the current +options+ for this action is used. Example:
# cache_page "I'm the cached content", :controller => "lists", :action => "show"
def cache_page(content = nil, options = {})
return unless perform_caching && caching_allowed
self.class.cache_page(content || @response.body, url_for(options.merge({ :only_path => true, :skip_relative_url_root => true })))
self.class.cache_page(content || response.body, url_for(options.merge(:only_path => true, :skip_relative_url_root => true, :format => params[:format])))
end
private
def caching_allowed
!@request.post? && @response.headers['Status'] && @response.headers['Status'].to_i < 400
request.get? && response.headers['Status'].to_i == 200
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
# "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.
#
# 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
def self.append_features(base) #:nodoc:
super
def self.included(base) #:nodoc:
base.extend(ClassMethods)
base.send(:attr_accessor, :rendered_action_cache)
end
@ -173,22 +177,24 @@ module ActionController #:nodoc:
return unless perform_caching
if options[:action].is_a?(Array)
options[:action].dup.each do |action|
expire_fragment(url_for(options.merge({ :action => action })).split("://").last)
expire_fragment(ActionCachePath.path_for(self, options.merge({ :action => action })))
end
else
expire_fragment(url_for(options).split("://").last)
expire_fragment(ActionCachePath.path_for(self, options))
end
end
class ActionCacheFilter #:nodoc:
def initialize(*actions)
def initialize(*actions, &block)
@actions = actions
end
def before(controller)
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
set_content_type!(action_cache_path)
controller.send(:render_text, cache)
false
end
@ -196,8 +202,60 @@ module ActionController #:nodoc:
def after(controller)
return if !@actions.include?(controller.action_name.intern) || controller.rendered_action_cache
controller.write_fragment(controller.url_for.split("://").last, controller.response.body)
controller.write_fragment(ActionCachePath.path_for(controller), controller.response.body)
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
@ -208,7 +266,7 @@ module ActionController #:nodoc:
# <b>Hello <%= @name %></b>
# <% cache do %>
# All the topics in the system:
# <%= render_collection_of_partials "topic", Topic.find_all %>
# <%= render :partial => "topic", :collection => Topic.find(:all) %>
# <% end %>
#
# This cache will bind to the name of action that called it. So you would be able to invalidate it using
@ -246,8 +304,7 @@ module ActionController #:nodoc:
# ActionController::Base.fragment_cache_store = :mem_cache_store, "localhost"
# ActionController::Base.fragment_cache_store = MyOwnStore.new("parameter")
module Fragments
def self.append_features(base) #:nodoc:
super
def self.included(base) #:nodoc:
base.class_eval do
@@fragment_cache_store = MemoryStore.new
cattr_reader :fragment_cache_store
@ -306,7 +363,12 @@ module ActionController #:nodoc:
# Name can take one of three forms:
# * String: This would normally take the form of a path like "pages/45/notes"
# * Hash: Is treated as an implicit call to url_for, like { :controller => "pages", :action => "notes", :id => 45 }
# * Regexp: Will destroy all the matched fragments, example: %r{pages/\d*/notes} Ensure you do not specify start and finish in the regex (^$) because the actual filename matched looks like ./cache/filename/path.cache
# * 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)
return unless perform_caching
@ -327,6 +389,7 @@ module ActionController #:nodoc:
def expire_matched_fragments(matcher = /.*/, options = nil) #:nodoc:
expire_fragment(matcher, options)
end
deprecate :expire_matched_fragments => :expire_fragment
class UnthreadedMemoryStore #:nodoc:
@ -430,7 +493,7 @@ module ActionController #:nodoc:
if f =~ matcher
begin
File.delete(f)
rescue Object => e
rescue SystemCallError => e
# If there's no cache, then there's nothing to complain about
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.
module Sweeping
def self.append_features(base) #:nodoc:
super
def self.included(base) #:nodoc:
base.extend(ClassMethods)
end
@ -503,8 +565,7 @@ module ActionController #:nodoc:
return unless perform_caching
configuration = sweepers.last.is_a?(Hash) ? sweepers.pop : {}
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
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.
# However, subclasses of ActionController::Caching::Sweeper should be Reloadable
include Reloadable::Subclasses
include Reloadable::Deprecated
def before(controller)
self.controller = controller

View file

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

View file

@ -1,217 +1,211 @@
require 'cgi'
require 'action_controller/vendor/xml_simple'
require 'action_controller/vendor/xml_node'
require 'strscan'
# Static methods for parsing the query and request parameters that can be used in
# a CGI extension class or testing in isolation.
class CGIMethods #:nodoc:
public
# Returns a hash with the pairs from the query string. The implicit hash construction that is done in
# parse_request_params is not done here.
def CGIMethods.parse_query_parameters(query_string)
parsed_params = {}
query_string.split(/[&;]/).each { |p|
# Ignore repeated delimiters.
next if p.empty?
class << self
# DEPRECATED: Use parse_form_encoded_parameters
def parse_query_parameters(query_string)
pairs = query_string.split('&').collect do |chunk|
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
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
FormEncodedPairParser.new(pairs).result
end
# Returns the request (POST/GET) parameters in a parsed form where pairs such as "customer[address][street]" /
# "Somewhere cool!" are translated into a full hash hierarchy, like
# { "customer" => { "address" => { "street" => "Somewhere cool!" } } }
def CGIMethods.parse_request_parameters(params)
parsed_params = {}
# DEPRECATED: Use parse_form_encoded_parameters
def parse_request_parameters(params)
parser = FormEncodedPairParser.new
for key, value in params
value = [value] if key =~ /.*\[\]$/
unless key.include?('[')
# much faster to test for the most common case first (GET)
# and avoid the call to build_deep_hash
parsed_params[key] = get_typed_value(value[0])
else
build_deep_hash(get_typed_value(value[0]), parsed_params, get_levels(key))
params = params.dup
until params.empty?
for key, value in params
if key.blank?
params.delete key
elsif !key.include?('[')
# much faster to test for the most common case first (GET)
# 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
parsed_params
parser.result
end
def self.parse_formatted_request_parameters(mime_type, raw_post_data)
params = case strategy = ActionController::Base.param_parsers[mime_type]
def parse_formatted_request_parameters(mime_type, raw_post_data)
case strategy = ActionController::Base.param_parsers[mime_type]
when Proc
strategy.call(raw_post_data)
when :xml_simple
raw_post_data.blank? ? nil :
typecast_xml_value(XmlSimple.xml_in(raw_post_data,
'forcearray' => false,
'forcecontent' => true,
'keeproot' => true,
'contentkey' => '__content__'))
raw_post_data.blank? ? {} : Hash.from_xml(raw_post_data)
when :yaml
YAML.load(raw_post_data)
when :xml_node
node = XmlNode.from_xml(raw_post_data)
{ node.node_name => node }
end
dasherize_keys(params || {})
rescue Object => e
rescue Exception => e # YAML, XML or Ruby code block errors
{ "exception" => "#{e.message} (#{e.class})", "backtrace" => e.backtrace,
"raw_post_data" => raw_post_data, "format" => mime_type }
end
def self.typecast_xml_value(value)
case value
when Hash
if value.has_key?("__content__")
content = translate_xml_entities(value["__content__"])
case value["type"]
when "integer" then content.to_i
when "boolean" then content == "true"
when "datetime" then Time.parse(content)
when "date" then Date.parse(content)
else content
end
else
value.empty? ? nil : value.inject({}) do |h,(k,v)|
h[k] = typecast_xml_value(v)
h
end
end
when Array
value.map! { |i| typecast_xml_value(i) }
case value.length
when 0 then nil
when 1 then value.first
else value
end
else
raise "can't typecast #{value.inspect}"
end
end
private
def get_typed_value(value)
case value
when String
value
when NilClass
''
when Array
value.map { |v| get_typed_value(v) }
else
# Uploaded file provides content type and filename.
if value.respond_to?(:content_type) &&
!value.content_type.blank? &&
!value.original_filename.blank?
unless value.respond_to?(:full_original_filename)
class << value
alias_method :full_original_filename, :original_filename
private
def self.translate_xml_entities(value)
value.gsub(/&lt;/, "<").
gsub(/&gt;/, ">").
gsub(/&quot;/, '"').
gsub(/&apos;/, "'").
gsub(/&amp;/, "&")
end
def self.dasherize_keys(params)
case params.class.to_s
when "Hash"
params.inject({}) do |h,(k,v)|
h[k.to_s.tr("-", "_")] = dasherize_keys(v)
h
end
when "Array"
params.map { |v| dasherize_keys(v) }
else
params
end
end
# Splits the given key into several pieces. Example keys are 'name', 'person[name]',
# 'person[name][first]', and 'people[]'. In each instance, an Array instance is returned.
# 'person[name][first]' produces ['person', 'name', 'first']; 'people[]' produces ['people', '']
def CGIMethods.split_key(key)
if /^([^\[]+)((?:\[[^\]]*\])+)$/ =~ key
keys = [$1]
keys.concat($2[1..-2].split(']['))
keys << '' if key[-2..-1] == '[]' # Have to add it since split will drop empty strings
keys
else
[key]
end
end
def CGIMethods.get_typed_value(value)
# test most frequent case first
if value.is_a?(String)
value
elsif value.respond_to?(:content_type) && ! value.content_type.blank?
# Uploaded file
unless value.respond_to?(:full_original_filename)
class << value
alias_method :full_original_filename, :original_filename
# Take the basename of the upload's original filename.
# This handles the full Windows paths given by Internet Explorer
# (and perhaps other broken user agents) without affecting
# those which give the lone filename.
# The Windows regexp is adapted from Perl's File::Basename.
def original_filename
if md = /^(?:.*[:\\\/])?(.*)/m.match(full_original_filename)
md.captures.first
else
File.basename full_original_filename
# Take the basename of the upload's original filename.
# This handles the full Windows paths given by Internet Explorer
# (and perhaps other broken user agents) without affecting
# those which give the lone filename.
# The Windows regexp is adapted from Perl's File::Basename.
def original_filename
if md = /^(?:.*[:\\\/])?(.*)/m.match(full_original_filename)
md.captures.first
else
File.basename full_original_filename
end
end
end
end
# Return the same value after overriding original_filename.
value
# 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
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
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
# Return the same value after overriding original_filename.
value
elsif value.respond_to?(:read)
# Value as part of a multipart request
value.read
elsif value.class == Array
value.collect { |v| CGIMethods.get_typed_value(v) }
else
# other value (neither string nor a multipart request)
value.to_s
return value
end
end
PARAMS_HASH_RE = /^([^\[]+)(\[.*\])?(.)?.*$/
def CGIMethods.get_levels(key)
all, main, bracketed, trailing = PARAMS_HASH_RE.match(key).to_a
if main.nil?
[]
elsif trailing
[key]
elsif bracketed
[main] + bracketed.slice(1...-1).split('][')
else
[main]
end
end
def CGIMethods.build_deep_hash(value, hash, levels)
if levels.length == 0
value
elsif hash.nil?
{ levels.first => CGIMethods.build_deep_hash(value, nil, levels[1..-1]) }
else
hash.update({ levels.first => CGIMethods.build_deep_hash(value, hash[levels.first], levels[1..-1]) })
def type_conflict!(klass, value)
raise TypeError,
"Conflicting types for parameter containers. " +
"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. "
end
end
end

View file

@ -1,27 +1,48 @@
class CGI #:nodoc:
# Add @request.env['RAW_POST_DATA'] for the vegans.
module QueryExtension
# Initialize the data from the query.
#
# Handles multipart forms (in particular, forms that involve file uploads).
# Reads query parameters in the @params field, and cookies into @cookies.
def initialize_query()
def initialize_query
@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']
method = method.to_s.downcase.intern
else
method = :get
end
if method == :post && (boundary = multipart_form_boundary)
@multipart = true
@params = read_multipart(boundary, Integer(env_table['CONTENT_LENGTH']))
else
@multipart = false
@params = CGI::parse(read_query_params(method) || "")
# POST assumes missing Content-Type is application/x-www-form-urlencoded.
content_type = env_table['CONTENT_TYPE']
if content_type.blank? && method == :post
content_type = 'application/x-www-form-urlencoded'
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
private
@ -29,16 +50,16 @@ class CGI #:nodoc:
MULTIPART_FORM_BOUNDARY_RE = %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|n #"
end
def multipart_form_boundary
MULTIPART_FORM_BOUNDARY_RE.match(env_table['CONTENT_TYPE']).to_a.pop
def extract_multipart_form_boundary(content_type)
MULTIPART_FORM_BOUNDARY_RE.match(content_type).to_a.pop
end
if defined? MOD_RUBY
def read_params_from_query
def read_query
Apache::request.args || ''
end
else
def read_params_from_query
def read_query
# fixes CGI querystring parsing for lighttpd
env_qs = env_table['QUERY_STRING']
if env_qs.blank? && !(uri = env_table['REQUEST_URI']).blank?
@ -49,25 +70,25 @@ class CGI #:nodoc:
end
end
def read_params_from_post
def read_body(content_length)
stdinput.binmode if stdinput.respond_to?(:binmode)
content = stdinput.read(Integer(env_table['CONTENT_LENGTH'])) || ''
# fix for Safari Ajax postings that always append \000
content = stdinput.read(content_length) || ''
# Fix for Safari Ajax postings that always append \000
content.chop! if content[-1] == 0
content.gsub! /&_=$/, ''
content.gsub!(/&_=$/, '')
env_table['RAW_POST_DATA'] = content.freeze
end
def read_query_params(method)
def read_params(method, content_length)
case method
when :get
read_params_from_query
read_query
when :post, :put
read_params_from_post
read_body(content_length)
when :cmd
read_from_cmdline
else # when :head, :delete, :options
read_params_from_query
else # :head, :delete, :options, :trace, :connect
read_query
end
end
end # module QueryExtension

View file

@ -8,13 +8,13 @@ module ActionController #:nodoc:
# sessions (large performance increase if sessions are not needed). The <tt>session_options</tt> are the same as for CGI::Session:
#
# * <tt>:database_manager</tt> - standard options are CGI::Session::FileStore, CGI::Session::MemoryStore, and CGI::Session::PStore
# (default). Additionally, there is CGI::Session::DRbStore and CGI::Session::ActiveRecordStore. Read more about these in
# (default). Additionally, there is CGI::Session::DRbStore and CGI::Session::ActiveRecordStore. Read more about these in
# lib/action_controller/session.
# * <tt>:session_key</tt> - the parameter name used for the session id. Defaults to '_session_id'.
# * <tt>:session_id</tt> - the session id to use. If not provided, then it is retrieved from the +session_key+ parameter
# of the request, or automatically generated for a new session.
# * <tt>:new_session</tt> - if true, force creation of a new session. If not set, a new session is only created if none currently
# exists. If false, a new session is never created, and if none currently exists and the +session_id+ option is not set,
# exists. If false, a new session is never created, and if none currently exists and the +session_id+ option is not set,
# an ArgumentError is raised.
# * <tt>:session_expires</tt> - the time the current session expires, as a +Time+ object. If not set, the session will continue
# indefinitely.
@ -22,10 +22,10 @@ module ActionController #:nodoc:
# server.
# * <tt>:session_secure</tt> - if +true+, this session will only work over HTTPS.
# * <tt>:session_path</tt> - the path for which this session applies. Defaults to the directory of the CGI script.
def self.process_cgi(cgi = CGI.new, session_options = {})
def self.process_cgi(cgi = CGI.new, session_options = {})
new.process_cgi(cgi, session_options)
end
def process_cgi(cgi, session_options = {}) #:nodoc:
process(CgiRequest.new(cgi, session_options), CgiResponse.new(cgi)).out
end
@ -51,7 +51,7 @@ module ActionController #:nodoc:
if (qs = @cgi.query_string) && !qs.empty?
qs
elsif uri = @env['REQUEST_URI']
parts = uri.split('?')
parts = uri.split('?')
parts.shift
parts.join('?')
else
@ -60,7 +60,8 @@ module ActionController #:nodoc:
end
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
def request_parameters
@ -71,7 +72,7 @@ module ActionController #:nodoc:
CGIMethods.parse_request_parameters(@cgi.params)
end
end
def cookies
@cgi.cookies.freeze
end
@ -101,15 +102,26 @@ module ActionController #:nodoc:
end
def session
unless @session
unless defined?(@session)
if @session_options == false
@session = Hash.new
else
stale_session_check! do
if session_options_with_string_keys['new_session'] == true
@session = new_session
else
@session = CGI::Session.new(@cgi, session_options_with_string_keys)
case value = session_options_with_string_keys['new_session']
when true
@session = new_session
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
@session['__valid_session']
end
@ -119,7 +131,7 @@ module ActionController #:nodoc:
end
def reset_session
@session.delete if CGI::Session === @session
@session.delete if defined?(@session) && @session.is_a?(CGI::Session)
@session = new_session
end
@ -141,11 +153,11 @@ module ActionController #:nodoc:
def stale_session_check!
yield
rescue ArgumentError => argument_error
if argument_error.message =~ %r{undefined class/module (\w+)}
if argument_error.message =~ %r{undefined class/module ([\w:]+)}
begin
Module.const_missing($1)
rescue LoadError, NameError => const_error
raise ActionController::SessionRestoreError, <<end_msg
raise ActionController::SessionRestoreError, <<-end_msg
Session contains objects whose class definition isn\'t available.
Remember to require the classes for all objects kept in the session.
(Original exception: #{const_error.message} [#{const_error.class}])
@ -159,7 +171,7 @@ end_msg
end
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
@ -170,38 +182,49 @@ end_msg
end
def out(output = $stdout)
convert_content_type!(@headers)
convert_content_type!
set_content_length!
output.binmode if output.respond_to?(:binmode)
output.sync = false if output.respond_to?(:sync=)
begin
output.write(@cgi.header(@headers))
if @cgi.send(:env_table)['REQUEST_METHOD'] == 'HEAD'
return
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)
else
output.write(@body)
end
output.flush if output.respond_to?(:flush)
rescue Errno::EPIPE => e
# lost connection to the FCGI process -- ignore the output, then
rescue Errno::EPIPE, Errno::ECONNRESET
# lost connection to parent process, ignore output
end
end
private
def convert_content_type!(headers)
if header = headers.delete("Content-Type")
headers["type"] = header
def convert_content_type!
if content_type = @headers.delete("Content-Type")
@headers["type"] = content_type
end
if header = headers.delete("Content-type")
headers["type"] = header
if content_type = @headers.delete("Content-type")
@headers["type"] = content_type
end
if header = headers.delete("content-type")
headers["type"] = header
if content_type = @headers.delete("content-type")
@headers["type"] = content_type
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

View file

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

View file

@ -4,13 +4,14 @@ module ActionController #:nodoc:
# itself back -- just the value it holds). Examples for writing:
#
# cookies[:user_name] = "david" # => Will set a simple session cookie
# cookies[:login] = { :value => "XJ-122", :expires => Time.now + 360} # => Will set a cookie that expires in 1 hour
#
# cookies[:login] = { :value => "XJ-122", :expires => 1.hour.from_now }
# # => Will set a cookie that expires in 1 hour
#
# Examples for reading:
#
# cookies[:user_name] # => "david"
# cookies.size # => 2
#
#
# Example for deleting:
#
# cookies.delete :user_name
@ -32,13 +33,13 @@ module ActionController #:nodoc:
# Deprecated cookie writer method
def cookie(*options)
@response.headers["cookie"] << CGI::Cookie.new(*options)
response.headers['cookie'] << CGI::Cookie.new(*options)
end
end
class CookieJar < Hash #:nodoc:
def initialize(controller)
@controller, @cookies = controller, controller.instance_variable_get("@cookies")
@controller, @cookies = controller, controller.request.cookies
super()
update(@cookies)
end
@ -48,7 +49,7 @@ module ActionController #:nodoc:
def [](name)
@cookies[name.to_s].value.first if @cookies[name.to_s] && @cookies[name.to_s].respond_to?(:value)
end
def []=(name, options)
if options.is_a?(Hash)
options = options.inject({}) { |options, pair| options[pair.first.to_s] = pair.last; options }
@ -56,10 +57,10 @@ module ActionController #:nodoc:
else
options = { "name" => name.to_s, "value" => options }
end
set_cookie(options)
end
# Removes the cookie on the client machine by setting the value to an empty string
# and setting its expiration date into the past
def delete(name)

View file

@ -5,19 +5,14 @@ module ActionController #:nodoc:
base.send(:include, ActionController::Filters::InstanceMethods)
end
# Filters enable controllers to run shared pre and post processing code for its actions. These filters can be used to do
# authentication, caching, or auditing before the intended action is performed. Or to do localization or output
# compression after the action has been performed.
#
# Filters have access to the request, response, and all the instance variables set by other filters in the chain
# or by the action (in the case of after filters). Additionally, it's possible for a pre-processing <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.
# Filters enable controllers to run shared pre and post processing code for its actions. These filters can be used to do
# authentication, caching, or auditing before the intended action is performed. Or to do localization or output
# compression after the action has been performed. Filters have access to the request, response, and all the instance
# variables set by other filters in the chain or by the action (in the case of after filters).
#
# == 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:
#
# class BankController < ActionController::Base
@ -39,7 +34,7 @@ module ActionController #:nodoc:
# end
#
# Now any actions performed on the BankController will have the audit method called before. On the VaultController,
# first the audit method is called, then the verify_credentials method. If the audit method returns false, then
# first the audit method is called, then the verify_credentials method. If the audit method returns false, then
# verify_credentials and the intended action are never called.
#
# == Filter types
@ -64,7 +59,7 @@ module ActionController #:nodoc:
# The filter method is passed the controller instance and is hence granted access to all aspects of the controller and can
# manipulate them as it sees fit.
#
# The inline method (using a proc) can be used to quickly do something small that doesn't require a lot of explanation.
# The inline method (using a proc) can be used to quickly do something small that doesn't require a lot of explanation.
# Or just as a quick test. It works like this:
#
# class WeblogController < ActionController::Base
@ -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
# 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
#
# Using <tt>before_filter</tt> and <tt>after_filter</tt> appends the specified filters to the existing chain. That's usually
@ -83,14 +81,14 @@ 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
# beginning of their respective chain and executed before the rest. For example:
#
# class ShoppingController
# class ShoppingController < ActionController::Base
# before_filter :verify_open_shop
#
# class CheckoutController
# class CheckoutController < ShoppingController
# 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>
# <tt>:verify_open_shop</tt>. So if either of the ensure filters return false, we'll never get around to see if the shop
# <tt>:verify_open_shop</tt>. So if either of the ensure filters return false, we'll never get around to see if the shop
# is open or not.
#
# You may pass multiple filter arguments of each type as well as a filter block.
@ -98,250 +96,511 @@ module ActionController #:nodoc:
#
# == Around filters
#
# In addition to the individual before and after filters, it's also possible to specify that a single object should handle
# both the before and after call. That's especially useful when you need to keep state active between the before and after,
# such as the example of a benchmark filter below:
#
# class WeblogController < ActionController::Base
# around_filter BenchmarkingFilter.new
#
# # Before this action is performed, BenchmarkingFilter#before(controller) is executed
# def index
# Around filters wrap an action, executing code both before and after.
# They may be declared as method references, blocks, or objects responding
# to #filter or to both #before and #after.
#
# To use a method as an around_filter, pass a symbol naming the Ruby method.
# Yield (or block.call) within the method to run the action.
#
# around_filter :catch_exceptions
#
# private
# def catch_exceptions
# yield
# rescue => exception
# logger.debug "Caught exception! #{exception}"
# raise
# 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
#
# 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
# def initialize
# @runtime
# end
#
# def before
# start_timer
# end
#
# def after
# stop_timer
# report_result
# def self.filter(controller, &block)
# Benchmark.measure(&block)
# 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
#
# Some times its convenient to specify a filter chain in a superclass that'll hold true for the majority of the
# subclasses, but not necessarily all of them. The subclasses that behave in exception can then specify which filters
# they would like to be relieved of. Examples
# Declaring a filter on a base class conveniently applies to its subclasses,
# but sometimes a subclass should skip some of its superclass' filters:
#
# class ApplicationController < ActionController::Base
# before_filter :authenticate
# around_filter :catch_exceptions
# end
#
# class WeblogController < ApplicationController
# # will run the :authenticate filter
# # Will run the :authenticate and :catch_exceptions filters.
# end
#
# class SignupController < ApplicationController
# # will not run the :authenticate filter
# # Skip :authenticate, run :catch_exceptions.
# skip_before_filter :authenticate
# 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
#
# Filters can be limited to run for only specific actions. This can be expressed either by listing the actions to
# exclude or the actions to include when executing the filter. Available conditions are +:only+ or +:except+, both
# of which accept an arbitrary number of method references. For example:
# Filters may be limited to specific actions by declaring the actions to
# include or exclude. Both options accept single actions (:only => :index)
# or arrays of actions (:except => [:foo, :bar]).
#
# class Journal < ActionController::Base
# # only require authentication if the current action is edit or delete
# before_filter :authorize, :only => [ :edit, :delete ]
#
# # Require authentication for edit and 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
# def authorize
# # redirect to login unless authenticated
# # Redirect to login unless authenticated.
# end
# end
#
# When setting conditions on inline method (proc) filters the condition must come first and be placed in parentheses.
#
# class UserPreferences < ActionController::Base
# before_filter(:except => :new) { # some proc ... }
# # ...
# end
# == Filter Chain Halting
#
# <tt>before_filter</tt> and <tt>around_filter</tt> may halt the request
# before controller action is run. This is useful, for example, to deny
# access to unauthenticated users or to redirect from http to https.
# 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
# The passed <tt>filters</tt> will be appended to the array of filters that's run _before_ actions
# on this controller are performed.
# The passed <tt>filters</tt> will be appended to the filter_chain and
# will execute before the action on this controller is performed.
def append_before_filter(*filters, &block)
conditions = extract_conditions!(filters)
filters << block if block_given?
add_action_conditions(filters, conditions)
append_filter_to_chain('before', filters)
append_filter_to_chain(filters, :before, &block)
end
# The passed <tt>filters</tt> will be prepended to the array of filters that's run _before_ actions
# on this controller are performed.
# The passed <tt>filters</tt> will be prepended to the filter_chain and
# will execute before the action on this controller is performed.
def prepend_before_filter(*filters, &block)
conditions = extract_conditions!(filters)
filters << block if block_given?
add_action_conditions(filters, conditions)
prepend_filter_to_chain('before', filters)
prepend_filter_to_chain(filters, :before, &block)
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
# The passed <tt>filters</tt> will be appended to the array of filters that's run _after_ actions
# on this controller are performed.
# The passed <tt>filters</tt> will be appended to the array of filters
# that run _after_ actions on this controller are performed.
def append_after_filter(*filters, &block)
conditions = extract_conditions!(filters)
filters << block if block_given?
add_action_conditions(filters, conditions)
append_filter_to_chain('after', filters)
prepend_filter_to_chain(filters, :after, &block)
end
# The passed <tt>filters</tt> will be prepended to the array of filters that's run _after_ actions
# on this controller are performed.
# The passed <tt>filters</tt> will be prepended to the array of filters
# that run _after_ actions on this controller are performed.
def prepend_after_filter(*filters, &block)
conditions = extract_conditions!(filters)
filters << block if block_given?
add_action_conditions(filters, conditions)
prepend_filter_to_chain("after", filters)
append_filter_to_chain(filters, :after, &block)
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
# 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
# respond to both +before+ and +after+. So if you do append_around_filter A.new, B.new, the callstack will look like:
# If you append_around_filter A.new, B.new, the filter chain looks like
#
# B#before
# A#before
# # run the action
# A#after
# B#after
def append_around_filter(*filters)
conditions = extract_conditions!(filters)
for filter in filters.flatten
ensure_filter_responds_to_before_and_after(filter)
append_before_filter(conditions || {}) { |c| filter.before(c) }
prepend_after_filter(conditions || {}) { |c| filter.after(c) }
#
# With around filters which yield to the action block, #before and #after
# are the code before and after the yield.
def append_around_filter(*filters, &block)
filters, conditions = extract_conditions(filters, &block)
filters.map { |f| proxy_before_and_after_filter(f) }.each do |filter|
append_filter_to_chain([filter, conditions])
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
# 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:
# If you prepend_around_filter A.new, B.new, the filter chain looks like:
#
# A#before
# B#before
# # run the action
# B#after
# A#after
def prepend_around_filter(*filters)
for filter in filters.flatten
ensure_filter_responds_to_before_and_after(filter)
prepend_before_filter { |c| filter.before(c) }
append_after_filter { |c| filter.after(c) }
#
# With around filters which yield to the action block, #before and #after
# are the code before and after the yield.
def prepend_around_filter(*filters, &block)
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
# 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
# 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
# filters, not procs. This is especially useful for managing the chain in inheritance hierarchies where only one out
# of many sub-controllers need a different hierarchy.
#
# You can control the actions to skip the filter for with the <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.
def skip_before_filter(*filters)
if conditions = extract_conditions!(filters)
remove_contradicting_conditions!(filters, conditions)
conditions[:only], conditions[:except] = conditions[:except], conditions[:only]
add_action_conditions(filters, conditions)
else
for filter in filters.flatten
write_inheritable_attribute("before_filters", read_inheritable_attribute("before_filters") - [ filter ])
end
end
skip_filter_in_chain(*filters, &:before?)
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
# filters, not procs. This is especially useful for managing the chain in inheritance hierarchies where only one out
# of many sub-controllers need a different hierarchy.
#
# You can control the actions to skip the filter for with the <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.
def skip_after_filter(*filters)
if conditions = extract_conditions!(filters)
remove_contradicting_conditions!(filters, conditions)
conditions[:only], conditions[:except] = conditions[:except], conditions[:only]
add_action_conditions(filters, conditions)
else
for filter in filters.flatten
write_inheritable_attribute("after_filters", read_inheritable_attribute("after_filters") - [ filter ])
end
end
skip_filter_in_chain(*filters, &:after?)
end
# Removes the specified filters from the filter chain. This only works for method reference (symbol)
# filters, not procs. This method is different from skip_after_filter and skip_before_filter in that
# it will match any before, after or yielding around filter.
#
# 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.
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
# 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:
@before_filters ||= read_inheritable_attribute("before_filters") || []
filter_chain.select(&:before?).map(&:filter)
end
# 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:
@after_filters ||= read_inheritable_attribute("after_filters") || []
filter_chain.select(&:after?).map(&:filter)
end
# Returns a mapping between filters and the actions that may run them.
def included_actions #:nodoc:
@included_actions ||= read_inheritable_attribute("included_actions") || {}
read_inheritable_attribute("included_actions") || {}
end
# Returns a mapping between filters and actions that may not run them.
def excluded_actions #:nodoc:
@excluded_actions ||= read_inheritable_attribute("excluded_actions") || {}
read_inheritable_attribute("excluded_actions") || {}
end
private
def append_filter_to_chain(condition, filters)
write_inheritable_array("#{condition}_filters", filters)
# Find a filter in the filter_chain where the filter method matches the _filter_ param
# and (optionally) the passed block evaluates to true (mostly used for testing before?
# 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
def prepend_filter_to_chain(condition, filters)
old_filters = read_inheritable_attribute("#{condition}_filters") || []
write_inheritable_attribute("#{condition}_filters", filters + old_filters)
def before?
false
end
def ensure_filter_responds_to_before_and_after(filter)
unless filter.respond_to?(:before) && filter.respond_to?(:after)
raise ActionControllerError, "Filter object must respond to both before and after"
def after?
false
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
def extract_conditions!(filters)
return nil unless filters.last.is_a? Hash
filters.pop
# The determination of the filter type was once done at run time.
# This method is here to extract as much logic from the filter run time as possible
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
def add_action_conditions(filters, conditions)
return unless conditions
included, excluded = conditions[:only], conditions[:except]
write_inheritable_hash('included_actions', condition_hash(filters, included)) && return if included
write_inheritable_hash('excluded_actions', condition_hash(filters, excluded)) if excluded
def extract_conditions(*filters, &block) #:nodoc:
filters.flatten!
conditions = filters.last.is_a?(Hash) ? filters.pop : {}
filters << block if block_given?
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
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
def remove_contradicting_conditions!(filters, conditions)
return unless conditions[:only]
filters.each do |filter|
next unless included_actions_for_filter = (read_inheritable_attribute('included_actions') || {})[filter]
[*conditions[:only]].each do |conditional_action|
conditional_action = conditional_action.to_s
included_actions_for_filter.delete(conditional_action) if included_actions_for_filter.include?(conditional_action)
def skip_filter_in_chain(*filters, &test) #:nodoc:
filters, conditions = extract_conditions(filters)
filters.map! { |f| block_given? ? find_filter(f, &test) : find_filter(f) }
filters.compact!
if conditions.empty?
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
@ -350,26 +609,14 @@ module ActionController #:nodoc:
module InstanceMethods # :nodoc:
def self.included(base)
base.class_eval do
alias_method :perform_action_without_filters, :perform_action
alias_method :perform_action, :perform_action_with_filters
alias_method :process_without_filters, :process
alias_method :process, :process_with_filters
alias_method :process_cleanup_without_filters, :process_cleanup
alias_method :process_cleanup, :process_cleanup_with_filters
alias_method_chain :perform_action, :filters
alias_method_chain :process, :filters
alias_method_chain :process_cleanup, :filters
end
end
def perform_action_with_filters
before_action_result = before_action
unless before_action_result == false || performed?
perform_action_without_filters
after_action
end
@before_filter_chain_aborted = (before_action_result == false)
call_filter(self.class.filter_chain, 0)
end
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)
end
# Calls all the defined before-filter filters, which are added by using "before_filter :method".
# If any of the filters return false, no more filters will be executed and the action is aborted.
def before_action #:doc:
call_filters(self.class.before_filters)
def filter_chain
self.class.filter_chain
end
# Calls all the defined after-filter filters, which are added by using "after_filter :method".
# If any of the filters return false, no more filters will be executed.
def after_action #:doc:
call_filters(self.class.after_filters)
def call_filter(chain, index)
return (performed? || perform_action_without_filters) if index >= chain.size
filter = chain[index]
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
private
def call_filters(filters)
filters.each do |filter|
next if action_exempted?(filter)
filter_result = case
when filter.is_a?(Symbol)
self.send(filter)
when filter_block?(filter)
filter.call(self)
when filter_class?(filter)
filter.filter(self)
else
raise(
ActionControllerError,
'Filters need to be either a symbol, proc/method, or class implementing a static filter method'
)
end
if filter_result == false
logger.info "Filter chain halted as [#{filter}] returned false" if logger
return false
end
end
end
def filter_block?(filter)
filter.respond_to?('call') && (filter.arity == 1 || filter.arity == -1)
end
def filter_class?(filter)
filter.respond_to?('filter')
end
def action_exempted?(filter)
case
when ia = self.class.included_actions[filter]
!ia.include?(action_name)
when ea = self.class.excluded_actions[filter]
ea.include?(action_name)
end
end
def process_cleanup_with_filters
if @before_filter_chain_aborted
close_session

View file

@ -17,7 +17,7 @@ module ActionController #:nodoc:
# end
#
# 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
# 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.class_eval do
alias_method :assign_shortcuts_without_flash, :assign_shortcuts
alias_method :assign_shortcuts, :assign_shortcuts_with_flash
alias_method :process_cleanup_without_flash, :process_cleanup
alias_method :process_cleanup, :process_cleanup_with_flash
alias_method_chain :assign_shortcuts, :flash
alias_method_chain :process_cleanup, :flash
alias_method_chain :reset_session, :flash
end
end
@ -94,7 +92,7 @@ module ActionController #:nodoc:
#
# flash.keep # keeps the entire flash
# flash.keep(:notice) # keeps only the "notice" entry, the rest of the flash is discarded
def keep(k=nil)
def keep(k = nil)
use(k, false)
end
@ -102,7 +100,7 @@ module ActionController #:nodoc:
#
# flash.keep # keep entire flash available for the next action
# flash.discard(:warning) # discard the "warning" entry (it'll still be available for the current action)
def discard(k=nil)
def discard(k = nil)
use(k)
end
@ -118,6 +116,7 @@ module ActionController #:nodoc:
@used.delete(k)
end
end
(@used.keys - keys).each{|k| @used.delete k } # clean up after keys that could have been left over by calling reject! or shift on the flash
end
@ -143,36 +142,41 @@ module ActionController #:nodoc:
end
def process_cleanup_with_flash
flash.sweep if @session
flash.sweep if @_session
process_cleanup_without_flash
end
def reset_session_with_flash
reset_session_without_flash
remove_instance_variable(:@_flash)
flash(:refresh)
end
protected
# 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.
# Note that if sessions are disabled only flash.now will work.
def flash(refresh = false) #:doc:
if @flash.nil? || refresh
@flash =
if @session.is_a?(Hash)
# @session is a Hash, if sessions are disabled
# we don't put the flash in the session in this case
if !defined?(@_flash) || refresh
@_flash =
if session.is_a?(Hash)
# don't put flash in session if disabled
FlashHash.new
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
@session["flash"] ||= FlashHash.new
session["flash"] ||= FlashHash.new
end
end
@flash
@_flash
end
# deprecated. use <tt>flash.keep</tt> instead
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
end
end
end
end
end

View file

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

View file

@ -1,6 +1,7 @@
require 'dispatcher'
require 'stringio'
require 'uri'
require 'action_controller/test_process'
module ActionController
module Integration #:nodoc:
@ -13,6 +14,7 @@ module ActionController
# rather than instantiating Integration::Session directly.
class Session
include Test::Unit::Assertions
include ActionController::Assertions
include ActionController::TestProcess
# The integer HTTP status code of the last request.
@ -73,11 +75,11 @@ module ActionController
unless @named_routes_configured
# install the named routes in this session instance.
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
# easier access during testing and troubleshooting.
klass.send(:public, *Routing::NamedRoutes::Helpers)
klass.send(:public, *Routing::Routes.named_routes.helpers)
@named_routes_configured = true
end
end
@ -111,7 +113,7 @@ module ActionController
# performed on the location header.
def follow_redirect!
raise "not a redirect! #{@status} #{@status_message}" unless redirect?
get(interpret_uri(headers["location"].first))
get(interpret_uri(headers['location'].first))
status
end
@ -143,19 +145,33 @@ module ActionController
# (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.
#
# You can also perform POST, PUT, DELETE, and HEAD requests with #post,
# #put, #delete, and #head.
def get(path, parameters=nil, headers=nil)
process :get, path, parameters, headers
end
# Performs a POST request with the given parameters. The parameters may
# 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.
# Performs a POST request with the given parameters. See get() for more details.
def post(path, parameters=nil, headers=nil)
process :post, path, parameters, headers
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
# the request environment created by the Prototype library. The parameters
# 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
# prefix 'HTTP_' added if needed.
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)
end
@ -174,7 +194,6 @@ module ActionController
end
private
class MockCGI < CGI #:nodoc:
attr_accessor :stdinput, :stdoutput, :env_table
@ -224,7 +243,7 @@ module ActionController
(headers || {}).each do |key, value|
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
end
@ -247,6 +266,8 @@ module ActionController
# tests.
@response.extend(TestResponseBehavior)
@html_document = nil
parse_result
return status
end
@ -317,9 +338,8 @@ module ActionController
def self.included(base)
base.extend(ClassMethods)
base.class_eval do
class <<self
alias_method :new_without_capture, :new
alias_method :new, :new_with_capture
class << self
alias_method_chain :new, :capture
end
end
end
@ -330,9 +350,11 @@ module ActionController
def clear_last_instantiation!
self.last_instantiation = nil
end
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
@ -471,6 +493,8 @@ module ActionController
%w(get post cookies assigns xml_http_request).each do |method|
define_method(method) do |*args|
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
copy_session_variables!
end

View file

@ -3,12 +3,13 @@ module ActionController #:nodoc:
def self.included(base)
base.extend(ClassMethods)
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, :render_with_a_layout
class << self
alias_method :inherited_without_layout, :inherited
alias_method :inherited, :inherited_with_layout
alias_method_chain :inherited, :layout
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
# 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 %>
# <!-- The footer part of this layout -->
# // The footer part of this layout -->
#
# 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,
# like this:
#
# <!-- The header part of this layout -->
# // The header part of this layout
# hello world
# <!-- The footer part of this layout -->
# // The footer part of this layout -->
#
# == Accessing shared variables
#
@ -182,7 +183,6 @@ module ActionController #:nodoc:
private
def inherited_with_layout(child)
inherited_without_layout(child)
child.send :include, Reloadable
layout_match = child.name.underscore.sub(/_controller$/, '').sub(/^controllers\//, '')
child.layout(layout_match) unless layout_list.grep(%r{layouts/#{layout_match}\.[a-z][0-9a-z]*$}).empty?
end
@ -235,6 +235,8 @@ module ActionController #:nodoc:
template_with_options = options.is_a?(Hash)
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
logger.info("Rendering #{options} within #{layout}") if logger
@ -248,6 +250,7 @@ module ActionController #:nodoc:
erase_render_results
add_variables_to_assigns
@template.instance_variable_set("@content_for_layout", content_for_layout)
response.layout = layout
render_text(@template.render_file(layout, true), deprecated_status)
else
render_with_no_layout(options, deprecated_status, &block)
@ -263,7 +266,7 @@ module ActionController #:nodoc:
def candidate_for_layout?(options)
(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]))
end

View file

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

View file

@ -1,11 +1,12 @@
module ActionController
module Macros
module InPlaceEditing #:nodoc:
def self.append_features(base) #:nodoc:
super
def self.included(base) #:nodoc:
base.extend(ClassMethods)
end
# DEPRECATION WARNING: This method will become a separate plugin when Rails 2.0 ships.
#
# Example:
#
# # 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
# might look something like this:
#
# def list
# def index
# @people = Person.find(:all)
# end
#
# Here's the same action, with web-service support baked in:
#
# def list
# def index
# @people = Person.find(:all)
#
# respond_to do |wants|
# wants.html
# wants.xml { render :xml => @people.to_xml }
# respond_to do |format|
# format.html
# format.xml { render :xml => @people.to_xml }
# end
# end
#
@ -30,7 +30,7 @@ module ActionController #:nodoc:
# Supposing you have an action that adds a new person, optionally creating their company
# (by name) if it does not already exist, without web-services, it might look like this:
#
# def add
# def create
# @company = Company.find_or_create_by_name(params[:company][:name])
# @person = @company.people.create(params[:person])
#
@ -39,15 +39,15 @@ module ActionController #:nodoc:
#
# Here's the same action, with web-service support baked in:
#
# def add
# def create
# company = params[:person].delete(:company)
# @company = Company.find_or_create_by_name(company[:name])
# @person = @company.people.create(params[:person])
#
# respond_to do |wants|
# wants.html { redirect_to(person_list_url) }
# wants.js
# wants.xml { render :xml => @person.to_xml(:include => @company) }
# respond_to do |format|
# format.html { redirect_to(person_list_url) }
# format.js
# format.xml { render :xml => @person.to_xml(:include => @company) }
# end
# end
#
@ -97,9 +97,8 @@ module ActionController #:nodoc:
# environment.rb as follows.
#
# Mime::Type.register "image/jpg", :jpg
#
def respond_to(*types, &block)
raise ArgumentError, "respond_to takes either types or a block, never bot" unless types.any? ^ block
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) } }
responder = Responder.new(block.binding)
block.call(responder)
@ -108,15 +107,19 @@ module ActionController #:nodoc:
end
class Responder #:nodoc:
DEFAULT_BLOCKS = {
:html => 'Proc.new { render }',
:js => 'Proc.new { render :action => "#{action_name}.rjs" }',
:xml => 'Proc.new { render :action => "#{action_name}.rxml" }'
}
DEFAULT_BLOCKS = [:html, :js, :xml].inject({}) do |blocks, ext|
template_extension = (ext == :html ? '' : ".r#{ext}")
blocks.update ext => %(Proc.new { render :action => "\#{action_name}#{template_extension}", :content_type => Mime::#{ext.to_s.upcase} })
end
def initialize(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 = []
@responses = {}
end
@ -127,24 +130,33 @@ module ActionController #:nodoc:
@order << mime_type
if block_given?
@responses[mime_type] = block
else
@responses[mime_type] = eval(DEFAULT_BLOCKS[mime_type.to_sym], @block_binding)
end
end
for mime_type in %w( all html js xml rss atom yaml )
eval <<-EOT
def #{mime_type}(&block)
custom(Mime::#{mime_type.upcase}, &block)
@responses[mime_type] = Proc.new do
eval "response.content_type = '#{mime_type.to_s}'", @block_binding
block.call
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
def any(*args, &block)
args.each { |type| send(type, &block) }
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
for priority in @mime_type_priority
if priority == Mime::ALL

View file

@ -1,5 +1,18 @@
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
class AcceptItem #:nodoc:
attr_accessor :order, :name, :q
@ -31,14 +44,20 @@ module Mime
LOOKUP[string]
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)
# keep track of creation order to keep the subsequent sort stable
index = 0
list = accept_header.split(/,/).
map! { |i| AcceptItem.new(index += 1, *i.split(/;\s*q=/)) }.sort!
list = accept_header.split(/,/).map! do |i|
AcceptItem.new(index += 1, *i.split(/;\s*q=/))
end.sort!
# Take care of the broken text/xml entry by renaming or deleting it
text_xml = list.index("text/xml")
app_xml = list.index("application/xml")
@ -112,31 +131,70 @@ module Mime
end
ALL = Type.new "*/*", :all
TEXT = Type.new "text/plain", :text
HTML = Type.new "text/html", :html, %w( application/xhtml+xml )
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 )
RSS = Type.new "application/rss+xml", :rss
ATOM = Type.new "application/atom+xml", :atom
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["text/plain"] = TEXT
LOOKUP["text/html"] = HTML
LOOKUP["application/xhtml+xml"] = HTML
LOOKUP["application/xml"] = XML
LOOKUP["text/xml"] = XML
LOOKUP["application/x-xml"] = XML
LOOKUP["text/javascript"] = JS
LOOKUP["application/javascript"] = JS
LOOKUP["application/x-javascript"] = JS
LOOKUP["text/calendar"] = ICS
LOOKUP["text/csv"] = CSV
LOOKUP["application/xml"] = XML
LOOKUP["text/xml"] = XML
LOOKUP["application/x-xml"] = XML
LOOKUP["text/yaml"] = YAML
LOOKUP["application/x-yaml"] = YAML
LOOKUP["application/rss+xml"] = RSS
LOOKUP["application/atom+xml"] = ATOM
end
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

View file

@ -1,6 +1,8 @@
module ActionController
# === 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
# Active Record objects. It offers macro-style automatic fetching of your
# model for multiple views, or explicit fetching for single actions. And if
@ -104,8 +106,7 @@ module ActionController
# ClassMethods#paginate.
#
# +options+ are:
# <tt>:singular_name</tt>:: the singular name to use, if it can't be inferred by
# singularizing the collection name
# <tt>:singular_name</tt>:: the singular name to use, if it can't be inferred by singularizing the collection name
# <tt>:class_name</tt>:: the class name to use, if it can't be inferred by
# camelizing the singular name
# <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:
klass = options[:class_name].constantize
page = @params[options[:parameter]]
page = params[options[:parameter]]
count = count_collection_for_pagination(klass, options)
paginator = Paginator.new(self, count, options[:per_page], page)
collection = find_collection_for_pagination(klass, options, paginator)

View file

@ -13,12 +13,18 @@ module ActionController
@parameters ||= request_parameters.update(query_parameters).update(path_parameters).with_indifferent_access
end
# Returns the HTTP request method as a lowercase symbol (:get, for example)
# 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
@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
# Is this a GET request? Equivalent to request.method == :get
# Is this a GET (or HEAD) request? Equivalent to request.method == :get
def get?
method == :get
end
@ -38,9 +44,10 @@ module ActionController
method == :delete
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?
method == :head
@env['REQUEST_METHOD'].downcase.to_sym == :head
end
# Determine whether the body of a HTTP call is URL-encoded (default)
@ -128,19 +135,21 @@ module ActionController
@env['RAW_POST_DATA']
end
# Returns the request URI correctly, taking into account the idiosyncracies
# of the various servers.
# Return the request URI, accounting for server idiosyncracies.
# WEBrick includes the full URL. IIS leaves REQUEST_URI blank.
def request_uri
if uri = @env['REQUEST_URI']
(%r{^\w+\://[^/]+(/.*|$)$} =~ uri) ? $1 : uri # Remove domain, which webrick puts into the request_uri.
else # REQUEST_URI is blank under IIS - get this from PATH_INFO and SCRIPT_NAME
# Remove domain, which webrick puts into the request_uri.
(%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{[^/]+$})
uri = @env['PATH_INFO']
uri = uri.sub(/#{script_filename}\//, '') unless script_filename.nil?
unless (env_qs = @env['QUERY_STRING']).nil? || env_qs.empty?
uri << '?' << env_qs
end
uri
@env['REQUEST_URI'] = uri
end
end
@ -159,11 +168,10 @@ module ActionController
path = (uri = request_uri) ? uri.split('?').first : ''
# Cut off the path to the installation directory if given
root = relative_url_root
path[0, root.length] = '' if root
path || ''
path.sub!(%r/^#{relative_url_root}/, '')
path || ''
end
# Returns the path minus the web server relative installation directory.
# This can be set with the environment variable RAILS_RELATIVE_URL_ROOT.
# It can be automatically extracted for Apache setups. If the server is not

View file

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

View file

@ -1,15 +1,33 @@
module ActionController
class AbstractResponse #:nodoc:
DEFAULT_HEADERS = { "Cache-Control" => "no-cache" }
attr_accessor :body, :headers, :session, :cookies, :assigns, :template, :redirected_to, :redirected_to_method_params
attr_accessor :body, :headers, :session, :cookies, :assigns, :template, :redirected_to, :redirected_to_method_params, :layout
def initialize
@body, @headers, @session, @assigns = "", DEFAULT_HEADERS.merge("cookie" => []), [], []
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)
@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>"
end

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,6 @@
module ActionController
module Scaffolding # :nodoc:
def self.append_features(base)
super
def self.included(base)
base.extend(ClassMethods)
end
@ -25,25 +24,25 @@ module ActionController
# end
#
# def list
# @entries = Entry.find_all
# @entries = Entry.find(:all)
# render_scaffold "list"
# end
#
#
# def show
# @entry = Entry.find(params[:id])
# render_scaffold
# end
#
#
# def destroy
# Entry.find(params[:id]).destroy
# redirect_to :action => "list"
# end
#
#
# def new
# @entry = Entry.new
# render_scaffold
# end
#
#
# def create
# @entry = Entry.new(params[:entry])
# if @entry.save
@ -53,16 +52,16 @@ module ActionController
# render_scaffold('new')
# end
# end
#
#
# def edit
# @entry = Entry.find(params[:id])
# render_scaffold
# end
#
#
# def update
# @entry = Entry.find(params[:id])
# @entry.attributes = params[:entry]
#
#
# if @entry.save
# flash[:notice] = "Entry was successfully updated"
# redirect_to :action => "show", :id => @entry
@ -72,17 +71,17 @@ module ActionController
# end
# end
#
# The <tt>render_scaffold</tt> method will first check to see if you've made your own template (like "weblog/show.rhtml" for
# the show action) and if not, then render the generic template for that action. This gives you the possibility of using the
# scaffold while you're building your specific application. Start out with a totally generic setup, then replace one template
# The <tt>render_scaffold</tt> method will first check to see if you've made your own template (like "weblog/show.rhtml" for
# the show action) and if not, then render the generic template for that action. This gives you the possibility of using the
# scaffold while you're building your specific application. Start out with a totally generic setup, then replace one template
# and one action at a time while relying on the rest of the scaffolded templates and actions.
module ClassMethods
# Adds a swath of generic CRUD actions to the controller. The +model_id+ is automatically converted into a class name unless
# one is specifically provide through <tt>options[:class_name]</tt>. So <tt>scaffold :post</tt> would use Post as the class
# and @post/@posts for the instance variables.
#
#
# It's possible to use more than one scaffold in a single controller by specifying <tt>options[:suffix] = true</tt>. This will
# make <tt>scaffold :post, :suffix => true</tt> use method names like list_post, show_post, and create_post
# make <tt>scaffold :post, :suffix => true</tt> use method names like list_post, show_post, and create_post
# instead of just list, show, and post. If suffix is used, then no index method is added.
def scaffold(model_id, options = {})
options.assert_valid_keys(:class_name, :suffix)
@ -99,13 +98,13 @@ module ActionController
end
end_eval
end
module_eval <<-"end_eval", __FILE__, __LINE__
verify :method => :post, :only => [ :destroy#{suffix}, :create#{suffix}, :update#{suffix} ],
:redirect_to => { :action => :list#{suffix} }
def list#{suffix}
@#{singular_name}_pages, @#{plural_name} = paginate :#{plural_name}, :per_page => 10
render#{suffix}_scaffold "list#{suffix}"
@ -115,17 +114,17 @@ module ActionController
@#{singular_name} = #{class_name}.find(params[:id])
render#{suffix}_scaffold
end
def destroy#{suffix}
#{class_name}.find(params[:id]).destroy
redirect_to :action => "list#{suffix}"
end
def new#{suffix}
@#{singular_name} = #{class_name}.new
render#{suffix}_scaffold
end
def create#{suffix}
@#{singular_name} = #{class_name}.new(params[:#{singular_name}])
if @#{singular_name}.save
@ -135,12 +134,12 @@ module ActionController
render#{suffix}_scaffold('new')
end
end
def edit#{suffix}
@#{singular_name} = #{class_name}.find(params[:id])
render#{suffix}_scaffold
end
def update#{suffix}
@#{singular_name} = #{class_name}.find(params[:id])
@#{singular_name}.attributes = params[:#{singular_name}]
@ -152,14 +151,14 @@ module ActionController
render#{suffix}_scaffold('edit')
end
end
private
def render#{suffix}_scaffold(action=nil)
action ||= caller_method_name(caller)
# logger.info ("testing template:" + "\#{self.class.controller_path}/\#{action}") if logger
if template_exists?("\#{self.class.controller_path}/\#{action}")
render_action(action)
render :action => action
else
@scaffold_class = #{class_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))
if !active_layout.nil?
render_file(active_layout, nil, true)
render :file => active_layout, :use_full_path => true
else
render_file(scaffold_path("layout"))
render :file => scaffold_path('layout')
end
end
end
@ -179,12 +178,12 @@ module ActionController
def scaffold_path(template_name)
File.dirname(__FILE__) + "/templates/scaffolds/" + template_name + ".rhtml"
end
def caller_method_name(caller)
caller.first.scan(/`(.*)'/).first.first # ' ruby-mode
end
end_eval
end
end
end
end
end

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,10 +8,10 @@
<% if false %>
<br /><br />
<% 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" />
<% for key, values in @params %>
<% for key, values in params %>
<% next if key == "BP-RETRY" %>
<% for value in Array(values) %>
<input type="hidden" name="<%= key %>" value="<%= value %>" />
@ -26,7 +26,7 @@
<% 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("controller")
@ -37,8 +37,8 @@
<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>
<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>
<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>
<%=h @exception.class.to_s %>
<% if @request.parameters['controller'] %>
in <%=h @request.parameters['controller'].humanize %>Controller<% if @request.parameters['action'] %>#<%=h @request.parameters['action'] %><% end %>
<% if request.parameters['controller'] %>
in <%=h request.parameters['controller'].humanize %>Controller<% if request.parameters['action'] %>#<%=h request.parameters['action'] %><% end %>
<% end %>
</h1>
<pre><%=h @exception.clean_message %></pre>

View file

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

View file

@ -1,6 +1,6 @@
<h1>
<%=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>
<p>

View file

@ -14,7 +14,7 @@
<% end %>
<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 "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>
<% end %>
</table>

View file

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

View file

@ -1,13 +1,71 @@
module ActionController
# 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:
RESERVED_OPTIONS = [:anchor, :params, :only_path, :host, :protocol, :trailing_slash, :skip_relative_url_root]
def initialize(request, parameters)
@request, @parameters = request, parameters
end
def rewrite(options = {})
def rewrite(options = {})
rewrite_url(rewrite_path(options), options)
end
@ -41,34 +99,10 @@ module ActionController
options.update(overwrite)
end
RESERVED_OPTIONS.each {|k| options.delete k}
path, extra_keys = Routing::Routes.generate(options.dup, @request) # Warning: Routes will mutate and violate the options hash
path << build_query_string(options, extra_keys) unless extra_keys.empty?
path
end
# Returns a query string with escaped keys and values from the passed hash. If the passed hash contains an "id" it'll
# be added as a path element instead of a regular parameter pair.
def build_query_string(hash, only_keys = nil)
elements = []
query_string = ""
only_keys ||= hash.keys
only_keys.each do |key|
value = hash[key]
key = CGI.escape key.to_s
if value.class == Array
key << '[]'
else
value = [ value ]
end
value.each { |val| elements << "#{key}=#{Routing.extract_parameter_value(val)}" }
end
query_string << ("?" + elements.join("&")) unless elements.empty?
query_string
# Generates the query string, too
Routing::Routes.generate(options, @request.symbolized_path_parameters)
end
end
end

View file

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

View file

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

View file

@ -1,7 +1,6 @@
module ActionController #:nodoc:
module Verification #:nodoc:
def self.append_features(base) #:nodoc:
super
def self.included(base) #:nodoc:
base.extend(ClassMethods)
end
@ -18,19 +17,26 @@ module ActionController #:nodoc:
# Usage:
#
# class GlobalController < ActionController::Base
# # prevent the #update_settings action from being invoked unless
# # the 'admin_privileges' request parameter exists.
# # Prevent the #update_settings action from being invoked unless
# # 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,
# :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
# # 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",
# :only => :update_post,
# :add_flash => { "alert" => "Failed to create your message" },
# :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
# Verify the given actions so that if certain prerequisites are not met,
# 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
# called.
# * <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
# 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
@ -51,8 +57,12 @@ module ActionController #:nodoc:
# from an Ajax call or not.
# * <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.
# * <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
# 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
# the prerequisites cannot be satisfied.
# * <tt>:only</tt>: only apply this verification to the actions specified
@ -69,19 +79,20 @@ module ActionController #:nodoc:
def verify_action(options) #:nodoc:
prereqs_invalid =
[*options[:params] ].find { |v| @params[v].nil? } ||
[*options[:session]].find { |v| @session[v].nil? } ||
[*options[:params] ].find { |v| params[v].nil? } ||
[*options[:session]].find { |v| session[v].nil? } ||
[*options[:flash] ].find { |v| flash[v].nil? }
if !prereqs_invalid && options[:method]
prereqs_invalid ||=
[*options[:method]].all? { |v| @request.method != v.to_sym }
[*options[:method]].all? { |v| request.method != v.to_sym }
end
prereqs_invalid ||= (request.xhr? != options[:xhr]) unless options[:xhr].nil?
if prereqs_invalid
flash.update(options[:add_flash]) if options[:add_flash]
response.headers.update(options[:add_headers]) if options[:add_headers]
unless performed?
render(options[:render]) if options[:render]
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
# a copy of this software and associated documentation files (the

View file

@ -1,8 +1,8 @@
module ActionPack #:nodoc:
module VERSION #:nodoc:
MAJOR = 1
MINOR = 12
TINY = 5
MINOR = 13
TINY = 2
STRING = [MAJOR, MINOR, TINY].join('.')
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
# a copy of this software and associated documentation files (the

View file

@ -1,7 +1,6 @@
require 'erb'
module ActionView #:nodoc:
class ActionViewError < StandardError #:nodoc:
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:
#
# <%= render "shared/header", { "headline" => "Welcome", "person" => person } %>
# <%= render "shared/header", { :headline => "Welcome", :person => person } %>
#
# These can now be accessed in shared/header with:
#
# Headline: <%= headline %>
# First name: <%= person.first_name %>
#
# If you need to find out whether a certain local variable has been assigned a value in a particular render call,
# you need to use the following pattern:
#
# <% if local_assigns.has_key? :headline %>
# Headline: <%= headline %>
# <% end %>
#
# Testing using <tt>defined? headline</tt> will not work. This is an implementation restriction.
#
# == Template caching
#
# By default, Rails will compile each template to a method in order to render it. When you alter a template, Rails will
@ -148,7 +156,8 @@ module ActionView #:nodoc:
attr_accessor :base_path, :assigns, :template_extension
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 '-'.
# See ERB documentation for suitable values.
@ -199,7 +208,7 @@ module ActionView #:nodoc:
end
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$/
require File.join(helper_dir, $1)
helper_module_name = $1.camelize
@ -301,6 +310,9 @@ module ActionView #:nodoc:
# will only be read if it has to be compiled.
#
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
if compile_template?(template, file_path, local_assigns)
template ||= read_template_file(file_path, extension)
@ -309,9 +321,7 @@ module ActionView #:nodoc:
# Get the method name for this template and run it
method_name = @@method_names[file_path || template]
evaluate_assigns
local_assigns = local_assigns.symbolize_keys if @@local_assigns_support_string_keys
evaluate_assigns
send(method_name, local_assigns) do |*name|
instance_variable_get "@content_for_#{name.first || 'layout'}"
@ -337,14 +347,14 @@ module ActionView #:nodoc:
def builder_template_exists?(template_path)#:nodoc:
template_exists?(template_path, :rxml)
end
def javascript_template_exists?(template_path)#:nodoc:
template_exists?(template_path, :rjs)
end
def file_exists?(template_path)#:nodoc:
template_file_name, template_file_extension = path_and_extension(template_path)
if template_file_extension
template_exists?(template_file_name, template_file_extension)
else
@ -374,11 +384,11 @@ module ActionView #:nodoc:
template_path_without_extension = template_path.sub(/\.(\w+)$/, '')
[ template_path_without_extension, $1 ]
end
def cached_template_extension(template_path)
@@cache_template_extensions && @@cached_template_extension[template_path]
end
end
def find_template_extension_for(template_path)
if match = delegate_template_exists?(template_path)
match.first.to_sym
@ -386,7 +396,7 @@ module ActionView #:nodoc:
elsif builder_template_exists?(template_path): :rxml
elsif javascript_template_exists?(template_path): :rjs
else
raise ActionViewError, "No rhtml, rxml, rjs or delegate template found for #{template_path}"
raise ActionViewError, "No rhtml, rxml, rjs or delegate template found for #{template_path} in #{@base_path}"
end
end
@ -416,7 +426,7 @@ module ActionView #:nodoc:
local_assigns.empty? ||
((args = @@template_args[render_symbol]) && local_assigns.all? { |k,_| args.has_key?(k) })
end
# Check whether compilation is necessary.
# Compile if the inline template or file has not been compiled yet.
# Or if local_assigns has a new key, which isn't supported by the compiled code yet.
@ -427,8 +437,8 @@ module ActionView #:nodoc:
if @@compile_time[render_symbol] && supports_local_assigns?(render_symbol, local_assigns)
if file_name && !@@cache_template_loading
@@compile_time[render_symbol] < File.mtime(file_name) || (File.symlink?(file_name) ?
@@compile_time[render_symbol] < File.lstat(file_name).mtime : false)
@@compile_time[render_symbol] < File.mtime(file_name) ||
(File.symlink?(file_name) && (@@compile_time[render_symbol] < File.lstat(file_name).mtime))
end
else
true
@ -440,11 +450,11 @@ module ActionView #:nodoc:
if template_requires_setup?(extension)
body = case extension.to_sym
when :rxml
"controller.response.content_type ||= 'application/xml'\n" +
"xml = Builder::XmlMarkup.new(:indent => 2)\n" +
"@controller.headers['Content-Type'] ||= 'application/xml'\n" +
template
when :rjs
"@controller.headers['Content-Type'] ||= 'text/javascript'\n" +
"controller.response.content_type ||= 'text/javascript'\n" +
"update_page do |page|\n#{template}\nend"
end
else
@ -457,7 +467,7 @@ module ActionView #:nodoc:
locals_code = ""
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
"def #{render_symbol}(local_assigns)\n#{locals_code}#{body}\nend"
@ -472,34 +482,27 @@ module ActionView #:nodoc:
end
def assign_method_name(extension, template, file_name)
method_name = '_run_'
method_name << "#{extension}_" if extension
method_key = file_name || template
@@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
file_path = File.expand_path(file_name)
base_path = File.expand_path(@base_path)
i = file_path.index(base_path)
l = base_path.length
method_name_file_part = i ? file_path[i+l+1,file_path.length-l-1] : file_path.clone
method_name_file_part.sub!(/\.r(html|xml|js)$/,'')
method_name_file_part.tr!('/:-', '_')
method_name_file_part.gsub!(/[^a-zA-Z0-9_]/){|s| s[0].to_s}
method_name += method_name_file_part
s = File.expand_path(file_name)
s.sub!(/^#{Regexp.escape(File.expand_path(RAILS_ROOT))}/, '') if defined?(RAILS_ROOT)
s.gsub!(/([^a-zA-Z0-9_])/) { $1[0].to_s }
s
else
@@inline_template_count += 1
method_name << @@inline_template_count.to_s
(@@inline_template_count += 1).to_s
end
@@method_names[file_name || template] = method_name.intern
end
def compile_template(extension, template, file_name, local_assigns)
method_key = file_name || template
render_symbol = @@method_names[method_key] || assign_method_name(extension, template, file_name)
render_symbol = assign_method_name(extension, template, file_name)
render_source = create_template_source(extension, template, render_symbol, local_assigns.keys)
line_offset = @@template_args[render_symbol].size
@ -516,18 +519,18 @@ module ActionView #:nodoc:
else
CompiledTemplates.module_eval(render_source, 'compiled-template', -line_offset)
end
rescue Object => e
rescue Exception => e # errors from template code
if logger
logger.debug "ERROR: compiling #{render_symbol} RAISED #{e}"
logger.debug "Function body: #{render_source}"
logger.debug "Backtrace: #{e.backtrace.join("\n")}"
end
raise TemplateError.new(@base_path, method_key, @assigns, template, e)
raise TemplateError.new(@base_path, file_name || template, @assigns, template, e)
end
@@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

View file

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

View file

@ -85,36 +85,62 @@ module ActionView
# <%= 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>
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)
else
''
end
end
# Returns a string with a div containing all the error messages for the object located as an instance variable by the name
# of <tt>object_name</tt>. This div can be tailored by the following options:
# Returns a string with a div containing all of the error messages for the objects located as instance variables by the names
# 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>id</tt> - The id 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
# you need is significantly different from the default presentation, it makes plenty of sense to access the object.errors
# instance yourself and set it up. View the source of this method to see how easy it is.
def error_messages_for(object_name, options = {})
options = options.symbolize_keys
object = instance_variable_get("@#{object_name}")
if object && !object.errors.empty?
content_tag("div",
content_tag(
options[:header_tag] || "h2",
"#{pluralize(object.errors.count, "error")} prohibited this #{object_name.to_s.gsub("_", " ")} from being saved"
) +
content_tag("p", "There were problems with the following fields:") +
content_tag("ul", object.errors.full_messages.collect { |msg| content_tag("li", msg) }),
"id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation"
def error_messages_for(*params)
options = params.last.is_a?(Hash) ? params.pop.symbolize_keys : {}
objects = params.collect {|object_name| instance_variable_get("@#{object_name}") }.compact
count = objects.inject(0) {|sum, object| sum + object.errors.count }
unless count.zero?
html = {}
[:id, :class].each do |key|
if options.include?(key)
value = options[key]
html[key] = value unless value.blank?
else
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
""
''
end
end
@ -137,12 +163,14 @@ module ActionView
to_input_field_tag(field_type, options)
when :text
to_text_area_tag(options)
when :integer, :float
when :integer, :float, :decimal
to_input_field_tag("text", options)
when :date
to_date_select_tag(options)
when :datetime, :timestamp
to_datetime_select_tag(options)
when :time
to_time_select_tag(options)
when :boolean
to_boolean_select_tag(options)
end
@ -184,6 +212,15 @@ module ActionView
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)
has_error ? Base.field_error_proc.call(html_tag, self) : html_tag
end

View file

@ -3,20 +3,36 @@ require File.dirname(__FILE__) + '/url_helper'
require File.dirname(__FILE__) + '/tag_helper'
module ActionView
module Helpers
# Provides methods for linking a HTML page together with other assets, such as javascripts, stylesheets, and feeds.
module Helpers #:nodoc:
# 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
# Returns a link tag that browsers and news readers can use to auto-detect a RSS or ATOM feed for this page. The +type+ can
# either be <tt>:rss</tt> (default) or <tt>:atom</tt> and the +options+ follow the url_for style of declaring a link target.
# Returns a link tag that browsers and news readers can use to auto-detect
# an RSS or ATOM feed. The +type+ can either be <tt>:rss</tt> (default) or
# <tt>:atom</tt>. Control the link options in url_for format using the
# +url_options+. You can modify the LINK tag itself in +tag_options+.
#
# Examples:
# auto_discovery_link_tag # =>
# Tag Options:
# * <tt>:rel</tt> - Specify the relation of this link, defaults to "alternate"
# * <tt>:type</tt> - Override the auto-generated mime type
# * <tt>:title</tt> - Specify the title of the link, defaults to the +type+
#
# auto_discovery_link_tag # =>
# <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" />
# 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" />
# 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" />
def auto_discovery_link_tag(type = :rss, url_options = {}, tag_options = {})
tag(
@ -28,9 +44,14 @@ module ActionView
)
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 "dir/xmlhr.js" # => /javascripts/dir/xmlhr.js
# javascript_path "/dir/xmlhr" # => /dir/xmlhr.js
def javascript_path(source)
compute_public_path(source, 'javascripts', 'js')
end
@ -38,7 +59,15 @@ module ActionView
JAVASCRIPT_DEFAULT_SOURCES = ['prototype', 'effects', 'dragdrop', 'controls'] unless const_defined?(:JAVASCRIPT_DEFAULT_SOURCES)
@@javascript_default_sources = JAVASCRIPT_DEFAULT_SOURCES.dup
# Returns a script include tag per source given as argument. Examples:
# 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" # =>
# <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/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)
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")
end
sources.collect { |source|
sources.collect do |source|
source = javascript_path(source)
content_tag("script", "", { "type" => "text/javascript", "src" => source }.merge(options))
}.join("\n")
end.join("\n")
end
# Register one or more additional JavaScript files to be included when
#
# javascript_include_tag :defaults
#
# is called. This method is intended to be called only from plugin initialization
# to register extra .js files the plugin installed in <tt>public/javascripts</tt>.
# <tt>javascript_include_tag :defaults</tt> is called. This method is
# only intended to be called from plugin initialization to register additional
# .js files that the plugin installed in <tt>public/javascripts</tt>.
def self.register_javascript_include_default(*sources)
@@javascript_default_sources.concat(sources)
end
@ -89,14 +111,21 @@ module ActionView
@@javascript_default_sources = JAVASCRIPT_DEFAULT_SOURCES.dup
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 "dir/style.css" # => /stylesheets/dir/style.css
# stylesheet_path "/dir/style.css" # => /dir/style.css
def stylesheet_path(source)
compute_public_path(source, 'stylesheets', 'css')
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" # =>
# <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" />
def stylesheet_link_tag(*sources)
options = sources.last.is_a?(Hash) ? sources.pop.stringify_keys : { }
sources.collect { |source|
sources.collect do |source|
source = stylesheet_path(source)
tag("link", { "rel" => "Stylesheet", "type" => "text/css", "media" => "screen", "href" => source }.merge(options))
}.join("\n")
end.join("\n")
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...
# * full path, like "/my_images/image.gif"
# * file name, like "rss.gif", that gets expanded to "/images/rss.gif"
# * file name without extension, like "logo", that gets expanded to "/images/logo.png"
# image_path("edit.png") # => /images/edit.png
# image_path("icons/edit.png") # => /images/icons/edit.png
# image_path("/icons/edit.png") # => /icons/edit.png
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')
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>:size</tt> - Supplied as "XxY", so "30x45" becomes width="30" and height="45"
# * <tt>:alt</tt> - If no alt text is given, the file name part of the
# +source+ is used (capitalized and without the extension)
# * <tt>:size</tt> - Supplied as "{Width}x{Height}", 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...
# * full path, like "/my_images/image.gif"
# * file name, like "rss.gif", that gets expanded to "/images/rss.gif"
# * file name without extension, like "logo", that gets expanded to "/images/logo.png"
# image_tag("icon.png") # =>
# <img src="/images/icon.png" alt="Icon" />
# image_tag("icon.png", :size => "16x10", :alt => "Edit Entry") # =>
# <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 = {})
options.symbolize_keys!
@ -141,8 +189,8 @@ module ActionView
options[:alt] ||= File.basename(options[:src], '.*').split('.').first.capitalize
if options[:size]
options[:width], options[:height] = options[:size].split("x")
options.delete :size
options[:width], options[:height] = options[:size].split("x") if options[:size] =~ %r{^\d+x\d+$}
options.delete(:size)
end
tag("img", options)
@ -150,11 +198,14 @@ module ActionView
private
def compute_public_path(source, dir, ext)
source = "/#{dir}/#{source}" unless source.first == "/" || source.include?(":")
source << ".#{ext}" unless source.split("/").last.include?(".")
source << '?' + rails_asset_id(source) if defined?(RAILS_ROOT) && %r{^[-a-z]+://} !~ source
source = "#{@controller.request.relative_url_root}#{source}" unless %r{^[-a-z]+://} =~ source
source = ActionController::Base.asset_host + source unless source.include?(":")
source = source.dup
source << ".#{ext}" if File.extname(source).blank?
unless source =~ %r{^[-a-z]+://}
source = "/#{dir}/#{source}" unless source[0] == ?/
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
end

View file

@ -89,7 +89,7 @@ module ActionView
# 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
# <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)"
end

View file

@ -13,14 +13,38 @@ module ActionView
module DateHelper
DEFAULT_PREFIX = 'date' unless const_defined?('DEFAULT_PREFIX')
# Reports the approximate distance in time between two Time objects or integers.
# For example, if the distance is 47 minutes, it'll return
# "about 1 hour". See the source for the complete wording list.
# Reports the approximate distance in time between two Time or Date objects or integers as seconds.
# Set <tt>include_seconds</tt> to true if you want more detailed approximations when distance < 1 min, 29 secs
# Distances are reported base on the following table:
#
# Integers are interpreted as seconds. So,
# <tt>distance_of_time_in_words(50)</tt> returns "less than a minute".
# 0 <-> 29 secs # => less than a minute
# 30 secs <-> 1 min, 29 secs # => 1 minute
# 1 min, 30 secs <-> 44 mins, 29 secs # => [2..44] minutes
# 44 mins, 30 secs <-> 89 mins, 29 secs # => about 1 hour
# 89 mins, 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)
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)
@ -29,29 +53,33 @@ module ActionView
case distance_in_minutes
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
when 0..5 then 'less than 5 seconds'
when 6..10 then 'less than 10 seconds'
when 11..20 then 'less than 20 seconds'
when 21..40 then 'half a minute'
when 41..59 then 'less than a minute'
when 0..4 then 'less than 5 seconds'
when 5..9 then 'less than 10 seconds'
when 10..19 then 'less than 20 seconds'
when 20..39 then 'half a minute'
when 40..59 then 'less than a minute'
else '1 minute'
end
when 2..45 then "#{distance_in_minutes} minutes"
when 46..90 then 'about 1 hour'
when 90..1440 then "about #{(distance_in_minutes.to_f / 60.0).round} hours"
when 1441..2880 then '1 day'
else "#{(distance_in_minutes / 1440).round} days"
when 2..44 then "#{distance_in_minutes} minutes"
when 45..89 then 'about 1 hour'
when 90..1439 then "about #{(distance_in_minutes.to_f / 60.0).round} hours"
when 1440..2879 then '1 day'
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
# Like distance_of_time_in_words, but where <tt>to_time</tt> is fixed to <tt>Time.now</tt>.
def time_ago_in_words(from_time, include_seconds = false)
distance_of_time_in_words(from_time, Time.now, include_seconds)
end
alias_method :distance_of_time_in_words_to_now, :time_ago_in_words
# Returns a set of select tags (one for year, month, and day) pre-selected for accessing a specified date-based attribute (identified by
@ -80,6 +108,19 @@ module ActionView
InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_date_select_tag(options)
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
# 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)
end
# Returns a set of html select-tags (one for year, month, and day) pre-selected with the +date+.
def select_date(date = Date.today, options = {})
select_year(date, options) + select_month(date, options) + select_day(date, options)
end
# Returns a set of html select-tags (one for year, month, day, hour, and minute) pre-selected with the +datetime+.
def select_datetime(datetime = Time.now, options = {})
select_year(datetime, options) + select_month(datetime, options) + select_day(datetime, options) +
select_hour(datetime, options) + select_minute(datetime, options)
# It's also possible to explicitly set the order of the tags using the <tt>:order</tt> option with an array of
# symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. If you do not supply a Symbol, it
# will be 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
# 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 = {})
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
# 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.
# Override the field name using the <tt>:field_name</tt> option, 'second' by default.
def select_second(datetime, options = {})
second_options = []
0.upto(59) do |second|
second_options << ((datetime && (datetime.kind_of?(Fixnum) ? datetime : datetime.sec) == second) ?
%(<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)
)
val = datetime ? (datetime.kind_of?(Fixnum) ? datetime : datetime.sec) : ''
if options[:use_hidden]
options[:include_seconds] ? hidden_html(options[:field_name] || 'second', val, options) : ''
else
second_options = []
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
select_html(options[:field_name] || 'second', second_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled])
end
# Returns a select tag with options for each of the minutes 0 through 59 with the current minute selected.
@ -128,84 +188,100 @@ module ActionView
# 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.
def select_minute(datetime, options = {})
minute_options = []
0.step(59, options[:minute_step] || 1) do |minute|
minute_options << ((datetime && (datetime.kind_of?(Fixnum) ? datetime : datetime.min) == minute) ?
%(<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)
)
end
select_html(options[:field_name] || 'minute', minute_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled])
val = datetime ? (datetime.kind_of?(Fixnum) ? datetime : datetime.min) : ''
if options[:use_hidden]
hidden_html(options[:field_name] || 'minute', val, options)
else
minute_options = []
0.step(59, options[:minute_step] || 1) do |minute|
minute_options << ((val == minute) ?
%(<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)
)
end
select_html(options[:field_name] || 'minute', minute_options, options)
end
end
# 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.
# Override the field name using the <tt>:field_name</tt> option, 'hour' by default.
def select_hour(datetime, options = {})
hour_options = []
0.upto(23) do |hour|
hour_options << ((datetime && (datetime.kind_of?(Fixnum) ? datetime : datetime.hour) == hour) ?
%(<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)
)
val = datetime ? (datetime.kind_of?(Fixnum) ? datetime : datetime.hour) : ''
if options[:use_hidden]
hidden_html(options[:field_name] || 'hour', val, options)
else
hour_options = []
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
select_html(options[:field_name] || 'hour', hour_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled])
end
# Returns a select tag with options for each of the days 1 through 31 with the current day selected.
# The <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.
def select_day(date, options = {})
day_options = []
1.upto(31) do |day|
day_options << ((date && (date.kind_of?(Fixnum) ? date : date.day) == day) ?
%(<option value="#{day}" selected="selected">#{day}</option>\n) :
%(<option value="#{day}">#{day}</option>\n)
)
val = date ? (date.kind_of?(Fixnum) ? date : date.day) : ''
if options[:use_hidden]
hidden_html(options[:field_name] || 'day', val, options)
else
day_options = []
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
select_html(options[:field_name] || 'day', day_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled])
end
# Returns a select tag with options for each of the months January through December with the current month selected.
# The month names are presented as keys (what's shown to the user) and the month numbers (1-12) are used as values
# (what's submitted to the server). It's also possible to use month numbers for the presentation instead of names --
# set the <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, :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, :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.
#
# 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 = {})
month_options = []
month_names = options[:use_short_month] ? Date::ABBR_MONTHNAMES : Date::MONTHNAMES
val = date ? (date.kind_of?(Fixnum) ? date : date.month) : ''
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_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]
month_options << ((val == month_number) ?
%(<option value="#{month_number}" selected="selected">#{month_name}</option>\n) :
%(<option value="#{month_number}">#{month_name}</option>\n)
)
end
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)
)
select_html(options[:field_name] || 'month', month_options, options)
end
select_html(options[:field_name] || 'month', month_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled])
end
# Returns a select tag with options for each of the five years on each side of the current, which is selected. The five year radius
@ -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 => 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.
def select_year(date, options = {})
year_options = []
y = date ? (date.kind_of?(Fixnum) ? (y = (date == 0) ? Date.today.year : date) : date.year) : Date.today.year
val = date ? (date.kind_of?(Fixnum) ? date : date.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)
step_val = start_year < end_year ? 1 : -1
start_year.step(end_year, step_val) do |year|
year_options << ((date && (date.kind_of?(Fixnum) ? date : date.year) == year) ?
%(<option value="#{year}" selected="selected">#{year}</option>\n) :
%(<option value="#{year}">#{year}</option>\n)
)
start_year, end_year = (options[:start_year] || y-5), (options[:end_year] || y+5)
step_val = start_year < end_year ? 1 : -1
start_year.step(end_year, step_val) do |year|
year_options << ((val == year) ?
%(<option value="#{year}" selected="selected">#{year}</option>\n) :
%(<option value="#{year}">#{year}</option>\n)
)
end
select_html(options[:field_name] || 'year', year_options, options)
end
select_html(options[:field_name] || 'year', year_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled])
end
private
def select_html(type, options, prefix = nil, include_blank = false, discard_type = false, disabled = false)
select_html = %(<select name="#{prefix || DEFAULT_PREFIX})
select_html << "[#{type}]" unless discard_type
select_html << %(")
select_html << %( disabled="disabled") if disabled
def select_html(type, html_options, options)
name_and_id_from_options(options, type)
select_html = %(<select id="#{options[:id]}" name="#{options[:name]}")
select_html << %( disabled="disabled") if options[:disabled]
select_html << %(>\n)
select_html << %(<option value=""></option>\n) if include_blank
select_html << options.to_s
select_html << %(<option value=""></option>\n) if options[:include_blank]
select_html << html_options.to_s
select_html << "</select>\n"
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)
number > 9 ? number : "0#{number}"
end
@ -255,43 +345,71 @@ module ActionView
include DateHelper
def to_date_select_tag(options = {})
defaults = { :discard_type => true }
options = defaults.merge(options)
options_with_prefix = Proc.new { |position| options.merge(:prefix => "#{@object_name}[#{@method_name}(#{position}i)]") }
date = options[:include_blank] ? (value || 0) : (value || Date.today)
date_or_time_select options.merge(:discard_hour => true)
end
date_select = ''
options[:order] = [:month, :year, :day] if options[:month_before_year] # For backwards compatibility
options[:order] ||= [:year, :month, :day]
position = {:year => 1, :month => 2, :day => 3}
discard = {}
discard[:year] = true if options[:discard_year]
discard[:month] = true if options[:discard_month]
discard[:day] = true if options[:discard_day] or options[:discard_month]
options[:order].each do |param|
date_select << self.send("select_#{param}", date, options_with_prefix.call(position[param])) unless discard[param]
end
date_select
def to_time_select_tag(options = {})
date_or_time_select options.merge(:discard_year => true, :discard_month => true)
end
def to_datetime_select_tag(options = {})
defaults = { :discard_type => true }
options = defaults.merge(options)
options_with_prefix = Proc.new { |position| options.merge(:prefix => "#{@object_name}[#{@method_name}(#{position}i)]") }
datetime = options[:include_blank] ? (value || nil) : (value || Time.now)
datetime_select = select_year(datetime, options_with_prefix.call(1))
datetime_select << select_month(datetime, options_with_prefix.call(2)) unless options[:discard_month]
datetime_select << select_day(datetime, options_with_prefix.call(3)) unless options[:discard_day] || options[:discard_month]
datetime_select << ' &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
date_or_time_select options
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
class FormBuilder
@ -299,6 +417,10 @@ module ActionView
@template.date_select(@object_name, method, options.merge(:object => @object))
end
def time_select(method, options = {})
@template.time_select(@object_name, method, options.merge(:object => @object))
end
def datetime_select(method, options = {})
@template.datetime_select(@object_name, method, options.merge(:object => @object))
end

View file

@ -7,7 +7,7 @@ module ActionView
begin
Marshal::dump(object)
"<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
"<code class='debug_dump'>#{h(object.inspect)}</code>"
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.
# 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?
options = args.last.is_a?(Hash) ? args.pop : {}
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
# 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
@object = object
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
@ -250,7 +256,7 @@ module ActionView
options.delete("size")
end
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)
tag("input", options)
end
@ -259,9 +265,15 @@ module ActionView
options = DEFAULT_RADIO_OPTIONS.merge(options.stringify_keys)
options["type"] = "radio"
options["value"] = tag_value
options["checked"] = "checked" if value.to_s == tag_value.to_s
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
options["id"] = @auto_index ?
options["id"] ||= defined?(@auto_index) ?
"#{@object_name}_#{@auto_index}_#{@method_name}_#{pretty_tag_value}" :
"#{@object_name}_#{@method_name}_#{pretty_tag_value}"
add_default_name_and_id(options)
@ -271,14 +283,82 @@ module ActionView
def to_text_area_tag(options = {})
options = DEFAULT_TEXT_AREA_OPTIONS.merge(options.stringify_keys)
add_default_name_and_id(options)
content_tag("textarea", html_escape(options.delete('value') || value_before_type_cast), options)
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
def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0")
options = options.stringify_keys
options["type"] = "checkbox"
options["value"] = checked_value
checked = case value
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
value
when NilClass
@ -290,55 +370,10 @@ module ActionView
else
value.to_i != 0
end
if checked || options["checked"] == "checked"
options["checked"] = "checked"
else
options.delete("checked")
end
add_default_name_and_id(options)
tag("input", options) << tag("input", "name" => options["name"], "type" => "hidden", "value" => unchecked_value)
end
def to_date_tag()
defaults = DEFAULT_DATE_OPTIONS.dup
date = value || Date.today
options = Proc.new { |position| defaults.merge(:prefix => "#{@object_name}[#{@method_name}(#{position}i)]") }
html_day_select(date, options.call(3)) +
html_month_select(date, options.call(2)) +
html_year_select(date, options.call(1))
end
def to_boolean_select_tag(options = {})
options = options.stringify_keys
add_default_name_and_id(options)
tag_text = "<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)
def radio_button_checked?(value, checked_value)
value.to_s == checked_value.to_s
end
end
@ -348,11 +383,11 @@ module ActionView
options["name"] ||= tag_name_with_index(options["index"])
options["id"] ||= tag_id_with_index(options["index"])
options.delete("index")
elsif @auto_index
elsif defined?(@auto_index)
options["name"] ||= tag_name_with_index(@auto_index)
options["id"] ||= tag_id_with_index(@auto_index)
else
options["name"] ||= tag_name
options["name"] ||= tag_name + (options.has_key?('multiple') ? '[]' : '')
options["id"] ||= tag_id
end
end
@ -379,7 +414,7 @@ module ActionView
class_inheritable_accessor :field_helpers
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)
@object_name, @object, @template, @options, @proc = object_name, object, template, options, proc
@ -403,4 +438,9 @@ module ActionView
end
end
end
class Base
cattr_accessor :default_form_builder
self.default_form_builder = ::ActionView::Helpers::FormBuilder
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,
#
# 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:
#
@ -43,7 +43,7 @@ module ActionView
# See options_for_select for the required format of the choices parameter.
#
# Example with @post.person_id => 1:
# select("post", "person_id", Person.find_all.collect {|p| [ p.name, p.id ] }, { :include_blank => true })
# select("post", "person_id", Person.find(:all).collect {|p| [ p.name, p.id ] }, { :include_blank => true })
#
# could become:
#
@ -113,7 +113,6 @@ module ActionView
options_for_select = container.inject([]) do |options, element|
if !element.is_a?(String) and element.respond_to?(:first) and element.respond_to?(:last)
is_selected = ( (selected.respond_to?(:include?) ? selected.include?(element.last) : element.last == selected) )
is_selected = ( (selected.respond_to?(:include?) && !selected.is_a?(String) ? selected.include?(element.last) : element.last == selected) )
if is_selected
options << "<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>"
end
else
is_selected = ( (selected.respond_to?(:include?) ? selected.include?(element) : element == selected) )
is_selected = ( (selected.respond_to?(:include?) && !selected.is_a?(String) ? selected.include?(element) : element == selected) )
options << ((is_selected) ? "<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
@ -299,13 +297,15 @@ module ActionView
def to_select_tag(choices, options, html_options)
html_options = html_options.stringify_keys
add_default_name_and_id(html_options)
value = value(object)
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
def to_collection_select_tag(collection, value_method, text_method, options, html_options)
html_options = html_options.stringify_keys
add_default_name_and_id(html_options)
value = value(object)
content_tag(
"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)
html_options = html_options.stringify_keys
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)
end
def to_time_zone_select_tag(priority_zones, options, html_options)
html_options = html_options.stringify_keys
add_default_name_and_id(html_options)
value = value(object)
content_tag("select",
add_options(
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
# 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:
# * <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".
def form_tag(url_for_options = {}, options = {}, *parameters_for_url, &proc)
html_options = { "method" => "post" }.merge(options.stringify_keys)
# * <tt>:method</tt> - The method to use when submitting the form, usually either "get" or "post".
# If "put", "delete", or another verb is used, a hidden input with name _method
# 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["action"] = url_for(url_for_options, *parameters_for_url)
tag :form, html_options, true
html_options["action"] = url_for(url_for_options, *parameters_for_url)
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
alias_method :start_form_tag, :form_tag
@ -28,6 +63,8 @@ module ActionView
def end_form_tag
"</form>"
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
# choice selection box.
@ -110,7 +147,8 @@ module ActionView
# Creates a radio button.
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
tag :input, html_options
end

View file

@ -7,6 +7,8 @@ module ActionView
# editing relies on ActionController::Base.in_place_edit_for and the autocompletion relies on
# ActionController::Base.auto_complete_for.
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
# editor of a property.
#
@ -27,20 +29,21 @@ module ActionView
# <tt>:url</tt>:: Specifies the url where the updated value should
# be sent after the user presses "ok".
#
#
# Addtional +options+ are:
# <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>: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>: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>: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>:with</tt>:: JavaScript snippet that should return what is to be sent
# 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>:click_to_edit_text</tt>::The text shown during mouseover the editable text (default: "Click to edit")
def in_place_editor(field_id, options = {})
function = "new Ajax.InPlaceEditor("
function << "'#{field_id}', "
@ -50,6 +53,7 @@ module ActionView
js_options['cancelText'] = %('#{options[:cancel_text]}') if options[:cancel_text]
js_options['okText'] = %('#{options[:save_text]}') if options[:save_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['cols'] = options[:cols] if options[:cols]
js_options['size'] = options[:size] if options[:size]
@ -58,6 +62,7 @@ module ActionView
js_options['ajaxOptions'] = options[:options] if options[:options]
js_options['evalScripts'] = options[:script] if options[:script]
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 << ')'
@ -65,6 +70,8 @@ module ActionView
javascript_tag(function)
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.
#
# 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)
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
# 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.
#
# 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.
#
# 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).
#
# Required +options+ are:
# <tt>:url</tt>:: URL to call for autocompletion results
# in url_for format.
# <tt>:url</tt>:: URL to call for autocompletion results
# in url_for format.
#
# Addtional +options+ are:
# <tt>:update</tt>:: Specifies the DOM ID of the element whose
# innerHTML should be updated with the autocomplete
# entries returned by the AJAX request.
# Defaults to field_id + '_auto_complete'
# <tt>:with</tt>:: A JavaScript expression specifying the
# parameters for the XMLHttpRequest. This defaults
# to 'fieldname=value'.
# <tt>:frequency</tt>:: Determines the time to wait after the last keystroke
# for the AJAX request to be initiated.
# <tt>:indicator</tt>:: Specifies the DOM ID of an element which will be
# displayed while autocomplete is running.
# <tt>:tokens</tt>:: A string or an array of strings containing
# separator tokens for tokenized incremental
# autocompletion. Example: <tt>:tokens => ','</tt> would
# allow multiple autocompletion entries, separated
# by commas.
# <tt>:min_chars</tt>:: The minimum number of characters that should be
# in the input field before an Ajax call is made
# to the server.
# <tt>:on_hide</tt>:: A Javascript expression that is called when the
# autocompletion div is hidden. The expression
# should take two variables: element and update.
# Element is a DOM element for the field, update
# is a DOM element for the div from which the
# innerHTML is replaced.
# <tt>:on_show</tt>:: Like on_hide, only now the expression is called
# then the div is shown.
# <tt>:after_update_element</tt>:: A Javascript expression that is called when the
# user has selected one of the proposed values.
# The expression should take two variables: element and value.
# Element is a DOM element for the field, value
# is the value selected by the user.
# <tt>:select</tt>:: Pick the class of the element from which the value for
# insertion should be extracted. If this is not specified,
# the entire element is used.
# <tt>:update</tt>:: Specifies the DOM ID of the element whose
# innerHTML should be updated with the autocomplete
# entries returned by the AJAX request.
# Defaults to <tt>field_id</tt> + '_auto_complete'
# <tt>:with</tt>:: A JavaScript expression specifying the
# parameters for the XMLHttpRequest. This defaults
# to 'fieldname=value'.
# <tt>:frequency</tt>:: Determines the time to wait after the last keystroke
# for the AJAX request to be initiated.
# <tt>:indicator</tt>:: Specifies the DOM ID of an element which will be
# displayed while autocomplete is running.
# <tt>:tokens</tt>:: A string or an array of strings containing
# separator tokens for tokenized incremental
# autocompletion. Example: <tt>:tokens => ','</tt> would
# allow multiple autocompletion entries, separated
# by commas.
# <tt>:min_chars</tt>:: The minimum number of characters that should be
# in the input field before an Ajax call is made
# to the server.
# <tt>:on_hide</tt>:: A Javascript expression that is called when the
# autocompletion div is hidden. The expression
# should take two variables: element and update.
# Element is a DOM element for the field, update
# is a DOM element for the div from which the
# innerHTML is replaced.
# <tt>:on_show</tt>:: Like on_hide, only now the expression is called
# then the div is shown.
# <tt>:after_update_element</tt>:: A Javascript expression that is called when the
# user has selected one of the proposed values.
# The expression should take two variables: element and value.
# Element is a DOM element for the field, value
# is the value selected by the user.
# <tt>:select</tt>:: Pick the class of the element from which the value for
# insertion should be extracted. If this is not specified,
# the entire element is used.
def auto_complete_field(field_id, options = {})
function = "var #{field_id}_auto_completer = new Ajax.Autocompleter("
function << "'#{field_id}', "
@ -141,6 +150,7 @@ module ActionView
js_options[:callback] = "function(element, value) { return #{options[:with]} }" if options[:with]
js_options[:indicator] = "'#{options[:indicator]}'" if options[:indicator]
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]
{ :after_update_element => :afterUpdateElement,
@ -153,6 +163,8 @@ module ActionView
javascript_tag(function)
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.
#
# Example action:
@ -161,7 +173,7 @@ module ActionView
# @items = Item.find(:all,
# :conditions => [ 'LOWER(description) LIKE ?',
# '%' + request.raw_post.downcase + '%' ])
# render :inline => '<%= auto_complete_result(@items, 'description') %>'
# render :inline => "<%= auto_complete_result(@items, 'description') %>"
# end
#
# 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)
end
# DEPRECATION WARNING: This method will become a separate plugin when Rails 2.0 ships.
#
# Wrapper for text_field with added AJAX autocompletion functionality.
#
# 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 = {})
(completion_options[:skip_style] ? "" : auto_complete_stylesheet) +
text_field(object, method, tag_options) +
@ -187,7 +201,7 @@ module ActionView
private
def auto_complete_stylesheet
content_tag("style", <<-EOT
content_tag('style', <<-EOT, :type => 'text/css')
div.auto_complete {
width: 350px;
background: #fff;
@ -212,7 +226,6 @@ module ActionView
padding:0;
}
EOT
)
end
end

View file

@ -1,4 +1,5 @@
require File.dirname(__FILE__) + '/tag_helper'
require File.dirname(__FILE__) + '/prototype_helper'
module ActionView
module Helpers
@ -40,15 +41,50 @@ module ActionView
unless const_defined? :JAVASCRIPT_PATH
JAVASCRIPT_PATH = File.join(File.dirname(__FILE__), 'javascripts')
end
include PrototypeHelper
# Returns a link that'll trigger a JavaScript +function+ using the
# Returns a link that will trigger a JavaScript +function+ using the
# 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:
# link_to_function "Greeting", "alert('Hello world!')"
# link_to_function(image_tag("delete"), "if confirm('Really?'){ do_delete(); }")
def link_to_function(name, function, html_options = {})
# Produces:
# <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!
function = update_page(&block) if block_given?
content_tag(
"a", name,
html_options.merge({
@ -58,14 +94,28 @@ module ActionView
)
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.
#
# 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:
# button_to_function "Greeting", "alert('Hello world!')"
# button_to_function "Delete", "if confirm('Really?'){ do_delete(); }")
def button_to_function(name, function, html_options = {})
# button_to_function "Delete", "if (confirm('Really?')) do_delete()"
# 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!
function = update_page(&block) if block_given?
tag(:input, html_options.merge({
:type => "button", :value => name,
: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.
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
# Returns a JavaScript tag with the +content+ inside. Example:
# javascript_tag "alert('All is good')" # => <script type="text/javascript">alert('All is good')</script>
def javascript_tag(content)
content_tag("script", javascript_cdata_section(content), :type => "text/javascript")
# javascript_tag "alert('All is good')"
#
# 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
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)
// (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
// (c) 2005 Jon Tirsen (http://www.tirsen.com)
// Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// (c) 2005, 2006 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
// (c) 2005, 2006 Jon Tirsen (http://www.tirsen.com)
// Contributors:
// Richard Livsey
// Rahul Bhargava
// 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
// 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
// allows smart autocompletion after linebreaks.
if(typeof Effect == 'undefined')
throw("controls.js requires including script.aculo.us' effects.js library");
var Autocompleter = {}
Autocompleter.Base = function() {};
Autocompleter.Base.prototype = {
@ -45,7 +49,7 @@ Autocompleter.Base.prototype = {
this.index = 0;
this.entryCount = 0;
if (this.setOptions)
if(this.setOptions)
this.setOptions(options);
else
this.options = options || {};
@ -55,17 +59,20 @@ Autocompleter.Base.prototype = {
this.options.frequency = this.options.frequency || 0.4;
this.options.minChars = this.options.minChars || 1;
this.options.onShow = this.options.onShow ||
function(element, update){
if(!update.style.position || update.style.position=='absolute') {
update.style.position = 'absolute';
Position.clone(element, update, {setHeight: false, offsetTop: element.offsetHeight});
}
Effect.Appear(update,{duration:0.15});
};
function(element, update){
if(!update.style.position || update.style.position=='absolute') {
update.style.position = 'absolute';
Position.clone(element, update, {
setHeight: false,
offsetTop: element.offsetHeight
});
}
Effect.Appear(update,{duration:0.15});
};
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.observer = null;
@ -94,7 +101,7 @@ Autocompleter.Base.prototype = {
},
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.update.style.zIndex = 2;
Element.show(this.iefix);
@ -202,11 +209,13 @@ Autocompleter.Base.prototype = {
markPrevious: function() {
if(this.index > 0) this.index--
else this.index = this.entryCount-1;
this.getEntry(this.index).scrollIntoView(true);
},
markNext: function() {
if(this.index < this.entryCount-1) this.index++
else this.index = 0;
this.getEntry(this.index).scrollIntoView(false);
},
getEntry: function(index) {
@ -254,11 +263,11 @@ Autocompleter.Base.prototype = {
if(!this.changed && this.hasFocus) {
this.update.innerHTML = choices;
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.update.firstChild.childNodes.length;
this.update.down().childNodes.length;
for (var i = 0; i < this.entryCount; i++) {
var entry = this.getEntry(i);
entry.autocompleteIndex = i;
@ -269,9 +278,14 @@ Autocompleter.Base.prototype = {
}
this.stopIndicator();
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.options = Object.extend({
paramName: "value",
okButton: true,
okText: "ok",
cancelLink: true,
@ -531,7 +546,7 @@ Ajax.InPlaceEditor.prototype = {
Element.hide(this.element);
this.createForm();
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
if (evt) {
Event.stop(evt);
@ -590,7 +605,7 @@ Ajax.InPlaceEditor.prototype = {
var textField = document.createElement("input");
textField.obj = this;
textField.type = "text";
textField.name = "value";
textField.name = this.options.paramName;
textField.value = text;
textField.style.backgroundColor = this.options.highlightcolor;
textField.className = 'editor_field';
@ -603,7 +618,7 @@ Ajax.InPlaceEditor.prototype = {
this.options.textarea = true;
var textArea = document.createElement("textarea");
textArea.obj = this;
textArea.name = "value";
textArea.name = this.options.paramName;
textArea.value = this.convertHTMLLineBreaks(text);
textArea.rows = this.options.rows;
textArea.cols = this.options.cols || 40;
@ -636,6 +651,7 @@ Ajax.InPlaceEditor.prototype = {
Element.removeClassName(this.form, this.options.loadingClassName);
this.editField.disabled = false;
this.editField.value = transport.responseText.stripTags();
Field.scrollFreeActivate(this.editField);
},
onclickCancel: function() {
this.onComplete();
@ -772,6 +788,8 @@ Object.extend(Ajax.InPlaceCollectionEditor.prototype, {
collection.each(function(e,i) {
optionTag = document.createElement("option");
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;
optionTag.appendChild(document.createTextNode((e instanceof Array) ? e[1] : e));
selectTag.appendChild(optionTag);

View file

@ -1,9 +1,11 @@
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// (c) 2005 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
// Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// (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 = {
drops: [],
@ -145,8 +147,16 @@ var Draggables = {
},
activate: function(draggable) {
window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
this.activeDraggable = draggable;
if(draggable.options.delay) {
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() {
@ -160,10 +170,15 @@ var Draggables = {
// the same coordinates, prevent needless redrawing (moz bug?)
if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
this._lastPointer = pointer;
this.activeDraggable.updateDrag(event, pointer);
},
endDrag: function(event) {
if(this._timeout) {
clearTimeout(this._timeout);
this._timeout = null;
}
if(!this.activeDraggable) return;
this._lastPointer = null;
this.activeDraggable.endDrag(event);
@ -190,6 +205,7 @@ var Draggables = {
this.observers.each( function(o) {
if(o[eventName]) o[eventName](eventName, draggable, event);
});
if(draggable.options[eventName]) draggable.options[eventName](draggable, event);
},
_cacheObserverCallbacks: function() {
@ -204,39 +220,59 @@ var Draggables = {
/*--------------------------------------------------------------------------*/
var Draggable = Class.create();
Draggable._dragging = {};
Draggable.prototype = {
initialize: function(element) {
var options = Object.extend({
var defaults = {
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) {
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) {
new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0});
endeffect: function(element) {
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,
revert: false,
scroll: false,
scrollSensitivity: 20,
scrollSpeed: 15,
snap: false // false, or xy or [x,y] or function(x,y){ return [x,y] }
}, arguments[1] || {});
snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] }
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);
if(options.handle && (typeof options.handle == 'string')) {
var h = Element.childrenWithClassName(this.element, options.handle, true);
if(h.length>0) this.handle = h[0];
}
if(options.handle && (typeof options.handle == 'string'))
this.handle = this.element.down('.'+options.handle, 0);
if(!this.handle) this.handle = $(options.handle);
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);
this._isScrollChild = Element.childOf(this.element, options.scroll);
}
Element.makePositioned(this.element); // fix IE
@ -262,6 +298,8 @@ Draggable.prototype = {
},
initDrag: function(event) {
if(typeof Draggable._dragging[this.element] != 'undefined' &&
Draggable._dragging[this.element]) return;
if(Event.isLeftClick(event)) {
// abort on form elements, fixes a Firefox issue
var src = Event.element(event);
@ -272,11 +310,6 @@ Draggable.prototype = {
src.tagName=='BUTTON' ||
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 pos = Position.cumulativeOffset(this.element);
this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });
@ -312,6 +345,7 @@ Draggable.prototype = {
}
Draggables.notify('onStart', this, event);
if(this.options.starteffect) this.options.starteffect(this.element);
},
@ -320,6 +354,7 @@ Draggable.prototype = {
Position.prepare();
Droppables.show(pointer, this.element);
Draggables.notify('onDrag', this, event);
this.draw(pointer);
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 ]; }
} else {
p = Position.page(this.options.scroll);
p[0] += this.options.scroll.scrollLeft;
p[1] += this.options.scroll.scrollTop;
p[0] += this.options.scroll.scrollLeft + Position.deltaX;
p[1] += this.options.scroll.scrollTop + Position.deltaY;
p.push(p[0]+this.options.scroll.offsetWidth);
p.push(p[1]+this.options.scroll.offsetHeight);
}
@ -378,7 +413,7 @@ Draggable.prototype = {
if(this.options.endeffect)
this.options.endeffect(this.element);
Draggables.deactivate(this);
Droppables.reset();
},
@ -398,10 +433,15 @@ Draggable.prototype = {
draw: function(point) {
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();
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[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
}
@ -412,7 +452,7 @@ Draggable.prototype = {
if(this.options.snap) {
if(typeof this.options.snap == 'function') {
p = this.options.snap(p[0],p[1]);
p = this.options.snap(p[0],p[1],this);
} else {
if(this.options.snap instanceof Array) {
p = p.map( function(v, i) {
@ -428,6 +468,7 @@ Draggable.prototype = {
style.left = p[0] + "px";
if((!this.options.constraint) || (this.options.constraint=='vertical'))
style.top = p[1] + "px";
if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
},
@ -440,6 +481,7 @@ Draggable.prototype = {
},
startScrolling: function(speed) {
if(!(speed[0] || speed[1])) return;
this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed];
this.lastScrolled = new Date();
this.scrollInterval = setInterval(this.scroll.bind(this), 10);
@ -464,14 +506,16 @@ Draggable.prototype = {
Position.prepare();
Droppables.show(Draggables._lastPointer, this.element);
Draggables.notify('onDrag', this);
Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer);
Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000;
Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000;
if (Draggables._lastScrollPointer[0] < 0)
Draggables._lastScrollPointer[0] = 0;
if (Draggables._lastScrollPointer[1] < 0)
Draggables._lastScrollPointer[1] = 0;
this.draw(Draggables._lastScrollPointer);
if (this._isScrollChild) {
Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer);
Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000;
Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000;
if (Draggables._lastScrollPointer[0] < 0)
Draggables._lastScrollPointer[0] = 0;
if (Draggables._lastScrollPointer[1] < 0)
Draggables._lastScrollPointer[1] = 0;
this.draw(Draggables._lastScrollPointer);
}
if(this.options.change) this.options.change(this);
},
@ -523,6 +567,8 @@ SortableObserver.prototype = {
}
var Sortable = {
SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/,
sortables: {},
_findRootElement: function(element) {
@ -563,12 +609,13 @@ var Sortable = {
containment: element, // also takes array of elements (or id's); or false
handle: false, // or a CSS class
only: false,
delay: 0,
hoverclass: null,
ghosting: false,
scroll: false,
scrollSensitivity: 20,
scrollSpeed: 15,
format: /^[^_]*_(.*)$/,
format: this.SERIALIZE_RULE,
onChange: Prototype.emptyFunction,
onUpdate: Prototype.emptyFunction
}, arguments[1] || {});
@ -582,6 +629,7 @@ var Sortable = {
scroll: options.scroll,
scrollSpeed: options.scrollSpeed,
scrollSensitivity: options.scrollSensitivity,
delay: options.delay,
ghosting: options.ghosting,
constraint: options.constraint,
handle: options.handle };
@ -610,7 +658,6 @@ var Sortable = {
tree: options.tree,
hoverclass: options.hoverclass,
onHover: Sortable.onHover
//greedy: !options.dropOnEmpty
}
var options_for_tree = {
@ -635,7 +682,7 @@ var Sortable = {
(this.findElements(element, options) || []).each( function(e) {
// handles are per-draggable
var handle = options.handle ?
Element.childrenWithClassName(e, options.handle)[0] : e;
$(e).down('.'+options.handle,0) : e;
options.draggables.push(
new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
Droppables.add(e, options_for_droppable);
@ -706,7 +753,7 @@ var Sortable = {
if(!Element.isParent(dropon, element)) {
var index;
var children = Sortable.findElements(dropon, {tag: droponOptions.tag});
var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only});
var child = null;
if(children) {
@ -733,7 +780,7 @@ var Sortable = {
},
unmark: function() {
if(Sortable._marker) Element.hide(Sortable._marker);
if(Sortable._marker) Sortable._marker.hide();
},
mark: function(dropon, position) {
@ -742,23 +789,21 @@ var Sortable = {
if(sortable && !sortable.ghosting) return;
if(!Sortable._marker) {
Sortable._marker = $('dropmarker') || document.createElement('DIV');
Element.hide(Sortable._marker);
Element.addClassName(Sortable._marker, 'dropmarker');
Sortable._marker.style.position = 'absolute';
Sortable._marker =
($('dropmarker') || Element.extend(document.createElement('DIV'))).
hide().addClassName('dropmarker').setStyle({position:'absolute'});
document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
}
var offsets = Position.cumulativeOffset(dropon);
Sortable._marker.style.left = offsets[0] + 'px';
Sortable._marker.style.top = offsets[1] + 'px';
Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'});
if(position=='after')
if(sortable.overlap == 'horizontal')
Sortable._marker.style.left = (offsets[0]+dropon.clientWidth) + 'px';
Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'});
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) {
@ -773,9 +818,9 @@ var Sortable = {
id: encodeURIComponent(match ? match[1] : null),
element: element,
parent: parent,
children: new Array,
children: [],
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 */
@ -788,17 +833,6 @@ var Sortable = {
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) {
element = $(element);
var sortableOptions = this.options(element);
@ -813,12 +847,12 @@ var Sortable = {
var root = {
id: null,
parent: null,
children: new Array,
children: [],
container: element,
position: 0
}
return Sortable._tree (element, options, root);
return Sortable._tree(element, options, root);
},
/* Construct a [i] index for a particular node */
@ -867,7 +901,7 @@ var Sortable = {
if (options.tree) {
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));
}).flatten().join('&');
} 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) {
if (!child.parentNode || child == element) return false;
if (child.parentNode == element) return true;
return Element.isParent(child.parentNode, element);
}
@ -906,8 +938,5 @@ Element.findChildren = function(element, only, recursive, tagName) {
}
Element.offsetSize = function (element, type) {
if (type == 'vertical' || type == 'height')
return element.offsetHeight;
else
return element.offsetWidth;
}
return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')];
}

View file

@ -1,15 +1,16 @@
// 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:
// Justin Palmer (http://encytemedia.com/)
// Mark Pilgrim (http://diveintomark.org/)
// 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,
// returns self (or first argument) if not convertable
String.prototype.parseColor = function() {
var color = '#';
var color = '#';
if(this.slice(0,4) == 'rgb(') {
var cols = this.slice(4,this.length-1).split(',');
var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3);
@ -41,15 +42,17 @@ Element.collectTextNodesIgnoreClass = function(element, className) {
Element.setContentZoom = function(element, percent) {
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);
return element;
}
Element.getOpacity = function(element){
Element.getOpacity = function(element){
element = $(element);
var opacity;
if (opacity = Element.getStyle(element, 'opacity'))
if (opacity = element.getStyle('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;
return 1.0;
}
@ -57,34 +60,26 @@ Element.getOpacity = function(element){
Element.setOpacity = function(element, value){
element= $(element);
if (value == 1){
Element.setStyle(element, { opacity:
element.setStyle({ opacity:
(/Gecko/.test(navigator.userAgent) && !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ?
0.999999 : null });
if(/MSIE/.test(navigator.userAgent))
Element.setStyle(element, {filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')});
0.999999 : 1.0 });
if(/MSIE/.test(navigator.userAgent) && !window.opera)
element.setStyle({filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')});
} else {
if(value < 0.00001) value = 0;
Element.setStyle(element, {opacity: value});
if(/MSIE/.test(navigator.userAgent))
Element.setStyle(element,
{ filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'') +
'alpha(opacity='+value*100+')' });
element.setStyle({opacity: value});
if(/MSIE/.test(navigator.userAgent) && !window.opera)
element.setStyle(
{ filter: element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'') +
'alpha(opacity='+value*100+')' });
}
return element;
}
Element.getInlineOpacity = function(element){
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) {
try {
element = $(element);
@ -104,9 +99,17 @@ Array.prototype.call = function() {
/*--------------------------------------------------------------------------*/
var Effect = {
_elementDoesNotExistError: {
name: 'ElementDoesNotExistError',
message: 'The specified DOM element does not exist, but is required for this effect to operate'
},
tagifyText: function(element) {
if(typeof Builder == 'undefined')
throw("Effect.tagifyText requires including script.aculo.us' builder.js library");
var tagifyStyle = 'position:relative';
if(/MSIE/.test(navigator.userAgent)) tagifyStyle += ';zoom:1';
if(/MSIE/.test(navigator.userAgent) && !window.opera) tagifyStyle += ';zoom:1';
element = $(element);
$A(element.childNodes).each( function(child) {
if(child.nodeType==3) {
@ -159,33 +162,35 @@ var Effect2 = Effect; // deprecated
/* ------------- transitions ------------- */
Effect.Transitions = {}
Effect.Transitions.linear = function(pos) {
return pos;
}
Effect.Transitions.sinoidal = function(pos) {
return (-Math.cos(pos*Math.PI)/2) + 0.5;
}
Effect.Transitions.reverse = function(pos) {
return 1-pos;
}
Effect.Transitions.flicker = function(pos) {
return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
}
Effect.Transitions.wobble = function(pos) {
return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
}
Effect.Transitions.pulse = function(pos) {
return (Math.floor(pos*10) % 2 == 0 ?
(pos*10-Math.floor(pos*10)) : 1-(pos*10-Math.floor(pos*10)));
}
Effect.Transitions.none = function(pos) {
return 0;
}
Effect.Transitions.full = function(pos) {
return 1;
}
Effect.Transitions = {
linear: Prototype.K,
sinoidal: function(pos) {
return (-Math.cos(pos*Math.PI)/2) + 0.5;
},
reverse: function(pos) {
return 1-pos;
},
flicker: function(pos) {
return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
},
wobble: function(pos) {
return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
},
pulse: function(pos, pulses) {
pulses = pulses || 5;
return (
Math.round((pos % (1/pulses)) * pulses) == 0 ?
((pos * pulses * 2) - Math.floor(pos * pulses * 2)) :
1 - ((pos * pulses * 2) - Math.floor(pos * pulses * 2))
);
},
none: function(pos) {
return 0;
},
full: function(pos) {
return 1;
}
};
/* ------------- core effects ------------- */
@ -212,6 +217,9 @@ Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), {
e.finishOn += effect.finishOn;
});
break;
case 'with-last':
timestamp = this.effects.pluck('startOn').max() || timestamp;
break;
case 'end':
// start effect after last queued effect has finished
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();
Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), {
initialize: function(element) {
this.element = $(element);
if(!this.element) throw(Effect._elementDoesNotExistError);
// 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});
var options = Object.extend({
from: this.element.getOpacity() || 0.0,
@ -370,6 +390,7 @@ Effect.Move = Class.create();
Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), {
initialize: function(element) {
this.element = $(element);
if(!this.element) throw(Effect._elementDoesNotExistError);
var options = Object.extend({
x: 0,
y: 0,
@ -393,8 +414,8 @@ Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), {
},
update: function(position) {
this.element.setStyle({
left: this.options.x * position + this.originalLeft + 'px',
top: this.options.y * position + this.originalTop + 'px'
left: Math.round(this.options.x * position + this.originalLeft) + '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();
Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), {
initialize: function(element, percent) {
this.element = $(element)
this.element = $(element);
if(!this.element) throw(Effect._elementDoesNotExistError);
var options = Object.extend({
scaleX: true,
scaleY: true,
@ -433,7 +455,7 @@ Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), {
this.originalLeft = this.element.offsetLeft;
var fontSize = this.element.getStyle('font-size') || '100%';
['em','px','%'].each( function(fontSizeType) {
['em','px','%','pt'].each( function(fontSizeType) {
if(fontSize.indexOf(fontSizeType)>0) {
this.fontSize = parseFloat(fontSize);
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);
},
finish: function(position) {
if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle);
if(this.restoreAfterFinish) this.element.setStyle(this.originalStyle);
},
setDimensions: function(height, width) {
var d = {};
if(this.options.scaleX) d.width = width + 'px';
if(this.options.scaleY) d.height = height + 'px';
if(this.options.scaleX) d.width = Math.round(width) + 'px';
if(this.options.scaleY) d.height = Math.round(height) + 'px';
if(this.options.scaleFromCenter) {
var topd = (height - this.dims[0])/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), {
initialize: function(element) {
this.element = $(element);
if(!this.element) throw(Effect._elementDoesNotExistError);
var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || {});
this.start(options);
},
@ -547,8 +570,7 @@ Effect.Fade = function(element) {
to: 0.0,
afterFinishInternal: function(effect) {
if(effect.options.to!=0) return;
effect.element.hide();
effect.element.setStyle({opacity: oldOpacity});
effect.element.hide().setStyle({opacity: oldOpacity});
}}, arguments[1] || {});
return new Effect.Opacity(element,options);
}
@ -563,25 +585,31 @@ Effect.Appear = function(element) {
effect.element.forceRerendering();
},
beforeSetup: function(effect) {
effect.element.setOpacity(effect.options.from);
effect.element.show();
effect.element.setOpacity(effect.options.from).show();
}}, arguments[1] || {});
return new Effect.Opacity(element,options);
}
Effect.Puff = function(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(
[ new Effect.Scale(element, 200,
{ sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }),
new Effect.Opacity(element, { sync: true, to: 0.0 } ) ],
Object.extend({ duration: 1.0,
beforeSetupInternal: function(effect) {
effect.effects[0].element.setStyle({position: 'absolute'}); },
Position.absolutize(effect.effects[0].element)
},
afterFinishInternal: function(effect) {
effect.effects[0].element.hide();
effect.effects[0].element.setStyle(oldStyle); }
effect.effects[0].element.hide().setStyle(oldStyle); }
}, arguments[1] || {})
);
}
@ -589,13 +617,12 @@ Effect.Puff = function(element) {
Effect.BlindUp = function(element) {
element = $(element);
element.makeClipping();
return new Effect.Scale(element, 0,
return new Effect.Scale(element, 0,
Object.extend({ scaleContent: false,
scaleX: false,
restoreAfterFinish: true,
afterFinishInternal: function(effect) {
effect.element.hide();
effect.element.undoClipping();
effect.element.hide().undoClipping();
}
}, arguments[1] || {})
);
@ -604,28 +631,25 @@ Effect.BlindUp = function(element) {
Effect.BlindDown = function(element) {
element = $(element);
var elementDimensions = element.getDimensions();
return new Effect.Scale(element, 100,
Object.extend({ scaleContent: false,
scaleX: false,
scaleFrom: 0,
scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
restoreAfterFinish: true,
afterSetup: function(effect) {
effect.element.makeClipping();
effect.element.setStyle({height: '0px'});
effect.element.show();
},
afterFinishInternal: function(effect) {
effect.element.undoClipping();
}
}, arguments[1] || {})
);
return new Effect.Scale(element, 100, Object.extend({
scaleContent: false,
scaleX: false,
scaleFrom: 0,
scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
restoreAfterFinish: true,
afterSetup: function(effect) {
effect.element.makeClipping().setStyle({height: '0px'}).show();
},
afterFinishInternal: function(effect) {
effect.element.undoClipping();
}
}, arguments[1] || {}));
}
Effect.SwitchOff = function(element) {
element = $(element);
var oldOpacity = element.getInlineOpacity();
return new Effect.Appear(element, {
return new Effect.Appear(element, Object.extend({
duration: 0.4,
from: 0,
transition: Effect.Transitions.flicker,
@ -634,18 +658,14 @@ Effect.SwitchOff = function(element) {
duration: 0.3, scaleFromCenter: true,
scaleX: false, scaleContent: false, restoreAfterFinish: true,
beforeSetup: function(effect) {
effect.element.makePositioned();
effect.element.makeClipping();
effect.element.makePositioned().makeClipping();
},
afterFinishInternal: function(effect) {
effect.element.hide();
effect.element.undoClipping();
effect.element.undoPositioned();
effect.element.setStyle({opacity: oldOpacity});
effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity});
}
})
}
});
}, arguments[1] || {}));
}
Effect.DropOut = function(element) {
@ -663,9 +683,7 @@ Effect.DropOut = function(element) {
effect.effects[0].element.makePositioned();
},
afterFinishInternal: function(effect) {
effect.effects[0].element.hide();
effect.effects[0].element.undoPositioned();
effect.effects[0].element.setStyle(oldStyle);
effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle);
}
}, arguments[1] || {}));
}
@ -687,54 +705,42 @@ Effect.Shake = function(element) {
{ x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
new Effect.Move(effect.element,
{ x: -20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
effect.element.undoPositioned();
effect.element.setStyle(oldStyle);
effect.element.undoPositioned().setStyle(oldStyle);
}}) }}) }}) }}) }}) }});
}
Effect.SlideDown = function(element) {
element = $(element);
element.cleanWhitespace();
element = $(element).cleanWhitespace();
// 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();
return new Effect.Scale(element, 100, Object.extend({
scaleContent: false,
scaleX: false,
scaleFrom: 0,
scaleFrom: window.opera ? 0 : 1,
scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
restoreAfterFinish: true,
afterSetup: function(effect) {
effect.element.makePositioned();
effect.element.firstChild.makePositioned();
effect.element.down().makePositioned();
if(window.opera) effect.element.setStyle({top: ''});
effect.element.makeClipping();
effect.element.setStyle({height: '0px'});
effect.element.show(); },
effect.element.makeClipping().setStyle({height: '0px'}).show();
},
afterUpdateInternal: function(effect) {
effect.element.firstChild.setStyle({bottom:
effect.element.down().setStyle({bottom:
(effect.dims[0] - effect.element.clientHeight) + 'px' });
},
afterFinishInternal: function(effect) {
effect.element.undoClipping();
// IE will crash if child is undoPositioned first
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}); }
effect.element.undoClipping().undoPositioned();
effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); }
}, arguments[1] || {})
);
}
Effect.SlideUp = function(element) {
element = $(element);
element.cleanWhitespace();
var oldInnerBottom = $(element.firstChild).getStyle('bottom');
return new Effect.Scale(element, 0,
element = $(element).cleanWhitespace();
var oldInnerBottom = element.down().getStyle('bottom');
return new Effect.Scale(element, window.opera ? 0 : 1,
Object.extend({ scaleContent: false,
scaleX: false,
scaleMode: 'box',
@ -742,32 +748,32 @@ Effect.SlideUp = function(element) {
restoreAfterFinish: true,
beforeStartInternal: function(effect) {
effect.element.makePositioned();
effect.element.firstChild.makePositioned();
effect.element.down().makePositioned();
if(window.opera) effect.element.setStyle({top: ''});
effect.element.makeClipping();
effect.element.show(); },
effect.element.makeClipping().show();
},
afterUpdateInternal: function(effect) {
effect.element.firstChild.setStyle({bottom:
(effect.dims[0] - effect.element.clientHeight) + 'px' }); },
effect.element.down().setStyle({bottom:
(effect.dims[0] - effect.element.clientHeight) + 'px' });
},
afterFinishInternal: function(effect) {
effect.element.hide();
effect.element.undoClipping();
effect.element.firstChild.undoPositioned();
effect.element.undoPositioned();
effect.element.setStyle({bottom: oldInnerBottom}); }
effect.element.hide().undoClipping().undoPositioned().setStyle({bottom: oldInnerBottom});
effect.element.down().undoPositioned();
}
}, arguments[1] || {})
);
}
// Bug in opera makes the TD containing this element expand for a instance after finish
Effect.Squish = function(element) {
return new Effect.Scale(element, window.opera ? 1 : 0,
{ restoreAfterFinish: true,
beforeSetup: function(effect) {
effect.element.makeClipping(effect.element); },
afterFinishInternal: function(effect) {
effect.element.hide(effect.element);
effect.element.undoClipping(effect.element); }
return new Effect.Scale(element, window.opera ? 1 : 0, {
restoreAfterFinish: true,
beforeSetup: function(effect) {
effect.element.makeClipping();
},
afterFinishInternal: function(effect) {
effect.element.hide().undoClipping();
}
});
}
@ -823,9 +829,7 @@ Effect.Grow = function(element) {
y: initialMoveY,
duration: 0.01,
beforeSetup: function(effect) {
effect.element.hide();
effect.element.makeClipping();
effect.element.makePositioned();
effect.element.hide().makeClipping().makePositioned();
},
afterFinishInternal: function(effect) {
new Effect.Parallel(
@ -836,13 +840,10 @@ Effect.Grow = function(element) {
sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true})
], Object.extend({
beforeSetup: function(effect) {
effect.effects[0].element.setStyle({height: '0px'});
effect.effects[0].element.show();
effect.effects[0].element.setStyle({height: '0px'}).show();
},
afterFinishInternal: function(effect) {
effect.effects[0].element.undoClipping();
effect.effects[0].element.undoPositioned();
effect.effects[0].element.setStyle(oldStyle);
effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle);
}
}, options)
)
@ -896,13 +897,10 @@ Effect.Shrink = function(element) {
new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition })
], Object.extend({
beforeStartInternal: function(effect) {
effect.effects[0].element.makePositioned();
effect.effects[0].element.makeClipping(); },
effect.effects[0].element.makePositioned().makeClipping();
},
afterFinishInternal: function(effect) {
effect.effects[0].element.hide();
effect.effects[0].element.undoClipping();
effect.effects[0].element.undoPositioned();
effect.effects[0].element.setStyle(oldStyle); }
effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); }
}, options)
);
}
@ -912,10 +910,10 @@ Effect.Pulsate = function(element) {
var options = arguments[1] || {};
var oldOpacity = element.getInlineOpacity();
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);
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}); }
}, options), {transition: reverser}));
}
@ -927,7 +925,7 @@ Effect.Fold = function(element) {
left: element.style.left,
width: element.style.width,
height: element.style.height };
Element.makeClipping(element);
element.makeClipping();
return new Effect.Scale(element, 5, Object.extend({
scaleContent: false,
scaleX: false,
@ -936,15 +934,147 @@ Effect.Fold = function(element) {
scaleContent: false,
scaleY: false,
afterFinishInternal: function(effect) {
effect.element.hide();
effect.element.undoClipping();
effect.element.setStyle(oldStyle);
effect.element.hide().undoClipping().setStyle(oldStyle);
} });
}}, 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',
'collectTextNodes','collectTextNodesIgnoreClass','childrenWithClassName'].each(
'collectTextNodes','collectTextNodesIgnoreClass','morph'].each(
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 Helpers
# Provides methods for converting a number into a formatted string that currently represents
# one of the following forms: phone number, percentage, money, or precision level.
module Helpers #:nodoc:
# Provides methods for converting a numbers into formatted strings.
# Methods are provided for phone numbers, currency, percentage,
# precision, positional notation, and file size.
module NumberHelper
# Formats a +number+ into a US phone number string. The +options+ can be a hash used to customize the format of the output.
# The area code can be surrounded by parentheses by setting +:area_code+ to true; default is false
# The delimiter can be set using +:delimiter+; default is "-"
# Examples:
# 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
# Formats a +number+ into a US phone number. You can customize the format
# in the +options+ hash.
# * <tt>:area_code</tt> - Adds parentheses around the area code.
# * <tt>:delimiter</tt> - Specifies the delimiter to use, defaults to "-".
# * <tt>:extension</tt> - Specifies an extension to add to the end of the
# generated number
# * <tt>:country_code</tt> - Sets the country code for the phone number.
#
# 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 = {})
options = options.stringify_keys
area_code = options.delete("area_code") { false }
delimiter = options.delete("delimiter") { "-" }
extension = options.delete("extension") { "" }
number = number.to_s.strip unless number.nil?
options = options.stringify_keys
area_code = options["area_code"] || nil
delimiter = options["delimiter"] || "-"
extension = options["extension"].to_s.strip || nil
country_code = options["country_code"] || nil
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")
extension.to_s.strip.empty? ? str : "#{str} x #{extension.to_s.strip}"
str = ""
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
number
end
end
# Formats a +number+ into a currency string. The +options+ hash can be used to customize the format of the output.
# The +number+ can contain a level of precision using the +precision+ key; default is 2
# The currency type can be set using the +unit+ key; default is "$"
# The unit separator can be set using the +separator+ key; default is "."
# The delimiter can be set using the +delimiter+ key; default is ","
# Examples:
# number_to_currency(1234567890.50) => $1,234,567,890.50
# number_to_currency(1234567890.506) => $1,234,567,890.51
# number_to_currency(1234567890.50, {:unit => "&pound;", :separator => ",", :delimiter => ""}) => &pound;1234567890,50
# Formats a +number+ into a currency string. You can customize the format
# in the +options+ hash.
# * <tt>:precision</tt> - Sets the level of precision, defaults to 2
# * <tt>:unit</tt> - Sets the denomination of the currency, defaults to "$"
# * <tt>:separator</tt> - Sets the separator between the units, defaults to "."
# * <tt>:delimiter</tt> - Sets the thousands delimiter, defaults to ","
#
# number_to_currency(1234567890.50) => $1,234,567,890.50
# number_to_currency(1234567890.506) => $1,234,567,890.51
# number_to_currency(1234567890.506, :precision => 3) => $1,234,567,890.506
# number_to_currency(1234567890.50, :unit => "&pound;", :separator => ",", :delimiter => "")
# => &pound;1234567890,50
def number_to_currency(number, options = {})
options = options.stringify_keys
precision, unit, separator, delimiter = options.delete("precision") { 2 }, options.delete("unit") { "$" }, options.delete("separator") { "." }, options.delete("delimiter") { "," }
separator = "" unless precision > 0
options = options.stringify_keys
precision = options["precision"] || 2
unit = options["unit"] || "$"
separator = precision > 0 ? options["separator"] || "." : ""
delimiter = options["delimiter"] || ","
begin
parts = number_with_precision(number, precision).split('.')
unit + number_with_delimiter(parts[0], delimiter) + separator + parts[1].to_s
@ -46,16 +67,19 @@ module ActionView
end
end
# Formats a +number+ as into a percentage string. The +options+ hash can be used to customize the format of the output.
# The +number+ can contain a level of precision using the +precision+ key; default is 3
# The unit separator can be set using the +separator+ key; default is "."
# Examples:
# number_to_percentage(100) => 100.000%
# number_to_percentage(100, {:precision => 0}) => 100%
# number_to_percentage(302.0574, {:precision => 2}) => 302.06%
# Formats a +number+ as a percentage string. You can customize the
# format in the +options+ hash.
# * <tt>:precision</tt> - Sets the level of precision, defaults to 3
# * <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(302.0574, {:precision => 2}) => 302.06%
def number_to_percentage(number, options = {})
options = options.stringify_keys
precision, separator = options.delete("precision") { 3 }, options.delete("separator") { "." }
options = options.stringify_keys
precision = options["precision"] || 3
separator = options["separator"] || "."
begin
number = number_with_precision(number, precision)
parts = number.split('.')
@ -69,41 +93,63 @@ module ActionView
end
end
# Formats a +number+ with a +delimiter+.
# Example:
# number_with_delimiter(12345678) => 12,345,678
def number_with_delimiter(number, delimiter=",")
number.to_s.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{delimiter}")
# Formats a +number+ with grouped thousands using +delimiter+. You
# can customize the format using optional <em>delimiter</em> and <em>separator</em> parameters.
# * <tt>delimiter</tt> - Sets the thousands delimiter, defaults to ","
# * <tt>separator</tt> - Sets the separator between the units, defaults to "."
#
# 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
# Returns a formatted-for-humans file size.
# Formats a +number+ with the specified level of +precision+. The default
# level of precision is 3.
#
# number_with_precision(111.2345) => 111.235
# number_with_precision(111.2345, 2) => 111.24
def number_with_precision(number, precision=3)
"%01.#{precision}f" % number
rescue
number
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+.
#
# Examples:
# human_size(123) => 123 Bytes
# human_size(1234) => 1.2 KB
# human_size(12345) => 12.1 KB
# human_size(1234567) => 1.2 MB
# human_size(1234567890) => 1.1 GB
def number_to_human_size(size)
# 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
when size < 1.kilobyte: '%d Bytes' % size
when size < 1.megabyte: '%.1f KB' % (size / 1.0.kilobyte)
when size < 1.gigabyte: '%.1f MB' % (size / 1.0.megabyte)
when size < 1.terabyte: '%.1f GB' % (size / 1.0.gigabyte)
else '%.1f TB' % (size / 1.0.terabyte)
when size == 1 : "1 Byte"
when size < 1.kilobyte: "%d Bytes" % size
when size < 1.megabyte: "%.#{precision}f KB" % (size / 1.0.kilobyte)
when size < 1.gigabyte: "%.#{precision}f MB" % (size / 1.0.megabyte)
when size < 1.terabyte: "%.#{precision}f GB" % (size / 1.0.gigabyte)
else "%.#{precision}f TB" % (size / 1.0.terabyte)
end.sub('.0', '')
rescue
nil
end
alias_method :human_size, :number_to_human_size # deprecated alias
# 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
deprecate :human_size => :number_to_human_size
end
end
end

View file

@ -1,4 +1,3 @@
require File.dirname(__FILE__) + '/javascript_helper'
require 'set'
module ActionView
@ -39,7 +38,7 @@ module ActionView
# XMLHttpRequest. The result of that request can then be inserted into a
# DOM object whose id can be specified with <tt>options[:update]</tt>.
# Usually, the result would be a partial prepared by the controller with
# either render_partial or render_partial_collection.
# render :partial.
#
# Examples:
# 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
# <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
# which various JavaScript callbacks can be triggered (for progress
# 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
# 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[:html] ||= {}
options[:html][:onsubmit] = "#{remote_function(options)}; return false;"
options[:html][:action] = options[:html][:action] || url_for(options[:url])
options[:html][:method] = options[:html][:method] || "post"
options[:html][:onsubmit] =
(options[:html][:onsubmit] ? options[:html][:onsubmit] + "; " : "") +
"#{remote_function(options)}; return false;"
tag("form", options[:html], true)
form_tag(options[:html].delete(:action) || url_for(options[:url]), options[:html], &block)
end
# Works like form_remote_tag, but uses form_for semantics.
@ -194,81 +204,6 @@ module ActionView
tag("input", options[:html], false)
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
# that form_remote_tag can call in :complete to evaluate a multiple
# update return document using update_element_function calls.
@ -289,7 +224,7 @@ module ActionView
javascript_options = options_for_ajax(options)
update = ''
if options[:update] and options[:update].is_a?Hash
if options[:update] && options[:update].is_a?(Hash)
update = []
update << "success:'#{options[:update][:success]}'" if options[:update][:success]
update << "failure:'#{options[:update][:failure]}'" if options[:update][:failure]
@ -303,7 +238,7 @@ module ActionView
"new Ajax.Updater(#{update}, "
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 << ", #{javascript_options})"
@ -438,7 +373,7 @@ module ActionView
if ActionView::Base.debug_rjs
source = javascript.dup
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
@ -453,6 +388,12 @@ module ActionView
JavaScriptElementProxy.new(self, id)
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
# used for further method calls. Examples:
#
@ -526,7 +467,7 @@ module ActionView
#
# # Replace the DOM element having ID 'person-45' with the
# # '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
# 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.
def remove(*ids)
record "#{javascript_object_for(ids)}.each(Element.remove)"
loop_on_multiple_args 'Element.remove', ids
end
# Shows hidden DOM elements with the given +ids+.
def show(*ids)
call 'Element.show', *ids
loop_on_multiple_args 'Element.show', ids
end
# Hides the visible DOM elements with the given +ids+.
def hide(*ids)
call 'Element.hide', *ids
loop_on_multiple_args 'Element.hide', ids
end
# Toggles the visibility of the DOM elements with the given +ids+.
def toggle(*ids)
call 'Element.toggle', *ids
loop_on_multiple_args 'Element.toggle', ids
end
# Displays an alert dialog with the given +message+.
@ -573,16 +514,18 @@ module ActionView
call 'alert', message
end
# Redirects the browser to the given +location+, in the same form as
# +url_for+.
# Redirects the browser to the given +location+, in the same form as +url_for+.
def redirect_to(location)
assign 'window.location.href', @context.url_for(location)
end
# Calls the JavaScript +function+, optionally with the given
# +arguments+.
def call(function, *arguments)
record "#{function}(#{arguments_for_call(arguments)})"
# Calls the JavaScript +function+, optionally with the given +arguments+.
#
# If a block is given, the block will be passed to a new JavaScriptGenerator;
# 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
# Assigns the JavaScript +variable+ the given +value+.
@ -633,12 +576,18 @@ module ActionView
end
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
self
end
def record(line)
returning line = "#{line.to_s.chomp.gsub /\;$/, ''};" do
returning line = "#{line.to_s.chomp.gsub(/\;\z/, '')};" do
self << line
end
end
@ -653,10 +602,16 @@ module ActionView
object.respond_to?(:to_json) ? object.to_json : object.inspect
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 ', '
end
def block_to_function(block)
generator = self.class.new(@context, &block)
literal("function() { #{generator.to_s} }")
end
def method_missing(method, *arguments)
JavaScriptProxy.new(self, method.to_s.camelize)
end
@ -673,8 +628,11 @@ module ActionView
# Works like update_page but wraps the generated JavaScript in a <script>
# tag. Use this to include generated JavaScript in an ERb template.
# 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
protected
@ -738,16 +696,16 @@ module ActionView
end
private
def method_missing(method, *arguments)
def method_missing(method, *arguments, &block)
if method.to_s =~ /(.*)=$/
assign($1, arguments.first)
else
call("#{method.to_s.camelize(:lower)}", *arguments)
call("#{method.to_s.camelize(:lower)}", *arguments, &block)
end
end
def call(function, *arguments)
append_to_function_chain!("#{function}(#{@generator.send(:arguments_for_call, arguments)})")
def call(function, *arguments, &block)
append_to_function_chain!("#{function}(#{@generator.send(:arguments_for_call, arguments, block)})")
self
end
@ -756,7 +714,7 @@ module ActionView
end
def function_chain
@function_chain ||= @generator.instance_variable_get("@lines")
@function_chain ||= @generator.instance_variable_get(:@lines)
end
def append_to_function_chain!(call)
@ -771,6 +729,21 @@ module ActionView
super(generator, "$(#{id.to_json})")
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)
call 'update', @generator.send(:render, *options_for_render)
end
@ -779,8 +752,8 @@ module ActionView
call 'replace', @generator.send(:render, *options_for_render)
end
def reload
replace :partial => @id.to_s
def reload(options_for_replace = {})
replace(options_for_replace.merge({ :partial => @id.to_s }))
end
end
@ -811,8 +784,8 @@ module ActionView
end
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 = ENUMERABLE_METHODS_WITH_RETURN + [:each]
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] unless defined? ENUMERABLE_METHODS
attr_reader :generator
delegate :arguments_for_call, :to => :generator
@ -899,3 +872,5 @@ module ActionView
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
# 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
# http://script.aculo.us for more documentation.
def sortable_element(element_id, options = {})

View file

@ -2,39 +2,87 @@ require 'cgi'
require 'erb'
module ActionView
module Helpers
# This is poor man's Builder for the rare cases where you need to programmatically make tags but can't use Builder.
module Helpers #:nodoc:
# Use these methods to generate HTML tags programmatically when you can't use
# a Builder. By default, they output XHTML compliant tags.
module TagHelper
include ERB::Util
# Examples:
# * <tt>tag("br") => <br /></tt>
# * <tt>tag("input", { "type" => "text"}) => <input type="text" /></tt>
# Returns an empty HTML tag of type +name+ which by default is XHTML
# compliant. Setting +open+ to true will create an open tag compatible
# 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)
"<#{name}#{tag_options(options.stringify_keys) if options}" + (open ? ">" : " />")
"<#{name}#{tag_options(options) if options}" + (open ? ">" : " />")
end
# Examples:
# * <tt>content_tag("p", "Hello world!") => <p>Hello world!</p></tt>
# * <tt>content_tag("div", content_tag("p", "Hello world!"), "class" => "strong") => </tt>
# <tt><div class="strong"><p>Hello world!</p></div></tt>
def content_tag(name, content, options = nil)
"<#{name}#{tag_options(options.stringify_keys) if options}>#{content}</#{name}>"
# Returns an HTML block tag of type +name+ surrounding the +content+. 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.
#
# 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
# 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
# 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>]]></tt>.
# <tt><![CDATA[</tt> and end with (and may not contain) the string <tt>]]></tt>.
#
# cdata_section("<hello world>")
# # => <![CDATA[<hello world>]]>
def cdata_section(content)
"<![CDATA[#{content}]]>"
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
def content_tag_string(name, content, options)
tag_options = options ? tag_options(options) : ""
"<#{name}#{tag_options}>#{content}</#{name}>"
end
def tag_options(options)
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
def convert_booleans(options)
@ -45,6 +93,11 @@ module ActionView
def boolean_attribute(options, attribute)
options[attribute] ? options[attribute] = attribute : options.delete(attribute)
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

View file

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

View file

@ -1,22 +1,21 @@
require File.dirname(__FILE__) + '/javascript_helper'
module ActionView
module Helpers
# Provides a set of methods for making easy links and getting urls that depend on the controller and action. This means that
# you can use the same format for links in the views that you do in the controller. The different methods are even named
# synchronously, so link_to uses that same url as is generated by url_for, which again is the same url used for
# redirection in redirect_to.
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 you can use the
# same format for links in the views that you do in the controller.
module UrlHelper
include JavaScriptHelper
# Returns the URL for the set of +options+ provided. This takes the same options
# as url_for. For a list, see the documentation for ActionController::Base#url_for.
# Note that it'll set :only_path => true so you'll get /controller/action instead of the
# http://example.com/controller/action part (makes it harder to parse httpd log files)
#
# When called from a view, url_for returns an HTML escaped url. If you need an unescaped
# url, pass :escape => false to url_for.
#
# Returns the URL for the set of +options+ provided. This takes the
# same options as url_for in action controller. For a list, see the
# documentation for ActionController::Base#url_for. Note that it'll
# 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 in the +options+.
def url_for(options = {}, *parameters_for_method_reference)
if options.kind_of? Hash
options = { :only_path => true }.update(options.symbolize_keys)
@ -24,30 +23,46 @@ module ActionView
else
escape = true
end
url = @controller.send(:url_for, options, *parameters_for_method_reference)
escape ? html_escape(url) : url
end
# Creates a link tag of the given +name+ using an URL created by the set of +options+. See the valid options in
# the documentation for ActionController::Base#url_for. It's also possible to pass a string instead of an options hash to
# get a link tag that just points without consideration. If nil is passed as a name, the link itself will become the name.
# Creates a link tag of the given +name+ using a URL created by the set
# of +options+. See the valid options in the documentation for
# 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 link will be guarded with a JS popup asking that question. If the user accepts, the link is processed, otherwise not.
# The +html_options+ will accept a hash of html attributes for the link tag.
# 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
# Javascript form.
# * <tt>:confirm => 'question?'</tt>: This will add a JavaScript confirm
# 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
# is instantly submitted. Note that if the user has turned off Javascript, the request will fall back on the GET. So its
# your responsibility to determine what the action should be once it arrives at the controller. The POST form is turned on by
# 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).
# You can mix and match the +html_options+ with the exception of
# :popup and :method which will raise an ActionView::ActionViewError
# exception.
#
# Examples:
# link_to "Delete this page", { :action => "destroy", :id => @page.id }, :confirm => "Are you sure?"
# link_to "Visit Other Site", "http://www.rubyonrails.org/", :confirm => "Are you sure?"
# link_to "Help", { :action => "help" }, :popup => true
# link_to "Busy loop", { :action => "busy" }, :popup => ['new_window', 'height=300,width=600']
# link_to "Destroy account", { :action => "destroy" }, :confirm => "Are you sure?", :post => true
# link_to "View Image", { :action => "view" }, :popup => ['new_window_name', 'height=300,width=600']
# 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)
if html_options
html_options = html_options.stringify_keys
@ -56,76 +71,77 @@ module ActionView
else
tag_options = nil
end
url = options.is_a?(String) ? options : self.url_for(options, *parameters_for_method_reference)
"<a href=\"#{url}\"#{tag_options}>#{name || url}</a>"
end
# Generates a form containing a sole button that submits to the
# URL given by _options_. Use this method instead of +link_to+
# for actions that do not have the safe HTTP GET semantics
# implied by using a hypertext link.
# Generates a form containing a single button that submits to the URL created
# by the set of +options+. This is the safest method to ensure links that
# cause changes to your data are not triggered by search bots or accelerators.
# If the HTML button does not work with your layout, you can also consider
# using the link_to method with the <tt>:method</tt> modifier as described in
# the link_to documentation.
#
# The parameters are the same as for +link_to+. Any _html_options_
# that you pass will be applied to the inner +input+ element.
# In particular, pass
#
# :disabled => true/false
# The generated FORM element has a class name of <tt>button-to</tt>
# to allow styling of the form itself and its children. You can control
# 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+.
#
# as part of _html_options_ to control whether the button is
# disabled. The generated form element is given the class
# 'button-to', to which you can attach CSS styles for display
# purposes.
# button_to "New", :action => "new"
#
# Example 1:
# Generates the following HTML:
#
# # 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 method="post" action="/controller/new" class="button-to">
# <div><input value="New" type="submit" /></div>
# </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 },
# :confirm => "Are you sure?"
# button_to "Delete Image", { :action => "delete", :id => @image.id },
# :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">
# <div><input onclick="return confirm('Are you sure?');"
# value="Destroy" type="submit" />
# <form method="post" action="/images/delete/1" class="button-to">
# <div>
# <input type="hidden" name="_method" value="delete" />
# <input onclick="return confirm('Are you sure?');"
# value="Delete" type="submit" />
# </div>
# </form>
#
# *NOTE*: This method generates HTML code that represents a form.
# 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
def button_to(name, options = {}, html_options = {})
html_options = html_options.stringify_keys
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")
html_options["onclick"] = "return #{confirm_javascript_function(confirm)};"
end
url = options.is_a?(String) ? options : url_for(options)
name ||= url
html_options.merge!("type" => "submit", "value" => name)
"<form method=\"post\" action=\"#{h url}\" class=\"button-to\"><div>" +
tag("input", html_options) + "</div></form>"
url = options.is_a?(String) ? options : self.url_for(options)
name ||= url
html_options.merge!("type" => "submit", "value" => name)
"<form method=\"#{form_method}\" action=\"#{escape_once url}\" class=\"button-to\"><div>" +
method_tag + tag("input", html_options) + "</div></form>"
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")
def link_image_to(src, options = {}, html_options = {}, *parameters_for_method_reference)
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)
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
# request uri is the same as the link's, in which case only the name is returned (or the
# given block is yielded, if one exists). This is useful for creating link bars where you don't want to link
# to the page currently being viewed.
# Creates a link tag of the given +name+ using a URL created by the set of
# +options+ unless the current request uri is the same as the links, in
# which case only the name is returned (or the given block is yielded, if
# one exists). 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)
link_to_unless current_page?(options), name, options, html_options, *parameters_for_method_reference, &block
end
# Create a link tag of the given +name+ using an URL created by the set of +options+, unless +condition+
# is true, in which case only the name is returned (or the given block is yielded, if one exists).
# Creates a link tag of the given +name+ using a URL created by the set of
# +options+ unless +condition+ is true, in which case only the name is
# returned. To specialize the default behavior, 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)
if condition
if block_given?
@ -181,30 +221,56 @@ module ActionView
end
end
# Create a link tag of the given +name+ using an URL created by the set of +options+, if +condition+
# is true, in which case only the name is returned (or the given block is yielded, if one exists).
# Creates a link tag of the given +name+ using a URL created by the set of
# +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)
link_to_unless !condition, name, options, html_options, *parameters_for_method_reference, &block
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
# link unless +name+ is specified. Additional HTML options, such as class or id, can be passed in the <tt>html_options</tt> hash.
# Creates a mailto link tag to the specified +email_address+, which is
# also used as the name of the link unless +name+ is specified. Additional
# html attributes for the link can be passed in +html_options+.
#
# mail_to has several methods for 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:
# mail_to "me@domain.com" # => <a href="mailto:me@domain.com">me@domain.com</a>
# 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" # =>
# <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
# corresponding +cc+, +bcc+, +subject+, and +body+ <tt>html_options</tt> keys. Each of these options are URI escaped and then appended to
# 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>
# Examples:
# 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"&bcc="bccaddress@domain.com"&body="This%20is%20the%20body%20of%20the%20message."&subject="This%20is%20an%20example%20email">My email</a>
# mail_to "me@domain.com", nil, :replace_at => "_at_", :replace_dot => "_dot_", :class => "email" # =>
# <a href="mailto:me@domain.com" class="email">me_at_domain_dot_com</a>
#
# mail_to "me@domain.com", "My email", :cc => "ccaddress@domain.com",
# :subject => "This is an example email" # =>
# <a href="mailto:me@domain.com?cc=ccaddress@domain.com&subject=This%20is%20an%20example%20email">My email</a>
def mail_to(email_address, name = nil, html_options = {})
html_options = html_options.stringify_keys
encode = html_options.delete("encode")
@ -218,17 +284,19 @@ module ActionView
extras << "subject=#{CGI.escape(subject).gsub("+", "%20")}&" unless subject.nil?
extras = "?" << extras.gsub!(/&?$/,"") unless extras.empty?
email_address = email_address.to_s
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_dot")) if html_options.has_key?("replace_dot")
if encode == 'javascript'
tmp = "document.write('#{content_tag("a", name || email_address, html_options.merge({ "href" => "mailto:"+email_address.to_s+extras }))}');"
if encode == "javascript"
tmp = "document.write('#{content_tag("a", name || email_address, html_options.merge({ "href" => "mailto:"+email_address+extras }))}');"
for i in 0...tmp.length
string << sprintf("%%%x",tmp[i])
end
"<script type=\"text/javascript\">eval(unescape('#{string}'))</script>"
elsif encode == 'hex'
elsif encode == "hex"
for i in 0...email_address.length
if email_address[i,1] =~ /\w/
string << sprintf("%%%x",email_address[i])
@ -242,26 +310,42 @@ module ActionView
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)
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
private
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
when popup && post
when popup && method
raise ActionView::ActionViewError, "You can't use :popup and :post in the same link"
when confirm && popup
"if (#{confirm_javascript_function(confirm)}) { #{popup_javascript_function(popup)} };return false;"
when confirm && post
"if (#{confirm_javascript_function(confirm)}) { #{post_javascript_function} };return false;"
when confirm && method
"if (#{confirm_javascript_function(confirm)}) { #{method_javascript_function(method)} };return false;"
when confirm
"return #{confirm_javascript_function(confirm)};"
when post
"#{post_javascript_function}return false;"
when method
"#{method_javascript_function(method)}return false;"
when popup
popup_javascript_function(popup) + 'return false;'
else
@ -277,8 +361,17 @@ module ActionView
popup.is_a?(Array) ? "window.open(this.href,'#{popup.first}','#{popup.last}');" : "window.open(this.href);"
end
def post_javascript_function
"var f = document.createElement('form'); this.parentNode.appendChild(f); f.method = 'POST'; f.action = this.href; f.submit();"
def method_javascript_function(method)
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
# Processes the _html_options_ hash, converting the boolean

View file

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